← All work

Saving Starman

A tilt-controlled space adventure for a bare ARM microcontroller and a 128×128 screen, written in C++ with no engine, starring a Dragon capsule and a Roadster with a dead battery.

Year
2019
Made with
C++, mbed LPC1768, a 128×128 uLCD, an accelerometer, three buttons, a speaker
I did
The game on top of the course skeleton: story, sprites, maps, hash table, menus
Links
Trailer on YouTubeSource on GitHub
Yes, the class project has a trailer.
Why

It was Project 2 of ECE 2035, Georgia Tech's embedded systems class, in spring 2019. The course handed us a skeleton: pin setup, the outline of a game loop, a few interfaces. The game was up to us. On the last day of class, mine was featured as one of the best.

The theme is SpaceX's Starman, stranded in space with a dead Roadster. The only battery is in the Tesla Gigafactory on Earth, so you fly there, work through a two-door maze to get it, and bring it back. Then the Roadster reveals a secret power, blowing up asteroids, and you blast your way through the ring around Mars to land.

How it works

main() is a fixed 100 ms loop, so at most ten frames a second and one tile of movement per frame. Tilt the board far enough and you move one tile that way, if the tile is walkable.

Read inputs→Pick an action→Update→Draw→Sleep to 100 ms

The map is a hash table

The world isn't a 2D array. Each map is a chained hash table that stores only occupied cells, so empty space is just a lookup that returns NULL. XY_KEY packs x and y into one key, and the hash is key % 127.

Every MapItem carries a pointer to its own draw function, and the renderer never switches on type. It just calls item->draw(u, v). That turned every state change into a pointer swap: a floor button lights up, an asteroid explodes, Starman's tile becomes your empty capsule.

Only redraw what changed

The screen hangs off a serial link, so every pixel costs time. The view is an 11×9 window of 11-pixel tiles, and each frame compares the MapItem pointer under every tile to the one there last frame. Same pointer, no draw. Flying through empty space mostly repaints the stars as they slide past.

Sprites are 121-character string literals, one character per pixel, so the art lives in flash and is sort of visible right in the source.

Hardware
mbed LPC1768ARM Cortex-M3, 512 KB flash, 32 KB of main RAM
4D Systems uLCD, 128×128Serial, driven at 3 Mbaud
MMA8452 accelerometerI2C; tilt to move
Three pushbuttonsAction, Menu, and Omni, which walks through walls (for testing, mostly)
SpeakerPWM music, each note re-armed from a timer interrupt
What it taught me
  1. 1

    Draw less

    Over a serial link, the cheapest pixel is the one you never send.

  2. 2

    Function pointers beat switch statements

    Giving every map item its own draw function made "change how this looks" a one-line swap.

  3. 3

    A sparse world is a hash table

    Storing only occupied cells kept memory proportional to what's actually on the map.

  4. 4

    Seed with the human

    How long you sit on the title screen seeds the galaxy, no entropy hardware required.

Rough edges
  • The walkability check dereferences NULL for empty space. Address 0 on this chip is readable flash, so it reads as walkable. It works for reasons nobody should rely on.
  • Replaying calls main() again, so every playthrough nests one more frame on a 4 KB stack.
  • deleteItem logs an error on every call, successful or not.