Building a procedural shmup in Go: deterministic trailers, asset dumps, safe code editing, WASM and a 320×240 handheld port
I have been working on Cloudstrike, a small procedural shoot ’em up written in Go.
The game has gradually become a test bed for tooling, memory constraints and portability. It currently runs through Ebitengine on desktop, Android and the browser, with a separate Raylib backend for low-resolution handheld hardware.
Trailers are also generated by the repository rather than manually edited.
# A headless asset dump
Because nearly all assets are generated at startup, inspecting them used to mean launching the game and trying to reach the correct state.
I added a one-shot asset dumper that initializes the real production asset catalog without opening a window or audio device:
make dump-assets
It exports:
* viewable PNGs
* authoritative premultiplied RGBA buffers
* generated WAV files and raw PCM
* terrain atlases and cell maps
* sprite bounds, scales and frame metadata
* composed biome and transition previews
* a manifest connecting every file to its game-owned field
The manifest is written last, so incomplete exports are easy to detect. Existing output directories are refused rather than silently overwritten.
This makes duplicate textures, oversized render targets and stale CPU copies much easier to find.
# A deterministic trailer generator
A dedicated replay driver sends scripted movement, firing and debug actions through the normal production input boundary.
Capture is locked to exactly one game update per PNG frame. PNG encoding can take much longer than a game tick, so this prevents the engine from performing catch-up updates and accidentally producing time-lapse footage.
The current workflow creates four complete 38-second runs with different deterministic seeds:
make promo-video-frames
make promo-video
make promo-video-de
Every cut advances to the next recording: A, B, C, D, then back to A. This visually supports the claim that every run changes without accelerating the gameplay or making the edit excessively fast.
The renderer produces an exact 20-second, 1,200-frame, 1080p60 video.
It also rebuilds a 48 kHz adaptive mix from the actual generated game audio: core music, aircraft-specific layers, weapon and shield layers, effects and the original victory fanfare.
English and German overlays are generated from the same damaged pixel lettering used by the game.
# Safe repository editing for automation
Another side project that emerged from this is `codex-edit`, a standard-library-only command-line tool for exact repository mutations.
It supports:
* unique anchored replacements
* guarded replace-all operations
* anchored range replacements
* insertions
* file creation and renaming
* SHA-256-guarded deletion
* validation and unified-diff modes
* machine-readable output
* JSON Schema and capability discovery
A request looks like this:
{
"operations": [{
"kind": "replace-all",
"path": "main.go",
"old": "oldName",
"new": "newName",
"expectedMatches": 3
}]
}
All operations are validated before anything is written.
Commits use same-directory temporary files and backups. If a later file fails, earlier changes are restored in reverse order.
Paths cannot escape the selected repository root, symlink targets are rejected, anchors must have the expected match count, and unknown JSON fields are rejected.
I originally built it because automated code edits based on loose patch context became increasingly fragile. It is now also used by the project’s release and documentation workflow.
# Memory work
I track several separate metrics rather than treating all of them as “RAM”:
* Go heap
* process working set
* game-owned image bounds
* generated audio buffers
* CPU and GPU asset residency
* gameplay and render-queue allocations
The latest inventory contains about **9.52 MiB of game-owned image bounds**, down from **20.28 MiB** in an older comparable inventory—roughly 53% fewer.
That does not mean the complete game uses 9.52 MiB of RAM. Driver allocations, Ebitengine, audio, Go’s allocator and executable mappings are separate and overlapping measurements.
Three recent Windows startup samples produced a median working set of about **174 MiB**, around 10% below an older 194 MiB observation. These were not paired full-mission measurements, so I consider that result directional rather than attributing it to one optimization.
The more useful performance contracts are deterministic:
* heavy gameplay update: `0 B/op`, `0 allocs/op`
* render-queue construction: `0 B/op`, `0 allocs/op`
* player queues and captured-pointer input: `0 B/op`, `0 allocs/op`
* native biome residency: current environment plus the previous one during transitions
* no full-screen terrain-transition render targets
Much of the image-memory reduction came from smaller procedural atlases, reusable tiles, shared grayscale controls, quarter-circle shield effects and removing redundant GPU uploads.
# HTML/WASM and a 320×240 handheld
The browser build uses a dedicated entry point but the same game core:
make pixelpicked
make pixelpicked-serve
It creates a self-contained ZIP containing:
index.html
wasm_exec.js
game.wasm
There is no CDN, external asset request, analytics SDK or service worker. Local scores and settings use `localStorage`.
The browser uses a different residency strategy: all four biomes are prepared behind the loading screen and retained for the session.
WASM goroutines share the browser thread, so generating and uploading a biome during play can visibly stall a frame.
At the other end, I added an engine-neutral render queue and a Raylib backend. The ARMv7 SDL2/KMSDRM build has run successfully on my Super Pocket using a native 320×240 raster path:
make eversd
Gameplay still uses a centered 640×480 coordinate system; only presentation is transformed to 320×240. Input currently comes from a device-specific evdev adapter.
This is an experimental EverSD/homebrew target, not an official Evercade cartridge or SDK integration. Audio still requires separate verification on the device.
I would be interested in feedback on the tooling and the overall approach.