# 21. Tutorial: a top-down JRPG A town, three people standing in it, a party member who follows you around, and a box that tells you what the elder thinks of the road north. About six hundred lines of C, in `examples/jrpg/`, built by `all` and run headless by `ctest`. This is the **content-pipeline** tutorial. Its subject is the road from a directory of JSON and PNG to a world with people in it: twenty-four sprites, three characters, one Tiled map that spawns its own actors, four-way per-facing animation, and text on top. [Chapter 20](20-tutorial-sidescroller.md) is the physics one — gravity, a jump, a coin — and the two are deliberately complementary. If you want to know how `ey` accumulates, read that one. Everything the chapter shows is quoted out of the program with `excerpt` blocks rather than retyped, so a chapter that no longer matches the game fails the build. The program itself is the specification. Six things in it are workarounds for gaps in the library rather than choices, and each one is called out where you would hit it. They are collected at the end under [What this costs](#what-this-costs). ## Build it and run it ```sh norun cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo cmake --build build --parallel ./build/examples/jrpg/jrpg ``` Arrow keys walk. Space talks to whoever is standing next to you, and dismisses the box again. There is nothing else; it is a tutorial, not a game. Two options exist for the sake of the smoke run. `--frames N` stops after N frames, and `--demo` drives the arrow keys from a script and steps the physics clock by a fixed 1/60 s so that a run is deterministic and takes no wall-clock time at all: ```sh norun SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIODRIVER=dummy \ ./build/examples/jrpg/jrpg --frames 320 --demo ``` Both blocks are `norun`: the first rebuilds the tree the documentation suite is running inside, and the second is registered as a CTest case already, so running it from here would run it twice. ## What is in the directory | File | What it holds | |---|---| | `jrpg.h` | Every declaration, the compile-time asset paths, and the constants | | `jrpg.c` | `main`, startup order, the frame, teardown, the demo script | | `world.c` | Asset loading, the actors the map spawned, walls, the follower | | `textbox.c` | The dialogue panel | | `CMakeLists.txt` | The target, the baked-in asset paths, and the smoke test | The art and the data are in `docs/tutorials/assets/jrpg/`: `tiles.png`, three 32×32 character sheets, `town.tmj`, three character definitions and twenty-four sprite definitions. They are CC0 — `docs/tutorials/assets/PROVENANCE.md` has the row-per-file provenance — so a reader who copies this into their own game inherits no obligation. ## Sprites, then characters, then the map The load order is not a preference. Each step resolves the previous one's output by name through a registry, so getting it wrong fails on every single mapping: - `akgl_character_load_json` looks each sprite up in `AKGL_REGISTRY_SPRITE` and refuses one it cannot find. See [Chapter 11](11-characters.md). - `akgl_tilemap_load` resolves each actor object's `character` property through `AKGL_REGISTRY_CHARACTER` while it spawns the actor. See [Chapter 13](13-tilemaps.md). ```c excerpt=examples/jrpg/world.c for ( c = 0; c < CAST_COUNT; c++ ) { for ( m = 0; m < MOTION_COUNT; m++ ) { for ( f = 0; f < FACING_COUNT; f++ ) { PASS(errctx, sprite_load(CAST[c], MOTIONS[m], FACINGS[f])); } } } for ( c = 0; c < CAST_COUNT; c++ ) { PASS(errctx, character_load(CAST[c])); } ``` Twenty-four sprites, and not one of them is named in the source. The file names are a product of three tables, because a product of three tables is what the naming convention *is* — one sprite per character, per motion, per facing: ```c excerpt=examples/jrpg/world.c static char *CAST[] = { "player", /* jrpg_player jrpg_player_* */ "elder", /* jrpg_elder jrpg_elder_* */ "shopkeeper" /* jrpg_shopkeeper jrpg_shopkeeper_* */ }; static char *MOTIONS[] = { "idle", "walk" }; static char *FACINGS[] = { "up", "down", "left", "right" }; ``` A missing combination then arrives as a load failure naming the file it could not open, at startup. Write the twenty-four names out by hand and a missing one arrives as art that never appears, in the middle of a frame, silently — because an actor with no sprite for its state is skipped rather than reported. That is the same trade the whole error protocol is about: fail early and loudly rather than surprisingly. ### Full four-way idle and walk is eight mappings per character The state-to-sprite lookup is an **exact match on the whole state word**, with no subset fallback — `akgl_character_sprite_get` builds a decimal key out of the `int32_t` and asks the property set for it. [Chapter 11](11-characters.md) covers the consequences; the one that binds here is that four facings times {standing, walking} is eight distinct combinations, and every one of them needs its own mapping or the actor vanishes when it reaches that state: | State | Value | Sprite | |---|---|---| | `ALIVE|FACE_DOWN` | 17 | `jrpg_player_idle_down` | | `ALIVE|FACE_DOWN|MOVING_DOWN` | 1041 | `jrpg_player_walk_down` | | `ALIVE|FACE_LEFT` | 18 | `jrpg_player_idle_left` | | `ALIVE|FACE_LEFT|MOVING_LEFT` | 146 | `jrpg_player_walk_left` | | `ALIVE|FACE_RIGHT` | 20 | `jrpg_player_idle_right` | | `ALIVE|FACE_RIGHT|MOVING_RIGHT` | 276 | `jrpg_player_walk_right` | | `ALIVE|FACE_UP` | 24 | `jrpg_player_idle_up` | | `ALIVE|FACE_UP|MOVING_UP` | 536 | `jrpg_player_walk_up` | There is no diagonal row and there does not need to be one. The default arrow bindings clear *every* facing and movement bit before setting their own (`akgl_actor_cmhf_left_on` and its five siblings all do), so holding two directions moves in whichever was pressed last rather than diagonally. That is documented in [Chapter 16](16-input.md), and it is why an eight-way character sheet would be wasted on the default bindings. ## The map spawns the actors `town.tmj` is 30×20 cells of 16×16 tiles with three layers: `ground`, `decoration`, and an object group holding three objects of type `actor`. | Object `name` | `character` (string) | `state` (int) | At | |---|---|---|---| | `player` | `jrpg_player` | 17 | (224, 256) | | `shopkeeper` | `jrpg_shopkeeper` | 17 | (80, 128) | | `elder` | `jrpg_elder` | 17 | (272, 112) | Loading the map creates all three, publishes them in `AKGL_REGISTRY_ACTOR` under the object's `name`, binds each to its character, and sets its position, visibility and layer. No code in this game creates them. The format's rules — every object needs a `type` string, `state` is an **int** here and not the string array that character JSON accepts, and the tileset must be **embedded** — are [Chapter 13](13-tilemaps.md)'s subject and are not restated. The map also declares `physics.model`, and that is worth a paragraph, because what the library does with it is half of what you would expect: ```c excerpt=examples/jrpg/world.c if ( akgl_gamemap->use_own_physics ) { akgl_physics = &akgl_gamemap->physics; } ``` `akgl_tilemap_load` reads the property, builds a whole `akgl_PhysicsBackend` on the map from it, and sets `use_own_physics`. It does not switch to it. `akgl_game_update` simulates through the global `akgl_physics`, whatever that happens to point at, and nothing in the library ever consults `use_own_physics`. Honouring the map is the caller's job, and the two lines above are it. ### Every actor the map spawned is invisible on frame one This is the first workaround, and it is the one most likely to cost you an afternoon, because the symptom is that nothing happens. `akgl_actor_initialize` sets `movement_controls_face` to true and installs `akgl_actor_automatic_face` as the `facefunc`. That function clears every facing bit and then sets the one matching a *movement* bit. An NPC has no movement bits. So on the first update its state falls from `ALIVE|FACE_DOWN` (17) to `ALIVE` (16) — a combination no character JSON maps a sprite to — and `akgl_actor_render` skips it rather than reporting anything. The player goes the same way the moment they stop walking. `actor.h` says as much in a `@note` on `akgl_actor_automatic_face`, and the implementation carries a `TODO : This doesn't really work properly` above the line that does it. The fix is one field: ```c excerpt=examples/jrpg/world.c for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) { if ( akgl_heap_actors[i].refcount == 0 ) { continue; } akgl_heap_actors[i].movement_controls_face = false; } ``` Turning the automatic facing *off* is not a compromise here. The arrow-key handlers already set the facing bit alongside the movement bit on the way down, and `akgl_actor_cmhf_*_off` clears only the movement bit on the way up — so the facing an actor is left with is exactly the one it was walking in. The automatic `facefunc` is for an actor whose facing is not otherwise decided. ## Walls, because the library has none There is no collision in libakgl. Not "a simple one" — none: - `akgl_physics_arcade_collide` raises `AKERR_API` with the message `"Not implemented"`. - `akgl_physics_simulate` never calls `collide` at all, so that status is unreachable through the normal path. - `akgl_physics_arcade_move` is `x += vx * dt` on three axes and consults no tilemap, no other actor and no bound of any kind. An actor therefore walks through a building and off the edge of the world. [Chapter 14](14-physics.md) lists this among what is not implemented. A game that wants a wall writes one, and the place to write it is the actor's `movementlogicfunc` — the one hook the physics step calls *before* it commits anything. The rule this game applies is that the outer ring of the map is solid and every non-empty cell of the `decoration` layer is solid: ```c excerpt=examples/jrpg/world.c if ( (tx <= 0) || (ty <= 0) || (tx >= (akgl_gamemap->width - 1)) || (ty >= (akgl_gamemap->height - 1)) ) { return true; } return (akgl_gamemap->layers[JRPG_LAYER_SOLID].data[(ty * akgl_gamemap->width) + tx] != 0); ``` `JRPG_LAYER_SOLID` is the number 1, not the string `"decoration"`, and that is the second workaround. **`akgl_TilemapLayer` has no `name` member.** The loader reads a layer's `id`, `type`, `opacity`, `visible` and offset, and drops the name Tiled wrote — the only `"name"` an object layer keeps is the one on each *object*. So a game cannot say "the layer called collision"; the map and the program agree on an index, and if somebody inserts a layer in Tiled the game starts colliding with the scenery. This is not in `TODO.md`. Then the hook itself. Two things make the prediction below exact rather than a guess, and both are worth understanding before you copy it: - By the time a `movementlogicfunc` runs, `akgl_physics_simulate` has **already** integrated this step's thrust and capped it against the character's speed ellipse. `tx` and `ty` are this step's final thrust. - Velocity is recomputed as `e + t` every step, and this map has zero gravity and zero drag, so `e` stays zero and `v` *is* `t`. A map with gravity would have to fold `ey` into the prediction as well. ```c excerpt=examples/jrpg/world.c if ( feet_blocked(obj->x + (obj->tx * dt), obj->y) ) { obj->tx = 0.0f; obj->ax = 0.0f; } if ( feet_blocked(obj->x, obj->y + (obj->ty * dt)) ) { obj->ty = 0.0f; obj->ay = 0.0f; } ``` One axis at a time, so walking diagonally into a wall slides along it instead of stopping dead. Zeroing thrust rather than moving the actor back is what keeps this inside the library's model: the arcade backend will still do its own `x += vx * dt` a few lines later, and it will add zero. `feet_blocked` tests a 20×10 footprint at the bottom of the 32×32 frame rather than the whole frame, because a character stands on their feet and every doorway in the town is one tile wide. Four corners is enough only because the footprint is smaller than a tile in both directions. ## Freezing the world with `AKGL_ERR_LOGICINTERRUPT` `AKGL_ERR_LOGICINTERRUPT` is the one status in libakgl that is a control-flow signal rather than a failure. Raised from a `movementlogicfunc` it means *skip the rest of this tick for this actor*, and `akgl_physics_simulate` swallows it in a `HANDLE` block: gravity, drag, the velocity recompute and the move are all skipped, and the frame carries on. Raised from `gravity` or `move` instead it aborts the whole step for every actor, which is not the same thing at all — see [Chapter 14](14-physics.md). A conversation is exactly the case it was made for: ```c excerpt=examples/jrpg/world.c if ( jrpg_textbox_showing() ) { obj->tx = 0.0f; obj->ty = 0.0f; AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_ALL); FAIL_RETURN( errctx, AKGL_ERR_LOGICINTERRUPT, "%s does not move while a conversation is open", (char *)obj->name ); } ``` `FAIL_RETURN` and not `FAIL_BREAK`, because this is not inside an `ATTEMPT` block — the `_BREAK` variants belong inside one and the `_RETURN` variants outside, and a `_RETURN` inside an `ATTEMPT` returns straight past `CLEANUP`. [Chapter 4](04-errors.md) has the whole protocol. The two lines above the raise are not decoration. Thrust has already been integrated for this step, so leaving it would let it pile up for as long as the box is open and lurch the actor forward on the first step after it closes. Clearing the movement bits stops the walk cycle marching on the spot, at the price of one honest wart: a direction held down across the dismissal has to be pressed again, because nothing will re-set the bit until the next key-down. A game with a real conversation state would push a different control map instead of leaving the walk bindings live. ## The party member, and a defect in the parent/child mechanism `akgl_actor_add_child` attaches one actor to another. A child is not simulated: `akgl_physics_simulate` snaps it to the parent's position plus its own velocity fields, used as a fixed offset, and `continue`s. The parent takes a reference, and releasing the parent releases the child with it. For a carried lantern, a turret on a tank, or a party member walking a step behind you, that is exactly right. ```c excerpt=examples/jrpg/world.c PASS(errctx, akgl_heap_next_actor(&follower)); PASS(errctx, akgl_actor_initialize(follower, JRPG_FOLLOWER_NAME)); PASS(errctx, akgl_actor_set_character(follower, "jrpg_elder")); follower->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN); follower->movement_controls_face = false; follower->visible = true; follower->layer = player->layer; follower->renderfunc = &jrpg_follower_render; PASS(errctx, player->addchild(player, follower)); ``` Note what the follower costs: one pool slot, and a `basechar` pointer at a character that is already registered and already dressed in eight sprites. That is the whole point of splitting the instance from the template ([Chapter 11](11-characters.md)) — the elder and the companion are two actors sharing one character. The offset is set **after** `addchild`, because `addchild` is what makes the velocity fields mean an offset: ```c excerpt=examples/jrpg/world.c follower->vx = FOLLOWER_OFFSET_X; follower->vy = FOLLOWER_OFFSET_Y; ``` ### The parent's position is counted twice at draw time This is a real defect, it is not recorded in `TODO.md`, and it makes the parent/child mechanism unusable as shipped for any parent that is not sitting at the world origin. `akgl_physics_simulate` writes a child's position as an **absolute world coordinate**: ```c excerpt=src/physics.c if ( actor->parent != NULL ) { // Children don't move independently of their parents, they just have an offset actor->x = actor->parent->x + actor->vx; actor->y = actor->parent->y + actor->vy; actor->z = actor->parent->z + actor->vz; continue; } ``` `akgl_actor_render` then reads the same field as an **offset** and adds the parent's position to it again: ```c excerpt=src/actor.c if ( obj->parent != NULL ) { dest.x = (obj->parent->x + obj->x - akgl_camera->x); dest.y = (obj->parent->y + obj->y - akgl_camera->y); } else { dest.x = (obj->x - akgl_camera->x); dest.y = (obj->y - akgl_camera->y); } ``` `actor.h` documents both readings without noticing that they contradict each other: the comment on `akgl_Actor::x` says *"For a child, an offset from the parent"*, and the one on `akgl_actor_render` says *"A child actor is drawn at its parent's position plus its own, which is what makes an offset mean an offset"* — while the field it is describing has held an absolute position since the physics step ran. `actor_visible`, three lines further up the same function, tests the camera against the raw `obj->x`, treating it as absolute. Two readings, one field, one function. Measured rather than reasoned about. At frame 300 of the scripted demo the player is at (280, 146), the follower's own `x` and `y` read (266, 156) — the player's position plus the (−14, +10) offset, so absolute — and the camera is at (136, 42). The guarded draw puts the sprite at (130, 114), on a 320×240 screen. The unguarded one computes `280 + 266 − 136` and `146 + 156 − 42` and puts it at (410, 260), off the screen entirely and off the 480×320 map. Rendering the same frame with and without the guard produces different pixels. It is invisible only while the parent sits at the origin, which is where a first test tends to put it. **The guard**, until the library picks one of the two readings: give the child a `renderfunc` that detaches the parent for the duration of the draw, so `akgl_actor_render` takes the branch that does not add it. ```c excerpt=examples/jrpg/world.c parent = obj->parent; obj->parent = NULL; ATTEMPT { CATCH(errctx, akgl_actor_render(obj)); } CLEANUP { obj->parent = parent; } PROCESS(errctx) { } FINISH(errctx, true); ``` `CLEANUP` restores it on every path, including the failing one. An actor left holding a `NULL` parent would stop being snapped and start being simulated as a free agent on the very next step, which is a stranger bug than the one being worked around. The alternative was to drive the follower by hand — no `addchild`, a position written from the game's own logic every frame — and it is a perfectly reasonable choice. This game does not take it, because the mechanism is worth showing and because a fifteen-line hook is cheaper than reimplementing the parent/child lifecycle. The real fix is one line in either `src/physics.c` or `src/actor.c` and a decision about which reading is canonical. ## The text box Text in libakgl is immediate mode. Every `akgl_text_rendertextat` call rasterizes the string through SDL_ttf, uploads it to a texture, blits it, and destroys the texture and the surface again — there is no glyph atlas and no text object to keep. [Chapter 17](17-text-and-fonts.md) says plainly that this is wrong for a page of static prose redrawn sixty times a second, and it is exactly right for a line of dialogue that is on screen only while somebody is talking. ```c excerpt=examples/jrpg/textbox.c panel.x = TEXTBOX_MARGIN; panel.w = akgl_camera->w - (2.0f * TEXTBOX_MARGIN); panel.h = TEXTBOX_HEIGHT; panel.y = akgl_camera->h - TEXTBOX_MARGIN - panel.h; PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &panel, TEXTBOX_FILL)); PASS(errctx, akgl_draw_rect(akgl_renderer, &panel, TEXTBOX_EDGE)); ``` The panel is placed off `akgl_camera->w` and `akgl_camera->h` rather than off a second copy of the window size, because `akgl_render_2d_init` copies the `game.screenwidth` and `game.screenheight` properties onto the camera on its way past. Two numbers, one place. Text coordinates are screen coordinates and do not go through the camera at all, so the box stays put while the world scrolls under it. Three smaller things this box has to know: - **The empty string is refused, not drawn as nothing.** SDL_ttf reports "Text has zero width" and libakgl passes it on as `AKERR_NULLPOINTER`, while `akgl_text_measure` accepts it happily. The two disagree; `TODO.md`, "Known and still open", carries it. Anything that might draw an empty line checks first. - **Fonts are not reference counted.** `akgl_text_unloadfont` invalidates every `TTF_Font *` anybody fetched earlier. Fetching it from `AKGL_REGISTRY_FONT` per frame is the cheap way to stay honest about that. - **The font is `tests/assets/akgl_test_mono.ttf`**, which ships beside its licence file. The two RPG-Maker-named images elsewhere in this tree do not, and that is recorded in `TODO.md` rather than fixed here. ## Startup, the frame, and teardown The startup order is [Chapter 7](07-the-game-and-the-frame.md)'s subject. Three things in it are easy to get wrong, and this block is where two of them land: ```c excerpt=examples/jrpg/jrpg.c PASS(errctx, akgl_game_init()); akgl_game.lowfpsfunc = &lowfps_quiet; PASS(errctx, akgl_set_property("game.screenwidth", JRPG_SCREEN_WIDTH)); PASS(errctx, akgl_set_property("game.screenheight", JRPG_SCREEN_HEIGHT)); PASS(errctx, akgl_render_2d_init(akgl_renderer)); ``` - **The properties go between `akgl_game_init` and `akgl_render_2d_init`**, because the second one reads them. Set them earlier and the properties registry does not exist yet; set them later and you get a zero-sized window. - **`akgl_game.name`, `.version` and `.uri` have to be filled in before `akgl_game_init`**, which refuses to run without all three. They are written with `aksl_strncpy`, never `strncpy`, because they are fixed-width fields. - **`akgl_game.lowfpsfunc` is worth replacing.** `akgl_game.fps` is a completed-second average, so it reads 0 for the first second of every process — which is under the threshold — and the default hook logs a line on *every* frame until the first second is up. The hook exists to be replaced. Then the fourth thing, which is the third workaround and the one that segfaults: ```c excerpt=examples/jrpg/jrpg.c PASS(errctx, akgl_physics_init_arcade(akgl_physics)); ``` `akgl_game_init` points `akgl_physics` at `akgl_default_physics` and stops. That storage is BSS, so all four of its method pointers are `NULL`, and `akgl_game_update` calls `akgl_physics->simulate(...)` without checking it — a `NULL` function pointer, and a `SIGSEGV` on frame one rather than an error context. Be precise about what saves this particular program: `town.tmj` declares its own `physics.model`, and the two-line switch above hands `akgl_physics` a backend that `akgl_tilemap_load` *did* initialize. Remove the line above alone and this game still runs. Remove it **and** that switch and frame one is a segmentation fault — measured, by removing both. So the line is not belt and braces for a map that declares no physics, which is most of them, and it is the only thing covering the window between `akgl_game_init` and the map being loaded. Nothing in the library calls the factory for you, and `physics.h`'s claim that `akgl_game_init` "passes whatever the `physics.engine` property holds" to the factory is false: no code anywhere reads that property. A top-down game wants the arcade backend with zero gravity, which is what the property defaults already give you. ### One frame ```c excerpt=examples/jrpg/jrpg.c PASS(errctx, akgl_renderer->frame_start(akgl_renderer)); PASS(errctx, akgl_game_update(&opflags)); PASS(errctx, jrpg_textbox_draw()); PASS(errctx, akgl_renderer->frame_end(akgl_renderer)); ``` `akgl_game_update` is update-every-actor, step the physics, draw the world. It does not clear the target and it does not present, so the frame is bracketed by the backend's own `frame_start` and `frame_end` — and anything drawn between the update and `frame_end` lands on top of the world. That is the entire trick to a HUD. A failing frame is treated as terminal, and that is deliberate: **every failure path out of `akgl_game_update` returns with the game-state mutex still held.** SDL's mutexes are recursive and this loop is single-threaded, so the next frame would not deadlock — it would just be running on top of a frame that never finished. Bailing out is the honest response. The loop is the last thing in its `ATTEMPT` block, and that is load-bearing: ```c excerpt=examples/jrpg/jrpg.c while ( running ) { started = SDL_GetTicks(); CATCH(errctx, frame(frameno)); frameno += 1; if ( (frame_limit > 0) && (frameno >= frame_limit) ) { running = false; } ``` `CATCH` reports failure by `break`ing, and a `break` inside a loop binds to the loop rather than to the block. With nothing after the loop, a failing frame leaves it and falls straight into `CLEANUP`, `PROCESS` and `FINISH`, which is what is wanted. Put a statement after the loop and it would run after a failure as well. `akgl_tilemap_load_layer_objects` carries the same note over the same shape; `AGENTS.md` states the rule. ### Teardown is yours There is no `akgl_game_shutdown`. Two of these three lines are load-bearing and the third thing here is a deliberate omission: ```c excerpt=examples/jrpg/jrpg.c IGNORE(akgl_text_unloadallfonts()); if ( akgl_window != NULL ) { SDL_DestroyWindow(akgl_window); akgl_window = NULL; } SDL_Quit(); ``` - **`akgl_text_unloadallfonts` must run before `SDL_Quit`.** Fonts live in an SDL property registry, and `SDL_Quit` destroys the registry — taking the last reference to every font still in it, with no way left to close them. [Chapter 17](17-text-and-fonts.md) covers the ordering. - **`akgl_tilemap_release` is not called**, and that is the fourth workaround. Its layer loop destroys `tilesets[i].texture` rather than `layers[i].texture`, so it double-frees every tileset texture and never frees an image layer's, and it NULLs nothing, so a second call is a use-after-free. `TODO.md`, "Known and still open" item 2, and `tilemap.h` carries the warning too. `SDL_Quit` reclaims the textures correctly; calling the function that is supposed to would be worse than not. - **The pools are static storage.** There is nothing to free and the process is about to exit. Releasing the characters would be actively risky: `akgl_heap_release_character` enumerates the state-sprite map through `akgl_character_state_sprites_iterate`, an SDL callback that returns `void`, ends in `FINISH_NORETURN`, and therefore **exits the process** on any error. [Chapter 5](05-the-heap.md) and [Chapter 4](04-errors.md) both cover it. ## The headless smoke run ```c excerpt=examples/jrpg/CMakeLists.txt add_test(NAME example_jrpg COMMAND jrpg --frames 320 --demo) set_tests_properties(example_jrpg PROPERTIES TIMEOUT 120 ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_RENDER_DRIVER=software;SDL_AUDIODRIVER=dummy" ) ``` A smoke test that only proves `main` returns is not worth registering, so this one walks. `--demo` synthesizes real `SDL_Event`s and pushes them through `akgl_controller_handle_event`, so the run goes through the control-map scan and the binding exactly as a keypress does: ```c excerpt=examples/jrpg/jrpg.c static const jrpg_ScriptStep JRPG_DEMO_SCRIPT[] = { { 10, SDLK_RIGHT, true }, /* the per-facing walk animation */ { 70, SDLK_RIGHT, false }, { 75, SDLK_UP, true }, /* up the map, past the buildings */ { 205, SDLK_UP, false }, { 215, SDLK_SPACE, true }, /* the elder is in range: open the box */ { 216, SDLK_SPACE, false }, { 225, SDLK_LEFT, true }, /* frozen: AKGL_ERR_LOGICINTERRUPT eats this */ { 245, SDLK_LEFT, false }, { 255, SDLK_SPACE, true }, /* dismiss */ { 256, SDLK_SPACE, false }, { 265, SDLK_DOWN, true }, /* and walk away */ { 285, SDLK_DOWN, false } }; ``` 320 frames of that would be five and a third seconds of test suite if the loop ran at 60 Hz, and it does not run at 60 Hz — headless with a software renderer it runs as fast as it can, which would make the walk cover about a fifth of the ground. Both problems have the same answer, and it is the one `tests/physics_sim.c` already uses: **drive the clock, do not sleep on it.** ```c excerpt=examples/jrpg/jrpg.c akgl_physics->gravity_time = SDL_GetTicksNS() - JRPG_FIXED_STEP_NS; ``` `akgl_physics_simulate` measures `dt` from `gravity_time`, which is a public field. Setting it one fixed step into the past before each update makes every frame worth exactly 1/60 s of simulated time no matter how fast the loop actually runs. The whole 320-frame run finishes in about a third of a second and lands the player in the same place every time. [Chapter 14](14-physics.md) covers `dt`, `max_timestep` and why the first step used to be however long the level took to load. The run prints where the player ended up, so the result can be read rather than merely passed: ```text jrpg: 320 frames, player at (280, 146) ``` ## What this costs Six things in this program exist because the library does not do them. None of them is hidden, and none of them is a criticism of a design — they are the current state of a library that is honest about being unfinished. | # | Gap | What this game does | Recorded in | |---|---|---|---| | 1 | `movement_controls_face` erases the facing bit of any actor that is not moving, so every map-spawned actor stops being drawn on frame one | Clears the field on every live actor after loading the map | `actor.h` `@note`; a `TODO` in `src/actor.c`. Not in `TODO.md` | | 2 | `akgl_TilemapLayer` has no `name`, so a collision layer cannot be found by name | Hard-codes the layer index in `JRPG_LAYER_SOLID` | Not recorded anywhere | | 3 | `akgl_default_physics` is zeroed BSS and `akgl_game_init` never calls the factory; `akgl_game_update` calls `simulate` through a `NULL` pointer | Calls `akgl_physics_init_arcade` explicitly, and switches to the map's backend when it declares one | The false `physics.engine` claim in `physics.h` is in the plan's out-of-scope list | | 4 | No collision of any kind: `arcade_collide` raises `AKERR_API`, `simulate` never calls it, `arcade_move` clamps nothing | Tile-based blocking in the player's `movementlogicfunc` | `TODO.md`; [Chapter 14](14-physics.md) | | 5 | A child actor's position is written as absolute by the physics step and read as relative by the renderer, so the parent's position is added twice | A `renderfunc` that detaches the parent for the duration of the draw | **Not in `TODO.md`.** `actor.h` documents both readings | | 6 | `akgl_tilemap_release` double-frees tileset textures and never frees image-layer ones | Does not call it; lets `SDL_Quit` reclaim them | `TODO.md`, "Known and still open" item 2 | Two more that are not workarounds but will bite anyone extending this: `akgl_Actor::layer` is a `uint32_t` with no bound, while `akgl_render_2d_draw_world` stops at `AKGL_TILEMAP_MAX_LAYERS` (16) — an actor on layer 20 is simulated and never drawn. And the SDL enumeration callbacks (`akgl_registry_iterate_actor`, `akgl_character_state_sprites_iterate`) return `void` and end in `FINISH_NORETURN`, so an error inside one **exits the process** rather than failing a frame. [Chapter 4](04-errors.md) covers installing your own `akerr_handler_unhandled_error` if that is not what you want. ## Where to look next - [Chapter 20](20-tutorial-sidescroller.md) — the same shape of program with gravity, a jump and platforms, which is where the physics model earns its keep. - [Chapter 13](13-tilemaps.md) — the map format, the three libakgl extensions to Tiled, and the four constraints the loader really enforces. - [Chapter 11](11-characters.md) and [Chapter 12](12-actors.md) — the template and the instance, and why the state word is the whole key. - [Chapter 16](16-input.md) — control maps, the default bindings, and the keystroke ring this game does not use. - [Chapter 22](22-appendix-limits.md) — every compile-time ceiling in one place, including the 64-actor pool this town uses four of.