diff --git a/docs/20-tutorial-sidescroller.md b/docs/20-tutorial-sidescroller.md index 49b3b29..889864f 100644 --- a/docs/20-tutorial-sidescroller.md +++ b/docs/20-tutorial-sidescroller.md @@ -134,19 +134,36 @@ target_compile_definitions(sidescroller The parts that matter: -Each `.c` file includes `sidescroller.h` plus whatever it calls directly. `main.c` adds -`akgl/character.h`, `akgl/controller.h`, `akgl/heap.h`, `akgl/registry.h`, `akgl/renderer.h`, -`akgl/sprite.h` and `akgl/text.h`; `player.c` adds `akgl/controller.h`, `akgl/heap.h`, -`akgl/registry.h`, `akgl/util.h` and ``; `actors.c` adds `akgl/heap.h`, -`akgl/registry.h` and ``. libakgl's headers are self-contained, so including the one -that declares what you are calling is always enough. +Each `.c` file includes `sidescroller.h` plus whatever it calls directly: + +| File | Adds | +|---|---| +| `main.c` | ``, `akstdlib.h`, `SDL3_image/SDL_image.h`, `SDL3_mixer/SDL_mixer.h`, `SDL3_ttf/SDL_ttf.h`, `akgl/character.h`, `akgl/controller.h`, `akgl/heap.h`, `akgl/registry.h`, `akgl/renderer.h`, `akgl/sprite.h`, `akgl/text.h`, `akgl/tilemap.h` | +| `player.c` | ``, `akstdlib.h`, `akgl/controller.h`, `akgl/heap.h`, `akgl/physics.h`, `akgl/registry.h`, `akgl/util.h` | +| `actors.c` | ``, `akstdlib.h`, `akgl/heap.h`, `akgl/registry.h` | + +**libakstdlib's header is ``**, not `` — its functions are prefixed +`aksl_` but the file is not. libakerror's is ``. libakgl's own headers are +self-contained, so including the one that declares what you are calling is always enough. The build file: - You link all four SDL libraries even though this game has no text and no sound, because libakgl itself is built against them. - `SS_ASSET_DIR` is baked in at compile time, so the program can be run from any working - directory. The code falls back to `"."` if it is not defined. + directory. Give it a fallback so the file still compiles without CMake, and **use it as the + default**: a program that defaults to `"."` only finds its assets when it happens to be + launched from the right directory. + +```c excerpt=examples/sidescroller/main.c +#ifndef SS_ASSET_DIR +#define SS_ASSET_DIR "." +#endif +``` + +```c excerpt=examples/sidescroller/main.c + char *assetdir = SS_ASSET_DIR; +``` ### The header @@ -649,7 +666,14 @@ reads. Load a character before its sprites and it fails on the first name it can ## 5. Draw a level -Open Tiled, make a new map, and set it up like this: +**Draw the map in [Tiled](https://mapeditor.org) and save it as `.tmj`.** The rest of this +section is what to set in the editor; Tiled writes the JSON. [Chapter 13](13-tilemaps.md) is +the reference for the format itself, and for what libakgl reads out of it and what it +ignores — read that before hand-editing a `.tmj`, because the loader needs several fields +Tiled fills in automatically (a root-level `width` and `height`, an `id` on every layer, and +`tilecount`, `columns`, `imagewidth` and `imageheight` on every tileset). + +Make a new map and set it up like this: | Setting | Value | |---|---| @@ -1625,6 +1649,19 @@ It prints where the player finished: sidescroller: 240 frames, 0 of 4 coins, 0 deaths, player at 136.0,160.0 grounded ``` +**Capture that position at the end of `run()`, not in `main`.** Teardown happens in `main`'s +`CLEANUP` block and releases the actor pool, so by the time the summary is printed +`ss_game.player` points at a slot that has been given back — and the line reports `0.0,0.0`: + +```c excerpt=examples/sidescroller/main.c + if ( ss_game.player != NULL ) { + ss_game.final_x = ss_game.player->x; + ss_game.final_y = ss_game.player->y; + } +``` + +That is what `final_x` and `final_y` on `ss_Game` are for. + `grounded` and a sensible `y` are how you know collision ran. If the player is hundreds of pixels below the level, revisit step 7. diff --git a/docs/21-tutorial-jrpg.md b/docs/21-tutorial-jrpg.md index e738374..c30661c 100644 --- a/docs/21-tutorial-jrpg.md +++ b/docs/21-tutorial-jrpg.md @@ -80,11 +80,16 @@ TrueType font at runtime. Bake both paths in at compile time: ```cmake target_compile_definitions(jrpg PRIVATE - JRPG_ASSET_DIR="${JRPG_REPO_ROOT}/docs/tutorials/assets/jrpg" - JRPG_FONT_FILE="${JRPG_REPO_ROOT}/tests/assets/akgl_test_mono.ttf" + JRPG_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets" + JRPG_FONT_FILE="${CMAKE_CURRENT_SOURCE_DIR}/assets/font.ttf" ) ``` +**Two definitions, not one.** The font is a separate path from the assets, and a game that +defines only `JRPG_ASSET_DIR` fails at `akgl_text_loadfont` with "Couldn't open" — after +everything else has loaded, which makes it look like a font problem rather than a build +one. + Give them defaults in the header so the file still compiles on its own: ```c excerpt=examples/jrpg/jrpg.h @@ -425,8 +430,8 @@ already works. ## 5. Draw the town -Set the map up as chapter 20 describes — orthogonal, CSV layer format, 16×16 tiles — at -30 × 20 tiles, with three layers: +Draw it in Tiled and save it as `.tmj`, set up as chapter 20 describes — orthogonal, CSV +layer format, 16×16 tiles — at 30 × 20 tiles, with three layers: | Layer | Type | What it is | |---|---|---| @@ -1072,6 +1077,15 @@ The `+ 16.0f` centres on the middle of a 32-pixel sprite rather than its top-lef The loop is chapter 20's, with two additions. Drain the events first: ```c excerpt=examples/jrpg/jrpg.c +static akerr_ErrorContext *frame(long frameno) +{ + PREPARE_ERROR(errctx); + SDL_Event event; + akgl_Iterator opflags = { + .flags = AKGL_ITERATOR_OP_UPDATE, + .layerid = 0 + }; + while ( SDL_PollEvent(&event) ) { if ( event.type == SDL_EVENT_QUIT ) { running = false; @@ -1080,6 +1094,10 @@ The loop is chapter 20's, with two additions. Drain the events first: } ``` +`frameno` is the outer loop's own counter, incremented once per call. **libakgl does not keep +a frame number** — `akgl_game` has an fps figure but no counter, so a game that wants one +keeps it. + Then the camera, the world, and the panel on top of it: ```c excerpt=examples/jrpg/jrpg.c @@ -1088,17 +1106,9 @@ Then the camera, the world, and the panel on top of it: PASS(errctx, akgl_renderer->frame_start(akgl_renderer)); ``` -This game passes an iterator to `akgl_game_update` rather than `NULL`: - -```c excerpt=examples/jrpg/jrpg.c - akgl_Iterator opflags = { - .flags = AKGL_ITERATOR_OP_UPDATE, - .layerid = 0 - }; -``` - -`AKGL_ITERATOR_OP_UPDATE` asks for the update pass. [Chapter 7](07-the-game-and-the-frame.md) -covers the other flags. +This game passes the `opflags` declared at the top of `frame()` to `akgl_game_update` rather +than the `NULL` chapter 20 passes. `AKGL_ITERATOR_OP_UPDATE` asks for the update pass; +[Chapter 7](07-the-game-and-the-frame.md) covers the other flags. ### Teardown @@ -1154,6 +1164,77 @@ where the player finished: jrpg: 320 frames, player at (280, 130) ``` +### Writing the scripted run + +Neither the demo nor that summary line is part of the game — both exist so the program can be +checked without a person at the keyboard, and both are worth having for exactly that reason. + +The script is a table of frame numbers and keys: + +```c excerpt=examples/jrpg/jrpg.h +typedef struct { + long frame; /**< Frame number this step fires on. */ + SDL_Keycode key; /**< Key to synthesize. */ + bool down; /**< True for a press, false for a release. */ +} jrpg_ScriptStep; +``` + +```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 } +}; +``` + +Playing it back is building an `SDL_Event` and handing it to the same function the real event +loop uses: + +```c excerpt=examples/jrpg/jrpg.c + PASS(errctx, aksl_memset((void *)&event, 0x00, sizeof(event))); + if ( JRPG_DEMO_SCRIPT[i].down ) { + event.type = SDL_EVENT_KEY_DOWN; + } else { + event.type = SDL_EVENT_KEY_UP; + } + event.key.which = 0; + event.key.key = JRPG_DEMO_SCRIPT[i].key; + PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event)); +``` + +**Go through `akgl_controller_handle_event`, not straight to the handlers.** A script that +calls the handlers directly still passes when the control map matches nothing at all, which +is the failure it most needs to catch. + +Two more things make a headless run reproducible. Drive the clock rather than sleeping on it, +so a simulated second does not cost a real one: + +```c excerpt=examples/jrpg/jrpg.c + akgl_physics->gravity_time = SDL_GetTicksNS() - JRPG_FIXED_STEP_NS; +``` + +```c excerpt=examples/jrpg/jrpg.c +#define JRPG_FIXED_STEP_NS (AKGL_TIME_ONESEC_NS / 60) +``` + +And print the position while the player still exists — before teardown releases the actor +pool: + +```c excerpt=examples/jrpg/jrpg.c + printf("jrpg: %ld frames, player at (%.0f, %.0f)\n", frameno, player->x, player->y); +``` + +The frame number is the loop's own counter, passed down. libakgl does not keep one. + ## Where to look next - [Chapter 20](20-tutorial-sidescroller.md) — the same shape of program with gravity, a jump