Unnamed Tech Demo Postmortem
At the end of 2022 (or more accurately the beginning of 2023), I released a tech-demo of a single-screen top-down homebrew SNES game.
This game was originally intended for the SNESDEV Game Jam, but I did not have the free time so I had to back out (while also completely underestimating the amount of work required). From there the scope increased from a simple and short dungeon to a complete Super Nintendo game. 16 months later, I finally released the first demo.
For anyone who wants to play the demo I recommend the second tech-demo, which has sound effects and can be found on GitHub.
The main goal of this new engine (besides creating a Super Nintendo game) is to keep things simple. Development of my previous engine (untech-engine) stalled when I ran into multiple issues with the MetaSprite subsystem and the engine's complexity sapped any motivation to properly fix them.
This time I'm sticking to a Keep-It-Simple philosophy.
- Single screen rooms. No camera position or entity offscreen tests to worry about. As a bonus all collision tests can be preformed using 8-bit bytes.
- A simple fixed tile, pattern based MetaSprite subsystem.
- Fixed tile: All sprite tiles are loaded into VRAM at once and no tiles are dynamically loaded into VRAM as the game is running.
- Pattern based: The position and size of the sprites within the MetaSprite frame are declared outside of the MetaSprite frame data.1
- A simple list based MetaSprite animation subsystem.
- Each animation is a list of two byte values; The frame index and a delay value.
- To get the most of the delay value, there are multiple delay types (frame count, distance_x, distance_y, distance_xy).
- Only the MetaSprite animation is allowed to set the current MetaSprite frame.
- All entities are running an animation, even it is a non-looping animation with a single frame.
- A simple collisions subsystem
- A 16px background tile is either fully solid or not solid. No slopes or curved tiles.
- Entities have a fixed tile hitbox. No worrying about potential zip or softlock when the tile hitbox changes.
- Each MetaSprite frame has an optional hitbox and hurtbox.
- Entities can only collide with the player, a bomb explosion or a player projectile (not yet implemented).
- A simple interactive-tiles subsystem: The player can activate a single interactive-tile function when they step on or collide with a tile.
- Each room has a single room event. For example, a room with a locked-door cannot spawn enemies in a puff of smoke.
Tooling
The unnamed-tech-demo was created using the following tools:
- wiz, a high level assembly language
- aseprite: to draw images, tilesets and sprites
- GNU Image Manipulation Program: To draw the credits screens
- Tiled: to build the dungeon rooms
- Python 3:
- To convert images and JSON files to engine/SNES data (using the Python Pillow Imaging Library)
- To convert Tiled maps to room data
- To generate enums, variable aliases and function tables from JSON files
- To generate MetaSprite drawing code from a JSON file
- To generate precalculated data tables
- resources-over-usb2snes (which uses the python websocket-client and python watchdog libraries)
- Mesen-S and Mesen 2: to debug my code
- 2 Super Famicom consoles
- An sd2snes: for testing the game on real hardware
- usb2snes: an sd2snes alternative firmware (now official as of FXPAK Firmware v1.11.0)
- QUsb2Snes: to create a websocket communication link between python and usb2snes
- usb2snes-uploader: to upload the game to my sd2snes without removing the SD card
- GNU Make
What went well
Enemy behaviour
The enemy behaviour was the easiest part of the game to code. Drawing from my years of experience in snesdev, I knew my limits and set my expectations accordingly. Enemy behaviour was initially planned on pen & paper, converted to a state machine (something I also have experience with), coded in wiz and tested thoroughly.
Quite of few of wiz's features helped simplify the development of the enemy behaviour.
- Variable aliases allowed me to give descriptive names to entity variables.
- Function aliases and namespacing allowed me to specifically declare the engine functions that are intended to be called in the "process entities" context. (Link to the entity function aliases, improved upon after the tech demo)
- Forward declared inline functions (macros) allowed me to place an inlined set-state function directly above the non-inlined process-state function, improving code legibility.
- Enums helped improve type safety. Not only do they give names to states and parameters, but wiz's type system generates a compile error if I accidentally mix-up or forget my enums (unless casting is involved).
Skeletons
The skeletons are actually two different entities. A skeleton
entity that patrols forwards and
backwards and a swarming
entity that walks towards the player.
The skeleton
entity preforms a fixed-angle, parameterised-length vision cone test if it can see
the player. When the player is inside the vision cone, the skeleton
entity's process()
function
(and thus its entity-type and behaviour) is changed to swarming
.

skeleton
and crossbow_man
enemies.The swarming
entity walks towards a target position. To prevent the swarming
enemies from
occupying the same spot and overlapping each other they target a random position near the player
instead of at the player. I also limited the number of skeletons in each room to reduce the
probability of overlapping swarming
entities.

Skulls
The floating skulls were the only enemy that was not coded as a state machine. They simply changed direction when the collided with something, speeding up whenever they collided with the player's sword.
Crossbow men
Tweaking the timing for the crossbowmen was difficult. If the delay between spotting the player and shooting their bolt was too short they killed me too easily. On the other hand, if the delay was too long I could easily defeat the crossbowmen before they had a chance to attack me.
My main problem was that I was using the crossbowmen for two different purposes, as an enemy to fight and way of creating projectiles for the player to dodge. I feel like I can improve this in the next demo by having multiple crossbowmen entities, each with different timings and wearing different clothes/medals/helmets.
Slimes
I gave the slimes random crawling behaviour because I wanted them to a distinctly unique behaviour compared to the other enemies. They did not turn out as well as I hoped and if I had more time I would have given them more interesting behaviour.
Rook Guards
The original idea for the game (back when it was intended for the SNESDEV Game Jam) was a small maze. The player would find a crystal, which contained the game's map and would allow the player to find the exit. I needed locks and doors felt boring, enter the rook guards.
The rooks are indestructible to your sword and are impassable (they set the tile underneath them solid). To defeat them you needed to find a large bomb, transport it across multiple rooms while also avoiding enemies (as you cannot attack a hold a bomb at the same time)2 and drop the bomb in-front of the rook.
The bomb challenge I added to the tech-demo was enjoyable to make and play. I will defiantly add more transport-the-thinggy challenges to future demos and games.
Room transitions
The room transitions were the most complicated part of the engine. Not only did I have to load a new room (and room entities), I also had to keep track of and manage a surprising number of states and resources.
The room transition code is the intersection of 12 different subsystems, all requiring initialization at various points in the room transition process. It may have taken a few rewrites, but I managed to get all of the different subsystems initialized at the right time and in the correct order.
Room transitions are also responsible for setting up the PPU and loading graphics into VRAM. If required (ie, the previous screen was the title screen) a fade-to-black will disable the screen, the new room is loaded, the graphical subsystems are initialized, tiles are loaded into VRAM, a fade-in will enable the screen and the gameloop starts.
One unused feature of the room transition code is the ability to rollback a room transition if the player gets stuck in the new room. This took a while to get right. Since I had a lot of unused Work-RAM, a backup of the entire room state (player, enemies, room events, room tiles, etc) is made before the room-transition starts. After the transition, a player-tile collision test occurs. If the player is stuck, the backup is restored and a new room-transition starts.
The room transition rollback code is incomplete and still needs work. It is possible for the player to get stuck in an infinite room-transition loop, softlocking the game.
Doorways are ladders
When I was adding doors to the engine I ran into two problems:
- With a 16x16 px collision grid, a north facing door required 3x2 MetaTiles. With 2 states per doorway (open and locked) and 4 doorway orientations, a lot of wasted MetaTiles were required.
- Enemies can walk into doorways and possibly get stuck in them.
I took inspiration from how ladders are implemented in most platformers. All doors are solid. When the player collides into an open doorway it will enter the WALK_THROUGH_DOORWAY state. WALK_THROUGH_DOORWAY has no tile-collision and independent player-movement code. Walking through a doorway will nudge the player towards the centre of the doorway and will only allow forward or backwards movement. When the player is no-longer inside solid tile it will return to the WALKING state.
I also added an ATTACKING_IN_DOORWAY state so the player can hurt any enemies that ended up close to the door. To prevent the player from cheesing an enemy (where the player can hurt the enemy but the enemy can not hurt the player) the player moves forward a single pixel when attacking in a doorway.
The leftwards and rightwards doorways had another problem. If the player was holding a bomb in a doorway, the bomb would clip through the ceiling and doorframe. Simple solution, the player cannot walk through a doorway while holding an object. As a bonus, the bomb cannot be taken out of the designated bomb-puzzle area.
Room Events
Each room has a single room event. Room events contain two function pointers, init()
and
process()
. init()
is called after the room and graphics are loaded into memory, it is
responsible for spawning room entities and changing room tiles before the room-transition starts.
process()
is called once per frame. 4 bytes in the room data are allocated for room event
parameters.
To coordinate room events between the game-code and the room compiler, I declared room events
(and their parameters) in a mappings.json
file. A series of python scripts generate wiz code
(containing room parameter variable aliases and function tables), while the room compiler (another
python script) uses mappings.json
to populate the room event data from Tiled TMX properties.
Three room parameter types are special. If a room event has an open_door
, optional_open_door
,
or locked_door
parameter, the room compiler will scan the room for door tiles and automatically
populate these parameters with the door locations.
To ensure all doors are handled by the room event, the room compiler will refuse to compile a room that:
- Has a locked door that is not attached to a room event parameter.
- Has too many open doorways when a room event has an
open_door
oroptional_open_door
parameter.
The tech demo used the following room events:
- normal
- No parameters
init()
: Spawn room entitiesprocess()
: Does nothing
- delayed_spawn
- Two
u8
frame delay parameters init()
: Starts a countdown timerprocess()
: Waits until the timer is 0 and spawns a room entity in a puff of smoke.
- Two
- locked_door
- 1 required
locked_door
and agamestate_flag
parameters init()
: Unlocks the door if it has already been unlocked.process()
: Waits until the player touches a locked door tile and has a key in their invintory. When these conditions are met, a key will be removed from the invintory and the door is unlocked.
- 1 required
- monster_doors
- 1 required
open_door
and 3optional_open_door
parameters init()
: Initializes a state machineprocess()
:- Waits until the player has exited the doorway
- Locks the doors and spawns enemies in a poof of smoke
- Waits until all enemies have been defeated
- Unlocks the doors
- 1 required
- defeat_enemies_get_key
gamestate_flag
and a key position parametersinit()
: Checks a game-state flag to see if the key has been collected. If the key is uncollected, spawn the room entities.process()
: Wait until the room enemy count is 0, then spawn a key that falls from the ceiling.
- repeatedly_spawn_enemies:
init()
: Starts a countdown timer, spawns all room entitiesprocess()
: Waits for the timer to reach 0, then spawns a room entity and restarts the timer.
Resources over usb2snes
Last year I took a break from the game to implement something I've been thinking about for many years, a communication link between my build tools and a real SNES console.
Thanks to the USB port on my SD2SNES, custom usb2snes firmware by RedGuyyyy (now official in firmware v1.11.0) and QUsb2snes I can create a python scripts that can:
- Write data to the micro SD card without removing it from the cart.
- Send a command to boot a ROM from the SD card.
- Send commands to read and write Cart-ROM and Cart-RAM
- Read a copy of Work-RAM, Video-RAM, CGRAM, OAM, PPU registers and S-CPU registers by bus-snooping3
- Send a command to reboot the console.
I created a special build of the game that did not contain any resources. Instead of loading resources from ROM, this build would write a request to Work-RAM and then spinloop. A python script (running on my PC) will read the request (via usb2snes), transfer the compiled resource data and a response message to Cart-ROM. The game code (on the console) will see the response, stop spinlooping and resume gameplay (until the next resource is required).
Thanks to python-watchdog, the script was able to detect file-system changes and automatically
recompile any changed resource. resources_over_usb2snes.py
was also able to upload new builds to
the cart and reboot the console whenever I recompiled the code.
For anyone who is interested, I've written a more detailed description of how resources-over-usb2snes works on the NESdev forums.
Game-state backups
The game-state backup was originally intended to reload a room in the event of a crash. A backup was made after every successful room transition. On power-on (or reset) the game would preform a checksum test on the backup. If the checksum is valid, the game assumes it had crashed and will restore the game-state and restart the gameloop.
While it was originally intended to prevent a loss off progress in the event of a crash, it ended up being very useful during development. I could reset the console and game would reload the room. Combined with resources-over-snes, I could quickly test the new code/graphics/rooms without having to restart the game and repeatedly replay the dungeon or temporarily edit the player's starting position.
Dungeon map layout
After learning that Donkey Kong Country 2 levels were designed with sticky notes4, I wondered if sticky notes could help me with level design.
The initial dungeon layout was designed using sticky notes and whiteboard markers. I knew the various locks, keys and challenges I wanted in the dungeon, I wasn't sure on the order or progression of the dungeon elements. Being able to quickly sketch out a design, trace the progression path with my finger and (more importantly) quickly rearrange the dungeon rooms significantly improved the initial design process.
After that, I started building the dungeon with Tiled. Thanks to my resources-over-usb2snes subsystem I was able to get into a quick build-test-iterate loop, tweaking tile and enemy placements whenever something didn't feel right. I had Tiled (map editor), Aseprite (image editor) and nvim (text editor) running on my PC on one monitor; with the game running on a Super Famicom console on a second monitor. Whenever I made a change I was happy with, I would hit save and be able to play it on my console as soon as I navigated to the correct room/scene without resetting the console (assuming no compilation errors).
If I ever needed to retest the room I was currently in, I would reset the console. The game-state backup would kick in, restarting the room; while resources-over-usb2snes would load the new map/resources/code from my PC.
In the end, I like the dungeon in the tech demo. It's nothing fancy, but it did not need to be. It just needs to showoff what my game-engine can do and succeeds with flying colours.
The Boss
I knew the demo needed a boss but had a very limited number of sprites tiles remaining. I needed a boss with a few frames a possible, preferably no animation. So, I put a crown on a rook and called it a boss.
I was also running low on time, so I reused the bomb sprites. The bombs provided an alternative method for the boss to hurt the player and a way for the player to hurt the boss (that is consistent with the player attacking the rook guards). With the benefit of hindsight I should have done a palette swap and made the boss's bombs red and the defused player bombs blue.
Again, my resources-over-usb2snes subsystem greatly simplified development. Every time I recompiled
the code, resources_over_usb2snes.py
would automatically upload the new build to my usb2snes and
reset the console. The game-state backup would reload the boss room and I would quickly test the new
boss code.
Many, many iterations later I had a boss that I was happy with. It behaviour felt like something you would find in a Super Nintendo game from the 90's.
With the boss defeated, a trinket can be collected and the dungeon is considered complete. Cue the credits.
Areas for Improvement
I ran out of MetaSprite tiles
The Super Nintendo's PPU allows for 512 8px VRAM tiles to be used for sprites. The tech demo uses a static sprite tileset where sprite tiles are loaded into VRAM at the start of the game and left unchanged. Naturally, I ran out of sprite tiles.
This severely limited the number of MetaSprite frames I could add to the game. Knowing that adding dynamic sprite tiles would take a while, I decided to focus on game-play and enemy behaviour and worry about it after the first tech demo was released.
In the end I ran out of MetaSprite tiles and a few particle effects, player animations and enemy animations were not implemented for fear of not having enough sprite tiles for the boss.

If you look at the sprite tileset, you will notice a lot of tiles are dedicated to the player. Since there is only 1 player-sprite on the screen at any given time, I do not need to dedicate a lot of VRAM to the player. Instead I could dedicate a small section of VRAM (possibly the first 3 or 4 large 16px tiles) to the player and swap-out the player tiles whenever the player's animation frame changes.
This will free up a lot of sprite tiles that will be used for new particles and enemy animations.
More player feedback is required
Since I ran out sprite tiles, I could not add player hurt sprites to the game. Instead I made a quick animation that flips the player's direction for a few frames and hoped the sudden movement provided enough feedback to the player. It did not.

The player is horizontally-flipped for a few frames while the player and the enemy is pushed away from each other.
The lack of sound effects made feedback worse. The only way to know if the player's sword hit an enemy was to wait a few frames and see if the enemy was rebounded by the sword.
I've since added sound effects to the game. Hearing a distinctive ugh sound when the player was hurt (with 3 short beeps when at low health) and enemy hurt/defeated sounds dramatically improved player feedback.
Once I've added a dynamic sprite tile system to the engine I'll have enough free tiles to add player hurt frames and player-action particles to the game.
Some tiles lack depth
Specifically these monster spawning statues.
This is a combination of three factors:
- I'm still learning pixel-art and have not figured out depth yet.
- The statues were a last-minute addition to the game.
- The game only uses a single background for the playfield.
Time and practice will hopefully improve my pixel-art skills.
As for the last factor, I am planning on adding a screen-effects subsystem to the engine. The most basic of screen-effects will be a background layer behind the playfield, which will allow me to add trees, pillars and statues the player can walk behind to the game.
Lots of crashes
Early on in development, I encountered a lot of crashes. Thanks to wiz's type system and how it handles 8 and 16 bit registers5 I had very few accidentally wrote a 16 bit value to an 8-bit variable (and vice-verca) crashes.
Most of my crashes were (in no particular order):
- Calling a function with the wrong register sizes.
- Calling a function with the wrong Data Bank register.
- Forgot to reset the accumulator/index register size at the end of a code block.
- Stack imbalance crashes (I pushed a value to the stack and forgot to pop it off the stack).
- Wrong context calls, where I (for example) called an entity-loop function in the room-event context (I'm probably using word context incorrectly here).
- Forgot to restore the
entityId
Y-Index register in an entity function. - Buffer overflows (rare, but I encountered a one or two of them).
With time, I evolved a series of calling conventions and got a feeling for the CPU register state for the different sections of the game's code. Eventually I confident enough to test new code almost exclusively on real hardware, switching to a debugging emulator when I encountered a crash I could not easily bugfix.
What's next?
I happy with the demo, considering the time constants. I've enjoyed the feedback I've received and would like to thank everyone who offered feedback, large or small.
Like all incomplete projects there is still a lot of work to be done.
Over the last 7 months, I have been working on writing my own audio driver. The audio driver is mostly complete, at stage where I can create sound-effects and music from it. I'm still working on creating a GUI for editing sound-effects/music and I still need to improve the APUIO communications link. No idea when it will done, the GUI is taking longer then expected to make.
After the audio-driver is ready for beta-testing, I'll start working on a dynamic MetaSprite tile subsystem. This will free up a lot of a lot of MetaSprite tiles and will allow me to add more player/particle/enemy sprites to the game.
I also need to go over and tweak the hitboxes and hurtboxes again. I did a recent playthrough of the game for the Homebrew Games Summer Showcase 2023 and I appear to have forgotten the hitbox locations. Some of my attacks did not hurt the enemies when I expected them to.
I'm planning on releasing a second demo at the end of the year. This demo will hopefully have NPCs, a small village, more enemies and a second dungeon.
-
There are many advantages to a pattern based MetaSprite system:
- It can save ROM space. Each frame uses 3 bytes to store the pattern and pattern offset, as opposed to 2 bytes per sprite for a flexible MetaSprite positioning system.
- It is faster if the MetaSprite patterns is drawn with code (compared to a sprite position/size list).
- Extracting MetaSprite and tile data from sprite-sheets can be easier as the developer and artist do not need to manually layout or position each sprite.
Disadvantages:
- Limited MetaSprite layouts. This is not a problem in a small-sprite top-down game but can be an issue in other games (ie, large sprite platformer or fighting games).
- Uses more code space if the MetaSprites patterns are hard-coded. Again, this is not a problem on the SNES where there is a lot Cart-ROM space.
-
The original SNESDEV Game Jam idea was that there were multiple rooks you had defeat and multiple paths from the bombs to the rooks. To progress you needed to look at the map, plan a route from the bomb to the rook and use the bomb to destroy the rook to get to the next level. ↩
-
usb2snes monitors the console's data bus for Work-RAM writes or PPU/MMIO/DMA register writes and mirrors a copy of the write to the RAM chip in the sd2snes. ↩
-
Gregg Mayles on Twitter: 2 more #DKC2is20 level designs you asked for - Animal Antics & Rambi Rumble. We did our bit to keep 3M in business! ↩
-
Wiz has separate register names for the 8-bit and 16-bit registers. For example, the 8-bit accumulator is called
a
and the 16 bit accumulator is calledaa
. Wiz on the 65816 platform requires you to tag a block of code with the memory and index register sizes. Accessing a 8-bit register in a 16-bit context (and vice-verca) will generate a compiler error. ↩