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
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
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:
Playeris embedded by value, not a pointer. This avoids a heap allocation and keeps the struct self-contained. The same applies toPlatform,Water,FogSystem, and all entity arrays.- Every pointer is set to
NULLafter freeing, making accidental double-frees safe. - Initialised with
GameState gs = {0}so every field starts as0/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
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
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:
- Creates
out/directory if it does not exist - Compiles each
src/**/*.c→src/**/*.o - Links all
.ofiles →out/super-mango - On macOS (
uname -s == Darwin), ad-hoc code signs the binary withcodesign --force --sign - $@(required on Apple Silicon to avoidKilled: 9errors). 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 subdirectoriessrc/**/*.d-- all generated dependency filesout/-- 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)
- Install MSYS2
- 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
- Build:
cd /c/path/to/super-mango-editor
make
- 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
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_Hfor 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
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),gcccompatible
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
NULLimmediately after freeing. (SDL_Destroy*andfree()onNULLare no-ops, preventing double-free crashes.) - Error paths call
SDL_GetError()/IMG_GetError()/Mix_GetError()and write tostderr. - Resources are always freed in reverse init order.
- Use
floatfor positions and velocities; cast tointonly at render time (SDL_Rectfields areint).
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:
- Place
.wavinassets/sounds/<category>/. - Add
Mix_Chunk *snd_<name>;toGameStateingame.h. - 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());
}
- Free in
game_cleanup:
if (gs->snd_<name>) { Mix_FreeChunk(gs->snd_<name>); gs->snd_<name> = NULL; }
- 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>.hwith struct and function declarations - Create
src/<entity>.cwith init, update, render, cleanup - Add
#include "<entity>.h"togame.h - Add texture pointer, entity array, and count to
GameState(by value, not pointer) - Load texture in
game_initingame.c - Call
<entity>_initingame_init - Call
<entity>_updateingame_loopupdate section - Call
<entity>_renderingame_looprender section (correct layer order) - Call
<entity>_cleanupingame_cleanup(beforeSDL_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_logcalls ingame.cfor significant entity events - Build with
make-- no Makefile changes needed - Test with
--debugflag to verify hitboxes render correctly
Related Pages
- Home -- project overview
- Architecture -- system design and game loop
- Build System -- compiling and running
- Source Files -- module-by-module reference
- Assets -- sprite sheets and textures
- Sounds -- audio files and music
- Player Module -- player-specific details
- Constants Reference -- all defined constants
Player Module
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.0fpx/s (upward) for both ground and vine dismount. - Gamepad jump impulse is
-500.0fpx/s (upward) for both ground and vine dismount. on_groundis set to0immediately 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:
player->on_vine = 1,player->vine_indexis set to the vine's index.- The player snaps horizontally to centre on the vine.
- Gravity is disabled while
on_vine == 1. - UP/DOWN keys control vertical movement at
CLIMB_SPEED(80 px/s). - LEFT/RIGHT keys allow horizontal drift at
CLIMB_H_SPEED(80 px/s). - Pressing Space while on a vine triggers a dismount:
on_vine = 0,vy = -325.0f(keyboard) or-500.0f(gamepad). - The
ANIM_CLIMBstate 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_idxis set to the bouncepad index if the player lands on one; callers initialise to-1.*out_fp_landed_idxis 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_idxis 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_CLIMBis 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_renderconverts 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
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
- Place the
.wavfile insounds/. - Add a
Mix_Chunk *snd_<name>field toGameStateingame.h. - 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 */
}
- Free it in
game_cleanup(beforeSDL_DestroyRenderer):
if (gs->snd_<name>) {
Mix_FreeChunk(gs->snd_<name>);
gs->snd_<name> = NULL;
}
- 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
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:
--debugand--level <path> - Call
SDL_Init,IMG_Init,TTF_Init,Mix_OpenAudioin order - Route to start menu or level mode via
--levelflag - 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:
- Window + renderer + logical size (400x300)
- Parallax background (7 layers)
- Floor tile (
grass_tileset.png) and platform tile (platform.png) - 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 - Sound effects:
bouncepad.wav,axe_trap.wav,bird.wav,spider.wav,fish.wav,player_jump.wav,coin.wav,player_hit.wav - Background music: per-level
music_path(viaMix_LoadMUS) - Entity init: player, water, fog, HUD, debug, level (via level loader)
- 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 fromgame_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.