GUIDE / Developer Guide
Developer Guide
Coding conventions, safe extension patterns, entity workflow, and contribution rules.
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, coins_render |
| 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 under assets/sprites/<category>/ | player/player.png, collectibles/coin.png, entities/spider.png |
| Sounds | component_descriptor.wav under assets/sounds/<category>/ | player/player_jump.wav, collectibles/coin.wav, entities/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
Most active entities follow this lifecycle pattern:
entity_init -> set initial state (textures often live shared in GameState)
entity_update -> move, apply physics, detect events
entity_render -> draw to renderer
entity_cleanup -> SDL_DestroyTexture, set to NULL
Collectibles and simple decorations may use lighter helpers. For example, coins store only placement state in Coin and render through coins_render() using a shared texture from GameState.
Active entities may also expose:
entity_handle_input -> if player-controlled
entity_animate -> static helper, called from entity_update
Step-by-Step
1. Create the header — coin-like collectible example
#pragma once
#include <SDL.h>
#define MAX_COINS 64
#define COIN_DISPLAY_W 16
#define COIN_DISPLAY_H 16
#define COIN_SCORE 100
typedef struct {
float x; /* logical position (top-left) */
float y;
int active; /* 1 = visible, 0 = collected */
} Coin;
void coins_render(const Coin *coins, int count,
SDL_Renderer *renderer, SDL_Texture *tex, int cam_x);
2. Create the implementation — src/collectibles/coin.c
#include "collectibles/coin.h"
void coins_render(const Coin *coins, int count,
SDL_Renderer *renderer, SDL_Texture *tex, int cam_x) {
if (!tex) return;
for (int i = 0; i < count; i++) {
if (!coins[i].active) continue;
SDL_Rect dst = {
(int)(coins[i].x - cam_x),
(int)coins[i].y,
COIN_DISPLAY_W,
COIN_DISPLAY_H
};
SDL_RenderCopy(renderer, tex, NULL, &dst);
}
}
The Makefile picks up coin.c automatically from the src/collectibles/ subdirectory — no Makefile changes needed.
3. Add texture to TextureResources in game.h
Textures are loaded in game_init() and stored under gs->textures. The entity array and count live directly in GameState:
#include "collectibles/coin.h"
typedef struct {
// ... existing fields ...
TextureResources textures; /* contains SDL_Texture *coin */
Coin coins[MAX_COINS]; /* fixed-size array -- simple and cache-friendly */
int coin_count; /* how many are currently active */
} GameState;
4. Wire up in the runtime core
// src/core/game_resources.c -- load shared texture:
gs->textures.coin = IMG_LoadTexture(gs->renderer, "assets/sprites/collectibles/coin.png");
if (!gs->textures.coin) {
fprintf(stderr, "Failed to load coin.png: %s\n", IMG_GetError());
return -1;
}
// level_loader.c -- populate array from TOML placements:
gs->coins[i] = (Coin){ .x = def->coins[i].x, .y = def->coins[i].y, .active = 1 };
gs->coin_count = def->coin_count;
// focused runtime helper render section, in the correct layer order:
coins_render(gs->coins, gs->coin_count, gs->renderer, gs->textures.coin, (int)gs->camera.x);
// src/core/game_lifecycle.c cleanup path, before SDL_DestroyRenderer:
DESTROY_TEX(gs->textures.coin);
Use the focused runtime module that owns the behavior: resource loading belongs in src/core/game_resources.c, lifecycle orchestration in src/core/game_lifecycle.c, per-frame update orchestration in src/core/game_update.c and its specialized helpers, and collision/pickup behavior in src/collision/.
5. Add to a TOML level file
Entity spawn positions are defined in TOML level files in the levels/ directory. Add your entity’s array table entry there:
# In levels/your_level.toml:
[[coins]]
x = 120.0
y = 180.0
[[coins]]
x = 200.0
y = 140.0
Then extend level_loader.c to parse the new array table and populate the GameState array (or call your _init function when the entity owns richer runtime state). See level_design for the full TOML schema and Level Design — TOML Reference for placement examples for every entity type.
You can also use the visual level editor (make run-editor) to place entities interactively without writing TOML by hand.
6. Add debug hitbox — src/core/debug.c
Every entity must have hitbox visualization in core/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,
COIN_DISPLAY_W, COIN_DISPLAY_H };
SDL_SetRenderDrawColor(gs->renderer, 255, 255, 0, 128);
SDL_RenderDrawRect(gs->renderer, &hb);
}
Also add debug_log calls in the module that owns the event, such as src/collision/game_collision.c, src/core/game_update.c, or the relevant focused runtime helper.
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 *<name>;toAudioResourcesingame.h. - Load in
game_init(non-fatal — warn but continue):
gs->audio.<name> = Mix_LoadWAV("assets/sounds/<category>/<name>.wav");
if (!gs->audio.<name>) {
fprintf(stderr, "Warning: could not load <name>.wav: %s\n", Mix_GetError());
}
- Free in
game_cleanup:
FREE_CHUNK(gs->audio.<name>);
- Play wherever needed:
if (gs->audio.<name>) Mix_PlayChannel(-1, gs->audio.<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). Runtime levels provide the active music path through TOML:
// Load from current LevelDef
gs->audio.music = Mix_LoadMUS(def->music_path);
// Play (looping)
Mix_PlayMusic(gs->audio.music, -1);
Mix_VolumeMusic(64); // 50% -- adjust as needed
// Cleanup
Mix_HaltMusic();
Mix_FreeMusic(gs->audio.music);
gs->audio.music = NULL;
Adding HUD / Text Rendering
SDL2_ttf is already initialized in main.c. The font 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);
int text_w = surf->w;
int text_h = surf->h;
SDL_FreeSurface(surf);
// Draw the texture
SDL_Rect dst = {10, 10, text_w, text_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 32 layers:
1. Parallax background (`assets/sprites/backgrounds/*.png` layers)
2. Platforms (`assets/sprites/levels/*_platform.png`, 9-slice pillars)
3. Floor tiles (level floor tile at FLOOR_Y, with floor-gap openings)
4. Float platforms (`assets/sprites/surfaces/float_platform.png`)
5. Spike rows (`assets/sprites/hazards/spike.png`)
6. Spike platforms (`assets/sprites/hazards/spike_platform.png`)
7. Bridges (`assets/sprites/surfaces/bridge.png`)
8. Bouncepads medium (`assets/sprites/surfaces/bouncepad_medium.png`)
9. Bouncepads small (`assets/sprites/surfaces/bouncepad_small.png`)
10. Bouncepads high (`assets/sprites/surfaces/bouncepad_high.png`)
11. Rails (`assets/sprites/surfaces/rail.png`)
12. Vines (`assets/sprites/surfaces/vine_green.png` / `vine_brown.png`)
13. Ladders (`assets/sprites/surfaces/ladder.png`)
14. Ropes (`assets/sprites/surfaces/rope.png`)
15. Coins (`assets/sprites/collectibles/coin.png`)
16. Yellow stars (`assets/sprites/collectibles/star_yellow.png`)
17. Last star (`assets/sprites/collectibles/last_star.png`)
18. Blue/fire flames (`assets/sprites/hazards/blue_flame.png` / `fire_flame.png`)
19. Fish (`assets/sprites/entities/fish.png`)
20. Faster fish (`assets/sprites/entities/faster_fish.png`)
21. Water (`assets/sprites/foregrounds/water.png`)
22. Spike blocks (`assets/sprites/hazards/spike_block.png`)
23. Axe traps (`assets/sprites/hazards/axe_trap.png`)
24. Circular saws (`assets/sprites/hazards/circular_saw.png`)
25. Spiders (`assets/sprites/entities/spider.png`)
26. Jumping spiders (`assets/sprites/entities/jumping_spider.png`)
27. Birds (`assets/sprites/entities/bird.png`)
28. Faster birds (`assets/sprites/entities/faster_bird.png`)
29. Player (`assets/sprites/player/player.png`)
30. Fog (`assets/sprites/foregrounds/fog_1.png` / `fog_2.png`)
31. HUD (hearts, lives, score -- always on top)
32. 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 .agents/scripts/analyze_sprite.py assets/sprites/<category>/<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/<category>/<entity>.hwith struct and function declarations (e.g.src/entities/,src/collectibles/,src/hazards/,src/surfaces/) - Create
src/<category>/<entity>.cwith init, update, render, cleanup - Add
#include "<category>/<entity>.h"togame.h - Add texture pointer to
TextureResources, plus entity array and count toGameState(by value, not pointer) - Load texture in the resource-loading path (
src/core/game_resources.c) - Call
<entity>_initingame_init - Call
<entity>_updatefrom the relevantsrc/core/update helper - Call
<entity>_renderfrom the relevantsrc/core/render helper (correct layer order) - Call
<entity>_cleanupingame_cleanup(beforeSDL_DestroyRenderer) - Set all freed pointers to
NULL - Add entity placement to a TOML level file in
levels/(or use the visual level editor) - Add hitbox visualization in
core/debug.c - Add
debug_logcalls in the module that owns significant entity events - Build game with
make— no Makefile changes needed for new.cfiles in existing source directories - Build editor with
make editorif editor placement/schema behavior changed - Run
make test - Run
make validate-levelsafter any level/schema/editor serializer change - Test with
--debugflag to verify hitboxes render correctly - Run relevant docs lint/build command when documentation pages changed
Related Pages
- Overview — 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