├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── assets ├── background.png ├── border.png ├── controls.png ├── spaceship_small_blue.png └── spaceship_small_red.png ├── screenshot0.png ├── screenshot1.png ├── screenshot2.png ├── shaders ├── Makefile ├── colorShader.gsh ├── colorShader.psh ├── colorShader.vsh ├── textureShader.gsh ├── textureShader.psh └── textureShader.vsh └── source ├── DrcPairing.cpp ├── DrcPairing.hpp ├── Game.cpp ├── Game.hpp ├── Gfx.cpp ├── Gfx.hpp ├── Menu.cpp ├── Menu.hpp ├── SceneMgr.cpp ├── SceneMgr.hpp ├── Sprite.cpp ├── Sprite.hpp ├── Text.cpp ├── Text.hpp ├── Utils.cpp ├── Utils.hpp └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | build/ 3 | *.rpx 4 | *.elf 5 | latte-assembler 6 | *.xcf 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 GaryOderNichts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | .SUFFIXES: 3 | #------------------------------------------------------------------------------- 4 | 5 | ifeq ($(strip $(DEVKITPRO)),) 6 | $(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") 7 | endif 8 | 9 | TOPDIR ?= $(CURDIR) 10 | 11 | include $(DEVKITPRO)/wut/share/wut_rules 12 | 13 | #------------------------------------------------------------------------------- 14 | # TARGET is the name of the output 15 | # BUILD is the directory where object files & intermediate files will be placed 16 | # SOURCES is a list of directories containing source code 17 | # DATA is a list of directories containing data files 18 | # INCLUDES is a list of directories containing header files 19 | # SHADERS is a list of directories containing gsh shader files 20 | #------------------------------------------------------------------------------- 21 | TARGET := $(notdir $(CURDIR)) 22 | BUILD := build 23 | SOURCES := source 24 | DATA := assets 25 | INCLUDES := include 26 | SHADERS := shaders 27 | 28 | #------------------------------------------------------------------------------- 29 | # options for code generation 30 | #------------------------------------------------------------------------------- 31 | CFLAGS := -Wall -O2 -ffunction-sections \ 32 | $(MACHDEP) 33 | 34 | CFLAGS += $(INCLUDE) -D__WIIU__ -D__WUT__ 35 | 36 | # turn off warning for glm with c++20 37 | CFLAGS += -Wno-volatile 38 | 39 | CXXFLAGS := $(CFLAGS) -std=gnu++20 40 | 41 | ASFLAGS := $(ARCH) 42 | LDFLAGS = $(ARCH) $(RPXSPECS) -Wl,-Map,$(notdir $*.map) 43 | 44 | LIBS := -lfreetype -lpng -lz -lbz2 -lwut 45 | 46 | #------------------------------------------------------------------------------- 47 | # list of directories containing libraries, this must be the top level 48 | # containing include and lib 49 | #------------------------------------------------------------------------------- 50 | LIBDIRS := $(PORTLIBS) $(WUT_ROOT) 51 | 52 | 53 | #------------------------------------------------------------------------------- 54 | # no real need to edit anything past this point unless you need to add additional 55 | # rules for different file extensions 56 | #------------------------------------------------------------------------------- 57 | ifneq ($(BUILD),$(notdir $(CURDIR))) 58 | #------------------------------------------------------------------------------- 59 | 60 | export OUTPUT := $(CURDIR)/$(TARGET) 61 | export TOPDIR := $(CURDIR) 62 | 63 | export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ 64 | $(foreach dir,$(DATA),$(CURDIR)/$(dir)) \ 65 | $(foreach dir,$(SHADERS),$(CURDIR)/$(dir)) 66 | 67 | export DEPSDIR := $(CURDIR)/$(BUILD) 68 | 69 | CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) 70 | CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) 71 | SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) 72 | BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) \ 73 | $(foreach dir,$(SHADERS),$(notdir $(wildcard $(dir)/*.gsh))) 74 | 75 | #------------------------------------------------------------------------------- 76 | # use CXX for linking C++ projects, CC for standard C 77 | #------------------------------------------------------------------------------- 78 | ifeq ($(strip $(CPPFILES)),) 79 | #------------------------------------------------------------------------------- 80 | export LD := $(CC) 81 | #------------------------------------------------------------------------------- 82 | else 83 | #------------------------------------------------------------------------------- 84 | export LD := $(CXX) 85 | #------------------------------------------------------------------------------- 86 | endif 87 | #------------------------------------------------------------------------------- 88 | 89 | export OFILES_BIN := $(addsuffix .o,$(BINFILES)) 90 | export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) 91 | export OFILES := $(OFILES_BIN) $(OFILES_SRC) 92 | export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) 93 | 94 | export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ 95 | $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ 96 | -I$(CURDIR)/$(BUILD) -I$(DEVKITPRO)/portlibs/ppc/include/freetype2 97 | 98 | export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) 99 | 100 | .PHONY: $(BUILD) clean all 101 | 102 | #------------------------------------------------------------------------------- 103 | all: $(BUILD) 104 | 105 | $(BUILD): 106 | @[ -d $@ ] || mkdir -p $@ 107 | @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile 108 | 109 | #------------------------------------------------------------------------------- 110 | clean: 111 | @echo clean ... 112 | @rm -fr $(BUILD) $(TARGET).rpx $(TARGET).elf 113 | 114 | #------------------------------------------------------------------------------- 115 | else 116 | .PHONY: all 117 | 118 | DEPENDS := $(OFILES:.o=.d) 119 | 120 | #------------------------------------------------------------------------------- 121 | # main targets 122 | #------------------------------------------------------------------------------- 123 | all : $(OUTPUT).rpx 124 | 125 | $(OUTPUT).rpx : $(OUTPUT).elf 126 | $(OUTPUT).elf : $(OFILES) 127 | 128 | $(OFILES_SRC) : $(HFILES_BIN) 129 | 130 | #------------------------------------------------------------------------------- 131 | # you need a rule like this for each extension you use as binary data 132 | #------------------------------------------------------------------------------- 133 | %.bin.o %_bin.h : %.bin 134 | #------------------------------------------------------------------------------- 135 | @echo $(notdir $<) 136 | @$(bin2o) 137 | #------------------------------------------------------------------------------- 138 | %.gsh.o %_gsh.h : %.gsh 139 | #------------------------------------------------------------------------------- 140 | @echo $(notdir $<) 141 | @$(bin2o) 142 | #------------------------------------------------------------------------------- 143 | %.png.o %_png.h : %.png 144 | #------------------------------------------------------------------------------- 145 | @echo $(notdir $<) 146 | @$(bin2o) 147 | 148 | -include $(DEPENDS) 149 | 150 | #------------------------------------------------------------------------------- 151 | endif 152 | #------------------------------------------------------------------------------- 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiDRCSpaceDemo 2 | A space shooter demo using the Wii U's MultiDRC mode. 3 | 4 | The Wii U has an unused feature allowing two GamePads to be connected to a single console. 5 | Nintendo [announced this back at the E3 2012](https://www.polygon.com/gaming/2012/6/5/3065588/wii-u-supports-two-gamepads-nintendo-confirms) before the Wii U was released. 6 | While this feature was never released it is still in the system files and can be used by homebrew applications. 7 | This project acts as a small demo and example for using that MultiDRC mode. 8 | 9 | ## Screenshots 10 | ![Screenshot 0](screenshot0.png) 11 | ![Screenshot 1](screenshot1.png) 12 | ![Screenshot 2](screenshot2.png) 13 | 14 | ## Controls 15 | ![GamePad controls](assets/controls.png) 16 | 17 | ## Assets 18 | ### Seamless Space Backgrounds - Screaming Brain Studios 19 | [ ](https://opengameart.org/content/seamless-space-backgrounds) 20 | 21 | [![CC0-1.0](https://licensebuttons.net/l/zero/1.0/80x15.png)](http://creativecommons.org/publicdomain/zero/1.0/) 22 | 23 | ### Pixel Spaceship - dsonyy 24 | [ ](https://opengameart.org/content/pixel-spaceship) 25 | 26 | [![CC0-1.0](https://licensebuttons.net/l/zero/1.0/80x15.png)](http://creativecommons.org/publicdomain/zero/1.0/) 27 | 28 | ### Wii U controller illustration - Tokyoship 29 | [](https://commons.wikimedia.org/wiki/File:Wii_U_controller_illustration.svg) 30 | 31 | [![CC-BY-3.0](https://licensebuttons.net/l/by/3.0/80x15.png)](https://creativecommons.org/licenses/by/3.0/) 32 | 33 | ## Building 34 | To build this application you need devkitPPC and the dependencies below. 35 | Get started with installing the toolchain [here](https://devkitpro.org/wiki/Getting_Started). 36 | Then run `make`. 37 | 38 | ### Dependencies 39 | - [wut](https://github.com/devkitPro/wut) 40 | Note that the wut releases are currently missing PR [#263](https://github.com/devkitPro/wut/pull/263), [#264](https://github.com/devkitPro/wut/pull/264), [#265](https://github.com/devkitPro/wut/pull/265), [#266](https://github.com/devkitPro/wut/pull/266) which are required to build this project. 41 | - ppc-glm 42 | - ppc-libpng 43 | - ppc-freetype 44 | 45 | To install the dependencies run `(dkp-)pacman -S wut ppc-glm ppc-libpng ppc-freetype` 46 | 47 | 48 | -------------------------------------------------------------------------------- /assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryOderNichts/MultiDRCSpaceDemo/30a6337a47dabafd6d601ce0555b326866f63247/assets/background.png -------------------------------------------------------------------------------- /assets/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryOderNichts/MultiDRCSpaceDemo/30a6337a47dabafd6d601ce0555b326866f63247/assets/border.png -------------------------------------------------------------------------------- /assets/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryOderNichts/MultiDRCSpaceDemo/30a6337a47dabafd6d601ce0555b326866f63247/assets/controls.png -------------------------------------------------------------------------------- /assets/spaceship_small_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryOderNichts/MultiDRCSpaceDemo/30a6337a47dabafd6d601ce0555b326866f63247/assets/spaceship_small_blue.png -------------------------------------------------------------------------------- /assets/spaceship_small_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryOderNichts/MultiDRCSpaceDemo/30a6337a47dabafd6d601ce0555b326866f63247/assets/spaceship_small_red.png -------------------------------------------------------------------------------- /screenshot0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryOderNichts/MultiDRCSpaceDemo/30a6337a47dabafd6d601ce0555b326866f63247/screenshot0.png -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryOderNichts/MultiDRCSpaceDemo/30a6337a47dabafd6d601ce0555b326866f63247/screenshot1.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryOderNichts/MultiDRCSpaceDemo/30a6337a47dabafd6d601ce0555b326866f63247/screenshot2.png -------------------------------------------------------------------------------- /shaders/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean colorShader textureShader 2 | 3 | all: colorShader textureShader 4 | 5 | colorShader: 6 | ./latte-assembler assemble --vsh=colorShader.vsh --psh=colorShader.psh colorShader.gsh 7 | 8 | textureShader: 9 | ./latte-assembler assemble --vsh=textureShader.vsh --psh=textureShader.psh textureShader.gsh 10 | 11 | clean: 12 | rm -f *.gsh 13 | -------------------------------------------------------------------------------- /shaders/colorShader.gsh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryOderNichts/MultiDRCSpaceDemo/30a6337a47dabafd6d601ce0555b326866f63247/shaders/colorShader.gsh -------------------------------------------------------------------------------- /shaders/colorShader.psh: -------------------------------------------------------------------------------- 1 | ; $MODE = "UniformRegister" 2 | 3 | ; $NUM_SPI_PS_INPUT_CNTL = 0 4 | 5 | ; C0 6 | ; $UNIFORM_VARS[0].name = "uColor" 7 | ; $UNIFORM_VARS[0].type = "vec4" 8 | ; $UNIFORM_VARS[0].count = 1 9 | ; $UNIFORM_VARS[0].block = -1 10 | ; $UNIFORM_VARS[0].offset = 0 11 | 12 | 00 ALU: ADDR(32) CNT(4) 13 | 0 x: MOV R0.x, C0.x 14 | y: MOV R0.y, C0.y 15 | z: MOV R0.z, C0.z 16 | w: MOV R0.w, C0.w 17 | 01 EXP_DONE: PIX0, R0 18 | END_OF_PROGRAM 19 | -------------------------------------------------------------------------------- /shaders/colorShader.vsh: -------------------------------------------------------------------------------- 1 | ; $MODE = "UniformRegister" 2 | 3 | ; $SPI_VS_OUT_CONFIG.VS_EXPORT_COUNT = 0 4 | 5 | ; C0 6 | ; $UNIFORM_VARS[0].name = "uProjection" 7 | ; $UNIFORM_VARS[0].type = "mat4" 8 | ; $UNIFORM_VARS[0].count = 1 9 | ; $UNIFORM_VARS[0].block = -1 10 | ; $UNIFORM_VARS[0].offset = 0 11 | 12 | ; R1 13 | ; $ATTRIB_VARS[0].name = "aPosition" 14 | ; $ATTRIB_VARS[0].type = "vec2" 15 | ; $ATTRIB_VARS[0].location = 0 16 | 17 | 00 CALL_FS NO_BARRIER 18 | 01 ALU: ADDR(32) CNT(14) 19 | 0 x: MUL ____, 1.0f, C3.x 20 | y: MUL ____, 1.0f, C3.y 21 | z: MUL ____, 1.0f, C3.z 22 | w: MUL ____, 1.0f, C3.w 23 | 1 x: MULADD R127.x, R1.y, C1.x, PV0.x 24 | y: MULADD R127.y, R1.y, C1.y, PV0.y 25 | z: MULADD R127.z, R1.y, C1.z, PV0.z 26 | w: MULADD R127.w, R1.y, C1.w, PV0.w 27 | 2 x: MULADD R1.x, R1.x, C0.x, PV0.x 28 | y: MULADD R1.y, R1.x, C0.y, PV0.y 29 | z: MULADD R1.z, R1.x, C0.z, PV0.z 30 | w: MULADD R1.w, R1.x, C0.w, PV0.w 31 | 02 EXP_DONE: POS0, R1 32 | END_OF_PROGRAM 33 | -------------------------------------------------------------------------------- /shaders/textureShader.gsh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GaryOderNichts/MultiDRCSpaceDemo/30a6337a47dabafd6d601ce0555b326866f63247/shaders/textureShader.gsh -------------------------------------------------------------------------------- /shaders/textureShader.psh: -------------------------------------------------------------------------------- 1 | ; $MODE = "UniformRegister" 2 | 3 | ; $NUM_SPI_PS_INPUT_CNTL = 1 4 | ; vTexCoord R0 5 | ; $SPI_PS_INPUT_CNTL[0].SEMANTIC = 0 6 | ; $SPI_PS_INPUT_CNTL[0].DEFAULT_VAL = 1 7 | 8 | ; C0 9 | ; $UNIFORM_VARS[0].name = "uColor" 10 | ; $UNIFORM_VARS[0].type = "vec4" 11 | ; $UNIFORM_VARS[0].count = 1 12 | ; $UNIFORM_VARS[0].block = -1 13 | ; $UNIFORM_VARS[0].offset = 0 14 | ; C1 15 | ; $UNIFORM_VARS[1].name = "uTexCoordParams" 16 | ; $UNIFORM_VARS[1].type = "vec4" 17 | ; $UNIFORM_VARS[1].count = 1 18 | ; $UNIFORM_VARS[1].block = -1 19 | ; $UNIFORM_VARS[1].offset = 4 20 | 21 | ; $SAMPLER_VARS[0].name = "uTexture" 22 | ; $SAMPLER_VARS[0].type = "SAMPLER2D" 23 | ; $SAMPLER_VARS[0].location = 0 24 | 25 | 00 ALU: ADDR(32) CNT(4) 26 | 0 x: ADD R0.x, R0.x, C1.x 27 | y: ADD R0.y, R0.y, C1.y 28 | 1 x: MUL R0.x, R0.x, C1.z 29 | y: MUL R0.y, R0.y, C1.w 30 | 01 TEX: ADDR(48) CNT(1) VALID_PIX 31 | 2 SAMPLE R0, R0.xy0x, t0, s0 32 | 02 ALU: ADDR(36) CNT(4) 33 | 3 x: MUL R0.x, R0.x, C0.x 34 | y: MUL R0.y, R0.y, C0.y 35 | z: MUL R0.z, R0.z, C0.z 36 | w: MUL R0.w, R0.w, C0.w 37 | 03 EXP_DONE: PIX0, R0 38 | END_OF_PROGRAM 39 | -------------------------------------------------------------------------------- /shaders/textureShader.vsh: -------------------------------------------------------------------------------- 1 | ; $MODE = "UniformRegister" 2 | 3 | ; $SPI_VS_OUT_CONFIG.VS_EXPORT_COUNT = 0 4 | ; $NUM_SPI_VS_OUT_ID = 1 5 | ; vTexCoord 6 | ; $SPI_VS_OUT_ID[0].SEMANTIC_0 = 0 7 | 8 | ; C0 9 | ; $UNIFORM_VARS[0].name = "uProjection" 10 | ; $UNIFORM_VARS[0].type = "mat4" 11 | ; $UNIFORM_VARS[0].count = 1 12 | ; $UNIFORM_VARS[0].block = -1 13 | ; $UNIFORM_VARS[0].offset = 0 14 | 15 | ; R1 16 | ; $ATTRIB_VARS[0].name = "aPosition" 17 | ; $ATTRIB_VARS[0].type = "vec2" 18 | ; $ATTRIB_VARS[0].location = 0 19 | ; R2 20 | ; $ATTRIB_VARS[1].name = "aTexCoord" 21 | ; $ATTRIB_VARS[1].type = "vec2" 22 | ; $ATTRIB_VARS[1].location = 1 23 | 24 | 00 CALL_FS NO_BARRIER 25 | 01 ALU: ADDR(32) CNT(14) 26 | 0 x: MUL ____, 1.0f, C3.x 27 | y: MUL ____, 1.0f, C3.y 28 | z: MUL ____, 1.0f, C3.z 29 | w: MUL ____, 1.0f, C3.w 30 | 1 x: MULADD R127.x, R1.y, C1.x, PV0.x 31 | y: MULADD R127.y, R1.y, C1.y, PV0.y 32 | z: MULADD R127.z, R1.y, C1.z, PV0.z 33 | w: MULADD R127.w, R1.y, C1.w, PV0.w 34 | 2 x: MULADD R1.x, R1.x, C0.x, PV0.x 35 | y: MULADD R1.y, R1.x, C0.y, PV0.y 36 | z: MULADD R1.z, R1.x, C0.z, PV0.z 37 | w: MULADD R1.w, R1.x, C0.w, PV0.w 38 | 02 EXP_DONE: POS0, R1 39 | 03 EXP_DONE: PARAM0, R2.xy00 NO_BARRIER 40 | END_OF_PROGRAM 41 | -------------------------------------------------------------------------------- /source/DrcPairing.cpp: -------------------------------------------------------------------------------- 1 | #include "DrcPairing.hpp" 2 | #include "SceneMgr.hpp" 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "background_png.h" 11 | 12 | // 2 minutes timeout 13 | #define TIMEOUT_SECONDS 120 14 | 15 | DrcPairing::DrcPairing(SceneMgr* sceneMgr) : 16 | sceneMgr(sceneMgr), 17 | frameCount(0), 18 | titleText("Pairing second GamePad", 96), 19 | pinText("Pin: ---- ", 48), 20 | timeoutText("000 seconds remaining", 48), 21 | doneText("Paired second GamePad", 48), 22 | errorText("Failed to pair GamePad", 48), 23 | hintText("Press the console's SYNC button to cancel", 32), 24 | hintText2("Press any button to continue", 32), 25 | state(STATE_START) 26 | { 27 | // Load the background 28 | background = Sprite::FromPNG(background_png, background_png_size); 29 | // Fill the entire screen 30 | background->SetSize(Gfx::screenSpace); 31 | // Adjust from 1:1 to 16:9 32 | background->SetUVScale(glm::vec2(1.77f, 1.0f)); 33 | 34 | // Setup texts 35 | titleText.SetCentered(true); 36 | titleText.SetPosition(glm::vec2(Gfx::screenSpace.x / 2, titleText.GetSize().y)); 37 | pinText.SetCentered(true); 38 | pinText.SetPosition(glm::vec2(Gfx::screenSpace.x / 2, (Gfx::screenSpace.y / 2) - pinText.GetSize().y)); 39 | timeoutText.SetCentered(true); 40 | timeoutText.SetPosition(glm::vec2(Gfx::screenSpace.x / 2, (Gfx::screenSpace.y / 2) + timeoutText.GetSize().y)); 41 | doneText.SetCentered(true); 42 | doneText.SetPosition(Gfx::screenSpace / 2.0f); 43 | errorText.SetCentered(true); 44 | errorText.SetPosition(Gfx::screenSpace / 2.0f); 45 | hintText.SetCentered(true); 46 | hintText.SetPosition(glm::vec2(Gfx::screenSpace.x / 2, Gfx::screenSpace.y - hintText.GetSize().y)); 47 | hintText2.SetCentered(true); 48 | hintText2.SetPosition(glm::vec2(Gfx::screenSpace.x / 2, Gfx::screenSpace.y - hintText2.GetSize().y)); 49 | 50 | // Initialize IM 51 | imHandle = IM_Open(); 52 | imRequest = (IMRequest*) memalign(0x40, sizeof(IMRequest)); 53 | // Allocate a separate request for IM_CancelGetEventNotify to avoid conflict with the pending IM_GetEventNotify request 54 | imCancelRequest = (IMRequest*) memalign(0x40, sizeof(IMRequest)); 55 | 56 | // Init CCRSys 57 | CCRSysInit(); 58 | } 59 | 60 | DrcPairing::~DrcPairing() 61 | { 62 | // Deinit CCRSys 63 | CCRSysExit(); 64 | 65 | // Close IM 66 | IM_CancelGetEventNotify(imHandle, imCancelRequest, nullptr, nullptr); 67 | IM_Close(imHandle); 68 | free(imCancelRequest); 69 | free(imRequest); 70 | 71 | delete background; 72 | } 73 | 74 | void DrcPairing::Update() 75 | { 76 | frameCount++; 77 | 78 | // Animate the background 79 | background->SetUVOffset(glm::vec2(frameCount * 0.005f, frameCount * -0.005f)); 80 | 81 | switch (state) { 82 | case STATE_START: { 83 | // Setup sync callback to allow cancelling with the SYNC button 84 | cancelPairing = false; 85 | // Set mask to only sync events 86 | imEventMask = IM_EVENT_SYNC; 87 | // Notify about sync button events 88 | IM_GetEventNotify(imHandle, imRequest, &imEventMask, DrcPairing::SyncButtonCallback, &imEventMask); 89 | 90 | // Get the pincode 91 | uint32_t pincode; 92 | if (CCRSysGetPincode(&pincode) != 0) { 93 | state = STATE_ERROR; 94 | return; 95 | } 96 | 97 | // Convert the pin to symbols and set the text 98 | static char pinSymbols[][4] = { 99 | "\u2660", 100 | "\u2665", 101 | "\u2666", 102 | "\u2663" 103 | }; 104 | std::string pin = std::string(pinSymbols[(pincode / 1000) % 10]) + 105 | pinSymbols[(pincode / 100) % 10] + 106 | pinSymbols[(pincode / 10) % 10] + 107 | pinSymbols[pincode % 10]; 108 | pinText.SetText("Pin: " + pin); 109 | 110 | // Start pairing to slot 1 (second gamepad) 111 | if (CCRSysStartPairing(1, TIMEOUT_SECONDS) != 0) { 112 | state = STATE_ERROR; 113 | return; 114 | } 115 | 116 | // Pairing has started, save start time 117 | startTime = OSGetTime(); 118 | state = STATE_PAIRING; 119 | break; 120 | } 121 | case STATE_PAIRING: { 122 | // Get the current pairing state 123 | CCRSysPairingState pairingState = CCRSysGetPairingState(); 124 | if (pairingState == CCR_SYS_PAIRING_TIMED_OUT || cancelPairing) { 125 | // Pairing has timed out or was cancelled 126 | CCRSysStopPairing(); 127 | state = STATE_ERROR; 128 | return; 129 | } else if (pairingState == CCR_SYS_PAIRING_FINISHED) { 130 | // Second gamepad was paired 131 | state = STATE_DONE; 132 | return; 133 | } 134 | 135 | // Update timeout text 136 | timeoutText.SetText(std::to_string(TIMEOUT_SECONDS - OSTicksToSeconds(OSGetTime() - startTime)) + " seconds remaining"); 137 | break; 138 | } 139 | case STATE_ERROR: 140 | case STATE_DONE: { 141 | VPADStatus status{}; 142 | VPADRead(VPAD_CHAN_0, &status, 1, nullptr); 143 | 144 | if (status.trigger) { 145 | // Reset state 146 | state = STATE_START; 147 | 148 | // Reset sync callback 149 | cancelPairing = false; 150 | IM_CancelGetEventNotify(imHandle, imCancelRequest, nullptr, nullptr); 151 | 152 | // Return to menu 153 | sceneMgr->SetScene(SceneMgr::SCENE_MENU); 154 | return; 155 | } 156 | break; 157 | } 158 | default: 159 | break; 160 | } 161 | } 162 | 163 | void DrcPairing::DrawScene(Gfx* gfx, Gfx::Target target) 164 | { 165 | // Default view (identity matrix) 166 | glm::mat4 view = glm::mat4(1.0f); 167 | gfx->SetView(view); 168 | 169 | background->Draw(gfx); 170 | 171 | titleText.Draw(gfx); 172 | 173 | switch (state) { 174 | case STATE_START: 175 | break; 176 | case STATE_PAIRING: 177 | pinText.Draw(gfx); 178 | timeoutText.Draw(gfx); 179 | hintText.Draw(gfx); 180 | break; 181 | case STATE_ERROR: 182 | errorText.Draw(gfx); 183 | if (target == Gfx::TARGET_DRC0) { 184 | hintText2.Draw(gfx); 185 | } 186 | break; 187 | case STATE_DONE: 188 | doneText.Draw(gfx); 189 | if (target == Gfx::TARGET_DRC0) { 190 | hintText2.Draw(gfx); 191 | } 192 | break; 193 | } 194 | } 195 | 196 | void DrcPairing::SyncButtonCallback(IOSError error, void* arg) 197 | { 198 | uint32_t event = *(uint32_t*) arg; 199 | 200 | // Cancel pairing if the sync button was pressed 201 | if (error == IOS_ERROR_OK && (event & IM_EVENT_SYNC)) { 202 | cancelPairing = true; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /source/DrcPairing.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Gfx.hpp" 4 | #include "Text.hpp" 5 | 6 | #include 7 | #include 8 | 9 | class SceneMgr; 10 | 11 | class DrcPairing { 12 | public: 13 | DrcPairing(SceneMgr* sceneMgr); 14 | virtual ~DrcPairing(); 15 | 16 | void Update(); 17 | 18 | void DrawScene(Gfx* gfx, Gfx::Target target); 19 | 20 | private: 21 | static void SyncButtonCallback(IOSError error, void* arg); 22 | static inline bool cancelPairing; 23 | 24 | IOSHandle imHandle; 25 | IMRequest* imRequest; 26 | IMRequest* imCancelRequest; 27 | IMEventMask imEventMask; 28 | 29 | SceneMgr* sceneMgr; 30 | uint32_t frameCount; 31 | 32 | Sprite* background; 33 | Text titleText; 34 | Text pinText; 35 | Text timeoutText; 36 | Text doneText; 37 | Text errorText; 38 | Text hintText; 39 | Text hintText2; 40 | 41 | enum State { 42 | STATE_START, 43 | STATE_PAIRING, 44 | STATE_ERROR, 45 | STATE_DONE, 46 | }; 47 | State state; 48 | OSTime startTime; 49 | }; 50 | -------------------------------------------------------------------------------- /source/Game.cpp: -------------------------------------------------------------------------------- 1 | #include "Game.hpp" 2 | #include "SceneMgr.hpp" 3 | 4 | #include 5 | 6 | #include "background_png.h" 7 | #include "border_png.h" 8 | #include "spaceship_small_red_png.h" 9 | #include "spaceship_small_blue_png.h" 10 | 11 | #define FIELD_WIDTH (1024.0f * 3) 12 | #define FIELD_HEIGHT (1024.0f * 3) 13 | #define BORDER_SIZE 1024.0f 14 | 15 | #define NUM_LIVES 5 16 | #define HEART_FULL "\ue017" // "\u2665" 17 | #define HEART_EMPTY "\ue01f" // "\u2661" 18 | 19 | Game::Game(SceneMgr* sceneMgr) : 20 | sceneMgr(sceneMgr), 21 | frameCount(0), 22 | paused(false), 23 | pauseBackground(glm::vec2(0.0f), Gfx::screenSpace, 0.0f, glm::vec4(0.5f)), 24 | pauseText("Game is paused", 64), 25 | pauseHint(" \ue000/\ue045 Continue \ue001 Return to menu", 32), 26 | gameOverText("Game Over", 96), 27 | gameOverSubText("Player X won!", 64), 28 | tvPlayerLives{ 29 | Text("-", 64, glm::vec2(0.0f), glm::vec2(1.0f), 0.0f, glm::vec4(0.89f, 0.0f, 0.0f, 1.0f)), 30 | Text("-", 64, glm::vec2(0.0f), glm::vec2(1.0f), 0.0f, glm::vec4(0.2f, 0.62f, 0.8f, 1.0f)), 31 | } 32 | { 33 | // Initialize pause items 34 | pauseBackground.SetVisible(false); 35 | pauseText.SetPosition(Gfx::screenSpace / 2.0f); 36 | pauseText.SetCentered(true); 37 | pauseText.SetVisible(false); 38 | pauseHint.SetPosition(glm::vec2(Gfx::screenSpace.x / 2, Gfx::screenSpace.y - pauseHint.GetSize().y)); 39 | pauseHint.SetCentered(true); 40 | pauseHint.SetVisible(false); 41 | 42 | // Initialize game over screens 43 | gameOverText.SetPosition(glm::vec2(Gfx::screenSpace.x / 2, pauseHint.GetSize().y)); 44 | gameOverText.SetCentered(true); 45 | gameOverText.SetVisible(false); 46 | gameOverSubText.SetPosition(Gfx::screenSpace / 2.0f); 47 | gameOverSubText.SetCentered(true); 48 | gameOverSubText.SetVisible(false); 49 | 50 | // Initialize map items 51 | mapBackground = Sprite::FromPNG(background_png, background_png_size); 52 | mapBackground->SetCentered(true); 53 | mapBackground->SetSize(glm::vec2(512.0f)); 54 | mapBackground->SetPosition(Gfx::screenSpace / 2.0f); 55 | mapPlayers[0] = Sprite::FromPNG(spaceship_small_red_png, spaceship_small_red_png_size); 56 | mapPlayers[0]->SetCentered(true); 57 | mapPlayers[0]->SetScale(glm::vec2(0.5f)); 58 | mapPlayers[1] = Sprite::FromPNG(spaceship_small_blue_png, spaceship_small_blue_png_size); 59 | mapPlayers[1]->SetCentered(true); 60 | mapPlayers[1]->SetScale(glm::vec2(0.5f)); 61 | 62 | // Initialize player lives 63 | tvPlayerLives[0].SetPosition(glm::vec2(8.0f, Gfx::screenSpace.y - tvPlayerLives[0].GetSize().y)); 64 | 65 | // Need to initialize right side text to calculate width 66 | std::string text; 67 | for (int i = 0; i < NUM_LIVES; ++i) { 68 | text += HEART_FULL; 69 | } 70 | tvPlayerLives[1].SetText(text); 71 | tvPlayerLives[1].SetPosition(glm::vec2(Gfx::screenSpace.x - tvPlayerLives[1].GetSize().x - 8.0f, 72 | Gfx::screenSpace.y - tvPlayerLives[1].GetSize().y)); 73 | 74 | // Load the backgrounds 75 | background = Sprite::FromPNG(background_png, background_png_size); 76 | tvBackground = Sprite::FromPNG(border_png, border_png_size); 77 | // Fill the entire screen 78 | background->SetSize(Gfx::screenSpace); 79 | tvBackground->SetSize(Gfx::screenSpace); 80 | // Adjust from 1:1 to 16:9 81 | background->SetUVScale(glm::vec2(1.77f, 1.0f)); 82 | tvBackground->SetUVScale(glm::vec2(1.77f, 1.0f)); 83 | 84 | // Create players 85 | players[0] = new Player(this, 0); 86 | players[1] = new Player(this, 1); 87 | 88 | // Create borders 89 | // Top 90 | borders[0] = Sprite::FromPNG(border_png, border_png_size); 91 | borders[0]->SetPosition(glm::vec2(0.0f, -((FIELD_HEIGHT + BORDER_SIZE) / 2))); 92 | borders[0]->SetSize(glm::vec2(FIELD_WIDTH + BORDER_SIZE, BORDER_SIZE)); 93 | borders[0]->SetCentered(true); 94 | borders[0]->SetUVScale(glm::vec2(borders[0]->GetSize().x / 1024.0f, borders[0]->GetSize().y / 1024.0f)); 95 | // Bottom 96 | borders[1] = Sprite::FromPNG(border_png, border_png_size); 97 | borders[1]->SetPosition(glm::vec2(0.0f, (FIELD_HEIGHT + BORDER_SIZE) / 2)); 98 | borders[1]->SetSize(glm::vec2(FIELD_WIDTH + BORDER_SIZE, BORDER_SIZE)); 99 | borders[1]->SetCentered(true); 100 | borders[1]->SetUVScale(glm::vec2(borders[1]->GetSize().x / 1024.0f, borders[1]->GetSize().y / 1024.0f)); 101 | // Left 102 | borders[2] = Sprite::FromPNG(border_png, border_png_size); 103 | borders[2]->SetPosition(glm::vec2(-((FIELD_WIDTH + BORDER_SIZE) / 2), 0.0f)); 104 | borders[2]->SetSize(glm::vec2(BORDER_SIZE, FIELD_HEIGHT + BORDER_SIZE)); 105 | borders[2]->SetCentered(true); 106 | borders[2]->SetUVScale(glm::vec2(borders[2]->GetSize().x / 1024.0f, borders[2]->GetSize().y / 1024.0f)); 107 | // Right 108 | borders[3] = Sprite::FromPNG(border_png, border_png_size); 109 | borders[3]->SetPosition(glm::vec2((FIELD_WIDTH + BORDER_SIZE) / 2, 0.0f)); 110 | borders[3]->SetSize(glm::vec2(BORDER_SIZE, FIELD_HEIGHT + BORDER_SIZE)); 111 | borders[3]->SetCentered(true); 112 | borders[3]->SetUVScale(glm::vec2(borders[3]->GetSize().x / 1024.0f, borders[3]->GetSize().y / 1024.0f)); 113 | 114 | // Initialize game 115 | Reset(); 116 | } 117 | 118 | Game::~Game() 119 | { 120 | for (Sprite* s : borders) { 121 | delete s; 122 | } 123 | 124 | for (Sprite* s : mapPlayers) { 125 | delete s; 126 | } 127 | 128 | delete players[0]; 129 | delete players[1]; 130 | delete tvBackground; 131 | delete mapBackground; 132 | delete background; 133 | } 134 | 135 | void Game::Update() 136 | { 137 | // Handle paused state 138 | if (paused || gameOver) { 139 | VPADStatus status{}; 140 | VPADRead(VPAD_CHAN_0, &status, 1, nullptr); 141 | 142 | // Continue 143 | if (status.trigger & (VPAD_BUTTON_A | VPAD_BUTTON_PLUS)) { 144 | if (gameOver) { 145 | Reset(); 146 | } else { 147 | PauseGame(false); 148 | } 149 | } 150 | 151 | // Return to menu 152 | if (status.trigger & VPAD_BUTTON_B) { 153 | // Reset game first 154 | Reset(); 155 | sceneMgr->SetScene(SceneMgr::SCENE_MENU); 156 | } 157 | 158 | return; 159 | } 160 | 161 | frameCount++; 162 | 163 | // Update players 164 | for (Player* p : players) { 165 | p->Update(); 166 | 167 | // Check for collision against the other players bullets 168 | for (Bullet& b : players[p->playerNum ? 0 : 1]->bullets) { 169 | // Use a simple distance check, not great but will do for this demo 170 | if (glm::length(b.sprite.GetPosition() - p->sprite->GetPosition()) < 32.0f) { 171 | // Spawn explosion particles 172 | for (int i = 0; i < 100; ++i) { 173 | p->particles.push_back(Particle{ 174 | Sprite( 175 | // Position: Create particles around player 176 | p->sprite->GetPosition() + glm::vec2(frand(-2.5f, 2.5f), frand(-2.5f, 2.5f)), 177 | // Size 178 | glm::vec2(2.0f), 179 | // Angle: Random rotations 180 | frand(-90.0f, 90.0f), 181 | // Color: Random reddish colors 182 | glm::vec4(frand(0.5f, 1.0f), 0.0f, 0.0f, 1.0f) 183 | ), 184 | // Velocity: random velocity 185 | glm::vec2(frand(-1.0f, 1.0f), frand(-1.0f, 1.0f)), 186 | // Random time-to-live 187 | 20u + (rand() % 20) 188 | }); 189 | } 190 | 191 | // Decrease lives 192 | p->lives--; 193 | std::string text; 194 | for (uint32_t i = 0; i < NUM_LIVES; ++i) { 195 | if (p->lives > i) { 196 | text += HEART_FULL; 197 | } else { 198 | text += HEART_EMPTY; 199 | } 200 | } 201 | p->livesText.SetText(text); 202 | tvPlayerLives[p->playerNum].SetText(text); 203 | 204 | // Handle game over 205 | if (p->lives == 0) { 206 | // Hide the player 207 | p->sprite->SetVisible(false); 208 | 209 | gameOver = true; 210 | winner = !p->playerNum; 211 | // Show pause and game over texts 212 | pauseBackground.SetVisible(true); 213 | pauseHint.SetVisible(true); 214 | gameOverText.SetVisible(true); 215 | gameOverSubText.SetVisible(true); 216 | 217 | // Update text and color to match winner 218 | gameOverSubText.SetText(winner ? "Player 2 won!" : "Player 1 won!"); 219 | gameOverSubText.SetColor(winner ? glm::vec4(0.2f, 0.62f, 0.8f, 1.0f) : glm::vec4(0.89f, 0.0f, 0.0f, 1.0f)); 220 | return; 221 | } 222 | 223 | // Place player in a random position 224 | p->sprite->SetPosition(glm::vec2( 225 | frand(-(FIELD_WIDTH / 2), (FIELD_WIDTH / 2)), 226 | frand(-(FIELD_HEIGHT / 2), (FIELD_HEIGHT / 2)))); 227 | } 228 | } 229 | } 230 | 231 | // Animate the TV background 232 | tvBackground->SetUVOffset(glm::vec2(frameCount * 0.005f, frameCount * -0.005f)); 233 | 234 | // Update map player icons 235 | mapPlayers[0]->SetPosition((Gfx::screenSpace / 2.0f) + (glm::vec2( 236 | players[0]->sprite->GetPosition().x / FIELD_WIDTH, players[0]->sprite->GetPosition().y / FIELD_HEIGHT) * 512.0f)); 237 | mapPlayers[0]->SetAngle(players[0]->sprite->GetAngle()); 238 | mapPlayers[1]->SetPosition((Gfx::screenSpace / 2.0f) + (glm::vec2( 239 | players[1]->sprite->GetPosition().x / FIELD_WIDTH, players[1]->sprite->GetPosition().y / FIELD_HEIGHT) * 512.0f)); 240 | mapPlayers[1]->SetAngle(players[1]->sprite->GetAngle()); 241 | } 242 | 243 | void Game::DrawScene(Gfx* gfx, Gfx::Target target) 244 | { 245 | if (target == Gfx::TARGET_TV) { 246 | // Default view (identity matrix) 247 | glm::mat4 view = glm::mat4(1.0f); 248 | gfx->SetView(view); 249 | 250 | // Draw the background 251 | tvBackground->Draw(gfx); 252 | 253 | // Draw the map 254 | mapBackground->Draw(gfx); 255 | mapPlayers[0]->Draw(gfx); 256 | mapPlayers[1]->Draw(gfx); 257 | 258 | // Draw player lives 259 | tvPlayerLives[0].Draw(gfx); 260 | tvPlayerLives[1].Draw(gfx); 261 | } else { 262 | Player* targetPlayer = players[target - 1]; 263 | 264 | // Default view (identity matrix) 265 | glm::mat4 view = glm::mat4(1.0f); 266 | gfx->SetView(view); 267 | 268 | // Offset the background based on player position 269 | background->SetUVOffset(targetPlayer->sprite->GetPosition() / background->GetScaledSize()); 270 | background->Draw(gfx); 271 | 272 | // Setup a centered camera which follows the player 273 | glm::vec3 cameraPosition = glm::vec3(targetPlayer->sprite->GetPosition() - Gfx::screenSpace / 2.0f, 0.0f); 274 | view = glm::lookAt(cameraPosition, cameraPosition + glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(0.0f, 1.0f, 0.0f)); 275 | gfx->SetView(view); 276 | 277 | // Draw borders 278 | for (Sprite* s : borders) { 279 | s->Draw(gfx); 280 | } 281 | 282 | // Draw particles and bullets first so they don't overlap players 283 | for (Player* p : players) { 284 | for (Particle& part : p->particles) { 285 | part.sprite.Draw(gfx); 286 | } 287 | for (Bullet& b : p->bullets) { 288 | b.sprite.Draw(gfx); 289 | } 290 | } 291 | 292 | // Draw player sprites 293 | for (Player* p : players) { 294 | p->sprite->Draw(gfx); 295 | } 296 | } 297 | 298 | // Default view (identity matrix) 299 | glm::mat4 view = glm::mat4(1.0f); 300 | gfx->SetView(view); 301 | 302 | if (target == Gfx::TARGET_DRC0) { 303 | players[0]->livesText.Draw(gfx); 304 | } else if (target == Gfx::TARGET_DRC1) { 305 | players[1]->livesText.Draw(gfx); 306 | } 307 | 308 | pauseBackground.Draw(gfx); 309 | pauseText.Draw(gfx); 310 | gameOverText.Draw(gfx); 311 | gameOverSubText.Draw(gfx); 312 | 313 | if (target == Gfx::TARGET_DRC0) { 314 | pauseHint.Draw(gfx); 315 | } 316 | } 317 | 318 | void Game::Reset() 319 | { 320 | for (Player* p : players) { 321 | p->bullets.clear(); 322 | p->particles.clear(); 323 | p->lives = NUM_LIVES; 324 | p->shootTimeout = 0; 325 | p->velocity = glm::vec2(0.0f); 326 | p->sprite->SetAngle(0.0f); 327 | p->sprite->SetVisible(true); 328 | 329 | std::string text; 330 | for (int i = 0; i < NUM_LIVES; ++i) { 331 | text += HEART_FULL; 332 | } 333 | p->livesText.SetText(text); 334 | tvPlayerLives[p->playerNum].SetText(text); 335 | } 336 | 337 | // Place players in a random position 338 | players[0]->sprite->SetPosition(glm::vec2( 339 | frand(-(FIELD_WIDTH / 2), (FIELD_WIDTH / 2)), 340 | frand(-(FIELD_HEIGHT / 2), (FIELD_HEIGHT / 2)))); 341 | players[1]->sprite->SetPosition(glm::vec2( 342 | frand(-(FIELD_WIDTH / 2), (FIELD_WIDTH / 2)), 343 | frand(-(FIELD_HEIGHT / 2), (FIELD_HEIGHT / 2)))); 344 | 345 | gameOver = false; 346 | gameOverText.SetVisible(false); 347 | gameOverSubText.SetVisible(false); 348 | 349 | PauseGame(false); 350 | } 351 | 352 | void Game::PauseGame(bool pause) 353 | { 354 | pauseBackground.SetVisible(pause); 355 | pauseText.SetVisible(pause); 356 | pauseHint.SetVisible(pause); 357 | 358 | paused = pause; 359 | } 360 | 361 | Game::Player::Player(Game* game, int playerNum) : 362 | game(game), 363 | playerNum(playerNum), 364 | lives(NUM_LIVES), 365 | livesText("-", 64, glm::vec2(0.0f), glm::vec2(1.0f), 0.0f, 366 | playerNum ? glm::vec4(0.2f, 0.62f, 0.8f, 1.0f) : glm::vec4(0.89f, 0.0f, 0.0f, 1.0f)), 367 | shootTimeout(0), 368 | velocity(glm::vec2(0.0f)) 369 | { 370 | // Load player sprite 371 | sprite = Sprite::FromPNG( 372 | playerNum ? spaceship_small_blue_png : spaceship_small_red_png, 373 | playerNum ? spaceship_small_blue_png_size : spaceship_small_red_png_size); 374 | sprite->SetCentered(true); 375 | sprite->SetScale(glm::vec2(1.5f)); 376 | // Disable filtering for pixel art 377 | sprite->SetLinearFilter(false); 378 | 379 | // Initialize lives text 380 | livesText.SetPosition(glm::vec2(8.0f, Gfx::screenSpace.y - livesText.GetSize().y)); 381 | } 382 | 383 | Game::Player::~Player() 384 | { 385 | delete sprite; 386 | } 387 | 388 | void Game::Player::Update() 389 | { 390 | VPADStatus status{}; 391 | VPADRead((VPADChan) playerNum, &status, 1, nullptr); 392 | 393 | // Pause (only allowed by host) 394 | if (playerNum == 0 && (status.trigger & VPAD_BUTTON_PLUS)) { 395 | game->PauseGame(true); 396 | } 397 | 398 | // Shoot 399 | if (status.hold & (VPAD_BUTTON_ZR | VPAD_BUTTON_R)) { 400 | if (shootTimeout == 0) { 401 | bullets.push_back(Bullet{ 402 | Sprite( 403 | // Position: Spawn bullet at player position 404 | sprite->GetPosition(), 405 | // Size 406 | glm::vec2(4.0f, 10.0f), 407 | // Use player angle for rotation 408 | sprite->GetAngle(), 409 | // Color 410 | glm::vec4(0.0f, 0.0f, 1.0f, 1.0f)), 411 | // Bullet velocity based on direction and current player velocity 412 | sprite->GetForwardVector() * (16.0f + glm::length(velocity)), 413 | // time to live 414 | 600 415 | }); 416 | 417 | // Set timeout 418 | shootTimeout = 5; 419 | } 420 | } 421 | 422 | // Update shoot timeout 423 | if (shootTimeout > 0) { 424 | shootTimeout--; 425 | } 426 | 427 | glm::vec2 leftStick = glm::vec2(status.leftStick.x, -status.leftStick.y); 428 | glm::vec2 rightStick = glm::vec2(status.rightStick.x, -status.rightStick.y); 429 | 430 | // Move 431 | if (glm::length(leftStick) > 0.1f) { 432 | float speed = 8.0f; 433 | 434 | // Speed boost 435 | // TODO maybe add a cooldown? 436 | if (status.hold & (VPAD_BUTTON_ZL | VPAD_BUTTON_L)) { 437 | // Double speed while boosting 438 | speed *= 2.0f; 439 | 440 | // Spawn boost particles 441 | for (int i = 0; i < 30; ++i) { 442 | particles.push_back(Particle{ 443 | Sprite( 444 | // Position: Create trail of particles behind player 445 | sprite->GetPosition() + (leftStick * glm::vec2(-(i % 15))), 446 | // Size 447 | glm::vec2(2.0f), 448 | // Angle: Random rotations 449 | frand(-90.0f, 90.0f), 450 | // Color: Random reddish colors 451 | glm::vec4(frand(0.5f, 1.0f) + 0.5f, 0.0f, 0.0f, 1.0f) 452 | ), 453 | // Velocity: Move in opposite player position with random offsets 454 | -sprite->GetForwardVector() + glm::vec2(frand(), frand()), 455 | // Random time-to-live 456 | 40u + (rand() % 40) 457 | }); 458 | } 459 | } 460 | 461 | // Set velocity based on stick and speed 462 | velocity = leftStick * speed; 463 | // Rotate player in move direction 464 | sprite->SetAngle(glm::degrees(atan2(leftStick.x, -leftStick.y))); 465 | } 466 | 467 | // Check for border collision 468 | if ((sprite->GetPosition().x + velocity.x) > (FIELD_WIDTH / 2) || (sprite->GetPosition().x + velocity.x) < -(FIELD_WIDTH / 2)) { 469 | velocity.x = -velocity.x * 3.0f; 470 | } 471 | if ((sprite->GetPosition().y + velocity.y) > (FIELD_HEIGHT / 2) || (sprite->GetPosition().y + velocity.y) < -(FIELD_HEIGHT / 2)) { 472 | velocity.y = -velocity.y * 3.0f; 473 | } 474 | 475 | // Update position 476 | sprite->SetPosition(sprite->GetPosition() + velocity); 477 | 478 | // Update velocity: Slowly decreasing 479 | velocity *= glm::vec2(0.95f); 480 | if (glm::length(velocity) < 0.002f) { 481 | velocity = glm::vec2(0.0f); 482 | } 483 | 484 | // Right stick overrides rotation 485 | if (glm::length(rightStick) > 0.1f) { 486 | sprite->SetAngle(glm::degrees(atan2(rightStick.x, -rightStick.y))); 487 | } 488 | 489 | // Update particles 490 | for (size_t i = 0; i < particles.size(); ++i) { 491 | Particle& p = particles[i]; 492 | if (--p.timeLeft == 0) { 493 | particles.erase(particles.begin() + i); 494 | continue;; 495 | } 496 | 497 | p.sprite.SetPosition(p.sprite.GetPosition() + p.velocity); 498 | } 499 | 500 | // Update bullets 501 | for (size_t i = 0; i < bullets.size(); ++i) { 502 | Bullet& b = bullets[i]; 503 | if (--b.timeLeft == 0) { 504 | bullets.erase(bullets.begin() + i); 505 | continue;; 506 | } 507 | 508 | b.sprite.SetPosition(b.sprite.GetPosition() + b.velocity); 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /source/Game.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Gfx.hpp" 4 | #include "Sprite.hpp" 5 | #include "Text.hpp" 6 | 7 | #include 8 | 9 | class SceneMgr; 10 | 11 | class Game { 12 | public: 13 | Game(SceneMgr* sceneMgr); 14 | virtual ~Game(); 15 | 16 | void Update(); 17 | 18 | void DrawScene(Gfx* gfx, Gfx::Target target); 19 | 20 | void Reset(); 21 | 22 | void PauseGame(bool pause); 23 | 24 | private: 25 | SceneMgr* sceneMgr; 26 | 27 | uint32_t frameCount; 28 | 29 | bool paused; 30 | Sprite pauseBackground; 31 | Text pauseText; 32 | Text pauseHint; 33 | 34 | bool gameOver; 35 | int winner; 36 | Text gameOverText; 37 | Text gameOverSubText; 38 | 39 | Sprite* tvBackground; 40 | Sprite* mapBackground; 41 | Sprite* mapPlayers[2]; 42 | Text tvPlayerLives[2]; 43 | 44 | Sprite* background; 45 | Sprite* borders[4]; 46 | 47 | struct Bullet { 48 | Sprite sprite; 49 | glm::vec2 velocity; 50 | uint32_t timeLeft; 51 | }; 52 | 53 | struct Particle { 54 | Sprite sprite; 55 | glm::vec2 velocity; 56 | uint32_t timeLeft; 57 | }; 58 | 59 | struct Player { 60 | Game* game; 61 | int playerNum; 62 | uint32_t lives; 63 | Text livesText; 64 | uint32_t shootTimeout; 65 | 66 | Sprite* sprite; 67 | glm::vec2 velocity; 68 | 69 | std::vector bullets; 70 | std::vector particles; 71 | 72 | Player(Game* game, int playerNum); 73 | virtual ~Player(); 74 | 75 | void Update(); 76 | }; 77 | 78 | Player* players[2]; 79 | }; 80 | -------------------------------------------------------------------------------- /source/Gfx.cpp: -------------------------------------------------------------------------------- 1 | #include "Gfx.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | #include "colorShader_gsh.h" 22 | #include "textureShader_gsh.h" 23 | 24 | static void InitColorBuffer(GX2ColorBuffer& cb, glm::uvec2& size, GX2SurfaceFormat format) 25 | { 26 | memset(&cb, 0, sizeof(GX2ColorBuffer)); 27 | cb.surface.use = GX2_SURFACE_USE_TEXTURE_COLOR_BUFFER_TV; 28 | cb.surface.dim = GX2_SURFACE_DIM_TEXTURE_2D; 29 | cb.surface.width = size.x; 30 | cb.surface.height = size.y; 31 | cb.surface.depth = 1; 32 | cb.surface.mipLevels = 1; 33 | cb.surface.format = format; 34 | cb.surface.tileMode = GX2_TILE_MODE_DEFAULT; 35 | cb.viewNumSlices = 1; 36 | GX2CalcSurfaceSizeAndAlignment(&cb.surface); 37 | GX2InitColorBufferRegs(&cb); 38 | } 39 | 40 | Gfx::Gfx() 41 | { 42 | inForeground = false; 43 | displaysEnabled = false; 44 | 45 | currentShader = SHADER_INVALID; 46 | 47 | modelMatrix = glm::mat4(1.0f); 48 | viewMatrix = glm::mat4(1.0f); 49 | projectionMatrix = glm::mat4(1.0f); 50 | } 51 | 52 | Gfx::~Gfx() 53 | { 54 | } 55 | 56 | uint32_t Gfx::ProcUiAcquired(void* arg) 57 | { 58 | Gfx* gfx = static_cast(arg); 59 | return gfx->OnForegroundAcquired(); 60 | } 61 | 62 | uint32_t Gfx::ProcUiReleased(void* arg) 63 | { 64 | Gfx* gfx = static_cast(arg); 65 | return gfx->OnForegroundReleased(); 66 | } 67 | 68 | int Gfx::OnForegroundAcquired() 69 | { 70 | inForeground = true; 71 | 72 | MEMHeapHandle fgHeap = MEMGetBaseHeapHandle(MEM_BASE_HEAP_FG); 73 | MEMHeapHandle mem1Heap = MEMGetBaseHeapHandle(MEM_BASE_HEAP_MEM1); 74 | 75 | // Allocate tv scan buffer 76 | tvScanBuffer = MEMAllocFromFrmHeapEx(fgHeap, tvScanBufferSize, GX2_SCAN_BUFFER_ALIGNMENT); 77 | if (!tvScanBuffer) { 78 | return -1; 79 | } 80 | GX2Invalidate(GX2_INVALIDATE_MODE_CPU, tvScanBuffer, tvScanBufferSize); 81 | GX2SetTVBuffer(tvScanBuffer, tvScanBufferSize, tvRenderMode, GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8, GX2_BUFFERING_MODE_DOUBLE); 82 | 83 | // Allocate drc scan buffer 84 | drcScanBuffer = MEMAllocFromFrmHeapEx(fgHeap, drcScanBufferSize, GX2_SCAN_BUFFER_ALIGNMENT); 85 | if (!drcScanBuffer) { 86 | return -1; 87 | } 88 | GX2Invalidate(GX2_INVALIDATE_MODE_CPU, drcScanBuffer, drcScanBufferSize); 89 | GX2SetDRCBuffer(drcScanBuffer, drcScanBufferSize, drcRenderMode, GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8, GX2_BUFFERING_MODE_DOUBLE); 90 | 91 | // Allocate colorbuffers 92 | for (int i = 0; i < NUM_TARGETS; ++i) { 93 | GX2ColorBuffer& cb = colorBuffers[i]; 94 | cb.surface.image = MEMAllocFromFrmHeapEx(mem1Heap, cb.surface.imageSize, cb.surface.alignment); 95 | if (!cb.surface.image) { 96 | return -1; 97 | } 98 | 99 | GX2Invalidate(GX2_INVALIDATE_MODE_CPU | GX2_INVALIDATE_MODE_COLOR_BUFFER, cb.surface.image, cb.surface.imageSize); 100 | } 101 | 102 | return 0; 103 | } 104 | 105 | int Gfx::OnForegroundReleased() 106 | { 107 | MEMHeapHandle fgHeap = MEMGetBaseHeapHandle(MEM_BASE_HEAP_FG); 108 | MEMHeapHandle mem1Heap = MEMGetBaseHeapHandle(MEM_BASE_HEAP_MEM1); 109 | 110 | // Free all foreground allocations 111 | MEMFreeToFrmHeap(fgHeap, MEM_FRM_HEAP_FREE_ALL); 112 | MEMFreeToFrmHeap(mem1Heap, MEM_FRM_HEAP_FREE_ALL); 113 | 114 | inForeground = false; 115 | 116 | return 0; 117 | } 118 | 119 | bool Gfx::Initialize() 120 | { 121 | // Initialize GX2 122 | commandBufferPool = memalign(GX2_COMMAND_BUFFER_ALIGNMENT, GX2_COMMAND_BUFFER_SIZE); 123 | if (!commandBufferPool) { 124 | return false; 125 | } 126 | 127 | uint32_t initAttribs[] = { 128 | GX2_INIT_CMD_BUF_BASE, (uintptr_t) commandBufferPool, 129 | GX2_INIT_CMD_BUF_POOL_SIZE, GX2_COMMAND_BUFFER_SIZE, 130 | GX2_INIT_ARGC, 0, 131 | GX2_INIT_ARGV, 0, 132 | GX2_INIT_END 133 | }; 134 | GX2Init(initAttribs); 135 | 136 | // Find the best TV render mode 137 | switch(GX2GetSystemTVScanMode()) { 138 | case GX2_TV_SCAN_MODE_480I: 139 | case GX2_TV_SCAN_MODE_480P: 140 | tvRenderMode = GX2_TV_RENDER_MODE_WIDE_480P; 141 | tvSize = glm::uvec2(854, 480); 142 | break; 143 | case GX2_TV_SCAN_MODE_1080I: 144 | case GX2_TV_SCAN_MODE_1080P: 145 | tvRenderMode = GX2_TV_RENDER_MODE_WIDE_1080P; 146 | tvSize = glm::uvec2(1920, 1080); 147 | break; 148 | case GX2_TV_SCAN_MODE_720P: 149 | default: 150 | tvRenderMode = GX2_TV_RENDER_MODE_WIDE_720P; 151 | tvSize = glm::uvec2(1280, 720); 152 | break; 153 | } 154 | 155 | // Always use double render mode as we might connect a second gamepad later 156 | drcRenderMode = GX2_DRC_RENDER_MODE_DOUBLE; //GX2GetSystemDRCMode(); 157 | drcSize = glm::uvec2(854, 480); 158 | 159 | // Calculate TV and DRC scanbuffer size 160 | uint32_t unk; 161 | GX2CalcTVSize(tvRenderMode, GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8, GX2_BUFFERING_MODE_DOUBLE, &tvScanBufferSize, &unk); 162 | GX2CalcDRCSize(drcRenderMode, GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8, GX2_BUFFERING_MODE_DOUBLE, &drcScanBufferSize, &unk); 163 | 164 | // Initialize all 3 colorbuffers 165 | InitColorBuffer(colorBuffers[TARGET_TV], tvSize, GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8); 166 | InitColorBuffer(colorBuffers[TARGET_DRC0], drcSize, GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8); 167 | InitColorBuffer(colorBuffers[TARGET_DRC1], drcSize, GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8); 168 | 169 | // Register callbacks for foreground allocations 170 | ProcUIRegisterCallback(PROCUI_CALLBACK_ACQUIRE, ProcUiAcquired, this, 100); 171 | ProcUIRegisterCallback(PROCUI_CALLBACK_RELEASE, ProcUiReleased, this, 100); 172 | 173 | // We're already in foreground at the time this is running so call the callback 174 | if (OnForegroundAcquired() != 0) { 175 | return false; 176 | } 177 | 178 | // Initialize a shared state 179 | contextState = (GX2ContextState*) memalign(GX2_CONTEXT_STATE_ALIGNMENT, sizeof(GX2ContextState)); 180 | if (!contextState) { 181 | return false; 182 | } 183 | 184 | GX2SetupContextStateEx(contextState, TRUE); 185 | GX2SetContextState(contextState); 186 | 187 | // Set TV and DRC scale 188 | GX2SetTVScale(tvSize.x, tvSize.y); 189 | GX2SetDRCScale(drcSize.x, drcSize.y); 190 | 191 | // Disable depth 192 | GX2SetDepthOnlyControl(FALSE, FALSE, GX2_COMPARE_FUNC_ALWAYS); 193 | 194 | // Enable blending 195 | GX2SetColorControl(GX2_LOGIC_OP_COPY, 0xFF, FALSE, TRUE); 196 | 197 | // Setup blend control 198 | GX2SetBlendControl(GX2_RENDER_TARGET_0, 199 | GX2_BLEND_MODE_SRC_ALPHA, 200 | GX2_BLEND_MODE_INV_SRC_ALPHA, 201 | GX2_BLEND_COMBINE_MODE_ADD, 202 | TRUE, 203 | GX2_BLEND_MODE_SRC_ALPHA, 204 | GX2_BLEND_MODE_INV_SRC_ALPHA, 205 | GX2_BLEND_COMBINE_MODE_ADD); 206 | 207 | // Set 60fps VSync 208 | GX2SetSwapInterval(1); 209 | 210 | // Load and initialize shaders 211 | WHBGfxShaderGroup* colorShader = &shaderGroups[SHADER_COLOR]; 212 | WHBGfxLoadGFDShaderGroup(colorShader, 0, colorShader_gsh); 213 | WHBGfxInitShaderAttribute(colorShader, "aPosition", 0, 0, GX2_ATTRIB_FORMAT_FLOAT_32_32); 214 | WHBGfxInitFetchShader(colorShader); 215 | 216 | WHBGfxShaderGroup* textureShader = &shaderGroups[SHADER_TEXTURE]; 217 | WHBGfxLoadGFDShaderGroup(textureShader, 0, textureShader_gsh); 218 | WHBGfxInitShaderAttribute(textureShader, "aPosition", 0, 0, GX2_ATTRIB_FORMAT_FLOAT_32_32); 219 | WHBGfxInitShaderAttribute(textureShader, "aTexCoord", 0, 8, GX2_ATTRIB_FORMAT_FLOAT_32_32); 220 | WHBGfxInitFetchShader(textureShader); 221 | 222 | // Initialize projection 223 | projectionMatrix = glm::ortho(0.0f, screenSpace.x, screenSpace.y, 0.0f, -1.0f, 1.0f); 224 | matrixUpdated = true; 225 | 226 | return true; 227 | } 228 | 229 | void Gfx::Finalize() 230 | { 231 | // Release foreground if we're still in foreground 232 | if (inForeground) { 233 | OnForegroundReleased(); 234 | } 235 | 236 | // Shut down GX2 237 | GX2Shutdown(); 238 | 239 | // Free allocations 240 | free(contextState); 241 | contextState = nullptr; 242 | 243 | free(commandBufferPool); 244 | commandBufferPool = nullptr; 245 | 246 | WHBGfxFreeShaderGroup(&shaderGroups[SHADER_COLOR]); 247 | WHBGfxFreeShaderGroup(&shaderGroups[SHADER_TEXTURE]); 248 | } 249 | 250 | void Gfx::SetModel(glm::mat4& model) 251 | { 252 | modelMatrix = model; 253 | matrixUpdated = true; 254 | } 255 | 256 | void Gfx::SetView(glm::mat4& view) 257 | { 258 | viewMatrix = view; 259 | matrixUpdated = true; 260 | } 261 | 262 | void Gfx::BeginDraw(Target target, glm::vec4 color) 263 | { 264 | currentTarget = target; 265 | GX2ColorBuffer* cb = &colorBuffers[target]; 266 | 267 | // Setup colorbuffer and viewport 268 | GX2SetColorBuffer(cb, GX2_RENDER_TARGET_0); 269 | GX2SetViewport(0.0f, 0.0f, (float) cb->surface.width, (float) cb->surface.height, 0.0f, 1.0f); 270 | GX2SetScissor(0, 0, cb->surface.width, cb->surface.height); 271 | 272 | // Clear colorbuffer 273 | GX2ClearColor(cb, color.r, color.g, color.b, color.a); 274 | GX2SetContextState(contextState); 275 | } 276 | 277 | void Gfx::EndDraw() 278 | { 279 | GX2ColorBuffer* cb = &colorBuffers[currentTarget]; 280 | 281 | static const GX2ScanTarget scanTargets[] = { 282 | // TARGET_TV 283 | GX2_SCAN_TARGET_TV, 284 | // TARGET_DRC0 285 | GX2_SCAN_TARGET_DRC0, 286 | // TARGET_DRC1 287 | GX2_SCAN_TARGET_DRC1, 288 | }; 289 | 290 | // Copy the target buffer to the scanbuffer 291 | GX2CopyColorBufferToScanBuffer(cb, scanTargets[currentTarget]); 292 | GX2SetContextState(contextState); 293 | } 294 | 295 | void Gfx::Draw(Texture* tex, const void* vertices, uint32_t numVertices, glm::vec4 color, bool quads) 296 | { 297 | // Set wanted shader 298 | Shader shader = tex ? SHADER_TEXTURE : SHADER_COLOR; 299 | WHBGfxShaderGroup* shaderGroup = &shaderGroups[shader]; 300 | bool shaderUpdated = false; 301 | if (currentShader != shader) { 302 | GX2SetFetchShader(&shaderGroup->fetchShader); 303 | GX2SetVertexShader(shaderGroup->vertexShader); 304 | GX2SetPixelShader(shaderGroup->pixelShader); 305 | currentShader = shader; 306 | shaderUpdated = true; 307 | } 308 | 309 | if (matrixUpdated || shaderUpdated) { 310 | // Calculate and set model view projection matrix 311 | glm::mat4 mvpMatrix = projectionMatrix * viewMatrix * modelMatrix; 312 | GX2SetVertexUniformReg(shaderGroup->vertexShader->uniformVars[0].offset, 16, glm::value_ptr(mvpMatrix)); 313 | } 314 | 315 | // Set color 316 | GX2SetPixelUniformReg(shaderGroup->pixelShader->uniformVars[0].offset, 4, glm::value_ptr(color)); 317 | 318 | // Set up texture 319 | if (tex) { 320 | uint32_t location = shaderGroup->pixelShader->samplerVars[0].location; 321 | GX2SetPixelTexture(&tex->texture, location); 322 | GX2SetPixelSampler(&tex->sampler, location); 323 | 324 | GX2SetPixelUniformReg(shaderGroup->pixelShader->uniformVars[1].offset, 4, tex->texCoordParams); 325 | } 326 | 327 | // Draw 328 | const uint32_t stride = tex ? 16 : 8; 329 | GX2SetAttribBuffer(0, stride * numVertices, stride, vertices); 330 | GX2DrawEx(quads ? GX2_PRIMITIVE_MODE_QUADS : GX2_PRIMITIVE_MODE_TRIANGLES, numVertices, 0, 1); 331 | } 332 | 333 | void Gfx::SwapBuffers(void) 334 | { 335 | // Swap scan buffers 336 | GX2SwapScanBuffers(); 337 | GX2SetContextState(contextState); 338 | 339 | // Flush all packets to the GPU 340 | GX2Flush(); 341 | 342 | // Enable TV and DRC on first frame rendered 343 | if (!displaysEnabled) { 344 | GX2SetTVEnable(TRUE); 345 | GX2SetDRCEnable(TRUE); 346 | displaysEnabled = true; 347 | } 348 | 349 | // Wait for flip 350 | uint32_t swapCount, flipCount; 351 | OSTime lastFlip, lastVsync; 352 | uint32_t waitCount = 0; 353 | while (true) { 354 | GX2GetSwapStatus(&swapCount, &flipCount, &lastFlip, &lastVsync); 355 | 356 | if (flipCount >= swapCount) { 357 | break; 358 | } 359 | 360 | if (waitCount >= 10) { 361 | // GPU timed out 362 | break; 363 | } 364 | 365 | waitCount++; 366 | GX2WaitForVsync(); 367 | } 368 | } 369 | 370 | Gfx::Texture* Gfx::NewTexture(glm::uvec2 size, void* rgba, bool clamp, bool linearFilter) 371 | { 372 | // Allocate texture 373 | Texture* tex = new Texture(); 374 | if (!tex) { 375 | return nullptr; 376 | } 377 | 378 | // Initialize texture 379 | tex->texture.surface.use = GX2_SURFACE_USE_TEXTURE; 380 | tex->texture.surface.dim = GX2_SURFACE_DIM_TEXTURE_2D; 381 | tex->texture.surface.width = size.x; 382 | tex->texture.surface.height = size.y; 383 | tex->texture.surface.depth = 1; 384 | tex->texture.surface.mipLevels = 1; 385 | tex->texture.surface.format = GX2_SURFACE_FORMAT_UNORM_R8_G8_B8_A8; 386 | tex->texture.surface.aa = GX2_AA_MODE1X; 387 | tex->texture.surface.tileMode = GX2_TILE_MODE_LINEAR_ALIGNED; 388 | tex->texture.viewFirstMip = 0; 389 | tex->texture.viewNumMips = 1; 390 | tex->texture.viewFirstSlice = 0; 391 | tex->texture.viewNumSlices = 1; 392 | tex->texture.compMap = GX2_COMP_MAP(GX2_SQ_SEL_R, GX2_SQ_SEL_G, GX2_SQ_SEL_B, GX2_SQ_SEL_A); 393 | GX2CalcSurfaceSizeAndAlignment(&tex->texture.surface); 394 | GX2InitTextureRegs(&tex->texture); 395 | 396 | // Allocate texture surface 397 | tex->texture.surface.image = memalign(tex->texture.surface.alignment, tex->texture.surface.imageSize); 398 | if (!tex->texture.surface.image) { 399 | delete tex; 400 | return nullptr; 401 | } 402 | 403 | // Clear and invalidate texture 404 | memset(tex->texture.surface.image, 0, tex->texture.surface.imageSize); 405 | GX2Invalidate(GX2_INVALIDATE_MODE_CPU_TEXTURE, tex->texture.surface.image, tex->texture.surface.imageSize); 406 | 407 | // If we have any data, copy it to the texture 408 | if (rgba) { 409 | tex->Update(rgba); 410 | } 411 | 412 | // Initialize the sampler 413 | GX2InitSampler(&tex->sampler, 414 | clamp ? GX2_TEX_CLAMP_MODE_CLAMP : GX2_TEX_CLAMP_MODE_WRAP, 415 | linearFilter ? GX2_TEX_XY_FILTER_MODE_LINEAR : GX2_TEX_XY_FILTER_MODE_POINT); 416 | 417 | // No offset by default, 1x scaling 418 | tex->texCoordParams[0] = 0.0f; 419 | tex->texCoordParams[1] = 0.0f; 420 | tex->texCoordParams[2] = 1.0f; 421 | tex->texCoordParams[3] = 1.0f; 422 | 423 | return tex; 424 | } 425 | 426 | void Gfx::Texture::Update(void* rgba) 427 | { 428 | uint32_t pitch = GetPitch(); 429 | glm::uvec2 size = GetSize(); 430 | uint8_t* dstPtr = (uint8_t*) Lock(); 431 | uint8_t* srcPtr = (uint8_t*) rgba; 432 | 433 | // Copy the texture row by row 434 | for (uint32_t y = 0; y < size.y; ++y) { 435 | memcpy(dstPtr + (y * pitch * 4), srcPtr + (y * size.x * 4), size.x * 4); 436 | } 437 | 438 | Unlock(); 439 | } 440 | 441 | uint32_t Gfx::Texture::GetPitch() 442 | { 443 | return texture.surface.pitch; 444 | } 445 | 446 | glm::uvec2 Gfx::Texture::GetSize() 447 | { 448 | return glm::uvec2(texture.surface.width, texture.surface.height); 449 | } 450 | 451 | void Gfx::Texture::SetClamp(bool clamp) 452 | { 453 | GX2TexClampMode mode = clamp ? GX2_TEX_CLAMP_MODE_CLAMP : GX2_TEX_CLAMP_MODE_WRAP; 454 | GX2InitSamplerClamping(&sampler, mode, mode, mode); 455 | } 456 | 457 | void Gfx::Texture::SetLinearFilter(bool linear) 458 | { 459 | GX2TexXYFilterMode mode = linear ? GX2_TEX_XY_FILTER_MODE_LINEAR : GX2_TEX_XY_FILTER_MODE_POINT; 460 | GX2InitSamplerXYFilter(&sampler, mode, mode, GX2_TEX_ANISO_RATIO_NONE); 461 | } 462 | 463 | void Gfx::Texture::SetUVOffset(glm::vec2 offset) 464 | { 465 | texCoordParams[0] = offset.x; 466 | texCoordParams[1] = offset.y; 467 | } 468 | 469 | void Gfx::Texture::SetUVScale(glm::vec2 scale) 470 | { 471 | texCoordParams[2] = scale.x; 472 | texCoordParams[3] = scale.y; 473 | } 474 | 475 | void* Gfx::Texture::Lock() 476 | { 477 | return texture.surface.image; 478 | } 479 | 480 | void Gfx::Texture::Unlock() 481 | { 482 | // Invalidate texture 483 | GX2Invalidate(GX2_INVALIDATE_MODE_CPU_TEXTURE, texture.surface.image, texture.surface.imageSize); 484 | } 485 | 486 | void Gfx::Texture::Delete() 487 | { 488 | // Free surface data and delete the texture 489 | free(texture.surface.image); 490 | delete this; 491 | } 492 | -------------------------------------------------------------------------------- /source/Gfx.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Utils.hpp" 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | class Gfx { 11 | public: 12 | enum Target { 13 | TARGET_TV, 14 | TARGET_DRC0, 15 | TARGET_DRC1, 16 | 17 | NUM_TARGETS, 18 | }; 19 | 20 | struct Texture { 21 | GX2Texture texture; 22 | GX2Sampler sampler; 23 | // xy: offset, zw: scale 24 | float texCoordParams[4]; 25 | 26 | uint32_t GetPitch(); 27 | 28 | glm::uvec2 GetSize(); 29 | 30 | void SetClamp(bool clamp); 31 | 32 | void SetLinearFilter(bool linear); 33 | 34 | void SetUVOffset(glm::vec2 offset); 35 | 36 | void SetUVScale(glm::vec2 scale); 37 | 38 | void Update(void* rgba); 39 | 40 | void* Lock(); 41 | 42 | void Unlock(); 43 | 44 | void Delete(); 45 | 46 | private: 47 | friend Gfx; 48 | Texture() = default; 49 | ~Texture() = default; 50 | }; 51 | 52 | Gfx(); 53 | virtual ~Gfx(); 54 | 55 | bool Initialize(); 56 | 57 | void Finalize(); 58 | 59 | void SetModel(glm::mat4& model); 60 | 61 | void SetView(glm::mat4& view); 62 | 63 | void BeginDraw(Target target, glm::vec4 color = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); 64 | 65 | void EndDraw(); 66 | 67 | void Draw(Texture* tex, const void* vertices, uint32_t numVertices, glm::vec4 color = glm::vec4(1.0f), bool quads = false); 68 | 69 | void SwapBuffers(void); 70 | 71 | static Texture* NewTexture(glm::uvec2 size, void* rgba = nullptr, bool clamp = false, bool linearFilter = true); 72 | 73 | // virtual screen space used in projection 74 | static inline glm::vec2 screenSpace = glm::vec2(1280.0f, 720.0f); 75 | 76 | private: 77 | static uint32_t ProcUiAcquired(void* arg); 78 | static uint32_t ProcUiReleased(void* arg); 79 | int OnForegroundAcquired(); 80 | int OnForegroundReleased(); 81 | 82 | bool inForeground; 83 | void* commandBufferPool; 84 | 85 | GX2TVRenderMode tvRenderMode; 86 | glm::uvec2 tvSize; 87 | uint32_t tvScanBufferSize; 88 | void* tvScanBuffer; 89 | 90 | GX2DrcRenderMode drcRenderMode; 91 | glm::uvec2 drcSize; 92 | uint32_t drcScanBufferSize; 93 | void* drcScanBuffer; 94 | 95 | GX2ColorBuffer colorBuffers[NUM_TARGETS]; 96 | 97 | GX2ContextState* contextState; 98 | 99 | Target currentTarget; 100 | 101 | bool displaysEnabled; 102 | 103 | bool matrixUpdated; 104 | glm::mat4 modelMatrix; 105 | glm::mat4 viewMatrix; 106 | glm::mat4 projectionMatrix; 107 | 108 | enum Shader { 109 | SHADER_INVALID = -1, 110 | 111 | SHADER_COLOR, 112 | SHADER_TEXTURE, 113 | 114 | NUM_SHADERS, 115 | }; 116 | 117 | WHBGfxShaderGroup shaderGroups[NUM_SHADERS]; 118 | Shader currentShader; 119 | }; 120 | -------------------------------------------------------------------------------- /source/Menu.cpp: -------------------------------------------------------------------------------- 1 | #include "Menu.hpp" 2 | #include "SceneMgr.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "background_png.h" 10 | #include "controls_png.h" 11 | 12 | Menu::Menu(SceneMgr* sceneMgr) : 13 | sceneMgr(sceneMgr), 14 | frameCount(0), 15 | title("MultiDRCSpaceDemo", 96), 16 | version("Version 0.1", 32), 17 | menuOptions{ 18 | Text("Start Game", 48), 19 | Text("Pair second GamePad", 48), 20 | Text("Exit", 48), 21 | }, 22 | selected(0), 23 | confirmPromptOpened(false), 24 | confirmText("No second GamePad connected!", 48), 25 | confirmHint("Press \ue000 to start anyways, any other button to cancel", 32), 26 | drc1Text("Waiting for host to start game...", 48) 27 | { 28 | // Load the background 29 | background = Sprite::FromPNG(background_png, background_png_size); 30 | // Fill the entire screen 31 | background->SetSize(Gfx::screenSpace); 32 | // Adjust from 1:1 to 16:9 33 | background->SetUVScale(glm::vec2(1.77f, 1.0f)); 34 | 35 | // Center the title 36 | title.SetCentered(true); 37 | title.SetPosition(glm::vec2(Gfx::screenSpace.x / 2, title.GetSize().y)); 38 | 39 | // Setup version text 40 | version.SetPosition(glm::vec2(Gfx::screenSpace.x - version.GetSize().x - 8.0f, 41 | Gfx::screenSpace.y - version.GetSize().y)); 42 | 43 | // Setup controls image 44 | controls = Sprite::FromPNG(controls_png, controls_png_size); 45 | controls->SetCentered(true); 46 | controls->SetSize(Gfx::screenSpace / 2.0f); 47 | controls->SetPosition(glm::vec2(Gfx::screenSpace.x / 2, (Gfx::screenSpace.y + controls->GetSize().y) / 2)); 48 | 49 | // Initialize option positions 50 | float yOffset = Gfx::screenSpace.y / 2.0f; 51 | for (size_t i = 0; i < COUNTOF(menuOptions); ++i) { 52 | menuOptions[i].SetCentered(true); 53 | menuOptions[i].SetPosition(glm::vec2(Gfx::screenSpace.x / 2, yOffset)); 54 | yOffset += menuOptions[i].GetSize().y; 55 | } 56 | 57 | // Highlight the initially selected option 58 | menuOptions[selected].SetColor(glm::vec4(0.75f, 0.33f, 0.92f, 1.0f)); 59 | 60 | // Initialize confirm prompt 61 | confirmText.SetPosition(Gfx::screenSpace / 2.0f); 62 | confirmText.SetCentered(true); 63 | confirmHint.SetPosition(glm::vec2(Gfx::screenSpace.x / 2, Gfx::screenSpace.y - confirmHint.GetSize().y)); 64 | confirmHint.SetCentered(true); 65 | 66 | // Set drc1 message position 67 | drc1Text.SetCentered(true); 68 | drc1Text.SetPosition(Gfx::screenSpace / 2.0f); 69 | } 70 | 71 | Menu::~Menu() 72 | { 73 | delete controls; 74 | delete background; 75 | } 76 | 77 | void Menu::Update() 78 | { 79 | frameCount++; 80 | 81 | // Animate the background 82 | background->SetUVOffset(glm::vec2(frameCount * 0.005f, frameCount * -0.005f)); 83 | 84 | // Animate the title 85 | title.SetAngle(sin(frameCount * 4.0f * M_PI / 180.0f) * 8.0f); 86 | title.SetScale(glm::vec2(SCALE(sin(frameCount * 10.0f * M_PI / 180.0f), -1.0f, 1.0f, 0.9f, 1.0f))); 87 | 88 | // Menu can only be controlled by the host (Gamepad 0) 89 | VPADStatus status{}; 90 | VPADRead(VPAD_CHAN_0, &status, 1, nullptr); 91 | 92 | // Handle the confirm prompt 93 | if (confirmPromptOpened) { 94 | if (status.trigger & VPAD_BUTTON_A) { 95 | sceneMgr->SetScene(SceneMgr::SCENE_GAME); 96 | } 97 | 98 | if (status.trigger) { 99 | confirmPromptOpened = false; 100 | } 101 | 102 | return; 103 | } 104 | 105 | // Update menu selection 106 | if (status.trigger & (VPAD_BUTTON_DOWN | VPAD_STICK_L_EMULATION_DOWN)) { 107 | if (selected <= COUNTOF(menuOptions) - 2) { 108 | selected++; 109 | } 110 | } else if (status.trigger & (VPAD_BUTTON_UP | VPAD_STICK_L_EMULATION_UP)) { 111 | if (selected > 0) { 112 | selected--; 113 | } 114 | } 115 | 116 | // Handle selection 117 | if (status.trigger & (VPAD_BUTTON_A | VPAD_BUTTON_PLUS)) { 118 | switch (selected) { 119 | case 0: 120 | if (GX2GetSystemDRCMode() == GX2_DRC_RENDER_MODE_DOUBLE) { 121 | sceneMgr->SetScene(SceneMgr::SCENE_GAME); 122 | } else { 123 | confirmPromptOpened = true; 124 | } 125 | break; 126 | case 1: 127 | sceneMgr->SetScene(SceneMgr::SCENE_DRC_PAIRING); 128 | break; 129 | case 2: 130 | // If we're not running from the HBL launch the Wii U menu 131 | if (!RunningFromHBL()) { 132 | SYSLaunchMenu(); 133 | } else { 134 | WHBProcStopRunning(); 135 | } 136 | break; 137 | } 138 | } 139 | 140 | // Highlight the selected entry 141 | for (size_t i = 0; i < COUNTOF(menuOptions); ++i) { 142 | menuOptions[i].SetColor(selected == i ? glm::vec4(0.75f, 0.33f, 0.92f, 1.0f) : glm::vec4(1.0f)); 143 | } 144 | } 145 | 146 | void Menu::DrawScene(Gfx* gfx, Gfx::Target target) 147 | { 148 | // Default view (identity matrix) 149 | glm::mat4 view = glm::mat4(1.0f); 150 | gfx->SetView(view); 151 | 152 | background->Draw(gfx); 153 | title.Draw(gfx); 154 | version.Draw(gfx); 155 | 156 | // Draw target specific elements 157 | if (target == Gfx::TARGET_TV) { 158 | controls->Draw(gfx); 159 | } else if (target == Gfx::TARGET_DRC0) { 160 | if (confirmPromptOpened) { 161 | confirmText.Draw(gfx); 162 | confirmHint.Draw(gfx); 163 | } else { 164 | for (size_t i = 0; i < COUNTOF(menuOptions); ++i) { 165 | menuOptions[i].Draw(gfx); 166 | } 167 | } 168 | } else if (target == Gfx::TARGET_DRC1) { 169 | drc1Text.Draw(gfx); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /source/Menu.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Gfx.hpp" 4 | #include "Text.hpp" 5 | 6 | class SceneMgr; 7 | 8 | class Menu { 9 | public: 10 | Menu(SceneMgr* sceneMgr); 11 | virtual ~Menu(); 12 | 13 | void Update(); 14 | 15 | void DrawScene(Gfx* gfx, Gfx::Target target); 16 | 17 | private: 18 | SceneMgr* sceneMgr; 19 | uint32_t frameCount; 20 | 21 | Sprite* background; 22 | Text title; 23 | Text version; 24 | Sprite* controls; 25 | 26 | Text menuOptions[3]; 27 | size_t selected; 28 | 29 | bool confirmPromptOpened; 30 | Text confirmText; 31 | Text confirmHint; 32 | 33 | Text drc1Text; 34 | Sprite* controlsImage; 35 | }; 36 | -------------------------------------------------------------------------------- /source/SceneMgr.cpp: -------------------------------------------------------------------------------- 1 | #include "SceneMgr.hpp" 2 | 3 | #include "Menu.hpp" 4 | #include "Game.hpp" 5 | #include "DrcPairing.hpp" 6 | 7 | SceneMgr::SceneMgr() 8 | { 9 | // Create scenes 10 | menu = new Menu(this); 11 | game = new Game(this); 12 | drcPairing = new DrcPairing(this); 13 | 14 | // Start with the menu scene 15 | currentScene = SCENE_MENU; 16 | } 17 | 18 | SceneMgr::~SceneMgr() 19 | { 20 | delete drcPairing; 21 | delete game; 22 | delete menu; 23 | } 24 | 25 | void SceneMgr::SetScene(Scene scene) 26 | { 27 | currentScene = scene; 28 | } 29 | 30 | void SceneMgr::Update() 31 | { 32 | switch (currentScene) { 33 | case SCENE_MENU: 34 | menu->Update(); 35 | break; 36 | case SCENE_GAME: 37 | game->Update(); 38 | break; 39 | case SCENE_DRC_PAIRING: 40 | drcPairing->Update(); 41 | break; 42 | } 43 | } 44 | 45 | void SceneMgr::DrawScene(Gfx* gfx, Gfx::Target target) 46 | { 47 | switch (currentScene) { 48 | case SCENE_MENU: 49 | menu->DrawScene(gfx, target); 50 | break; 51 | case SCENE_GAME: 52 | game->DrawScene(gfx, target); 53 | break; 54 | case SCENE_DRC_PAIRING: 55 | drcPairing->DrawScene(gfx, target); 56 | break; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /source/SceneMgr.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Gfx.hpp" 4 | 5 | class SceneMgr { 6 | public: 7 | enum Scene { 8 | SCENE_MENU, 9 | SCENE_GAME, 10 | SCENE_DRC_PAIRING, 11 | }; 12 | 13 | SceneMgr(); 14 | virtual ~SceneMgr(); 15 | 16 | void SetScene(Scene scene); 17 | 18 | void Update(); 19 | 20 | void DrawScene(Gfx* gfx, Gfx::Target target); 21 | 22 | protected: 23 | Scene currentScene; 24 | 25 | class Menu* menu; 26 | class Game* game; 27 | class DrcPairing* drcPairing; 28 | }; 29 | -------------------------------------------------------------------------------- /source/Sprite.cpp: -------------------------------------------------------------------------------- 1 | #include "Sprite.hpp" 2 | 3 | #include 4 | 5 | // Aligned vertex buffers which can be directly sent to the GPU 6 | static float colorVertices[][2] __attribute__ ((aligned (GX2_VERTEX_BUFFER_ALIGNMENT))) = { 7 | { 0.0f, 1.0f, }, 8 | { 1.0f, 0.0f, }, 9 | { 0.0f, 0.0f, }, 10 | { 0.0f, 1.0f, }, 11 | { 1.0f, 1.0f, }, 12 | { 1.0f, 0.0f, }, 13 | }; 14 | 15 | static const float textureVertices[][4] __attribute__ ((aligned (GX2_VERTEX_BUFFER_ALIGNMENT))) = { 16 | { 0.0f, 1.0f, 0.0f, 1.0f, }, 17 | { 1.0f, 0.0f, 1.0f, 0.0f, }, 18 | { 0.0f, 0.0f, 0.0f, 0.0f, }, 19 | { 0.0f, 1.0f, 0.0f, 1.0f, }, 20 | { 1.0f, 1.0f, 1.0f, 1.0f, }, 21 | { 1.0f, 0.0f, 1.0f, 0.0f, }, 22 | }; 23 | 24 | static void png_read_data(png_structp png_ptr, png_bytep outBytes, png_size_t byteCountToRead) 25 | { 26 | void** data = (void**) png_get_io_ptr(png_ptr); 27 | 28 | memcpy(outBytes, *data, byteCountToRead); 29 | *((uint8_t**) data) += byteCountToRead; 30 | } 31 | 32 | Sprite* Sprite::FromPNG(const void* data, uint32_t size) 33 | { 34 | Sprite* s = new Sprite(); 35 | 36 | // Setup png read and info struct 37 | png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); 38 | if (!png_ptr) { 39 | delete s; 40 | return nullptr; 41 | } 42 | 43 | png_infop info_ptr = png_create_info_struct(png_ptr); 44 | if (!info_ptr) { 45 | png_destroy_read_struct(&png_ptr, nullptr, nullptr); 46 | delete s; 47 | return nullptr; 48 | } 49 | 50 | // Set read function and info 51 | png_set_read_fn(png_ptr, (void *) &data, png_read_data); 52 | png_read_info(png_ptr, info_ptr); 53 | 54 | // Read the IHDR 55 | png_uint_32 width = 0; 56 | png_uint_32 height = 0; 57 | int bitDepth = 0; 58 | int colorType = -1; 59 | if (png_get_IHDR(png_ptr, info_ptr, &width, &height, &bitDepth, &colorType, nullptr, nullptr, nullptr) != 1) { 60 | png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); 61 | delete s; 62 | return nullptr; 63 | } 64 | 65 | // convert RGB data to RGBA 66 | if (colorType == PNG_COLOR_TYPE_RGB) { 67 | png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER); 68 | } 69 | 70 | // Create the texture 71 | Gfx::Texture* tex = Gfx::NewTexture(glm::uvec2(width, height)); 72 | if (!tex) { 73 | png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); 74 | delete s; 75 | return nullptr; 76 | } 77 | 78 | s->SetTexture(tex, true); 79 | 80 | // Read the png data into the texture 81 | uint32_t pitch = tex->GetPitch(); 82 | uint8_t* textureData = (uint8_t*) tex->Lock(); 83 | for (png_uint_32 y = 0; y < height; y++) { 84 | png_read_row(png_ptr, (png_bytep) textureData + (y * pitch * 4), nullptr); 85 | } 86 | tex->Unlock(); 87 | 88 | // Cleanup 89 | png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); 90 | 91 | return s; 92 | } 93 | 94 | Sprite::Sprite(glm::vec2 position, glm::vec2 size, float angle, glm::vec4 color) : 95 | deleteTexture(false), 96 | texture(nullptr), 97 | position(position), 98 | size(size), 99 | scale(1.0f), 100 | angle(angle), 101 | color(color), 102 | centered(false), 103 | visible(true) 104 | { 105 | // Calculate scaled size 106 | this->scaledSize = size * scale; 107 | 108 | // Calculate the model matrix 109 | UpdateModel(); 110 | } 111 | 112 | Sprite::Sprite(Gfx::Texture* texture, glm::vec2 position, float angle, glm::vec4 color) : 113 | deleteTexture(false), 114 | texture(texture), 115 | position(position), 116 | scale(1.0f), 117 | angle(angle), 118 | color(color), 119 | centered(false), 120 | visible(true) 121 | { 122 | // Get the size from the texture 123 | SetSize(texture->GetSize()); 124 | } 125 | 126 | Sprite::~Sprite() 127 | { 128 | if (texture && deleteTexture) { 129 | texture->Delete(); 130 | } 131 | } 132 | 133 | void Sprite::SetTexture(Gfx::Texture* texture, bool updateSize) 134 | { 135 | this->texture = texture; 136 | 137 | // Set the size of the sprite to match the texture size if wanted 138 | if (updateSize) { 139 | SetSize(texture->GetSize()); 140 | } 141 | } 142 | 143 | void Sprite::SetPosition(glm::vec2 pos) 144 | { 145 | this->position = pos; 146 | UpdateModel(); 147 | } 148 | 149 | void Sprite::SetSize(glm::vec2 size) 150 | { 151 | this->size = size; 152 | this->scaledSize = size * scale; 153 | UpdateModel(); 154 | } 155 | 156 | void Sprite::SetScale(glm::vec2 scale) 157 | { 158 | this->scale = scale; 159 | this->scaledSize = size * scale; 160 | UpdateModel(); 161 | } 162 | 163 | void Sprite::SetAngle(float angle) 164 | { 165 | this->angle = angle; 166 | UpdateModel(); 167 | } 168 | 169 | void Sprite::SetColor(glm::vec4 color) 170 | { 171 | this->color = color; 172 | UpdateModel(); 173 | } 174 | 175 | void Sprite::SetCentered(bool centered) 176 | { 177 | this->centered = centered; 178 | UpdateModel(); 179 | } 180 | 181 | void Sprite::SetVisible(bool visible) 182 | { 183 | this->visible = visible; 184 | } 185 | 186 | void Sprite::SetUVOffset(glm::vec2 off) 187 | { 188 | if (texture) { 189 | texture->SetUVOffset(off); 190 | } 191 | } 192 | 193 | void Sprite::SetUVScale(glm::vec2 scale) 194 | { 195 | if (texture) { 196 | texture->SetUVScale(scale); 197 | } 198 | } 199 | 200 | void Sprite::SetLinearFilter(bool linear) 201 | { 202 | if (texture) { 203 | texture->SetLinearFilter(linear); 204 | } 205 | } 206 | 207 | glm::vec2 const& Sprite::GetPosition() const 208 | { 209 | return position; 210 | } 211 | 212 | glm::vec2 const& Sprite::GetSize() const 213 | { 214 | return size; 215 | } 216 | 217 | glm::vec2 const& Sprite::GetScaledSize() const 218 | { 219 | return scaledSize; 220 | } 221 | 222 | float Sprite::GetAngle() const 223 | { 224 | return angle; 225 | } 226 | 227 | glm::vec4 const& Sprite::GetColor() const 228 | { 229 | return color; 230 | } 231 | 232 | glm::vec2 Sprite::GetForwardVector() const 233 | { 234 | glm::vec2 forwardVector; 235 | forwardVector.x = sin(glm::radians(angle)); 236 | forwardVector.y = -cos(glm::radians(angle)); 237 | return glm::normalize(forwardVector); 238 | } 239 | 240 | void Sprite::Draw(Gfx* gfx) 241 | { 242 | // No need to draw if the sprite is not visible 243 | if (!visible) { 244 | return; 245 | } 246 | 247 | // Set model matrix 248 | gfx->SetModel(model); 249 | 250 | // draw the sprite 251 | if (texture) { 252 | gfx->Draw(texture, textureVertices, 6, color); 253 | } else { 254 | gfx->Draw(nullptr, colorVertices, 6, color); 255 | } 256 | } 257 | 258 | // Update the model matrix 259 | void Sprite::UpdateModel() 260 | { 261 | // Reset model matrix 262 | model = glm::mat4(1.0f); 263 | 264 | // Setup positions 265 | model = glm::translate(model, glm::vec3(position, 0.0f)); 266 | 267 | // If we're centered move the coords upwards so the position is the center 268 | if (centered) { 269 | model = glm::translate(model, glm::vec3(-0.5f * scaledSize.x, -0.5f * scaledSize.y, 0.0f)); 270 | } 271 | 272 | // Center coords and rotate 273 | model = glm::translate(model, glm::vec3(0.5f * scaledSize.x, 0.5f * scaledSize.y, 0.0f)); 274 | model = glm::rotate(model, glm::radians(angle), glm::vec3(0.0f, 0.0f, 1.0f)); 275 | model = glm::translate(model, glm::vec3(-0.5f * scaledSize.x, -0.5f * scaledSize.y, 0.0f)); 276 | 277 | // Scale 278 | model = glm::scale(model, glm::vec3(scaledSize, 1.0f)); 279 | } 280 | -------------------------------------------------------------------------------- /source/Sprite.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Gfx.hpp" 4 | 5 | class Sprite { 6 | public: 7 | static Sprite* FromPNG(const void* data, uint32_t size); 8 | 9 | public: 10 | Sprite(glm::vec2 position = glm::vec2(), glm::vec2 size = glm::vec2(), float angle = 0.0f, glm::vec4 color = glm::vec4(1.0f)); 11 | Sprite(Gfx::Texture* texture, glm::vec2 position = glm::vec2(), float angle = 0.0f, glm::vec4 color = glm::vec4(1.0f)); 12 | virtual ~Sprite(); 13 | 14 | void SetTexture(Gfx::Texture* texture, bool updateSize = false); 15 | 16 | void SetPosition(glm::vec2 pos); 17 | 18 | void SetSize(glm::vec2 size); 19 | 20 | void SetScale(glm::vec2 scale); 21 | 22 | void SetAngle(float angle); 23 | 24 | void SetColor(glm::vec4 color); 25 | 26 | void SetCentered(bool centered); 27 | 28 | void SetVisible(bool visible); 29 | 30 | void SetUVOffset(glm::vec2 off); 31 | 32 | void SetUVScale(glm::vec2 scale); 33 | 34 | void SetLinearFilter(bool linear); 35 | 36 | glm::vec2 const& GetPosition() const; 37 | 38 | glm::vec2 const& GetSize() const; 39 | 40 | glm::vec2 const& GetScaledSize() const; 41 | 42 | float GetAngle() const; 43 | 44 | glm::vec4 const& GetColor() const; 45 | 46 | glm::vec2 GetForwardVector() const; 47 | 48 | virtual void Draw(Gfx* gfx); 49 | 50 | protected: 51 | void UpdateModel(); 52 | 53 | bool deleteTexture; 54 | Gfx::Texture* texture; 55 | 56 | glm::mat4 model; 57 | 58 | glm::vec2 position; 59 | glm::vec2 scaledSize; 60 | glm::vec2 size; 61 | glm::vec2 scale; 62 | float angle; 63 | glm::vec4 color; 64 | bool centered; 65 | bool visible; 66 | }; 67 | -------------------------------------------------------------------------------- /source/Text.cpp: -------------------------------------------------------------------------------- 1 | #include "Text.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | #include FT_FREETYPE_H 10 | 11 | static FT_Library ft_lib = nullptr; 12 | static FT_Face ft_face = nullptr; 13 | 14 | void Text::InitializeFont() 15 | { 16 | // Initialize freetype 17 | FT_Init_FreeType(&ft_lib); 18 | 19 | // Load the system font 20 | void *font = nullptr; 21 | uint32_t size = 0; 22 | OSGetSharedData(OS_SHAREDDATATYPE_FONT_STANDARD, 0, &font, &size); 23 | 24 | // Create face 25 | if (font && size) { 26 | FT_New_Memory_Face(ft_lib, (FT_Byte*) font, size, 0, &ft_face); 27 | } 28 | } 29 | 30 | void Text::DeinitializeFont() 31 | { 32 | // Finish face and lib 33 | FT_Done_Face(ft_face); 34 | FT_Done_FreeType(ft_lib); 35 | } 36 | 37 | Text::Text(std::string text, uint32_t textSize, glm::vec2 pos, glm::vec2 scale, float angle, glm::vec4 color) : 38 | Sprite(pos, glm::vec2(), angle, color), 39 | texture(nullptr) 40 | { 41 | this->textSize = textSize; 42 | SetText(text); 43 | } 44 | 45 | Text::~Text() 46 | { 47 | texture->Delete(); 48 | } 49 | 50 | void Text::SetText(std::string text) 51 | { 52 | // Convert the multi-byte string to a wstring 53 | static std::wstring_convert> converter; 54 | std::wstring wtext = converter.from_bytes(text); 55 | 56 | // Only redraw if the text actually changed 57 | if (this->text != wtext) { 58 | this->text = wtext; 59 | 60 | RedrawTexture(); 61 | } 62 | } 63 | 64 | void Text::SetTextSize(uint32_t textSize) 65 | { 66 | // Only redraw if the textSize actually changed 67 | if (this->textSize != textSize) { 68 | this->textSize = textSize; 69 | 70 | RedrawTexture(); 71 | } 72 | } 73 | 74 | void Text::RedrawTexture() 75 | { 76 | // Set the wanted text size 77 | FT_Set_Pixel_Sizes(ft_face, 0, textSize); 78 | 79 | // Calculate the necessary size for the texture 80 | const uint32_t face_height = ft_face->size->metrics.height >> 6; 81 | 82 | uint32_t width = 0; 83 | bounds = glm::uvec2(0, face_height); 84 | FT_GlyphSlot slot = ft_face->glyph; 85 | for (const wchar_t charcode : text) { 86 | if (charcode == '\n') { 87 | if (width > bounds.x) { 88 | bounds.x = width; 89 | width = 0; 90 | } 91 | bounds.y += face_height; 92 | continue; 93 | } 94 | 95 | FT_Load_Glyph(ft_face, FT_Get_Char_Index(ft_face, charcode), FT_LOAD_BITMAP_METRICS_ONLY); 96 | 97 | width += slot->advance.x >> 6; 98 | } 99 | 100 | if (width > bounds.x) { 101 | bounds.x = width; 102 | } 103 | 104 | // Add some extra height for the bottom bearing 105 | bounds.y += (ft_face->bbox.yMax - ft_face->bbox.yMin) >> 6; 106 | 107 | if (texture) { 108 | if (texture->GetSize() != bounds) { 109 | // Re-create the already existing texture if it doesn't match the wanted size 110 | texture->Delete(); 111 | texture = Gfx::NewTexture(bounds); 112 | } else { 113 | // Clear the already existing texture 114 | void* data = texture->Lock(); 115 | memset(data, 0, texture->GetSize().y * texture->GetPitch() * 4); 116 | texture->Unlock(); 117 | } 118 | } else { 119 | // Allocate the texture 120 | texture = Gfx::NewTexture(bounds); 121 | } 122 | 123 | if (!texture) { 124 | return; 125 | } 126 | 127 | // Get pitch and lock the texture 128 | uint32_t pitch = texture->GetPitch(); 129 | uint8_t* pixels = (uint8_t*) texture->Lock(); 130 | 131 | // Render the glyphs into the texture 132 | FT_Vector pen = { 0, 0 }; 133 | for (const wchar_t charcode : text) { 134 | if (charcode == '\n') { 135 | pen.x = 0; 136 | pen.y += ft_face->size->metrics.height >> 6; 137 | continue; 138 | } 139 | 140 | FT_Load_Glyph(ft_face, FT_Get_Char_Index(ft_face, charcode), FT_LOAD_DEFAULT); 141 | FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL); 142 | 143 | const FT_Int x = pen.x + slot->bitmap_left; 144 | const FT_Int y = (pen.y - slot->bitmap_top) + face_height; 145 | const FT_Int x_max = x + slot->bitmap.width; 146 | const FT_Int y_max = y + slot->bitmap.rows; 147 | 148 | for (FT_Int i = x, p = 0; i < x_max; ++i, ++p) { 149 | for (FT_Int j = y, q = 0; j < y_max; ++j, ++q) { 150 | if (i < 0 || j < 0 || i >= (FT_Int) bounds.x || j >= (FT_Int) bounds.y) { 151 | continue; 152 | } 153 | 154 | uint32_t offset = (j * pitch + i) * 4; 155 | pixels[offset ] = 255; 156 | pixels[offset + 1] = 255; 157 | pixels[offset + 2] = 255; 158 | pixels[offset + 3] = slot->bitmap.buffer[q * slot->bitmap.pitch + p]; 159 | } 160 | } 161 | 162 | pen.x += slot->advance.x >> 6; 163 | } 164 | 165 | // Unlock the finished texture 166 | texture->Unlock(); 167 | 168 | // Set the texture and scale of the underlying sprite 169 | SetTexture(texture, true); 170 | SetScale(scale); 171 | } 172 | -------------------------------------------------------------------------------- /source/Text.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Gfx.hpp" 4 | #include "Sprite.hpp" 5 | 6 | #include 7 | 8 | class Text : public Sprite { 9 | public: 10 | static void InitializeFont(); 11 | static void DeinitializeFont(); 12 | 13 | public: 14 | Text(std::string text, uint32_t textSize = 24, glm::vec2 pos = glm::vec2(), glm::vec2 scale = glm::vec2(1.0f), float angle = 0.0f, glm::vec4 color = glm::vec4(1.0f)); 15 | virtual ~Text(); 16 | 17 | void SetText(std::string text); 18 | 19 | void SetTextSize(uint32_t textSize); 20 | 21 | private: 22 | void RedrawTexture(); 23 | 24 | glm::uvec2 bounds; 25 | Gfx::Texture* texture; 26 | 27 | std::wstring text; 28 | uint32_t textSize; 29 | }; 30 | -------------------------------------------------------------------------------- /source/Utils.cpp: -------------------------------------------------------------------------------- 1 | #include "Utils.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #define HBL_TITLE_ID 0x0005000013374842 7 | #define MII_MAKER_JPN_TITLE_ID 0x000500101004A000 8 | #define MII_MAKER_USA_TITLE_ID 0x000500101004A100 9 | #define MII_MAKER_EUR_TITLE_ID 0x000500101004A200 10 | 11 | bool RunningFromHBL() 12 | { 13 | uint64_t titleID = OSGetTitleID(); 14 | 15 | return titleID == HBL_TITLE_ID || 16 | titleID == MII_MAKER_JPN_TITLE_ID || 17 | titleID == MII_MAKER_USA_TITLE_ID || 18 | titleID == MII_MAKER_EUR_TITLE_ID; 19 | } 20 | 21 | float frand(float min, float max) 22 | { 23 | return min + (float) rand() / ((float) RAND_MAX / (max - min)); 24 | } 25 | -------------------------------------------------------------------------------- /source/Utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define SCALE(x, oldMin, oldMax, targetMin, targetMax) \ 4 | (((x - oldMin) / (oldMax - oldMin)) * (targetMax - targetMin) + targetMin) 5 | 6 | #define COUNTOF(x) (sizeof(x) / sizeof(x[0])) 7 | 8 | bool RunningFromHBL(); 9 | 10 | float frand(float min = 0.0f, float max = 1.0f); 11 | -------------------------------------------------------------------------------- /source/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "Gfx.hpp" 10 | #include "Text.hpp" 11 | #include "SceneMgr.hpp" 12 | 13 | static uint32_t OnForegroundAcquired(void* arg) 14 | { 15 | // Enable multi drc to allow connecting a second gamepad 16 | CCRCDCSetMultiDrc(2); 17 | 18 | return 0; 19 | } 20 | 21 | static uint32_t OnForegroundReleased(void* arg) 22 | { 23 | // Disconnect the second gamepad 24 | CCRCDCDrcState state = CCR_CDC_DRC_STATE_DISCONNECT; 25 | CCRCDCSysSetDrcState(CCR_CDC_DESTINATION_DRC1, &state); 26 | 27 | // Disable multidrc 28 | CCRCDCSetMultiDrc(1); 29 | 30 | return 0; 31 | } 32 | 33 | int main(int argc, char const* argv[]) 34 | { 35 | // Initialize ProcUI 36 | WHBProcInit(); 37 | 38 | // Seed rand, used throughout the application 39 | srand(OSGetTick()); 40 | 41 | // We'll need to call the CCR* functions while still in foreground so setup callbacks 42 | ProcUIRegisterCallback(PROCUI_CALLBACK_ACQUIRE, OnForegroundAcquired, nullptr, 100); 43 | ProcUIRegisterCallback(PROCUI_CALLBACK_RELEASE, OnForegroundReleased, nullptr, 100); 44 | 45 | // Call acquired callback since we're already in foreground 46 | OnForegroundAcquired(nullptr); 47 | 48 | // Initialize graphics 49 | Gfx gfx; 50 | gfx.Initialize(); 51 | 52 | // Initialize AX to stop current sound from playing 53 | AXInit(); 54 | 55 | // Initialize font rendering 56 | Text::InitializeFont(); 57 | 58 | // Create the scene manager 59 | SceneMgr sceneMgr; 60 | 61 | while (WHBProcIsRunning()) { 62 | // Update scene 63 | sceneMgr.Update(); 64 | 65 | // Draw TV 66 | gfx.BeginDraw(Gfx::TARGET_TV); 67 | sceneMgr.DrawScene(&gfx, Gfx::TARGET_TV); 68 | gfx.EndDraw(); 69 | 70 | // Draw DRC0 71 | gfx.BeginDraw(Gfx::TARGET_DRC0); 72 | sceneMgr.DrawScene(&gfx, Gfx::TARGET_DRC0); 73 | gfx.EndDraw(); 74 | 75 | // Draw DRC1 76 | if (GX2GetSystemDRCMode() == GX2_DRC_RENDER_MODE_DOUBLE) { 77 | gfx.BeginDraw(Gfx::TARGET_DRC1); 78 | sceneMgr.DrawScene(&gfx, Gfx::TARGET_DRC1); 79 | gfx.EndDraw(); 80 | } 81 | 82 | // Swap buffers 83 | gfx.SwapBuffers(); 84 | } 85 | 86 | // Deinit font rendering 87 | Text::DeinitializeFont(); 88 | 89 | // Deinit AX 90 | AXQuit(); 91 | 92 | // Shutdown graphics 93 | gfx.Finalize(); 94 | 95 | // Call release callback 96 | OnForegroundReleased(nullptr); 97 | 98 | // Shutdown ProcUI 99 | WHBProcShutdown(); 100 | 101 | return 0; 102 | } 103 | --------------------------------------------------------------------------------