diff --git a/docs/20-tutorial-sidescroller.md b/docs/20-tutorial-sidescroller.md index 13558e8..49b3b29 100644 --- a/docs/20-tutorial-sidescroller.md +++ b/docs/20-tutorial-sidescroller.md @@ -49,6 +49,18 @@ library — you never call `malloc`. **A tilemap is the level.** It is a [Tiled](https://mapeditor.org) `.tmj` file. libakgl draws its tile layers, and creates an actor for every object in its object layers. +**Four things are library globals you use rather than create.** You do not declare these; +including the right header is enough: + +| Global | Is | +|---|---| +| `akgl_game` | The game's own name, version, frame rate and hooks | +| `akgl_renderer` | The render backend. `akgl_render_2d_init` fills it in | +| `akgl_physics` | The physics backend. `akgl_physics_init_arcade` fills it in | +| `akgl_camera` | An `SDL_FRect` in map pixels. Moving it is scrolling | +| `akgl_gamemap` | The level. It already points at storage the library owns | +| `akgl_heap_actors` | The actor pool, as an array of `AKGL_MAX_HEAP_ACTOR` slots | + **One call per frame does the work.** `akgl_game_update()` updates every actor, steps the physics, and draws the world. Your frame loop reads input, then calls it. @@ -71,7 +83,11 @@ physics, and draws the world. Your frame loop reads input, then calls it. 12. **Add moving enemies** — a blob that patrols and a moth that flies. 13. **Tear down** — the order that matters. -Each step below is complete on its own. Build and run after each one. +The steps are cumulative — each one adds to the same files rather than replacing them. Steps +3, 4 and 5 produce data files rather than code; step 1 produces a header. From step 2 onward +the program builds and runs at the end of every step, and the text says what you should see. +Where a step refers forward to a function a later step writes, it says so and tells you what +to leave out until then. --- @@ -88,10 +104,18 @@ sidescroller/ actors.c everything else the map places ``` -Splitting into three `.c` files is not required — it is what keeps each one readable. The -build file: +Splitting into three `.c` files is not required — it is what keeps each one readable. + +A complete `CMakeLists.txt` for a game built beside libakgl: ```cmake +cmake_minimum_required(VERSION 3.10) +project(sidescroller VERSION 1.0.0 LANGUAGES C) + +# libakgl and its dependencies. add_subdirectory if you have the source beside +# you; find_package if you have it installed. Chapter 3 covers both. +add_subdirectory(../libakgl libakgl) + add_executable(sidescroller main.c player.c @@ -103,17 +127,26 @@ target_include_directories(sidescroller PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(sidescroller PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm) + +target_compile_definitions(sidescroller + PRIVATE "SS_ASSET_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/assets\"") ``` -You link all four SDL libraries even though this game has no text and no sound, because -libakgl itself is built against them. +The parts that matter: -Your assets — the PNGs, the JSON and the map — go in a directory of their own. Tell the -program where at compile time, so it can be run from any working directory: +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. -```cmake -target_compile_definitions(sidescroller PRIVATE "SS_ASSET_DIR=\"${SS_ASSET_DIR}\"") -``` +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. ### The header @@ -127,7 +160,40 @@ with the level's geometry: #define SS_WINDOW_SCALE 2 /* Integer upscale from the view to the window */ ``` -The game state is one struct with one instance, declared `extern` here and defined in +How many of each thing the level places, and how the player moves: + +```c excerpt=examples/sidescroller/sidescroller.h +#define SS_COIN_COUNT 4 +#define SS_HAZARD_COUNT 2 +``` + +```c excerpt=examples/sidescroller/sidescroller.h +#define SS_JUMP_SPEED 420.0f +``` + +```c excerpt=examples/sidescroller/sidescroller.h +#define SS_PLAYER_BOX_X 8.0f +#define SS_PLAYER_BOX_Y 0.0f +#define SS_PLAYER_BOX_W 16.0f +#define SS_PLAYER_BOX_H 32.0f +``` + +The player's collision box is an offset into its 32×32 sprite frame — step 7 explains the +inset. + +Per-actor data the library has no field for. `akgl_Actor::actorData` is a `void *` the +library never reads or frees, and this is what this game hangs off it: + +```c excerpt=examples/sidescroller/sidescroller.h +typedef struct { + float32_t home_x; /**< Where the map placed this actor. The moth orbits it; the player respawns at it. */ + float32_t home_y; + float32_t phase; /**< Seconds of flight, for the moth's orbit. */ + float32_t facing; /**< -1.0 walking left, +1.0 walking right. The blob's patrol direction. */ +} ss_ActorData; +``` + +And the game state — one struct, one instance, declared `extern` here and defined in `main.c`: ```c excerpt=examples/sidescroller/sidescroller.h @@ -137,10 +203,75 @@ typedef struct { akgl_Actor *hazards[SS_HAZARD_COUNT]; /**< The blob and the moth. Borrowed, never released. */ int coins_taken; int deaths; + bool jump_requested; /**< Set by the jump binding, consumed by the movement logic. */ + bool grounded; /**< Last step's verdict; what gates the next jump. */ + bool autoplay; /**< Drive the player from a script instead of the keyboard. */ + int frame; /**< Frames drawn so far. */ + float32_t final_x; /**< Where the player finished, stamped before teardown for the summary line. */ + float32_t final_y; +} ss_Game; ``` Fixed arrays, not allocations. The level places a known number of things. +### What the header has to include, and declare + +Everything the three `.c` files share goes here. The includes first: + +```c excerpt=examples/sidescroller/sidescroller.h +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +``` + +Then the two globals `main.c` defines, and the functions the other files call: + +```c excerpt=examples/sidescroller/sidescroller.h +extern ss_Game ss_game; +``` + +```c excerpt=examples/sidescroller/sidescroller.h +extern akgl_CollisionWorld ss_collision; +``` + +```c excerpt=examples/sidescroller/sidescroller.h +akerr_ErrorContext AKERR_NOIGNORE *ss_grounded(akgl_CollisionShape *shape, float32_t x, float32_t y, bool *dest); + +/* player.c */ +akerr_ErrorContext AKERR_NOIGNORE *ss_player_bind(akgl_Actor *obj); +akerr_ErrorContext AKERR_NOIGNORE *ss_player_controls(int controlmapid, char *actorname); +akerr_ErrorContext AKERR_NOIGNORE *ss_player_autoplay(int frame); + +/* actors.c */ +akerr_ErrorContext AKERR_NOIGNORE *ss_actors_bind(void); +``` + +Those five are what the steps below fill in: + +| Function | Defined in | Called from | Step | +|---|---|---|---| +| `ss_grounded` | `main.c` | `player.c`, `actors.c` | 9 | +| `ss_player_bind` | `player.c` | `main.c`, after the map loads | 7, 8 | +| `ss_player_controls` | `player.c` | `main.c`, after the map loads | 8 | +| `ss_actors_bind` | `actors.c` | `main.c`, after the map loads | 12 | +| `ss_player_autoplay` | `player.c` | the frame loop, only with `--autoplay` | — | + +`AKERR_NOIGNORE` on a declaration is what makes the compiler warn if a caller +throws the return value away. Put it on every function of your own that returns +an error context. + +Wrap the whole file in the usual include guard. + --- ## 2. Open a window and run a frame loop @@ -198,8 +329,24 @@ Here is the first part: sizeof(akgl_game.name), "libakgl sidescroller tutorial", sizeof(akgl_game.name) - 1)); + PASS(errctx, aksl_strncpy( + (char *)&akgl_game.version, + sizeof(akgl_game.version), + "1.0.0", + sizeof(akgl_game.version) - 1)); + PASS(errctx, aksl_strncpy( + (char *)&akgl_game.uri, + sizeof(akgl_game.uri), + "net.aklabs.libakgl.sidescroller", + sizeof(akgl_game.uri) - 1)); + + PASS(errctx, akgl_game_init()); ``` +`version` is your game's, in any form you like. `uri` is a reverse-DNS application identifier +— SDL uses it as the application id, and libakgl stamps it into savegames so a file from +another game is refused. + `aksl_strncpy` rather than `strncpy`: it reports a truncation as an error instead of quietly producing a shortened string. The same goes for `aksl_snprintf` and `aksl_atoi` later on. libakstdlib is documented in `deps/libakstdlib`. @@ -258,6 +405,13 @@ Three things happen per frame: drain the event queue, position the camera, and c call with the backend's own `frame_start` and `frame_end`: ```c excerpt=examples/sidescroller/main.c +static akerr_ErrorContext *frame(bool *running) +{ + SDL_Event event; + PREPARE_ERROR(errctx); + + FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running"); + while ( SDL_PollEvent(&event) == true ) { if ( event.type == SDL_EVENT_QUIT ) { *running = false; @@ -271,20 +425,66 @@ call with the backend's own `frame_start` and `frame_end`: Hand *every* event to `akgl_controller_handle_event`. An event nothing is bound to is not an error; it is a call that does nothing. +The first argument is the app-state pointer SDL's callback API passes around. **Nothing in +libakgl reads it**; it is required only as a non-`NULL` token, so any non-`NULL` pointer will +do. `&akgl_game.state` is a convenient one — it is a struct of application-defined flags the +library also never reads, and it is already there. + +Then the drawing half of the same function: + ```c excerpt=examples/sidescroller/main.c PASS(errctx, akgl_renderer->frame_start(akgl_renderer)); ``` `akgl_game_update(NULL)` goes between `frame_start` and `frame_end`. The `NULL` means "no -iterator options" — update every actor on every layer. - -The outer loop calls that once per frame: +iterator options" — update every actor on every layer: ```c excerpt=examples/sidescroller/main.c + PASS(errctx, akgl_game_update(NULL)); +``` + +and then present what it drew, which ends the function: + +```c excerpt=examples/sidescroller/main.c + PASS(errctx, akgl_renderer->frame_end(akgl_renderer)); + SUCCEED_RETURN(errctx); +} +``` + +The outer loop calls that once per frame, and sleeps so it does not spin: + +```c excerpt=examples/sidescroller/main.c +static akerr_ErrorContext *run(int frames) +{ + bool running = true; + PREPARE_ERROR(errctx); + while ( running == true ) { PASS(errctx, frame(&running)); + if ( (frames > 0) && (ss_game.frame >= frames) ) { + running = false; + } + /* A crude frame limiter. A game with a window on a real display should + * ask SDL for vsync instead; this one has to work under the dummy video + * driver, where there is nothing to sync to. */ + SDL_Delay(16); + } ``` +The frame counter is incremented inside `frame()`, between the events and the drawing: + +```c excerpt=examples/sidescroller/main.c + ss_game.frame += 1; + if ( ss_game.autoplay == true ) { + PASS(errctx, ss_player_autoplay(ss_game.frame)); + } + PASS(errctx, update_camera()); +``` + +`update_camera` is step 10 and `ss_player_autoplay` is the scripted-input hook described at +the end of the chapter. Leave both out for now — a `frame()` that polls events, calls +`frame_start`, `akgl_game_update(NULL)` and `frame_end` is a complete program. + Build and run now. You get a window of the renderer's clear colour and nothing else. That is correct — there is nothing in the world yet. @@ -563,8 +763,25 @@ characters, and characters name sprites. } ``` -`asset_path` is a three-line helper that joins the directory and the file name with -`aksl_snprintf`. +`asset_path` is the helper that joins the directory and the file name: + +```c excerpt=examples/sidescroller/main.c +static akerr_ErrorContext *asset_path(char *dir, char *name, char *dest, size_t size) +{ + int count = 0; + PREPARE_ERROR(errctx); + + FAIL_ZERO_RETURN(errctx, dir, AKERR_NULLPOINTER, "dir"); + FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name"); + FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest"); + PASS(errctx, aksl_snprintf(&count, dest, size, "%s/%s", dir, name)); + SUCCEED_RETURN(errctx); +} +``` + +`aksl_snprintf` rather than `snprintf`, so a path that does not fit arrives as +`AKERR_OUTOFBOUNDS` naming both lengths. Truncated silently, it reports itself much later as +a missing file with a name nobody wrote. Then the map: @@ -611,11 +828,19 @@ The map published them by name. Look each one up: ```c excerpt=examples/sidescroller/main.c player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL); FAIL_ZERO_RETURN(errctx, player, AKERR_KEY, "The map placed no actor called player"); + PASS(errctx, ss_player_bind(player)); + PASS(errctx, ss_actors_bind()); + PASS(errctx, ss_player_controls(0, "player")); ``` A miss means the map and the code disagree about what the level contains, which is worth failing on. +Those three calls are where steps 7 to 12 attach: `ss_player_bind` gives the player its shape +and its hooks, `ss_actors_bind` does the same for the coins and the hazards, and +`ss_player_controls` binds the keys. All three run **after** the map has loaded, because the +map is what creates the actors they touch. + Run now. The level draws, the actors appear, and everything falls through the floor. --- @@ -650,11 +875,28 @@ character visibly clears: #define SS_PLAYER_BOX_H 32.0f ``` +`ss_player_body` is a file-scope rectangle in `player.c`, built from the constants in your +header: + +```c excerpt=examples/sidescroller/player.c +static SDL_FRect ss_player_body = { + .x = SS_PLAYER_BOX_X, + .y = SS_PLAYER_BOX_Y, + .w = SS_PLAYER_BOX_W, + .h = SS_PLAYER_BOX_H +}; +``` + +Turn it into a shape on the actor: + ```c excerpt=examples/sidescroller/player.c PASS(errctx, akgl_collision_shape_box(&obj->shape, &ss_player_body, 0.0f)); obj->shape_override = true; ``` +`akgl_collision_shape_box(dest, body, depth)` takes the shape to fill in, the frame-relative +rectangle, and a z depth. + The `0.0f` is the depth along z; passing 0 lets libakgl choose one. [Chapter 15](15-collision.md) explains why a 2D shape has a depth at all. `shape_override = true` says this actor's shape is its own rather than its character's. @@ -681,13 +923,26 @@ Run now. The player and the blob stand on the ground. Nothing moves yet. ### The two hooks -An actor carries seven function pointers. Two matter here, and the difference between them is -*when in the frame they run*: +An actor carries seven function pointers. `akgl_actor_initialize` installs a working default +on every one of them, and a game replaces the ones it wants to change **on the actor it wants +to change them on**: -| Hook | Called by | Put here | +| Hook | Called by | Default does | |---|---|---| -| `updatefunc` | the actor sweep, before physics | per-frame game logic: picking things up, dying | -| `movementlogicfunc` | the physics step, before gravity and the move | anything that has to affect this step's motion | +| `updatefunc` | the actor sweep, before physics | choose the facing, then advance the animation | +| `movementlogicfunc` | the physics step, before gravity and the move | turn the movement bits into signed acceleration | +| `renderfunc` | the draw pass | draw the sprite the state selects | +| `facefunc` | `updatefunc` | choose the facing bits from the movement bits | +| `changeframefunc` | `updatefunc`, when a frame is due | step to the next animation frame | +| `collidefunc` | collision, after the move | push the actor out and stop it going further in | +| `addchild` | you | attach a child actor | + +Two matter here, and the difference between them is *when in the frame they run*: + +| Hook | Put here | +|---|---| +| `updatefunc` | per-frame game logic: picking things up, dying | +| `movementlogicfunc` | anything that has to affect this step's motion | Install them **after** the actor exists. `akgl_actor_initialize` — which the map loader already ran — overwrites all seven: @@ -708,8 +963,28 @@ movement bits into signed acceleration. ### Binding keys -A control map ties an event to a handler and a target actor. Fill in an `akgl_Control` and -push it: +Control maps live in a library array, `akgl_controlmaps`, indexed by a small integer you +choose. A one-player game uses 0. Point one at your actor: + +```c excerpt=examples/sidescroller/player.c +akerr_ErrorContext *ss_player_controls(int controlmapid, char *actorname) +{ + akgl_ControlMap *controlmap = NULL; + akgl_Control control; + PREPARE_ERROR(errctx); +``` + +```c excerpt=examples/sidescroller/player.c + controlmap = &akgl_controlmaps[controlmapid]; + controlmap->target = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, actorname, NULL); +``` + +Then fill in an `akgl_Control` and push it. Zero the struct first — a binding is copied into +the map, so a stack local is fine, but an uninitialized field matches against garbage: + +```c excerpt=examples/sidescroller/player.c + PASS(errctx, aksl_memset((void *)&control, 0x00, sizeof(akgl_Control))); +``` ```c excerpt=examples/sidescroller/player.c control.event_on = SDL_EVENT_KEY_DOWN; @@ -742,8 +1017,16 @@ is matched on `key` whatever else the binding carries, and 0 is a keycode like a control.key = 0; control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN; control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP; + + control.button = SDL_GAMEPAD_BUTTON_DPAD_LEFT; + control.handler_on = &akgl_actor_cmhf_left_on; + control.handler_off = &ss_control_left_off; + PASS(errctx, akgl_controller_pushmap(controlmapid, &control)); ``` +`SDL_GAMEPAD_BUTTON_DPAD_RIGHT` and `SDL_GAMEPAD_BUTTON_SOUTH` — the A button — get the same +treatment for right and jump. + Bind controls **after** the map is loaded. The map is what creates the actor the control map targets. @@ -814,15 +1097,31 @@ of 0 and scaled to nothing. The key handler only *asks*; whether a jump is allowed is the movement function's decision: ```c excerpt=examples/sidescroller/player.c +static akerr_ErrorContext *ss_control_jump_on(akgl_Actor *obj, SDL_Event *event) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); + FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event"); ss_game.jump_requested = true; + SUCCEED_RETURN(errctx); +} ``` -Releasing the button early cuts the jump short. Four lines gives you variable jump height: +Every control handler has that signature: an actor, an SDL event, an error context back. + +Releasing the button early cuts the jump short. That is variable jump height, for four lines: ```c excerpt=examples/sidescroller/player.c +static akerr_ErrorContext *ss_control_jump_off(akgl_Actor *obj, SDL_Event *event) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); + FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event"); if ( obj->ey < 0.0f ) { obj->ey *= 0.4f; } + SUCCEED_RETURN(errctx); +} ``` ### Picking the jump sprite @@ -857,6 +1156,8 @@ static akerr_ErrorContext *ss_control_left_off(akgl_Actor *obj, SDL_Event *event } ``` +`ss_control_right_off` is the same function with `AKGL_ACTOR_STATE_MOVING_RIGHT`. + Then decay the thrust yourself, faster on the ground than in the air: ```c excerpt=examples/sidescroller/player.c @@ -878,6 +1179,20 @@ in one frame — right for a top-down game, wrong for a sidescroller. Friction a in the backend are tracked in `TODO.md` under "Arcade physics feel"; when they land, this whole section becomes a setting. +### The whole movement function, in order + +`ss_player_movement` now does five things, and the order is the order they were introduced: + +1. `akgl_actor_logic_movement(obj, dt)` — the default logic, first. +2. `ss_grounded(...)` — record whether there is a floor underfoot. +3. Decay `tx` if no direction is held. +4. If a jump was requested and the actor is grounded, write `ey`. Clear the request either + way. +5. Set or clear `AKGL_ACTOR_STATE_MOVING_UP` from `grounded`. + +Steps 2 and 3 have to come before 4: the jump reads the same `grounded` the friction does, +and reading it twice in one step would cost a query for nothing. + Run now. The player runs, slides to a stop, jumps, and holds the jump sprite through the arc. --- @@ -905,6 +1220,8 @@ that is fractionally different every frame makes the tile grid shimmer. Call it once per frame, before `akgl_game_update`. +Run now. The level scrolls as the player walks, and stops scrolling at both ends. + --- ## 11. Collect coins and die on hazards @@ -926,7 +1243,26 @@ the right tool for a pickup — you want to know, not to be pushed: ``` `hitbox` insets a rectangle into the 32×32 frame, for the same reason the collision shape was -inset: a hazard box the full size of the frame kills a player who is visibly nowhere near it. +inset — a hazard box the full size of the frame kills a player who is visibly nowhere near +it: + +```c excerpt=examples/sidescroller/player.c +static akerr_ErrorContext *hitbox(akgl_Actor *obj, float32_t inset, SDL_FRect *dest) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); + FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest"); + + dest->x = obj->x + inset; + dest->y = obj->y + inset; + dest->w = 32.0f - (inset * 2.0f); + dest->h = 32.0f - (inset * 2.0f); + SUCCEED_RETURN(errctx); +} +``` + +The coin test uses an inset of 8, the hazard test 6 — a coin should be easy to take and a +spike should be hard to hit. ### Removing an actor @@ -956,9 +1292,33 @@ Nothing stops an actor leaving the map, so check for it: ### Respawning -Clear everything the simulation carries between steps, not just the position: +The player needs a data slot of its own, filled in when you set the player up: ```c excerpt=examples/sidescroller/player.c +static ss_ActorData ss_player_data; +``` + +```c excerpt=examples/sidescroller/player.c + ss_player_data.home_x = obj->x; + ss_player_data.home_y = obj->y; + obj->actorData = (void *)&ss_player_data; +``` + +Record it **after** settling the spawn point, so a respawn does not put the player back inside +the geometry it was lifted out of. + +Respawning clears everything the simulation carries between steps, not just the position: + +```c excerpt=examples/sidescroller/player.c +static akerr_ErrorContext *respawn(akgl_Actor *obj) +{ + ss_ActorData *data = NULL; + PREPARE_ERROR(errctx); + + FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); + FAIL_ZERO_RETURN(errctx, obj->actorData, AKERR_NULLPOINTER, "obj->actorData"); + + data = (ss_ActorData *)obj->actorData; obj->x = data->home_x; obj->y = data->home_y; obj->ex = 0.0f; @@ -967,6 +1327,7 @@ Clear everything the simulation carries between steps, not just the position: obj->ty = 0.0f; obj->vx = 0.0f; obj->vy = 0.0f; + ss_game.deaths += 1; ``` `ey` is where the fall accumulated. A player who respawns still holding a full-speed fall @@ -981,14 +1342,20 @@ table: static ss_ActorData ss_hazard_data[SS_HAZARD_COUNT]; ``` -```c excerpt=examples/sidescroller/sidescroller.h -typedef struct { - float32_t home_x; /**< Where the map placed this actor. The moth orbits it; the player respawns at it. */ - float32_t home_y; - float32_t phase; /**< Seconds of flight, for the moth's orbit. */ - float32_t facing; /**< -1.0 walking left, +1.0 walking right. The blob's patrol direction. */ -} ss_ActorData; -``` +The player's slot is `ss_player_data` in `player.c`; the hazards share a table in `actors.c`. + +### The whole update function, in order + +`ss_player_update` does four things: + +1. `akgl_actor_update(obj)` — the default, which chooses the facing and advances the + animation. +2. If the actor is below the bottom of the map, respawn and return. +3. Test the player's hitbox against each remaining coin; take any it overlaps. +4. Test it against each hazard; respawn on any it overlaps. + +Run now. Walking into a coin takes it, walking into the blob or falling into the pit +respawns you at the start. --- @@ -1033,7 +1400,14 @@ Set the facing and movement bits, then let the default logic sign the accelerati } ``` -Then probe for a wall ahead and a floor ahead: +Then probe. `step` is one pixel in the direction of travel: + +```c excerpt=examples/sidescroller/actors.c + step = 1.0f; + if ( data->facing < 0.0f ) { + step = -1.0f; + } +``` ```c excerpt=examples/sidescroller/actors.c PASS(errctx, akgl_collision_shape_bounds(&obj->shape, (obj->x + step), obj->y, &ahead)); @@ -1041,10 +1415,21 @@ Then probe for a wall ahead and a floor ahead: PASS(errctx, ss_grounded(&obj->shape, obj->x, obj->y, &grounded)); ``` +The floor probe is one pixel past the leading edge of the box and one pixel below its feet: + ```c excerpt=examples/sidescroller/actors.c + probe_y = obj->y + ss_blob_body.y + ss_blob_body.h + 1.0f; + if ( data->facing < 0.0f ) { + probe_x = obj->x + ss_blob_body.x - 1.0f; + } else { + probe_x = obj->x + ss_blob_body.x + ss_blob_body.w + 1.0f; + } PASS(errctx, akgl_collision_solid_at(&ss_collision, probe_x, probe_y, &floor_ahead)); ``` +One pixel, not one tile. The probe is asking "is there ground under my next step", and a +longer reach turns the blob round a tile before the ledge. + `akgl_collision_solid_at` is the cheapest query there is: is the tile under this one point solid. @@ -1074,15 +1459,68 @@ Two sines at a 1:2 ratio is a figure eight. ### Wiring them up -Find each actor by its Tiled name and install its hook: +`actors.c` needs its own lookup helper. A miss means the map and the code disagree about what +the level contains, which is worth failing on rather than working around: + +```c excerpt=examples/sidescroller/actors.c +static akerr_ErrorContext *find_actor(char *name, akgl_Actor **dest) +{ + PREPARE_ERROR(errctx); + FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name"); + FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest"); + + *dest = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, name, NULL); + FAIL_ZERO_RETURN(errctx, *dest, AKERR_KEY, "The map placed no actor called %s", name); + SUCCEED_RETURN(errctx); +} +``` + +Then find each actor by its Tiled name and install its hook: + +```c excerpt=examples/sidescroller/actors.c +static SDL_FRect ss_blob_body = { .x = 8.0f, .y = 0.0f, .w = 16.0f, .h = 32.0f }; +``` ```c excerpt=examples/sidescroller/actors.c PASS(errctx, find_actor("blob1", &ss_game.hazards[0])); PASS(errctx, akgl_collision_shape_box(&ss_game.hazards[0]->shape, &ss_blob_body, 0.0f)); ss_game.hazards[0]->shape_override = true; + PASS(errctx, akgl_collision_settle(&ss_collision, &ss_game.hazards[0]->shape, + &ss_game.hazards[0]->x, &ss_game.hazards[0]->y, 0)); + ss_hazard_data[0].home_x = ss_game.hazards[0]->x; + ss_hazard_data[0].home_y = ss_game.hazards[0]->y; + ss_hazard_data[0].facing = -1.0f; + ss_game.hazards[0]->actorData = (void *)&ss_hazard_data[0]; + ss_game.hazards[0]->movementlogicfunc = &ss_blob_movement; ``` -The moth needs no shape — it never touches terrain. +The coins are found the same way, by the names Tiled gave them — `coin1` through `coin4`: + +```c excerpt=examples/sidescroller/actors.c + for ( i = 0; i < SS_COIN_COUNT; i++ ) { + PASS(errctx, aksl_snprintf(&count, (char *)&name, sizeof(name), "coin%d", (i + 1))); + PASS(errctx, find_actor((char *)&name, &ss_game.coins[i])); + ss_game.coins[i]->movementlogicfunc = &ss_static_movement; + } +``` + +The moth needs no shape — it never touches terrain — but it does need its home position, since +that is what it orbits: + +```c excerpt=examples/sidescroller/actors.c + PASS(errctx, find_actor("moth1", &ss_game.hazards[1])); + ss_hazard_data[1].home_x = ss_game.hazards[1]->x; + ss_hazard_data[1].home_y = ss_game.hazards[1]->y; + ss_hazard_data[1].facing = 1.0f; + ss_game.hazards[1]->actorData = (void *)&ss_hazard_data[1]; + ss_game.hazards[1]->movementlogicfunc = &ss_moth_movement; +``` + +All of that lives in one function, `ss_actors_bind`, which `main.c` calls after the map is +loaded. + +Run now. The coins hang in the air instead of falling, the blob patrols its platform and +turns at the edges, and the moth traces a figure eight. That is the whole game. --- @@ -1127,8 +1565,13 @@ exiting can leave the textures to `SDL_Quit`. LOG_ERROR_WITH_MESSAGE(errctx, "the sidescroller could not run"); ``` +- `parse_args` is ordinary `strcmp` over `argv`, filling in `assetdir` and `frames`. It + exists for the headless run at the end of this chapter; a game that only ever opens a + window can drop it and pass `SS_ASSET_DIR` and `0` straight in. - `ATTEMPT` … `CLEANUP` … `PROCESS` … `HANDLE_DEFAULT` … `FINISH_NORETURN` is the error-handling block. `CATCH` inside it is what `PASS` is outside it. +- `LOG_ERROR_WITH_MESSAGE` is libakerror's, like every other macro here. It prints the + status, your message and the stack trace the context accumulated on its way up. - `CLEANUP` runs on every path, success or failure. Teardown goes there. - **Never use a `*_RETURN` macro inside `ATTEMPT`.** It returns past `CLEANUP`, so every release and `fclose` is skipped. @@ -1160,6 +1603,22 @@ SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIODRIVER=dummy \ ./build/examples/sidescroller/sidescroller --frames 240 --autoplay ``` +Those two flags are scaffolding for that run and are not part of the game. `--frames N` stops +after N frames, which is the `if` in `run()` above. `--autoplay` synthesizes key events on a +fixed schedule so the level plays itself: + +```c excerpt=examples/sidescroller/player.c + if ( (frame % SS_AUTOPLAY_JUMP_PERIOD) == 0 ) { + synthetic.type = SDL_EVENT_KEY_DOWN; + synthetic.key.which = SS_AUTOPLAY_KBID; + synthetic.key.key = SDLK_SPACE; + PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &synthetic)); +``` + +Feed synthetic events through `akgl_controller_handle_event` rather than calling the handlers +directly. A scripted run then exercises the binding table the same way a player does, so a +control map that matches nothing fails the smoke test instead of passing it. + It prints where the player finished: ```text diff --git a/docs/21-tutorial-jrpg.md b/docs/21-tutorial-jrpg.md index e64d976..e738374 100644 --- a/docs/21-tutorial-jrpg.md +++ b/docs/21-tutorial-jrpg.md @@ -58,6 +58,10 @@ else stops with them. libakgl has a status for exactly this. 12. **Freeze the world** — `AKGL_ERR_LOGICINTERRUPT`, in a game. 13. **Follow with the camera, and tear down**. +The steps are cumulative — each one adds to the same files rather than replacing them. Steps +3, 4 and 5 produce data files rather than code. From step 6 onward the program builds and +runs at the end of every step, and the text says what you should see. + --- ## 1. Set up the project @@ -100,6 +104,107 @@ These are strings because `akgl_set_property` takes strings. `akgl_render_2d_ini them onto `akgl_camera` on the way past, so everything downstream reads the camera rather than a second copy of the same two numbers. +### What the header has to include, and declare + +Each `.c` file includes `jrpg.h` plus whatever it uses directly: + +| File | Adds | +|---|---| +| `world.c` | ``, ``, `akstdlib.h`, `akgl/character.h`, `akgl/error.h`, `akgl/game.h`, `akgl/heap.h`, `akgl/physics.h`, `akgl/registry.h`, `akgl/sprite.h`, `akgl/tilemap.h` | +| `jrpg.c` | ``, `akstdlib.h`, `SDL3_ttf/SDL_ttf.h`, `akgl/controller.h`, `akgl/error.h`, `akgl/game.h`, `akgl/iterator.h`, `akgl/physics.h`, `akgl/registry.h`, `akgl/renderer.h`, `akgl/text.h`, `akgl/tilemap.h` | +| `textbox.c` | listed in step 10 | + +`akgl/collision.h` and `akgl/actor.h` come in through `jrpg.h`, so nothing needs to include +them twice. libakgl's headers are self-contained: including the one that declares what you +are calling is always enough. + +The header itself needs: + +```c excerpt=examples/jrpg/jrpg.h +#include +#include +#include +#include +#include +#include +``` + +Then the collision world and the twelve functions the four `.c` files share: + +```c excerpt=examples/jrpg/jrpg.h +extern akgl_CollisionWorld jrpg_collision; +``` + +```c excerpt=examples/jrpg/jrpg.h +akerr_ErrorContext AKERR_NOIGNORE *jrpg_world_load(void); +``` + +```c excerpt=examples/jrpg/jrpg.h +akerr_ErrorContext AKERR_NOIGNORE *jrpg_world_populate(void); +``` + +```c excerpt=examples/jrpg/jrpg.h +akerr_ErrorContext AKERR_NOIGNORE *jrpg_camera_follow(void); +``` + +```c excerpt=examples/jrpg/jrpg.h +akerr_ErrorContext AKERR_NOIGNORE *jrpg_actor_logic_walk(akgl_Actor *obj, float32_t dt); +``` + +```c excerpt=examples/jrpg/jrpg.h +akerr_ErrorContext AKERR_NOIGNORE *jrpg_follower_render(akgl_Actor *obj); +``` + +```c excerpt=examples/jrpg/jrpg.h +akerr_ErrorContext AKERR_NOIGNORE *jrpg_cmhf_talk(akgl_Actor *obj, SDL_Event *event); +``` + +```c excerpt=examples/jrpg/jrpg.h +akerr_ErrorContext AKERR_NOIGNORE *jrpg_cmhf_ignore(akgl_Actor *obj, SDL_Event *event); +``` + +```c excerpt=examples/jrpg/jrpg.h +akerr_ErrorContext AKERR_NOIGNORE *jrpg_textbox_open(char *text); +/** @brief Dismiss the text box. */ +void jrpg_textbox_close(void); +/** @brief True while a line of dialogue is on screen. */ +bool jrpg_textbox_showing(void); +``` + +```c excerpt=examples/jrpg/jrpg.h +akerr_ErrorContext AKERR_NOIGNORE *jrpg_textbox_draw(void); +``` + +Which file defines what, and which step writes it: + +| Function | Defined in | Step | +|---|---|---| +| `jrpg_world_load` | `world.c` | 3, 5, 7 | +| `jrpg_world_populate` | `world.c` | 6, 7, 8, 9 | +| `jrpg_actor_logic_walk` | `world.c` | 12 | +| `jrpg_follower_render` | `world.c` | 9 | +| `jrpg_camera_follow` | `world.c` | 13 | +| `jrpg_cmhf_talk`, `jrpg_cmhf_ignore` | `world.c` | 11 | +| `jrpg_textbox_open`, `_close`, `_showing`, `_draw` | `textbox.c` | 10 | + +Two more constants the chapters below use: + +```c excerpt=examples/jrpg/jrpg.h +#define JRPG_FONT_NAME "jrpg" +``` + +```c excerpt=examples/jrpg/jrpg.h +#define JRPG_FONT_SIZE 12 +``` + +```c excerpt=examples/jrpg/jrpg.h +#define JRPG_TEXTBOX_MAX_TEXT 256 +``` + +```c excerpt=examples/jrpg/jrpg.h +#define JRPG_FOLLOWER_NAME "companion" +``` + --- ## 2. Start up and load the town @@ -130,7 +235,11 @@ Then one new call — load the font: ``` A font is registered under a name and a size. The size is baked into the handle, so 12pt and -24pt of the same file are two registrations. +24pt of the same file are two registrations. `JRPG_FONT_NAME` is the key the text box looks +it up under in step 10. + +`lowfps_quiet` is the do-nothing frame-rate hook chapter 20 explains — a `static void +lowfps_quiet(void) { }` in `jrpg.c`. Then the world, in two functions — everything the assets need, then everything the actors need: @@ -173,6 +282,18 @@ Then one nested loop loads them all: The helper builds the path from the three components: +```c excerpt=examples/jrpg/world.c +static akerr_ErrorContext *sprite_load(char *cast, char *motion, char *facing) +{ + PREPARE_ERROR(errctx); + char path[PATH_MAX]; + int written = 0; + + FAIL_ZERO_RETURN(errctx, cast, AKERR_NULLPOINTER, "cast"); + FAIL_ZERO_RETURN(errctx, motion, AKERR_NULLPOINTER, "motion"); + FAIL_ZERO_RETURN(errctx, facing, AKERR_NULLPOINTER, "facing"); +``` + ```c excerpt=examples/jrpg/world.c aksl_snprintf( &written, @@ -184,6 +305,41 @@ The helper builds the path from the three components: motion, facing )); + PASS(errctx, akgl_sprite_load_json(path)); + SUCCEED_RETURN(errctx); +} +``` + +`character_load` is the same shape against a different file name and a different loader: + +```c excerpt=examples/jrpg/world.c +static akerr_ErrorContext *character_load(char *cast) +{ + PREPARE_ERROR(errctx); + char path[PATH_MAX]; + int written = 0; + + FAIL_ZERO_RETURN(errctx, cast, AKERR_NULLPOINTER, "cast"); + PASS(errctx, + aksl_snprintf( + &written, + path, + sizeof(path), + "%s/character_jrpg_%s.json", + JRPG_ASSET_DIR, + cast + )); + PASS(errctx, akgl_character_load_json(path)); + SUCCEED_RETURN(errctx); +} +``` + +`PATH_MAX` comes from ``. The counts come from the tables: + +```c excerpt=examples/jrpg/world.c +#define CAST_COUNT (sizeof(CAST) / sizeof(CAST[0])) +#define MOTION_COUNT (sizeof(MOTIONS) / sizeof(MOTIONS[0])) +#define FACING_COUNT (sizeof(FACINGS) / sizeof(FACINGS[0])) ``` A missing combination now shows up as a load failure naming the exact file, rather than as @@ -378,9 +534,15 @@ Clearing the field leaves the facing bits wherever they were last set, which is top-down game wants. `TODO.md` tracks making the default behave; until then this loop belongs in every game with a standing NPC in it. +The loop covers the player as well as the NPCs, which is what you want — a player who stops +walking would disappear for the same reason. Chapter 20 sets the same field on one actor +because that game only has one that stands still. + `akgl_heap_actors` is the actor pool, and walking it directly is the supported way to touch every live actor. A slot with `refcount == 0` is free. +Run now. The town draws, the player and the townsfolk are visible, and nothing moves. + --- ## 7. Wall the town in @@ -420,7 +582,32 @@ parent every step, so a shape on it would fight the snap rather than stop anythi The map's edge is not on any layer, and nothing else stops an actor walking off it. Four static collision proxies do the job — one per side. A long thin box costs one pool slot -whatever its length, which is what proxies are for: +whatever its length, which is what proxies are for. + +Keep the proxies and the shapes they were built from in file-scope arrays, because both have +to outlive the function that made them: + +```c excerpt=examples/jrpg/world.c +static akgl_CollisionProxy *jrpg_edges[4]; +static akgl_CollisionShape jrpg_edge_shapes[4]; +``` + +Work the four rectangles out from the map's own size: + +```c excerpt=examples/jrpg/world.c + ow = (float32_t)(akgl_gamemap->width * akgl_gamemap->tilewidth) + (2.0f * EDGE_OVERHANG); + oh = (float32_t)(akgl_gamemap->height * akgl_gamemap->tileheight) + (2.0f * EDGE_OVERHANG); + tw = (float32_t)akgl_gamemap->tilewidth + EDGE_OVERHANG; + th = (float32_t)akgl_gamemap->tileheight + EDGE_OVERHANG; + + /* x y w h */ + sides[0] = (SDL_FRect){ -EDGE_OVERHANG, -EDGE_OVERHANG, tw, oh }; /* west */ + sides[1] = (SDL_FRect){ (ow - tw - EDGE_OVERHANG), -EDGE_OVERHANG, tw, oh }; /* east */ + sides[2] = (SDL_FRect){ -EDGE_OVERHANG, -EDGE_OVERHANG, ow, th }; /* north */ + sides[3] = (SDL_FRect){ -EDGE_OVERHANG, (oh - th - EDGE_OVERHANG), ow, th }; /* south */ +``` + +Then register each one, in a loop over the four: ```c excerpt=examples/jrpg/world.c PASS(errctx, akgl_heap_next_collision_proxy(&jrpg_edges[i])); @@ -474,12 +661,25 @@ and facing bits your character mappings key on. A sidescroller replaces the `_of to get friction; a top-down game does not, because stopping dead when you release a direction is exactly right here. -Install the player's own movement hook too — step 12 is what goes in it: +Run now. The arrow keys walk the player in four directions with the right animation for each, +and buildings and the map's edge stop them. + +The player itself comes out of the registry, under the name its Tiled object carried: ```c excerpt=examples/jrpg/world.c + player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL); + FAIL_ZERO_RETURN( + errctx, + player, + AKGL_ERR_REGISTRY, + "town.tmj spawned no actor named \"player\"" + ); player->movementlogicfunc = &jrpg_actor_logic_walk; ``` +That lookup and everything after it lives in `jrpg_world_populate`, which runs after +`jrpg_world_load` has loaded the map. Step 12 is what goes in the hook. + --- ## 9. Follow the player @@ -503,6 +703,12 @@ it: gives it a name. Write those two adjacent, with nothing between them — the slot is not yours until the initialize. +`addchild` is one of the actor's seven behaviour hooks, not a library function, which is why +it is called through the actor and takes it as the first argument: +`player->addchild(player, follower)` reads as "player, add this child". Every hook works that +way, so a game can replace one on a single actor — chapter 20 replaces `updatefunc` and +`movementlogicfunc` the same way. + The follower borrows an existing character. An actor is an instance; a character is a template. That is what splitting them is for. @@ -540,6 +746,8 @@ failing one — an actor left with a `NULL` parent would be simulated as a free next step. This is `TODO.md`, "Known and still open"; when the two agree on one reading, this hook becomes `akgl_actor_render` again. +Run now. A second character walks a fixed distance behind and below the player. + --- ## 10. Put text on screen @@ -548,13 +756,59 @@ Text in libakgl is immediate mode: each call rasterizes the string, uploads it, destroys the texture. That is the wrong shape for a page of prose redrawn sixty times a second and exactly right for one line of dialogue. -Keep the state in two variables: +`textbox.c` needs these includes: + +```c excerpt=examples/jrpg/textbox.c +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +``` + +The colours are `SDL_Color`, which is four bytes of RGBA: + +```c excerpt=examples/jrpg/textbox.c +/* + * Colour R G B A + */ +static const SDL_Color TEXTBOX_FILL = { 24, 20, 37, 235 }; +static const SDL_Color TEXTBOX_EDGE = { 240, 236, 214, 255 }; +static const SDL_Color TEXTBOX_INK = { 240, 236, 214, 255 }; +``` + +An alpha of 235 on the fill lets the world show faintly through the panel. + +```c excerpt=examples/jrpg/textbox.c +#define TEXTBOX_MARGIN 8.0f +``` + +```c excerpt=examples/jrpg/textbox.c +#define TEXTBOX_HEIGHT 56.0f +``` + +```c excerpt=examples/jrpg/textbox.c +#define TEXTBOX_PADDING 8.0f +``` + +Keep the state in two file-scope variables: ```c excerpt=examples/jrpg/textbox.c static char textbox_text[JRPG_TEXTBOX_MAX_TEXT]; static bool textbox_visible = false; ``` +`jrpg_textbox_showing` returns `textbox_visible` and `jrpg_textbox_close` sets it to `false`. +Both are two-line functions returning `bool` and `void` — they cannot fail, so they do not +return an error context. + Opening the box copies the line in: ```c excerpt=examples/jrpg/textbox.c @@ -577,9 +831,24 @@ Fetch the font from the registry, size a panel from the camera, and draw three t a filled rectangle, an outline, and the text: ```c excerpt=examples/jrpg/textbox.c - font = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, JRPG_FONT_NAME, NULL); + TTF_Font *font = NULL; + SDL_FRect panel; ``` +```c excerpt=examples/jrpg/textbox.c + font = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, JRPG_FONT_NAME, NULL); + FAIL_ZERO_RETURN( + errctx, + font, + AKGL_ERR_REGISTRY, + "No font registered as \"%s\"", + JRPG_FONT_NAME + ); +``` + +A font is an SDL_ttf `TTF_Font *`. libakgl does not wrap it in a type of its own — the +registry hands you SDL_ttf's handle. + ```c excerpt=examples/jrpg/textbox.c panel.x = TEXTBOX_MARGIN; panel.w = akgl_camera->w - (2.0f * TEXTBOX_MARGIN); @@ -633,11 +902,21 @@ The panel is drawn *after* `akgl_game_update`, so it lands on top of everything: `akgl_game_update` does not clear or present, so anything you draw between it and `frame_end` is on top of the world. +Nothing opens the box yet, so there is nothing to see until step 11. + --- ## 11. Talk to somebody -Keep the dialogue in a table beside the actor names the map gave them: +Keep the dialogue in a table beside the actor names the map gave them. The row type is your +own: + +```c excerpt=examples/jrpg/world.c +typedef struct { + char *actor; /**< Registry key, which is the `name` of the map object that spawned them. */ + char *line; /**< What they say. */ +} jrpg_Townsfolk; +``` ```c excerpt=examples/jrpg/world.c static jrpg_Townsfolk TOWNSFOLK[] = { @@ -678,6 +957,10 @@ range, otherwise do nothing. #define TALK_RANGE 64.0f ``` +```c excerpt=examples/jrpg/world.c +#define TOWNSFOLK_COUNT (sizeof(TOWNSFOLK) / sizeof(TOWNSFOLK[0])) +``` + Look the NPC up by name every time rather than caching the pointer. An actor that has been released is out of the registry, so the lookup returns `NULL` and the loop skips it — a cached pointer would be stale. @@ -708,6 +991,9 @@ Three things to get right, all of them cheap: match are evaluated together, and 0 is a real button. Recorded in `TODO.md`; until it is settled, name a button no gamepad reports. +Run now. Standing near the elder and pressing Space opens their line; pressing it again +dismisses the box. The player can still walk while it is open — step 12 fixes that. + --- ## 12. Freeze the world @@ -751,6 +1037,9 @@ If the box is not showing, the default logic runs as usual: PASS(errctx, akgl_actor_logic_movement(obj, dt)); ``` +Run now. Opening a conversation stops the player where they stand, and the arrow keys do +nothing until the box is dismissed. + --- ## 13. Follow with the camera, and tear down @@ -780,6 +1069,19 @@ The `+ 16.0f` centres on the middle of a 32-pixel sprite rather than its top-lef ### The frame +The loop is chapter 20's, with two additions. Drain the events first: + +```c excerpt=examples/jrpg/jrpg.c + while ( SDL_PollEvent(&event) ) { + if ( event.type == SDL_EVENT_QUIT ) { + running = false; + } + PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event)); + } +``` + +Then the camera, the world, and the panel on top of it: + ```c excerpt=examples/jrpg/jrpg.c PASS(errctx, jrpg_camera_follow()); diff --git a/examples/sidescroller/sidescroller.h b/examples/sidescroller/sidescroller.h index a77ba8a..2f832ff 100644 --- a/examples/sidescroller/sidescroller.h +++ b/examples/sidescroller/sidescroller.h @@ -109,7 +109,7 @@ extern ss_Game ss_game; * * The physics backend resolves through it after every sub-move; the game only * touches it to ask questions -- a ledge probe, a wall probe, a spawn point that - * needs lifting clear. Everything that used to be in collision.c is behind it. + * needs lifting clear. */ extern akgl_CollisionWorld ss_collision;