Builder Manual Play
Builder Manual / Architecture

ENGINE / Architecture

Architecture

Init, loop, cleanup, GameState ownership, render order, and runtime flow.

Architecture


Overview

Super Mango follows a classic init → loop → cleanup pattern. A single GameState struct is the owner of every resource in the game and is passed by pointer to every function that needs to read or modify it.


Startup Sequence

main()
  ├── SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)
  ├── IMG_Init(IMG_INIT_PNG)
  ├── TTF_Init()
  ├── Mix_OpenAudio(44100, stereo, 2048 buffer)

  ├── game_init(&gs)
  │     ├── SDL_CreateWindow  → gs.window
  │     ├── SDL_CreateRenderer → gs.renderer
  │     ├── SDL_RenderSetLogicalSize(GAME_W, GAME_H)
  │     │
  │     │   ── Load all textures (engine resources) ──
  │     ├── parallax_init(&gs.parallax, gs.renderer)  (multi-layer background)
  │     ├── IMG_LoadTexture → gs.textures.floor_tile (grass_tileset.png)
  │     ├── IMG_LoadTexture → gs.textures.platform   (grass_platform.png)
  │     ├── water_init(&gs.water, gs.renderer)      (water.png)
  │     ├── IMG_LoadTexture → gs.textures.*         (entities, hazards, collectibles, surfaces)
  │     ├── level resource reload → parallax/floor/water/fog/music from active LevelDef
  │     │
  │     │   ── Load all sound effects ──
  │     ├── Mix_LoadWAV     → gs.audio.*           (jump, coin, hit, spring, axe, flap, spider_attack, dive)
  │     ├── Mix_LoadMUS     → gs.audio.music       (from active LevelDef music_path)
  │     ├── Mix_PlayMusic(gs.audio.music, -1)      (loop forever at level volume)
  │     │
  │     │   ── Initialise game objects ──
  │     ├── player_init(&gs.player, gs.renderer)
  │     ├── fog_init(&gs.fog, gs.renderer)         (level fog layers, e.g. fog_1.png/fog_2.png)
  │     ├── hud_init(&gs.hud, gs.renderer)
  │     ├── if (debug_mode) debug_init(&gs.debug)
  │     ├── level_load_toml(level_path, &def)       (staged TOML parse → runtime validation → cleanup → caller assignment)
  │     ├── level_load(&gs, &def)                   (apply LevelDef to runtime GameState)
  │     ├── hearts/lives/score/score_life_next initialisation
  │     ├── SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) — lazy init, non-fatal
  │     └── scan joysticks for first connected gamepad

  ├── game_loop(&gs)          ← see Game Loop section below

  └── game_cleanup(&gs)       ← reverse init order
        ├── SDL_GameControllerClose(gs->controller)  ← if non-NULL
        ├── SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER)
        ├── hud_cleanup
        ├── fog_cleanup
        ├── player_cleanup
        ├── Mix_HaltMusic + Mix_FreeMusic (gs.audio.music)
        ├── FREE_CHUNK(gs.audio.*)
        ├── water_cleanup
        ├── DESTROY_TEX(gs.textures.*)
        ├── parallax_cleanup
        ├── SDL_DestroyRenderer
        └── SDL_DestroyWindow

  ├── Mix_CloseAudio
  ├── TTF_Quit
  ├── IMG_Quit
  └── SDL_Quit

Game Loop

The loop runs at 60 FPS, capped via VSync + a manual SDL_Delay fallback. Each frame has four distinct phases:

while (gs.running) {
  1. Delta Time   — measure ms since last frame → dt (seconds)
  2. Events       — SDL_PollEvent (quit window / pause and overlay controls)
                    SDL_CONTROLLERDEVICEADDED   — opens a newly plugged-in controller
                    SDL_CONTROLLERDEVICEREMOVED — closes and NULLs gs->controller when unplugged
                    SDL_CONTROLLERBUTTONDOWN (START) — pauses/resumes, restarts game-over, or advances completion overlay
                    SDL_CONTROLLERBUTTONDOWN (BACK)  — exits completion/game-over overlays
  3. Update       — player_handle_input → player_update (incl. bouncepad, float-platform, bridge landing)
                    → bouncepad response (animation + spring sound)
                    → spiders_update → jumping_spiders_update → birds_update → faster_birds_update
                    → fish_update → faster_fish_update → spike_blocks_update → spikes_update
                    → spike_platforms_update → circular_saws_update → axe_traps_update
                    → blue_flames_update → float_platforms_update → bridges_update
                    → spider collision → jumping_spider collision → bird collision → faster_bird collision
                    → fish collision → faster_fish collision → spike_block collision (+ push impulse)
                    → spike collision → spike_platform collision → circular_saw collision
                    → axe_trap collision → blue_flame collision → fire_flame collision
                    → sea gap fall detection (instant death)
                    → coin-player collision → star-player collision → last_star-player collision
                    → completion summary snapshot / next_phase pending state when last_star is collected
                    → heart/lives/score_life_next logic
                    → water_update → fog_update → bouncepads_update (small, medium, high)
                    → debug_update (if --debug)
  4. Render       — clear → parallax background → platforms → floor tiles
                    → float platforms → spike rows → spike platforms → bridges
                    → bouncepads (medium, small, high) → rails
                    → vines → ladders → ropes → coins → yellow stars → last star
                    → blue flames → fire flames → fish → faster fish → water
                    → spike blocks → axe traps → circular saws
                    → spiders → jumping spiders → birds → faster birds
                    → player → fog → hud
                    → debug overlay (if --debug) → pause/game-over/completion overlay → present
}

Delta Time

Uint64 now = SDL_GetTicks64();
float  dt  = (float)(now - prev) / 1000.0f;
prev = now;

All velocities are expressed in pixels per second. Multiplying by dt (seconds) gives the correct displacement per frame regardless of the actual frame rate.

Render Order (back to front)

LayerWhatHow
1BackgroundPer-level background_layers from assets/sprites/backgrounds/, tiled horizontally with each layer’s configured scroll speed
2Platformsactive level platform tile, 9-slice tiled pillar stacks (drawn before floor so pillars sink into ground)
3Flooractive level floor tile tiled across world width at FLOOR_Y, with floor-gap openings
4Float platformsfloat_platform.png 3-slice hovering surfaces (static, crumble, rail modes)
5Spike rowsspike.png ground-level spike strips on the floor surface
6Spike platformsspike_platform.png elevated spike hazard surfaces
7Bridgesbridge.png tiled crumble walkways
8Bouncepads (medium)bouncepad_medium.png standard-launch spring pads
9Bouncepads (small)bouncepad_small.png low-launch spring pads
10Bouncepads (high)bouncepad_high.png high-launch spring pads
11Railsrail.png bitmask tile tracks for spike blocks and float platforms
12Vinesvine_green.png / vine_brown.png climbable plant decorations hanging from platforms
13Laddersladder.png climbable ladder structures
14Ropesrope.png climbable rope segments
15Coinscoin.png collectible sprites drawn on top of platforms
16Yellow starsstar_yellow.png collectible star pickups
17Last starend-of-level star collectible (uses HUD star sprite)
18Blue/fire flamesblue_flame.png / fire_flame.png animated flame hazards erupting from floor gaps
19Fishfish.png animated jumping enemies, drawn before water for submerged look
20Faster fishfaster_fish.png fast aggressive jumping fish enemies
21Waterwater.png animated scrolling strip at the bottom
22Spike blocksspike_block.png rotating rail-riding hazards
23Axe trapsaxe_trap.png swinging axe hazards
24Circular sawscircular_saw.png spinning blade hazards
25Spidersspider.png animated ground patrol enemies
26Jumping spidersjumping_spider.png animated jumping patrol enemies
27Birdsbird.png slow sine-wave sky patrol enemies
28Faster birdsfaster_bird.png fast aggressive sky patrol enemies
29PlayerAnimated sprite sheet, drawn on top of environment
30FogPer-level fog_layers from assets/sprites/foregrounds/ (for example fog_1.png, fog_2.png, fog_fire_1.png, fog_fire_2.png, smoke.png)
31HUDhud_render: hearts, lives, score — always drawn on top
32Debugdebug_render: FPS counter, collision boxes, event log — when --debug active

Note: Per-level visual layers are split by role: background_layers feed the parallax renderer, foreground_layers select the water/lava foreground strip texture, and fog_layers feed the atmospheric fog system. Fog renders before the HUD so hearts/lives/score remain legible.

Level Completion Flow

Collecting last_star calls game_complete_level(). The game snapshots elapsed time, coins collected, total coins, and the resolved next_phase path (if any), then shows a completion overlay. While the overlay is active, gameplay update pauses; pressing Enter, Space, or controller Start calls game_load_next_phase() when next_phase is configured, otherwise it exits the run. Esc or controller Back exits the overlay/run without advancing.

Pause Overlay Flow

During active gameplay, Esc or controller Start toggles the player pause reason through the overlay helper in src/core/game_overlay.c. Paused frames keep rendering the last camera position, skip gameplay updates, pause music, and draw a semi-transparent pause overlay with resume hints. Enter, Space, Esc, or controller Start resumes gameplay. Window focus loss uses a separate focus pause reason, so regaining focus does not clear an intentional player pause. Completion and game-over overlays take priority over pause.

Game-Over Flow

When lethal damage consumes the final life, apply_damage() sets gs->game_over and returns without resetting the level. The shared overlay helper reports GAME_OVERLAY_GAME_OVER, so the loop blocks gameplay updates and rendering draws a game-over overlay with the final score. Enter, Space, or controller Start confirms restart through game_restart_after_game_over(), which restores level-defined lives/hearts, resets score and bonus-life threshold, and reloads the current level. Esc or controller Back exits the overlay/run.


Coordinate System

SDL’s Y-axis increases downward. The origin (0, 0) is at the top-left of the logical canvas.

(0,0) ──────────────────► x  (GAME_W = 400)

  │   LOGICAL CANVAS (400 × 300)


  y
(GAME_H = 300)
              ┌──────────────────────────────────────────┐
              │ ←──────── GAME_W (400 px) ─────────────► │
  FLOOR_Y ──►│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│
  (300-48=252)│          Grass Tileset (48px tall)        │
              └──────────────────────────────────────────┘

SDL_RenderSetLogicalSize(renderer, 400, 300) makes SDL scale this canvas 2x to fill the 800x600 OS window automatically, giving the chunky pixel-art look with no changes to game logic.


GameState Struct

Defined in game.h. The single container for every runtime resource.

typedef struct {
    SDL_Window         *window;
    SDL_Renderer       *renderer;
    SDL_GameController *controller;
    TextureResources    textures;  /* all owned SDL_Texture pointers */
    AudioResources      audio;     /* all Mix_Chunk plus Mix_Music */

    ParallaxSystem parallax;
    Player         player;
    Platform       platforms[MAX_PLATFORMS];
    Water          water;
    FogSystem      fog;
    /* fixed-size arrays + counts for every enemy, hazard, collectible, surface */

    Hud     hud;
    Camera  camera;
    int     hearts, lives, score, score_life_next;
    int     running;
    int     game_over;
    int     paused;
    unsigned int pause_reasons;
    float   checkpoint_x;
    int     debug_mode;
    int     smoke_test_frames;
    char    level_path[256];
    void   *level_def;      /* owned active LevelDef backing storage */

    LevelRuntime        runtime;
    GameRules           rules;
    GameLoopState       loop;
    GameCompletionState completion;
    DebugOverlay        debug;
} GameState;

Key design decisions:

  • Textures are grouped in TextureResources (gs->textures.*) and audio in AudioResources (gs->audio.*) so cleanup can be centralized.
  • Player is embedded by value, not a pointer. This avoids a heap allocation and keeps the struct self-contained. The same applies to Platform, Water, FogSystem, and all entity arrays.
  • Every pointer is set to NULL after freeing, making accidental double-frees safe.
  • Initialised with GameState gs = {0} so every field starts as 0 / NULL.

Error Handling Strategy

SituationAction
SDL subsystem init failure (in main)fprintf(stderr, ...) → clean up already-inited subsystems → return EXIT_FAILURE
Resource load failure (in game_init)fprintf(stderr, ...) → clean up partially-created GameState resources → return -1; the top-level runner returns EXIT_FAILURE
Sound load failure (non-fatal pattern)fprintf(stderr, ...) then continue — play is guarded by if (gs->audio.<name>)
Optional texture load failure (non-fatal)fprintf(stderr, ...) then continue — render is guarded by if (texture)

All SDL error strings are retrieved with SDL_GetError(), IMG_GetError(), or Mix_GetError() and printed to stderr.