329 lines
14 KiB
Markdown
329 lines
14 KiB
Markdown
|
|
# 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.
|