Welcome back, Adventurer! Last time, we learned how to build our code with CMake.
This time, we’ll restructure our sprite demo to
- separate assets into their own subdirectory
- autogenerate assets and headers with CMake
At the end of this tutorial, you’ll have a nice project template you can reuse in the future. As always, the code can be found on GitHub.

Without further ado, let’s get started!
Keep Assets Separate From Code
We already saw a bit of code generation with CMake in the last tutorial. At the moment, our source and asset files live in the same directory. As your project grows, this can become unwieldy. So the first thing we’ll do is move the assets into their own subdirectory. Create a new subdirectory assets
and move both SpriteColors.pal
and Sprites.vra
into it. Create a new file called CMakeLists.txt
in the new subdirectory assets
:
# snes_cradle/assets/CMakeLists.txt
cmake_minimum_required( VERSION 3.21 FATAL_ERROR )
We’ll add commands to it in a moment. Your directory structure should look like this now:
snes_cradle/
assets/
CMakeLists.txt
SpriteColors.pal
Sprites.vra
cmake/
CMakeCA65816Compiler.cmake.in
CMakeCA65816Information.cmake
CMakeDetermineCA65816Compiler.cmake
CMakeTestCA65816Compiler.cmake
src/
CMakeLists.txt
JoypadSprite.s
CMakeLists.txt
MemoryMap.cfg
If we try to build this, it will fail with the following error messages:
$ cmake --build build
[1/2] Building CA65816 object CMakeFiles\JoypadSpriteDemo.dir\src\JoypadSprite.s.o
FAILED: CMakeFiles/JoypadSpriteDemo.dir/src/JoypadSprite.s.o
ca65.exe --cpu 65816
-s
-o CMakeFiles\JoypadSpriteDemo.dir\src\JoypadSprite.s.o
~\snes_cradle\src\JoypadSprite.s
~\snes_cradle\src\JoypadSprite.s:69: Error: Cannot open include file 'Sprites.vra': No such file or directory
~\snes_cradle\src\JoypadSprite.s:70: Error: Cannot open include file 'SpriteColors.pal': No such file or directory
ninja: build stopped: subcommand failed.
As you can see, ca65 can no longer find Sprites.vra
and SpriteColors.pal
after we moved them into assets
(The output has been edited for clarity, so your output may differ a bit). This problem can be resolved in two ways. We could change the commands in JoypadSprite.s
to include relative paths, namely, .incbin "../assets/Sprites.vra"
and .incbin "../assets/SpriteColors.pal"
, respectively. The assembler will then find the asset files with no problem. But this isn’t ideal. As the number of source and asset files grows, we’ll probably want to move them into a more granular directory structure. So each time we move a file, we would have to change every single file that references it. That’s how bugs and build errors are born.
The second option is to tell the ca65 assembler where to find include files. The documentation tells us that the --bin-include-dir
command line argument is used for that. So we need to execute
$ ca65 --cpu 65816 --bin-include-dir ../assets -s -o JoypadSprite.o JoypadSprite.s
For the source code to assemble correctly. This puts us a bit in a pickle. The ca65 assembler uses two different command line arguments for file inclusion: one for (binary) files included with .incbin
(--bin-include-dir
), and another for .include
(--include-dir
or shorter -I
).
The question we have to answer here is whether we want to use the CMake command target_include_directories
for binary (.incbin
) or source files (.include
). I’ve decided to opt for the latter, as a SNES game project will probably include a bit more source files than binary/asset files.
For binary files, we’ll instead use the CMake command target_compile_options
to add --bin-include-dir
to our build command. Let’s edit assets/CMakeLists.txt
:
# snes_cradle/assets/CMakeLists.txt
cmake_minimum_required( VERSION 3.21 FATAL_ERROR )
target_compile_options( ${PROJECT_NAME}
PRIVATE "SHELL:--bin-include-dir ${CMAKE_CURRENT_SOURCE_DIR}" )
Here we tell CMake to add --bin-include-dir
followed by the directory the CMakeLists.txt
file is located in (here, assets
) to the compiler flags for the target. Next, we need to tell CMake what to do with those flags. The SHELL
part is necessary to avoid option de-duplication. See ca65 search paths for details.
Next, we need to add our new assets
subdirectory to the main CMakeLists.txt
in the project’s root directory file:
# snes_cradle/CMakeLists.txt
cmake_minimum_required( VERSION 3.21 FATAL_ERROR )
set( MEMORY_MAP_FILE ${CMAKE_CURRENT_SOURCE_DIR}/MemoryMap.cfg )
list( APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake" )
project( JoypadSpriteDemo
LANGUAGES CA65816 )
add_executable( ${PROJECT_NAME} "" )
add_subdirectory( assets )
add_subdirectory( src )
set_target_properties( ${PROJECT_NAME}
PROPERTIES SUFFIX ".smc" )
# Check if the path to the memory map file is set
if( NOT DEFINED MEMORY_MAP_FILE )
message( FATAL_ERROR "Path to memory map file not set!" )
endif( NOT DEFINED MEMORY_MAP_FILE )
All we did here was add add_subdirectory( assets )
. Finally, we need to update the build command itself in cmake/CMakeCA65816Information.cmake
:
# snes_cradle/cmake/CMakeCA65816Information.cmake
# How to build objects
set( CMAKE_CA65816_COMPILE_OBJECT
"<CMAKE_CA65816_COMPILER> --cpu 65816 \
<FLAGS> \
-s \
-o <OBJECT> \
<SOURCE>"
)
# How to build executables
set( CMAKE_CA65816_LINK_EXECUTABLE
"ld65 -C ${CMAKE_SOURCE_DIR}/MemoryMap.cfg \
<OBJECTS> \
-o <TARGET>"
)
set( CMAKE_CA65816_INFORMATION_LOADED 1 )
We only need to add a single line again, namely, the <FLAGS>
argument. Now rerun CMake and the error message from earlier should no longer show. As always, test the ROM you just built in your emulator and verify everything still works.
Creating a Header and Source File For Assets
We’ll need to generate a header so that different source files can access the addresses where the assets are stored. We want to be able to access assets/addresses with the .include
directive. In contrast, if we included the assets with .incbin
in each source file (that accesses the assets) it could copy the whole asset data several times into the ROM! That’d be a waste of precious ROM memory. Instead, we’re going to create a header and a source file for the assets. Thankfully, cc65 makes it easy for us to create something akin to header guards.
First, we’re going to create a new source file called assets/Assets.s
:
; ----------------------------------------------------------------------------- | |
; File: Assets.s | |
; Description: Creates a segment for all assets and exports symbols to make | |
; them accessible to other parts of the project. | |
; ----------------------------------------------------------------------------- | |
;----- Export ------------------------------------------------------------------ | |
.export SpriteData | |
.export ColorData | |
;------------------------------------------------------------------------------- | |
;----- Assset Data ------------------------------------------------------------- | |
.segment "SPRITEDATA" | |
SpriteData: .incbin "Sprites.vra" | |
ColorData: .incbin "SpriteColors.pal" | |
;------------------------------------------------------------------------------- |
This file essentially moves the SPRITEDATA
segment from lines 68 through 70 of src/JoypadSprite.s
into its own file. It also adds a couple of .export
directives to make the symbols available to other parts of the code (i.e., modules). In general, all symbols are local to the files/modules they’re in. If you want to make them available to other sources/modules, you need to export them.
Next, we create the assets/Assets.inc
header file:
.ifndef ASSETS_INC | |
ASSETS_INC = 1 | |
; ----------------------------------------------------------------------------- | |
; File: Assets.inc | |
; Description: Holds address symbols for all data defined in Assets.s | |
; ----------------------------------------------------------------------------- | |
;----- Import ------------------------------------------------------------------ | |
.import SpriteData | |
.import ColorData | |
;------------------------------------------------------------------------------- | |
.endif |
This file simply imports the symbols we just exported from assets/Assets.s
into each file that includes assets/Asset.inc
. We add a header guard to protect the file from multiple inclusions (and therefore causing symbol redefined errors).
Since there’s a new file that needs assembling, we also update assets/CMakeLists.txt
:
# snes_cradle/assets/CMakeLists.txt
cmake_minimum_required( VERSION 3.21 FATAL_ERROR )
target_compile_options( ${PROJECT_NAME}
PRIVATE "SHELL:-I ${CMAKE_CURRENT_SOURCE_DIR}" )
target_sources( ${PROJECT_NAME}
PRIVATE Assets.s )
Note that we replaced some options in target_compile_options
. This is about --bin-include-dir
VS --include-dir
we talked about above.
Finally, we update src/JoypadSprite.s
. Just replace lines 68 through 70 with `.include “Assets.inc”. Don’t forget to rerun CMake and test your ROM in an emulator.
Your directory structure should now look like this:
snes_cradle/
assets/
Assets.inc
Assets.s
CMakeLists.txt
SpriteColors.pal
Sprites.vra
cmake/
CMakeCA65816Compiler.cmake.in
CMakeCA65816Information.cmake
CMakeDetermineCA65816Compiler.cmake
CMakeTestCA65816Compiler.cmake
src/
CMakeLists.txt
JoypadSprite.s
CMakeLists.txt
MemoryMap.cfg
I chose to put all assets into a single header for convenience. But you can be as granular as you want. For example, it would probably be useful to be able to access your main character’s and enemy graphics separately. Just create a separate header for each (whether the graphic data itself goes into a separate module, depends on your ROM’s memory layout).
Refactoring JoypadSprite.s
Finally, we’re going to split up our main source file into smaller chunks. Several parts of the main source make use of the constants defined at the beginning of src/JoypadSprite.s
. So let’s split those off for easy access. Create a new subdirectory src/common
with three new header files in it:
.ifndef GAME_CONSTANTS_INC | |
GAME_CONSTANTS_INC = 1 | |
; ----------------------------------------------------------------------------- | |
; File: GameConstants.inc | |
; Description: Constant values used by this demo | |
; ----------------------------------------------------------------------------- | |
;----- Game Constants ---------------------------------------------------------- | |
; we use these constants to check for collisions with the screen boundaries | |
SCREEN_LEFT = $00 ; left screen boundary = 0 | |
SCREEN_RIGHT = $ff ; right screen boundary = 255 | |
SCREEN_TOP = $00 ; top screen boundary = 0 | |
SCREEN_BOTTOM = $df ; bottom screen boundary = 223 | |
; a simple constant to define the sprite movement speed | |
SPRITE_SPEED = $02 ; the sprites will move 2 pixel per frame | |
; this makes the code a bit more readable | |
SPRITE_SIZE = $08 ; sprites are 8 by 8 pixel | |
OAMMIRROR_SIZE = $0220 ; OAMRAM can hold data for 128 sprites, 4 bytes each | |
; constants to use as masks | |
UP_BUTTON = $0800 | |
DOWN_BUTTON = $0400 | |
LEFT_BUTTON = $0200 | |
RIGHT_BUTTON = $0100 | |
;------------------------------------------------------------------------------- | |
.endif |
.ifndef MEMORY_MAP_WRAM_INC | |
MEMORY_MAP_WRAM_INC = 1 | |
; ----------------------------------------------------------------------------- | |
; File: MemoryMapWRAM.inc | |
; Description: Symbols representing addresses in WRAM | |
; ----------------------------------------------------------------------------- | |
;----- Memory Map WRAM --------------------------------------------------------- | |
HOR_SPEED = $0300 ; the horizontal speed | |
VER_SPEED = $0301 ; the vertical speed | |
JOYPAD1 = $0302 ; data read from joypad 1 | |
JOYTRIGGER1 = $0304 ; trigger read from joypad 1 | |
JOYHELD1 = $0306 ; held buttons read from joypad 1 | |
OAMMIRROR = $0400 ; location of OAMRAM mirror in WRAM | |
;------------------------------------------------------------------------------- | |
.endif |
.ifndef REGISTERS_INC | |
REGISTERS_INC = 1 | |
; ----------------------------------------------------------------------------- | |
; File: Registers.inc | |
; Description: Symbols representing the memory mapped registers of the SNES | |
; ----------------------------------------------------------------------------- | |
;----- Aliases/Labels ---------------------------------------------------------- | |
; these are aliases for the Memory Mapped Registers we will use | |
INIDISP = $2100 ; inital settings for screen | |
OBJSEL = $2101 ; object size $ object data area designation | |
OAMADDL = $2102 ; address for accessing OAM | |
OAMADDH = $2103 | |
OAMDATA = $2104 ; data for OAM write | |
VMAINC = $2115 ; VRAM address increment value designation | |
VMADDL = $2116 ; address for VRAM read and write | |
VMADDH = $2117 | |
VMDATAL = $2118 ; data for VRAM write | |
VMDATAH = $2119 | |
CGADD = $2121 ; address for CGRAM read and write | |
CGDATA = $2122 ; data for CGRAM write | |
TM = $212c ; main screen designation | |
HVBJOY = $4212 ; H/V blank flags and standard controller enable flag | |
STDCNTRL1L = $4218 ; data for standard controller I | |
STDCNTRL1H = $4219 | |
NMITIMEN = $4200 ; enable flaog for v-blank | |
MDMAEN = $420b ; DMA enable register | |
RDNMI = $4210 ; read the NMI flag status | |
DMAP0 = $4300 ; DMA control register, channel 0 | |
BBAD0 = $4301 ; DMA destination register, channel 0 | |
A1T0L = $4302 ; DMA source address register low, channel 0 | |
A1T0H = $4303 ; DMA source address register high, channel 0 | |
A1T0B = $4304 ; DMA source address register bank, channel 0 | |
DAS0L = $4305 ; DMA size register low, channel 0 | |
DAS0H = $4306 ; DMA size register high, channel 0 | |
;------------------------------------------------------------------------------- | |
.endif |
As with the assets, you can get as granular with your headers as you want. I personally tend to keep my code in many separate files. As your code grows, it is a lot easier to merge several files into one later on than to split up code (several modules/subroutines/etc.)
As always, we also need a new src/common/CMakeLists.txt
:
# snes_cradle/src/common/CMakeLists.txt
cmake_minimum_required( VERSION 3.21 FATAL_ERROR )
target_compile_options( ${PROJECT_NAME}
PRIVATE "SHELL:-I ${CMAKE_CURRENT_SOURCE_DIR}" )
Modify src/CMakeLists.txt
to include add_subdirectory( common )
. Remove lines 6 through 62 in src/JoypadSprite.s
and include the newly created headers with .include
.
Now we’re going to factor out some of the subroutines related to graphics. Namely, LoadVRAM
, LoadCGRAM
, and UpdateOAMRAM
. Those functions are universal, you can use them in all kinds of projects. I choose to put them in a subdirectory called src/PPU
, where I keep all code related to the PPU/graphics.
We’re going to follow a similar pattern as we did with the assets. Create a new header-source pair src/PPU/PPU.s
and src/PPU/PPU.inc
respectively:
.ifndef PPU_INC | |
PPU_INC = 1 | |
; ----------------------------------------------------------------------------- | |
; File: PPU.inc | |
; Description: Holds address symbols for all subroutines defined in PPU.s | |
; ----------------------------------------------------------------------------- | |
;----- Import ------------------------------------------------------------------ | |
.import LoadVRAM | |
.import LoadCGRAM | |
.import UpdateOAMRAM | |
;------------------------------------------------------------------------------- | |
.endif |
; ----------------------------------------------------------------------------- | |
; File: PPU.s | |
; Description: A collection of subroutines for interacting with VRAM, CGRAM, | |
; OAMRAM, and the PPU. | |
; ----------------------------------------------------------------------------- | |
;----- Export ------------------------------------------------------------------ | |
.export LoadVRAM | |
.export LoadCGRAM | |
.export UpdateOAMRAM | |
;------------------------------------------------------------------------------- | |
;----- Assembler Directives ---------------------------------------------------- | |
.p816 ; tell the assembler this is 65816 code | |
.A8 ; set accumulator to 8-bit | |
.I16 ; set index registers to 16-bit | |
;------------------------------------------------------------------------------- | |
;----- Includes ---------------------------------------------------------------- | |
.include "Registers.inc" | |
;------------------------------------------------------------------------------- | |
.segment "CODE" | |
;------------------------------------------------------------------------------- | |
; Load sprite data into VRAM | |
; Parameters: NumBytes: .byte, SrcPointer: .addr, DestPointer: .addr | |
;------------------------------------------------------------------------------- | |
.proc LoadVRAM | |
phx ; save old stack pointer | |
; create frame pointer | |
phd ; push Direct Register to stack | |
tsc ; transfer Stack to... (via Accumulator) | |
tcd ; ...Direct Register. | |
; use constants to access arguments on stack with Direct Addressing | |
NumBytes = $07 ; number of bytes to transfer | |
SrcPointer = $08 ; source address of sprite data | |
DestPointer = $0a ; destination address in VRAM | |
; set destination address in VRAM, and address increment after writing to VRAM | |
ldx DestPointer ; load the destination pointer... | |
stx VMADDL ; ...and set VRAM address register to it | |
lda #$80 | |
sta VMAINC ; increment VRAM address by 1 when writing to VMDATAH | |
; loop through source data and transfer to VRAM | |
ldy #$0000 ; set register Y to zero, we will use Y as a loop counter and offset | |
VRAMLoop: | |
lda (SrcPointer, S), Y ; get bitplane 0/2 byte from the sprite data | |
sta VMDATAL ; write the byte in A to VRAM | |
iny ; increment counter/offset | |
lda (SrcPointer, S), Y ; get bitplane 1/3 byte from the sprite data | |
sta VMDATAH ; write the byte in A to VRAM | |
iny ; increment counter/offset | |
cpy NumBytes ; check whether we have written $04 * $20 = $80 bytes to VRAM (four sprites) | |
bcc VRAMLoop ; if X is smaller than $80, continue the loop | |
; all done | |
pld ; restore caller's frame pointer | |
plx ; restore old stack pointer | |
rts | |
.endproc | |
;------------------------------------------------------------------------------- | |
;------------------------------------------------------------------------------- | |
; Load color data into CGRAM | |
; NumBytes: .byte, SrcPointer: .byte, DestPointer: .addr | |
;------------------------------------------------------------------------------- | |
.proc LoadCGRAM | |
phx ; save old stack pointer | |
; create frame pointer | |
phd ; push Direct Register to stack | |
tsc ; transfer Stack to... (via Accumulator) | |
tcd ; ...Direct Register. | |
; use constants to access arguments on stack with Direct Addressing | |
NumBytes = $07 ; number of bytes to transfer | |
SrcPointer = $08 ; source address of sprite data | |
DestPointer = $0a ; destination address in VRAM | |
; set CGDRAM destination address | |
lda DestPointer ; get destination address | |
sta CGADD ; set CGRAM destination address | |
ldy #$0000 ; set Y to zero, use it as loop counter and offset | |
CGRAMLoop: | |
lda (SrcPointer, S), Y ; get the color low byte | |
sta CGDATA ; store it in CGRAM | |
iny ; increase counter/offset | |
lda (SrcPointer, S), Y ; get the color high byte | |
sta CGDATA ; store it in CGRAM | |
iny ; increase counter/offset | |
cpy NumBytes ; check whether 32/$20 bytes were transfered | |
bcc CGRAMLoop ; if not, continue loop | |
; all done | |
pld ; restore caller's frame pointer | |
plx ; restore old stack pointer | |
rts | |
.endproc | |
;------------------------------------------------------------------------------- | |
;------------------------------------------------------------------------------- | |
; Copies the OAMRAM mirror into OAMRAM | |
;------------------------------------------------------------------------------- | |
.proc UpdateOAMRAM | |
phx ; save old stack pointer | |
; create frame pointer | |
phd ; push Direct Register to stack | |
tsc ; transfer Stack to... (via Accumulator) | |
tcd ; ...Direct Register. | |
; use constants to access arguments on stack with Direct Addressing | |
MirrorAddr = $07 ; address of the mirror we want to copy | |
; set up DMA channel 0 to transfer data to OAMRAM | |
lda #%00000010 ; set DMA channel 0 | |
sta DMAP0 | |
lda #$04 ; set destination to OAMDATA | |
sta BBAD0 | |
ldx MirrorAddr ; get address of OAMRAM mirror | |
stx A1T0L ; set low and high byte of address | |
stz A1T0B ; set bank to zero, since the mirror is in WRAM | |
ldx #$0220 ; set the number of bytes to transfer | |
stx DAS0L | |
lda #$01 ; start DMA transfer | |
sta MDMAEN | |
; OAMRAM update is done, restore frame and stack pointer | |
pld ; restore caller's frame pointer | |
plx ; restore old stack pointer | |
rts | |
.endproc | |
;------------------------------------------------------------------------------- |
Same idea as with the assets: We put the subroutines into their own file/module and then create a header so other files/modules know where to find the subroutines. While ca65’s smart mode is pretty good, it sometimes can go astray, especially when you’re code is spread among several files. So in lines 15 and 16, I tell the assembler explicitly which register sizes to assume. Mind that this does not generate any code. The assembler will still track rep
and sep
instructions but will need some help from time to time as your project’s code grows.
You should have a good understanding of it now, so I’m going to skip the necessary updates to the CMake files. If it gets confusing, you can see the final code on Github.
As a final step, we’ll move the VECTOR
segment into its own file src/common/Vector.s
, as well as create a separate src/Game/Init.s
file:
.ifndef INIT_INC | |
INIT_INC = 1 | |
; ----------------------------------------------------------------------------- | |
; File: Init.inc | |
; Description: Holds address symbols for all subroutines defined in Init.s | |
; ----------------------------------------------------------------------------- | |
;----- Import ------------------------------------------------------------------ | |
.import InitDemo | |
;------------------------------------------------------------------------------- | |
.endif |
; ----------------------------------------------------------------------------- | |
; File: Init.s | |
; Description: Holds subroutines to initialize the demo | |
; ----------------------------------------------------------------------------- | |
;----- Export ------------------------------------------------------------------ | |
.export InitDemo | |
;------------------------------------------------------------------------------- | |
;----- Assembler Directives ---------------------------------------------------- | |
.p816 ; tell the assembler this is 65816 code | |
.a8 | |
.i16 | |
;------------------------------------------------------------------------------- | |
;----- Includes ---------------------------------------------------------------- | |
.include "Assets.inc" | |
.include "GameConstants.inc" | |
.include "MemoryMapWRAM.inc" | |
.include "PPU.inc" | |
.include "Registers.inc" | |
;------------------------------------------------------------------------------- | |
.segment "CODE" | |
;------------------------------------------------------------------------------- | |
; This initializes the demo | |
;------------------------------------------------------------------------------- | |
.proc InitDemo | |
; load sprites into VRAM | |
tsx ; save current stack pointer | |
pea $0000 ; push VRAM destination address to stack | |
pea SpriteData ; push sprite data source address to stack | |
lda #$80 ; number of bytes (128/$80) to transfer | |
pha | |
jsr LoadVRAM ; transfer sprite data to VRAM | |
txs ; restore old stack pointer | |
; load color data into CGRAM | |
tsx ; save current stack pointer | |
lda #$80 ; destination address in CGRAM | |
pha | |
pea ColorData ; color data source address | |
lda #$20 ; number of bytes (32/$20) to transfer | |
pha | |
jsr LoadCGRAM ; transfer color data into CGRAM | |
txs ; restore old stack pointer | |
; initialize OAMRAM mirror | |
ldx #$00 | |
; upper-left sprite | |
lda #(SCREEN_RIGHT/2 - SPRITE_SIZE) ; sprite 1, horizontal position | |
sta OAMMIRROR, X | |
inx ; increment index | |
lda #(SCREEN_BOTTOM/2 - SPRITE_SIZE); sprite 1, vertical position | |
sta OAMMIRROR, X | |
inx | |
lda #$00 ; sprite 1, name | |
sta OAMMIRROR, X | |
inx | |
lda #$00 ; no flip, palette 0 | |
sta OAMMIRROR, X | |
inx | |
; upper-right sprite | |
lda #(SCREEN_RIGHT/2) ; sprite 3, horizontal position | |
sta OAMMIRROR, X | |
inx ; increment index | |
lda #(SCREEN_BOTTOM/2 - SPRITE_SIZE); sprite 3, vertical position | |
sta OAMMIRROR, X | |
inx | |
lda #$01 ; sprite 3, name | |
sta OAMMIRROR, X | |
inx | |
lda #$00 ; no flip, palette 0 | |
sta OAMMIRROR, X | |
inx | |
; lower-left sprite | |
lda #(SCREEN_RIGHT/2 - SPRITE_SIZE) ; sprite 2, horizontal position | |
sta OAMMIRROR, X | |
inx ; increment index | |
lda #(SCREEN_BOTTOM/2) ; sprite 2, vertical position | |
sta OAMMIRROR, X | |
inx | |
lda #$02 ; sprite 2, name | |
sta OAMMIRROR, X | |
inx | |
lda #$00 ; no flip, palette 0 | |
sta OAMMIRROR, X | |
inx | |
; lower-right sprite | |
lda #(SCREEN_RIGHT/2) ; sprite 4, horizontal position | |
sta OAMMIRROR, X | |
inx ; increment index | |
lda #(SCREEN_BOTTOM/2) ; sprite 4, vertical position | |
sta OAMMIRROR, X | |
inx | |
lda #$03 ; sprite 4, name | |
sta OAMMIRROR, X | |
inx | |
lda #$00 ; no flip, palette 0 | |
sta OAMMIRROR, X | |
inx | |
; move the other sprites off screen | |
rep #$20 ; set A to 16-bit | |
lda #$f180 ; Y = 241, X = -128 | |
OAMLoop: | |
sta OAMMIRROR, X | |
inx | |
inx | |
cpx #(OAMMIRROR_SIZE - $20) | |
bne OAMLoop | |
; correct bit 9 of horizontal/X position, set size to 8x8 | |
lda #$5555 | |
OBJLoop: | |
sta OAMMIRROR, X | |
inx | |
inx | |
cpx #OAMMIRROR_SIZE | |
bne OBJLoop | |
sep #$20 ; set A to 8-bit | |
; correct extra OAM byte for first four sprites | |
ldx #$0200 | |
lda #$00 | |
sta OAMMIRROR, X | |
; set initial horizontal and vertical speed | |
lda #SPRITE_SPEED | |
sta HOR_SPEED | |
sta VER_SPEED | |
; make Objects visible | |
lda #$10 | |
sta TM | |
; release forced blanking, set screen to full brightness | |
lda #$0f | |
sta INIDISP | |
; enable NMI, turn on automatic joypad polling | |
lda #$81 | |
sta NMITIMEN | |
; jmp GameLoop ; all initialization is done | |
rts ; all initialization is done | |
.endproc | |
;------------------------------------------------------------------------------- |
; ----------------------------------------------------------------------------- | |
; File: Vector.s | |
; Description: Interrup and reset vectors for the 65816 CPU | |
; ----------------------------------------------------------------------------- | |
;----- Assembler Directives ---------------------------------------------------- | |
.p816 ; tell the assembler this is 65816 code | |
;------------------------------------------------------------------------------- | |
;----- Imports ----------------------------------------------------------------- | |
.import NMIHandler ; We need these two addresses so the SNES ... | |
.import ResetHandler ; ... knows where you start execution | |
;------------------------------------------------------------------------------- | |
.segment "VECTOR" | |
;------------------------------------------------------------------------------- | |
; native mode COP, BRK, ABT, | |
.addr $0000, $0000, $0000 | |
; NMI, RST, IRQ | |
.addr NMIHandler, $0000, $0000 | |
.word $0000, $0000 ; four unused bytes | |
; emulation m. COP, BRK, ABT, | |
.addr $0000, $0000, $0000 | |
; NMI, RST, IRQ | |
.addr $0000, ResetHandler, $0000 | |
;------------------------------------------------------------------------------- |
The only thing left to do, is to refactor your src/JoypadSprite.s
:
; ----------------------------------------------------------------------------- | |
; File: JoypadSprite.s | |
; Description: Displays a sprite that bounces off the edges | |
; ----------------------------------------------------------------------------- | |
;----- Export ------------------------------------------------------------------ | |
.export ResetHandler ; export the entry point of the demo | |
.export NMIHandler | |
;------------------------------------------------------------------------------- | |
;----- Assembler Directives ---------------------------------------------------- | |
.p816 ; tell the assembler this is 65816 code | |
;------------------------------------------------------------------------------- | |
;----- Includes ---------------------------------------------------------------- | |
.include "Assets.inc" | |
.include "GameConstants.inc" | |
.include "Init.inc" | |
.include "MemoryMapWRAM.inc" | |
.include "PPU.inc" | |
.include "Registers.inc" | |
;------------------------------------------------------------------------------- | |
.segment "CODE" | |
;------------------------------------------------------------------------------- | |
; This is the entry point of the demo | |
;------------------------------------------------------------------------------- | |
.proc ResetHandler | |
sei ; disable interrupts | |
clc ; clear the carry flag | |
xce ; switch the 65816 to native (16-bit mode) | |
rep #$10 ; set X and Y to 16-bit | |
sep #$20 ; set A to 8-bit | |
lda #$8f ; force v-blanking | |
sta INIDISP | |
stz NMITIMEN ; disable NMI | |
; set the stack pointer to $1fff | |
ldx #$1fff ; load X with $1fff | |
txs ; copy X to stack pointer | |
jsr InitDemo ; initialize the demo | |
jmp GameLoop ; enter main game loop | |
.endproc | |
;------------------------------------------------------------------------------- | |
;------------------------------------------------------------------------------- | |
; Executed during V-Blank | |
;------------------------------------------------------------------------------- | |
.proc GameLoop | |
wai ; wait for NMI / V-Blank | |
; read joypad 1 | |
; check whether joypad is ready | |
WaitForJoypad: | |
lda HVBJOY ; get joypad status | |
and #$01 ; check whether joypad done reading... | |
beq WaitForJoypad ; ...if not, wait a bit more | |
; first, check for newly pressed buttons since last frame | |
rep #$20 ; set A to 16-bit | |
lda STDCNTRL1L ; get new input from this frame | |
ldy JOYPAD1 ; get input from last frame | |
sta JOYPAD1 ; store new input from this frame | |
tya ; check for newly pressed buttons... | |
eor JOYPAD1 ; filter buttons that were not pressed last frame | |
and JOYPAD1 ; filter held buttons from last frame | |
sta JOYTRIGGER1 ; ...and store them | |
; second, check for buttons held from last frame | |
tya ; get input from last frame | |
and JOYPAD1 ; filter held buttons from last frame... | |
sta JOYHELD1 ; ...store them | |
; check the dpad, if any of the directional buttons where pressed or held, | |
; move the sprites accordingly | |
CheckUpButton: | |
lda #$0000 ; set A to zero | |
ora JOYTRIGGER1 ; check whether the up button was pressed this frame... | |
ora JOYHELD1 ; ...or held from last frame | |
and #UP_BUTTON | |
beq CheckUpButtonDone ; if neither has occured, move on | |
; else, move sprites up | |
ldy #$0000 ; Y is the loop counter | |
ldx #$0001 ; set offset to 1, to manipulate sprite vertical positions | |
sep #$20 ; set A to 8-bit | |
MoveSpritesUp: | |
lda OAMMIRROR, X | |
sec | |
sbc VER_SPEED | |
bcc CorrectVerticalPositionDown ; if vertical position is below zero, correct it down | |
sta OAMMIRROR, X | |
inx ; increment X by 4 | |
inx | |
inx | |
inx | |
iny | |
cpy #$0004 ; unless Y = 4, continue loop | |
bne MoveSpritesUp | |
CheckUpButtonDone: | |
rep #$20 ; set A to 16-bit | |
CheckDownButton: | |
lda #$0000 ; set A to zero | |
ora JOYTRIGGER1 ; check whether the down button was pressed this frame... | |
ora JOYHELD1 ; ...or held from last frame | |
and #DOWN_BUTTON | |
beq CheckDownButtonDone ; if neither has occured, move on | |
; else, move sprites down | |
ldy #$0000 ; Y is the loop counter | |
ldx #$0001 ; set offset to 1, to manipulate sprite vertical positions | |
sep #$20 ; set A to 8-bit | |
; check if sprites move below buttom boundry | |
lda OAMMIRROR, X | |
clc | |
adc VER_SPEED | |
cmp #(SCREEN_BOTTOM - 2 * SPRITE_SIZE) | |
bcs CorrectVerticalPositionUp | |
MoveSpritesDown: | |
lda OAMMIRROR, X | |
clc | |
adc VER_SPEED | |
sta OAMMIRROR, X | |
inx ; increment X by 4 | |
inx | |
inx | |
inx | |
iny | |
cpy #$0004 ; unless Y = 4, continue loop | |
bne MoveSpritesDown | |
CheckDownButtonDone: | |
rep #$20 ; set A to 16-bit | |
jmp CheckLeftButton ; continue input check | |
CorrectVerticalPositionDown: | |
sep #$20 ; set A to 8-bit | |
stz OAMMIRROR + 1 ; sprite 1, vertical position | |
stz OAMMIRROR + 5 ; sprite 3, vertical position | |
lda #SPRITE_SIZE | |
sta OAMMIRROR + 9 ; sprite 2, vertical position | |
sta OAMMIRROR + 13 ; sprite 4, vertical position | |
rep #$20 ; set A to 16-bit | |
jmp CheckLeftButton ; continue input check | |
CorrectVerticalPositionUp: | |
sep #$20 ; set A to 8-bit | |
lda #(SCREEN_BOTTOM - 2 * SPRITE_SIZE) | |
sta OAMMIRROR + 1 ; sprite 1, vertical position | |
sta OAMMIRROR + 5 ; sprite 3, vertical position | |
lda #(SCREEN_BOTTOM - SPRITE_SIZE) | |
sta OAMMIRROR + 9 ; sprite 2, vertical position | |
sta OAMMIRROR + 13 ; sprite 4, vertical position | |
rep #$20 ; set A to 16-bit | |
CheckLeftButton: | |
lda #$0000 ; set A to zero | |
ora JOYTRIGGER1 ; check whether the up button was pressed this frame... | |
ora JOYHELD1 ; ...or held from last frame | |
and #LEFT_BUTTON | |
beq CheckLeftButtonDone ; if neither has occured, move on | |
; else, move sprites up | |
ldy #$0000 ; Y is the loop counter | |
ldx #$0000 ; set offset to 0, to manipulate sprite horizontal positions | |
sep #$20 ; set A to 8-bit | |
MoveSpritesLeft: | |
lda OAMMIRROR, X | |
sec | |
sbc HOR_SPEED | |
bcc CorrectHorizontalPositionRight | |
sta OAMMIRROR, X | |
inx ; increment X by 4 | |
inx | |
inx | |
inx | |
iny | |
cpy #$0004 ; unless Y = 4, continue loop | |
bne MoveSpritesLeft | |
CheckLeftButtonDone: | |
rep #$20 ; set A to 16-bit | |
CheckRightButton: | |
lda #$0000 ; set A to zero | |
ora JOYTRIGGER1 ; check whether the down button was pressed this frame... | |
ora JOYHELD1 ; ...or held from last frame | |
and #RIGHT_BUTTON | |
beq CheckRightButtonDone ; if neither has occured, move on | |
; else, move sprites down | |
ldy #$0000 ; Y is the loop counter | |
ldx #$0000 ; set offset to 0, to manipulate sprite horizontal positions | |
sep #$20 ; set A to 8-bit | |
; check whether sprites move beyond right boundry | |
lda OAMMIRROR, X | |
clc | |
adc HOR_SPEED | |
cmp #(SCREEN_RIGHT - 2 * SPRITE_SIZE) | |
bcs CorrectHorizontalPositionLeft | |
MoveSpritesRight: | |
lda OAMMIRROR, X | |
clc | |
adc HOR_SPEED | |
sta OAMMIRROR, X | |
inx ; increment X by 4 | |
inx | |
inx | |
inx | |
iny | |
cpy #$0004 ; unless Y = 4, continue loop | |
bne MoveSpritesRight | |
CheckRightButtonDone: | |
rep #$20 ; set A to 16-bit | |
jmp InputDone | |
CorrectHorizontalPositionRight: | |
sep #$20 ; set A to 8-bit | |
stz OAMMIRROR + 0 ; sprite 1, horizontal position | |
stz OAMMIRROR + 8 ; sprite 2, horizontal position | |
lda #SPRITE_SIZE | |
sta OAMMIRROR + 4 ; sprite 3, horizontal position | |
sta OAMMIRROR + 12 ; sprite 4, horizontal position | |
jmp InputDone | |
CorrectHorizontalPositionLeft: | |
sep #$20 ; set A to 8-bit | |
lda #(SCREEN_RIGHT - 2 * SPRITE_SIZE) | |
sta OAMMIRROR + 0 ; sprite 1, horizontal position | |
sta OAMMIRROR + 8 ; sprite 2, horizontal position | |
lda #(SCREEN_RIGHT - SPRITE_SIZE) | |
sta OAMMIRROR + 4 ; sprite 3, horizontal position | |
sta OAMMIRROR + 12 ; sprite 4, horizontal position | |
InputDone: | |
sep #$20 ; set A back to 8-bit | |
jmp GameLoop | |
.endproc | |
;------------------------------------------------------------------------------- | |
;------------------------------------------------------------------------------- | |
; Will be called during V-Blank every frame | |
;------------------------------------------------------------------------------- | |
.proc NMIHandler | |
lda RDNMI ; read NMI status, acknowledge NMI | |
tsx ; save old stack pointer | |
pea OAMMIRROR ; push mirror address to stack | |
jsr UpdateOAMRAM ; update OAMRAM | |
txs ; restore old stack pointer | |
rti | |
.endproc | |
;------------------------------------------------------------------------------- | |
;------------------------------------------------------------------------------- | |
; Is not used in this program | |
;------------------------------------------------------------------------------- | |
.proc IRQHandler | |
; code | |
rti | |
.endproc | |
;------------------------------------------------------------------------------- |
Now it only contains your main game loop, which I think is much nicer to handle and expand this way.
Your final directory structure should now look like this:
snes_cradle/
assets/
Assets.inc
Assets.s
CMakeLists.txt
SpriteColors.pal
Sprites.vra
cmake/
CMakeCA65816Compiler.cmake.in
CMakeCA65816Information.cmake
CMakeDetermineCA65816Compiler.cmake
CMakeTestCA65816Compiler.cmake
src/
common/
CMakeLists.txt
GameConstants.inc
MemoryMapWRAM.inc
Registers.inc
Vector.s
Game/
CMakeLists.txt
Init.inc
Init.s
PPU/
CMakeLists.txt
PPU.inc
PPU.s
CMakeLists.txt
JoypadSprite.s
CMakeLists.txt
MemoryMap.cfg
Mind that is directory structure is pretty arbitrary and you can and should tailor it to your own needs. If you got lost somewhere, check out the full code here.
Conclusion
This and the last entry may seem to overly complicate things. But I’m certain that this will pay off in the long run. As of now, we have a simple project structure that can easily be expanded without it causing us headaches. There are a few things I’ve skipped for the sake of brevity. For example, it is possible to completely automate the generation of header files from sources with CMake. But I opted to keep things simple for now.
Next time, we’ll finally return to more SNES programming. What’s a game without backgrounds? Exactly, boring.
Thank you for reading and see you next time!
Links and References
- Another SNES project template by ARM9 that uses C
- The SNESKIT by Daniel Oaks includes very useful extra tools and code templates
- The full code of this tutorial