They read as design commentary: why a choice was made, what the previous state of the project was, which library defect a line works around. That is useful to somebody who already knows the library and useless to somebody trying to build a game, which is who a tutorial is for. Both chapters now open with a screenshot of what the reader is building and a numbered list of the steps to get there, and each step is its own section: what you are trying to achieve, the code that achieves it, and the two or three things about that code that are easy to get wrong. Chapter 20 starts from first principles -- what a sprite, a character, an actor and a tilemap are, and the four error macros -- and chapter 21 assumes it and covers what a top-down game adds. Where a line exists because of a defect, the line comes first and the defect comes after it, with a pointer to the TODO entry that will remove the need. A reader who is copying code should not have to read an apology before they can use it. The library's own history is gone from both. That belongs in the design chapters and in git. Two new docs_examples setups stage the tutorials' own art, so the sprite and character files a reader is shown are loaded exactly as printed rather than through a generic fixture. `json excerpt=` is new for the same reason: a Tiled object is now quoted out of the real map instead of being retyped. Excerpts across docs/ go from 91 to 137. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
1182 lines
41 KiB
Markdown
1182 lines
41 KiB
Markdown
# 20. Tutorial: a 2D sidescroller
|
||
|
||

|
||
|
||
This chapter builds the game in that picture, from an empty directory. When you finish, you
|
||
will have a program that opens a window, loads a level drawn in [Tiled](https://mapeditor.org),
|
||
and runs a character who walks, jumps, lands on platforms, collects coins and dies on hazards.
|
||
|
||
The finished program is `examples/sidescroller/` in this repository, and every listing below
|
||
is quoted from it by the `docs_examples` test — so nothing here can describe code that does
|
||
not exist. You can read that program at any point, but you should not need to: the steps
|
||
below are complete.
|
||
|
||
## Before you start
|
||
|
||
You need a C compiler, CMake 3.10 or newer, and libakgl built. [Chapter 3](03-getting-started.md)
|
||
covers getting the library on your machine. You do not need to have read any other chapter.
|
||
|
||
Two conventions used throughout, both explained where they first appear:
|
||
|
||
- Every libakgl function returns `akerr_ErrorContext AKERR_NOIGNORE *`. That is how failures
|
||
travel. [Chapter 4](04-errors.md) is the reference; this chapter shows you the four macros
|
||
you actually need.
|
||
- Every symbol this game defines is prefixed `ss_`. The `akgl_` prefix belongs to the
|
||
library. A game built on it takes its own.
|
||
|
||
## First principles: what libakgl does for you
|
||
|
||
libakgl is a library, not an engine. It does not own your `main`, it has no editor, and it
|
||
never calls your code except through function pointers you install. You write a program that
|
||
calls it.
|
||
|
||
Five ideas hold the whole thing up. Read these once; the rest of the chapter is built out of
|
||
them.
|
||
|
||
**A spritesheet is one image file. A sprite is an animation cut out of it.** You give libakgl
|
||
a PNG and say how big one frame is; a sprite names that sheet and lists which frames to play,
|
||
in what order, at what speed.
|
||
|
||
**A character is a mapping from state to sprite.** An actor carries a 32-bit `state` word
|
||
made of bits like `AKGL_ACTOR_STATE_MOVING_LEFT` and `AKGL_ACTOR_STATE_FACE_RIGHT`. A
|
||
character says "when the state is exactly *these* bits, draw *this* sprite". One character is
|
||
shared by every goblin in the game.
|
||
|
||
**An actor is one thing in the world.** It has a position, a velocity, a state word, and a
|
||
pointer to the character that tells it how to look. Actors come from a fixed pool inside the
|
||
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.
|
||
|
||
**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.
|
||
|
||
## The steps
|
||
|
||
1. **Set up the project** — a directory, a `CMakeLists.txt`, and a header for your own
|
||
declarations.
|
||
2. **Open a window and run a frame loop** — the startup order that works, and the shortest
|
||
program that successfully draws nothing.
|
||
3. **Describe your art** — the spritesheet and sprite JSON files.
|
||
4. **Bind sprites to states** — the character JSON file.
|
||
5. **Draw a level** — a Tiled map, its tile layers, and the objects that become actors.
|
||
6. **Load the level** — in the one order that works, and honour the physics the map carries.
|
||
7. **Turn on collision** — a collision world, a shape on the player, and a `collidable`
|
||
layer.
|
||
8. **Make the player walk** — control maps, and the two hooks an actor gives you.
|
||
9. **Make the player jump** — an impulse, and how to know you are on the ground.
|
||
10. **Scroll the camera** — three lines.
|
||
11. **Collect coins and die on hazards** — overlap tests, and how to remove an actor.
|
||
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.
|
||
|
||
---
|
||
|
||
## 1. Set up the project
|
||
|
||
Make a directory with four files:
|
||
|
||
```text
|
||
sidescroller/
|
||
CMakeLists.txt
|
||
sidescroller.h your own declarations, shared between the .c files
|
||
main.c startup, the frame loop, teardown
|
||
player.c the player's behaviour and controls
|
||
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:
|
||
|
||
```cmake
|
||
add_executable(sidescroller
|
||
main.c
|
||
player.c
|
||
actors.c
|
||
)
|
||
|
||
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)
|
||
```
|
||
|
||
You link all four SDL libraries even though this game has no text and no sound, because
|
||
libakgl itself is built against them.
|
||
|
||
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:
|
||
|
||
```cmake
|
||
target_compile_definitions(sidescroller PRIVATE "SS_ASSET_DIR=\"${SS_ASSET_DIR}\"")
|
||
```
|
||
|
||
### The header
|
||
|
||
`sidescroller.h` holds the constants and declarations the three `.c` files share. Start it
|
||
with the level's geometry:
|
||
|
||
```c excerpt=examples/sidescroller/sidescroller.h
|
||
#define SS_TILE_SIZE 16 /* Pixels per map cell, from level1.tmj */
|
||
#define SS_VIEW_WIDTH 480 /* Camera width in map pixels */
|
||
#define SS_VIEW_HEIGHT 240 /* Camera height; the whole map is this tall */
|
||
#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
|
||
`main.c`:
|
||
|
||
```c excerpt=examples/sidescroller/sidescroller.h
|
||
typedef struct {
|
||
akgl_Actor *player; /**< Borrowed from the actor pool; the map created it. */
|
||
akgl_Actor *coins[SS_COIN_COUNT]; /**< Cleared to NULL as each one is collected. */
|
||
akgl_Actor *hazards[SS_HAZARD_COUNT]; /**< The blob and the moth. Borrowed, never released. */
|
||
int coins_taken;
|
||
int deaths;
|
||
```
|
||
|
||
Fixed arrays, not allocations. The level places a known number of things.
|
||
|
||
---
|
||
|
||
## 2. Open a window and run a frame loop
|
||
|
||
### How a libakgl function reports failure
|
||
|
||
Every call returns a pointer. `NULL` means success; anything else is an error context
|
||
carrying a status, a message and a stack trace. You never check it by hand — four macros do
|
||
that for you.
|
||
|
||
```c
|
||
#include <akerror.h>
|
||
#include <akgl/game.h>
|
||
#include <akgl/registry.h>
|
||
|
||
/* A function that calls libakgl and can fail. */
|
||
akerr_ErrorContext AKERR_NOIGNORE *my_setup(void)
|
||
{
|
||
PREPARE_ERROR(errctx); /* declares errctx; always first */
|
||
|
||
FAIL_ZERO_RETURN(errctx, akgl_gamemap, AKERR_NULLPOINTER, "no map");
|
||
PASS(errctx, akgl_set_property("game.screenwidth", "960"));
|
||
SUCCEED_RETURN(errctx); /* always last */
|
||
}
|
||
```
|
||
|
||
- `PREPARE_ERROR(errctx)` declares the local context. It is the first line of the function.
|
||
- `PASS(errctx, call)` makes the call and, if it failed, returns the error to *your* caller
|
||
with your function added to the trace. This is what you write most of the time.
|
||
- `FAIL_ZERO_RETURN(errctx, ptr, status, msg)` returns an error if `ptr` is `NULL`. Use it on
|
||
every pointer parameter before you dereference it.
|
||
- `SUCCEED_RETURN(errctx)` returns `NULL`. It is the last line of the function.
|
||
|
||
That is enough to write this entire game. [Chapter 4](04-errors.md) covers the rest,
|
||
including how to *handle* an error rather than propagate it — which you need exactly once, in
|
||
step 13.
|
||
|
||
### Startup, in order
|
||
|
||
libakgl has one startup sequence that works:
|
||
|
||
1. Fill in `akgl_game.name`, `.version` and `.uri`. `akgl_game_init` refuses to run without
|
||
all three — they become the window title and SDL's application metadata.
|
||
2. Call `akgl_game_init()`.
|
||
3. Set the configuration properties. **Before the renderer**, because the renderer reads
|
||
them.
|
||
4. Call `akgl_render_2d_init(akgl_renderer)`.
|
||
5. Call `akgl_physics_init_arcade(akgl_physics)`.
|
||
|
||
Here is the first part:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
PASS(errctx, aksl_strncpy(
|
||
(char *)&akgl_game.name,
|
||
sizeof(akgl_game.name),
|
||
"libakgl sidescroller tutorial",
|
||
sizeof(akgl_game.name) - 1));
|
||
```
|
||
|
||
`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`.
|
||
|
||
Then the properties and the renderer:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
PASS(errctx, akgl_set_property("game.screenwidth", "960"));
|
||
PASS(errctx, akgl_set_property("game.screenheight", "480"));
|
||
PASS(errctx, akgl_render_2d_init(akgl_renderer));
|
||
```
|
||
|
||
An unset property defaults to the string `"0"`, which asks SDL for a zero-sized window — so
|
||
set them before this call, not after.
|
||
|
||
### Scaling up the pixels
|
||
|
||
libakgl draws in map pixels. A 16-pixel tile is 16 screen pixels, which is very small on a
|
||
modern display. Ask SDL to scale the whole picture by whole multiples:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
SDL_SetRenderLogicalPresentation(
|
||
akgl_renderer->sdl_renderer,
|
||
SS_VIEW_WIDTH,
|
||
SS_VIEW_HEIGHT,
|
||
SDL_LOGICAL_PRESENTATION_INTEGER_SCALE),
|
||
```
|
||
|
||
The game now renders a 480×240 view into a 960×480 window. Tell the camera the same thing —
|
||
the camera is what the game looks *through*, and `akgl_render_2d_init` sized it from the
|
||
window:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
akgl_camera->x = 0.0f;
|
||
akgl_camera->y = 0.0f;
|
||
akgl_camera->w = (float32_t)SS_VIEW_WIDTH;
|
||
akgl_camera->h = (float32_t)SS_VIEW_HEIGHT;
|
||
```
|
||
|
||
### Choosing a physics backend
|
||
|
||
`akgl_game_init` does not choose one. Do it yourself:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
PASS(errctx, akgl_physics_init_arcade(akgl_physics));
|
||
```
|
||
|
||
Without this line the first frame calls through a null function pointer. Making the library
|
||
pick a default is tracked in `TODO.md` under "Known and still open"; until it does, this call
|
||
belongs in every libakgl program.
|
||
|
||
### The frame loop
|
||
|
||
Three things happen per frame: drain the event queue, position the camera, and call
|
||
`akgl_game_update`. The library does not clear or present the target, so you bracket that
|
||
call with the backend's own `frame_start` and `frame_end`:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
while ( SDL_PollEvent(&event) == true ) {
|
||
if ( event.type == SDL_EVENT_QUIT ) {
|
||
*running = false;
|
||
}
|
||
/* Every event, unconditionally: one that no control map binds is not an
|
||
* error, it is a call that did nothing. */
|
||
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
|
||
}
|
||
```
|
||
|
||
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.
|
||
|
||
```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:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
while ( running == true ) {
|
||
PASS(errctx, frame(&running));
|
||
```
|
||
|
||
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.
|
||
|
||
### Silencing the frame-rate warning
|
||
|
||
`akgl_game.lowfpsfunc` is called on every frame the frame rate is under 30, and
|
||
`akgl_game.fps` reads 0 until the first second has elapsed — so the default handler logs a
|
||
line per frame for the first second of every run. Install your own:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
static void ss_lowfps(void)
|
||
{
|
||
}
|
||
```
|
||
|
||
Assign it right after `akgl_game_init()`:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
akgl_game.lowfpsfunc = &ss_lowfps;
|
||
```
|
||
|
||
The hook exists so a game can shed work when it is running slowly. This one has nothing to
|
||
shed. The first-second false positive is recorded in `TODO.md`.
|
||
|
||
---
|
||
|
||
## 3. Describe your art
|
||
|
||
Put your PNG in the asset directory. This game uses a 32×32-per-frame sheet for the player,
|
||
laid out left to right.
|
||
|
||
A **sprite** is a JSON file naming the sheet and listing frames:
|
||
|
||
```json kind=sprite setup=sidescroller
|
||
{
|
||
"spritesheet": {
|
||
"filename": "player.png",
|
||
"frame_width": 32,
|
||
"frame_height": 32
|
||
},
|
||
"name": "ss_player_run_right",
|
||
"width": 32,
|
||
"height": 32,
|
||
"speed": 90,
|
||
"loop": true,
|
||
"loopReverse": false,
|
||
"frames": [
|
||
0,
|
||
1,
|
||
2,
|
||
1
|
||
]
|
||
}
|
||
```
|
||
|
||
| Field | Means |
|
||
|---|---|
|
||
| `spritesheet.filename` | The PNG, resolved relative to this file |
|
||
| `spritesheet.frame_width`/`_height` | How the sheet is cut into numbered frames, left to right then top to bottom |
|
||
| `name` | The registry name. This is how a character asks for it |
|
||
| `width`/`height` | How big to draw it. Usually the same as the frame size |
|
||
| `speed` | Milliseconds per frame |
|
||
| `loop` | Restart at the end, rather than holding the last frame |
|
||
| `frames` | Frame numbers, in play order. `0,1,2,1` is a four-step walk cycle from three drawings |
|
||
|
||
A still image is the same file with one frame and `"loop": false`.
|
||
|
||
This game needs nine sprites: idle, run and jump for the player facing each way, plus one
|
||
each for the coin, the blob and the moth. Load them from a list:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
static char *ss_sprite_files[] = {
|
||
"sprite_ss_player_idle_left.json", /* one frame, held */
|
||
"sprite_ss_player_idle_right.json",
|
||
"sprite_ss_player_run_left.json", /* four frames at 90 ms */
|
||
"sprite_ss_player_run_right.json",
|
||
"sprite_ss_player_jump_left.json", /* one frame, held for the whole arc */
|
||
"sprite_ss_player_jump_right.json",
|
||
"sprite_ss_coin.json",
|
||
"sprite_ss_hazard_blob.json",
|
||
"sprite_ss_hazard_moth.json",
|
||
NULL
|
||
};
|
||
```
|
||
|
||
Two sprites naming the same PNG share one texture. The spritesheet registry is keyed on the
|
||
file path, so `player.png` is decoded and uploaded once no matter how many sprites use it.
|
||
|
||
---
|
||
|
||
## 4. Bind sprites to states
|
||
|
||
An actor's whole 32-bit `state` word is the key that picks a sprite. A **character** is the
|
||
table that maps one to the other:
|
||
|
||
```json kind=character setup=sidescroller
|
||
{
|
||
"name": "ss_player",
|
||
"speedtime": 120,
|
||
"speed_x": 90.0,
|
||
"speed_y": 0.0,
|
||
"acceleration_x": 600.0,
|
||
"acceleration_y": 0.0,
|
||
"sprite_mappings": [
|
||
{
|
||
"state": [
|
||
"AKGL_ACTOR_STATE_ALIVE",
|
||
"AKGL_ACTOR_STATE_FACE_RIGHT"
|
||
],
|
||
"sprite": "ss_player_idle_right"
|
||
},
|
||
{
|
||
"state": [
|
||
"AKGL_ACTOR_STATE_ALIVE",
|
||
"AKGL_ACTOR_STATE_FACE_RIGHT",
|
||
"AKGL_ACTOR_STATE_MOVING_RIGHT"
|
||
],
|
||
"sprite": "ss_player_run_right"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
| Field | Means |
|
||
|---|---|
|
||
| `name` | Registry name. A Tiled object asks for it |
|
||
| `speed_x`/`speed_y` | Top speed on each axis, pixels per second |
|
||
| `acceleration_x`/`_y` | How fast the actor gets to that speed |
|
||
| `speedtime` | Milliseconds between animation frame advances |
|
||
| `sprite_mappings[].state` | The state bits, named. They are OR'd together |
|
||
| `sprite_mappings[].sprite` | The sprite's registry `name` |
|
||
|
||
**A mapping matches the whole state word, not a subset.** `ALIVE|FACE_RIGHT` and
|
||
`ALIVE|FACE_RIGHT|MOVING_RIGHT` are two different keys and need two entries. An actor whose
|
||
state matches no entry is silently not drawn — if a character disappears, this is the first
|
||
thing to check.
|
||
|
||
The player needs ten entries: idle and running each way, and jumping each way, with and
|
||
without a horizontal direction held. The full file is
|
||
`docs/tutorials/assets/sidescroller/character_ss_player.json`.
|
||
|
||
`speed_y` is `0.0` deliberately. This character never *thrusts* upward — jumping is an
|
||
impulse, added in step 9, and a zero vertical top speed keeps that impulse out of the physics
|
||
engine's speed cap.
|
||
|
||
Load characters after sprites:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
static char *ss_character_files[] = {
|
||
"character_ss_player.json",
|
||
"character_ss_coin.json",
|
||
"character_ss_hazard_blob.json",
|
||
"character_ss_hazard_moth.json",
|
||
NULL
|
||
};
|
||
```
|
||
|
||
A character's JSON names its sprites by registry name and the loader resolves each one as it
|
||
reads. Load a character before its sprites and it fails on the first name it cannot find.
|
||
|
||
---
|
||
|
||
## 5. Draw a level
|
||
|
||
Open Tiled, make a new map, and set it up like this:
|
||
|
||
| Setting | Value |
|
||
|---|---|
|
||
| Orientation | Orthogonal |
|
||
| Tile layer format | **CSV** (libakgl does not read compressed layer data) |
|
||
| Tile size | 16 × 16 |
|
||
| Map size | 40 × 15 tiles |
|
||
|
||
Add your tileset image, then three layers:
|
||
|
||
| Layer | Type | What it is |
|
||
|---|---|---|
|
||
| `background` | Tile layer | Sky, clouds. Drawn, never solid |
|
||
| `terrain` | Tile layer | Ground and platforms |
|
||
| `actors` | Object layer | Where things start |
|
||
|
||
### Making a layer solid
|
||
|
||
Select the `terrain` layer and add a custom property:
|
||
|
||
| Property | Type | Value |
|
||
|---|---|---|
|
||
| `collidable` | bool | true |
|
||
|
||
In the saved `.tmj` that is:
|
||
|
||
```json excerpt=docs/tutorials/assets/sidescroller/level1.tmj
|
||
"properties": [
|
||
{
|
||
"name": "collidable",
|
||
"type": "bool",
|
||
"value": true
|
||
}
|
||
]
|
||
```
|
||
|
||
Every non-empty cell of a `collidable` layer is solid. A layer without the property is drawn
|
||
and nothing more.
|
||
|
||
### Placing actors
|
||
|
||
On the `actors` layer, place a rectangle where each thing starts. Give each one:
|
||
|
||
- a **Name** — this is how your code finds it, so `player`, `coin1`, `blob1`
|
||
- a **Type** of `actor` — this is what tells libakgl to create one
|
||
- a custom property `character` (string) naming the character it uses
|
||
- a custom property `state` (int) — the starting state word
|
||
|
||
In the file, one object looks like this:
|
||
|
||
```json excerpt=docs/tutorials/assets/sidescroller/level1.tmj
|
||
"height": 32,
|
||
"id": 1,
|
||
"name": "player",
|
||
"properties": [
|
||
{
|
||
"name": "character",
|
||
"type": "string",
|
||
"value": "ss_player"
|
||
},
|
||
{
|
||
"name": "state",
|
||
"type": "int",
|
||
"value": 20
|
||
}
|
||
],
|
||
"rotation": 0,
|
||
"type": "actor",
|
||
"visible": true,
|
||
"width": 32,
|
||
"x": 32,
|
||
"y": 160
|
||
```
|
||
|
||
`20` is `AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_RIGHT` — 16 plus 4. Tiled has no
|
||
symbolic constants, so you write the number. `16` alone is alive and facing nowhere, which is
|
||
right for a coin.
|
||
|
||
### Giving the map its physics
|
||
|
||
Add three custom properties to the **map itself** (Map → Map Properties):
|
||
|
||
| Property | Type | Value | Means |
|
||
|---|---|---|---|
|
||
| `physics.model` | string | `arcade` | Which backend to build |
|
||
| `physics.gravity.y` | float | `900.0` | Downward acceleration, px/s² |
|
||
| `physics.drag.y` | float | `1.5` | Air resistance on the vertical axis |
|
||
|
||
Gravity of 900 px/s² with a drag of 1.5 gives a terminal fall speed of 600 px/s. Drag is what
|
||
bounds a fall, so do not set it to zero. (A `terminal_velocity` setting is in `TODO.md` under
|
||
"Arcade physics feel".)
|
||
|
||
Putting physics in the map rather than in code is what lets a swimming level and a walking
|
||
level differ by data.
|
||
|
||
---
|
||
|
||
## 6. Load the level
|
||
|
||
Order matters: **sprites, then characters, then the map**. The map creates actors that name
|
||
characters, and characters name sprites.
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
for ( i = 0; ss_sprite_files[i] != NULL; i++ ) {
|
||
PASS(errctx, asset_path(assetdir, ss_sprite_files[i], (char *)&path, sizeof(path)));
|
||
PASS(errctx, akgl_sprite_load_json((char *)&path));
|
||
}
|
||
for ( i = 0; ss_character_files[i] != NULL; i++ ) {
|
||
PASS(errctx, asset_path(assetdir, ss_character_files[i], (char *)&path, sizeof(path)));
|
||
PASS(errctx, akgl_character_load_json((char *)&path));
|
||
}
|
||
```
|
||
|
||
`asset_path` is a three-line helper that joins the directory and the file name with
|
||
`aksl_snprintf`.
|
||
|
||
Then the map:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
PASS(errctx, asset_path(assetdir, "level1.tmj", (char *)&path, sizeof(path)));
|
||
PASS(errctx, akgl_tilemap_load((char *)&path, akgl_gamemap));
|
||
```
|
||
|
||
**Load into `akgl_gamemap`, which already points at storage the library owns.** Do not
|
||
declare an `akgl_Tilemap` on the stack: it is about 26 MB, several times a default thread
|
||
stack, and you get a segfault before the loader writes a byte. Shrinking it is `TODO.md`
|
||
targets 14 and 15.
|
||
|
||
That one call creates an actor for every `actor` object in the object layer, binds each to
|
||
the character its property names, and publishes it in the actor registry under its Tiled
|
||
name.
|
||
|
||
### Honouring the map's physics
|
||
|
||
The loader built a backend from the map's properties but did not switch to it. That is your
|
||
call, one line:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
if ( akgl_gamemap->use_own_physics == true ) {
|
||
akgl_physics = &akgl_gamemap->physics;
|
||
```
|
||
|
||
### Restamping the clock
|
||
|
||
The physics step measures elapsed time from `akgl_physics->gravity_time`. Loading nine
|
||
sprites, four characters and a map took real time, and the first step would otherwise try to
|
||
simulate all of it at once:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
akgl_physics->gravity_time = SDL_GetTicksNS();
|
||
```
|
||
|
||
Do this after every load, immediately before the first frame.
|
||
|
||
### Finding the actors
|
||
|
||
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");
|
||
```
|
||
|
||
A miss means the map and the code disagree about what the level contains, which is worth
|
||
failing on.
|
||
|
||
Run now. The level draws, the actors appear, and everything falls through the floor.
|
||
|
||
---
|
||
|
||
## 7. Turn on collision
|
||
|
||
Collision is opt-in. Three lines switch it on:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
PASS(errctx, akgl_collision_world_init(&ss_collision, NULL, (float32_t)SS_TILE_SIZE, (float32_t)SS_TILE_SIZE));
|
||
PASS(errctx, akgl_collision_bind_tilemap(&ss_collision, akgl_gamemap));
|
||
akgl_physics->collision = &ss_collision;
|
||
```
|
||
|
||
`ss_collision` is one global `akgl_CollisionWorld`, defined in `main.c` and declared `extern`
|
||
in your header. `NULL` for the second argument means the default spatial index, which is the
|
||
one to use. `akgl_collision_bind_tilemap` reads the tile size off the map and finds the
|
||
`collidable` layers you marked in step 5.
|
||
|
||
Do this *after* loading the map and *after* switching to the map's physics backend.
|
||
|
||
### Giving the player a body
|
||
|
||
A collision shape is a box measured from the actor's position. Inset it into the sprite frame
|
||
— art does not reach the edges of its cell, and a full-frame box catches on doorways the
|
||
character visibly clears:
|
||
|
||
```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
|
||
```
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
PASS(errctx, akgl_collision_shape_box(&obj->shape, &ss_player_body, 0.0f));
|
||
obj->shape_override = true;
|
||
```
|
||
|
||
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.
|
||
|
||
An actor with a shape collides with map geometry and with *no other actor*. That is the
|
||
default, and it is what you want in a level full of scenery. Actor-versus-actor is one added
|
||
bit; [Chapter 15](15-collision.md) covers the masks.
|
||
|
||
### Lifting a spawn point clear
|
||
|
||
Level editors round objects onto a grid, so a spawn point often overlaps a tile. Collision
|
||
stops an actor *entering* geometry and has nothing to say about one that started inside it —
|
||
such an actor is simply stuck. Lift it clear once, before the first frame:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
PASS(errctx, akgl_collision_settle(&ss_collision, &obj->shape, &obj->x, &obj->y, 0));
|
||
```
|
||
|
||
Run now. The player and the blob stand on the ground. Nothing moves yet.
|
||
|
||
---
|
||
|
||
## 8. Make the player walk
|
||
|
||
### The two hooks
|
||
|
||
An actor carries seven function pointers. Two matter here, and the difference between them is
|
||
*when in the frame they run*:
|
||
|
||
| Hook | Called by | Put here |
|
||
|---|---|---|
|
||
| `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 |
|
||
|
||
Install them **after** the actor exists. `akgl_actor_initialize` — which the map loader
|
||
already ran — overwrites all seven:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
obj->movementlogicfunc = &ss_player_movement;
|
||
obj->updatefunc = &ss_player_update;
|
||
```
|
||
|
||
Replacing a hook does not mean reimplementing it. Call the default first and add to it:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
PASS(errctx, akgl_actor_logic_movement(obj, dt));
|
||
```
|
||
|
||
`akgl_actor_logic_movement` copies the character's speeds onto the actor and turns the
|
||
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:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
control.event_on = SDL_EVENT_KEY_DOWN;
|
||
control.event_off = SDL_EVENT_KEY_UP;
|
||
|
||
control.key = SDLK_LEFT;
|
||
control.handler_on = &akgl_actor_cmhf_left_on;
|
||
control.handler_off = &ss_control_left_off;
|
||
PASS(errctx, akgl_controller_pushmap(controlmapid, &control));
|
||
```
|
||
|
||
`akgl_actor_cmhf_left_on` is one of the library's built-in handlers: it sets
|
||
`AKGL_ACTOR_STATE_MOVING_LEFT` and signs the acceleration. There is a matching
|
||
`akgl_actor_cmhf_left_off`; step 9 explains why this game supplies its own instead.
|
||
|
||
Point the map at the actor, and set both device ids to 0, which means "any":
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
controlmap->kbid = 0;
|
||
controlmap->jsid = 0;
|
||
```
|
||
|
||
Use 0 unless you are writing a two-player game on two keyboards. The id a key event carries
|
||
is chosen by the video backend and is not the id `SDL_GetKeyboards()` reports.
|
||
|
||
A gamepad is the same table with different event types. Clear `key` first — a keyboard event
|
||
is matched on `key` whatever else the binding carries, and 0 is a keycode like any other:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
control.key = 0;
|
||
control.event_on = SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
||
control.event_off = SDL_EVENT_GAMEPAD_BUTTON_UP;
|
||
```
|
||
|
||
Bind controls **after** the map is loaded. The map is what creates the actor the control map
|
||
targets.
|
||
|
||
### Keeping the player visible
|
||
|
||
Add one line where you set up the player:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
obj->movement_controls_face = false;
|
||
```
|
||
|
||
The default facing logic clears every facing bit and sets one from the *movement* bits — so
|
||
an actor that stops moving is left facing nowhere. Its state drops to bare `ALIVE`, which
|
||
your character has no sprite for, and it stops being drawn. Clearing this field leaves the
|
||
facing bits wherever the control handlers put them. Making the default behave is tracked in
|
||
`TODO.md`.
|
||
|
||
Run now. The arrow keys walk the player, the run animation plays, and the player stops at
|
||
walls.
|
||
|
||
---
|
||
|
||
## 9. Make the player jump
|
||
|
||
### Knowing you are on the ground
|
||
|
||
Nothing in libakgl records whether an actor is standing on something, and a collision contact
|
||
does not answer it either — a contact says something pushed back this step, which is a
|
||
different question from "is there a floor to push off". Ask directly:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
PASS(errctx, akgl_collision_shape_bounds(shape, x, y + 1.0f, &feet));
|
||
PASS(errctx, akgl_collision_box_blocked(&ss_collision, &feet, AKGL_COLLISION_LAYER_STATIC, dest));
|
||
```
|
||
|
||
`akgl_collision_shape_bounds` turns a shape plus a position into a rectangle;
|
||
`akgl_collision_box_blocked` says whether that rectangle overlaps anything solid. One pixel
|
||
down, and only one — a taller probe reports a floor the actor is still falling towards, and a
|
||
jump that fires off it looks like the player jumped out of thin air.
|
||
|
||
Call it at the top of `movementlogicfunc`:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
PASS(errctx, ss_grounded(&obj->shape, obj->x, obj->y, &ss_game.grounded));
|
||
```
|
||
|
||
### The jump
|
||
|
||
An actor's velocity has two parts that are added together every step:
|
||
|
||
- **`tx`, `ty`** — *thrust*. What the character is pushing itself with. Capped against the
|
||
character's `speed_x`/`speed_y`.
|
||
- **`ex`, `ey`** — *environment*. What the world is doing to it. Gravity accumulates here.
|
||
|
||
A jump is an impulse into `ey`, not thrust:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
if ( (ss_game.jump_requested == true) && (ss_game.grounded == true) ) {
|
||
obj->ey = -SS_JUMP_SPEED;
|
||
}
|
||
ss_game.jump_requested = false;
|
||
```
|
||
|
||
It has to be `ey`, because `ey` is where gravity accumulates and the two must cancel for the
|
||
arc to come back down. Written as thrust it would be capped against the character's `speed_y`
|
||
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
|
||
ss_game.jump_requested = true;
|
||
```
|
||
|
||
Releasing the button early cuts the jump short. Four lines gives you variable jump height:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
if ( obj->ey < 0.0f ) {
|
||
obj->ey *= 0.4f;
|
||
}
|
||
```
|
||
|
||
### Picking the jump sprite
|
||
|
||
Set `AKGL_ACTOR_STATE_MOVING_UP` whenever the actor is off the ground, and your character's
|
||
mappings do the rest:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
if ( ss_game.grounded == true ) {
|
||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||
} else {
|
||
AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||
}
|
||
```
|
||
|
||
Use `AKGL_BITMASK_ADD`, `_DEL` and `_HAS` rather than writing `|` and `&` by hand.
|
||
|
||
### Friction
|
||
|
||
Bind your own release handlers, which clear the movement bit and the acceleration but leave
|
||
the thrust alone:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
static akerr_ErrorContext *ss_control_left_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");
|
||
obj->ax = 0.0f;
|
||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_LEFT);
|
||
SUCCEED_RETURN(errctx);
|
||
}
|
||
```
|
||
|
||
Then decay the thrust yourself, faster on the ground than in the air:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
obj->tx -= obj->tx * friction * dt;
|
||
if ( fabsf(obj->tx) < 1.0f ) {
|
||
obj->tx = 0.0f;
|
||
}
|
||
```
|
||
|
||
```c excerpt=examples/sidescroller/sidescroller.h
|
||
#define SS_FRICTION_GROUND 12.0f
|
||
#define SS_FRICTION_AIR 1.5f
|
||
```
|
||
|
||
Snap to zero below a pixel per second, because an exponential decay never actually arrives.
|
||
|
||
The library's own `akgl_actor_cmhf_left_off` zeroes `tx` outright, which stops the actor dead
|
||
in one frame — right for a top-down game, wrong for a sidescroller. Friction and deceleration
|
||
in the backend are tracked in `TODO.md` under "Arcade physics feel"; when they land, this
|
||
whole section becomes a setting.
|
||
|
||
Run now. The player runs, slides to a stop, jumps, and holds the jump sprite through the arc.
|
||
|
||
---
|
||
|
||
## 10. Scroll the camera
|
||
|
||
`akgl_camera` is a plain `SDL_FRect` in map pixels that the library reads. Moving it is the
|
||
whole of scrolling:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
limit = (float32_t)(akgl_gamemap->width * akgl_gamemap->tilewidth) - akgl_camera->w;
|
||
akgl_camera->x = (ss_game.player->x + 16.0f) - (akgl_camera->w / 2.0f);
|
||
if ( akgl_camera->x > limit ) {
|
||
akgl_camera->x = limit;
|
||
}
|
||
if ( akgl_camera->x < 0.0f ) {
|
||
akgl_camera->x = 0.0f;
|
||
}
|
||
akgl_camera->x = (float32_t)((int)akgl_camera->x);
|
||
```
|
||
|
||
Centre on the player, clamp to the level, and **floor to a whole pixel**. The tile drawing
|
||
truncates the camera position when it works out how much of an edge tile to show, so a camera
|
||
that is fractionally different every frame makes the tile grid shimmer.
|
||
|
||
Call it once per frame, before `akgl_game_update`.
|
||
|
||
---
|
||
|
||
## 11. Collect coins and die on hazards
|
||
|
||
This is game logic, not physics, so it goes in `updatefunc`. Call the default first:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
PASS(errctx, akgl_actor_update(obj));
|
||
```
|
||
|
||
### Overlap tests
|
||
|
||
`akgl_collide_rectangles` answers "do these two rectangles overlap" with a `bool`. That is
|
||
the right tool for a pickup — you want to know, not to be pushed:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
PASS(errctx, hitbox(ss_game.coins[i], 8.0f, &other));
|
||
PASS(errctx, akgl_collide_rectangles(&player, &other, &hit));
|
||
```
|
||
|
||
`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.
|
||
|
||
### Removing an actor
|
||
|
||
There is no "despawn" call. Giving the pool slot back is what unregisters the actor and stops
|
||
it being drawn:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
PASS(errctx, akgl_heap_release_actor(ss_game.coins[i]));
|
||
ss_game.coins[i] = NULL;
|
||
ss_game.coins_taken += 1;
|
||
```
|
||
|
||
Releasing another actor from inside the update sweep is safe: the sweep re-reads the
|
||
reference count at the top of every iteration and skips a slot that has gone free. Clear your
|
||
own pointer to it in the same breath.
|
||
|
||
### Falling out of the level
|
||
|
||
Nothing stops an actor leaving the map, so check for it:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
if ( obj->y > (float32_t)(akgl_gamemap->height * akgl_gamemap->tileheight) ) {
|
||
PASS(errctx, respawn(obj));
|
||
SUCCEED_RETURN(errctx);
|
||
}
|
||
```
|
||
|
||
### Respawning
|
||
|
||
Clear everything the simulation carries between steps, not just the position:
|
||
|
||
```c excerpt=examples/sidescroller/player.c
|
||
obj->x = data->home_x;
|
||
obj->y = data->home_y;
|
||
obj->ex = 0.0f;
|
||
obj->ey = 0.0f;
|
||
obj->tx = 0.0f;
|
||
obj->ty = 0.0f;
|
||
obj->vx = 0.0f;
|
||
obj->vy = 0.0f;
|
||
```
|
||
|
||
`ey` is where the fall accumulated. A player who respawns still holding a full-speed fall
|
||
lands dead again immediately.
|
||
|
||
### Where to keep per-actor data
|
||
|
||
`akgl_Actor::actorData` is a `void *` the library never reads or frees. Point it at a fixed
|
||
table:
|
||
|
||
```c excerpt=examples/sidescroller/actors.c
|
||
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;
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Add moving enemies
|
||
|
||
### Something that does not move at all
|
||
|
||
The coins need a `movementlogicfunc` of their own. Their character has no speed and no
|
||
acceleration so they cannot thrust — but gravity is not thrust, and a coin left to the
|
||
default logic falls out of the level with everything else.
|
||
|
||
Raising `AKGL_ERR_LOGICINTERRUPT` is how an actor opts out of the rest of its step. It is
|
||
**not a failure**: the physics step catches it, skips gravity, drag, the move and collision
|
||
for that actor, and carries on to the next one.
|
||
|
||
```c excerpt=examples/sidescroller/actors.c
|
||
static akerr_ErrorContext *ss_static_movement(akgl_Actor *obj, float32_t dt)
|
||
{
|
||
PREPARE_ERROR(errctx);
|
||
(void)dt;
|
||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||
FAIL_RETURN(errctx, AKGL_ERR_LOGICINTERRUPT, "%s does not simulate", (char *)obj->name);
|
||
}
|
||
```
|
||
|
||
**Only a `movementlogicfunc` may raise it.** From anywhere else it aborts the whole physics
|
||
step and leaves every remaining actor unsimulated.
|
||
|
||
### A blob that patrols
|
||
|
||
The blob walks under gravity like the player does and the library resolves it the same way.
|
||
Its hook only decides which way to face next.
|
||
|
||
Set the facing and movement bits, then let the default logic sign the acceleration:
|
||
|
||
```c excerpt=examples/sidescroller/actors.c
|
||
AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL));
|
||
if ( data->facing < 0.0f ) {
|
||
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_LEFT | AKGL_ACTOR_STATE_MOVING_LEFT));
|
||
} else {
|
||
AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_MOVING_RIGHT));
|
||
}
|
||
```
|
||
|
||
Then probe for a wall ahead and a floor ahead:
|
||
|
||
```c excerpt=examples/sidescroller/actors.c
|
||
PASS(errctx, akgl_collision_shape_bounds(&obj->shape, (obj->x + step), obj->y, &ahead));
|
||
PASS(errctx, akgl_collision_box_blocked(&ss_collision, &ahead, AKGL_COLLISION_LAYER_STATIC, &wall_ahead));
|
||
PASS(errctx, ss_grounded(&obj->shape, obj->x, obj->y, &grounded));
|
||
```
|
||
|
||
```c excerpt=examples/sidescroller/actors.c
|
||
PASS(errctx, akgl_collision_solid_at(&ss_collision, probe_x, probe_y, &floor_ahead));
|
||
```
|
||
|
||
`akgl_collision_solid_at` is the cheapest query there is: is the tile under this one point
|
||
solid.
|
||
|
||
Turn around on either:
|
||
|
||
```c excerpt=examples/sidescroller/actors.c
|
||
if ( (wall_ahead == true) || ((grounded == true) && (floor_ahead == false)) ) {
|
||
data->facing = -data->facing;
|
||
```
|
||
|
||
### A moth that flies
|
||
|
||
A flying enemy wants no gravity, and the map has gravity because the player needs it. Rather
|
||
than fighting the backend, write the position directly and then opt out:
|
||
|
||
```c excerpt=examples/sidescroller/actors.c
|
||
data->phase += dt;
|
||
obj->x = data->home_x + (sinf(data->phase) * 64.0f);
|
||
obj->y = data->home_y + (sinf(data->phase * 2.0f) * 24.0f);
|
||
```
|
||
|
||
```c excerpt=examples/sidescroller/actors.c
|
||
FAIL_RETURN(errctx, AKGL_ERR_LOGICINTERRUPT, "%s flies itself", (char *)obj->name);
|
||
```
|
||
|
||
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:
|
||
|
||
```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;
|
||
```
|
||
|
||
The moth needs no shape — it never touches terrain.
|
||
|
||
---
|
||
|
||
## 13. Tear down
|
||
|
||
**There is no `akgl_game_shutdown`.** Teardown is yours:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
IGNORE(akgl_text_unloadallfonts());
|
||
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||
if ( akgl_heap_actors[i].refcount > 0 ) {
|
||
IGNORE(akgl_heap_release_actor(&akgl_heap_actors[i]));
|
||
}
|
||
}
|
||
```
|
||
|
||
`IGNORE` is the fourth macro: make the call and discard the error. It is the right thing on a
|
||
teardown path, where there is nobody left to report to. It is the *wrong* thing anywhere
|
||
else.
|
||
|
||
**`akgl_text_unloadallfonts` must run before `TTF_Quit`**, which destroys the fonts
|
||
underneath the registry that still points at them.
|
||
|
||
Do not call `akgl_tilemap_release` unless you are loading a second level — it has a
|
||
double-free, recorded in `TODO.md` under "Known and still open" item 2. A process that is
|
||
exiting can leave the textures to `SDL_Quit`.
|
||
|
||
### Reporting a failure from `main`
|
||
|
||
`main` returns `int`, so the usual `FINISH` will not compile there. Use this shape:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
ATTEMPT {
|
||
CATCH(errctx, parse_args(argc, argv, &assetdir, &frames));
|
||
CATCH(errctx, startup());
|
||
CATCH(errctx, load_level(assetdir));
|
||
CATCH(errctx, run(frames));
|
||
} CLEANUP {
|
||
shutdown_game();
|
||
} PROCESS(errctx) {
|
||
} HANDLE_DEFAULT(errctx) {
|
||
LOG_ERROR_WITH_MESSAGE(errctx, "the sidescroller could not run");
|
||
```
|
||
|
||
- `ATTEMPT` … `CLEANUP` … `PROCESS` … `HANDLE_DEFAULT` … `FINISH_NORETURN` is the
|
||
error-handling block. `CATCH` inside it is what `PASS` is outside it.
|
||
- `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.
|
||
- Set a flag in `HANDLE_DEFAULT` and return it after `FINISH_NORETURN`. Returning from inside
|
||
a `HANDLE` block leaks the error context's pool slot.
|
||
|
||
---
|
||
|
||
## Build it and run it
|
||
|
||
```sh norun
|
||
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||
cmake --build build --parallel
|
||
./build/examples/sidescroller/sidescroller
|
||
```
|
||
|
||
| Control | Keyboard | Gamepad |
|
||
|---|---|---|
|
||
| Walk left / right | ← → | D-pad left / right |
|
||
| Jump | Space | A (south) |
|
||
| Quit | Close the window | — |
|
||
|
||
Holding the jump key longer jumps higher.
|
||
|
||
To run it without a display — in CI, or to check that it still works:
|
||
|
||
```sh norun
|
||
SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIODRIVER=dummy \
|
||
./build/examples/sidescroller/sidescroller --frames 240 --autoplay
|
||
```
|
||
|
||
It prints where the player finished:
|
||
|
||
```text
|
||
sidescroller: 240 frames, 0 of 4 coins, 0 deaths, player at 136.0,160.0 grounded
|
||
```
|
||
|
||
`grounded` and a sensible `y` are how you know collision ran. If the player is hundreds of
|
||
pixels below the level, revisit step 7.
|
||
|
||
## Where to look next
|
||
|
||
- [Chapter 15](15-collision.md) — the collision masks, the query API, and what happens when
|
||
something moves faster than 1280 px/s.
|
||
- [Chapter 14](14-physics.md) — thrust versus environment, and the four gaps in the feel this
|
||
chapter works around.
|
||
- [Chapter 12](12-actors.md) — the other five behaviour hooks, and parent/child actors.
|
||
- [Chapter 13](13-tilemaps.md) — the rest of the map format and the limits that bind a level.
|
||
- [Chapter 21](21-tutorial-jrpg.md) — the same shape of program with no gravity, NPCs and a
|
||
text box.
|