Building a 3D Engine for the Playdate
I started The Cursed Forest in August 2025 as a personal project. I'd been lurking on the Playdate Squad Discord and kept seeing people push 3D games onto this tiny handheld, and I wanted to try my own. A few months later it ended up as the submission for my university Design Innovation module and landed an 80. This first entry is an overview of where things are: the engine, the game, and the performance story. I want to cover the individual bugs in later entries. There were a lot.
The Engine
The renderer is a software rasteriser written in C, running entirely on the Playdate's 168 MHz ARM Cortex-M7F. No GPU. The pipeline transforms meshes from world to screen space, sorts and clips triangles, and rasterises to an 8-bit grayscale buffer that gets dithered down to the display's 1-bit panel at the end.
The scene switches between two rendering strategies based on triangle count. Under 50 triangles it uses a painter's algorithm: sort back-to-front, fast memset fill, no Z-buffer. Over 50 and it switches to a Z-buffer with per-pixel depth testing. The trees and wolves aren't polygons, they're 2D billboards that rotate to face the camera, which keeps the triangle count down in a dense forest.
Everything that can be is fixed-point. 16.16 fixed-point arithmetic for horizontal interpolation in the ground renderer, Quake-style fast square root, Newton-Raphson fast reciprocal, pre-computed sin/cos lookup tables. No floating-point if I can help it.
The Game
It's a full game, not just a tech demo. The player has an inventory, stamina, health, and equipment. You chop trees and mine stone, gather berries and mushrooms, craft weapons and armour at a workbench inside a cabin. You fight wolves and spiders with a melee weapon or a charged bow. Kill 15 wolves and the Alpha Wolf boss spawns. Die and you hit the death screen. Beat the game and you hit the victory screen.
Enemy AI runs on a state machine (IDLE, WANDER, CHASE, ATTACK, RETREAT, COOLDOWN, DEAD) with per-frame distance caching to avoid repeated square roots. The cabin is a GUI-based safe zone where you upgrade your crafting bench to unlock better gear.
Performance
The Playdate is tight on resources: 168 MHz, 16 MB RAM, a 1-bit 400x240 display, and only 4 KB of instruction cache on Rev A units. The grayscale buffer alone is 96 KB and the Z-buffer is another 192 KB. Everything has to fit.
A lot of the optimisation came down to cache awareness. Core rendering functions need to fit in the 4 KB instruction cache, loops are aligned on 32-byte boundaries, function alignment is forced, and dead code/data elimination is aggressive. The ground renderer unrolls to process 8 pixels per iteration. Guard-band clipping reduces the frustum from 6 planes to 2. Triangle sorting uses insertion sort under 50 triangles because it beats qsort on cache locality at those sizes.
Output is a 4x4 Bayer matrix ordered dither with 8-pixel batch processing. The final grayscale-to-1-bit step is where all the visual detail comes from on a display that technically only has black and white.
Bugs and What's Next
Plenty of things went wrong. Trees popping in and out at random, trees disappearing when you got close, trees rising out of the ground, distant trees rendering wrong, dithering scaling strangely on big sprites. Every one of them had a satisfying fix, usually because I was wrong about what was actually on screen versus what I thought was on screen. My uni evaluation goes into the full list.
The next entry will walk through the tree rendering bug progression, with the gifs I captured along the way. After that, the crafting and cabin systems, the enemy AI, and the specific optimisations I had to make to hit 45 FPS on real hardware.