A sweep of all 265 tracked files, not just the ones the migration touched. Two cross-repository citations named deps/libakerror/TODO.md item 8, a numbering that has not existed for two releases; both now name libakerror issue #18. Three tutorial and plan references pointed at TODO.md for defects that are tracked: the invisible-on-frame-one actor is #39, the control map traps are #81, and the pkg-config gap is #17. plan.md is a live instruction document for writing the manual and still told its reader to file findings in TODO.md; it says the tracker now. Filed while sweeping: a zeroed akgl_ControlMap matches gamepad South, because SDL_GAMEPAD_BUTTON_SOUTH is 0 and the two arms of the match are one expression, and handler_on/handler_off are called without a NULL check. Both are documented as workarounds in chapter 21 and neither had an issue. #81. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
1676 lines
60 KiB
Markdown
1676 lines
60 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.
|
||
|
||
**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.
|
||
|
||
## 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.
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
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
|
||
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)
|
||
|
||
target_compile_definitions(sidescroller
|
||
PRIVATE "SS_ASSET_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/assets\"")
|
||
```
|
||
|
||
The parts that matter:
|
||
|
||
Each `.c` file includes `sidescroller.h` plus whatever it calls directly:
|
||
|
||
| File | Adds |
|
||
|---|---|
|
||
| `main.c` | `<string.h>`, `akstdlib.h`, `SDL3_image/SDL_image.h`, `SDL3_mixer/SDL_mixer.h`, `SDL3_ttf/SDL_ttf.h`, `akgl/character.h`, `akgl/controller.h`, `akgl/heap.h`, `akgl/registry.h`, `akgl/renderer.h`, `akgl/sprite.h`, `akgl/text.h`, `akgl/tilemap.h` |
|
||
| `player.c` | `<math.h>`, `akstdlib.h`, `akgl/controller.h`, `akgl/heap.h`, `akgl/physics.h`, `akgl/registry.h`, `akgl/util.h` |
|
||
| `actors.c` | `<math.h>`, `akstdlib.h`, `akgl/heap.h`, `akgl/registry.h` |
|
||
|
||
**libakstdlib's header is `<akstdlib.h>`**, not `<aksl.h>` — its functions are prefixed
|
||
`aksl_` but the file is not. libakerror's is `<akerror.h>`. libakgl's own headers are
|
||
self-contained, so including the one that declares what you are calling is always enough.
|
||
|
||
The build file:
|
||
|
||
- 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. Give it a fallback so the file still compiles without CMake, and **use it as the
|
||
default**: a program that defaults to `"."` only finds its assets when it happens to be
|
||
launched from the right directory.
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
#ifndef SS_ASSET_DIR
|
||
#define SS_ASSET_DIR "."
|
||
#endif
|
||
```
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
char *assetdir = SS_ASSET_DIR;
|
||
```
|
||
|
||
### The header
|
||
|
||
`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 */
|
||
```
|
||
|
||
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
|
||
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;
|
||
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 <stdbool.h>
|
||
|
||
#include <SDL3/SDL.h>
|
||
|
||
#include <akerror.h>
|
||
|
||
#include <akgl/actor.h>
|
||
#include <akgl/collision.h>
|
||
#include <akgl/error.h>
|
||
#include <akgl/game.h>
|
||
#include <akgl/physics.h>
|
||
#include <akgl/tilemap.h>
|
||
#include <akgl/types.h>
|
||
```
|
||
|
||
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
|
||
|
||
### 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));
|
||
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`.
|
||
|
||
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 issue #36; 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
|
||
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;
|
||
}
|
||
/* 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.
|
||
|
||
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:
|
||
|
||
```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.
|
||
|
||
### 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`, "Performance".
|
||
|
||
---
|
||
|
||
## 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
|
||
|
||
**Draw the map in [Tiled](https://mapeditor.org) and save it as `.tmj`.** The rest of this
|
||
section is what to set in the editor; Tiled writes the JSON. [Chapter 13](13-tilemaps.md) is
|
||
the reference for the format itself, and for what libakgl reads out of it and what it
|
||
ignores — read that before hand-editing a `.tmj`, because the loader needs several fields
|
||
Tiled fills in automatically (a root-level `width` and `height`, an `id` on every layer, and
|
||
`tilecount`, `columns`, `imagewidth` and `imageheight` on every tileset).
|
||
|
||
Make a new map and set it up like this:
|
||
|
||
| Setting | Value |
|
||
|---|---|
|
||
| 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 issue #29.)
|
||
|
||
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 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:
|
||
|
||
```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 issue #28
|
||
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");
|
||
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.
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
`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.
|
||
|
||
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. `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 | Default does |
|
||
|---|---|---|
|
||
| `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:
|
||
|
||
```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
|
||
|
||
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;
|
||
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;
|
||
|
||
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.
|
||
|
||
### 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 issue #39.
|
||
|
||
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
|
||
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);
|
||
}
|
||
```
|
||
|
||
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
|
||
|
||
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);
|
||
}
|
||
```
|
||
|
||
`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
|
||
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 issues #29 through #32; 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.
|
||
|
||
---
|
||
|
||
## 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`.
|
||
|
||
Run now. The level scrolls as the player walks, and stops scrolling at both ends.
|
||
|
||
---
|
||
|
||
## 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:
|
||
|
||
```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
|
||
|
||
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
|
||
|
||
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;
|
||
obj->ey = 0.0f;
|
||
obj->tx = 0.0f;
|
||
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
|
||
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];
|
||
```
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
## 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. `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));
|
||
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));
|
||
```
|
||
|
||
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.
|
||
|
||
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
|
||
|
||
`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 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.
|
||
|
||
---
|
||
|
||
## 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 "Defects". 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");
|
||
```
|
||
|
||
- `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.
|
||
- 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
|
||
```
|
||
|
||
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
|
||
sidescroller: 240 frames, 0 of 4 coins, 0 deaths, player at 136.0,160.0 grounded
|
||
```
|
||
|
||
**Capture that position at the end of `run()`, not in `main`.** Teardown happens in `main`'s
|
||
`CLEANUP` block and releases the actor pool, so by the time the summary is printed
|
||
`ss_game.player` points at a slot that has been given back — and the line reports `0.0,0.0`:
|
||
|
||
```c excerpt=examples/sidescroller/main.c
|
||
if ( ss_game.player != NULL ) {
|
||
ss_game.final_x = ss_game.player->x;
|
||
ss_game.final_y = ss_game.player->y;
|
||
}
|
||
```
|
||
|
||
That is what `final_x` and `final_y` on `ss_Game` are for.
|
||
|
||
`grounded` and a sensible `y` are how you know collision ran. If the player is hundreds of
|
||
pixels below the level, revisit step 7.
|
||
|
||
## 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.
|