Two readers were given the library, the art, the chapter, and no reference
program, and told to compile and run. Both got a working game. These are the
defects that only came out because a compiler and a frame loop were involved --
four earlier read-only reviews had missed every one.
libakstdlib's header was never named. The chapters used aksl_strncpy,
aksl_snprintf and aksl_atoi throughout and said only "libakstdlib is documented
in deps/libakstdlib"; the obvious guess from the function prefix is <aksl.h> and
it is wrong. That was the only compile error the sidescroller reader hit, and it
was the chapter's fault. Both chapters now carry a per-file include table and say
which header it is.
The sidescroller's asset directory defaulted to ".". The chapter said the code
"falls back to `.`" without showing the #ifndef or that assetdir is initialised
from the macro, so a reader gets a game that only finds its art when launched
from exactly the right directory.
The sidescroller's summary line reported 0.0,0.0 airborne while the screenshot
showed the player standing on the ground. Teardown runs in main's CLEANUP block
and releases the actor pool before the summary prints. The chapter published an
example output a reader could not reproduce; the capture that makes it true is
now shown beside it.
The JRPG's CMake block interpolated ${JRPG_REPO_ROOT}, which is this repository's
own variable and undefined for anybody else -- so the font path came out wrong
and akgl_text_loadfont failed long after everything else had loaded. It is
${CMAKE_CURRENT_SOURCE_DIR} now, and the chapter says why both definitions are
needed rather than one.
Chapter 21 described --demo and a summary line in its build section that no step
wrote. Both are now a section: the script table, playback through
akgl_controller_handle_event rather than the handlers, the fixed clock step, and
printing the position before teardown. It also now says libakgl keeps no frame
counter, which a reader assumed it did.
Both map sections now say to draw it in Tiled and point at chapter 13, naming the
fields the loader needs that Tiled fills in automatically -- both readers
hand-wrote a .tmj before being given one and hit exactly those.
Excerpts across docs/ go from 190 to 198.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
1248 lines
41 KiB
Markdown
1248 lines
41 KiB
Markdown
# 21. Tutorial: a top-down JRPG
|
||
|
||

|
||
|
||
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 town drawn in [Tiled](https://mapeditor.org),
|
||
and runs a character who walks in four directions, is blocked by buildings, is followed by a
|
||
party member, and can talk to the townsfolk.
|
||
|
||
The finished program is `examples/jrpg/` 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.
|
||
|
||
## 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.
|
||
|
||
**[Chapter 20](20-tutorial-sidescroller.md) is the better place to start** if you have not
|
||
used libakgl before. It covers the same ground from a standing start — the error macros, the
|
||
startup order, the sprite and character JSON formats, the Tiled setup — and this chapter
|
||
assumes them rather than repeating them. What is new here is four-way movement, NPCs, text on
|
||
screen, a follower and freezing the world.
|
||
|
||
Every symbol this game defines is prefixed `jrpg_`.
|
||
|
||
## First principles: what a top-down game changes
|
||
|
||
The library is the same. Four things about *using* it differ from a sidescroller, and they
|
||
are why this chapter exists.
|
||
|
||
**Gravity is zero on both axes.** The map declares it. Nothing falls, so `speed_y` on a
|
||
character is a real walking speed rather than 0.
|
||
|
||
**There are four facing directions, not two.** A character needs eight sprite mappings —
|
||
idle and walking, up, down, left and right — instead of the sidescroller's four.
|
||
|
||
**Most actors never move.** NPCs stand where the map put them, and are spoken to rather than
|
||
walked into. That changes what you do about their facing bits, and it means they get no
|
||
collision shape.
|
||
|
||
**The world can be frozen.** While a conversation is open the player stops, and everything
|
||
else stops with them. libakgl has a status for exactly this.
|
||
|
||
## The steps
|
||
|
||
1. **Set up the project** — the directory, the build file, and where the font comes from.
|
||
2. **Start up and load the town** — the same order as chapter 20, plus a font.
|
||
3. **Describe four-way art** — twenty-four sprites from a naming convention.
|
||
4. **Bind eight states per character** — the character JSON for four-way movement.
|
||
5. **Draw the town** — a Tiled map with zero gravity and a solid decoration layer.
|
||
6. **Keep the NPCs visible** — one line, and the reason it is needed.
|
||
7. **Wall the town in** — solid tiles for buildings, static proxies for the map's edge.
|
||
8. **Walk in four directions** — one call binds the whole arrow-key set.
|
||
9. **Follow the player** — a child actor, and the one thing to know about drawing it.
|
||
10. **Put text on screen** — a font, a panel and a line of dialogue.
|
||
11. **Talk to somebody** — a proximity test and a key binding of your own.
|
||
12. **Freeze the world** — `AKGL_ERR_LOGICINTERRUPT`, in a game.
|
||
13. **Follow with the camera, and tear down**.
|
||
|
||
The steps are cumulative — each one adds to the same files rather than replacing them. Steps
|
||
3, 4 and 5 produce data files rather than code. From step 6 onward the program builds and
|
||
runs at the end of every step, and the text says what you should see.
|
||
|
||
---
|
||
|
||
## 1. Set up the project
|
||
|
||
```text
|
||
jrpg/
|
||
CMakeLists.txt
|
||
jrpg.h shared declarations
|
||
jrpg.c startup, the frame loop, teardown, controls
|
||
world.c assets, the map, the actors and their behaviour
|
||
textbox.c the dialogue panel
|
||
```
|
||
|
||
The build file is the sidescroller's, plus one thing: this game draws text, so it needs a
|
||
TrueType font at runtime. Bake both paths in at compile time:
|
||
|
||
```cmake
|
||
target_compile_definitions(jrpg PRIVATE
|
||
JRPG_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets"
|
||
JRPG_FONT_FILE="${CMAKE_CURRENT_SOURCE_DIR}/assets/font.ttf"
|
||
)
|
||
```
|
||
|
||
**Two definitions, not one.** The font is a separate path from the assets, and a game that
|
||
defines only `JRPG_ASSET_DIR` fails at `akgl_text_loadfont` with "Couldn't open" — after
|
||
everything else has loaded, which makes it look like a font problem rather than a build
|
||
one.
|
||
|
||
Give them defaults in the header so the file still compiles on its own:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
#ifndef JRPG_ASSET_DIR
|
||
#define JRPG_ASSET_DIR "docs/tutorials/assets/jrpg"
|
||
```
|
||
|
||
The screen is smaller than the sidescroller's and is not scaled up, because the town's art is
|
||
drawn at map scale:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
#define JRPG_SCREEN_WIDTH "320"
|
||
#define JRPG_SCREEN_HEIGHT "240"
|
||
```
|
||
|
||
These are strings because `akgl_set_property` takes strings. `akgl_render_2d_init` copies
|
||
them onto `akgl_camera` on the way past, so everything downstream reads the camera rather
|
||
than a second copy of the same two numbers.
|
||
|
||
### What the header has to include, and declare
|
||
|
||
Each `.c` file includes `jrpg.h` plus whatever it uses directly:
|
||
|
||
| File | Adds |
|
||
|---|---|
|
||
| `world.c` | `<limits.h>`, `<math.h>`, `akstdlib.h`, `akgl/character.h`, `akgl/error.h`, `akgl/game.h`, `akgl/heap.h`, `akgl/physics.h`, `akgl/registry.h`, `akgl/sprite.h`, `akgl/tilemap.h` |
|
||
| `jrpg.c` | `<string.h>`, `akstdlib.h`, `SDL3_ttf/SDL_ttf.h`, `akgl/controller.h`, `akgl/error.h`, `akgl/game.h`, `akgl/iterator.h`, `akgl/physics.h`, `akgl/registry.h`, `akgl/renderer.h`, `akgl/text.h`, `akgl/tilemap.h` |
|
||
| `textbox.c` | listed in step 10 |
|
||
|
||
`akgl/collision.h` and `akgl/actor.h` come in through `jrpg.h`, so nothing needs to include
|
||
them twice. libakgl's headers are self-contained: including the one that declares what you
|
||
are calling is always enough.
|
||
|
||
The header itself needs:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
#include <stdbool.h>
|
||
#include <SDL3/SDL.h>
|
||
#include <akerror.h>
|
||
#include <akgl/actor.h>
|
||
#include <akgl/collision.h>
|
||
#include <akgl/types.h>
|
||
```
|
||
|
||
Then the collision world and the twelve functions the four `.c` files share:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
extern akgl_CollisionWorld jrpg_collision;
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_world_load(void);
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_world_populate(void);
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_camera_follow(void);
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_actor_logic_walk(akgl_Actor *obj, float32_t dt);
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_follower_render(akgl_Actor *obj);
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_cmhf_talk(akgl_Actor *obj, SDL_Event *event);
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_cmhf_ignore(akgl_Actor *obj, SDL_Event *event);
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_textbox_open(char *text);
|
||
/** @brief Dismiss the text box. */
|
||
void jrpg_textbox_close(void);
|
||
/** @brief True while a line of dialogue is on screen. */
|
||
bool jrpg_textbox_showing(void);
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
akerr_ErrorContext AKERR_NOIGNORE *jrpg_textbox_draw(void);
|
||
```
|
||
|
||
Which file defines what, and which step writes it:
|
||
|
||
| Function | Defined in | Step |
|
||
|---|---|---|
|
||
| `jrpg_world_load` | `world.c` | 3, 5, 7 |
|
||
| `jrpg_world_populate` | `world.c` | 6, 7, 8, 9 |
|
||
| `jrpg_actor_logic_walk` | `world.c` | 12 |
|
||
| `jrpg_follower_render` | `world.c` | 9 |
|
||
| `jrpg_camera_follow` | `world.c` | 13 |
|
||
| `jrpg_cmhf_talk`, `jrpg_cmhf_ignore` | `world.c` | 11 |
|
||
| `jrpg_textbox_open`, `_close`, `_showing`, `_draw` | `textbox.c` | 10 |
|
||
|
||
Two more constants the chapters below use:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
#define JRPG_FONT_NAME "jrpg"
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
#define JRPG_FONT_SIZE 12
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
#define JRPG_TEXTBOX_MAX_TEXT 256
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
#define JRPG_FOLLOWER_NAME "companion"
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Start up and load the town
|
||
|
||
The sequence is chapter 20's. Name the game, initialize, set the properties, then the
|
||
renderer, then the physics backend:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
PASS(errctx, akgl_game_init());
|
||
akgl_game.lowfpsfunc = &lowfps_quiet;
|
||
|
||
PASS(errctx, akgl_set_property("game.screenwidth", JRPG_SCREEN_WIDTH));
|
||
PASS(errctx, akgl_set_property("game.screenheight", JRPG_SCREEN_HEIGHT));
|
||
PASS(errctx, akgl_render_2d_init(akgl_renderer));
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
PASS(errctx, akgl_physics_init_arcade(akgl_physics));
|
||
```
|
||
|
||
The arcade backend with no gravity is exactly what a top-down game wants, and zero gravity is
|
||
what the property defaults already give.
|
||
|
||
Then one new call — load the font:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
PASS(errctx, akgl_text_loadfont(JRPG_FONT_NAME, JRPG_FONT_FILE, JRPG_FONT_SIZE));
|
||
```
|
||
|
||
A font is registered under a name and a size. The size is baked into the handle, so 12pt and
|
||
24pt of the same file are two registrations. `JRPG_FONT_NAME` is the key the text box looks
|
||
it up under in step 10.
|
||
|
||
`lowfps_quiet` is the do-nothing frame-rate hook chapter 20 explains — a `static void
|
||
lowfps_quiet(void) { }` in `jrpg.c`.
|
||
|
||
Then the world, in two functions — everything the assets need, then everything the actors
|
||
need:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
PASS(errctx, jrpg_world_load());
|
||
PASS(errctx, jrpg_world_populate());
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Describe four-way art
|
||
|
||
Twenty-four sprites: three characters × two motions × four facings. Rather than a list of
|
||
twenty-four file names, make the names a product of three tables — the naming convention *is*
|
||
the data:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
static char *CAST[] = {
|
||
"player", /* jrpg_player jrpg_player_* */
|
||
"elder", /* jrpg_elder jrpg_elder_* */
|
||
"shopkeeper" /* jrpg_shopkeeper jrpg_shopkeeper_* */
|
||
};
|
||
|
||
static char *MOTIONS[] = { "idle", "walk" };
|
||
static char *FACINGS[] = { "up", "down", "left", "right" };
|
||
```
|
||
|
||
Then one nested loop loads them all:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
for ( c = 0; c < CAST_COUNT; c++ ) {
|
||
for ( m = 0; m < MOTION_COUNT; m++ ) {
|
||
for ( f = 0; f < FACING_COUNT; f++ ) {
|
||
PASS(errctx, sprite_load(CAST[c], MOTIONS[m], FACINGS[f]));
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
The helper builds the path from the three components:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
static akerr_ErrorContext *sprite_load(char *cast, char *motion, char *facing)
|
||
{
|
||
PREPARE_ERROR(errctx);
|
||
char path[PATH_MAX];
|
||
int written = 0;
|
||
|
||
FAIL_ZERO_RETURN(errctx, cast, AKERR_NULLPOINTER, "cast");
|
||
FAIL_ZERO_RETURN(errctx, motion, AKERR_NULLPOINTER, "motion");
|
||
FAIL_ZERO_RETURN(errctx, facing, AKERR_NULLPOINTER, "facing");
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
aksl_snprintf(
|
||
&written,
|
||
path,
|
||
sizeof(path),
|
||
"%s/sprite_jrpg_%s_%s_%s.json",
|
||
JRPG_ASSET_DIR,
|
||
cast,
|
||
motion,
|
||
facing
|
||
));
|
||
PASS(errctx, akgl_sprite_load_json(path));
|
||
SUCCEED_RETURN(errctx);
|
||
}
|
||
```
|
||
|
||
`character_load` is the same shape against a different file name and a different loader:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
static akerr_ErrorContext *character_load(char *cast)
|
||
{
|
||
PREPARE_ERROR(errctx);
|
||
char path[PATH_MAX];
|
||
int written = 0;
|
||
|
||
FAIL_ZERO_RETURN(errctx, cast, AKERR_NULLPOINTER, "cast");
|
||
PASS(errctx,
|
||
aksl_snprintf(
|
||
&written,
|
||
path,
|
||
sizeof(path),
|
||
"%s/character_jrpg_%s.json",
|
||
JRPG_ASSET_DIR,
|
||
cast
|
||
));
|
||
PASS(errctx, akgl_character_load_json(path));
|
||
SUCCEED_RETURN(errctx);
|
||
}
|
||
```
|
||
|
||
`PATH_MAX` comes from `<limits.h>`. The counts come from the tables:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
#define CAST_COUNT (sizeof(CAST) / sizeof(CAST[0]))
|
||
#define MOTION_COUNT (sizeof(MOTIONS) / sizeof(MOTIONS[0]))
|
||
#define FACING_COUNT (sizeof(FACINGS) / sizeof(FACINGS[0]))
|
||
```
|
||
|
||
A missing combination now shows up as a load failure naming the exact file, rather than as
|
||
art that silently never appears.
|
||
|
||
Each sprite file is the format chapter 20 covers. A walk cycle:
|
||
|
||
```json kind=sprite setup=jrpg
|
||
{
|
||
"spritesheet": {
|
||
"filename": "player.png",
|
||
"frame_width": 32,
|
||
"frame_height": 32
|
||
},
|
||
"name": "jrpg_player_walk_down",
|
||
"width": 32,
|
||
"height": 32,
|
||
"speed": 150,
|
||
"loop": true,
|
||
"loopReverse": false,
|
||
"frames": [
|
||
0,
|
||
1,
|
||
0,
|
||
2
|
||
]
|
||
}
|
||
```
|
||
|
||
All eight of a character's sprites name the same PNG. The spritesheet registry is keyed on
|
||
the file path, so the image is decoded and uploaded once.
|
||
|
||
---
|
||
|
||
## 4. Bind eight states per character
|
||
|
||
A four-way character needs eight mappings — an idle and a walking sprite for each facing:
|
||
|
||
```json kind=character setup=jrpg
|
||
{
|
||
"name": "jrpg_player",
|
||
"speedtime": 150,
|
||
"speed_x": 60.0,
|
||
"speed_y": 60.0,
|
||
"acceleration_x": 400.0,
|
||
"acceleration_y": 400.0,
|
||
"sprite_mappings": [
|
||
{
|
||
"state": [
|
||
"AKGL_ACTOR_STATE_ALIVE",
|
||
"AKGL_ACTOR_STATE_FACE_DOWN"
|
||
],
|
||
"sprite": "jrpg_player_idle_down"
|
||
},
|
||
{
|
||
"state": [
|
||
"AKGL_ACTOR_STATE_ALIVE",
|
||
"AKGL_ACTOR_STATE_FACE_DOWN",
|
||
"AKGL_ACTOR_STATE_MOVING_DOWN"
|
||
],
|
||
"sprite": "jrpg_player_walk_down"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Repeat that pair for `UP`, `LEFT` and `RIGHT`. `speed_y` is a real speed here — 60 px/s, the
|
||
same as `speed_x`, so diagonal movement is not faster than straight movement.
|
||
|
||
Load the characters after the sprites:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
for ( c = 0; c < CAST_COUNT; c++ ) {
|
||
PASS(errctx, character_load(CAST[c]));
|
||
}
|
||
```
|
||
|
||
The NPC characters are the same eight mappings against their own art. They never walk, but
|
||
giving them the walking sprites costs nothing and means an NPC that starts moving later
|
||
already works.
|
||
|
||
---
|
||
|
||
## 5. Draw the town
|
||
|
||
Draw it in Tiled and save it as `.tmj`, set up as chapter 20 describes — orthogonal, CSV
|
||
layer format, 16×16 tiles — at 30 × 20 tiles, with three layers:
|
||
|
||
| Layer | Type | What it is |
|
||
|---|---|---|
|
||
| `ground` | Tile layer | Grass, paths, water. Drawn, never solid |
|
||
| `decoration` | Tile layer | Buildings, trees, lamp posts |
|
||
| `actors` | Object layer | The player and the townsfolk |
|
||
|
||
Mark `decoration` with a `collidable` boolean custom property, exactly as in chapter 20:
|
||
|
||
```json excerpt=docs/tutorials/assets/jrpg/town.tmj
|
||
"properties": [
|
||
{
|
||
"name": "collidable",
|
||
"type": "bool",
|
||
"value": true
|
||
}
|
||
]
|
||
```
|
||
|
||
### Zero gravity
|
||
|
||
On the map itself, three properties:
|
||
|
||
| Property | Type | Value |
|
||
|---|---|---|
|
||
| `physics.model` | string | `arcade` |
|
||
| `physics.gravity.x` | float | `0.0` |
|
||
| `physics.gravity.y` | float | `0.0` |
|
||
|
||
Set both axes explicitly rather than leaving them out. A map that states its physics is a map
|
||
you can read.
|
||
|
||
### The actors
|
||
|
||
Place a rectangle for the player and one for each townsperson. Each needs a **Name**, a
|
||
**Type** of `actor`, and two custom properties:
|
||
|
||
```json excerpt=docs/tutorials/assets/jrpg/town.tmj
|
||
"height": 32,
|
||
"id": 1,
|
||
"name": "player",
|
||
"properties": [
|
||
{
|
||
"name": "character",
|
||
"type": "string",
|
||
"value": "jrpg_player"
|
||
},
|
||
{
|
||
"name": "state",
|
||
"type": "int",
|
||
"value": 17
|
||
}
|
||
],
|
||
"rotation": 0,
|
||
"type": "actor",
|
||
"visible": true,
|
||
"width": 32,
|
||
"x": 224,
|
||
"y": 256
|
||
```
|
||
|
||
`17` is `AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN` — 16 plus 1. Facing down is the
|
||
convention for a character standing still and looking at the player.
|
||
|
||
### Loading it
|
||
|
||
Sprites, then characters, then the map:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
aksl_snprintf(&written, path, sizeof(path), "%s/town.tmj", JRPG_ASSET_DIR));
|
||
PASS(errctx, akgl_tilemap_load(path, akgl_gamemap));
|
||
```
|
||
|
||
Then switch to the physics the map declared:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
if ( akgl_gamemap->use_own_physics ) {
|
||
akgl_physics = &akgl_gamemap->physics;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Keep the NPCs visible
|
||
|
||
Add this loop right after the map loads:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||
if ( akgl_heap_actors[i].refcount == 0 ) {
|
||
continue;
|
||
}
|
||
akgl_heap_actors[i].movement_controls_face = false;
|
||
}
|
||
```
|
||
|
||
`akgl_actor_initialize` sets `movement_controls_face`, and the default facing logic it
|
||
installs clears every facing bit and then sets the one matching a *movement* bit. An NPC has
|
||
no movement bits, so on the first frame its state falls from `ALIVE|FACE_DOWN` (17) to
|
||
`ALIVE` (16) — a combination no character maps a sprite to. An actor with no sprite for its
|
||
state is skipped rather than reported, so every NPC in the town disappears on frame one,
|
||
silently, and so does the player as soon as they stop walking.
|
||
|
||
Clearing the field leaves the facing bits wherever they were last set, which is what a
|
||
top-down game wants. `TODO.md` tracks making the default behave; until then this loop belongs
|
||
in every game with a standing NPC in it.
|
||
|
||
The loop covers the player as well as the NPCs, which is what you want — a player who stops
|
||
walking would disappear for the same reason. Chapter 20 sets the same field on one actor
|
||
because that game only has one that stands still.
|
||
|
||
`akgl_heap_actors` is the actor pool, and walking it directly is the supported way to touch
|
||
every live actor. A slot with `refcount == 0` is free.
|
||
|
||
Run now. The town draws, the player and the townsfolk are visible, and nothing moves.
|
||
|
||
---
|
||
|
||
## 7. Wall the town in
|
||
|
||
Buildings are solid tiles, and they are already handled — the `collidable` property in step 5
|
||
did it. Turn collision on:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
PASS(errctx, akgl_collision_world_init(&jrpg_collision, NULL, 16.0f, 16.0f));
|
||
PASS(errctx, akgl_collision_bind_tilemap(&jrpg_collision, akgl_gamemap));
|
||
PASS(errctx, wall_off_the_edges());
|
||
akgl_physics->collision = &jrpg_collision;
|
||
```
|
||
|
||
Give the player a footprint rather than a full-frame box. A character stands on the bottom
|
||
third of their sprite, and that is the part that has to fit through a doorway — testing the
|
||
whole frame would make every corridor two tiles wide:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
body = (SDL_FRect){ FEET_X, FEET_TOP, FEET_W, FEET_H };
|
||
PASS(errctx, akgl_collision_shape_box(&player->shape, &body, 0.0f));
|
||
player->shape_override = true;
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
#define FEET_X 6.0f
|
||
#define FEET_W 20.0f
|
||
#define FEET_TOP 20.0f
|
||
#define FEET_H 10.0f
|
||
```
|
||
|
||
**Only the player gets a shape.** The NPCs stand still and are spoken to, not walked into;
|
||
shapes on them would make the town a pinball table. The follower is a child snapped to its
|
||
parent every step, so a shape on it would fight the snap rather than stop anything.
|
||
|
||
### The edge of the world
|
||
|
||
The map's edge is not on any layer, and nothing else stops an actor walking off it. Four
|
||
static collision proxies do the job — one per side. A long thin box costs one pool slot
|
||
whatever its length, which is what proxies are for.
|
||
|
||
Keep the proxies and the shapes they were built from in file-scope arrays, because both have
|
||
to outlive the function that made them:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
static akgl_CollisionProxy *jrpg_edges[4];
|
||
static akgl_CollisionShape jrpg_edge_shapes[4];
|
||
```
|
||
|
||
Work the four rectangles out from the map's own size:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
ow = (float32_t)(akgl_gamemap->width * akgl_gamemap->tilewidth) + (2.0f * EDGE_OVERHANG);
|
||
oh = (float32_t)(akgl_gamemap->height * akgl_gamemap->tileheight) + (2.0f * EDGE_OVERHANG);
|
||
tw = (float32_t)akgl_gamemap->tilewidth + EDGE_OVERHANG;
|
||
th = (float32_t)akgl_gamemap->tileheight + EDGE_OVERHANG;
|
||
|
||
/* x y w h */
|
||
sides[0] = (SDL_FRect){ -EDGE_OVERHANG, -EDGE_OVERHANG, tw, oh }; /* west */
|
||
sides[1] = (SDL_FRect){ (ow - tw - EDGE_OVERHANG), -EDGE_OVERHANG, tw, oh }; /* east */
|
||
sides[2] = (SDL_FRect){ -EDGE_OVERHANG, -EDGE_OVERHANG, ow, th }; /* north */
|
||
sides[3] = (SDL_FRect){ -EDGE_OVERHANG, (oh - th - EDGE_OVERHANG), ow, th }; /* south */
|
||
```
|
||
|
||
Then register each one, in a loop over the four:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
PASS(errctx, akgl_heap_next_collision_proxy(&jrpg_edges[i]));
|
||
PASS(errctx, akgl_collision_shape_box(&jrpg_edge_shapes[i], &sides[i], 0.0f));
|
||
jrpg_edge_shapes[i].layermask = AKGL_COLLISION_LAYER_STATIC;
|
||
jrpg_edge_shapes[i].collidemask = AKGL_COLLISION_LAYER_NONE;
|
||
jrpg_edge_shapes[i].flags |= AKGL_COLLISION_FLAG_STATIC;
|
||
PASS(errctx, akgl_collision_proxy_initialize(jrpg_edges[i], NULL, &jrpg_edge_shapes[i],
|
||
0.0f, 0.0f, 0.0f));
|
||
PASS(errctx, jrpg_collision.partitioner.insert(&jrpg_collision.partitioner, jrpg_edges[i]));
|
||
```
|
||
|
||
Three details in those lines:
|
||
|
||
- **Set `layermask` by hand.** `akgl_collision_shape_box` defaults a shape to
|
||
`AKGL_COLLISION_LAYER_ACTOR`, which is right for an actor and wrong for scenery. An actor
|
||
responds to `AKGL_COLLISION_LAYER_STATIC` and nothing else, so a wall left on the default
|
||
layer is a wall everything walks through.
|
||
- **Clear `collidemask`.** The wall is not asking to be pushed out of anything.
|
||
- **Keep the acquire and the initialize adjacent, with nothing between them.**
|
||
`akgl_heap_next_collision_proxy` finds a free slot and does not claim it; the initialize
|
||
takes the reference. Between the two lines a second acquire returns the same pointer.
|
||
[Chapter 5](05-the-heap.md) explains why the pools work that way.
|
||
|
||
The walls cover the map's outer ring of cells *and* extend a tile beyond it:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
#define EDGE_OVERHANG 16.0f
|
||
```
|
||
|
||
The overhang is what stops an actor arriving fast enough from being resolved out the far side
|
||
of a wall exactly one tile thick.
|
||
|
||
---
|
||
|
||
## 8. Walk in four directions
|
||
|
||
The arrow keys and the D-pad, wired to the library's own handlers, in one call:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
PASS(errctx, akgl_controller_default(0, "player", 0, 0));
|
||
```
|
||
|
||
The arguments are the control map id, the actor's registry name, the keyboard id and the
|
||
gamepad id. `0` for both devices means "the first of each", which is what a one-player game
|
||
wants.
|
||
|
||
That is all four-way movement needs. `akgl_controller_default` binds all eight
|
||
`akgl_actor_cmhf_*` handlers — up, down, left, right, on and off — and those set the movement
|
||
and facing bits your character mappings key on. A sidescroller replaces the `_off` handlers
|
||
to get friction; a top-down game does not, because stopping dead when you release a direction
|
||
is exactly right here.
|
||
|
||
Run now. The arrow keys walk the player in four directions with the right animation for each,
|
||
and buildings and the map's edge stop them.
|
||
|
||
The player itself comes out of the registry, under the name its Tiled object carried:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
player = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, "player", NULL);
|
||
FAIL_ZERO_RETURN(
|
||
errctx,
|
||
player,
|
||
AKGL_ERR_REGISTRY,
|
||
"town.tmj spawned no actor named \"player\""
|
||
);
|
||
player->movementlogicfunc = &jrpg_actor_logic_walk;
|
||
```
|
||
|
||
That lookup and everything after it lives in `jrpg_world_populate`, which runs after
|
||
`jrpg_world_load` has loaded the map. Step 12 is what goes in the hook.
|
||
|
||
---
|
||
|
||
## 9. Follow the player
|
||
|
||
A party member is an ordinary actor made a *child* of the player. Nothing in the map creates
|
||
it:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
PASS(errctx, akgl_heap_next_actor(&follower));
|
||
PASS(errctx, akgl_actor_initialize(follower, JRPG_FOLLOWER_NAME));
|
||
PASS(errctx, akgl_actor_set_character(follower, "jrpg_elder"));
|
||
follower->state = (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN);
|
||
follower->movement_controls_face = false;
|
||
follower->visible = true;
|
||
follower->layer = player->layer;
|
||
follower->renderfunc = &jrpg_follower_render;
|
||
PASS(errctx, player->addchild(player, follower));
|
||
```
|
||
|
||
`akgl_heap_next_actor` takes a free slot from the pool; `akgl_actor_initialize` claims it and
|
||
gives it a name. Write those two adjacent, with nothing between them — the slot is not yours
|
||
until the initialize.
|
||
|
||
`addchild` is one of the actor's seven behaviour hooks, not a library function, which is why
|
||
it is called through the actor and takes it as the first argument:
|
||
`player->addchild(player, follower)` reads as "player, add this child". Every hook works that
|
||
way, so a game can replace one on a single actor — chapter 20 replaces `updatefunc` and
|
||
`movementlogicfunc` the same way.
|
||
|
||
The follower borrows an existing character. An actor is an instance; a character is a
|
||
template. That is what splitting them is for.
|
||
|
||
### Where a child stands
|
||
|
||
A child's velocity fields are read as a **fixed offset from its parent**, not as a velocity.
|
||
The physics step snaps a child to `parent->x + vx` and never simulates it:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
follower->vx = FOLLOWER_OFFSET_X;
|
||
follower->vy = FOLLOWER_OFFSET_Y;
|
||
```
|
||
|
||
Set them **after** `addchild`. That call is what makes them mean an offset.
|
||
|
||
### Drawing a child
|
||
|
||
Give the follower a `renderfunc` that detaches the parent for the duration of the draw:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
parent = obj->parent;
|
||
obj->parent = NULL;
|
||
ATTEMPT {
|
||
CATCH(errctx, akgl_actor_render(obj));
|
||
} CLEANUP {
|
||
obj->parent = parent;
|
||
} PROCESS(errctx) {
|
||
} FINISH(errctx, true);
|
||
```
|
||
|
||
The physics step writes a child's `x` and `y` as an absolute world position, and
|
||
`akgl_actor_render` adds the parent's position to it a second time. Detaching takes the
|
||
branch that does not add it, and `CLEANUP` puts the parent back on every path including the
|
||
failing one — an actor left with a `NULL` parent would be simulated as a free agent on the
|
||
next step. This is `TODO.md`, "Known and still open"; when the two agree on one reading, this
|
||
hook becomes `akgl_actor_render` again.
|
||
|
||
Run now. A second character walks a fixed distance behind and below the player.
|
||
|
||
---
|
||
|
||
## 10. Put text on screen
|
||
|
||
Text in libakgl is immediate mode: each call rasterizes the string, uploads it, blits it and
|
||
destroys the texture. That is the wrong shape for a page of prose redrawn sixty times a
|
||
second and exactly right for one line of dialogue.
|
||
|
||
`textbox.c` needs these includes:
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
#include <stdbool.h>
|
||
|
||
#include <SDL3/SDL.h>
|
||
#include <SDL3_ttf/SDL_ttf.h>
|
||
#include <akerror.h>
|
||
#include <akstdlib.h>
|
||
|
||
#include <akgl/draw.h>
|
||
#include <akgl/error.h>
|
||
#include <akgl/game.h>
|
||
#include <akgl/registry.h>
|
||
#include <akgl/text.h>
|
||
```
|
||
|
||
The colours are `SDL_Color`, which is four bytes of RGBA:
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
/*
|
||
* Colour R G B A
|
||
*/
|
||
static const SDL_Color TEXTBOX_FILL = { 24, 20, 37, 235 };
|
||
static const SDL_Color TEXTBOX_EDGE = { 240, 236, 214, 255 };
|
||
static const SDL_Color TEXTBOX_INK = { 240, 236, 214, 255 };
|
||
```
|
||
|
||
An alpha of 235 on the fill lets the world show faintly through the panel.
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
#define TEXTBOX_MARGIN 8.0f
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
#define TEXTBOX_HEIGHT 56.0f
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
#define TEXTBOX_PADDING 8.0f
|
||
```
|
||
|
||
Keep the state in two file-scope variables:
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
static char textbox_text[JRPG_TEXTBOX_MAX_TEXT];
|
||
static bool textbox_visible = false;
|
||
```
|
||
|
||
`jrpg_textbox_showing` returns `textbox_visible` and `jrpg_textbox_close` sets it to `false`.
|
||
Both are two-line functions returning `bool` and `void` — they cannot fail, so they do not
|
||
return an error context.
|
||
|
||
Opening the box copies the line in:
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
aksl_strncpy(
|
||
textbox_text,
|
||
sizeof(textbox_text),
|
||
text,
|
||
sizeof(textbox_text) - 1
|
||
));
|
||
textbox_visible = true;
|
||
```
|
||
|
||
`aksl_strncpy` rather than `strncpy`: this is a fixed-width field, and `strncpy` at exactly
|
||
the field width leaves it unterminated. Bounding at one less truncates an over-long line
|
||
rather than refusing it.
|
||
|
||
### Drawing the panel
|
||
|
||
Fetch the font from the registry, size a panel from the camera, and draw three things —
|
||
a filled rectangle, an outline, and the text:
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
TTF_Font *font = NULL;
|
||
SDL_FRect panel;
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
font = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, JRPG_FONT_NAME, NULL);
|
||
FAIL_ZERO_RETURN(
|
||
errctx,
|
||
font,
|
||
AKGL_ERR_REGISTRY,
|
||
"No font registered as \"%s\"",
|
||
JRPG_FONT_NAME
|
||
);
|
||
```
|
||
|
||
A font is an SDL_ttf `TTF_Font *`. libakgl does not wrap it in a type of its own — the
|
||
registry hands you SDL_ttf's handle.
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
panel.x = TEXTBOX_MARGIN;
|
||
panel.w = akgl_camera->w - (2.0f * TEXTBOX_MARGIN);
|
||
panel.h = TEXTBOX_HEIGHT;
|
||
panel.y = akgl_camera->h - TEXTBOX_MARGIN - panel.h;
|
||
|
||
PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &panel, TEXTBOX_FILL));
|
||
PASS(errctx, akgl_draw_rect(akgl_renderer, &panel, TEXTBOX_EDGE));
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
akgl_text_rendertextat(
|
||
font,
|
||
textbox_text,
|
||
TEXTBOX_INK,
|
||
(int)(panel.w - (2.0f * TEXTBOX_PADDING)),
|
||
(int)(panel.x + TEXTBOX_PADDING),
|
||
(int)(panel.y + TEXTBOX_PADDING)
|
||
));
|
||
```
|
||
|
||
The fourth argument is a wrap width in pixels; the last two are the top-left corner. These
|
||
are **screen** coordinates, not map coordinates, which is why the panel is sized from the
|
||
camera's `w` and `h`.
|
||
|
||
Fetch the font every frame rather than caching the pointer. Fonts live in a registry under a
|
||
name with no reference counting, so whoever calls `akgl_text_unloadfont` invalidates every
|
||
pointer anybody else is holding.
|
||
|
||
Guard against the empty string:
|
||
|
||
```c excerpt=examples/jrpg/textbox.c
|
||
if ( textbox_text[0] == '\0' ) {
|
||
SUCCEED_RETURN(errctx);
|
||
}
|
||
```
|
||
|
||
`akgl_text_rendertextat` refuses a zero-width string while `akgl_text_measure` accepts one.
|
||
The disagreement is recorded in `TODO.md` under "Known and still open"; the check above is
|
||
the guard until it is settled.
|
||
|
||
### Drawing it over the world
|
||
|
||
The panel is drawn *after* `akgl_game_update`, so it lands on top of everything:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
PASS(errctx, akgl_game_update(&opflags));
|
||
PASS(errctx, jrpg_textbox_draw());
|
||
```
|
||
|
||
`akgl_game_update` does not clear or present, so anything you draw between it and
|
||
`frame_end` is on top of the world.
|
||
|
||
Nothing opens the box yet, so there is nothing to see until step 11.
|
||
|
||
---
|
||
|
||
## 11. Talk to somebody
|
||
|
||
Keep the dialogue in a table beside the actor names the map gave them. The row type is your
|
||
own:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
typedef struct {
|
||
char *actor; /**< Registry key, which is the `name` of the map object that spawned them. */
|
||
char *line; /**< What they say. */
|
||
} jrpg_Townsfolk;
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
static jrpg_Townsfolk TOWNSFOLK[] = {
|
||
{ "elder", "ELDER: The old road north is closed. Nothing to be done about it,\nand nothing beyond it worth the walk." },
|
||
{ "shopkeeper", "SHOPKEEPER: Nothing in stock but tiles and good intentions.\nCome back when I have inventory." }
|
||
};
|
||
```
|
||
|
||
`\n` in a line is a real line break — `akgl_text_rendertextat` honours it.
|
||
|
||
The handler does three things: close the box if it is open, otherwise find a townsperson in
|
||
range, otherwise do nothing.
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
if ( jrpg_textbox_showing() ) {
|
||
jrpg_textbox_close();
|
||
SUCCEED_RETURN(errctx);
|
||
}
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
for ( i = 0; i < TOWNSFOLK_COUNT; i++ ) {
|
||
npc = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, TOWNSFOLK[i].actor, NULL);
|
||
if ( npc == NULL ) {
|
||
continue;
|
||
}
|
||
dx = npc->x - obj->x;
|
||
dy = npc->y - obj->y;
|
||
if ( sqrtf((dx * dx) + (dy * dy)) > TALK_RANGE ) {
|
||
continue;
|
||
}
|
||
PASS(errctx, jrpg_textbox_open(TOWNSFOLK[i].line));
|
||
SUCCEED_RETURN(errctx);
|
||
}
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
#define TALK_RANGE 64.0f
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
#define TOWNSFOLK_COUNT (sizeof(TOWNSFOLK) / sizeof(TOWNSFOLK[0]))
|
||
```
|
||
|
||
Look the NPC up by name every time rather than caching the pointer. An actor that has been
|
||
released is out of the registry, so the lookup returns `NULL` and the loop skips it — a
|
||
cached pointer would be stale.
|
||
|
||
### Binding the key
|
||
|
||
`akgl_controller_default` used control map 0; push one more binding onto the same map:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
PASS(errctx, aksl_memset((void *)&talk, 0x00, sizeof(talk)));
|
||
talk.event_on = SDL_EVENT_KEY_DOWN;
|
||
talk.event_off = SDL_EVENT_KEY_UP;
|
||
talk.key = SDLK_SPACE;
|
||
talk.button = (uint8_t)SDL_GAMEPAD_BUTTON_INVALID;
|
||
talk.handler_on = &jrpg_cmhf_talk;
|
||
talk.handler_off = &jrpg_cmhf_ignore;
|
||
PASS(errctx, akgl_controller_pushmap(0, &talk));
|
||
```
|
||
|
||
Three things to get right, all of them cheap:
|
||
|
||
- **Zero the struct first.** A binding is a struct copied into the map, so a stack local is
|
||
fine — but an uninitialized field is a match against garbage.
|
||
- **`handler_off` must be non-`NULL`.** `akgl_controller_handle_event` calls it without
|
||
checking. `jrpg_cmhf_ignore` is a handler that does nothing, which is what you want on the
|
||
release of a key whose press is the whole action.
|
||
- **Set `button` to `SDL_GAMEPAD_BUTTON_INVALID`.** The keyboard and gamepad arms of the
|
||
match are evaluated together, and 0 is a real button. Recorded in `TODO.md`; until it is
|
||
settled, name a button no gamepad reports.
|
||
|
||
Run now. Standing near the elder and pressing Space opens their line; pressing it again
|
||
dismisses the box. The player can still walk while it is open — step 12 fixes that.
|
||
|
||
---
|
||
|
||
## 12. Freeze the world
|
||
|
||
While a conversation is open, nothing should move. Put the check at the top of the player's
|
||
`movementlogicfunc`:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
if ( jrpg_textbox_showing() ) {
|
||
obj->tx = 0.0f;
|
||
obj->ty = 0.0f;
|
||
AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_MOVING_ALL);
|
||
FAIL_RETURN(
|
||
errctx,
|
||
AKGL_ERR_LOGICINTERRUPT,
|
||
"%s does not move while a conversation is open",
|
||
(char *)obj->name
|
||
);
|
||
}
|
||
```
|
||
|
||
`AKGL_ERR_LOGICINTERRUPT` is the one status in libakgl that is **not a failure**. Raised from
|
||
a `movementlogicfunc` it means "skip the rest of this tick for this actor": the physics step
|
||
catches it, so gravity, drag, the velocity recompute, the move and collision are all skipped,
|
||
and the frame carries on to the next actor.
|
||
|
||
Two things happen before the raise, and both matter:
|
||
|
||
- **Zero the thrust.** By the time this hook runs, this step's thrust has already been
|
||
integrated. Leaving it would let it accumulate while frozen and lurch on the first step
|
||
after the box closes.
|
||
- **Clear the movement bits.** Otherwise the walk animation marches on the spot. The cost is
|
||
that a direction held across the close has to be pressed again.
|
||
|
||
**Only a `movementlogicfunc` may raise it.** Raised from a backend's `gravity` or `move` it
|
||
aborts the whole physics step, leaving every remaining actor unsimulated.
|
||
|
||
If the box is not showing, the default logic runs as usual:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
PASS(errctx, akgl_actor_logic_movement(obj, dt));
|
||
```
|
||
|
||
Run now. Opening a conversation stops the player where they stand, and the arrow keys do
|
||
nothing until the box is dismissed.
|
||
|
||
---
|
||
|
||
## 13. Follow with the camera, and tear down
|
||
|
||
The camera centres on the player and clamps to the map on both axes — the sidescroller only
|
||
had to do one:
|
||
|
||
```c excerpt=examples/jrpg/world.c
|
||
akgl_camera->x = (player->x + 16.0f) - (akgl_camera->w / 2.0f);
|
||
akgl_camera->y = (player->y + 16.0f) - (akgl_camera->h / 2.0f);
|
||
|
||
if ( akgl_camera->x < 0.0f ) {
|
||
akgl_camera->x = 0.0f;
|
||
}
|
||
if ( akgl_camera->y < 0.0f ) {
|
||
akgl_camera->y = 0.0f;
|
||
}
|
||
if ( akgl_camera->x > (mapw - akgl_camera->w) ) {
|
||
akgl_camera->x = mapw - akgl_camera->w;
|
||
}
|
||
if ( akgl_camera->y > (maph - akgl_camera->h) ) {
|
||
akgl_camera->y = maph - akgl_camera->h;
|
||
}
|
||
```
|
||
|
||
The `+ 16.0f` centres on the middle of a 32-pixel sprite rather than its top-left corner.
|
||
|
||
### The frame
|
||
|
||
The loop is chapter 20's, with two additions. Drain the events first:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
static akerr_ErrorContext *frame(long frameno)
|
||
{
|
||
PREPARE_ERROR(errctx);
|
||
SDL_Event event;
|
||
akgl_Iterator opflags = {
|
||
.flags = AKGL_ITERATOR_OP_UPDATE,
|
||
.layerid = 0
|
||
};
|
||
|
||
while ( SDL_PollEvent(&event) ) {
|
||
if ( event.type == SDL_EVENT_QUIT ) {
|
||
running = false;
|
||
}
|
||
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
|
||
}
|
||
```
|
||
|
||
`frameno` is the outer loop's own counter, incremented once per call. **libakgl does not keep
|
||
a frame number** — `akgl_game` has an fps figure but no counter, so a game that wants one
|
||
keeps it.
|
||
|
||
Then the camera, the world, and the panel on top of it:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
PASS(errctx, jrpg_camera_follow());
|
||
|
||
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
|
||
```
|
||
|
||
This game passes the `opflags` declared at the top of `frame()` to `akgl_game_update` rather
|
||
than the `NULL` chapter 20 passes. `AKGL_ITERATOR_OP_UPDATE` asks for the update pass;
|
||
[Chapter 7](07-the-game-and-the-frame.md) covers the other flags.
|
||
|
||
### Teardown
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
IGNORE(akgl_text_unloadallfonts());
|
||
if ( akgl_window != NULL ) {
|
||
SDL_DestroyWindow(akgl_window);
|
||
akgl_window = NULL;
|
||
}
|
||
SDL_Quit();
|
||
```
|
||
|
||
`akgl_text_unloadallfonts` must run **before** `SDL_Quit`. Fonts live in an SDL property
|
||
registry and `SDL_Quit` destroys it, taking the last reference to every font with no way left
|
||
to close them.
|
||
|
||
`akgl_tilemap_release` is not called. It double-frees tileset textures, which is `TODO.md`
|
||
"Known and still open" item 2; `SDL_Quit` reclaims them correctly. A game that loads a second
|
||
level has to unwind properly, and that is what the fix will make possible.
|
||
|
||
The pools are static storage and the process is exiting, so there is nothing else to free.
|
||
|
||
---
|
||
|
||
## Build it and run it
|
||
|
||
```sh norun
|
||
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||
cmake --build build --parallel
|
||
./build/examples/jrpg/jrpg
|
||
```
|
||
|
||
| Control | Keyboard | Gamepad |
|
||
|---|---|---|
|
||
| Walk | ← → ↑ ↓ | D-pad |
|
||
| Talk / dismiss | Space | — |
|
||
| Quit | Close the window | — |
|
||
|
||
Stand next to the elder or the shopkeeper and press Space.
|
||
|
||
To run it without a display:
|
||
|
||
```sh norun
|
||
SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIODRIVER=dummy \
|
||
./build/examples/jrpg/jrpg --frames 320 --demo
|
||
```
|
||
|
||
`--demo` drives a scripted walk: right, then north past the buildings, then Space to open the
|
||
elder's line, an arrow key that the freeze eats, Space to dismiss, and a walk away. It prints
|
||
where the player finished:
|
||
|
||
```text
|
||
jrpg: 320 frames, player at (280, 130)
|
||
```
|
||
|
||
### Writing the scripted run
|
||
|
||
Neither the demo nor that summary line is part of the game — both exist so the program can be
|
||
checked without a person at the keyboard, and both are worth having for exactly that reason.
|
||
|
||
The script is a table of frame numbers and keys:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.h
|
||
typedef struct {
|
||
long frame; /**< Frame number this step fires on. */
|
||
SDL_Keycode key; /**< Key to synthesize. */
|
||
bool down; /**< True for a press, false for a release. */
|
||
} jrpg_ScriptStep;
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
static const jrpg_ScriptStep JRPG_DEMO_SCRIPT[] = {
|
||
{ 10, SDLK_RIGHT, true }, /* the per-facing walk animation */
|
||
{ 70, SDLK_RIGHT, false },
|
||
{ 75, SDLK_UP, true }, /* up the map, past the buildings */
|
||
{ 205, SDLK_UP, false },
|
||
{ 215, SDLK_SPACE, true }, /* the elder is in range: open the box */
|
||
{ 216, SDLK_SPACE, false },
|
||
{ 225, SDLK_LEFT, true }, /* frozen: AKGL_ERR_LOGICINTERRUPT eats this */
|
||
{ 245, SDLK_LEFT, false },
|
||
{ 255, SDLK_SPACE, true }, /* dismiss */
|
||
{ 256, SDLK_SPACE, false },
|
||
{ 265, SDLK_DOWN, true }, /* and walk away */
|
||
{ 285, SDLK_DOWN, false }
|
||
};
|
||
```
|
||
|
||
Playing it back is building an `SDL_Event` and handing it to the same function the real event
|
||
loop uses:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
PASS(errctx, aksl_memset((void *)&event, 0x00, sizeof(event)));
|
||
if ( JRPG_DEMO_SCRIPT[i].down ) {
|
||
event.type = SDL_EVENT_KEY_DOWN;
|
||
} else {
|
||
event.type = SDL_EVENT_KEY_UP;
|
||
}
|
||
event.key.which = 0;
|
||
event.key.key = JRPG_DEMO_SCRIPT[i].key;
|
||
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
|
||
```
|
||
|
||
**Go through `akgl_controller_handle_event`, not straight to the handlers.** A script that
|
||
calls the handlers directly still passes when the control map matches nothing at all, which
|
||
is the failure it most needs to catch.
|
||
|
||
Two more things make a headless run reproducible. Drive the clock rather than sleeping on it,
|
||
so a simulated second does not cost a real one:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
akgl_physics->gravity_time = SDL_GetTicksNS() - JRPG_FIXED_STEP_NS;
|
||
```
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
#define JRPG_FIXED_STEP_NS (AKGL_TIME_ONESEC_NS / 60)
|
||
```
|
||
|
||
And print the position while the player still exists — before teardown releases the actor
|
||
pool:
|
||
|
||
```c excerpt=examples/jrpg/jrpg.c
|
||
printf("jrpg: %ld frames, player at (%.0f, %.0f)\n", frameno, player->x, player->y);
|
||
```
|
||
|
||
The frame number is the loop's own counter, passed down. libakgl does not keep one.
|
||
|
||
## Where to look next
|
||
|
||
- [Chapter 20](20-tutorial-sidescroller.md) — the same shape of program with gravity, a jump
|
||
and platforms.
|
||
- [Chapter 15](15-collision.md) — the masks, the four queries and the static-proxy path this
|
||
chapter used for the town walls.
|
||
- [Chapter 17](17-text-and-fonts.md) — measuring text, and the teardown ordering trap.
|
||
- [Chapter 12](12-actors.md) — parents, children, and the seven behaviour hooks.
|
||
- [Chapter 7](07-the-game-and-the-frame.md) — the iterator flags, and what `akgl_game_update`
|
||
does in what order.
|