Files
libakgl/docs/11-characters.md

329 lines
14 KiB
Markdown
Raw Permalink Normal View History

Add the manual: nineteen chapters and a corrected README docs/ is a narrative manual, not a second reference. Every header already carries a substantial @file/@brief block and Doxyfile sets WARN_IF_UNDOCUMENTED with WARN_AS_ERROR, so an undocumented symbol already fails CI. The gap was navigation and worked examples. Chapters teach a task and link to the Doxygen output; where a declaration or a constant table has to be in front of the reader it arrives as a `c excerpt=` block, so the text *is* the header and cannot diverge from it. That also preserves the hand-aligned bit-flag tables scripts/reindent.el goes out of its way not to destroy. The manual does not re-document its dependencies. libakerror owns the ATTEMPT/CLEANUP/PROCESS/HANDLE/FINISH protocol, SDL3 owns renderers and events, Tiled owns the map format, jansson owns json_t. A chapter that restated any of them would be wrong the day upstream changed and nothing here would notice -- the same drift this work exists to fix, arriving from a different direction. So each chapter says what libakgl adds or constrains and links out for the rest. Chapter 4 is the exception and the reason for it: libakerror documents the mechanism, but only libakgl can say which statuses its own functions raise and what they mean here, and that was written down nowhere. It carries three tables -- libakgl's five status codes, the libakerror statuses libakgl actually raises with their meaning in this library, and the exit-status trap where `exit(AKGL_ERR_SDL)` is a wait status of 0 because the band starts at 256. Every chapter was written against src/ rather than against the header comments, which is how 27 false claims in those comments came to light. Where a chapter documents a known defect rather than a design decision it says so and points at TODO.md. README.md keeps the development process and hands the reader to docs/. Its task-oriented FAQ is deleted rather than moved, because one source of truth per topic is the whole point and that FAQ's examples did not compile. Census: 39 compiled snippets, 89 verbatim header excerpts, 4 JSON documents run through the real loaders, one linked-and-executed program with its output compared byte for byte, one generated figure. 11 norun blocks, each justified. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:37 -04:00
# 11. Characters
**A character is the reusable template. An actor is the instance.**
Everything that is the same for every goblin on the map — top speed, acceleration,
how long an animation frame is held, and which sprite to draw for which combination
of state bits — lives in one `akgl_Character` and is shared by pointer. A hundred
goblins cost one character. Everything that is unique to a particular goblin — where
it is, which way it is going, which frame it is on — lives in its `akgl_Actor`. See
[Chapter 12](12-actors.md).
Characters are pool objects (`akgl_heap_next_character`, 256 slots) published in
`AKGL_REGISTRY_CHARACTER` under their name, which is how `akgl_actor_set_character`
finds them. `akgl_registry_init` has to have run first.
```text
akgl_Character "hero" akgl_Actor "player"
+--------------------------------+ +---------------------+
| sx, sy, sz top speeds |<---------| basechar |
| ax, ay, az accelerations | borrowed| x, y, z |
| speedtime frame dwell (ns) | | state (bitmask) |
| state_sprites -----------------+---+ | curSpriteFrameId |
+--------------------------------+ | +---------------------+
|
SDL_PropertiesID keyed by the DECIMAL state bitmask
"18" -> akgl_Sprite *hero standing left
"146" -> akgl_Sprite *hero walking left
```
## The state-to-sprite map is an exact-match lookup
`state_sprites` is an `SDL_PropertiesID`. The key is **the decimal spelling of the
whole state value** — `SDL_itoa(state, buf, 10)` — and the value is an
`akgl_Sprite *`.
Two things follow, and both of them shape how you build a character:
**1. The key is the entire state word, not a set of bits to match against.** A
sprite added for `FACE_LEFT | MOVING_LEFT` is *not* found by a lookup for
`FACE_LEFT`. There is no subset fallback, no wildcard, no priority order, and no
default sprite. **You need a mapping for every state combination the character can
actually be in.**
**2. A missing mapping is an error, not a quiet miss.** `akgl_character_sprite_get`
raises `AKERR_KEY` — this is the one lookup in the library that treats absence as a
failure rather than as a successful answer of "nothing here", on the grounds that an
actor with no sprite for its current state cannot be drawn. `*dest` is set to `NULL`
alongside the error.
```c
#include <akgl/actor.h>
#include <akgl/character.h>
/*
* sprite_get treats "no mapping" as AKERR_KEY. Handle the status; do not test
* the return value for NULL and hope.
*/
akerr_ErrorContext *sprite_or_none(akgl_Character *basechar, int state, akgl_Sprite **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "basechar");
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
*dest = NULL;
ATTEMPT {
CATCH(errctx, basechar->sprite_get(basechar, state, dest));
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_KEY) {
/* No sprite for this exact combination. *dest is already NULL. */
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
```
`akgl_actor_update` and `akgl_actor_render` both swallow that `AKERR_KEY` themselves
— states change faster than art gets drawn, and a missing sprite should not stop the
frame — so an under-mapped character shows up as an actor that silently does not
draw. That is the symptom to recognize.
### The combinations you will actually need
Four facings, moving and standing, alive: eight mappings for the simplest walking
character. Written out, with the decimal key each one lands on:
| Combination | Decimal key | What it draws |
|---|---|---|
| `ALIVE` | 16 | standing still, **no facing** — see below |
| `ALIVE\|FACE_DOWN` | 17 | standing, facing the camera |
| `ALIVE\|FACE_LEFT` | 18 | standing, facing left |
| `ALIVE\|FACE_RIGHT` | 20 | standing, facing right |
| `ALIVE\|FACE_UP` | 24 | standing, facing away |
| `ALIVE\|FACE_DOWN\|MOVING_DOWN` | 1041 | walking towards the camera |
| `ALIVE\|FACE_LEFT\|MOVING_LEFT` | 146 | walking left |
| `ALIVE\|FACE_RIGHT\|MOVING_RIGHT` | 276 | walking right |
| `ALIVE\|FACE_UP\|MOVING_UP` | 536 | walking away |
**Do not skip the first row.** The default `facefunc`
(`akgl_actor_automatic_face`) clears every facing bit and then sets one only if a
movement bit is set — so an actor that stops moving is left with *no* facing bit at
all, and its state drops to bare `ALIVE`. Without a mapping for 16 it vanishes the
moment it stops. This is a known rough edge; the implementation carries a `TODO`
saying as much, and [Chapter 12](12-actors.md) covers the workaround.
The decimals are shown to make the mechanism concrete. **Write the constants**, or
the state-name strings in JSON — never the numbers.
## Adding a mapping
`akgl_character_sprite_add` takes a reference on the sprite, so the character keeps
it alive. The reference is taken *before* the property write, so a failed write
cannot leave the sprite bound with no reference behind it.
Rebinding a state that is already mapped releases the sprite it displaced. **The
header says otherwise** — `character.h`'s `@note` on `akgl_character_sprite_add`
claims the displaced sprite's reference "is never given back". That was true and is
not; `src/character.c` reads the old binding out first and releases it, with a
comment explaining that without it a character rebinding a state while alive leaks
one sprite slot per rebind.
> **A defect the release path does have.** The release is guarded by
> `(displaced != NULL) && (displaced != ref)`. Re-adding the *same* sprite for the
> *same* state therefore increments its reference count and releases nothing — one
> leaked reference per redundant call. It is idempotent-looking and is not.
> Harmless in a loader that runs once; not harmless in a loop.
State `0` is accepted and is a usable key.
## Building a character in code
```c
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/game.h>
#include <akgl/heap.h>
akerr_ErrorContext *build_hero(akgl_Sprite *idle_left, akgl_Sprite *walk_left)
{
akgl_Character *hero = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, idle_left, AKERR_NULLPOINTER, "idle_left");
FAIL_ZERO_RETURN(errctx, walk_left, AKERR_NULLPOINTER, "walk_left");
ATTEMPT {
CATCH(errctx, akgl_heap_next_character(&hero));
/* Zeroes the struct, creates the map, binds the two hooks, registers. */
CATCH(errctx, akgl_character_initialize(hero, "hero"));
/* Everything numeric is left at zero for us. */
hero->speedtime = 100 * AKGL_TIME_ONEMS_NS; /* 100 ms, in nanoseconds */
hero->sx = 0.20f;
hero->sy = 0.20f;
hero->ax = 0.20f;
hero->ay = 0.20f;
/* One mapping per exact combination we intend to draw. */
CATCH(errctx,
hero->sprite_add(
hero,
idle_left,
AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT)
);
CATCH(errctx,
hero->sprite_add(
hero,
walk_left,
AKGL_ACTOR_STATE_ALIVE
| AKGL_ACTOR_STATE_FACE_LEFT
| AKGL_ACTOR_STATE_MOVING_LEFT)
);
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
```
`sprite_add` and `sprite_get` are function pointers on the struct, bound to
`akgl_character_sprite_add` and `akgl_character_sprite_get` by
`akgl_character_initialize`. Call through the pointers, as with the render backend
in [Chapter 08](08-rendering.md) — that is what lets a game substitute its own
lookup, a state machine with fallbacks for instance, on one character.
`az` and `sz` are **not** read from JSON. They stay 0 unless you set them by hand.
## The character JSON format
```json kind=character setup=character
{
"name": "hero",
"speedtime": 100,
"speed_x": 0.20,
"speed_y": 0.20,
"acceleration_x": 0.20,
"acceleration_y": 0.20,
"sprite_mappings": [
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_LEFT"
],
"sprite": "hero standing left"
},
{
"state": [
"AKGL_ACTOR_STATE_ALIVE",
"AKGL_ACTOR_STATE_FACE_LEFT",
"AKGL_ACTOR_STATE_MOVING_LEFT"
],
"sprite": "hero walking left"
}
]
}
```
| Key | Type | Meaning |
|---|---|---|
| `name` | string | Registry key in `AKGL_REGISTRY_CHARACTER`. |
| `speedtime` | integer | **Milliseconds** a frame is held, scaled to nanoseconds by `AKGL_TIME_ONEMS_NS`. |
| `speed_x`, `speed_y` | number | Top speed per axis. Copied onto an actor as `sx`/`sy`. |
| `acceleration_x`, `acceleration_y` | number | Acceleration per axis, signed by direction each step. |
| `sprite_mappings` | array of objects | One entry per state combination. |
| `…[].sprite` | string | A name already in `AKGL_REGISTRY_SPRITE`. |
| `…[].state` | array of strings | State names, **OR-ed together** into one key. |
All of them are required. An absent key is `AKERR_KEY` naming it; a wrong type is
`AKERR_TYPE`.
`character.h`'s `@brief` describes `speedtime` as "in seconds". It is milliseconds
— the struct field's own documentation says so and `src/character.c` multiplies by
`AKGL_TIME_ONEMS_NS`.
**Sprites must be loaded first.** This loads characters, not sprites: every name in
a `sprite` field has to already be in `AKGL_REGISTRY_SPRITE`, so
`akgl_sprite_load_json` runs before `akgl_character_load_json`. A mapping naming an
unregistered sprite fails with `AKERR_NULLPOINTER` (not `AKERR_KEY` — it is a
lookup miss reported with a pointer status), and the message names the character,
the state and the sprite.
Two keys you may see in existing files and should not copy: `movementspeed` and
`velocity_x`. **Neither is read by anything.** They are ignored, silently.
### State names are prefixed
The strings come from `AKGL_ACTOR_STATE_STRING_NAMES` in
`src/actor_state_string_names.c`, which `akgl_registry_init_actor_state_strings`
turns into a registry. Entry `i` names the bit `1 << i`, and **every name carries
the `AKGL_ACTOR_STATE_` prefix**:
```text
AKGL_ACTOR_STATE_FACE_DOWN AKGL_ACTOR_STATE_MOVING_LEFT
AKGL_ACTOR_STATE_FACE_LEFT AKGL_ACTOR_STATE_MOVING_RIGHT
AKGL_ACTOR_STATE_FACE_RIGHT AKGL_ACTOR_STATE_MOVING_UP
AKGL_ACTOR_STATE_FACE_UP AKGL_ACTOR_STATE_MOVING_DOWN
AKGL_ACTOR_STATE_ALIVE AKGL_ACTOR_STATE_MOVING_IN
AKGL_ACTOR_STATE_DYING AKGL_ACTOR_STATE_MOVING_OUT
AKGL_ACTOR_STATE_DEAD AKGL_ACTOR_STATE_UNDEFINED_13 .. _31
```
A name that is not in that list is `AKERR_KEY`, "Unknown actor state", quoting it —
a typo is refused rather than skipped, because a skipped typo would bind a sprite to
the wrong combination and show up as art that never appears. An empty `state` array
is legal and gives you the key `0`.
Adding a new state means adding a bit in `include/akgl/actor.h` **and** its name at
the matching index in `src/actor_state_string_names.c`. A bit whose name is missing
cannot be referred to from JSON at all — which is exactly what happened to
`MOVING_IN` and `MOVING_OUT` until 0.5.0.
> ### `util/assets/littleguy.json` is invalid against the current loader
>
> The sample data for the one shipped demo does not load, and it is worth naming
> the three separate reasons because each is a different class of drift:
>
> 1. **The state names are unprefixed** — `"ACTOR_STATE_ALIVE"` rather than
> `"AKGL_ACTOR_STATE_ALIVE"`. Every one fails with `AKERR_KEY`, "Unknown actor
> state".
> 2. **It has `velocity_x` and `velocity_y`**, which nothing reads, and lacks
> `speed_x` and `speed_y`, which are required.
> 3. **It has no `speedtime`, `acceleration_x` or `acceleration_y`** either — all
> three required, all three `AKERR_KEY`.
>
> `tests/assets/testcharacter.json` is the file to copy from. Fixing the sample is
> out of scope here (`plan.md`, "Out of scope, deliberately"); the finding is
> recorded so nobody quotes it forward.
## Teardown
`akgl_heap_release_character` decrements the reference count, and at zero it walks
`state_sprites` with `akgl_character_state_sprites_iterate`, releasing every sprite
reference `sprite_add` took, destroys the property set, clears the registry entry
and zeroes the slot.
That walk is an `SDL_EnumerateProperties` callback, so it returns `void` and ends in
`FINISH_NORETURN`. **An error inside it exits the process** via libakerror's default
unhandled-error handler. The same hazard, in its more common form, is in
[Chapter 12](12-actors.md) under `akgl_registry_iterate_actor`; install your own
`akerr_handler_unhandled_error` if a game should survive it.
An actor **borrows** its character and takes no reference on it. Releasing a
character out from under a live actor leaves a dangling `basechar`.
## Known defects worth knowing here
- **Re-adding the same sprite for the same state leaks a reference.** See above.
- **Long names register and unregister under different keys.**
`akgl_character_initialize` writes the registry entry under the *caller's* `name`
pointer, while `akgl_heap_release_character` clears it under the character's own
truncated 128-byte copy. For a name over 127 bytes those differ, and the registry
entry survives the release — pointing at a zeroed slot. Latent for ordinary names;
related to `TODO.md`, "Truncated registry keys can collide", which covers the
collision half.
- **`speedtime` is written through an `int *` cast of a `uint64_t` field.**
`src/character.c` passes `(int *)&obj->speedtime` to
`akgl_get_json_integer_value`. It works because the struct was zeroed and the
platform is little-endian; it is the same class of cast-over-a-signature-mismatch
that `AGENTS.md` argues against under "Never silence a warning with a cast".
- **`akgl_character_initialize` silently replaces a same-named character**, and the
one it displaced becomes unreachable rather than being released.
## Where to look next
- [Chapter 10](10-spritesheets-and-sprites.md) — the sprites these map to.
- [Chapter 12](12-actors.md) — the state bitmask itself, and what sets it.
- [Chapter 14](14-physics.md) — what `sx`/`sy` and `ax`/`ay` mean once an actor is
simulated.