Files
libakgl/docs/14-physics.md

359 lines
18 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
# 14. Physics
The physics subsystem has the same shape as the renderer: a record of function pointers
plus an initializer that fills it in. Two backends ship. `null` accepts every call and
changes nothing, which is what a menu screen or a test harness wants. `arcade` applies
gravity, drag, thrust and a speed cap, which is what a 2D game wants.
**There is no "physics body" type.** The body *is* the actor plus the character it
instantiates. An actor carries the position and the three velocity terms; its character
supplies the accelerations and the top speeds. Nothing else participates. If you have read
[Chapter 11](11-characters.md) and [Chapter 12](12-actors.md) you have already met every
field this chapter uses.
## The model
```text
thrust (tx, ty, tz) the actor's own effort, while it is moving
+
environ (ex, ey, ez) gravity, less drag -- accumulates across steps
=
velocity(vx, vy, vz) recomputed every step; writing it directly is overwritten
(a CHILD actor is the exception -- see below)
caps (sx, sy, sz) from the character. Caps the THRUST VECTOR, not velocity
accel (ax, ay, az) from the character, signed by the direction of travel
```
Two properties of that arrangement carry all the consequences.
**The cap is on thrust, not on velocity.** `sx`/`sy`/`sz` are the character's own top
speed, not a speed limit on the world, and `akgl_physics_arcade_gravity` touches only the
environmental term. So a falling actor accelerates straight past its stated top speed —
which is the point, for a falling one. It also means there is no terminal velocity; see
below.
**The cap is on the thrust *vector*, scaled to an ellipse, not on each axis separately.**
```text
ty
| Capping each axis against its own limit
......|...... admits the corner of the box: an actor
..' | '.. holding two directions got both caps
.' +---+---+ '. at once and travelled their
: | | | : diagonal -- 41% faster than either
: ----+---+---+---- : alone. Scaling the vector to the
----:------+---+---+------:---- tx ellipse keeps a character
: | | | : whose horizontal and vertical
:. +---+---+ .: speeds differ moving at the ratio
'.. | sx ..' it asked for, and makes them equal
''|.......''' where the speeds are.
|
dotted ellipse: the cap solid box: the old per-axis clamp
```
```c excerpt=src/physics.c
overshoot = 0.0f;
if ( actor->sx != 0.0f ) {
overshoot += (actor->tx / actor->sx) * (actor->tx / actor->sx);
} else {
actor->tx = 0.0f;
}
if ( actor->sy != 0.0f ) {
overshoot += (actor->ty / actor->sy) * (actor->ty / actor->sy);
} else {
actor->ty = 0.0f;
}
if ( actor->sz != 0.0f ) {
overshoot += (actor->tz / actor->sz) * (actor->tz / actor->sz);
} else {
actor->tz = 0.0f;
}
if ( overshoot > 1.0f ) {
thrustscale = 1.0f / sqrtf(overshoot);
actor->tx *= thrustscale;
actor->ty *= thrustscale;
actor->tz *= thrustscale;
}
```
An axis whose top speed is zero cannot be thrust along at all — that is what the old
per-axis clamp did with it — and it stays out of the magnitude entirely, because dividing
by it would not end well.
**Only x and y ever accumulate thrust.** `akgl_physics_simulate` reads the
`AKGL_ACTOR_STATE_MOVING_LEFT`/`RIGHT` bits into `tx` and `MOVING_UP`/`DOWN` into `ty`.
`AKGL_ACTOR_STATE_MOVING_IN` and `MOVING_OUT` exist and nothing reads them, so `tz` is only
ever zeroed or scaled — the z axis has gravity, drag and movement but **no thrust an actor
can apply**. Set `tz` yourself from a `movementlogicfunc` if you need depth motion.
## One step, in order
`akgl_physics_simulate` is shared by both backends; what differs is the `gravity` and
`move` they point at. It walks the whole actor pool in index order, and per actor:
```text
refcount == 0 ? ------------------------------> skip (free pool slot)
parent != NULL ? -> x,y,z = parent's + own v, -> skip (never simulates)
basechar == NULL ? ------------------------------> skip (no speeds)
LAYERMASK set and actor->layer != layerid ? -------> skip
tx += ax * dt if MOVING_LEFT or MOVING_RIGHT
ty += ay * dt if MOVING_UP or MOVING_DOWN
scale (tx,ty,tz) to the (sx,sy,sz) ellipse if it is outside
+- ATTEMPT ----------------------------------------------------------+
| actor->movementlogicfunc(actor, dt) |
| AKGL_ERR_LOGICINTERRUPT raised here is swallowed: the actor |
| is skipped for the rest of the tick and the loop continues |
| self->gravity(self, actor, dt) |
| ex -= ex * drag_x * dt (and y, z, each only if drag is nonzero) |
| vx = ex + tx (and y, z) |
| self->move(self, actor, dt) |
+--------------------------------------------------------------------+
after the loop: self->gravity_time = curtime
```
**A child actor is snapped and skipped.** An actor with a `parent` has its position set to
the parent's plus its own `vx`/`vy`/`vz`, read as a fixed offset rather than as a velocity,
and then it is skipped entirely. Children do not simulate. That is the mechanism a party
member or a held item is built on — see [Chapter 12](12-actors.md).
**The thrust accumulation happens *before* `movementlogicfunc`, which is what sets `ax` and
`sx`.** So the `ax` and `sx` a step uses are the ones the *previous* step's movement logic
installed, and an actor's very first step sees `sx == 0` and has its thrust zeroed. The
header's summary — "movement logic, then gravity, then drag, then velocity, then move" —
leaves that out. It is one frame of lag at the start of a movement and no player will see
it, but it is the reason a unit test that calls `simulate` once and checks `tx` gets zero.
**`gravity_time` is stamped after the loop, not before it.** A `gravity` or `move` that
raises returns straight out of `akgl_physics_simulate` without stamping, so the *next*
step measures `dt` from the older epoch and takes a double-length step — bounded by
`max_timestep`, but still a visible jump. Treat a raised status out of `simulate` as
needing a re-stamp of `gravity_time` before you continue.
## `dt` is not yours to supply
`akgl_physics_simulate` takes no timestep. It calls `SDL_GetTicksNS()` itself and measures
against `self->gravity_time`, then bounds the result:
```text
dt = (SDL_GetTicksNS() - self->gravity_time) / 1e9
if ( max_timestep > 0 and dt > max_timestep ) dt = max_timestep
if ( dt < 0 ) dt = 0
```
`max_timestep` comes from the `physics.max_timestep` property and defaults to
`AKGL_PHYSICS_DEFAULT_MAX_TIMESTEP` — a twentieth of a second, three frames at 60 Hz, so an
ordinary dropped frame passes through untouched and a level load does not.
**That bound is not a nicety.** Everything below it multiplies by `dt`, so one enormous
`dt` moves every actor by however far its velocity carries it over the whole stall. A
quarter-second load under platformer gravity is a hundred pixels of fall between the first
frame and the second, straight through whatever was underneath. That was a live defect
until 0.6.0: `gravity_time` was never seeded, so the first step measured the entire time
since SDL started. Both initializers seed it now, and the bound catches the rest. Advancing
the world in slow motion through a hitch is the trade every engine makes here.
A zero or negative `max_timestep` disables the bound, for a caller who would rather have
the real elapsed time and handle it themselves.
### Stepping deterministically in a test
Because `dt` is measured rather than passed, **there is no way to step the engine by an
exact interval — but there is a way to step it by an exact bound.** `tests/physics_sim.c`
documents the only technique that holds up under load:
```text
sim_physics.max_timestep = dt; /* the step you want */
sim_physics.gravity_time = 0; /* measured interval = whole process uptime */
sim_physics.simulate(&sim_physics, NULL);
```
Zeroing `gravity_time` makes the measured interval the process's whole uptime, which is
always past the bound, so the step taken is `max_timestep` and nothing else. The scheduler
cannot get in the way of that. The first version of that helper set `gravity_time` to
`now - dt` and let the real clock supply the step; a machine busy enough to deschedule the
process between that store and the `SDL_GetTicksNS()` inside `simulate` produced a longer
step and a red suite, exactly once, under a parallel `ctest`. **That the engine cannot be
stepped exactly is itself a finding**, recorded in `TODO.md` under "Arcade physics feel".
Two more rules from that suite, both learned the hard way. `main` must call `SDL_Init`
without it SDL sets its clock epoch on first use, `SDL_GetTicksNS()` returns something
around 130 ns, and every `dt`-dependent assertion passes for a reason unrelated to the
code. And read the numbers each simulation prints even when it is green: a change that
keeps every assertion passing while moving the apex of a jump by 30 px is a change worth
noticing.
## Choosing a backend
```c excerpt=src/physics.c
if ( strncmp(type->data, "null", 4) == 0) {
PASS(errctx, akgl_physics_init_null(self));
SUCCEED_RETURN(errctx);
}
if ( strncmp(type->data, "arcade", 6) == 0) {
PASS(errctx, akgl_physics_init_arcade(self));
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKERR_KEY, "Invalid physics engine %s", type->data);
```
**The match is on leading characters.** `"nullify"` selects `null` and `"arcadia"` selects
`arcade`; anything else raises `AKERR_KEY` quoting what was asked for. Adding a backend
means adding a name here, not a branch in the simulation.
**`akgl_game_init` does not call the factory, and there is no `physics.engine` property.**
The `@file` block and the `akgl_physics_factory` doc comment in `include/akgl/physics.h`
both say otherwise. They are wrong: `akgl_game_init` points `akgl_physics` at
`akgl_default_physics` — zeroed storage whose four method pointers are all `NULL` — and
never initializes it, and the string `physics.engine` appears nowhere in `src/`. The
startup sequence in `include/akgl/game.h` has it right: **the application calls an
initializer or the factory itself**, at step 4, after the configuration properties are set
and before the first frame. Skip that and the first `akgl_game_update` calls through a
`NULL` `simulate`.
The only caller of `akgl_physics_factory` inside the library is
`akgl_tilemap_load_physics`, driven by a map's `physics.model` custom property — see
[Chapter 13](13-tilemaps.md). `util/charviewer.c` shows the direct form,
`akgl_physics_init_null(akgl_physics)`. Correcting the header comments is filed separately;
until then, believe `game.h` and this chapter.
```c
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/physics.h>
#include <akgl/registry.h>
/*
* Bring up the arcade backend. akgl_game_init does NOT do this: it points
* akgl_physics at akgl_default_physics, which is zeroed storage whose method
* pointers are all NULL. Call an initializer, or the first akgl_game_update
* calls through a NULL simulate.
*
* Properties first, because akgl_physics_init_arcade reads them.
*/
akerr_ErrorContext AKERR_NOIGNORE *bring_up_physics(void)
{
PREPARE_ERROR(errctx);
PASS(errctx, akgl_set_property("physics.gravity.y", "900.0"));
/* There is no terminal-velocity setting. Drag is the mechanism: ey
* approaches gravity_y / drag_y, so 900 / 1.8 caps the fall at 500 px/s. */
PASS(errctx, akgl_set_property("physics.drag.y", "1.8"));
PASS(errctx, akgl_physics_init_arcade(akgl_physics));
SUCCEED_RETURN(errctx);
}
```
`akgl_physics_init_arcade` reads seven properties — `physics.gravity.x`/`.y`/`.z`,
`physics.drag.x`/`.y`/`.z` and `physics.max_timestep` — all with defaults, so an arcade
backend with no configuration behaves like the null one until something is set. **Set the
properties first.** With `AKGL_REGISTRY_PROPERTIES` uninitialized every read comes back as
its default and this silently configures zero gravity and zero drag rather than reporting
anything; see [Chapter 6](06-the-registry.md).
The gravity signs are chosen for screen space: x pulls toward screen left, y pulls down
because y grows downward, z pulls away from the camera.
## `AKGL_ERR_LOGICINTERRUPT`
The one status in libakgl that is not a failure. Raised from an actor's
`movementlogicfunc`, it means **"skip the rest of this tick for me"** — no gravity, no drag,
no move for that actor this step — and `akgl_physics_simulate` handles it and carries on to
the next actor. It is a control signal wearing an error's clothes.
**Only `movementlogicfunc` gets that treatment.** The `gravity` and `move` calls return
straight out of the loop on any status, `AKGL_ERR_LOGICINTERRUPT` included, so a backend
must never use it to opt an actor out from there — there it aborts the whole step, leaving
the actors already processed advanced and the rest not.
```c
#include <akgl/actor.h>
#include <akgl/error.h>
#include <akgl/physics.h>
/*
* A movement logic function that pins an actor to a floor at y = 400.
*
* With no collision world attached the backend consults nothing, so standing
* on something is the caller's job. Doing it here rather than after the step is
* what keeps the actor from being drawn inside the floor for one frame. A game
* that attaches a world (Chapter 15) deletes this function instead of writing
* it.
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
*/
akerr_ErrorContext AKERR_NOIGNORE *stand_on_floor(akgl_Actor *obj, float32_t dt)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
/* The default logic still has to run: it is what copies the character's
* speeds and accelerations onto the actor. */
PASS(errctx, akgl_actor_logic_movement(obj, dt));
if ( obj->y >= 400.0f ) {
obj->y = 400.0f;
/* ey is where gravity accumulates. Zero it, or the actor keeps
* "falling" into the floor and cannot jump out of it. */
obj->ey = 0.0f;
}
SUCCEED_RETURN(errctx);
}
/* Raising this from movement logic means "skip the rest of this tick for me".
* akgl_physics_simulate handles it and moves on to the next actor; the actor
* gets no gravity, no drag and no move this step. */
akerr_ErrorContext AKERR_NOIGNORE *frozen_in_cutscene(akgl_Actor *obj, float32_t dt)
{
PREPARE_ERROR(errctx);
(void)dt;
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
FAIL_RETURN(errctx, AKGL_ERR_LOGICINTERRUPT, "cutscene");
}
```
## What is not implemented
Stated plainly, because all of it is reachable by an ordinary-looking call and none of it
announces itself at compile time.
**Collision is [Chapter 15](15-collision.md), and it is opt-in.** A backend with no
collision world attached does exactly what it did before collision existed: `move` is
`position += velocity * dt` and consults nothing. Attach a world and the step resolves after
every sub-move.
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
What remains missing here is the *feel*, below, and that is a different list from the one
this section used to carry.
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
### Four gaps in the feel
Found by `tests/physics_sim.c`, which runs the arcade backend the way a game does — a
Mario-esque jump and fall, Zelda-style top-down walking, and a run reversed at full speed —
and prints what the actor actually did. Three other defects that suite found *are* fixed in
0.6.0. These four are not. All are in `TODO.md`, "Arcade physics feel".
| Gap | What you see | The workaround today |
|---|---|---|
| **No terminal velocity** | Gravity adds `gravity_y * dt` to `ey` every step and nothing bounds it. The jump simulation reaches 560 px/s in 0.7 s and would keep going | `physics.drag.y`. It makes `ey` approach `gravity_y / drag_y` instead of diverging. That is the only brake, and it is not documented as one anywhere else |
| **Releasing a direction stops the actor dead** | `akgl_actor_cmhf_*_off` sets `tx` (or `ty`) to zero, so a character at full speed stops within one frame — 0.0 px of drift measured in the second after release | Correct for Zelda, wrong for Mario. There is no friction or deceleration anywhere in the backend; `ax` is the only rate and it applies only while a direction is held. Decay `tx` yourself in `movementlogicfunc` |
| **Explicit Euler, so trajectories are frame-rate dependent** | The same jump traces a slightly different arc at 30 Hz than at 144 Hz. The reversal measures 0.500 s to turn around where the closed form predicts 0.489 s — about 2% at 60 Hz, growing with the step | Nothing. 2% is not a bug a player can see. It is recorded because the fix (a fixed-step accumulator) is the same piece of work as removing the slow-motion trade `max_timestep` makes |
| **A large drag coefficient inverts velocity** | `ex -= ex * drag_x * dt` is a first-order decay, so it only decays for `drag * dt < 1`. Past that it overshoots zero; past `2` it diverges. **Nothing rejects it** | With `dt` bounded to the default 0.05 s that needs a drag above 20, which is not a plausible setting. But `max_timestep` is caller-settable, so keep `drag * max_timestep` below 1 |
The `_off` handlers used to be worse: they zeroed `ay`, `ey`, `ty` and `vy` together, and
`ey` is where gravity accumulates, so tapping down mid-jump stopped the character in the
air. They clear `ax`/`tx` (or `ay`/`ty`) and nothing else now. Velocity was never theirs to
clear either — `simulate` recomputes `v` as `e + t` every step.
## Statuses
`akgl_physics_simulate` propagates whatever an actor's `movementlogicfunc`, or the
backend's `gravity`, `move` or `collide`, raises, and **the first failure aborts the whole
step** —
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
the actors already processed are advanced and the rest are not. The status meanings are in
[Chapter 4](04-errors.md); the ones you will meet here are `AKERR_NULLPOINTER` (a `NULL`
`self` or `self->move`), `AKERR_VALUE` and `ERANGE` (a
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
`physics.*` property that is not a number, or does not fit a `double`), and
`AKGL_ERR_LOGICINTERRUPT`, which is not a failure.