Tank Zone is a proof of concept showing that an STM32F091RCT6 can render a simple 3D game on a 160x128 ST7735 TFT display. I built this as a way to try out the STM32Cube tools and FreeRTOS. It's still a work in progress - I currently have it on a breadboard but I've ordered some PCBs and plan on putting it in a 3D printed case, powered by 2 AAA batteries.
Tank Zone is similar to the 1980 arcade game Battlezone. You control a tank, shooting enemy tanks and missiles as they spawn. Enemy tanks drive towards and shoot at the player. Missiles move faster while turning towards the player. Rocks are scattered about the battlefield, which can be used to block enemy shells and missiles. A HUD at the top of the screen shows enemy locations on a radar display.
With the game's resolution and framerate, we get about 65 CPU cycles per pixel. That's enough to do a decent amount of math, but only the kind of math the CPU is good at, which doesn't include floating-point. So I used entirely fixed-point math, with lookup tables for trigonometry and reciprocals (as the CPU lacks a division instruction).
Most games render everything to a framebuffer in RAM, then transfer that buffer to the display. For this game, that would take at least 30KiB of the availa ble 32KiB of RAM, leaving no room for a depth buffer and little room for game state, FreeRTOS tasks, and temporary space to store the 3D models as they're being transformed. It would also have the downside of not being able to fill the buffer while it's being transferred to the display. That transfer takes about 27ms, meaning to run at 30FPS we'd only have 6ms for rendering. Plus, it would require packing pixel data into 12 bits per pixel, which would mean more time spent shifting and masking pixels when drawing. So instead, I divided the screen into 10 slices of 16x128 16-bit pixels, using 3 buffers to minimize time waiting for transfers to complete. In order to avoid repeating calculations across the 10 slices, I save the positions and colors of triangles in a buffer, so that to render a slice I only need to fill in the triagles' pixels.
All of this allows the game to run at a reasonable 36 FPS while leaving plenty of RAM for everything else.
The game uses a simple piezo buzzer controlled by an I/O pin for audio. The only thing that can be controlled is the duration between each toggle of the I/O pin. I was able to create somewhat passable sound effects by varying the period (time between each low to high transition) and duty cycle (time held high). These clips were captured with a very sophisticated setup of putting the buzzer on a microphone.
Engine Idling: The period is randomized every 1 ms within a range, with a very low fixed duty cycle to keep it quiet.
Engine Active: Also a randomized period, with the duty cycle varying periodically to simulate engine cycles.
Tank Shot: A 50% duty cycle square wave, with period that rapidly sweeps up (so frequency sweeps down).
Explosion: Also a 50% duty square wave whose period sweeps up, but the period is also randomized every 1 ms.
Missile Alarm: A simple square wave, with fixed period and 50% duty cycle. The engine idling sound can be heard in between beeps.
The game saves the top 5 high scores via self-flashing. Ideally I would implement wear leveling to avoid erasing the flash sector every time, since it wears out after about 10k cycles. But if a player gets a high score every 5 minutes, it would still take over 800 hours to reach 10k cycles, which is fine for a simple proof of concept.