Files
libakgl/docs/12-actors.md

448 lines
21 KiB
Markdown
Raw 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
# 12. Actors
An actor is a live thing in the world. It carries only what is unique to it — where
it is, which way it is moving, which animation frame it is on — and borrows speed,
acceleration and the whole state-to-sprite map from the `akgl_Character` it points
at. That is what makes a hundred of one kind of thing cheap. See
[Chapter 11](11-characters.md) for the other half.
Actors are pool objects (`akgl_heap_next_actor`, 64 slots by default) published in
`AKGL_REGISTRY_ACTOR` under their name.
```c
#include <akgl/actor.h>
#include <akgl/game.h>
#include <akgl/heap.h>
akerr_ErrorContext *spawn_player(void)
{
akgl_Actor *player = NULL;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&player));
/* Zeroes it, sets scale 1.0 and movement_controls_face, installs the
six default hooks, registers it, takes the first reference. */
CATCH(errctx, akgl_actor_initialize(player, "player"));
/* Until this runs the actor cannot update, render or simulate. */
CATCH(errctx, akgl_actor_set_character(player, "hero"));
player->state = AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_DOWN;
player->visible = true;
player->layer = 1;
player->x = 64.0f;
player->y = 64.0f;
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
```
`akgl_actor_initialize` does **not** set a character. `akgl_actor_set_character`
looks one up by name, copies its `sx`/`sy` onto the actor and zeroes `ax`/`ay` so it
starts from rest. The character is borrowed, not referenced. A name that is not
registered comes back as `AKERR_NULLPOINTER` — a lookup miss reported with a pointer
status rather than `AKERR_KEY`.
> `sz` is not copied and `az` is not zeroed, so an actor's depth speed keeps
> whatever it had. The default movement logic re-copies all three every step, which
> papers over it for anything that simulates.
## State is a 32-bit bitmask
An actor is facing left *and* moving left *and* alive at the same time, and **the
whole combination is the key that selects a sprite**. It is not an enum and there is
no current-state field.
Thirteen bits are defined:
```c excerpt=include/akgl/actor.h
#define AKGL_ACTOR_STATE_FACE_DOWN (1 << 0) // 1 0000 0000 0000 0001
#define AKGL_ACTOR_STATE_FACE_LEFT (1 << 1) // 2 0000 0000 0000 0010
#define AKGL_ACTOR_STATE_FACE_RIGHT (1 << 2) // 4 0000 0000 0000 0100
#define AKGL_ACTOR_STATE_FACE_UP (1 << 3) // 8 0000 0000 0000 1000
#define AKGL_ACTOR_STATE_ALIVE (1 << 4) // 16 0000 0000 0001 0000
#define AKGL_ACTOR_STATE_DYING (1 << 5) // 32 0000 0000 0010 0000
#define AKGL_ACTOR_STATE_DEAD (1 << 6) // 64 0000 0000 0100 0000
#define AKGL_ACTOR_STATE_MOVING_LEFT (1 << 7) // 128 0000 0000 1000 0000
#define AKGL_ACTOR_STATE_MOVING_RIGHT (1 << 8) // 256 0000 0001 0000 0000
#define AKGL_ACTOR_STATE_MOVING_UP (1 << 9) // 512 0000 0010 0000 0000
#define AKGL_ACTOR_STATE_MOVING_DOWN (1 << 10) // 1024 0000 0100 0000 0000
#define AKGL_ACTOR_STATE_MOVING_IN (1 << 11) // 2048 0000 1000 0000 0000
#define AKGL_ACTOR_STATE_MOVING_OUT (1 << 12) // 4096 0001 0000 0000 0000
```
The trailing bit pattern in that table is the position **within its own 16-bit
half**, not the full 32-bit value. `actor.h` says so at the top of the block; read
as a whole word it looks like bit 16 restarts at bit 0, which it does not.
**Bits 13 through 31 are undefined and are yours.** They are declared as
`AKGL_ACTOR_STATE_UNDEFINED_13` .. `_31` and — this is the useful part — they are
*already named* at the matching indices in `src/actor_state_string_names.c`, so a
character JSON can bind a sprite to one without any code change. Renaming a bit means
editing both files at the same index; see [Chapter 11](11-characters.md).
`state` is an `int32_t`, so bit 31 makes it negative and its decimal property key is
`-2147483648`. It works. Prefer the low undefined bits.
Two composite masks are provided:
```c excerpt=include/akgl/actor.h
#define AKGL_ACTOR_STATE_FACE_ALL (AKGL_ACTOR_STATE_FACE_DOWN | AKGL_ACTOR_STATE_FACE_LEFT | AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_FACE_UP)
#define AKGL_ACTOR_STATE_MOVING_ALL (AKGL_ACTOR_STATE_MOVING_LEFT | AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_MOVING_UP | AKGL_ACTOR_STATE_MOVING_DOWN)
```
**`MOVING_ALL` does not include `MOVING_IN` or `MOVING_OUT`.** It is the four
planar directions only. Clearing it leaves the two depth bits set.
### AKGL_BITMASK_HAS means all of them, not any of them
The four manipulation macros live in `game.h`:
| Macro | Means |
|---|---|
| `AKGL_BITMASK_HAS(x, y)` | **every** bit of `y` is set in `x``((x) & (y)) == (y)` |
| `AKGL_BITMASK_HASNOT(x, y)` | at least one bit of `y` is missing from `x` |
| `AKGL_BITMASK_ADD(x, y)` | set every bit of `y` in `x`; modifies `x` |
| `AKGL_BITMASK_DEL(x, y)` | clear every bit of `y` in `x`; modifies `x` |
The "all, not any" reading is the one that catches people. `AKGL_BITMASK_HAS(state,
AKGL_ACTOR_STATE_FACE_ALL)` asks whether the actor is facing **all four ways at
once**, which is never. To ask "is it facing at all", test
`(state & AKGL_ACTOR_STATE_FACE_ALL)` directly, or `AKGL_BITMASK_HASNOT` against a
single bit.
All five macros are fully parenthesized, so `!AKGL_BITMASK_HAS(a, b)` means what it
reads as. Until 0.5.0 it expanded to a bare `(x & y) == y` and a negation bound to
the `&`, parsing as `!(a & b) == b`. Nothing in the tree negated it, which is the
only reason it was latent rather than live — and the test that distinguishes the
two parses is a bit that is *not* set whose value is *not* 1, not the obvious one.
## The six behaviour hooks
**Behaviour attaches as function pointers, not by inheritance.** There is no actor
subclass. `akgl_actor_initialize` installs the library's defaults on every actor,
and a game that wants a different flavour of anything replaces the pointer **on that
one actor**.
| Hook | Default | Called by | Job |
|---|---|---|---|
| `updatefunc` | `akgl_actor_update` | `akgl_game_update`'s sweep | per-frame logic: `facefunc`, then advance the animation if due |
| `renderfunc` | `akgl_actor_render` | `akgl_render_2d_draw_world` | per-frame draw |
| `facefunc` | `akgl_actor_automatic_face` | `akgl_actor_update` | choose the facing bits |
| `movementlogicfunc` | `akgl_actor_logic_movement` | `akgl_physics_simulate`, before gravity | turn movement bits into signed acceleration |
| `changeframefunc` | `akgl_actor_logic_changeframe` | `akgl_actor_update`, when the frame is due | step to the next animation frame |
| `addchild` | `akgl_actor_add_child` | you | attach a child actor |
Replace them after `akgl_actor_initialize`, never before — it overwrites all six.
The `movementlogicfunc` slot is the interesting one, because it is where
`AKGL_ERR_LOGICINTERRUPT` stops being a table row in [Chapter 04](04-errors.md) and
becomes something you write. It is **not a failure**; it is a control signal meaning
"skip the rest of this tick for this actor", and `akgl_physics_simulate` swallows it:
```c
#include <akgl/actor.h>
#include <akgl/error.h>
#include <akgl/game.h>
/*
* A movementlogicfunc that freezes a dying actor. AKGL_ERR_LOGICINTERRUPT is a
* control signal, not a failure -- akgl_physics_simulate swallows it and skips
* the rest of this actor's step. Only a movementlogicfunc may raise it; from a
* backend's gravity or move it aborts the whole step.
*/
static akerr_ErrorContext *frozen_movement(akgl_Actor *obj, float32_t dt)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
if ( AKGL_BITMASK_HAS(obj->state, AKGL_ACTOR_STATE_DYING) ) {
FAIL_RETURN(
errctx,
AKGL_ERR_LOGICINTERRUPT,
"%s is dying; skip its step",
(char *)obj->name);
}
PASS(errctx, akgl_actor_logic_movement(obj, dt));
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *freeze_the_dying(akgl_Actor *obj)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
obj->movementlogicfunc = &frozen_movement;
SUCCEED_RETURN(errctx);
}
```
The default `movementlogicfunc` re-copies the character's speed limits onto the
actor each step — so editing a character takes effect next step — and sets `ax`/`ay`
to plus or minus the character's acceleration according to which movement bits are
set. It integrates nothing; the physics backend does that
([Chapter 14](14-physics.md)). Opposing bits do not cancel: `MOVING_LEFT` wins over
`MOVING_RIGHT` because it is tested first. **An actor with no movement bits keeps
its previous acceleration** rather than being zeroed, which is why the input
handlers clear it themselves on release.
The default `facefunc` clears every facing bit and sets the one matching the first
movement bit it finds, in the order left, right, up, down — so a diagonal faces its
horizontal component. An actor with `movement_controls_face` clear is left alone
entirely, which is the hook for something that aims independently of the way it
walks.
> **An actor that stops moving is left with no facing bit at all**, rather than
> keeping the way it was last facing. Its state drops to bare `ALIVE` and no longer
> matches a "standing still facing left" sprite. The implementation carries a `TODO`
> saying as much. Two workarounds: map bare `ALIVE` to something, or clear
> `movement_controls_face` and set the facing bits yourself.
## layer
`layer` is which tilemap layer the actor is drawn on, and it is set from the object
layer it was placed in. `akgl_render_2d_draw_world` interleaves the map's layers
with the actors standing on them, in ascending order — see
[Chapter 08](08-rendering.md).
**Only layers 0 through 15 are drawn.** The field is a `uint32_t` and nothing
range-checks it, but `draw_world`'s loop stops at `AKGL_TILEMAP_MAX_LAYERS` (16). An
actor on layer 16 is never drawn and never reports why.
## Parents and children
`addchild` attaches one actor to another so it moves with it — a carried lantern, a
turret on a tank, a party member walking behind the hero. Up to
`AKGL_ACTOR_MAX_CHILDREN` (8) per parent.
**A child is not simulated.** The physics step reaches it, sees a parent, snaps it
to the parent's position plus its own offset, and moves on: no thrust, no gravity,
no drag, no acceleration. That is the whole behaviour, and games that use it depend
on exactly that.
The parent takes a reference on the child, and **releasing a parent releases every
child with it, recursively**. There is no detach function; `AKERR_RELATIONSHIP` is
what you get for attaching an actor that already has a parent, and the only way out
is to release and rebuild. Running out of slots is `AKERR_OUTOFBOUNDS`.
> **Nothing checks for a cycle.** Making an actor its own ancestor makes
> `akgl_heap_release_actor` recurse until the stack runs out. This builds a tree,
> not a graph, and the tree-ness is your responsibility.
### The offset lives in two places, and they disagree
This is a real defect and it will bite anyone using children, so it gets stated
plainly rather than buried:
```text
akgl_physics_simulate: actor->x = parent->x + actor->vx <- offset is vx
akgl_actor_render: dest.x = parent->x + actor->x <- offset is x
```
`src/physics.c` treats `vx`/`vy`/`vz` as the offset and writes the result into
`x`/`y`/`z`, turning the child's `x` into an absolute world position.
`src/actor.c` then treats `x`/`y` as an offset and adds the parent's position **a
second time**. In `akgl_game_update` the physics step runs before the draw, so a
child is drawn one full parent-offset away from where it should be.
`actor.h` documents both readings, in two different places, without noticing they
contradict — `akgl_Actor::x` says "For a child, an offset from the parent" and
`akgl_Actor::vy` says "On a child actor this is read as an offset from the parent
instead."
It is invisible when the parent sits at the origin, which is why it survived. Until
it is fixed, the guard is: **do not call `akgl_actor_render` for a child through
`draw_world` if you also run the physics step.** Bind a `renderfunc` on the child
that draws at `obj->x`/`obj->y` without re-adding the parent, or keep children
purely decorative and position them yourself.
```c
#include <akgl/actor.h>
#include <akgl/game.h>
#include <akgl/heap.h>
/* A lantern carried 8 px right of and 8 px above the player. */
akerr_ErrorContext *attach_lantern(akgl_Actor *player)
{
akgl_Actor *lantern = NULL;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
ATTEMPT {
CATCH(errctx, akgl_heap_next_actor(&lantern));
CATCH(errctx, akgl_actor_initialize(lantern, "player lantern"));
CATCH(errctx, akgl_actor_set_character(lantern, "lantern"));
lantern->state = AKGL_ACTOR_STATE_ALIVE;
lantern->visible = true;
lantern->layer = player->layer;
/* The physics step reads the offset out of vx/vy/vz. */
lantern->vx = 8.0f;
lantern->vy = -8.0f;
CATCH(errctx, player->addchild(player, lantern));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
```
## The eight control handlers
The `akgl_actor_cmhf_*` functions ("control map handler function") are what
`akgl_controller_default` binds the arrow keys and the D-pad to, and what a game's
own `akgl_Control` bindings can point at. They take the control map's **target
actor**, not a global "player", so they work for any number of locally controlled
actors. See [Chapter 15](15-input.md).
| Handler | Sets | Clears first | Writes |
|---|---|---|---|
| `akgl_actor_cmhf_left_on` | `MOVING_LEFT \| FACE_LEFT` | `FACE_ALL \| MOVING_ALL` | `ax = -basechar->ax` |
| `akgl_actor_cmhf_left_off` | — | `MOVING_LEFT` | `ax = 0`, `tx = 0` |
| `akgl_actor_cmhf_right_on` | `MOVING_RIGHT \| FACE_RIGHT` | `FACE_ALL \| MOVING_ALL` | `ax = basechar->ax` |
| `akgl_actor_cmhf_right_off` | — | `MOVING_RIGHT` | `ax = 0`, `tx = 0` |
| `akgl_actor_cmhf_up_on` | `MOVING_UP \| FACE_UP` | `FACE_ALL \| MOVING_ALL` | `ay = -basechar->ay` |
| `akgl_actor_cmhf_up_off` | — | `MOVING_UP` | `ay = 0`, `ty = 0` |
| `akgl_actor_cmhf_down_on` | `MOVING_DOWN \| FACE_DOWN` | `FACE_ALL \| MOVING_ALL` | `ay = basechar->ay` |
| `akgl_actor_cmhf_down_off` | — | `MOVING_DOWN` | `ay = 0`, `ty = 0` |
`ay` is negated for *up* because y grows downward.
**Two directions is not a diagonal.** Every `_on` handler clears the whole of
`FACE_ALL | MOVING_ALL` before setting its own two bits, so holding left and up at
once moves in whichever was pressed **last**. That is deliberate — it keeps the
state word to one facing and one direction, which keeps the sprite mapping in
[Chapter 11](11-characters.md) from needing a mapping per diagonal. A game that
wants diagonals binds its own handlers that `AKGL_BITMASK_ADD` without clearing, and
maps the extra combinations.
**The `_off` handlers clear exactly two fields.** `ax` and `tx` for the horizontal
pair, `ay` and `ty` for the vertical. They do **not** touch `ex`/`ey` and they do
not touch `vx`/`vy`.
`actor.h` still says otherwise — the block comment above them describes the `_off`
handlers as "zeroing acceleration, thrust, environmental velocity and velocity on
that axis", and `akgl_actor_cmhf_up_off` and `_down_off` each carry a `@note`
saying they zero `ey`. **They did, and that was a defect**: `ey` is where the arcade
backend accumulates gravity, so tapping down mid-jump stopped the character in the
air. Velocity was never theirs to clear either — `simulate` recomputes `v` as
`e + t` every step. Those notes predate the fix.
> **Known defect (`TODO.md`, "Arcade physics feel").** There is no friction and no
> deceleration: zeroing `tx` on release **stops the actor dead**. Correct for Zelda,
> wrong for Mario. Bind your own `_off` handler that decays `tx` over time if you
> want momentum.
Releasing left while holding right also stops the rightward movement, since
`left_off` clears the whole x axis whichever way the actor was going.
## Warning: an error inside `akgl_registry_iterate_actor` exits the process
`akgl_registry_iterate_actor` is an `SDL_EnumerateProperties` callback. SDL
callbacks return `void`, so it has nowhere to propagate an error to — it ends in
`FINISH_NORETURN`, which logs the stack trace and then calls libakerror's
unhandled-error handler, **whose default implementation calls `akerr_exit()` and
terminates the process**.
Everything it can go wrong on is ordinary content:
- a registry key with no pointer behind it — `AKERR_KEY`;
- an actor whose `updatefunc` fails;
- `akgl_tilemap_scale_actor` failing;
- an actor whose `renderfunc` fails;
- a `NULL` `userdata` — the iterator is required despite the `void *`.
**A wrong name in an asset file therefore kills the game rather than skipping an
actor.** This is not theoretical: it is the first thing a reader hits when a sprite
or character name is misspelled.
`akgl_game_update` does not use this callback — it sweeps the actor pool directly —
so you reach it by driving a registry sweep yourself, which is what
`util/charviewer.c` does:
```c norun
SDL_EnumerateProperties(AKGL_REGISTRY_ACTOR, &akgl_registry_iterate_actor, (void *)&opflags);
```
(`norun` because it needs a live registry and a populated `opflags`; it is quoted to
show the shape, and `util/charviewer.c` is the compiled instance of it.)
The same hazard is in `akgl_character_state_sprites_iterate`, which
**`akgl_heap_release_character` calls on every character release** — so that one is
on an ordinary path, not an unusual one. See [Chapter 11](11-characters.md).
If a game should survive these, install your own handler:
```c
#include <akerror.h>
#include <akgl/actor.h>
#include <akgl/error.h>
static akerr_ErrorContext *last_callback_error = NULL;
/*
* The default akerr_handler_unhandled_error calls akerr_exit(). Replacing it
* is the only way an error raised inside an SDL callback does not take the
* process with it. Take a reference: the context is released after this
* returns.
*/
static void survive_unhandled_error(akerr_ErrorContext *errctx)
{
if ( errctx == NULL ) {
return;
}
LOG_ERROR(errctx);
errctx->refcount += 1;
last_callback_error = errctx;
}
void install_survivable_handler(void)
{
akerr_handler_unhandled_error = &survive_unhandled_error;
}
```
Note what surviving costs you: the sweep carries on with the *next* entry, and the
actor that failed simply did not update or draw this frame. Check
`last_callback_error` somewhere, or you have traded a loud crash for a silent one.
## The rest of the struct
A few fields worth calling out; the full annotated list is in the generated Doxygen
for `akgl_Actor`.
| Field | Note |
|---|---|
| `visible` | Deliberate hiding. Off-camera actors are skipped separately. |
| `scale` | Draw scale. Written by `akgl_tilemap_scale_actor`, or forced to 1.0 when tilemap scaling is off — **it is overwritten every frame**, so setting it by hand does not stick. |
| `actorData` | Your per-actor data. The library never reads or frees it. |
| `curSpriteFrameId` | Index into the sprite's `frameids` playlist, **not** a frame number on the sheet. See [Chapter 10](10-spritesheets-and-sprites.md). |
| `movetimer` | Unused. Nothing in the library reads or writes it. |
| `vx`/`vy`/`vz` | Recomputed each step as `e + t`. Writing them directly is overwritten — except on a child, where they are the offset. |
## Known defects worth knowing here
- **The parent/child offset is double-counted at draw time.** See above.
- **`akgl_heap_release_actor` recurses with no cycle check.**
- **Long names register and unregister under different keys.**
`akgl_actor_initialize` writes the registry entry under the *caller's* `name`
pointer while `akgl_heap_release_actor` clears it under the actor's truncated
128-byte copy. Identical for ordinary names; divergent past 127 bytes, and then
the registry keeps an entry pointing at a zeroed slot. Related to `TODO.md`,
"Truncated registry keys can collide".
- **`akgl_actor_initialize` silently replaces a same-named actor**, and the one it
displaced becomes unreachable rather than being released.
- **No friction on release**, and **no terminal velocity** under gravity —
`TODO.md`, "Arcade physics feel". [Chapter 14](14-physics.md) covers both.
## Where to look next
- [Chapter 11](11-characters.md) — the template, and the state-to-sprite map.
- [Chapter 14](14-physics.md) — what happens to `t`, `e`, `v` and `a` each step.
- [Chapter 15](15-input.md) — control maps, and binding the handlers above.
- [Chapter 08](08-rendering.md) — where `renderfunc` gets called from.
- [Chapter 04](04-errors.md) — `AKGL_ERR_LOGICINTERRUPT` and the rest of the
status tables.