Architecture
The layers of the game, the rules that keep them apart, and how the parts talk to each other (as of 2026-11-10).
Galaxy Eggbert has exactly one game target, the CMake target GalaxyEggbertCNA (the
executable is called GalaxyEggbert). It is written directly against CNA, a C++
implementation of the XNA 4.0 API, and uses two strict modules of cna-engine as helpers.
Above them the code is layered so that most of the game's rules can be compiled and tested without a
window, a GPU or CNA at all.
The layers
+--------------------------------------------------------------+
| CNA: the XNA 4.0 API in C++ (Game loop, GraphicsDevice, | ../cna
| Input, Audio, Media, Storage; SDL3 underneath) | (sibling repo)
| cna-engine strict modules: CnaEngine::Core, ::Graphics | ../cna-engine
| (Camera, MeshBuilder, GpuMesh, UvRect) | (sibling repo)
+--------------------------------------------------------------+
|
+--------------------------------------------------------------+
| GalaxyEggbertCNA host src/GalaxyEggbertCNA/ |
| GalaxyEggbertGame : Microsoft::Xna::Framework::Game |
| phases, input capture, the tick order, SceneFrame building,|
| the frame, resume log and demos, platform glue |
+--------------------------------------------------------------+
| |
+-----------------------------+ +-----------------------------+
| Game | | Editor |
| src/GalaxyEggbert/Game/ | | src/GalaxyEggbert/Editor/ |
| BlupiController, | | WorldEditor, palette, |
| InteractionSystem, | | undo/redo, clipboard, |
| WorldRuntime, Hud, InputPad,| | CustomWorldStorage, the |
| Sound, WorldRenderer, ... | | world and galaxy screens |
+-----------------------------+ +-----------------------------+
| |
+--------------------------------------------------------------+
| Engine-independent model include/GalaxyEggbert/, |
| src/GalaxyEggbert/Worlds/, src/GalaxyEggbert/*.cpp |
| World, Chunk, Block, records, HubDoors, |
| BlockDefinitionRegistry, Def/ (the original's enums) |
+--------------------------------------------------------------+
| Layer | Where | Namespace | May use |
|---|---|---|---|
| Engine and helpers | sibling repositories cna, cna-engine |
Microsoft::Xna::Framework, CnaEngine::Graphics | SDL3, SDL3_mixer, GL/WebGL |
| Host | src/GalaxyEggbertCNA/ | GalaxyEggbert::CNA |
everything below it; the only place with platform code (Android assets, Emscripten paths, executable hashing) |
| Game | src/GalaxyEggbert/Game/ | GalaxyEggbert::Game |
the model; CNA and cna-engine where it draws, plays sound or reads devices |
| Editor | src/GalaxyEggbert/Editor/ | GalaxyEggbert::Editor |
the model and the game's registries and drawing helpers |
| Model | include/GalaxyEggbert/, src/GalaxyEggbert/Worlds/, src/GalaxyEggbert/*.cpp |
GalaxyEggbert, GalaxyEggbert::Worlds, GalaxyEggbert::Def |
the C++ standard library only |
The game layer is mixed on purpose: about 75 of its files include nothing from CNA or cna-engine
(BlupiController, WorldRuntime, SaveData, ResumeLog,
DemoReplay, Autoplay, ObjectDefinitionRegistry, ...), while the renderers,
Hud, InputPad and Sound do. The file names of the host are listed on
Project Structure; the engine side is described on
Engine Integration.
The rules that shape the code
These come from the project's own rules (CURRENT.md "Non-negotiable boundaries",
docs/handbook/01-overview.md). Most defects in the project's history came from breaking one of them.
- Faithful remake. Every gameplay feature must trace to confirmed behaviour of
Mobile Eggbert / Speedy Blupi. Where a 2D rule has no single 3D reading, the chosen
reading is recorded as an intentional 3D adaptation (
plans/audit/mechanics_3d_decisions.md, rowsM3D-NNNN). Numeric IDs (object types, block icons, sound channels) are never renumbered: they are stored in world files. - The editor exception. The in-game 3D world editor is the one approved exception: its user interface may be invented, but it must not add gameplay mechanics.
- An engine-independent model.
include/GalaxyEggbert/andsrc/GalaxyEggbert/Worlds/include nothing from CNA or cna-engine. CMake compiles them asGE_SHARED_SOURCESinto the game, the tools and the GoogleTest suite alike. InteractionSystemhas no controller, camera or graphics dependency. It includesWorldRuntime.hpp,Sound.hpp,ObjectiveHint.hppandDef/headers only, takes Blupi's position and state as plain values, and reports what happened as events and requests (below).Sound.hppbrings in CNA's audio types, so the test of it links CNA, but it never opens a window or an audio device.- Two registries are the single sources of truth.
BlockDefinitionRegistry holds one entry per block icon
(0-440) and ObjectDefinitionRegistry one per
Def::ObjectType(0-203). Rendering, collision, editor validation, animation and interactions read them and keep no parallel lists. A new behaviour starts with the registry entry (tutorial). - Determinism. The simulation steps a fixed 1/60 s, uses a recorded random seed and never reads the wall clock. Golden traces, autoplay playthroughs, mission resume and the main-menu demos all depend on it (Game Loop).
- No backend branches in game logic. A defect found in CNA or cna-engine is fixed there
and recorded in
upstream_bugs.md, not worked around here.
Main components
| Component | Source | Role |
|---|---|---|
| GalaxyEggbertGame | src/GalaxyEggbertCNA/GalaxyEggbertGame*.cpp |
The XNA Game: owns every system below, runs the phases, the tick order and the frame. |
| World, Chunk | src/GalaxyEggbert/Worlds/ |
The 100×100×100 voxel grid in 10×10×10 chunks, with a block revision counter and a change journal. |
| WorldRuntime | src/GalaxyEggbert/Game/WorldRuntime.cpp |
A loaded .vwr: the world, its moving objects (MoveObjects), the 20 Hz animation clock. |
| BlupiController | src/GalaxyEggbert/Game/BlupiController.cpp |
Blupi's movement and collision model and animation state; engine-independent. |
| InteractionSystem | src/GalaxyEggbert/Game/InteractionSystem.cpp |
Lifts, enemies, pickups, hazards, projectiles, dynamite, crates, lives and counters. |
| HubDoors | src/GalaxyEggbert/HubDoors.cpp |
The hubs' door bars, gold bars and signs, from the saved progress. |
| TerrainRenderer, TileAtlas | src/GalaxyEggbert/Game/ |
The terrain meshes built from the world and patched from its change journal. |
| WorldRenderer | src/GalaxyEggbert/Game/WorldRenderer.cpp |
Draws the 3D world from a SceneFrame (below). |
| Hud, InputPad | src/GalaxyEggbert/Game/ |
The HUD; the menus, Setup, Pause, Win/Lost screens, the on-screen pad and typed cheats. |
| Sound | src/GalaxyEggbert/Game/Sound.cpp |
The 93 sound channels with the original's volume and pitch table, heard from Blupi's place. |
| SaveData, ResumeLog, DemoReplay | src/GalaxyEggbert/Game/ |
Settings and progress, the interrupted-mission log, the demo files (Save Data & Resume). |
| Autoplay | src/GalaxyEggbert/Game/Autoplay.cpp |
The QA harness: a script decides the keys to hold each tick. |
| WorldEditor, CustomWorldStorage | src/GalaxyEggbert/Editor/ |
The in-game editor and the player's world files, galaxies and their sharing. |
How the components talk
The host owns the systems as plain members and calls them in a fixed order every tick (the full order is on Game Loop). The systems never call each other's owners back; they hand results upward:
- Values in.
BlupiController::Step()gets theWorld, the inputs as floats and booleans, and the time step.InteractionSystem::Update()gets theWorldRuntime, Blupi's position, facing and a set of flags (in a vehicle, hidden, ballooned, invincible, which powers may be granted...). - Events out.
InteractionSystem::EventsThisFrame()is a typed per-tick queue, cleared and refilled by everyUpdate():Died,BalloonTouched,BalloonPopped,SmallShakeTriggered,BigShakeTriggered,TankFired,GlueDropStarted,GlueDropped,EnemyNearMiss,ShieldGranted,PowerGranted,CloudGranted,HideGranted,InvertGranted,DynamiteTaken.BlupiControllerhas its ownEventsThisFrame()for sound cues (a turn, a head bump, a bar grabbed, a crate gripped...). - Requests out. Two events carry a payload and at most one of each survives a tick:
VoyageRequested(a pickup flying to the HUD, which needs the camera to project its end point) andDeathLockRequested. The host resolves them withResolvePendingVoyage()andResolveDeathLock(), the only code that has both the camera and the HUD. - The world as shared state. Tile reactions change blocks through the
World; the terrain notices through the block revision and the change journal (RefreshTerrainMeshIfChanged()) instead of being told.
// src/GalaxyEggbertCNA/GalaxyEggbertGameUpdate.cpp, UpdateTickEvents()
if (FindInteractionEvent(InteractionSystem::EventKind::SmallShakeTriggered))
{
cameraShake_.Trigger(CameraShakeType::Small);
}
...
if (FindInteractionEvent(InteractionSystem::EventKind::TankFired))
{
blupi_.TriggerOneShotAnim(BlupiController::AnimState::FireTank,
BlupiController::kFireTankDuration);
}
The editor follows the same pattern: WorldEditor::UpdateBrowsing() returns a request
(open a world, return to the menu, a galaxy was imported) and the host acts on it.
The render seam: SceneFrame → WorldRenderer
Since 2026-11-09 the 3D world is drawn in two steps, the seam described in renderers.md:
simulation state (WorldRuntime, BlupiController, InteractionSystem, camera)
|
| GalaxyEggbertGame::BuildSceneFrame() src/GalaxyEggbertCNA/GalaxyEggbertGameScene.cpp
v
SceneFrame -- a draw list: camera, cubes, models and poses, decals,
| sparks, billboard batches, effects (shadows, lights, particles)
| src/GalaxyEggbert/Game/SceneFrame.hpp
|
| WorldRenderer::Draw(device, frame, assets)
v
the 3D world on screen src/GalaxyEggbert/Game/WorldRenderer.cpp
then HUD, menus, editor overlays src/GalaxyEggbertCNA/GalaxyEggbertGameDraw.cpp
SceneFrameholds no device, effect or GPU mesh: geometry is XNA math and cna-engine's CPU-sideUvRectand box faces; loaded models and textures are only pointed at. Its lists keep the order in which the world has always been drawn, so blending and depth come out the same.WorldRendererdecides how: effects, per-frame meshes, device state, and the optional 3D-effects layer (fog, shadows, particles and, at higher levels, shader passes). The game lends it the loaded content each frame asWorldRenderAssets.- There is one renderer, by the project owner's decision; the "second renderer" of
renderers.mdbecame the effect levels inside this one (3D Effects & Graphics). - The split was a pure refactor: golden frames, 1728 autoplay frames of 11 missions and 28 editor frames were byte-equal to the build before it.
Details of the passes are on Rendering.
What the layering buys
VerifyBlupiMovementlinksBlupiController.cppand the shared sources with no CNA at all;VerifyGESaveData,VerifyAutoplayandVerifyDemoReplaytest engine-independent classes too; the GoogleTest targetGalaxyEggbertWorldsTestscovers the world model and the record codecs.- Python tools (
tools/vwr_inspect.py,tools/worldkit.py, the level checkers) read the same block classes throughtools/data/blocks.csv, exported from the registry byDumpBlockDefinitions; a test fails when the CSV is stale. - The same simulation runs in the desktop, web and Android builds; only the host's platform glue differs.
See Testing for the whole set.
GalaxyEggbertGame.hpp
and InteractionSystem.hpp are historical and sometimes describe states that no longer exist (an
invisible Blupi, a single-world save). Trust the code, CURRENT.md and the ledgers in
plans/audit/ over them.