Super Mango Editor

Home

2D side-scrolling platformer written in C using SDL2 -- browser-playable via WebAssembly

Super Mango is a 2D platformer built in C11 with SDL2, designed as an educational project with well-commented source code for learning C + SDL2 game development. The game features TOML-based levels with configurable multi-screen stages, parallax backgrounds, enemies, hazards, collectibles, and delta-time physics with walk/run acceleration. It includes a standalone visual level editor and builds natively on macOS/Linux/Windows and as WebAssembly for browser play.


Quick Links

Page Description
Architecture Game loop, init/loop/cleanup pattern, GameState container, render order
Source Files Module-by-module reference for every .c / .h file
Player Module Input, physics, animation -- deep dive into player.c
Build System Makefile, compiler flags, build targets, prerequisites
Assets All sprite sheets, tilesets, and fonts in assets/
Sounds All audio files in sounds/
Constants Reference Every #define in game.h and entity headers explained
Developer Guide How to add new entities, sound effects, and features

Key Features

  • 2D side-scrolling platformer with TOML-based levels (dynamic world width via screen_count, default 1600px / 4 screens)
  • 35 render layers drawn back-to-front with configurable parallax scrolling background
  • Delta-time physics with walk/run acceleration, momentum, and friction at 60 FPS
  • Six enemy types, seven hazard types, collectibles (coins, 3 star colors, end-of-level star), bouncepads, climbable vines/ladders/ropes
  • Standalone visual level editor (canvas, palette, tools, properties, undo, serializer, exporter)
  • Start menu, HUD (hearts/lives/score), lives system, debug overlay
  • Keyboard and gamepad (lazy-initialized, hot-plug) controls
  • Per-level configurable physics, music, floor tilesets, and background layers
  • Builds natively on macOS, Linux, Windows; WebAssembly via Emscripten

Play in browser →


Project at a Glance

Item Detail
Language C11
Compiler clang (default), gcc compatible
Window size 800 x 600 px (OS window)
Logical canvas 400 x 300 px (2x pixel scale)
Target FPS 60
Audio 44100 Hz, stereo, 16-bit
Level format TOML (via vendored tomlc17 parser)
Libraries SDL2, SDL2_image, SDL2_ttf, SDL2_mixer, tomlc17 (vendored TOML parser)

Quick Start

# macOS -- install dependencies
brew install sdl2 sdl2_image sdl2_ttf sdl2_mixer

# Build and run the game
make run

# Build and run a specific level
make run-level LEVEL=levels/00_sandbox_01.toml

# Build and run the level editor
make run-editor

See Build System for Linux and Windows instructions.

Architecture

← Home


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, configured per level)
  │     ├── IMG_LoadTexture → gs.floor_tile        (sprites/levels/grass_tileset.png — fatal)
  │     ├── IMG_LoadTexture → gs.platform_tex      (sprites/surfaces/Platform.png — fatal)
  │     ├── water_init(&gs.water, gs.renderer)      (sprites/foregrounds/water.png)
  │     ├── IMG_LoadTexture → gs.spider_tex        (sprites/entities/spider.png — fatal)
  │     ├── IMG_LoadTexture → gs.jumping_spider_tex (sprites/entities/jumping_spider.png — fatal)
  │     ├── IMG_LoadTexture → gs.bird_tex          (sprites/entities/bird.png — fatal)
  │     ├── IMG_LoadTexture → gs.faster_bird_tex   (sprites/entities/faster_bird.png — fatal)
  │     ├── IMG_LoadTexture → gs.fish_tex          (sprites/entities/fish.png — fatal)
  │     ├── IMG_LoadTexture → gs.coin_tex          (sprites/collectibles/coin.png — fatal)
  │     ├── IMG_LoadTexture → gs.bouncepad_medium_tex (sprites/surfaces/bouncepad_medium.png — fatal)
  │     ├── IMG_LoadTexture → gs.vine_tex          (sprites/surfaces/vine.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.ladder_tex        (sprites/surfaces/ladder.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.rope_tex          (sprites/surfaces/rope.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.bouncepad_small_tex  (sprites/surfaces/bouncepad_small.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.bouncepad_high_tex   (sprites/surfaces/bouncepad_high.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.rail_tex          (sprites/surfaces/rail.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.spike_block_tex   (sprites/hazards/spike_block.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.float_platform_tex (sprites/surfaces/float_platform.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.bridge_tex        (sprites/surfaces/bridge.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.star_yellow_tex   (sprites/collectibles/star_yellow.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.star_green_tex    (sprites/collectibles/star_green.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.star_red_tex      (sprites/collectibles/star_red.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.axe_trap_tex      (sprites/hazards/axe_trap.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.circular_saw_tex  (sprites/hazards/circular_saw.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.blue_flame_tex    (sprites/hazards/blue_flame.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.fire_flame_tex    (sprites/hazards/fire_flame.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.faster_fish_tex   (sprites/entities/faster_fish.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.spike_tex         (sprites/hazards/spike.png — non-fatal)
  │     ├── IMG_LoadTexture → gs.spike_platform_tex (sprites/hazards/spike_platform.png — non-fatal)
  │     │
  │     │   ── Load all sound effects ──
  │     ├── Mix_LoadWAV     → gs.snd_spring        (sounds/surfaces/bouncepad.wav — non-fatal)
  │     ├── Mix_LoadWAV     → gs.snd_axe           (sounds/hazards/axe_trap.wav — non-fatal)
  │     ├── Mix_LoadWAV     → gs.snd_flap          (sounds/entities/bird.wav — non-fatal)
  │     ├── Mix_LoadWAV     → gs.snd_spider_attack (sounds/entities/spider.wav — non-fatal)
  │     ├── Mix_LoadWAV     → gs.snd_dive          (sounds/entities/fish.wav — non-fatal)
  │     ├── Mix_LoadWAV     → gs.snd_jump          (sounds/player/player_jump.wav — fatal)
  │     ├── Mix_LoadWAV     → gs.snd_coin          (sounds/collectibles/coin.wav — non-fatal)
  │     ├── Mix_LoadWAV     → gs.snd_hit           (sounds/player/player_hit.wav — non-fatal)
  │     ├── Mix_LoadMUS     → gs.music             (per-level music_path — non-fatal)
  │     ├── Mix_PlayMusic(-1)                      (loop forever, per-level volume)
  │     │
  │     │   ── Initialise game objects ──
  │     ├── player_init(&gs.player, gs.renderer)
  │     ├── fog_init(&gs.fog, gs.renderer)         (fog_background_1.png, fog_background_2.png)
  │     ├── hud_init(&gs.hud, gs.renderer)
  │     ├── if (debug_mode) debug_init(&gs.debug)
  │     ├── level_loader_load(&gs)                 (load level from TOML, entity inits + floor gap positions)
  │     ├── hearts/lives/score/score_life_next initialisation
  │     ├── ctrl_pending_init = 1 — deferred gamepad init (avoids antivirus/HID delays)
  │     └── gamepad subsystem initializes on first rendered frame via background thread
  │
  ├── 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
        ├── Mix_FreeChunk (snd_jump)
        ├── Mix_FreeChunk (snd_coin)
        ├── Mix_FreeChunk (snd_hit)
        ├── Mix_FreeChunk (snd_spring)
        ├── Mix_FreeChunk (snd_axe)
        ├── Mix_FreeChunk (snd_flap)
        ├── Mix_FreeChunk (snd_spider_attack)
        ├── Mix_FreeChunk (snd_dive)
        ├── water_cleanup
        ├── SDL_DestroyTexture (fire_flame_tex)
        ├── SDL_DestroyTexture (blue_flame_tex)
        ├── SDL_DestroyTexture (axe_trap_tex)
        ├── SDL_DestroyTexture (circular_saw_tex)
        ├── SDL_DestroyTexture (spike_platform_tex)
        ├── SDL_DestroyTexture (spike_tex)
        ├── SDL_DestroyTexture (spike_block_tex)
        ├── SDL_DestroyTexture (bridge_tex)
        ├── SDL_DestroyTexture (float_platform_tex)
        ├── SDL_DestroyTexture (rail_tex)
        ├── SDL_DestroyTexture (bouncepad_high_tex)
        ├── SDL_DestroyTexture (bouncepad_medium_tex)
        ├── SDL_DestroyTexture (bouncepad_small_tex)
        ├── SDL_DestroyTexture (rope_tex)
        ├── SDL_DestroyTexture (ladder_tex)
        ├── SDL_DestroyTexture (vine_tex)
        ├── last_star_cleanup
        ├── SDL_DestroyTexture (star_red_tex)
        ├── SDL_DestroyTexture (star_green_tex)
        ├── SDL_DestroyTexture (star_yellow_tex)
        ├── SDL_DestroyTexture (coin_tex)
        ├── SDL_DestroyTexture (faster_fish_tex)
        ├── SDL_DestroyTexture (fish_tex)
        ├── SDL_DestroyTexture (faster_bird_tex)
        ├── SDL_DestroyTexture (bird_tex)
        ├── SDL_DestroyTexture (jumping_spider_tex)
        ├── SDL_DestroyTexture (spider_tex)
        ├── SDL_DestroyTexture (platform_tex)
        ├── SDL_DestroyTexture (floor_tile)
        ├── 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 / ESC key)
                    SDL_CONTROLLERDEVICEADDED   — opens a newly plugged-in controller
                    SDL_CONTROLLERDEVICEREMOVED — closes and NULLs gs->controller when unplugged
                    SDL_CONTROLLERBUTTONDOWN (START) — sets gs->running = 0 to quit
  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 → fire_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
                    → floor gap fall detection (instant death)
                    → coin–player collision → star_yellow–player collision
                    → star_green–player collision → star_red–player collision
                    → last_star–player collision
                    → 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
                    → green stars → red 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) → 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)

Layer What How
1 Background Up to 8 layers from assets/sprites/backgrounds/ configured per level via [[background_layers]] in TOML, tiled horizontally, each scrolling at a different speed fraction of cam_x
2 Platforms platform.png 9-slice tiled pillar stacks (drawn before floor so pillars sink into ground)
3 Floor grass_tileset.png 9-slice tiled across world width at FLOOR_Y, with floor-gap openings
4 Float platforms float_platform.png 3-slice hovering surfaces (static, crumble, rail modes)
5 Spike rows spike.png ground-level spike strips on the floor surface
6 Spike platforms spike_platform.png elevated spike hazard surfaces
7 Bridges bridge.png tiled crumble walkways
8 Bouncepads (medium) bouncepad_medium.png standard-launch spring pads
9 Bouncepads (small) bouncepad_small.png low-launch spring pads
10 Bouncepads (high) bouncepad_high.png high-launch spring pads
11 Rails rail.png bitmask tile tracks for spike blocks and float platforms
12 Vines vine.png climbable plant decorations hanging from platforms
13 Ladders ladder.png climbable ladder structures
14 Ropes rope.png climbable rope segments
15 Coins coin.png collectible sprites drawn on top of platforms
16 Star yellows star_yellow.png collectible star pickups
17 Star greens star_green.png collectible star pickups
18 Star reds star_red.png collectible star pickups
19 Last star end-of-level star collectible (uses HUD star sprite)
20 Blue flames blue_flame.png animated flame hazards erupting from floor gaps
21 Fire flames fire_flame.png animated fire variant flame hazards erupting from floor gaps
22 Fish fish.png animated jumping enemies, drawn before water for submerged look
23 Faster fish faster_fish.png fast aggressive jumping fish enemies
24 Water water.png animated scrolling strip at the bottom
25 Spike blocks spike_block.png rotating rail-riding hazards
26 Axe traps axe_trap.png swinging axe hazards
27 Circular saws circular_saw.png spinning blade hazards
28 Spiders spider.png animated ground patrol enemies
29 Jumping spiders jumping_spider.png animated jumping patrol enemies
30 Birds bird.png slow sine-wave sky patrol enemies
31 Faster birds faster_bird.png fast aggressive sky patrol enemies
32 Player Animated sprite sheet, drawn on top of environment
33 Fog fog_background_1.png / fog_background_2.png semi-transparent sliding overlay
34 HUD hud_render: hearts, lives, score -- always drawn on top
35 Debug debug_render: FPS counter, collision boxes, event log -- when --debug active

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;      // OS window handle
    SDL_Renderer       *renderer;    // GPU drawing context
    SDL_GameController *controller;  // first connected gamepad; NULL = none
    ParallaxSystem      parallax;    // multi-layer scrolling background

    SDL_Texture   *floor_tile;       // grass_tileset.png (GPU)
    SDL_Texture   *platform_tex;     // Shared tile for platform pillars (GPU)

    SDL_Texture   *spider_tex;       // Shared texture for all spiders (GPU)
    Spider         spiders[MAX_SPIDERS];
    int            spider_count;

    SDL_Texture   *jumping_spider_tex;  // Shared texture for jumping spiders (GPU)
    JumpingSpider  jumping_spiders[MAX_JUMPING_SPIDERS];
    int            jumping_spider_count;

    SDL_Texture   *bird_tex;         // Shared texture for Bird enemies (GPU)
    Bird           birds[MAX_BIRDS];
    int            bird_count;

    SDL_Texture   *faster_bird_tex;  // Shared texture for FasterBird (GPU)
    FasterBird     faster_birds[MAX_FASTER_BIRDS];
    int            faster_bird_count;

    SDL_Texture   *fish_tex;         // Shared texture for all fish enemies (GPU)
    Fish           fish[MAX_FISH];
    int            fish_count;

    SDL_Texture   *faster_fish_tex;  // Shared texture for faster fish enemies (GPU)
    FasterFish     faster_fish[MAX_FASTER_FISH];
    int            faster_fish_count;

    SDL_Texture   *coin_tex;         // Shared texture for all coin collectibles (GPU)
    Coin           coins[MAX_COINS];
    int            coin_count;

    SDL_Texture   *star_yellow_tex;  // Shared texture for star yellow pickups (GPU)
    StarYellow     star_yellows[MAX_STAR_YELLOWS];
    int            star_yellow_count;

    SDL_Texture   *star_green_tex;   // Shared texture for star green pickups (GPU)
    StarYellow     star_greens[MAX_STAR_YELLOWS];
    int            star_green_count;

    SDL_Texture   *star_red_tex;     // Shared texture for star red pickups (GPU)
    StarYellow     star_reds[MAX_STAR_YELLOWS];
    int            star_red_count;

    LastStar       last_star;        // Special end-of-level star collectible

    SDL_Texture   *vine_tex;         // Shared texture for vine decorations (GPU)
    VineDecor      vines[MAX_VINES];
    int            vine_count;

    SDL_Texture   *ladder_tex;       // Shared texture for ladders (GPU)
    LadderDecor    ladders[MAX_LADDERS];
    int            ladder_count;

    SDL_Texture   *rope_tex;         // Shared texture for ropes (GPU)
    RopeDecor      ropes[MAX_ROPES];
    int            rope_count;

    SDL_Texture   *bouncepad_small_tex;    // Shared texture for small bouncepads (GPU)
    Bouncepad      bouncepads_small[MAX_BOUNCEPADS_SMALL];
    int            bouncepad_small_count;

    SDL_Texture   *bouncepad_medium_tex;   // Shared texture for medium bouncepads (GPU)
    Bouncepad      bouncepads_medium[MAX_BOUNCEPADS_MEDIUM];
    int            bouncepad_medium_count;

    SDL_Texture   *bouncepad_high_tex;     // Shared texture for high bouncepads (GPU)
    Bouncepad      bouncepads_high[MAX_BOUNCEPADS_HIGH];
    int            bouncepad_high_count;

    SDL_Texture   *rail_tex;         // Shared texture for all rail tiles (GPU)
    Rail           rails[MAX_RAILS];
    int            rail_count;

    SDL_Texture   *spike_block_tex;  // Shared texture for spike blocks (GPU)
    SpikeBlock     spike_blocks[MAX_SPIKE_BLOCKS];
    int            spike_block_count;

    SDL_Texture   *spike_tex;        // Shared texture for ground spikes (GPU)
    SpikeRow       spike_rows[MAX_SPIKE_ROWS];
    int            spike_row_count;

    SDL_Texture   *spike_platform_tex;  // Shared texture for spike platforms (GPU)
    SpikePlatform  spike_platforms[MAX_SPIKE_PLATFORMS];
    int            spike_platform_count;

    SDL_Texture   *circular_saw_tex;    // Shared texture for circular saws (GPU)
    CircularSaw    circular_saws[MAX_CIRCULAR_SAWS];
    int            circular_saw_count;

    SDL_Texture   *axe_trap_tex;        // Shared texture for axe traps (GPU)
    AxeTrap        axe_traps[MAX_AXE_TRAPS];
    int            axe_trap_count;

    SDL_Texture   *blue_flame_tex;      // Shared texture for blue flames (GPU)
    BlueFlame      blue_flames[MAX_BLUE_FLAMES];
    int            blue_flame_count;

    SDL_Texture   *fire_flame_tex;      // Shared texture for fire flames (GPU)
    BlueFlame      fire_flames[MAX_BLUE_FLAMES];
    int            fire_flame_count;

    SDL_Texture   *float_platform_tex;  // float_platform.png 3-slice (GPU)
    FloatPlatform  float_platforms[MAX_FLOAT_PLATFORMS];
    int            float_platform_count;

    SDL_Texture   *bridge_tex;       // bridge.png tile (GPU)
    Bridge         bridges[MAX_BRIDGES];
    int            bridge_count;

    Mix_Chunk     *snd_jump;         // Player jump sound effect (WAV)
    Mix_Chunk     *snd_coin;         // Coin collect sound effect (WAV)
    Mix_Chunk     *snd_hit;          // Player hurt sound effect (WAV)
    Mix_Chunk     *snd_spring;       // Bouncepad spring sound effect (WAV)
    Mix_Chunk     *snd_axe;          // Axe trap swing sound effect (WAV)
    Mix_Chunk     *snd_flap;         // Bird flap sound effect (WAV)
    Mix_Chunk     *snd_spider_attack;// Spider attack sound effect (WAV)
    Mix_Chunk     *snd_dive;         // Fish dive sound effect (WAV)
    Mix_Music     *music;            // Background music stream (WAV)

    Player         player;           // Player data — stored by value
    Platform       platforms[MAX_PLATFORMS];
    int            platform_count;
    Water          water;            // Animated water strip at the bottom
    FogSystem      fog;              // Atmospheric fog overlay — topmost layer

    int            floor_gaps[MAX_FLOOR_GAPS];
    int            floor_gap_count;

    Hud            hud;              // HUD display: hearts, lives, score
    int            hearts;           // Current hit points (0-MAX_HEARTS)
    int            lives;            // Remaining lives; <0 triggers game over
    int            score;            // Cumulative score from collecting coins/stars
    int            score_life_next;  // Score threshold for next bonus life

    Camera         camera;           // Viewport scroll position; updated every frame
    int            running;          // Loop flag: 1 = keep going, 0 = quit
    int            paused;           // 1 = window lost focus; physics/music frozen
    int            debug_mode;       // 1 = debug overlays active (--debug flag)
    DebugOverlay   debug;            // FPS counter, collision vis, event log

    char           level_path[256];  // Path to loaded TOML level file
    const void    *current_level;    // Pointer to active LevelDef
    int            fog_enabled;      // 1 = fog rendering active
    int            water_enabled;    // 1 = water strip rendered
    int            world_w;          // Dynamic level width (set per level)
    int            score_per_life;   // Per-level score threshold for bonus life
    int            coin_score;       // Per-level points per coin

    // ---- Gamepad lazy init (deferred to avoid antivirus/HID delays) ----
    int            ctrl_pending_init;   // 0=idle, 1=first frame, 2=thread running
    SDL_Thread    *ctrl_init_thread;    // Background init thread
    volatile int   ctrl_init_done;     // Thread completion flag
    SDL_Texture   *ctrl_init_msg_tex;  // Cached HUD "Initializing gamepad..." texture

    // ---- Loop state (persists across frames for emscripten callback) ----
    Uint64         loop_prev_ticks;  // timestamp of previous frame
    int            fp_prev_riding;   // float platform player stood on last frame
} GameState;

Key design decisions:

  • 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

Situation Action
SDL subsystem init failure (in main) fprintf(stderr, ...) → clean up already-inited subsystems → return EXIT_FAILURE
Resource load failure (in game_init) fprintf(stderr, ...) → destroy already-created resources → exit(EXIT_FAILURE)
Sound load failure (non-fatal pattern) fprintf(stderr, ...) then continue -- play is guarded by if (snd_jump)
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.

Assets

← Home


All visual assets live in the assets/sprites/ directory, organized by category (backgrounds, collectibles, entities, foregrounds, hazards, levels, player, screens, surfaces). They are PNG files (loaded via SDL2_image). Fonts live in assets/fonts/.

Coordinate note: All game objects use logical space (400x300). SDL scales to the 800x600 OS window 2x. A 48x48 sprite appears as 96x96 physical pixels on screen.


Currently Used Assets

File GameState Field / Used By Description
sprites/backgrounds/*.png parallax.c (configured per level via [[background_layers]]) Up to 8 parallax layers (sky_blue, sky_fire, clouds, glacial/volcanic_mountains, forest_leafs, castle_pillars, smoke variants, etc.) -- each with a scroll speed factor (0.0-1.0)
sprites/levels/grass_tileset.png gs->floor_tile 48x48 tile, 9-slice rendered across FLOOR_Y to form the floor (per-level configurable via floor_tile_path)
sprites/surfaces/Platform.png gs->platform_tex 48x48 tile, 9-slice rendered as one-way platform pillars
sprites/player/player.png player->texture 192x288 sprite sheet, 4 cols x 6 rows, 48x48 frames
sprites/foregrounds/water.png water->texture 384x64 sprite sheet, 8 frames of 48x64 with 16x31 art crop
sprites/entities/spider.png gs->spider_tex Spider enemy sprite sheet (ground patrol)
sprites/entities/jumping_spider.png gs->jumping_spider_tex Jumping spider enemy sprite sheet
sprites/entities/bird.png gs->bird_tex Slow sine-wave bird enemy sprite sheet
sprites/entities/faster_bird.png gs->faster_bird_tex Fast aggressive bird enemy sprite sheet
sprites/entities/fish.png gs->fish_tex Jumping fish enemy sprite sheet
sprites/entities/faster_fish.png gs->faster_fish_tex Faster fish enemy sprite sheet
sprites/collectibles/coin.png gs->coin_tex 16x16 coin collectible sprite
sprites/collectibles/star_yellow.png gs->star_yellow_tex Star yellow collectible sprite
sprites/collectibles/star_green.png gs->star_green_tex Star green collectible sprite
sprites/collectibles/star_red.png gs->star_red_tex Star red collectible sprite
sprites/collectibles/last_star.png last_star.c Last star goal collectible sprite
sprites/hazards/spike.png gs->spike_tex Floor/ceiling spike hazard
sprites/hazards/spike_block.png gs->spike_block_tex Rail-riding rotating hazard sprite
sprites/hazards/spike_platform.png gs->spike_platform_tex Spiked platform hazard sprite
sprites/hazards/circular_saw.png gs->circular_saw_tex Rotating saw blade hazard
sprites/hazards/axe_trap.png gs->axe_trap_tex Swinging axe trap hazard
sprites/hazards/blue_flame.png gs->blue_flame_tex Blue flame hazard sprite
sprites/hazards/fire_flame.png gs->fire_flame_tex Fire flame hazard sprite (fire-colored variant)
sprites/surfaces/float_platform.png gs->float_platform_tex 48x16 sprite, 3-slice horizontal strip (left cap, centre fill, right cap)
sprites/surfaces/bridge.png gs->bridge_tex 16x16 single-frame brick tile for crumble walkways
sprites/surfaces/bouncepad_small.png gs->bouncepad_small_tex Small bouncepad sprite (low launch)
sprites/surfaces/bouncepad_medium.png gs->bouncepad_medium_tex Medium bouncepad sprite (standard launch)
sprites/surfaces/bouncepad_high.png gs->bouncepad_high_tex High bouncepad sprite (max launch)
sprites/surfaces/rail.png gs->rail_tex 64x64 sprite sheet, 4x4 grid of 16x16 bitmask rail tiles
sprites/surfaces/vine.png gs->vine_tex 16x48 single-frame plant sprite for climbable vines
sprites/surfaces/ladder.png gs->ladder_tex Climbable ladder sprite
sprites/surfaces/rope.png gs->rope_tex Climbable rope sprite
sprites/foregrounds/fog_background_1.png fog.c (fog->textures[0]) Fog overlay layer, semi-transparent sliding effect
sprites/foregrounds/fog_background_2.png fog.c (fog->textures[1]) Fog overlay layer, semi-transparent sliding effect
sprites/screens/hud_coins.png hud.c Coin count UI icon used in the HUD
sprites/screens/start_menu_logo.png start_menu.c Game logo displayed on the start menu screen
fonts/round9x13.ttf hud.c (hud->font) Bitmap font for score and lives text in the HUD

Player Sprite Sheet -- player.png

Sheet dimensions: 192 x 288 px Grid: 4 columns x 6 rows Frame size: 48 x 48 px

Animation Row Map

Row AnimState Frame Count Frame Duration Notes
0 ANIM_IDLE 4 150 ms/frame Subtle breathing cycle
1 ANIM_WALK 4 100 ms/frame Looping run cycle
2 ANIM_JUMP 2 150 ms/frame Rising phase poses
3 ANIM_FALL 1 200 ms/frame Descent pose
4 ANIM_CLIMB 2 100 ms/frame Vine climbing cycle
5 (unused) -- -- Available for future states

Frame Source Rect Formula

frame.x = anim_frame_index * FRAME_W;   // column x 48
frame.y = ANIM_ROW[anim_state] * FRAME_H; // row x 48

Horizontal Flipping

When player->facing_left == 1, the sprite is drawn with SDL_FLIP_HORIZONTAL via SDL_RenderCopyEx. This means the same right-facing animation frames are used for both directions -- no duplicate assets needed.


Unused Assets

The following assets are stored in assets/sprites/unused/ and are not loaded by the game. They are available as reserves for future use.

File Description
brick_oneway.png One-way brick platform tile
brick_tileset.png Brick wall / platform tile
castle_background_0.png Castle/dungeon interior background
cloud_tileset.png Cloud platform tile
clouds.png Decorative cloud layer
clouds_mg_1_lightened.png Lightened midground cloud variant
flame_1.png Flame hazard variant
forest_background_0.png Forest scene background
glacial_mountains_lightened.png Lightened mountain variant
grass_rock_oneway.png One-way grass + rock platform
grass_rock_tileset.png Grass + rock mixed tileset
lava.png Lava hazard tile
leaf_tileset.png Leaf / foliage platform tile
sky_background_0.png Sky gradient background
sky_lightened.png Lightened sky variant
stone_tileset.png Stone floor / wall tile

Sprite Sheet Analysis

To inspect any sprite sheet's exact dimensions and pixel layout:

python3 .claude/scripts/analyze_sprite.py assets/<sprite>.png

Frame Math Reference

Sheet width  = cols x frame_w
Sheet height = rows x frame_h

source_x = (frame_index % cols) * frame_w
source_y = (frame_index / cols) * frame_h

Loading an Asset

// In game_init or an entity's init function:
SDL_Texture *tex = IMG_LoadTexture(gs->renderer, "assets/sprites/collectibles/coin.png");
if (!tex) {
    fprintf(stderr, "Failed to load coin.png: %s\n", IMG_GetError());
    exit(EXIT_FAILURE);
}

// At cleanup (reverse init order):
if (tex) { SDL_DestroyTexture(tex); tex = NULL; }

Build System

← Home


Makefile Overview

The project uses a GNU Makefile that auto-discovers source files via a wildcard -- no manual edits required when adding new .c files.

CC      = clang
CFLAGS  = -std=c11 -Wall -Wextra -Wpedantic $(shell sdl2-config --cflags)
LIBS    = $(shell sdl2-config --libs) -lSDL2_image -lSDL2_ttf -lSDL2_mixer -lm
OUTDIR  = out
TARGET  = $(OUTDIR)/super-mango
SRCDIR  = src
SRCS    = $(wildcard $(SRCDIR)/*.c) \
          $(wildcard $(SRCDIR)/collectibles/*.c) \
          $(wildcard $(SRCDIR)/core/*.c) \
          $(wildcard $(SRCDIR)/effects/*.c) \
          $(wildcard $(SRCDIR)/entities/*.c) \
          $(wildcard $(SRCDIR)/hazards/*.c) \
          $(wildcard $(SRCDIR)/levels/*.c) \
          $(wildcard $(SRCDIR)/player/*.c) \
          $(wildcard $(SRCDIR)/screens/*.c) \
          $(wildcard $(SRCDIR)/surfaces/*.c) \
          $(SRCDIR)/editor/serializer.c \
          vendor/tomlc17/tomlc17.c
OBJS    = $(SRCS:.c=.o)
DEPS    = $(OBJS:.o=.d)

Key Variables

Variable Value Description
CC clang C compiler (override with CC=gcc if needed)
CFLAGS see below Compiler flags
LIBS see below Linker flags
TARGET out/super-mango Output binary path
SRCS src/**/*.c All C source files per subdirectory (auto-discovered via explicit wildcards)
OBJS src/*.o Object files, placed next to sources
DEPS src/*.d Auto-generated dependency files (tracks header changes)

Compiler Flags Explained

Flag Meaning
-std=c11 Compile as C11 (ISO/IEC 9899:2011)
-Wall Enable common warnings
-Wextra Enable extra warnings beyond -Wall
-Wpedantic Strict ISO compliance warnings
-MMD Generate .d dependency files for each .o (tracks header changes) -- passed in compile rule, not in CFLAGS
-MP Add phony targets for each dependency (prevents errors when headers are deleted) -- passed in compile rule, not in CFLAGS
$(shell sdl2-config --cflags) SDL2 include paths (-I/opt/homebrew/include/SDL2)

Linker Flags Explained

Flag Meaning
$(shell sdl2-config --libs) SDL2 core library (-L/opt/homebrew/lib -lSDL2)
-lSDL2_image PNG/JPG texture loading
-lSDL2_ttf TrueType font rendering
-lSDL2_mixer Audio mixing (WAV, MP3, OGG)
-lm Math library (math.h functions: sinf, cosf, fmodf, etc.)

Build Targets

make / make all

Compiles all src/**/*.c files (across all subdirectories) to .o objects, then links them into out/super-mango.

make

Steps:

  1. Creates out/ directory if it does not exist
  2. Compiles each src/**/*.csrc/**/*.o
  3. Links all .o files → out/super-mango
  4. On macOS (uname -s == Darwin), ad-hoc code signs the binary with codesign --force --sign - $@ (required on Apple Silicon to avoid Killed: 9 errors). On other platforms this step is skipped

make run

Builds (if out of date) then immediately executes the binary (no CLI flags).

make run

The binary must be run from the repo root because asset paths are relative:

IMG_LoadTexture(renderer, "assets/sprites/backgrounds/sky_blue.png");
Mix_LoadWAV("assets/sounds/player/player_jump.wav");

make run-debug

Builds (if out of date) then runs the binary with the --debug flag, which enables the debug overlay: FPS counter, collision hitbox visualization, and scrolling event log.

make run-debug

make run-level LEVEL=<path>

Builds (if out of date) then runs the binary with the --level <path> flag, which loads the specified TOML level file.

make run-level LEVEL=levels/00_sandbox_01.toml

make run-level-debug LEVEL=<path>

Builds (if out of date) then runs the binary with both --level <path> and --debug flags, combining level loading with the debug overlay.

make run-level-debug LEVEL=levels/00_sandbox_01.toml

make editor

Compiles the standalone visual level editor binary to out/super-mango-editor.

make editor

make run-editor

Builds (if out of date) then launches the level editor.

make run-editor

make web

Compiles the game to WebAssembly using the Emscripten SDK (emcc). Requires Emscripten to be installed and emcc on PATH.

make web

Produces out/super-mango.html, .js, .wasm, and .data (bundled assets/sounds). SDL2 ports are compiled from source by Emscripten on first build; subsequent builds reuse cached port libraries. Uses a custom shell template from web/shell.html.

make clean

Removes all build artifacts.

make clean

Deletes:

  • src/**/*.o -- all object files across subdirectories
  • src/**/*.d -- all generated dependency files
  • out/ -- the output directory and binary

Prerequisites

macOS (Apple Silicon / Intel)

# Install Homebrew if needed: https://brew.sh
brew install sdl2 sdl2_image sdl2_ttf sdl2_mixer

# Xcode Command Line Tools (provides clang and make)
xcode-select --install

SDL2 libraries are installed to /opt/homebrew/ on Apple Silicon. sdl2-config resolves the correct paths automatically.

Linux -- Debian / Ubuntu

sudo apt update
sudo apt install build-essential clang \
    libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev

Linux -- Fedora / RHEL / CentOS

sudo dnf install clang make \
    SDL2-devel SDL2_image-devel SDL2_ttf-devel SDL2_mixer-devel

Linux -- Arch Linux

sudo pacman -S clang make sdl2 sdl2_image sdl2_ttf sdl2_mixer

Windows (MSYS2)

  1. Install MSYS2
  2. Open the MSYS2 UCRT64 terminal:
pacman -S mingw-w64-ucrt-x86_64-clang \
          mingw-w64-ucrt-x86_64-make \
          mingw-w64-ucrt-x86_64-SDL2 \
          mingw-w64-ucrt-x86_64-SDL2_image \
          mingw-w64-ucrt-x86_64-SDL2_ttf \
          mingw-w64-ucrt-x86_64-SDL2_mixer
  1. Build:
cd /c/path/to/super-mango-editor
make
  1. SDL2 DLLs must be in the same directory as the binary. Copy them from the MSYS2 prefix.

CI/CD Pipelines

Three GitHub Actions workflows:

Workflow File Trigger Purpose
Build & Release build.yml Push to main, pull requests Multi-platform build (Linux x86_64, macOS arm64, Windows x86_64, WebAssembly); on main push: GitHub Release creation + Pages deployment of WebAssembly build
CodeQL codeql.yml Push/PR to main, weekly Automated code security and quality analysis
Deploy deploy.yml Push to main, manual Deploys docs/ to GitHub Pages via actions/deploy-pages

All workflows install SDL2 dependencies per platform and compile with the project Makefile. The Deploy workflow handles static site deployment only.


Adding New Source Files

The Makefile uses per-subdirectory wildcards. Any new .c file placed in src/ or its recognized subdirectories (collectibles/, core/, effects/, entities/, hazards/, levels/, player/, screens/, surfaces/) is compiled automatically. New subdirectories require adding a wildcard line to the Makefile.

# Example: adding a new enemy entity
touch src/entities/new_enemy.c src/entities/new_enemy.h
make   # new_enemy.c is compiled automatically

See Developer Guide for the full new-entity workflow.


Output Structure

After a successful build:

out/
└── super-mango          ← the game binary
src/
├── main.o
├── game.o
├── collectibles/        ← coin.o, star_yellow.o, star_green.o, star_red.o, last_star.o
├── core/                ← debug.o, entity_utils.o
├── effects/             ← fog.o, parallax.o, water.o
├── entities/            ← spider.o, jumping_spider.o, bird.o, faster_bird.o, fish.o, faster_fish.o
├── hazards/             ← spike.o, spike_block.o, spike_platform.o, circular_saw.o, axe_trap.o, blue_flame.o
├── levels/              ← level_loader.o
├── player/              ← player.o
├── screens/             ← hud.o, start_menu.o
├── surfaces/            ← platform.o, float_platform.o, bridge.o, bouncepad.o, bouncepad_*.o, rail.o, vine.o, ladder.o, rope.o
├── editor/              ← serializer.o (shared with game build)
└── (plus corresponding .d dependency files)
vendor/
└── tomlc17/tomlc17.o

Constants Reference

← Home


A complete reference for every compile-time constant in the codebase.


game.h Constants

These are available to every file that #include "game.h".

Window

Constant Value Description
WINDOW_TITLE "Super Mango" Text shown in the OS title bar
WINDOW_W 800 OS window width in physical pixels
WINDOW_H 600 OS window height in physical pixels

Do not use WINDOW_W / WINDOW_H for game object math. All game objects live in logical space.

Logical Canvas

Constant Value Description
GAME_W 400 Internal canvas width in logical pixels
GAME_H 300 Internal canvas height in logical pixels

SDL_RenderSetLogicalSize(renderer, GAME_W, GAME_H) makes SDL scale every draw call from 400x300 up to 800x600 automatically, producing a 2x pixel scale (each logical pixel = 2x2 physical pixels).

Timing

Constant Value Description
TARGET_FPS 60 Desired frames per second

Used to compute frame_ms = 1000 / TARGET_FPS (approximately 16 ms), which is the manual frame-cap duration when VSync is unavailable.

Tiles and Floor

Constant Value Expression Description
TILE_SIZE 48 literal Width and height of one grass tile (px)
FLOOR_Y 252 GAME_H - TILE_SIZE Y coordinate of the floor's top edge

The floor is drawn by repeating the 48x48 grass tile across the full WORLD_W at y=FLOOR_Y, with gaps cut out at each floor_gaps[] position.

Physics

Constant Value Type Description
GRAVITY 800.0f float Downward acceleration in px/s^2
FLOOR_GAP_W 32 int Width of each floor gap in logical pixels
MAX_FLOOR_GAPS 16 int Maximum number of floor gaps per level

Every frame while airborne: player->vy += GRAVITY * dt.

At 60 FPS (dt approximately 0.016s) gravity adds ~12.8 px/s per frame. The jump impulse (-325.0f px/s) produces a moderate arc.

Camera

Constant Value Type Description
WORLD_W 1600 int Total logical level width (4 x GAME_W)
CAM_LOOKAHEAD_VX_FACTOR 0.20f float Pixels of lookahead per px/s of player velocity (dynamic lookahead)
CAM_LOOKAHEAD_MAX 50.0f float Maximum forward-look offset in px
CAM_SMOOTHING 8.0f float Lerp speed factor (per second); higher = snappier follow
CAM_SNAP_THRESHOLD 0.5f float Sub-pixel distance at which the camera snaps exactly to target

WORLD_W defines the full scrollable level width. The visible canvas is always GAME_W (400 px); the Camera struct tracks the left edge of the viewport in world coordinates.


player.c Local Constants

These are #defines local to player.c (not visible to other files).

Constant Value Description
FRAME_W 48 Width of one sprite frame in the sheet (px)
FRAME_H 48 Height of one sprite frame in the sheet (px)
FLOOR_SINK 16 Visual overlap onto the floor tile to prevent floating feet
PHYS_PAD_X 15 Pixels trimmed from each horizontal side of the frame for the physics box (physics width = 48 - 30 = 18 px)
PHYS_PAD_TOP 18 Pixels trimmed from the top of the frame for the physics box (physics height = 48 - 18 - 16 = 14; combined with FLOOR_SINK gives a 30 px tall box)

Why FLOOR_SINK?

The player.png sprite sheet has transparent padding at the bottom of each 48x48 frame. Without the sink offset, the physics floor edge (y + h = FLOOR_Y) would leave the character visually floating 16 px above the grass. FLOOR_SINK compensates:

floor_snap = FLOOR_Y - player->h + FLOOR_SINK
           = 252      - 48        + 16
           = 220

The character's sprite appears to rest naturally on the grass at that Y.


Animation Tables in player.c

Static arrays indexed by AnimState (0 = ANIM_IDLE, 1 = ANIM_WALK, 2 = ANIM_JUMP, 3 = ANIM_FALL, 4 = ANIM_CLIMB):

static const int ANIM_FRAME_COUNT[5] = { 4,   4,   2,   1,   2   };
static const int ANIM_FRAME_MS[5]    = { 150, 100, 150, 200, 100 };
static const int ANIM_ROW[5]         = { 0,   1,   2,   3,   4   };
Index State Frames ms/frame Sheet row
0 ANIM_IDLE 4 150 Row 0
1 ANIM_WALK 4 100 Row 1
2 ANIM_JUMP 2 150 Row 2
3 ANIM_FALL 1 200 Row 3
4 ANIM_CLIMB 2 100 Row 4

Movement Constants in player.c

Constant Value Description
WALK_MAX_SPEED 100.0f Maximum walking speed (px/s)
RUN_MAX_SPEED 250.0f Maximum running speed (px/s, Shift held)
WALK_GROUND_ACCEL 750.0f Ground acceleration while walking (px/s^2)
RUN_GROUND_ACCEL 600.0f Ground acceleration while running (px/s^2)
GROUND_FRICTION 550.0f Ground deceleration when no input (px/s^2)
GROUND_COUNTER_ACCEL 100.0f Extra deceleration when reversing direction (px/s^2)
AIR_ACCEL_WALK 350.0f Airborne acceleration while walking (px/s^2)
AIR_ACCEL_RUN 180.0f Airborne acceleration while running (px/s^2)
AIR_FRICTION 80.0f Airborne deceleration when no input (px/s^2)
WALK_ANIM_MIN_VX 8.0f Minimum horizontal speed to trigger walk animation (px/s)

The player uses an acceleration-based movement model. Hold Shift to run. Physics overrides for all these values can be configured per level in the TOML [physics] section.

Vine Climbing Constants in player.c

Constant Value Type Description
CLIMB_SPEED 80.0f float Vertical climbing speed on vines (px/s)
CLIMB_H_SPEED 80.0f float Horizontal drift speed while on vine (px/s)
VINE_GRAB_PAD 4 int Extra pixels on each side of vine sprite that count as the grab zone (total grab width = VINE_W + 2 x 4 = 24 px)

Audio Constants in main.c

Value Description
44100 Audio sample rate (Hz)
MIX_DEFAULT_FORMAT 16-bit signed samples
2 Stereo channels
2048 Mixer buffer size (samples)
per level Music volume (0-128) configured via music_volume in level TOML (default 13, ~10%)

Derived Values Quick Reference

Expression Result Meaning
GAME_W / WINDOW_W 2x Pixel scale factor
GAME_H / WINDOW_H 2x Pixel scale factor
1000 / TARGET_FPS ~16 ms Frame budget
GAME_H - TILE_SIZE 252 FLOOR_Y
FLOOR_Y - FRAME_H + FLOOR_SINK 220 Player start / floor snap Y
GAME_W / TILE_SIZE ~8.3 Tiles needed to fill the floor
WATER_FRAMES x WATER_ART_W 128 WATER_PERIOD -- seamless repeat distance

platform.h Constants

Constant Value Description
MAX_PLATFORMS 32 Maximum number of platforms in the game

water.h Constants

Constant Value Type Description
WATER_FRAMES 8 int Total animation frames in water.png
WATER_FRAME_W 48 int Full slot width per frame in the sheet (px)
WATER_ART_DX 16 int Left offset to visible art within each slot
WATER_ART_W 16 int Width of actual art pixels per frame
WATER_ART_Y 17 int First visible row within each frame
WATER_ART_H 31 int Height of visible art pixels
WATER_PERIOD 128 int Pattern repeat distance: WATER_FRAMES x WATER_ART_W
WATER_SCROLL_SPEED 40.0f float Rightward scroll speed (px/s)

spider.h Constants

Constant Value Type Description
MAX_SPIDERS 16 int Maximum simultaneous spider enemies
SPIDER_FRAMES 3 int Animation frames in spider.png (192/64 = 3)
SPIDER_FRAME_W 64 int Width of one frame slot in the sheet (px)
SPIDER_ART_X 20 int First visible col within each frame slot
SPIDER_ART_W 25 int Width of visible art (cols 20-44)
SPIDER_ART_Y 22 int First visible row within each frame slot
SPIDER_ART_H 10 int Height of visible art (rows 22-31)
SPIDER_SPEED 50.0f float Walk speed (px/s)
SPIDER_FRAME_MS 150 int Milliseconds each animation frame is held

fog.h Constants

Constant Value Type Description
FOG_TEX_COUNT 2 int Number of fog texture assets in rotation
FOG_MAX 4 int Maximum concurrent fog instances

parallax.h Constants

Constant Value Type Description
MAX_BACKGROUND_LAYERS 8 int Maximum number of background layers the system can hold

coin.h Constants

Constant Value Type Description
MAX_COINS 64 int Maximum simultaneous coins on screen
COIN_DISPLAY_W 16 int Render width in logical pixels
COIN_DISPLAY_H 16 int Render height in logical pixels
COIN_SCORE 100 int Score awarded per coin collected
SCORE_PER_LIFE 1000 int Score multiple that grants a bonus life

vine.h Constants

Constant Value Type Description
MAX_VINES 24 int Maximum number of vine instances
VINE_W 16 int Sprite width in logical pixels
VINE_H 32 int Content height after removing transparent padding
VINE_SRC_Y 8 int First pixel row with content in vine.png
VINE_SRC_H 32 int Height of content area in vine.png
VINE_STEP 19 int Vertical spacing between stacked tiles (px)

fish.h Constants

Constant Value Type Description
MAX_FISH 16 int Maximum simultaneous fish instances
FISH_FRAMES 2 int Horizontal frames in fish.png (96x48 sheet)
FISH_FRAME_W 48 int Width of one frame slot in the sheet (px)
FISH_FRAME_H 48 int Height of one frame slot in the sheet (px)
FISH_RENDER_W 48 int On-screen render width in logical pixels
FISH_RENDER_H 48 int On-screen render height in logical pixels
FISH_SPEED 70.0f float Horizontal patrol speed (px/s)
FISH_JUMP_VY -280.0f float Upward jump impulse (px/s)
FISH_JUMP_MIN 1.4f float Minimum seconds before next jump
FISH_JUMP_MAX 3.0f float Maximum seconds before next jump
FISH_HITBOX_PAD_X 16 int Horizontal inset for fair AABB collision (hitbox width = 16 px)
FISH_HITBOX_PAD_Y 13 int Vertical inset for fair AABB collision (hitbox height = 19 px)
FISH_FRAME_MS 120 int Milliseconds per swim animation frame

hud.h Constants

Constant Value Type Description
MAX_HEARTS 3 int Maximum hearts the player can have
DEFAULT_LIVES 3 int Lives the player starts with
HUD_MARGIN 4 int Pixel margin from screen edges
HUD_HEART_SIZE 16 int Display size of each heart icon (px)
HUD_HEART_GAP 2 int Horizontal gap between heart icons (px)
HUD_ICON_W 16 int Display width of the player icon (px)
HUD_ICON_H 13 int Display height of the player icon (px)
HUD_ROW_H 16 int Row height for text alignment (font px)
HUD_COIN_ICON_SIZE 12 int Display size of the coin count icon (px)

bouncepad.h Constants

Constant Value Type Description
MAX_BOUNCEPADS 16 int Maximum simultaneous bouncepad instances (per variant)
BOUNCEPAD_W 48 int Display width of one bouncepad frame (px)
BOUNCEPAD_H 48 int Display height of one bouncepad frame (px)
BOUNCEPAD_VY_SMALL -380.0f float Small bouncepad launch impulse (px/s)
BOUNCEPAD_VY_MEDIUM -536.25f float Medium bouncepad launch impulse (px/s)
BOUNCEPAD_VY_HIGH -700.0f float High bouncepad launch impulse (px/s)
BOUNCEPAD_VY -536.25f float Default launch impulse (alias for BOUNCEPAD_VY_MEDIUM)
BOUNCEPAD_FRAME_MS 80 int Milliseconds per animation frame during release
BOUNCEPAD_SRC_Y 14 int First non-transparent row in the frame
BOUNCEPAD_SRC_H 18 int Height of the art region (rows 14-31)
BOUNCEPAD_ART_X 16 int First non-transparent col in the frame
BOUNCEPAD_ART_W 16 int Width of the art region (cols 16-31)

rail.h Constants

Constant Value Type Description
RAIL_N 1 << 0 bitmask Tile opens upward
RAIL_E 1 << 1 bitmask Tile opens rightward
RAIL_S 1 << 2 bitmask Tile opens downward
RAIL_W 1 << 3 bitmask Tile opens leftward
RAIL_TILE_W 16 int Width of one tile in the sprite sheet (px)
RAIL_TILE_H 16 int Height of one tile in the sprite sheet (px)
MAX_RAIL_TILES 128 int Maximum tiles in a single Rail path
MAX_RAILS 16 int Maximum Rail instances per level

spike_block.h Constants

Constant Value Type Description
SPIKE_DISPLAY_W 24 int On-screen width in logical pixels (16x16 scaled up)
SPIKE_DISPLAY_H 24 int On-screen height in logical pixels
SPIKE_SPIN_DEG_PER_SEC 360.0f float Rotation speed -- one full turn per second
SPIKE_SPEED_SLOW 1.5f float Rail traversal: 1.5 tiles/s
SPIKE_SPEED_NORMAL 3.0f float Rail traversal: 3.0 tiles/s
SPIKE_SPEED_FAST 6.0f float Rail traversal: 6.0 tiles/s
SPIKE_PUSH_SPEED 220.0f float Horizontal push impulse magnitude (px/s)
SPIKE_PUSH_VY -150.0f float Upward push component on collision (px/s)
MAX_SPIKE_BLOCKS 16 int Maximum spike block instances per level

debug.h Constants

Constant Value Type Description
DEBUG_LOG_MAX_ENTRIES 8 int Maximum visible log messages
DEBUG_LOG_MSG_LEN 64 int Max characters per log message (incl. null)
DEBUG_LOG_DISPLAY_SEC 4.0f float Seconds each log entry stays visible
DEBUG_FPS_SAMPLE_MS 500 int Milliseconds between FPS counter refreshes

jumping_spider.h Constants

Constant Value Type Description
MAX_JUMPING_SPIDERS 16 int Maximum simultaneous jumping spider instances
JSPIDER_FRAMES 3 int Animation frames in jumping_spider.png (192/64 = 3)
JSPIDER_FRAME_W 64 int Width of one frame slot in the sheet (px)
JSPIDER_ART_X 20 int First visible col within each frame
JSPIDER_ART_W 25 int Width of visible art (cols 20-44)
JSPIDER_ART_Y 22 int First visible row within each frame
JSPIDER_ART_H 10 int Height of visible art (rows 22-31)
JSPIDER_SPEED 55.0f float Walk speed (px/s)
JSPIDER_FRAME_MS 150 int Milliseconds per animation frame
JSPIDER_JUMP_VY -200.0f float Upward jump impulse (px/s)
JSPIDER_GRAVITY 600.0f float Downward acceleration while airborne (px/s^2)

bird.h Constants

Constant Value Type Description
MAX_BIRDS 16 int Maximum simultaneous bird instances
BIRD_FRAMES 3 int Animation frames in bird.png (144/48 = 3)
BIRD_FRAME_W 48 int Width of one frame slot in the sheet (px)
BIRD_ART_X 17 int First visible col within each frame
BIRD_ART_W 15 int Width of visible art (cols 17-31)
BIRD_ART_Y 17 int First visible row within each frame
BIRD_ART_H 14 int Height of visible art (rows 17-30)
BIRD_SPEED 45.0f float Horizontal flight speed (px/s)
BIRD_FRAME_MS 140 int Milliseconds per wing animation frame
BIRD_WAVE_AMP 20.0f float Sine-wave amplitude in logical pixels
BIRD_WAVE_FREQ 0.015f float Sine cycles per pixel of horizontal travel

faster_bird.h Constants

Constant Value Type Description
MAX_FASTER_BIRDS 16 int Maximum simultaneous faster bird instances
FBIRD_FRAMES 3 int Animation frames in faster_bird.png (144/48 = 3)
FBIRD_FRAME_W 48 int Width of one frame slot in the sheet (px)
FBIRD_ART_X 17 int First visible col within each frame
FBIRD_ART_W 15 int Width of visible art (cols 17-31)
FBIRD_ART_Y 17 int First visible row within each frame
FBIRD_ART_H 14 int Height of visible art (rows 17-30)
FBIRD_SPEED 80.0f float Horizontal speed -- nearly 2x the slow bird
FBIRD_FRAME_MS 90 int Faster wing animation (ms per frame)
FBIRD_WAVE_AMP 15.0f float Tighter sine-wave amplitude (px)
FBIRD_WAVE_FREQ 0.025f float Higher frequency -- more erratic curves

float_platform.h Constants

Constant Value Type Description
FLOAT_PLATFORM_PIECE_W 16 int Width of each 3-slice piece (px)
FLOAT_PLATFORM_H 16 int Height of the platform sprite (px)
MAX_FLOAT_PLATFORMS 16 int Maximum float platform instances per level
CRUMBLE_STAND_LIMIT 0.75f float Seconds of standing before crumble-fall starts
CRUMBLE_FALL_GRAVITY 250.0f float Downward acceleration during crumble fall (px/s^2)
CRUMBLE_FALL_INITIAL_VY 20.0f float Initial downward velocity on crumble-start (px/s)

bridge.h Constants

Constant Value Type Description
MAX_BRIDGES 16 int Maximum bridge instances per level
MAX_BRIDGE_BRICKS 16 int Maximum bricks in a single bridge
BRIDGE_TILE_W 16 int Width of one bridge.png tile (px)
BRIDGE_TILE_H 16 int Height of one bridge.png tile (px)
BRIDGE_FALL_DELAY 0.1f float Seconds between touch and first brick falling
BRIDGE_CASCADE_DELAY 0.06f float Extra seconds between successive bricks cascading outward
BRIDGE_FALL_GRAVITY 250.0f float Downward acceleration per brick during fall (px/s^2)
BRIDGE_FALL_INITIAL_VY 20.0f float Initial downward velocity on fall-start (px/s)

star_yellow.h Constants

Constant Value Type Description
MAX_STAR_YELLOWS 16 int Maximum star instances per color per level
STAR_YELLOW_DISPLAY_W 16 int Display width (logical px)
STAR_YELLOW_DISPLAY_H 16 int Display height (logical px)

last_star.h Constants

Constant Value Type Description
LAST_STAR_DISPLAY_W 24 int Display width (logical px)
LAST_STAR_DISPLAY_H 24 int Display height (logical px)

axe_trap.h Constants

Constant Value Type Description
AXE_FRAME_W 48 int Source sprite width (px)
AXE_FRAME_H 64 int Source sprite height (px)
AXE_DISPLAY_W 48 int On-screen display width (logical px)
AXE_DISPLAY_H 64 int On-screen display height (logical px)
AXE_SWING_AMPLITUDE 60.0f float Maximum pendulum angle from vertical (degrees)
AXE_SWING_PERIOD 2.0f float Time for one full pendulum cycle (s)
AXE_SPIN_SPEED 180.0f float Rotation speed for spin variant (degrees/s)
MAX_AXE_TRAPS 16 int Maximum axe trap instances per level

circular_saw.h Constants

Constant Value Type Description
SAW_FRAME_W 32 int Source sprite width (px)
SAW_FRAME_H 32 int Source sprite height (px)
SAW_DISPLAY_W 32 int On-screen display width (logical px)
SAW_DISPLAY_H 32 int On-screen display height (logical px)
SAW_SPIN_DEG_PER_SEC 720.0f float Rotation speed (degrees/s)
SAW_PATROL_SPEED 180.0f float Horizontal patrol speed (px/s)
SAW_PUSH_SPEED 220.0f float Push impulse magnitude (px/s)
SAW_PUSH_VY -150.0f float Upward bounce component on collision (px/s)
MAX_CIRCULAR_SAWS 16 int Maximum circular saw instances per level

blue_flame.h Constants

Constant Value Type Description
BLUE_FLAME_FRAME_W 48 int Animation frame width (px)
BLUE_FLAME_FRAME_H 48 int Animation frame height (px)
BLUE_FLAME_DISPLAY_W 48 int On-screen display width (logical px)
BLUE_FLAME_DISPLAY_H 48 int On-screen display height (logical px)
BLUE_FLAME_FRAME_COUNT 2 int Number of animation frames
BLUE_FLAME_ANIM_SPEED 0.1f float Seconds between frame advances
BLUE_FLAME_LAUNCH_VY -550.0f float Initial upward impulse (px/s)
BLUE_FLAME_RISE_DECEL 800.0f float Deceleration during rise (px/s^2)
BLUE_FLAME_APEX_Y 60.0f float World-space y coordinate at apex (px)
BLUE_FLAME_FLIP_DURATION 0.12f float Time to rotate 180 degrees at apex (s)
BLUE_FLAME_WAIT_DURATION 1.5f float Time hidden below floor before next eruption (s)
MAX_BLUE_FLAMES 16 int Maximum blue/fire flame instances per level

faster_fish.h Constants

Constant Value Type Description
MAX_FASTER_FISH 16 int Maximum faster fish instances per level
FFISH_FRAMES 2 int Number of animation frames
FFISH_FRAME_W 48 int Frame width (px)
FFISH_FRAME_H 48 int Frame height (px)
FFISH_RENDER_W 48 int Render width (logical px)
FFISH_RENDER_H 48 int Render height (logical px)
FFISH_SPEED 120.0f float Patrol speed (px/s)
FFISH_JUMP_VY -420.0f float Jump impulse (px/s)
FFISH_JUMP_MIN 1.0f float Minimum delay between jumps (s)
FFISH_JUMP_MAX 2.2f float Maximum delay between jumps (s)
FFISH_HITBOX_PAD_X 16 int Horizontal hitbox inset (px)
FFISH_HITBOX_PAD_Y 13 int Vertical hitbox inset (px)
FFISH_FRAME_MS 100 int Frame animation duration (ms)

spike.h Constants

Constant Value Type Description
MAX_SPIKE_ROWS 16 int Maximum spike row instances per level
MAX_SPIKE_TILES 16 int Maximum tiles in a single spike row
SPIKE_TILE_W 16 int Spike tile width (px)
SPIKE_TILE_H 16 int Spike tile height (px)

spike_platform.h Constants

Constant Value Type Description
MAX_SPIKE_PLATFORMS 16 int Maximum spike platform instances per level
SPIKE_PLAT_PIECE_W 16 int Width of one 3-slice piece (px)
SPIKE_PLAT_H 16 int Full frame height (px)
SPIKE_PLAT_SRC_Y 5 int First content row in each piece (px)
SPIKE_PLAT_SRC_H 11 int Content height (rows 5-15, px)

ladder.h Constants

Constant Value Type Description
MAX_LADDERS 16 int Maximum ladder instances per level
LADDER_W 16 int Sprite width (px)
LADDER_H 22 int Content height after cropping padding (px)
LADDER_SRC_Y 13 int First pixel row with content
LADDER_SRC_H 22 int Height of content area (px)
LADDER_STEP 8 int Vertical overlap when tiling (px)

rope.h Constants

Constant Value Type Description
MAX_ROPES 16 int Maximum rope instances per level
ROPE_W 12 int Display width with padding (px)
ROPE_H 36 int Display height with padding (px)
ROPE_SRC_X 2 int Source crop x offset (px)
ROPE_SRC_Y 6 int Source crop y offset (px)
ROPE_SRC_W 12 int Source crop width (px)
ROPE_SRC_H 36 int Source crop height (px)
ROPE_STEP 34 int Vertical spacing between stacked tiles (px)

bouncepad_small.h / bouncepad_medium.h / bouncepad_high.h Constants

Constant Value Type Description
MAX_BOUNCEPADS_SMALL 16 int Maximum small bouncepad instances
MAX_BOUNCEPADS_MEDIUM 16 int Maximum medium bouncepad instances
MAX_BOUNCEPADS_HIGH 16 int Maximum high bouncepad instances

Developer Guide

← Home


This guide covers the patterns and conventions used in Super Mango and explains how to extend the game safely and consistently.


Coding Conventions

Language and Standard

  • C11 (-std=c11)
  • Compiler: clang (default), gcc compatible

Naming

Category Convention Example
Files snake_case player.c, coin.h
Functions module_verb player_init, coin_update
Struct types PascalCase via typedef Player, GameState, Coin
Enum values UPPER_SNAKE_CASE ANIM_IDLE, ANIM_WALK
Constants (#define) UPPER_SNAKE_CASE FLOOR_Y, TILE_SIZE
Local variables snake_case dt, frame_ms, elapsed
Assets snake_case player.png, coin.png, spider.png
Sounds component_descriptor.wav player_jump.wav, coin.wav, bird.wav

Memory and Safety Rules

  • Every pointer must be set to NULL immediately after freeing. (SDL_Destroy* and free() on NULL are no-ops, preventing double-free crashes.)
  • Error paths call SDL_GetError() / IMG_GetError() / Mix_GetError() and write to stderr.
  • Resources are always freed in reverse init order.
  • Use float for positions and velocities; cast to int only at render time (SDL_Rect fields are int).

Coordinate System

All game-object positions and sizes live in logical space (400x300). Never use WINDOW_W / WINDOW_H for game math -- SDL scales the logical canvas to the OS window automatically.

See Constants Reference for all defined constants.


Adding a New Entity

Every entity follows the same lifecycle pattern:

entity_init    -> load texture, set initial state
entity_update  -> move, apply physics, detect events
entity_render  -> draw to renderer
entity_cleanup -> SDL_DestroyTexture, set to NULL

And optionally:

entity_handle_input   -> if player-controlled
entity_animate        -> static helper, called from entity_update

Step-by-Step

1. Create the header -- src/collectibles/coin.h

#pragma once
#include <SDL.h>

typedef struct {
    float        x, y;    /* logical position (top-left) */
    int          w, h;    /* display size in logical px  */
    int          active;  /* 1 = visible, 0 = collected  */
    SDL_Texture *texture;
} Coin;

void coin_init(Coin *coin, SDL_Renderer *renderer, float x, float y);
void coin_update(Coin *coin, float dt);
void coin_render(Coin *coin, SDL_Renderer *renderer);
void coin_cleanup(Coin *coin);

2. Create the implementation -- src/collectibles/coin.c

#include <SDL_image.h>
#include <stdio.h>
#include <stdlib.h>
#include "coin.h"

void coin_init(Coin *coin, SDL_Renderer *renderer, float x, float y) {
    coin->texture = IMG_LoadTexture(renderer, "assets/sprites/collectibles/coin.png");
    if (!coin->texture) {
        fprintf(stderr, "Failed to load coin.png: %s\n", IMG_GetError());
        exit(EXIT_FAILURE);
    }
    coin->x = x;
    coin->y = y;
    coin->w = 48;
    coin->h = 48;
    coin->active = 1;
}

void coin_render(Coin *coin, SDL_Renderer *renderer) {
    if (!coin->active) return;
    SDL_Rect dst = { (int)coin->x, (int)coin->y, coin->w, coin->h };
    SDL_RenderCopy(renderer, coin->texture, NULL, &dst);
}

void coin_cleanup(Coin *coin) {
    if (coin->texture) {
        SDL_DestroyTexture(coin->texture);
        coin->texture = NULL;
    }
}

The Makefile picks up coin.c automatically -- no Makefile changes needed.

3. Add texture to GameState in game.h

Textures are loaded in game_init() and stored in GameState. The entity array and count also live in GameState:

#include "coin.h"

typedef struct {
    // ... existing fields ...
    SDL_Texture *tex_coin;    /* GPU texture, loaded in game_init */
    Coin coins[32];           /* fixed-size array -- simple and cache-friendly */
    int  coin_count;          /* how many are currently active */
} GameState;

4. Wire up in game.c

// game_init -- load texture and init entities:
gs->tex_coin = IMG_LoadTexture(gs->renderer, "assets/sprites/collectibles/coin.png");
coin_init(&gs->coins[0], gs->tex_coin, 200.0f, 100.0f);
gs->coin_count = 1;

// game_loop update section:
for (int i = 0; i < gs->coin_count; i++)
    coin_update(&gs->coins[i], dt);

// game_loop render section (correct layer order):
for (int i = 0; i < gs->coin_count; i++)
    coin_render(&gs->coins[i], gs->renderer);

// game_cleanup (before SDL_DestroyRenderer):
for (int i = 0; i < gs->coin_count; i++)
    coin_cleanup(&gs->coins[i]);

5. Define entity placements in level TOML

Entity spawn positions are defined in level TOML files (e.g. levels/00_sandbox_01.toml). Add your entity placements there, or use the visual editor (make run-editor) to place entities graphically.

6. Add debug hitbox -- src/core/debug.c

Every entity must have hitbox visualization in debug.c:

// In debug_render:
for (int i = 0; i < gs->coin_count; i++) {
    if (!gs->coins[i].active) continue;
    SDL_Rect hb = { (int)gs->coins[i].x, (int)gs->coins[i].y,
                     gs->coins[i].w, gs->coins[i].h };
    SDL_SetRenderDrawColor(gs->renderer, 255, 255, 0, 128);
    SDL_RenderDrawRect(gs->renderer, &hb);
}

Also add debug_log calls in game.c for any significant entity events (collection, destruction, spawn).


Adding Physics to an Entity

Use the same pattern as player_update:

/* Apply gravity while airborne */
if (!entity->on_ground) {
    entity->vy += GRAVITY * dt;
}

/* Integrate position */
entity->x += entity->vx * dt;
entity->y += entity->vy * dt;

/* Floor collision */
if (entity->y + entity->h >= FLOOR_Y) {
    entity->y        = (float)(FLOOR_Y - entity->h);
    entity->vy       = 0.0f;
    entity->on_ground = 1;
} else {
    entity->on_ground = 0;
}

/* Horizontal clamp */
if (entity->x < 0.0f)                entity->x = 0.0f;
if (entity->x > GAME_W - entity->w)  entity->x = (float)(GAME_W - entity->w);

GRAVITY, FLOOR_Y, GAME_W, and GAME_H are all defined in game.h and available to any file that includes it. See Constants Reference for values.


Adding a New Sound Effect

All sound files are .wav format, named with the convention component_descriptor.wav:

Sound File
Player jump player_jump.wav
Player hit player_hit.wav
Coin collect coin.wav
Bouncepad bouncepad.wav
Bird bird.wav
Fish fish.wav
Spider spider.wav
Axe trap axe_trap.wav

Steps to add a new sound:

  1. Place .wav in assets/sounds/<category>/.
  2. Add Mix_Chunk *snd_<name>; to GameState in game.h.
  3. Load in game_init (non-fatal -- warn but continue):
gs->snd_<name> = Mix_LoadWAV("assets/sounds/<category>/<name>.wav");
if (!gs->snd_<name>) {
    fprintf(stderr, "Warning: could not load <name>.wav: %s\n", Mix_GetError());
}
  1. Free in game_cleanup:
if (gs->snd_<name>) { Mix_FreeChunk(gs->snd_<name>); gs->snd_<name> = NULL; }
  1. Play wherever needed:
if (gs->snd_<name>) Mix_PlayChannel(-1, gs->snd_<name>, 0);

See Sounds for the full list of available sound files.


Adding Background Music

Background music is loaded via Mix_LoadMUS (not Mix_LoadWAV). The track path is configured per level via music_path in the TOML file:

// Load (path from level TOML music_path field)
gs->music = Mix_LoadMUS(level->music_path);

// Play (looping)
Mix_PlayMusic(gs->music, -1);
Mix_VolumeMusic(level->music_volume);  // 0-128, configured per level

// Cleanup
Mix_HaltMusic();
Mix_FreeMusic(gs->music);
gs->music = NULL;

Adding HUD / Text Rendering

SDL2_ttf is already initialized in main.c. The font round9x13.ttf is in assets/fonts/.

// Load font
TTF_Font *font = TTF_OpenFont("assets/fonts/round9x13.ttf", 13);
if (!font) { fprintf(stderr, "TTF_OpenFont: %s\n", TTF_GetError()); }

// Render text to a surface, then upload to a texture
SDL_Color white = {255, 255, 255, 255};
SDL_Surface *surf = TTF_RenderText_Solid(font, "Score: 0", white);
SDL_Texture *tex  = SDL_CreateTextureFromSurface(renderer, surf);
SDL_FreeSurface(surf);

// Draw the texture
SDL_Rect dst = {10, 10, surf->w, surf->h};
SDL_RenderCopy(renderer, tex, NULL, &dst);

// Cleanup
SDL_DestroyTexture(tex);
TTF_CloseFont(font);

The HUD renders hearts (lives), life counter, and score. It is drawn after all game entities so it always appears on top.


Render Layer Order

Always draw in painter's algorithm order (back to front). The game currently uses 35 layers:

 1. Parallax background    (up to 8 layers from assets/sprites/backgrounds/, per level)
 2. Platforms              (platform.png 9-slice pillars)
 3. Floor tiles            (per-level tileset at FLOOR_Y, with floor-gap openings)
 4. Float platforms        (float_platform.png 3-slice hovering surfaces)
 5. Spike rows             (spike.png ground-level spike strips)
 6. Spike platforms        (spike_platform.png elevated spike hazards)
 7. Bridges                (bridge.png tiled crumble walkways)
 8. Bouncepads medium      (bouncepad_medium.png standard spring pads)
 9. Bouncepads small       (bouncepad_small.png low spring pads)
10. Bouncepads high        (bouncepad_high.png tall spring pads)
11. Rails                  (rail.png bitmask tile tracks)
12. Vines                  (vine.png climbable)
13. Ladders                (ladder.png climbable)
14. Ropes                  (rope.png climbable)
15. Coins                  (coin.png collectibles)
16. Star yellows           (star_yellow.png health pickups)
17. Star greens            (star_green.png health pickups)
18. Star reds              (star_red.png health pickups)
19. Last star              (end-of-level star using HUD star sprite)
20. Blue flames            (blue_flame.png erupting from floor gaps)
21. Fire flames            (fire_flame.png fire variant erupting from floor gaps)
22. Fish                   (fish.png jumping water enemies)
23. Faster fish            (faster_fish.png fast jumping enemies)
24. Water                  (water.png animated strip)
25. Spike blocks           (spike_block.png rail-riding hazards)
26. Axe traps              (axe_trap.png swinging hazards)
27. Circular saws          (circular_saw.png patrol hazards)
28. Spiders                (spider.png ground patrol)
29. Jumping spiders        (jumping_spider.png jumping patrol)
30. Birds                  (bird.png slow sine-wave)
31. Faster birds           (faster_bird.png fast sine-wave)
32. Player                 (player.png animated)
33. Fog                    (fog_background_1/2.png sliding overlay)
34. HUD                    (hearts, lives, score -- always on top)
35. Debug overlay          (FPS, hitboxes, event log -- when --debug)

See Architecture for details on the render pipeline.


Sprite Sheet Workflow

To analyze a new sprite sheet:

python3 .claude/scripts/analyze_sprite.py assets/<sprite>.png

Frame math:

source_x = (frame_index % num_cols) * frame_w
source_y = (frame_index / num_cols) * frame_h

Standard animation row layout (most assets in this pack):

Row Animation Notes
0 Idle 1-4 frames, subtle
1 Walk / Run 6-8 frames, looping
2 Jump (up) 2-4 frames, one-shot
3 Fall / Land 2-4 frames
4 Attack 4-8 frames, one-shot
5 Death / Hurt 4-6 frames, one-shot

See Assets for sprite sheet dimensions and Player Module for animation state machine details.


Checklist: Adding a New Entity

  • Create src/<entity>.h with struct and function declarations
  • Create src/<entity>.c with init, update, render, cleanup
  • Add #include "<entity>.h" to game.h
  • Add texture pointer, entity array, and count to GameState (by value, not pointer)
  • Load texture in game_init in game.c
  • Call <entity>_init in game_init
  • Call <entity>_update in game_loop update section
  • Call <entity>_render in game_loop render section (correct layer order)
  • Call <entity>_cleanup in game_cleanup (before SDL_DestroyRenderer)
  • Set all freed pointers to NULL
  • Define entity spawn positions in a level TOML file or use the visual editor
  • Add hitbox visualization in debug.c
  • Add debug_log calls in game.c for significant entity events
  • Build with make -- no Makefile changes needed
  • Test with --debug flag to verify hitboxes render correctly

Related Pages

Player Module

← Home


The player module spans player.h and player.c and owns the full lifecycle of the player character: texture loading, keyboard input, physics simulation, sprite animation, rendering, and cleanup.


Lifecycle at a Glance

player_init        ← called once from game_init
  └── IMG_LoadTexture (player.png → GPU)
  └── set initial position, speed, animation state

per frame (game_loop):
  player_handle_input   ← sample keyboard and gamepad, set vx / vy / on_ground
  player_update         ← apply gravity, integrate position, collide, animate
  player_render         ← draw the current frame to the renderer
  player_get_hitbox     ← return physics hitbox used by game_loop for collision

player_reset       ← called from game_loop when the player loses a life
  └── reset position, velocity, and animation state (texture is reused, not reloaded)

player_cleanup     ← called once from game_cleanup
  └── SDL_DestroyTexture

Initialization -- player_init

void player_init(Player *player, SDL_Renderer *renderer);
Action Detail
Load texture IMG_LoadTexture(renderer, "assets/sprites/player/player.png") -- 192x288 sheet
Frame rect {x=0, y=0, w=48, h=48} -- first cell (row 0, col 0)
Display size w = h = 48 px (logical coordinates)
Start position On pillar 0: x = 80.0f + (TILE_SIZE - 48) / 2.0f = 80
Start Y FLOOR_Y - 2*TILE_SIZE + 16 - 48 + FLOOR_SINK = 172 (on top of 2-high pillar)
Speed Walk: 100.0f px/s, Run: 250.0f px/s (acceleration-based)
Initial velocity vx = vy = 0.0f
on_ground 1 (starts on the floor)
Animation ANIM_IDLE, frame 0, facing right

FLOOR_SINK = 16: The sprite sheet has transparent padding at the bottom of each 48x48 frame. Sinking 16 px makes the character's feet visually rest on the grass, even though the physics edge (y + h) is 16 px above FLOOR_Y.


Input -- player_handle_input

void player_handle_input(Player *player, Mix_Chunk *snd_jump,
                         SDL_GameController *ctrl,
                         const VineDecor *vines, int vine_count,
                         const LadderDecor *ladders, int ladder_count,
                         const RopeDecor *ropes, int rope_count);

Called once per frame before player_update. Uses SDL_GetKeyboardState to read the instantaneous keyboard state (held keys), not events. This gives smooth, continuous movement. The ctrl parameter is the active gamepad handle; pass NULL when no controller is connected -- keyboard input still works normally. The vines, ladders, and ropes arrays are used for climbable grab detection when the player presses UP.

Key Bindings

Input Action
Left Arrow / A Move left (vx -= speed), facing_left = 1
Right Arrow / D Move right (vx += speed), facing_left = 0
Up Arrow / W Grab nearest vine / climb up on vine
Down Arrow / S Climb down on vine
D-Pad left / right Move left / right (gamepad)
D-Pad up / down Grab vine / climb up / climb down (gamepad)
Left analog stick (X-axis) Move left / right (dead-zone: 8000 / 32767)
Left analog stick (Y-axis) Climb up / down on vine (gamepad)
Left Shift Run (hold for RUN_MAX_SPEED, release for WALK_MAX_SPEED)
Space Jump (-325 px/s impulse -- ground and vine dismount)
A button / Cross (gamepad) Jump
ESC Quit (handled in game_loop, not here)
Start button (gamepad) Quit (handled in game_loop, not here)

Jump Logic

int want_jump = keys[SDL_SCANCODE_SPACE];
if (ctrl) {
    want_jump |= SDL_GameControllerGetButton(ctrl, SDL_CONTROLLER_BUTTON_A);
}
if (player->on_ground && want_jump) {
    player->vy        = -325.0f;   // upward impulse (negative = up in SDL)
    player->on_ground  = 0;
    if (snd_jump) Mix_PlayChannel(-1, snd_jump, 0);
}
  • Keyboard jump impulse is -325.0f px/s (upward) for both ground and vine dismount.
  • Gamepad jump impulse is -500.0f px/s (upward) for both ground and vine dismount.
  • on_ground is set to 0 immediately so the jump condition fires only once.
  • The sound is guarded by if (snd_jump) to tolerate a failed WAV load.

Vine Climbing

When the player presses UP (and is not holding Space), the input handler searches for the nearest vine within grab range (VINE_W + 2 x VINE_GRAB_PAD = 24 px wide). If found:

  1. player->on_vine = 1, player->vine_index is set to the vine's index.
  2. The player snaps horizontally to centre on the vine.
  3. Gravity is disabled while on_vine == 1.
  4. UP/DOWN keys control vertical movement at CLIMB_SPEED (80 px/s).
  5. LEFT/RIGHT keys allow horizontal drift at CLIMB_H_SPEED (80 px/s).
  6. Pressing Space while on a vine triggers a dismount: on_vine = 0, vy = -325.0f (keyboard) or -500.0f (gamepad).
  7. The ANIM_CLIMB state plays (row 4, 2 frames at 100 ms each); the animation freezes when the player is stationary on the vine.

Walk / Run Mode

Hold Shift (keyboard) or Right Trigger (gamepad) to run. Walking uses WALK_MAX_SPEED (100 px/s) with WALK_GROUND_ACCEL (750 px/s^2). Running uses RUN_MAX_SPEED (250 px/s) with RUN_GROUND_ACCEL (600 px/s^2). The player decelerates via GROUND_FRICTION (550 px/s^2) when no input is held, and GROUND_COUNTER_ACCEL (100 px/s^2) is added when reversing direction. Air movement uses reduced acceleration (AIR_ACCEL_WALK = 350, AIR_ACCEL_RUN = 180) and AIR_FRICTION (80 px/s^2).


Physics -- player_update

void player_update(Player *player, float dt,
                   const Platform *platforms, int platform_count,
                   const FloatPlatform *float_platforms, int float_platform_count,
                   const Bouncepad *bouncepads, int bouncepad_count,
                   const VineDecor *vines, int vine_count,
                   const LadderDecor *ladders, int ladder_count,
                   const RopeDecor *ropes, int rope_count,
                   const Bridge *bridges, int bridge_count,
                   const SpikePlatform *spike_platforms, int spike_platform_count,
                   const int *floor_gaps, int floor_gap_count,
                   int *out_bounce_idx,
                   int *out_fp_landed_idx,
                   int prev_fp_landed_idx);

dt is the time in seconds since the last frame (e.g. 0.016 for 60 FPS). Multiplying speed by dt makes movement frame-rate independent. The function resolves collisions against the floor, one-way platforms, float platforms, bridges, spike platforms, bouncepads, vines, ladders, and ropes.

  • *out_bounce_idx is set to the bouncepad index if the player lands on one; callers initialise to -1.
  • *out_fp_landed_idx is set to the float platform index if the player lands on one; used to drive the crumble timer and nudge the player along with moving rail platforms.
  • prev_fp_landed_idx is the float platform the player was standing on last frame -- needed for the "stay on" check when a platform moves upward.

Gravity

player->on_ground = 0;          // reset every frame — walk-off edges start falling immediately
player->vy += GRAVITY * dt;     // GRAVITY = 800.0f px/s²; runs unconditionally

on_ground is cleared to 0 at the start of every player_update call so the player immediately begins falling when they walk off a platform edge. Gravity then runs unconditionally; the floor/platform snap below cancels the tiny fall each frame while the player stands on a surface, keeping them rock-solid on the ground.

Position Integration

player->x += player->vx * dt;
player->y += player->vy * dt;

Floor Collision

float floor_snap = (float)(FLOOR_Y - player->h + FLOOR_SINK);
// = 252 - 48 + 16 = 220

if (player->y >= floor_snap) {
    player->y        = floor_snap;
    player->vy       = 0.0f;
    player->on_ground = 1;
}

When the player's bottom edge reaches the floor surface, position is snapped, vy is zeroed, and on_ground becomes 1.

Horizontal Clamp

if (player->x + PHYS_PAD_X < 0.0f)
    player->x = -(float)PHYS_PAD_X;
if (player->x + player->w - PHYS_PAD_X > world_w)
    player->x = (float)(world_w - player->w + PHYS_PAD_X);

Keeps the player's physics body (inset by PHYS_PAD_X = 15 px on each side) inside the full WORLD_W (dynamic, default 1600 px) scrollable world. The transparent side-padding of the sprite frame is allowed to slide off-screen while the visible character stays flush with the world border.

Ceiling Clamp

if (player->y + PHYS_PAD_TOP < 0.0f) {
    player->y  = -(float)PHYS_PAD_TOP;
    player->vy = 0.0f;
}

Stops upward movement when the physics top edge (y + PHYS_PAD_TOP) hits the canvas ceiling. PHYS_PAD_TOP = 18 lets the transparent head-room of the sprite frame slide above y = 0 before the physics edge triggers.


Animation -- player_animate (static)

Called at the end of player_update. Selects the correct AnimState based on physics state, advances the frame timer, and updates player->frame (the source rect cutting into the sprite sheet).

State Selection

AnimState target;
if (player->on_vine) {
    target = ANIM_CLIMB;
} else if (!player->on_ground) {
    target = (player->vy < 0.0f) ? ANIM_JUMP : ANIM_FALL;
} else if (player->vx != 0.0f) {
    target = ANIM_WALK;
} else {
    target = ANIM_IDLE;
}
Condition Selected State
Climbing a vine (on_vine == 1) ANIM_CLIMB
Airborne + moving up (vy < 0) ANIM_JUMP
Airborne + moving down (vy >= 0) ANIM_FALL
On ground + horizontal velocity ANIM_WALK
On ground + no movement ANIM_IDLE

Climb freeze: When ANIM_CLIMB is active but the player has zero vertical velocity (stationary on vine), the animation timer is paused -- the climb sprite holds its current frame.

Frame Timer

player->anim_timer_ms += dt_ms;
if (player->anim_timer_ms >= frame_duration) {
    player->anim_timer_ms -= frame_duration;   // carry-over, not reset
    player->anim_frame_index =
        (player->anim_frame_index + 1) % ANIM_FRAME_COUNT[state];
}

Leftover time carries into the next frame to keep animation speed accurate across variable frame rates.

Animation Table

State Row Frames ms/frame Total cycle
ANIM_IDLE 0 4 150 600 ms
ANIM_WALK 1 4 100 400 ms
ANIM_JUMP 2 2 150 300 ms
ANIM_FALL 3 1 200 200 ms
ANIM_CLIMB 4 2 100 200 ms

Source Rect Update

player->frame.x = player->anim_frame_index * FRAME_W;   // col x 48
player->frame.y = ANIM_ROW[player->anim_state]  * FRAME_H;  // row x 48

Rendering -- player_render

void player_render(Player *player, SDL_Renderer *renderer, int cam_x);
/* Invincibility blink: skip every alternate 100 ms window */
if (player->hurt_timer > 0.0f) {
    int interval = (int)(player->hurt_timer * 1000.0f) / 100;
    if (interval % 2 == 1) return;   /* blink off — skip this frame */
}

SDL_Rect dst = {
    .x = (int)player->x - cam_x,  // world → screen: subtract camera offset
    .y = (int)player->y,
    .w = player->w,         // 48
    .h = player->h          // 48
};

SDL_RendererFlip flip = player->facing_left
    ? SDL_FLIP_HORIZONTAL
    : SDL_FLIP_NONE;

SDL_RenderCopyEx(renderer, player->texture, &player->frame, &dst,
                 0.0, NULL, flip);

SDL_RenderCopyEx is used (instead of SDL_RenderCopy) to support horizontal flipping. Angle and center are 0 / NULL so no rotation is applied.

Invincibility blink: While player->hurt_timer > 0, player_render converts the remaining time into a 100 ms cadence -- interval = (int)(player->hurt_timer * 1000.0f) / 100. On odd intervals the function returns early, skipping the draw call and making the sprite flash to indicate temporary invincibility.


Hitbox -- player_get_hitbox

SDL_Rect player_get_hitbox(const Player *player);

Returns an SDL_Rect representing the player's tightly-inset physics hitbox in logical pixels. The hitbox is smaller than the full 48x48 display frame to exclude transparent padding in the sprite sheet. It is used by game_loop for AABB intersection tests against spider enemies.

Inset Constant Value Effect
Left and Right PHYS_PAD_X 15 px Physics width = 48 - 30 = 18 px
Top PHYS_PAD_TOP 18 px Physics top tracks the character's head
Bottom FLOOR_SINK 16 px Physics bottom tracks the character's feet
SDL_Rect r;
r.x = (int)(player->x) + PHYS_PAD_X;
r.y = (int)(player->y) + PHYS_PAD_TOP;
r.w = player->w - 2 * PHYS_PAD_X;
r.h = player->h - PHYS_PAD_TOP - FLOOR_SINK;
return r;

game_loop calls player_get_hitbox each frame (when hurt_timer == 0) and passes the result to SDL_HasIntersection alongside each spider's rect. On overlap, hurt_timer is set to 1.5 seconds.


Cleanup -- player_cleanup

void player_cleanup(Player *player) {
    if (player->texture) {
        SDL_DestroyTexture(player->texture);
        player->texture = NULL;
    }
}

Must be called before SDL_DestroyRenderer, because textures are owned by the renderer.


Reset -- player_reset

void player_reset(Player *player);

Resets the player's position and state to the starting values without reloading the texture. Called by game_loop when the player loses a life (hearts reach 0). Because the GPU texture is already loaded, only the position, velocity, on_ground, and animation fields need to be re-initialised -- the same player.png texture handle is reused directly.

Action Detail
Position Reset to pillar 0 (x=80), snapped to pillar top surface
Velocity vx = vy = 0.0f
on_ground 1
hurt_timer 0.0f (no invincibility)
Animation ANIM_IDLE, frame 0
Texture unchanged -- reuses the already-loaded handle

Physics Constants Reference

Constant Value Location
GRAVITY 800.0f px/s^2 game.h
FLOOR_Y 252 px game.h (GAME_H - TILE_SIZE)
Jump impulse vy (keyboard) -325.0f px/s player.c (hard-coded)
Jump impulse vy (gamepad) -500.0f px/s player.c (hard-coded)
CLIMB_SPEED 80.0f px/s player.c (local #define)
CLIMB_H_SPEED 80.0f px/s player.c (local #define)
VINE_GRAB_PAD 4 px player.c (local #define)
Walk max speed 100.0f px/s player.c (WALK_MAX_SPEED)
Run max speed 250.0f px/s player.c (RUN_MAX_SPEED)
FLOOR_SINK 16 px player.c (local #define)
FRAME_W / FRAME_H 48 px player.c (local #define)

Sounds

← Home


All audio files live in the assets/sounds/ directory, organized by category (collectibles/, entities/, hazards/, levels/, player/, screens/, surfaces/). All sound files use the .wav format. SDL2_mixer handles both short sound effects (Mix_Chunk via Mix_LoadWAV) and streaming music (Mix_Music via Mix_LoadMUS).


Currently Used Sounds

File Type GameState Field Description
sounds/player/player_jump.wav Mix_Chunk gs->snd_jump Played on Space press when on_ground == 1
sounds/collectibles/coin.wav Mix_Chunk gs->snd_coin Played when the player collects a coin
sounds/player/player_hit.wav Mix_Chunk gs->snd_hit Played when the player takes damage
sounds/surfaces/bouncepad.wav Mix_Chunk gs->snd_spring Played when the player lands on a bouncepad
sounds/entities/bird.wav Mix_Chunk gs->snd_flap Played for bird enemy wing flap
sounds/entities/spider.wav Mix_Chunk gs->snd_spider_attack Played for spider enemy attack
sounds/entities/fish.wav Mix_Chunk gs->snd_dive Played for fish enemy dive
sounds/hazards/axe_trap.wav Mix_Chunk gs->snd_axe Played for axe trap swing
music_path (per level) Mix_Music gs->music Background music loop, configured per level in TOML, loaded via Mix_LoadMUS (streaming)

Unused Sounds

Other Available Sounds

Additional sounds available in assets/sounds/ by category:

File Category Description
sounds/screens/confirm_ui.wav Screens Menu confirmation / level complete
sounds/levels/water.wav Levels Water ambience
sounds/levels/lava.wav Levels Lava ambience
sounds/levels/winds.wav Levels Wind gust / outdoor ambience

Unused Sounds

The following sounds are stored in assets/sounds/unused/ and are not loaded by the game.

File Description
fireball.wav Projectile / fireball effect
saw.wav Circular saw spinning

Audio Configuration

SDL2_mixer is opened in main.c with:

Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);
Parameter Value Meaning
Frequency 44100 Hz CD quality
Format MIX_DEFAULT_FORMAT 16-bit signed samples
Channels 2 Stereo
Chunk size 2048 samples ~46 ms buffer at 44100 Hz

Sound Effects vs. Music

Aspect Mix_Chunk (sound effects) Mix_Music (background music)
API Mix_LoadWAV, Mix_PlayChannel Mix_LoadMUS, Mix_PlayMusic
Loading Fully decoded into RAM Streamed from disk
Best for Short, triggered sounds Long looping tracks
Simultaneous Multiple channels One at a time
Volume Mix_VolumeChunk Mix_VolumeMusic

All eight sound effects use Mix_LoadWAV and are fully loaded into memory. The background music (configured per level via music_path) uses Mix_LoadMUS which streams from disk, keeping memory usage low.


Adding a New Sound Effect

  1. Place the .wav file in sounds/.
  2. Add a Mix_Chunk *snd_<name> field to GameState in game.h.
  3. Load it in game_init:
gs->snd_<name> = Mix_LoadWAV("assets/sounds/<category>/<name>.wav");
if (!gs->snd_<name>) {
    fprintf(stderr, "Warning: failed to load <name>.wav: %s\n", Mix_GetError());
    /* Non-fatal — game continues without this sound */
}
  1. Free it in game_cleanup (before SDL_DestroyRenderer):
if (gs->snd_<name>) {
    Mix_FreeChunk(gs->snd_<name>);
    gs->snd_<name> = NULL;
}
  1. Play it wherever the event occurs:
if (gs->snd_<name>) Mix_PlayChannel(-1, gs->snd_<name>, 0);

The if guard is important: if the WAV fails to load for any reason, the game continues without crashing.


Adding a New Music Track

// Load (streaming — not fully decoded into RAM)
gs->music = Mix_LoadMUS("assets/sounds/levels/new_track.wav");
if (!gs->music) { /* handle error */ }

// Start (loop forever)
Mix_PlayMusic(gs->music, -1);

// Volume (0-128)
Mix_VolumeMusic(64);  // 50%

// Stop and free
Mix_HaltMusic();
Mix_FreeMusic(gs->music);
gs->music = NULL;

Mix_Music streams from disk; it does not load the entire file into RAM. This keeps memory usage low for large audio files.

Source Files

← Home


File Map

src/
├── main.c                        Entry point -- SDL subsystem lifecycle
├── game.h                        Shared constants + GameState struct (included everywhere)
├── game.c                        Window, renderer, textures, sounds, game loop
├── collectibles/
│   ├── coin.h / .c               Coin collectible: placement, AABB collection, render
│   ├── star_yellow.h / .c        Star yellow health pickup
│   ├── star_green.h / .c         Star green health pickup
│   ├── star_red.h / .c           Star red health pickup
│   └── last_star.h / .c          End-of-level star collectible
├── core/
│   ├── debug.h / .c              Debug overlay: FPS counter, collision hitboxes, event log
│   └── entity_utils.h / .c      Shared entity helper functions
├── effects/
│   ├── fog.h / .c                Atmospheric fog overlay: init, slide, spawn, render
│   ├── parallax.h / .c           Multi-layer scrolling background: init, tiled render, cleanup
│   └── water.h / .c              Animated water strip: init, scroll, tile render
├── entities/
│   ├── spider.h / .c             Spider enemy: ground patrol, animation, render
│   ├── jumping_spider.h / .c     Jumping spider: patrol, jump arcs, floor-gap awareness
│   ├── bird.h / .c               Slow bird enemy: sine-wave sky patrol, animation
│   ├── faster_bird.h / .c        Fast bird enemy: tighter sine-wave, faster animation
│   ├── fish.h / .c               Fish enemy: patrol, random jump arcs, render
│   └── faster_fish.h / .c        Fast fish enemy: higher jumps, faster patrol
├── hazards/
│   ├── spike.h / .c              Static ground spike hazard rows
│   ├── spike_block.h / .c        Rail-riding rotating spike hazard
│   ├── spike_platform.h / .c     Elevated spike surface hazard
│   ├── circular_saw.h / .c       Fast rotating patrol saw hazard
│   ├── axe_trap.h / .c           Swinging/spinning axe hazard
│   ├── blue_flame.h / .c         Blue flame hazard: erupts from floor gaps, rise/flip/fall cycle
│   └── fire_flame.h / .c         Fire flame hazard: fire-colored variant (reuses BlueFlame struct)
├── levels/
│   ├── level.h                   LevelDef struct and placement types
│   ├── level_loader.h / .c       Level loading from LevelDef
│   └── exported/                 Auto-generated C level exports
├── editor/
│   ├── editor.h / .c             Editor state and main loop
│   ├── editor_main.c             Editor entry point
│   ├── canvas.h / .c             Canvas rendering and interaction
│   ├── palette.h / .c            Entity palette panel
│   ├── properties.h / .c         Entity property inspector
│   ├── tools.h / .c              Editor tools (select, place, erase)
│   ├── ui.h / .c                 Editor UI framework
│   ├── undo.h / .c               Undo/redo stack
│   ├── serializer.h / .c         TOML level serialization
│   ├── exporter.h / .c           C level export
│   └── file_dialog.h / .c        Native file dialog integration
├── player/
│   └── player.h / .c             Player input, physics, animation, rendering
├── screens/
│   ├── start_menu.h / .c         Start menu screen with logo
│   └── hud.h / .c                HUD renderer: hearts, lives counter, score text
└── surfaces/
    ├── platform.h / .c           One-way platform pillar init and 9-slice rendering
    ├── float_platform.h / .c     Hovering platform: static, crumble, and rail behaviours
    ├── bridge.h / .c             Tiled crumble walkway: init, cascade-fall, render
    ├── bouncepad.h / .c          Shared bouncepad mechanics (squash/release animation)
    ├── bouncepad_small.h / .c    Green bouncepad (small jump)
    ├── bouncepad_medium.h / .c   Wood bouncepad (medium jump)
    ├── bouncepad_high.h / .c     Red bouncepad (high jump)
    ├── rail.h / .c               Rail path builder, bitmask tile render, position interpolation
    ├── vine.h / .c               Climbable vine decoration
    ├── ladder.h / .c             Climbable ladder decoration
    └── rope.h / .c               Climbable rope decoration

Every .c file in src/ and its subdirectories is automatically picked up by the Makefile wildcard -- no changes to the build system are needed when adding new source files.


main.c

Role: Owns the program entry point and every SDL subsystem's lifetime.

Responsibilities

  • Parse CLI flags: --debug and --level <path>
  • Call SDL_Init, IMG_Init, TTF_Init, Mix_OpenAudio in order
  • Route to start menu or level mode via --level flag
  • Tear down SDL subsystems in reverse order before returning

Subsystem Init Order

Order Call Purpose
1 SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) Core: window + audio device
2 IMG_Init(IMG_INIT_PNG) PNG decoder
3 TTF_Init() FreeType / font support
4 Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) Audio mixer

On failure at any step, all previously-succeeded subsystems are torn down before returning EXIT_FAILURE.


game.h

Role: The single shared header. Defines constants and GameState. Included by all other .c files.

Constants

See Constants Reference for full details.

#define WINDOW_TITLE  "Super Mango"
#define WINDOW_W      800
#define WINDOW_H      600
#define TARGET_FPS    60
#define GAME_W        400
#define GAME_H        300
#define TILE_SIZE     48
#define FLOOR_Y       (GAME_H - TILE_SIZE)   // = 252
#define GRAVITY       800.0f
#define WORLD_W       1600
#define CAM_LOOKAHEAD_VX_FACTOR 0.20f
#define CAM_LOOKAHEAD_MAX       50.0f
#define CAM_SMOOTHING           8.0f
#define CAM_SNAP_THRESHOLD      0.5f
#define FLOOR_GAP_W             32
#define MAX_FLOOR_GAPS          16

Includes

#include "player.h"          // Player struct
#include "platform.h"        // Platform struct + MAX_PLATFORMS
#include "water.h"           // Water struct
#include "fog.h"             // FogSystem struct
#include "spider.h"          // Spider struct + MAX_SPIDERS
#include "fish.h"            // Fish struct + MAX_FISH
#include "coin.h"            // Coin struct + MAX_COINS
#include "vine.h"            // VineDecor struct + MAX_VINES
#include "bouncepad.h"       // Bouncepad struct (shared mechanics)
#include "bouncepad_small.h" // Small bouncepad
#include "bouncepad_medium.h"// Medium bouncepad
#include "bouncepad_high.h"  // High bouncepad
#include "hud.h"             // Hud struct
#include "parallax.h"        // ParallaxSystem
#include "rail.h"            // Rail, RailTile
#include "spike_block.h"     // SpikeBlock
#include "float_platform.h"  // FloatPlatform
#include "bridge.h"          // Bridge
#include "jumping_spider.h"  // JumpingSpider
#include "bird.h"            // Bird
#include "faster_bird.h"     // FasterBird
#include "star_yellow.h"     // StarYellow (also used for green/red)
#include "axe_trap.h"        // AxeTrap
#include "circular_saw.h"    // CircularSaw
#include "blue_flame.h"      // BlueFlame
#include "ladder.h"          // LadderDecor
#include "rope.h"            // RopeDecor
#include "faster_fish.h"     // FasterFish
#include "last_star.h"       // LastStar
#include "spike.h"           // SpikeRow
#include "spike_platform.h"  // SpikePlatform
#include "entity_utils.h"    // Shared entity helpers
#include "debug.h"           // DebugOverlay

Function Declarations

void game_init(GameState *gs);
void game_loop(GameState *gs);
void game_cleanup(GameState *gs);

game.c

Role: Implements the three game lifecycle functions.

game_init(GameState *gs)

Creates all runtime resources:

  1. Window + renderer + logical size (400x300)
  2. Parallax background (7 layers)
  3. Floor tile (grass_tileset.png) and platform tile (platform.png)
  4. Entity textures: spider.png, jumping_spider.png, bird.png, faster_bird.png, fish.png, faster_fish.png, coin.png, bouncepad_medium.png, bouncepad_small.png, bouncepad_high.png, vine.png, ladder.png, rope.png, rail.png, spike_block.png, float_platform.png, bridge.png, star_yellow.png, star_green.png, star_red.png, axe_trap.png, circular_saw.png, blue_flame.png, fire_flame.png, spike.png, spike_platform.png
  5. Sound effects: bouncepad.wav, axe_trap.wav, bird.wav, spider.wav, fish.wav, player_jump.wav, coin.wav, player_hit.wav
  6. Background music: per-level music_path (via Mix_LoadMUS)
  7. Entity init: player, water, fog, HUD, debug, level (via level loader)
  8. Gamepad controller init

game_loop(GameState *gs)

60 FPS loop: delta time -> events -> update -> render. See Architecture for the full render order.

game_cleanup(GameState *gs)

Frees all resources in reverse init order.


player.h / player.c

Role: Player character lifecycle. See Player Module for the deep dive.

Key functions: player_init, player_handle_input, player_update, player_render, player_get_hitbox, player_reset, player_cleanup


level.h / level_loader.h / level_loader.c

Role: Level definitions and loading. level.h defines the LevelDef struct and placement types. level_loader loads level data from TOML files and initialises all entities accordingly.

Key functions:

  • level_loader_load(GameState *gs, const char *path) -- parse a TOML level file, place all entities, define floor gaps (called once from game_init)
  • level_loader_reset(GameState *gs, int *fp_prev_riding) -- reset all entities on player death

Editor Modules (src/editor/)

Role: Standalone visual level editor (11 modules). Build with make editor, run with make run-editor.

Modules: editor.h/.c, editor_main.c, canvas, palette, properties, tools, ui, undo, serializer, exporter, file_dialog


start_menu.h / start_menu.c

Role: Start menu screen with centred title text and start_menu_logo.png logo.

Key functions: start_menu_init, start_menu_loop, start_menu_cleanup


Enemy Modules

spider.h / spider.c

Ground-patrol spider enemy with 3-frame walk animation. Reverses at patrol boundaries and respects floor gaps. Asset: spider.png.

jumping_spider.h / jumping_spider.c

Faster spider variant that periodically jumps in short arcs to clear floor gaps. Asset: jumping_spider.png.

bird.h / bird.c

Slow sine-wave sky patrol bird. Asset: bird.png.

faster_bird.h / faster_bird.c

Fast aggressive sine-wave sky patrol bird with tighter curves and quicker wing animation. Asset: faster_bird.png.

fish.h / fish.c

Jumping water enemy that patrols the bottom lane and leaps on random arcs. Asset: fish.png.

faster_fish.h / faster_fish.c

Fast fish variant with higher jumps and faster patrol speed. Asset: faster_fish.png.


Hazard Modules

blue_flame.h / blue_flame.c

Erupting fire hazard from floor gaps. Cycles through waiting -> rising -> flipping (180 degree rotation at apex) -> falling. 2-frame animation. Asset: blue_flame.png.

spike.h / spike.c

Static ground spike rows placed along the floor. Asset: spike.png.

spike_block.h / spike_block.c

Rail-riding rotating hazard (360 degrees/s spin). Travels along rail paths, pushes player on collision. Asset: spike_block.png.

spike_platform.h / spike_platform.c

Elevated spike surface hazard. 3-slice rendered. Asset: spike_platform.png.

circular_saw.h / circular_saw.c

Fast rotating patrol saw hazard (720 degrees/s). Patrols horizontally. Asset: circular_saw.png.

axe_trap.h / axe_trap.c

Swinging pendulum or spinning axe hazard. Two behaviour modes: swing (60 degree amplitude) and spin (180 degrees/s). Asset: axe_trap.png.

fire_flame.h / fire_flame.c

Fire-colored variant of the blue flame hazard. Reuses the BlueFlame struct and functions. Erupts from floor gaps with the same rise/flip/fall cycle. Asset: fire_flame.png.


Collectible Modules

coin.h / coin.c

Gold coin collectible. AABB pickup awards 100 points. Every 3 coins restores one heart. Asset: coin.png.

star_yellow.h / star_yellow.c

Star yellow health pickup that restores hearts. Asset: star_yellow.png.

star_green.h / star_green.c

Star green health pickup that restores hearts. Asset: star_green.png.

star_red.h / star_red.c

Star red health pickup that restores hearts. Asset: star_red.png.

last_star.h / last_star.c

End-of-level star collectible. Asset: last_star.png.


Platform Modules

platform.h / platform.c

One-way pillar platforms built from 9-slice tiled grass blocks. Player can jump through from below and land on top. Asset: platform.png.

float_platform.h / float_platform.c

Hovering surfaces with three modes: static, crumble (falls after 0.75s), and rail (follows a rail path). 3-slice rendered. Asset: float_platform.png.

bridge.h / bridge.c

Tiled crumble walkway. Bricks cascade-fall outward from the player's feet after a short delay. Asset: bridge.png.

bouncepad.h / bouncepad.c

Shared bouncepad mechanics: squash/release 3-frame animation.

bouncepad_small.h / bouncepad_small.c

Green bouncepad -- small jump height. Asset: bouncepad_small.png.

bouncepad_medium.h / bouncepad_medium.c

Wood bouncepad -- medium jump height. Asset: bouncepad_medium.png.

bouncepad_high.h / bouncepad_high.c

Red bouncepad -- high jump height. Asset: bouncepad_high.png.


Decoration Modules

vine.h / vine.c

Climbable vine decoration. Player can grab, climb up/down, drift horizontally, and dismount with a jump. Asset: vine.png.

ladder.h / ladder.c

Climbable ladder decoration. Same climbing mechanics as vines. Asset: ladder.png.

rope.h / rope.c

Climbable rope decoration. Same climbing mechanics as vines. Asset: rope.png.


Environment Modules

water.h / water.c

Animated scrolling water strip at the bottom of the screen. 8 frames tiled seamlessly. Asset: water.png.

fog.h / fog.c

Atmospheric fog overlay. Two semi-transparent sky layers slide across the screen with random direction, duration, and fade-in/out. Assets: fog_background_1.png, fog_background_2.png.

parallax.h / parallax.c

Multi-layer scrolling background. Up to 8 layers tiled horizontally, each at a different scroll speed configured per level via [[background_layers]] in TOML. Assets: assets/sprites/backgrounds/ (sky_blue, sky_fire, clouds, glacial_mountains, volcanic_mountains, forest_leafs, castle_pillars, smoke variants, etc.).


System Modules

rail.h / rail.c

Rail path system. Builds closed-loop and open-line rail paths from tile definitions. 4x4 bitmask tileset for rendering. Used by spike blocks and float platforms. Asset: rail.png.

hud.h / hud.c

HUD renderer. Draws heart icons (health), player icon + lives counter, coin icon + score. Assets: star_yellow.png (hearts), hud_coins.png (coin icon), player.png (lives icon), round9x13.ttf (font).

debug.h / debug.c

Debug overlay (activated with --debug flag). FPS counter, collision hitbox visualization for all entities, and a scrolling event log.