A reader drew their own town, put the elder 115 pixels from anywhere the player could stand, and spent the rest of their time looking for the bug in jrpg_cmhf_talk. There is no bug: the handler finds nobody within TALK_RANGE and returns success, which is indistinguishable from the game not being wired up. The chapter stated the constant and not what it means in the editor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
1255 lines
42 KiB
Markdown
1255 lines
42 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
|
||
```
|
||
|
||
**Place your NPCs with that number in mind.** The distance is measured between the two
|
||
actors' positions, and both sprites are 32 pixels, so 64 is two sprite widths — close, by
|
||
design. An NPC parked behind a building or across a wall from anywhere the player can stand
|
||
is an NPC the player can see and never talk to, and nothing reports that: the handler finds
|
||
nobody in range and returns success. If a conversation will not open, print the distance
|
||
before you go looking at the code.
|
||
|
||
```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.
|