Fix what showed up when the tutorials were actually built and run
Two readers were given the library, the art, the chapter, and no reference
program, and told to compile and run. Both got a working game. These are the
defects that only came out because a compiler and a frame loop were involved --
four earlier read-only reviews had missed every one.
libakstdlib's header was never named. The chapters used aksl_strncpy,
aksl_snprintf and aksl_atoi throughout and said only "libakstdlib is documented
in deps/libakstdlib"; the obvious guess from the function prefix is <aksl.h> and
it is wrong. That was the only compile error the sidescroller reader hit, and it
was the chapter's fault. Both chapters now carry a per-file include table and say
which header it is.
The sidescroller's asset directory defaulted to ".". The chapter said the code
"falls back to `.`" without showing the #ifndef or that assetdir is initialised
from the macro, so a reader gets a game that only finds its art when launched
from exactly the right directory.
The sidescroller's summary line reported 0.0,0.0 airborne while the screenshot
showed the player standing on the ground. Teardown runs in main's CLEANUP block
and releases the actor pool before the summary prints. The chapter published an
example output a reader could not reproduce; the capture that makes it true is
now shown beside it.
The JRPG's CMake block interpolated ${JRPG_REPO_ROOT}, which is this repository's
own variable and undefined for anybody else -- so the font path came out wrong
and akgl_text_loadfont failed long after everything else had loaded. It is
${CMAKE_CURRENT_SOURCE_DIR} now, and the chapter says why both definitions are
needed rather than one.
Chapter 21 described --demo and a summary line in its build section that no step
wrote. Both are now a section: the script table, playback through
akgl_controller_handle_event rather than the handlers, the fixed clock step, and
printing the position before teardown. It also now says libakgl keeps no frame
counter, which a reader assumed it did.
Both map sections now say to draw it in Tiled and point at chapter 13, naming the
fields the loader needs that Tiled fills in automatically -- both readers
hand-wrote a .tmj before being given one and hit exactly those.
Excerpts across docs/ go from 190 to 198.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
This commit is contained in:
@@ -134,19 +134,36 @@ target_compile_definitions(sidescroller
|
|||||||
|
|
||||||
The parts that matter:
|
The parts that matter:
|
||||||
|
|
||||||
Each `.c` file includes `sidescroller.h` plus whatever it calls directly. `main.c` adds
|
Each `.c` file includes `sidescroller.h` plus whatever it calls directly:
|
||||||
`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`,
|
| File | Adds |
|
||||||
`akgl/registry.h`, `akgl/util.h` and `<math.h>`; `actors.c` adds `akgl/heap.h`,
|
|---|---|
|
||||||
`akgl/registry.h` and `<math.h>`. libakgl's headers are self-contained, so including the one
|
| `main.c` | `<string.h>`, `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` |
|
||||||
that declares what you are calling is always enough.
|
| `player.c` | `<math.h>`, `akstdlib.h`, `akgl/controller.h`, `akgl/heap.h`, `akgl/physics.h`, `akgl/registry.h`, `akgl/util.h` |
|
||||||
|
| `actors.c` | `<math.h>`, `akstdlib.h`, `akgl/heap.h`, `akgl/registry.h` |
|
||||||
|
|
||||||
|
**libakstdlib's header is `<akstdlib.h>`**, not `<aksl.h>` — its functions are prefixed
|
||||||
|
`aksl_` but the file is not. libakerror's is `<akerror.h>`. libakgl's own headers are
|
||||||
|
self-contained, so including the one that declares what you are calling is always enough.
|
||||||
|
|
||||||
The build file:
|
The build file:
|
||||||
|
|
||||||
- You link all four SDL libraries even though this game has no text and no sound, because
|
- You link all four SDL libraries even though this game has no text and no sound, because
|
||||||
libakgl itself is built against them.
|
libakgl itself is built against them.
|
||||||
- `SS_ASSET_DIR` is baked in at compile time, so the program can be run from any working
|
- `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
|
### 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
|
## 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 |
|
| 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
|
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
|
`grounded` and a sensible `y` are how you know collision ran. If the player is hundreds of
|
||||||
pixels below the level, revisit step 7.
|
pixels below the level, revisit step 7.
|
||||||
|
|
||||||
|
|||||||
@@ -80,11 +80,16 @@ TrueType font at runtime. Bake both paths in at compile time:
|
|||||||
|
|
||||||
```cmake
|
```cmake
|
||||||
target_compile_definitions(jrpg PRIVATE
|
target_compile_definitions(jrpg PRIVATE
|
||||||
JRPG_ASSET_DIR="${JRPG_REPO_ROOT}/docs/tutorials/assets/jrpg"
|
JRPG_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets"
|
||||||
JRPG_FONT_FILE="${JRPG_REPO_ROOT}/tests/assets/akgl_test_mono.ttf"
|
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:
|
Give them defaults in the header so the file still compiles on its own:
|
||||||
|
|
||||||
```c excerpt=examples/jrpg/jrpg.h
|
```c excerpt=examples/jrpg/jrpg.h
|
||||||
@@ -425,8 +430,8 @@ already works.
|
|||||||
|
|
||||||
## 5. Draw the town
|
## 5. Draw the town
|
||||||
|
|
||||||
Set the map up as chapter 20 describes — orthogonal, CSV layer format, 16×16 tiles — at
|
Draw it in Tiled and save it as `.tmj`, set up as chapter 20 describes — orthogonal, CSV
|
||||||
30 × 20 tiles, with three layers:
|
layer format, 16×16 tiles — at 30 × 20 tiles, with three layers:
|
||||||
|
|
||||||
| Layer | Type | What it is |
|
| 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:
|
The loop is chapter 20's, with two additions. Drain the events first:
|
||||||
|
|
||||||
```c excerpt=examples/jrpg/jrpg.c
|
```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) ) {
|
while ( SDL_PollEvent(&event) ) {
|
||||||
if ( event.type == SDL_EVENT_QUIT ) {
|
if ( event.type == SDL_EVENT_QUIT ) {
|
||||||
running = false;
|
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:
|
Then the camera, the world, and the panel on top of it:
|
||||||
|
|
||||||
```c excerpt=examples/jrpg/jrpg.c
|
```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));
|
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
|
||||||
```
|
```
|
||||||
|
|
||||||
This game passes an iterator to `akgl_game_update` rather than `NULL`:
|
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;
|
||||||
```c excerpt=examples/jrpg/jrpg.c
|
[Chapter 7](07-the-game-and-the-frame.md) covers the other flags.
|
||||||
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.
|
|
||||||
|
|
||||||
### Teardown
|
### Teardown
|
||||||
|
|
||||||
@@ -1154,6 +1164,77 @@ where the player finished:
|
|||||||
jrpg: 320 frames, player at (280, 130)
|
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
|
## Where to look next
|
||||||
|
|
||||||
- [Chapter 20](20-tutorial-sidescroller.md) — the same shape of program with gravity, a jump
|
- [Chapter 20](20-tutorial-sidescroller.md) — the same shape of program with gravity, a jump
|
||||||
|
|||||||
Reference in New Issue
Block a user