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>
448 lines
21 KiB
Markdown
448 lines
21 KiB
Markdown
# 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.
|