Document collision, and correct what it made false elsewhere
The new chapter 15 covers the opt-in setup, where resolution runs in the step and why it runs after the move, shapes and the z-extrusion trap, the asymmetric masks and the defaults that matter more than the mechanism, tiles as a source rather than proxies, the response hook and the three ways it is easy to write wrongly, sensors, the four queries, both partitioners, and what is still not implemented. Collision falsified claims in eight other chapters. The physics chapter said "There is no collision detection, at all" and that collide always raises AKERR_API; actors had six behaviour hooks; the heap had five pools and four non-claiming acquires; the status band held five codes; and the design philosophy had exactly two pluggable subsystems. The appendix gains a collision.h status section, the three new pool ceilings and a table of the collision constants that are deliberately not in a public header. Also fixes chapter labels the renumbering commit left pointing at their old numbers -- "18. Utilities" linked to 19-utilities.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
This commit is contained in:
@@ -65,10 +65,11 @@ hit it, and an entry in `TODO.md`.
|
|||||||
|
|
||||||
| Gap | Behaviour today | Chapter |
|
| Gap | Behaviour today | Chapter |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Collision response | `akgl_physics_arcade_collide` raises `AKERR_API`, and `akgl_physics_simulate` never calls `collide` at all. An actor walks through a wall | [14](14-physics.md) |
|
|
||||||
| Terminal velocity | Gravity accumulates into `ey` unbounded. `physics.drag.y` is the only brake | [14](14-physics.md) |
|
| Terminal velocity | Gravity accumulates into `ey` unbounded. `physics.drag.y` is the only brake | [14](14-physics.md) |
|
||||||
| Friction / deceleration | Releasing a direction zeroes thrust and stops the actor dead. Right for Zelda, wrong for Mario | [14](14-physics.md) |
|
| Friction / deceleration | Releasing a direction zeroes thrust and stops the actor dead. Right for Zelda, wrong for Mario | [14](14-physics.md) |
|
||||||
| Savegames | `akgl_game_save` writes the name tables but not the objects, so a file is not yet enough to restore a session | [07](07-the-game-and-the-frame.md) |
|
| Savegames | `akgl_game_save` writes the name tables but not the objects, so a file is not yet enough to restore a session | [07](07-the-game-and-the-frame.md) |
|
||||||
|
| Rotation | No actor carries an angle, and every collision shape is axis-aligned | [15](15-collision.md) |
|
||||||
|
| Continuous collision | Sub-stepping is capped, so something moving faster than about 1280 px/s on 16-pixel tiles can still pass through a wall | [15](15-collision.md) |
|
||||||
| Mesh drawing | `akgl_render_2d_draw_mesh` raises `AKERR_API`; the hook is reserved for a 3D backend | [08](08-rendering.md) |
|
| Mesh drawing | `akgl_render_2d_draw_mesh` raises `AKERR_API`; the hook is reserved for a 3D backend | [08](08-rendering.md) |
|
||||||
| A text cache | Every `akgl_text_rendertextat` rasterizes, uploads, blits and destroys | [16](17-text-and-fonts.md) |
|
| A text cache | Every `akgl_text_rendertextat` rasterizes, uploads, blits and destroys | [16](17-text-and-fonts.md) |
|
||||||
|
|
||||||
|
|||||||
@@ -95,29 +95,30 @@ akerr_ErrorContext *scratch_path(char *root, char *name)
|
|||||||
**If the pool has no layer for your type, add one.** A new kind of runtime object gets a new
|
**If the pool has no layer for your type, add one.** A new kind of runtime object gets a new
|
||||||
array, a `next`, and a `release`, in `heap.h` and `src/heap.c`. It does not get an
|
array, a `next`, and a `release`, in `heap.h` and `src/heap.c`. It does not get an
|
||||||
allocator. [Chapter 5](05-the-heap.md) covers the layers, the reference-count asymmetry
|
allocator. [Chapter 5](05-the-heap.md) covers the layers, the reference-count asymmetry
|
||||||
between `akgl_heap_next_string` and the other four, and what the ceilings cost you.
|
between `akgl_heap_next_string` and the other seven, and what the ceilings cost you.
|
||||||
|
|
||||||
## Variation lives in a backend, not in a branch
|
## Variation lives in a backend, not in a branch
|
||||||
|
|
||||||
There are exactly two pluggable subsystems, and both have the same shape: **a struct of
|
There are exactly three pluggable subsystems, and all have the same shape: **a struct of
|
||||||
function pointers, plus an initializer that populates it.**
|
function pointers, plus an initializer that populates it.**
|
||||||
|
|
||||||
```text
|
```text
|
||||||
akgl_RenderBackend akgl_PhysicsBackend
|
akgl_RenderBackend akgl_PhysicsBackend akgl_Partitioner
|
||||||
+--------------------+ +--------------------+
|
+--------------------+ +--------------------+ +-----------------+
|
||||||
| sdl_renderer | | simulate |
|
| sdl_renderer | | simulate | | reset |
|
||||||
| shutdown | | gravity |
|
| shutdown | | gravity | | insert |
|
||||||
| frame_start | | collide |
|
| frame_start | | collide | | remove |
|
||||||
| frame_end | | move |
|
| frame_end | | move | | move |
|
||||||
| draw_texture | | drag_x/y/z |
|
| draw_texture | | drag_x/y/z | | query |
|
||||||
| draw_mesh | | gravity_x/y/z |
|
| draw_mesh | | gravity_x/y/z | | each_pair |
|
||||||
| draw_world | | max_timestep |
|
| draw_world | | max_timestep | | state |
|
||||||
+--------------------+ +--------------------+
|
+--------------------+ | collision | +-----------------+
|
||||||
^ ^
|
^ +--------------------+ ^
|
||||||
| populated by | populated by
|
| populated by ^ | populated by
|
||||||
akgl_render_2d_bind akgl_physics_init_null
|
akgl_render_2d_bind | populated by akgl_partitioner_init_grid
|
||||||
akgl_render_2d_init akgl_physics_init_arcade
|
akgl_render_2d_init akgl_physics_init_null akgl_partitioner_init_bsp
|
||||||
(chosen by akgl_physics_factory)
|
akgl_physics_init_arcade (akgl_partitioner_factory)
|
||||||
|
(akgl_physics_factory)
|
||||||
```
|
```
|
||||||
|
|
||||||
The shipped physics initializers are five assignments and nothing else:
|
The shipped physics initializers are five assignments and nothing else:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ The house rules for *writing* code against the protocol — never a `*_RETURN` i
|
|||||||
|
|
||||||
libakerror reserves statuses 0–255 for the host's `errno` values and its own `AKERR_*`
|
libakerror reserves statuses 0–255 for the host's `errno` values and its own `AKERR_*`
|
||||||
codes; consumers allocate upward from `AKERR_FIRST_CONSUMER_STATUS`, which is **256**.
|
codes; consumers allocate upward from `AKERR_FIRST_CONSUMER_STATUS`, which is **256**.
|
||||||
libakgl claims a band of five starting there, under the owner string `"libakgl"`:
|
libakgl claims a band of six starting there, under the owner string `"libakgl"`:
|
||||||
|
|
||||||
```c excerpt=include/akgl/error.h
|
```c excerpt=include/akgl/error.h
|
||||||
#define AKGL_ERR_OWNER "libakgl"
|
#define AKGL_ERR_OWNER "libakgl"
|
||||||
@@ -36,12 +36,13 @@ libakgl claims a band of five starting there, under the owner string `"libakgl"`
|
|||||||
#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /**< A heap pool has no free object left to hand out */
|
#define AKGL_ERR_HEAP (AKGL_ERR_BASE + 2) /**< A heap pool has no free object left to hand out */
|
||||||
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
|
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
|
||||||
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
|
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
|
||||||
|
#define AKGL_ERR_COLLISION (AKGL_ERR_BASE + 5) /**< A collision query could not be answered; the message says why */
|
||||||
```
|
```
|
||||||
|
|
||||||
**`AKGL_ERR_LIMIT` is not a status code.** It is `AKGL_ERR_BASE + 5`, one past the last
|
**`AKGL_ERR_LIMIT` is not a status code.** It is `AKGL_ERR_BASE + 6`, one past the last
|
||||||
one, and exists only so `AKGL_ERR_COUNT` can be computed from it. Nothing in `src/` raises
|
one, and exists only so `AKGL_ERR_COUNT` can be computed from it. Nothing in `src/` raises
|
||||||
it and `akgl_error_init` does not name it. Do not write a `HANDLE(e, AKGL_ERR_LIMIT)`
|
it and `akgl_error_init` does not name it. Do not write a `HANDLE(e, AKGL_ERR_LIMIT)`
|
||||||
arm — it would catch nothing, and if a sixth real code is ever added it would silently
|
arm — it would catch nothing, and if a seventh real code is ever added it would silently
|
||||||
start catching that instead.
|
start catching that instead.
|
||||||
|
|
||||||
| Code | Value | Means | Raised by | What the caller does |
|
| Code | Value | Means | Raised by | What the caller does |
|
||||||
@@ -51,10 +52,11 @@ start catching that instead.
|
|||||||
| `AKGL_ERR_HEAP` | 258 | A pool has no free slot | every `akgl_heap_next_*` | **Normally a missing release, not a small pool.** See [Chapter 5](05-the-heap.md) before raising `AKGL_MAX_HEAP_*` |
|
| `AKGL_ERR_HEAP` | 258 | A pool has no free slot | every `akgl_heap_next_*` | **Normally a missing release, not a small pool.** See [Chapter 5](05-the-heap.md) before raising `AKGL_MAX_HEAP_*` |
|
||||||
| `AKGL_ERR_BEHAVIOR` | 259 | A component did not behave the way its contract requires | **Nothing in the library.** Only `tests/` raises it, through `testutil.h` | Available to you for the same purpose: asserting a contract in your own code |
|
| `AKGL_ERR_BEHAVIOR` | 259 | A component did not behave the way its contract requires | **Nothing in the library.** Only `tests/` raises it, through `testutil.h` | Available to you for the same purpose: asserting a contract in your own code |
|
||||||
| `AKGL_ERR_LOGICINTERRUPT` | 260 | **Not a failure — a control signal.** "Skip the rest of this step for this actor" | your own `movementlogicfunc` | Raise it deliberately. See the note below |
|
| `AKGL_ERR_LOGICINTERRUPT` | 260 | **Not a failure — a control signal.** "Skip the rest of this step for this actor" | your own `movementlogicfunc` | Raise it deliberately. See the note below |
|
||||||
|
| `AKGL_ERR_COLLISION` | 261 | A collision query could not be answered | **One cause only**: `akgl_collision_test`, when the ccd arena is exhausted. The message carries the high-water mark | Raise `AKGL_CCD_ARENA_BYTES` to the reported figure. See [Chapter 15](15-collision.md) |
|
||||||
|
|
||||||
`akgl_error_init` reserves the band all-or-nothing and registers a name for each of the
|
`akgl_error_init` reserves the band all-or-nothing and registers a name for each of the
|
||||||
five: `"SDL Error"`, `"Registry Error"`, `"Heap Error"`, `"Behavior Error"`,
|
six: `"SDL Error"`, `"Registry Error"`, `"Heap Error"`, `"Behavior Error"`,
|
||||||
`"Logic Interrupt"`. Those names are what a stack trace prints.
|
`"Logic Interrupt"`, `"Collision Error"`. Those names are what a stack trace prints.
|
||||||
|
|
||||||
Two rows deserve more than a cell.
|
Two rows deserve more than a cell.
|
||||||
|
|
||||||
@@ -87,7 +89,7 @@ grepping every `FAIL_*` and `HANDLE*` site in `src/`, not by reading header pros
|
|||||||
| `AKERR_OUTOFBOUNDS` | A value did not fit its destination, or an index ran off the end. `aksl_strncpy` truncation, an array index past the end of a JSON array (`akgl_get_json_array_index_*`), a ninth child on an actor, a tile past `AKGL_TILEMAP_MAX_*`, a control map id out of range |
|
| `AKERR_OUTOFBOUNDS` | A value did not fit its destination, or an index ran off the end. `aksl_strncpy` truncation, an array index past the end of a JSON array (`akgl_get_json_array_index_*`), a ninth child on an actor, a tile past `AKGL_TILEMAP_MAX_*`, a control map id out of range |
|
||||||
| `AKERR_VALUE` | A value parsed but was not usable: a version string that is not semver (`akgl_game_load_versioncmp`), two surfaces that differ (`akgl_compare_sdl_surfaces`), a zero-sized destination or an overlapping copy from the `aksl_` wrappers |
|
| `AKERR_VALUE` | A value parsed but was not usable: a version string that is not semver (`akgl_game_load_versioncmp`), two surfaces that differ (`akgl_compare_sdl_surfaces`), a zero-sized destination or an overlapping copy from the `aksl_` wrappers |
|
||||||
| `AKERR_RELATIONSHIP` | **One site**: `akgl_actor_add_child`, when the child already has a parent. Detach it first |
|
| `AKERR_RELATIONSHIP` | **One site**: `akgl_actor_add_child`, when the child already has a parent. Detach it first |
|
||||||
| `AKERR_API` | Two distinct meanings, so read the message. (1) **The function is not implemented** — `akgl_physics_arcade_collide` and `akgl_render_2d_draw_mesh` both `FAIL_RETURN(..., AKERR_API, "Not implemented")`, and both are reached by ordinary-looking calls. (2) **A savegame does not match this build** — `akgl_game_load` and `akgl_game_load_versioncmp` |
|
| `AKERR_API` | Two distinct meanings, so read the message. (1) **The function is not implemented** — `akgl_render_2d_draw_mesh` does `FAIL_RETURN(..., AKERR_API, "Not implemented")`, and it is reached by an ordinary-looking call. (2) **A savegame does not match this build** — `akgl_game_load` and `akgl_game_load_versioncmp` |
|
||||||
| `AKERR_IO` | A read or write failed, or a savegame had trailing data after its name tables. Distinct from `AKERR_EOF` — that separation is the whole reason `aksl_fgetc` exists |
|
| `AKERR_IO` | A read or write failed, or a savegame had trailing data after its name tables. Distinct from `AKERR_EOF` — that separation is the whole reason `aksl_fgetc` exists |
|
||||||
| `AKERR_EOF` | End of input. Sometimes the *desired* outcome: `require_at_eof` in `src/game.c` handles it and treats anything else as a failure |
|
| `AKERR_EOF` | End of input. Sometimes the *desired* outcome: `require_at_eof` in `src/game.c` handles it and treats anything else as a failure |
|
||||||
| `AKERR_INDEX` | **libakgl never raises this.** It appears once, as a `HANDLE_GROUP` arm in `akgl_get_json_with_default`, so that a caller whose *own* code raises it still gets a default. An out-of-range JSON array index arrives as `AKERR_OUTOFBOUNDS` |
|
| `AKERR_INDEX` | **libakgl never raises this.** It appears once, as a `HANDLE_GROUP` arm in `akgl_get_json_with_default`, so that a caller whose *own* code raises it still gets a default. An out-of-range JSON array index arrives as `AKERR_OUTOFBOUNDS` |
|
||||||
@@ -122,7 +124,7 @@ status yourself. If you do either, call `akerr_exit()`.
|
|||||||
|
|
||||||
### `akgl_error_init()` must run before anything that can raise
|
### `akgl_error_init()` must run before anything that can raise
|
||||||
|
|
||||||
`akgl_error_init` claims the status band *and* registers the five names. A code raised
|
`akgl_error_init` claims the status band *and* registers the six names. A code raised
|
||||||
before it runs has no name in the registry, so every stack trace carrying it prints
|
before it runs has no name in the registry, so every stack trace carrying it prints
|
||||||
**"Unknown Error"** instead of "Heap Error" — and the status number, 258, means nothing to
|
**"Unknown Error"** instead of "Heap Error" — and the status number, 258, means nothing to
|
||||||
a reader.
|
a reader.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ slot whose `refcount` is zero.
|
|||||||
out-of-memory catastrophe.** `AKGL_ERR_HEAP` is a status you can handle. It is also,
|
out-of-memory catastrophe.** `AKGL_ERR_HEAP` is a status you can handle. It is also,
|
||||||
almost always, a missing release rather than a pool that is genuinely too small.
|
almost always, a missing release rather than a pool that is genuinely too small.
|
||||||
|
|
||||||
## The five pools
|
## The eight pools
|
||||||
|
|
||||||
A "heap layer" is one array plus its `next`/`release` pair. There are five:
|
A "heap layer" is one array plus its `next`/`release` pair. There are five:
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ rather than a design (`TODO.md`, "Known and still open" item 8; also the `@warni
|
|||||||
| `akgl_heap_next_character` | No | `akgl_character_initialize` |
|
| `akgl_heap_next_character` | No | `akgl_character_initialize` |
|
||||||
|
|
||||||
**Until a reference is taken, the slot is still free and the next acquire hands out the
|
**Until a reference is taken, the slot is still free and the next acquire hands out the
|
||||||
same pointer.** Four of the five acquires therefore leave a window in which two callers can
|
same pointer.** Seven of the eight acquires therefore leave a window in which two callers can
|
||||||
be holding the same object, and the window closes only when `*_initialize` runs. In
|
be holding the same object, and the window closes only when `*_initialize` runs. In
|
||||||
practice you never see it, because the four are always immediately followed by their
|
practice you never see it, because the four are always immediately followed by their
|
||||||
initializer — which is exactly why the asymmetry has survived. Write them adjacent and
|
initializer — which is exactly why the asymmetry has survived. Write them adjacent and
|
||||||
@@ -316,7 +316,7 @@ genuinely needs 256 actors should say so once, for the whole build, per the ABI
|
|||||||
|
|
||||||
## Initialization and reset
|
## Initialization and reset
|
||||||
|
|
||||||
`akgl_heap_init` zeroes all five pools. `akgl_game_init` calls it for you, before any
|
`akgl_heap_init` zeroes all eight pools. `akgl_game_init` calls it for you, before any
|
||||||
registry exists.
|
registry exists.
|
||||||
|
|
||||||
**Calling it again is a reset, not a refresh.** It does not release textures, clear
|
**Calling it again is a reset, not a refresh.** It does not release textures, clear
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ Verified against `src/game.c`. The order matters in three places, marked:
|
|||||||
5. Create the state mutex. Failure is `AKGL_ERR_SDL`.
|
5. Create the state mutex. Failure is `AKGL_ERR_SDL`.
|
||||||
6. **Take the state lock.** Everything from here to the end runs holding it.
|
6. **Take the state lock.** Everything from here to the end runs holding it.
|
||||||
7. Check `name`, `version` and `uri` are non-empty.
|
7. Check `name`, `version` and `uri` are non-empty.
|
||||||
8. `akgl_heap_init()` — zero all five pools.
|
8. `akgl_heap_init()` — zero all eight pools.
|
||||||
9. The eight registry initializers: actor, sprite, spritesheet, character, font, music,
|
9. The eight registry initializers: actor, sprite, spritesheet, character, font, music,
|
||||||
properties, actor-state-strings. **Note that `akgl_registry_init` is not called** —
|
properties, actor-state-strings. **Note that `akgl_registry_init` is not called** —
|
||||||
`akgl_game_init` calls the eight individually. See [Chapter 6](06-the-registry.md).
|
`akgl_game_init` calls the eight individually. See [Chapter 6](06-the-registry.md).
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ the `&`, parsing as `!(a & b) == b`. Nothing in the tree negated it, which is th
|
|||||||
only reason it was latent rather than live — and the test that distinguishes 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.
|
two parses is a bit that is *not* set whose value is *not* 1, not the obvious one.
|
||||||
|
|
||||||
## The six behaviour hooks
|
## The seven behaviour hooks
|
||||||
|
|
||||||
**Behaviour attaches as function pointers, not by inheritance.** There is no actor
|
**Behaviour attaches as function pointers, not by inheritance.** There is no actor
|
||||||
subclass. `akgl_actor_initialize` installs the library's defaults on every actor,
|
subclass. `akgl_actor_initialize` installs the library's defaults on every actor,
|
||||||
@@ -134,8 +134,9 @@ one actor**.
|
|||||||
| `movementlogicfunc` | `akgl_actor_logic_movement` | `akgl_physics_simulate`, before gravity | turn movement bits into signed acceleration |
|
| `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 |
|
| `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 |
|
| `addchild` | `akgl_actor_add_child` | you | attach a child actor |
|
||||||
|
| `collidefunc` | `akgl_actor_collide_block` | `akgl_collision_resolve`, after the move | answer a contact — see [Chapter 15](15-collision.md) |
|
||||||
|
|
||||||
Replace them after `akgl_actor_initialize`, never before — it overwrites all six.
|
Replace them after `akgl_actor_initialize`, never before — it overwrites all seven.
|
||||||
|
|
||||||
The `movementlogicfunc` slot is the interesting one, because it is where
|
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
|
`AKGL_ERR_LOGICINTERRUPT` stops being a table row in [Chapter 04](04-errors.md) and
|
||||||
|
|||||||
@@ -278,10 +278,11 @@ the actors already processed advanced and the rest not.
|
|||||||
/*
|
/*
|
||||||
* A movement logic function that pins an actor to a floor at y = 400.
|
* A movement logic function that pins an actor to a floor at y = 400.
|
||||||
*
|
*
|
||||||
* The arcade backend never calls collide and never consults the tilemap, so
|
* With no collision world attached the backend consults nothing, so standing
|
||||||
* standing on something is the caller's job. Doing it here rather than after
|
* on something is the caller's job. Doing it here rather than after the step is
|
||||||
* the step is what keeps the actor from being drawn inside the floor for one
|
* what keeps the actor from being drawn inside the floor for one frame. A game
|
||||||
* frame.
|
* that attaches a world (Chapter 15) deletes this function instead of writing
|
||||||
|
* it.
|
||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *stand_on_floor(akgl_Actor *obj, float32_t dt)
|
akerr_ErrorContext AKERR_NOIGNORE *stand_on_floor(akgl_Actor *obj, float32_t dt)
|
||||||
{
|
{
|
||||||
@@ -318,23 +319,13 @@ akerr_ErrorContext AKERR_NOIGNORE *frozen_in_cutscene(akgl_Actor *obj, float32_t
|
|||||||
Stated plainly, because all of it is reachable by an ordinary-looking call and none of it
|
Stated plainly, because all of it is reachable by an ordinary-looking call and none of it
|
||||||
announces itself at compile time.
|
announces itself at compile time.
|
||||||
|
|
||||||
**There is no collision detection, at all.**
|
**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.
|
||||||
|
|
||||||
- `akgl_physics_arcade_collide` always raises `AKERR_API` with the message "Not
|
What remains missing here is the *feel*, below, and that is a different list from the one
|
||||||
implemented". It is deliberately loud rather than silently permissive.
|
this section used to carry.
|
||||||
- **`akgl_physics_simulate` never calls `collide`.** Not for the arcade backend, not for
|
|
||||||
the null one, not ever. The vtable slot exists and the simulation does not use it.
|
|
||||||
- `akgl_physics_arcade_move` does `position += velocity * dt` and nothing else. It does not
|
|
||||||
clamp to the map, consult the tilemap, or test anything. **An actor walks straight
|
|
||||||
through a wall and off the edge of the world.**
|
|
||||||
|
|
||||||
`akgl_physics_null_collide` *succeeds*, and that is not an oversight: the null backend's
|
|
||||||
contract is "nothing collides", which is an answer. The arcade one means "nobody has
|
|
||||||
written this yet", and a caller who reaches it should find out.
|
|
||||||
|
|
||||||
Until it exists, collision belongs in your `movementlogicfunc`, as in the example above.
|
|
||||||
The sidescroller tutorial in [Chapter 20](20-tutorial-sidescroller.md) does exactly that and
|
|
||||||
says why. `akgl_collide_*` in [Chapter 19](19-utilities.md) gives you the rectangle tests.
|
|
||||||
|
|
||||||
### Four gaps in the feel
|
### Four gaps in the feel
|
||||||
|
|
||||||
@@ -358,9 +349,10 @@ clear either — `simulate` recomputes `v` as `e + t` every step.
|
|||||||
## Statuses
|
## Statuses
|
||||||
|
|
||||||
`akgl_physics_simulate` propagates whatever an actor's `movementlogicfunc`, or the
|
`akgl_physics_simulate` propagates whatever an actor's `movementlogicfunc`, or the
|
||||||
backend's `gravity` or `move`, raises, and **the first failure aborts the whole step** —
|
backend's `gravity`, `move` or `collide`, raises, and **the first failure aborts the whole
|
||||||
|
step** —
|
||||||
the actors already processed are advanced and the rest are not. The status meanings are in
|
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`
|
[Chapter 4](04-errors.md); the ones you will meet here are `AKERR_NULLPOINTER` (a `NULL`
|
||||||
`self` or `self->move`), `AKERR_API` (`collide`, always), `AKERR_VALUE` and `ERANGE` (a
|
`self` or `self->move`), `AKERR_VALUE` and `ERANGE` (a
|
||||||
`physics.*` property that is not a number, or does not fit a `double`), and
|
`physics.*` property that is not a number, or does not fit a `double`), and
|
||||||
`AKGL_ERR_LOGICINTERRUPT`, which is not a failure.
|
`AKGL_ERR_LOGICINTERRUPT`, which is not a failure.
|
||||||
|
|||||||
248
docs/15-collision.md
Normal file
248
docs/15-collision.md
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
# 15. Collision
|
||||||
|
|
||||||
|
Collision is **opt-in**. A physics backend with no collision world attached behaves exactly
|
||||||
|
as one did before any of this existed and costs one comparison per actor per step, so a game
|
||||||
|
that does not want it pays nothing and a game written before it existed keeps working.
|
||||||
|
|
||||||
|
Turning it on is three things: a world, a shape on anything that should collide, and — for
|
||||||
|
map geometry — a `collidable` property on the tile layers that are solid.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <akgl/collision.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/physics.h>
|
||||||
|
|
||||||
|
static akgl_CollisionWorld world;
|
||||||
|
|
||||||
|
/* After the map is loaded and the physics backend is initialized. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *turn_collision_on(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, akgl_collision_world_init(&world, NULL, 16.0f, 16.0f));
|
||||||
|
PASS(errctx, akgl_collision_bind_tilemap(&world, akgl_gamemap));
|
||||||
|
akgl_physics->collision = &world;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`NULL` for the partitioner name means the default, which is the one to use. Binding a map
|
||||||
|
takes the cell size from its tiles, so the two arguments above are overwritten immediately
|
||||||
|
— they matter only for a world with no map.
|
||||||
|
|
||||||
|
## Where it runs in the step
|
||||||
|
|
||||||
|
Collision resolves **after** the move, not before it.
|
||||||
|
|
||||||
|
```text
|
||||||
|
movementlogicfunc
|
||||||
|
v
|
||||||
|
gravity
|
||||||
|
v
|
||||||
|
drag
|
||||||
|
v
|
||||||
|
v = e + t
|
||||||
|
v
|
||||||
|
+--------------------------------+
|
||||||
|
| move(subdt) | x substeps
|
||||||
|
| resolve(subdt) |
|
||||||
|
+--------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
That ordering is the whole reason a game can stop duplicating the integrator. A resolver
|
||||||
|
called from `movementlogicfunc` runs before gravity, drag and the move, so it has to
|
||||||
|
*predict* where the actor will end up — which means re-implementing the arithmetic above, in
|
||||||
|
the game, and being wrong whenever that arithmetic changes.
|
||||||
|
|
||||||
|
**Only `move` is subdivided.** Gravity, drag, the thrust integration and the speed ellipse
|
||||||
|
all still run once against the whole `dt`, and the sub-steps sum to `dt`. An actor with
|
||||||
|
nothing to collide with takes exactly one sub-step of exactly `dt`.
|
||||||
|
|
||||||
|
## Shapes
|
||||||
|
|
||||||
|
A shape is a convex volume positioned relative to an actor's origin. It lives on the
|
||||||
|
[character](11-characters.md), so every goblin sharing a character shares one definition —
|
||||||
|
and an individual actor can override it.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/collision.h>
|
||||||
|
|
||||||
|
/* A hitbox inset into a 32x32 sprite frame, because art does not reach the
|
||||||
|
* edges of its cell and a full-frame box catches on doorways the character
|
||||||
|
* visibly clears. */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *give_player_a_body(akgl_Actor *player)
|
||||||
|
{
|
||||||
|
SDL_FRect body = { .x = 8.0f, .y = 0.0f, .w = 16.0f, .h = 32.0f };
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
|
||||||
|
|
||||||
|
PASS(errctx, akgl_collision_shape_box(&player->shape, &body, 0.0f));
|
||||||
|
player->shape_override = true;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`shape_override` is not redundant with an empty shape. "This actor deliberately has no
|
||||||
|
collider" and "this actor has not been configured yet" are different states, and without
|
||||||
|
somewhere to record the difference `akgl_actor_set_character` cannot tell whether it is
|
||||||
|
allowed to overwrite what it finds.
|
||||||
|
|
||||||
|
Three kinds: box, circle, and a capsule on either axis. A box against a box is answered in
|
||||||
|
closed form, which is both cheaper and *exact* — an actor resting on a floor gets a normal of
|
||||||
|
precisely `(0, -1, 0)` rather than one converged to a tolerance, and the difference between
|
||||||
|
those accumulates into a slow sideways creep.
|
||||||
|
|
||||||
|
### Why a 2D shape has a depth
|
||||||
|
|
||||||
|
The narrowphase is three-dimensional, which is what lets these shapes carry into a 3D game
|
||||||
|
later. It answers with the **minimum** translation that separates two volumes — and if a 2D
|
||||||
|
shape were extruded into a thin slab, the cheapest way to separate two of them would be
|
||||||
|
along z. The narrowphase would report a contact, the resolver would push the actor into the
|
||||||
|
screen, and **on screen nothing would happen at all while the actor sank through the floor.**
|
||||||
|
|
||||||
|
So the setters give every shape a depth of `AKGL_COLLISION_DEPTH_RATIO` times its largest
|
||||||
|
planar half-extent unless told otherwise. That is not a comfortable-looking number; it is the
|
||||||
|
smallest ratio for which the z overlap of any two shapes built this way provably exceeds any
|
||||||
|
planar penetration they can reach. Pass a depth by hand only if you know what that costs, and
|
||||||
|
leave `AKGL_COLLISION_TEST_PLANAR` on if you do.
|
||||||
|
|
||||||
|
## Masks: what collides with what
|
||||||
|
|
||||||
|
Every shape carries two words, and they are asymmetric on purpose.
|
||||||
|
|
||||||
|
| Field | Means |
|
||||||
|
|---|---|
|
||||||
|
| `layermask` | which layers this shape *is on* — what others test against |
|
||||||
|
| `collidemask` | which layers this shape *responds to* |
|
||||||
|
|
||||||
|
`a` is resolved against `b` when `b->layermask & a->collidemask`. The reverse is a separate
|
||||||
|
question with its own answer, which is how a player blocks against a pushable crate while the
|
||||||
|
crate ignores everything, and how a bullet collides with a wall while the wall ignores the
|
||||||
|
bullet.
|
||||||
|
|
||||||
|
**The defaults matter more than the mechanism.** A new shape is on
|
||||||
|
`AKGL_COLLISION_LAYER_ACTOR` and responds to `AKGL_COLLISION_LAYER_STATIC` — so **an actor
|
||||||
|
given a shape and nothing else collides with map geometry and with no other actor.**
|
||||||
|
"Everything with a hitbox shoves everything else" is a surprising default for a town full of
|
||||||
|
scenery and a painful one to discover after the fact. Opting in to actor-versus-actor is one
|
||||||
|
added bit:
|
||||||
|
|
||||||
|
```c norun
|
||||||
|
player->shape.collidemask |= AKGL_COLLISION_LAYER_ENEMY | AKGL_COLLISION_LAYER_PICKUP;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tiles
|
||||||
|
|
||||||
|
A tile layer is solid when it carries a `collidable` boolean custom property in Tiled. The
|
||||||
|
map declares what is solid; before this a game had to hard-code a layer index, and
|
||||||
|
`akgl_TilemapLayer` does not retain the name Tiled wrote, so that index broke the moment
|
||||||
|
somebody reordered layers in the editor.
|
||||||
|
|
||||||
|
Solid tiles are **not** given proxies. At the maximum map size that would be a quarter of a
|
||||||
|
million per layer — tens of megabytes of index describing data that is already a dense grid
|
||||||
|
sitting in the tilemap. The world keeps a borrowed pointer and reads `layers[i].data[]`
|
||||||
|
directly over whatever cell range a query covers: nine array reads for a 32-pixel actor on
|
||||||
|
16-pixel tiles, nothing built at level load, nothing maintained per frame.
|
||||||
|
|
||||||
|
Static geometry that is *not* tile-aligned — a slope, a Tiled object rectangle, a platform
|
||||||
|
that only moves between levels — is still an ordinary proxy carrying
|
||||||
|
`AKGL_COLLISION_FLAG_STATIC`. Both mechanisms exist; tiles use the free one because there are
|
||||||
|
a hundred thousand of them.
|
||||||
|
|
||||||
|
## Responding to a contact
|
||||||
|
|
||||||
|
Every actor gets a seventh behaviour hook, `collidefunc`, defaulting to
|
||||||
|
`akgl_actor_collide_block`. Contacts arrive one actor at a time, with the normal already
|
||||||
|
pointing the way *that* actor has to move — so a game overriding one actor's response never
|
||||||
|
has to reason about the other's.
|
||||||
|
|
||||||
|
The default pushes the actor out along the normal and removes the component of its motion
|
||||||
|
going into the surface. Three details of that are worth knowing, because each is a way the
|
||||||
|
same function is easy to write wrongly:
|
||||||
|
|
||||||
|
- **It writes `e` and `t`, never `v`.** The step recomputes `v = e + t` at the top of every
|
||||||
|
frame, so a write to `v` is discarded before anything reads it. An actor holding a
|
||||||
|
direction into a wall would otherwise accumulate thrust while standing still and leave at
|
||||||
|
speed the instant the wall ended.
|
||||||
|
- **It removes the component, not the axis.** Zeroing the whole thrust vector costs the
|
||||||
|
motion *along* the surface too. Sliding along a wall is the difference between a wall and
|
||||||
|
glue.
|
||||||
|
- **A separating contact is left alone.** An actor already moving out of a surface whose
|
||||||
|
velocity gets zeroed is an actor stuck to a wall it is walking away from, and it reads to a
|
||||||
|
player as the collision grabbing them.
|
||||||
|
|
||||||
|
A contact carries the global tile id, layer and cell when the other side was a tile, which is
|
||||||
|
what tells a spike from a floor without a second lookup.
|
||||||
|
|
||||||
|
### Sensors
|
||||||
|
|
||||||
|
A shape flagged `AKGL_COLLISION_FLAG_SENSOR` reports a contact and never pushes. Coins,
|
||||||
|
trigger volumes, damage zones. The default response returns having changed nothing — walking
|
||||||
|
through a coin is not being blocked by it.
|
||||||
|
|
||||||
|
## Asking without being pushed
|
||||||
|
|
||||||
|
Four queries answer a question and resolve nothing. They are what a game reaches for when it
|
||||||
|
wants to *know*: a ledge probe ahead of a walking enemy, a check that a doorway is clear, a
|
||||||
|
spawn point that needs validating.
|
||||||
|
|
||||||
|
| Function | Answers |
|
||||||
|
|---|---|
|
||||||
|
| `akgl_collision_solid_at` | is the tile under this point solid |
|
||||||
|
| `akgl_collision_box_blocked` | would this rectangle overlap anything solid, tiles and proxies both |
|
||||||
|
| `akgl_collision_query_box` | visit every proxy that may overlap this rectangle |
|
||||||
|
| `akgl_collision_settle` | lift a shape out of geometry it was placed inside |
|
||||||
|
|
||||||
|
`akgl_collision_settle` deserves the explanation. Resolution stops a shape *entering*
|
||||||
|
geometry and has nothing to say about one that began inside it — what it does instead is
|
||||||
|
refuse every move, so an actor spawned in a wall is simply stuck. Level authors produce that
|
||||||
|
constantly, because an editor rounds an object onto a step. Settling walks the shape up a
|
||||||
|
tile at a time and refuses loudly rather than searching forever.
|
||||||
|
|
||||||
|
## Partitioners
|
||||||
|
|
||||||
|
The broad phase is pluggable, the same way the renderer and the physics backend are: a record
|
||||||
|
of function pointers plus an initializer.
|
||||||
|
|
||||||
|
**Use the grid.** It is the default, and `PERFORMANCE.md` measures it at 2.6 times faster
|
||||||
|
than the tree on the same population — before counting that the tree also rebuilds whenever
|
||||||
|
anything moves. A grid `move` for a proxy that has not left its cells is 11.5 ns and touches
|
||||||
|
nothing.
|
||||||
|
|
||||||
|
The BSP ships alongside it so that "pluggable" means something: a vtable with one
|
||||||
|
implementation behind it has never been asked to be a vtable, and the partitioner suite runs
|
||||||
|
its entire contract against every entry in a table. It would earn its place in a world with
|
||||||
|
wildly non-uniform object sizes, or one larger than the grid's fixed cell array covers.
|
||||||
|
|
||||||
|
```c norun
|
||||||
|
/* Only if you have measured your own scene and the tree wins. */
|
||||||
|
akgl_collision_world_init(&world, "bsp", 16.0f, 16.0f);
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is not implemented
|
||||||
|
|
||||||
|
- **No rotation.** No actor carries an angle, `akgl_actor_render` hard-codes
|
||||||
|
`SDL_FLIP_NONE`, and every shape is axis-aligned. The support functions are written so that
|
||||||
|
adding it touches one static function in the narrowphase; see `TODO.md`.
|
||||||
|
- **No restitution and no friction.** The default response blocks. Anything bouncier is your
|
||||||
|
`collidefunc`.
|
||||||
|
- **No continuous collision.** Sub-stepping bounds how far an actor travels between tests, and
|
||||||
|
it is capped: above roughly `8 x 0.5 x cellsize` per step — about 1280 px/s on 16-pixel
|
||||||
|
tiles at the default `max_timestep` — an actor can still pass through a wall. That is a
|
||||||
|
projectile, not a walker. `AKGL_COLLISION_FLAG_BULLET` reserves the bit for the swept
|
||||||
|
narrowphase that would fix it, and does nothing today.
|
||||||
|
- **No concave shapes.** The narrowphase is convex-only, always. A concave collider is
|
||||||
|
several convex ones.
|
||||||
|
- **Resolution is pool-order dependent.** An actor low in the pool sees the world one
|
||||||
|
sub-step stale. This is the arcade bargain, and it is the same one Construct and Phaser
|
||||||
|
make.
|
||||||
|
|
||||||
|
## Where to look next
|
||||||
|
|
||||||
|
- [Chapter 14](14-physics.md) — the step this runs inside, and the four gaps in the feel that
|
||||||
|
collision does not close.
|
||||||
|
- [Chapter 12](12-actors.md) — the other six behaviour hooks.
|
||||||
|
- [Chapter 13](13-tilemaps.md) — the map, and what else its custom properties can say.
|
||||||
|
- [Chapter 20](20-tutorial-sidescroller.md) — a game that uses all of this.
|
||||||
@@ -6,7 +6,7 @@ everything else passes around. The JSON accessors are the valuable part of this
|
|||||||
their status semantics are what every asset loader in the library is written against, and
|
their status semantics are what every asset loader in the library is written against, and
|
||||||
they are documented nowhere else.
|
they are documented nowhere else.
|
||||||
|
|
||||||
## Collision helpers
|
## Rectangle overlap helpers
|
||||||
|
|
||||||
Three functions, all axis-aligned, all in `util.h`:
|
Three functions, all axis-aligned, all in `util.h`:
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ tall thin character is exactly that shape. The header carried a `@note` describi
|
|||||||
than a fix.
|
than a fix.
|
||||||
|
|
||||||
It is four comparisons now, `r1.x <= r2.x + r2.w && r2.x <= r1.x + r1.w` on both axes.
|
It is four comparisons now, `r1.x <= r2.x + r2.w && r2.x <= r1.x + r1.w` on both axes.
|
||||||
`<=` rather than `<` because [`akgl_collide_point_rectangle`](#collision-helpers) is
|
`<=` rather than `<` because [`akgl_collide_point_rectangle`](#rectangle-overlap-helpers) is
|
||||||
inclusive on all four edges and these two have always agreed: touching counts.
|
inclusive on all four edges and these two have always agreed: touching counts.
|
||||||
|
|
||||||
**If you are upgrading from 0.7.x, two answers change.** The cross now reports `true`, which
|
**If you are upgrading from 0.7.x, two answers change.** The cross now reports `true`, which
|
||||||
@@ -92,10 +92,13 @@ is the point. And because the comparison no longer round-trips through `akgl_Poi
|
|||||||
members, overlaps and gaps smaller than a pixel are now seen rather than truncated away — so
|
members, overlaps and gaps smaller than a pixel are now seen rather than truncated away — so
|
||||||
a pickup test that was accidentally forgiving by up to a pixel is no longer forgiving.
|
a pickup test that was accidentally forgiving by up to a pixel is no longer forgiving.
|
||||||
|
|
||||||
This is separate from the fact that **nothing in the library calls these during a frame.**
|
**The library runs its own collision now** — see [Chapter 15](15-collision.md) — and it does
|
||||||
`akgl_physics_simulate` never calls `collide`, and `akgl_physics_arcade_collide` raises
|
not use these. `akgl_collision_test` works in centres and half-extents, answers with a normal
|
||||||
`AKERR_API` — see [Chapter 14](14-physics.md). Collision detection is yours to run, and these
|
and a depth rather than a boolean, and handles circles and capsules as well as boxes.
|
||||||
are the helpers for running it.
|
|
||||||
|
These stay because a game-level overlap question is not always a physics question: a
|
||||||
|
minimap marker, a UI hit test, "is the cursor over this". For anything that should push or be
|
||||||
|
pushed, use a collision shape.
|
||||||
|
|
||||||
## Path resolution
|
## Path resolution
|
||||||
|
|
||||||
@@ -408,7 +411,7 @@ typedef struct
|
|||||||
|
|
||||||
`data` is a plain `char[]`, so `str->data` goes anywhere a `char *` does. `refcount` belongs
|
`data` is a plain `char[]`, so `str->data` goes anywhere a `char *` does. `refcount` belongs
|
||||||
to the heap layer — [Chapter 5](05-the-heap.md) — and **`akgl_heap_next_string` is the one
|
to the heap layer — [Chapter 5](05-the-heap.md) — and **`akgl_heap_next_string` is the one
|
||||||
acquire function that takes the reference for you**, unlike the other four pools.
|
acquire function that takes the reference for you**, unlike the other seven pools.
|
||||||
|
|
||||||
Two functions operate on the contents:
|
Two functions operate on the contents:
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ What the statuses mean is [Chapter 4](04-errors.md).
|
|||||||
| Function | Raises |
|
| Function | Raises |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `akgl_heap_init`, `akgl_heap_init_actor` | *nothing — no failure path today* |
|
| `akgl_heap_init`, `akgl_heap_init_actor` | *nothing — no failure path today* |
|
||||||
| `akgl_heap_next_actor`, `_sprite`, `_spritesheet`, `_character`, `_string` | `AKGL_ERR_HEAP` |
|
| `akgl_heap_next_actor`, `_sprite`, `_spritesheet`, `_character`, `_string`, `_collision_proxy`, `_collision_cell`, `_bspnode` | `AKGL_ERR_HEAP` |
|
||||||
| `akgl_heap_release_actor`, `_sprite`, `_spritesheet`, `_character`, `_string` | `AKERR_NULLPOINTER` |
|
| `akgl_heap_release_actor`, `_sprite`, `_spritesheet`, `_character`, `_string`, `_collision_proxy`, `_collision_cell`, `_bspnode` | `AKERR_NULLPOINTER` |
|
||||||
|
|
||||||
### `registry.h`
|
### `registry.h`
|
||||||
|
|
||||||
@@ -113,7 +113,23 @@ What the statuses mean is [Chapter 4](04-errors.md).
|
|||||||
| `akgl_physics_simulate` | `AKERR_NULLPOINTER`; handles `AKGL_ERR_LOGICINTERRUPT` (H) |
|
| `akgl_physics_simulate` | `AKERR_NULLPOINTER`; handles `AKGL_ERR_LOGICINTERRUPT` (H) |
|
||||||
| `akgl_physics_null_gravity`, `_collide`, `_move` | `AKERR_NULLPOINTER` |
|
| `akgl_physics_null_gravity`, `_collide`, `_move` | `AKERR_NULLPOINTER` |
|
||||||
| `akgl_physics_arcade_gravity`, `_move` | `AKERR_NULLPOINTER` |
|
| `akgl_physics_arcade_gravity`, `_move` | `AKERR_NULLPOINTER` |
|
||||||
| `akgl_physics_arcade_collide` | `AKERR_NULLPOINTER`, **`AKERR_API` — not implemented** |
|
| `akgl_physics_arcade_collide` | `AKERR_NULLPOINTER`, plus `akgl_collision_resolve`'s. A no-op when `self->collision` is `NULL` |
|
||||||
|
|
||||||
|
### `collision.h`
|
||||||
|
|
||||||
|
| Function | Raises |
|
||||||
|
|---|---|
|
||||||
|
| `akgl_collision_shape_box`, `_shape_circle`, `_shape_capsule`, `_shape_bounds`, `_shape_interacts` | `AKERR_NULLPOINTER` |
|
||||||
|
| `akgl_collision_world_init` | `AKERR_NULLPOINTER`, **`AKERR_VALUE` for a non-positive cell size**, plus `akgl_partitioner_factory`'s |
|
||||||
|
| `akgl_collision_bind_tilemap` | `AKERR_NULLPOINTER`, plus `akgl_get_json_properties_boolean`'s; handles `AKERR_KEY` (H) for a layer with no `collidable` property |
|
||||||
|
| `akgl_collision_test` | `AKERR_NULLPOINTER`, **`AKGL_ERR_COLLISION` when the ccd arena is exhausted** — the message carries the high-water mark |
|
||||||
|
| `akgl_collision_resolve` | `AKERR_NULLPOINTER`, plus `akgl_collision_test`'s and the actor's own `collidefunc`'s |
|
||||||
|
| `akgl_collision_settle` | `AKERR_NULLPOINTER`, **`AKERR_VALUE` when the shape is not clear within `maxsteps`** |
|
||||||
|
| `akgl_collision_solid_at`, `_box_blocked`, `_query_box`, `_substeps`, `_sync_actors` | `AKERR_NULLPOINTER` |
|
||||||
|
| `akgl_collision_proxy_initialize`, `_proxy_sync`, `_cell_initialize` | `AKERR_NULLPOINTER` |
|
||||||
|
| `akgl_partitioner_factory` | `AKERR_NULLPOINTER`, **`AKERR_KEY` for an unknown partitioner name** |
|
||||||
|
| `akgl_partitioner_init_grid`, `_init_bsp` | `AKERR_NULLPOINTER`. The BSP's `insert` raises `AKGL_ERR_HEAP` past `AKGL_MAX_HEAP_COLLISION_PROXY` proxies |
|
||||||
|
| `akgl_actor_collide_block` | `AKERR_NULLPOINTER` |
|
||||||
|
|
||||||
### `renderer.h`
|
### `renderer.h`
|
||||||
|
|
||||||
@@ -180,8 +196,8 @@ What the statuses mean is [Chapter 4](04-errors.md).
|
|||||||
|
|
||||||
## B. Compile-time limits
|
## B. Compile-time limits
|
||||||
|
|
||||||
Everything below is fixed when the library is compiled. **Only the `AKGL_MAX_HEAP_*` five
|
Everything below is fixed when the library is compiled. **Only the `AKGL_MAX_HEAP_*` eight
|
||||||
are overridable**, and even those have to be overridden for the whole build — see
|
and `AKGL_CCD_ARENA_BYTES` are overridable**, and even those have to be overridden for the whole build — see
|
||||||
[Chapter 5](05-the-heap.md), "The ceilings are a compile-time ABI constraint". The rest are
|
[Chapter 5](05-the-heap.md), "The ceilings are a compile-time ABI constraint". The rest are
|
||||||
plain `#define`s with no `#ifndef` guard: changing one means editing the header and
|
plain `#define`s with no `#ifndef` guard: changing one means editing the header and
|
||||||
rebuilding libakgl and everything linking it.
|
rebuilding libakgl and everything linking it.
|
||||||
@@ -206,8 +222,37 @@ rebuilding libakgl and everything linking it.
|
|||||||
#endif
|
#endif
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```c excerpt=include/akgl/heap.h
|
||||||
|
#ifndef AKGL_MAX_HEAP_COLLISION_PROXY
|
||||||
|
#define AKGL_MAX_HEAP_COLLISION_PROXY (AKGL_MAX_HEAP_ACTOR * 2)
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
```c excerpt=include/akgl/heap.h
|
||||||
|
#ifndef AKGL_MAX_HEAP_BSPNODE
|
||||||
|
#define AKGL_MAX_HEAP_BSPNODE (AKGL_MAX_HEAP_COLLISION_PROXY)
|
||||||
|
#endif
|
||||||
|
#ifndef AKGL_MAX_HEAP_COLLISION_CELL
|
||||||
|
#define AKGL_MAX_HEAP_COLLISION_CELL (AKGL_MAX_HEAP_COLLISION_PROXY * 16)
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
Exceeding any of them is `AKGL_ERR_HEAP`.
|
Exceeding any of them is `AKGL_ERR_HEAP`.
|
||||||
|
|
||||||
|
### Collision
|
||||||
|
|
||||||
|
Three of these are not in a public header, because nothing outside the broad phase may size
|
||||||
|
anything against them:
|
||||||
|
|
||||||
|
| Constant | Value | Where | Bounds |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `AKGL_CCD_ARENA_BYTES` | 65536 | `collision_arena.h` | The narrowphase's whole allocation budget. Overridable. One GJK/EPA box pair measures 7,264 bytes |
|
||||||
|
| `AKGL_COLLISION_GRID_COLS` / `_ROWS` | 128 | `src/collision_grid.c` | The dense cell-head array, 32 KB. A map wider than 128 cells wraps rather than growing |
|
||||||
|
| `AKGL_COLLISION_GRID_MAX_SPAN` | 16 | `src/collision_grid.c` | Cells one proxy may occupy. Anything larger spills to one oversized chain, which is what makes `AKGL_MAX_HEAP_COLLISION_CELL` provable rather than hopeful |
|
||||||
|
| `AKGL_COLLISION_MAX_SUBSTEPS` | 8 | `src/collision.c` | Sub-steps per actor per step, and therefore the speed above which tunnelling returns |
|
||||||
|
| `AKGL_COLLISION_MAX_ITERATIONS` | 100 | `src/collision.c` | `ccd_t.max_iterations`. libccd's own default is `(unsigned long)-1` — an unbounded loop inside a frame |
|
||||||
|
| `AKGL_COLLISION_DEPTH_RATIO` | 2.0 | `collision.h` | The z extrusion, as a multiple of the largest planar half-extent. See [Chapter 15](15-collision.md) before changing it |
|
||||||
|
|
||||||
### Strings
|
### Strings
|
||||||
|
|
||||||
`AKGL_MAX_STRING_LENGTH` is `PATH_MAX`, which is 4096 on Linux. That is the capacity of
|
`AKGL_MAX_STRING_LENGTH` is `PATH_MAX`, which is 4096 on Linux. That is the capacity of
|
||||||
|
|||||||
@@ -17,23 +17,24 @@ per-function reference, build the Doxygen output with `doxygen Doxyfile`.
|
|||||||
| **[2. Design philosophy](02-design-philosophy.md)** | Bounded pools, pluggable backends, bit flags, name-based registries, one world at a time |
|
| **[2. Design philosophy](02-design-philosophy.md)** | Bounded pools, pluggable backends, bit flags, name-based registries, one world at a time |
|
||||||
| **[3. Getting started](03-getting-started.md)** | Dependencies, `add_subdirectory` vs `akgl.pc`, and the smallest program that opens a window |
|
| **[3. Getting started](03-getting-started.md)** | Dependencies, `add_subdirectory` vs `akgl.pc`, and the smallest program that opens a window |
|
||||||
| **[4. Errors and status codes](04-errors.md)** | The status tables, what each code means *here*, and the traps that fail silently |
|
| **[4. Errors and status codes](04-errors.md)** | The status tables, what each code means *here*, and the traps that fail silently |
|
||||||
| **[5. The heap](05-the-heap.md)** | The five pools, `akgl_heap_next_*`, `akgl_String`, and why exhaustion usually means a missing release |
|
| **[5. The heap](05-the-heap.md)** | The eight pools, `akgl_heap_next_*`, `akgl_String`, and why exhaustion usually means a missing release |
|
||||||
| **[6. The registry](06-the-registry.md)** | The eight registries, configuration properties, and the id-0 silent no-op |
|
| **[6. The registry](06-the-registry.md)** | The eight registries, configuration properties, and the id-0 silent no-op |
|
||||||
| **[7. The game and the frame](07-the-game-and-the-frame.md)** | Startup order, `akgl_game_update`, the state lock, iterators, savegames |
|
| **[7. The game and the frame](07-the-game-and-the-frame.md)** | Startup order, `akgl_game_update`, the state lock, iterators, savegames |
|
||||||
| **[8. Rendering](08-rendering.md)** | The backend vtable, the frame contract, cameras, and embedding a host's own `SDL_Renderer` |
|
| **[8. Rendering](08-rendering.md)** | The backend vtable, the frame contract, cameras, and embedding a host's own `SDL_Renderer` |
|
||||||
| **[9. Drawing](09-drawing.md)** | Points, lines, rectangles, circles, flood fill, and saving a region |
|
| **[9. Drawing](09-drawing.md)** | Points, lines, rectangles, circles, flood fill, and saving a region |
|
||||||
| **[10. Spritesheets and sprites](10-spritesheets-and-sprites.md)** | One texture shared by path, the sprite JSON format, frame animation |
|
| **[10. Spritesheets and sprites](10-spritesheets-and-sprites.md)** | One texture shared by path, the sprite JSON format, frame animation |
|
||||||
| **[11. Characters](11-characters.md)** | State-to-sprite mappings, movement constants, the character JSON format |
|
| **[11. Characters](11-characters.md)** | State-to-sprite mappings, movement constants, the character JSON format |
|
||||||
| **[12. Actors](12-actors.md)** | The state bitmask, the six behaviour hooks, parents and children, layers |
|
| **[12. Actors](12-actors.md)** | The state bitmask, the seven behaviour hooks, parents and children, layers |
|
||||||
| **[13. Tilemaps](13-tilemaps.md)** | Tiled TMJ, libakgl's three extensions, and the limits that bind a map |
|
| **[13. Tilemaps](13-tilemaps.md)** | Tiled TMJ, libakgl's three extensions, and the limits that bind a map |
|
||||||
| **[14. Physics](14-physics.md)** | Thrust, environment and velocity; the `null` and `arcade` backends; what is not implemented |
|
| **[14. Physics](14-physics.md)** | Thrust, environment and velocity; the `null` and `arcade` backends; what is not implemented |
|
||||||
| **[15. Input](16-input.md)** | Control maps, push-not-poll dispatch, the keystroke ring, gamepads |
|
| **[15. Collision](15-collision.md)** | Shapes, masks, tiles as geometry, the response hook, and the two partitioners |
|
||||||
| **[16. Text and fonts](17-text-and-fonts.md)** | Loading, drawing, measuring, and the teardown order that matters |
|
| **[16. Input](16-input.md)** | Control maps, push-not-poll dispatch, the keystroke ring, gamepads |
|
||||||
| **[17. Audio](18-audio.md)** | The three-voice synthesizer, and the separate background-music path |
|
| **[17. Text and fonts](17-text-and-fonts.md)** | Loading, drawing, measuring, and the teardown order that matters |
|
||||||
| **[18. Utilities](19-utilities.md)** | Collision helpers, path resolution, the JSON accessors, static strings |
|
| **[18. Audio](18-audio.md)** | The three-voice synthesizer, and the separate background-music path |
|
||||||
| **[19. Tutorial: a 2D sidescroller](20-tutorial-sidescroller.md)** | Gravity, a jump, collision you write yourself, coins and hazards |
|
| **[19. Utilities](19-utilities.md)** | Rectangle overlap, path resolution, the JSON accessors, static strings |
|
||||||
| **[20. Tutorial: a top-down JRPG](21-tutorial-jrpg.md)** | A town map, NPCs spawned from map objects, four-way animation, a text box, a follower |
|
| **[20. Tutorial: a 2D sidescroller](20-tutorial-sidescroller.md)** | Gravity, a jump, collision, coins and hazards |
|
||||||
| **[21. Appendix](22-appendix-limits.md)** | Every limit, every status, every configuration property |
|
| **[21. Tutorial: a top-down JRPG](21-tutorial-jrpg.md)** | A town map, NPCs spawned from map objects, four-way animation, a text box, a follower |
|
||||||
|
| **[22. Appendix](22-appendix-limits.md)** | Every limit, every status, every configuration property |
|
||||||
|
|
||||||
Both tutorials are complete programs under [`examples/`](../examples). They build with the
|
Both tutorials are complete programs under [`examples/`](../examples). They build with the
|
||||||
library and run headless in CI, and the chapters quote them rather than restating them — so
|
library and run headless in CI, and the chapters quote them rather than restating them — so
|
||||||
|
|||||||
Reference in New Issue
Block a user