Files
libakgl/include/akgl/collision.h

506 lines
28 KiB
C
Raw Normal View History

Add collision shapes, on the character and overridable per actor A shape is a convex volume positioned relative to an actor's origin. It lives on akgl_Character, so every goblin sharing a character shares one shape definition the way they already share speeds and the state-to-sprite map, and an actor may override it with akgl_Actor::shape_override. The flag is not redundant with an empty shape. "This actor deliberately has no collider" and "this actor has not been given one 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. include/akgl/collision.h is a leaf: it names actors and tilemaps as incomplete struct pointers rather than including their headers, because both of those need akgl_CollisionShape by value and a cycle forms otherwise. The `headers` suite compiles it as the first include of a translation unit, so that property is enforced rather than merely intended -- which is why the tilemap types got struct tags two commits ago. The masks are asymmetric and the defaults are the load-bearing part: layermask ACTOR, collidemask STATIC. An actor given a shape and nothing else collides with map geometry and with no other actor. "Everything with a hitbox shoves everything else" would be 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 bit, opting out of it would have been a hunt through every NPC a game spawns. Every shape gets a z depth, because the narrowphase behind this is three dimensional and answers with the *minimum* separating translation. A thin extrusion makes z the cheapest axis, and an actor pushed along z goes nowhere a player can see while remaining inside the floor -- a collision system reporting success while doing nothing. The ratio of 2 is the smallest for which the z overlap of any two shapes the setters build provably exceeds any planar penetration they can reach. The test for that asserts the inequality across a spread of sizes rather than asserting the constant is 2, so a change to the constant fails here rather than in a game. Two things about that test are worth recording, because the first version had both: - It could not fail. TEST_ASSERT expands to FAIL_BREAK, which reports by `break`ing, and the assertion was inside a nested `for` -- so the break left the loop rather than the ATTEMPT block and the failure evaporated. This is the hazard AGENTS.md documents for CATCH; it applies to the whole family. Verified by setting the ratio to 0.5 and watching the suite still pass, then fixing it and watching it report the 512x512 case by name. - tests/collision_arena.c had the same bug in a loop added in the previous commit, and it is fixed here too. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:48:05 -04:00
/**
* @file collision.h
* @brief Collision shapes: what an actor occupies, and what it will collide with.
*
* A shape is a convex volume positioned relative to an actor's origin. It lives
* on the akgl_Character, so every goblin sharing a character shares one shape
* definition, exactly as they share speeds and the state-to-sprite map -- and an
* individual actor may override it when it needs to.
*
* @section collision_leaf This header includes nothing of ours but types.h
*
* `actor.h` and `character.h` both need akgl_CollisionShape by value, so this
* header cannot include either of them without a cycle. It names actors and
* tilemaps as incomplete struct pointers instead, which is why they need struct
* tags and why `akgl_Tilemap` grew one. The `headers` suite compiles this file
* as the first include of a translation unit and would fail immediately if that
* ever stopped being true.
*
* @section collision_extrusion Why a 2D shape has a depth
*
* The narrowphase behind this is three-dimensional, which is what lets the same
* 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 large
* number chosen for comfort; it is the smallest ratio for which the z overlap of
* any two shapes built this way is provably larger than any planar penetration
* they can reach, so z can never be the minimum axis. Pass a depth by hand only
* if you know what that costs.
*/
#ifndef _AKGL_COLLISION_H_
#define _AKGL_COLLISION_H_
#include <stdbool.h>
#include <stdint.h>
#include <SDL3/SDL_rect.h>
#include <akerror.h>
#include <akgl/types.h>
struct akgl_Actor;
struct akgl_Tilemap;
Add a pluggable broad phase, and the incremental uniform grid behind it akgl_Partitioner is a record of function pointers and an initializer, the same shape as the render and physics backends. `move` is its own slot rather than remove-then-insert, and that is the whole design: a uniform grid's move is a comparison and a return when the proxy has not left the cells it was in, which is the steady state for a walking actor and the permanent state for a static one. Spelling it as remove-and-insert would turn the incremental grid into the rebuild-every-frame tree that PERFORMANCE.md already measured and rejected. The grid is a dense 128x128 array of cell heads over the world, cells keyed on tile size, with two intrusive chains per entry -- one through the cell so a query can walk it, one through the proxy so removal unlinks a whole span without searching. Everything is an int16_t index rather than a pointer, so the structure is relocatable and clearing it is a memset. A proxy covering more cells than AKGL_COLLISION_GRID_MAX_SPAN spills onto one chain every query walks, which is what makes the cell pool's ceiling provable rather than hopeful. tests/partition.c compares every query against a linear scan over the same proxies and asserts containment in one direction, not equality. That asymmetry is the contract: over-reporting costs a narrowphase call, under-reporting is a wall an actor walks through. The suite is a table over partitioners so the same assertions run against every implementation, which is what the second one landing later will be held to. Three deliberate breaks, and two of them were not caught the first time: - Removing the min-cell rule so a shared-cell pair reports repeatedly: caught. - Dropping the unlink in `move`: **not caught**, initially. Stale entries do not produce wrong query answers, because the bounds re-check filters them out -- they leak the cell pool until the grid stops working, on a timescale no unit test reaches by accident. Counting live pool entries across 200 moves is the only thing that sees it, and now does: 505 entries against an expected 5. - Swapping floorf for a truncating cast: **not caught, and correctly so.** Everything below zero is clamped to cell 0 either way, so today the two are indistinguishable. The comment claiming truncation is a defect was overstating it and now says what is actually true: the clamp is the only thing hiding it, and a negative world origin or a relaxed clamp would make it real with no test standing in front of it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 06:29:02 -04:00
struct akgl_CollisionWorld;
Add collision shapes, on the character and overridable per actor A shape is a convex volume positioned relative to an actor's origin. It lives on akgl_Character, so every goblin sharing a character shares one shape definition the way they already share speeds and the state-to-sprite map, and an actor may override it with akgl_Actor::shape_override. The flag is not redundant with an empty shape. "This actor deliberately has no collider" and "this actor has not been given one 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. include/akgl/collision.h is a leaf: it names actors and tilemaps as incomplete struct pointers rather than including their headers, because both of those need akgl_CollisionShape by value and a cycle forms otherwise. The `headers` suite compiles it as the first include of a translation unit, so that property is enforced rather than merely intended -- which is why the tilemap types got struct tags two commits ago. The masks are asymmetric and the defaults are the load-bearing part: layermask ACTOR, collidemask STATIC. An actor given a shape and nothing else collides with map geometry and with no other actor. "Everything with a hitbox shoves everything else" would be 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 bit, opting out of it would have been a hunt through every NPC a game spawns. Every shape gets a z depth, because the narrowphase behind this is three dimensional and answers with the *minimum* separating translation. A thin extrusion makes z the cheapest axis, and an actor pushed along z goes nowhere a player can see while remaining inside the floor -- a collision system reporting success while doing nothing. The ratio of 2 is the smallest for which the z overlap of any two shapes the setters build provably exceeds any planar penetration they can reach. The test for that asserts the inequality across a spread of sizes rather than asserting the constant is 2, so a change to the constant fails here rather than in a game. Two things about that test are worth recording, because the first version had both: - It could not fail. TEST_ASSERT expands to FAIL_BREAK, which reports by `break`ing, and the assertion was inside a nested `for` -- so the break left the loop rather than the ATTEMPT block and the failure evaporated. This is the hazard AGENTS.md documents for CATCH; it applies to the whole family. Verified by setting the ratio to 0.5 and watching the suite still pass, then fixing it and watching it report the 512x512 case by name. - tests/collision_arena.c had the same bug in a loop added in the previous commit, and it is fixed here too. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:48:05 -04:00
/** @brief No shape. The actor takes no part in collision. */
#define AKGL_COLLISION_SHAPE_NONE 0
/** @brief An axis-aligned box. The common case, and the one with a closed-form answer. */
#define AKGL_COLLISION_SHAPE_BOX 1
/** @brief A circle in the xy plane, extruded along z. `hx` is the radius and `hy` is ignored. */
#define AKGL_COLLISION_SHAPE_CIRCLE 2
/** @brief A capsule whose long axis is x: a box with semicircular caps left and right. */
#define AKGL_COLLISION_SHAPE_CAPSULE_X 3
/** @brief A capsule whose long axis is y: a box with semicircular caps top and bottom. */
#define AKGL_COLLISION_SHAPE_CAPSULE_Y 4
/** @brief No flags. */
#define AKGL_COLLISION_FLAG_NONE 0x00000000u
/** @brief Never moved by the resolver and never re-inserted into the broad phase. */
#define AKGL_COLLISION_FLAG_STATIC 0x00000001u
/** @brief Reports a contact and never pushes. Pickups, trigger volumes, damage zones. */
#define AKGL_COLLISION_FLAG_SENSOR 0x00000002u
/** @brief Present but ignored this step. Cheaper than dropping the shape and putting it back. */
#define AKGL_COLLISION_FLAG_DISABLED 0x00000004u
/**
* @brief Reserved for a swept narrowphase. **Not implemented; setting it does nothing.**
*
* Sub-stepping bounds how far an actor moves between collision tests, which
* stops anything moving at ordinary speeds from passing through a wall. A
* projectile is not moving at ordinary speeds. See `TODO.md`.
*/
#define AKGL_COLLISION_FLAG_BULLET 0x00000008u
/** @brief On no layer. A shape with this `layermask` collides with nothing. */
#define AKGL_COLLISION_LAYER_NONE 0x00000000u
/** @brief Map geometry: solid tiles and static proxies. The default `collidemask`. */
#define AKGL_COLLISION_LAYER_STATIC (1u << 0)
/** @brief Ordinary actors. The default `layermask`. */
#define AKGL_COLLISION_LAYER_ACTOR (1u << 1)
/** @brief Suggested layer for the player. Nothing in the library treats it specially. */
#define AKGL_COLLISION_LAYER_PLAYER (1u << 2)
/** @brief Suggested layer for hostiles. */
#define AKGL_COLLISION_LAYER_ENEMY (1u << 3)
/** @brief Suggested layer for things picked up rather than bumped into. */
#define AKGL_COLLISION_LAYER_PICKUP (1u << 4)
/** @brief Every layer, including the ones a game defines for itself in bits 5 and above. */
#define AKGL_COLLISION_LAYER_ALL 0xFFFFFFFFu
/**
* @brief Depth given to a 2D shape, as a multiple of its largest planar half-extent.
*
* 2 and not 1. At 1, two identical squares fully overlapped have the same
* overlap on z as in the plane, and which axis the narrowphase calls "minimum"
* comes down to floating-point luck. At 2 the z overlap of any pair is at least
* twice the largest planar half-extent either can reach, and the planar
* penetration is at most that -- so z loses every time, by construction rather
* than by margin.
*/
#define AKGL_COLLISION_DEPTH_RATIO 2.0f
/**
* @brief A convex volume, positioned relative to an actor's origin.
*
* Stored as a centre and half-extents rather than as an `SDL_FRect`, because
* that is the form both the overlap test and the narrowphase want and this way
* the conversion happens once, in a setter, instead of on every one of the tens
* of thousands of tests a frame. The setters take the rectangle form a game
* already has.
*/
typedef struct akgl_CollisionShape {
uint8_t kind; /**< One of the `AKGL_COLLISION_SHAPE_*` values. #AKGL_COLLISION_SHAPE_NONE means this actor does not collide, and is what a zeroed shape is. */
uint32_t flags; /**< Bitwise OR of the `AKGL_COLLISION_FLAG_*` values. */
uint32_t layermask; /**< Which layers this shape is *on*. What other shapes test against. */
uint32_t collidemask; /**< Which layers this shape *responds to*. Asymmetric on purpose: a player can block against an NPC while the NPC ignores the player. */
float32_t ox; /**< Centre offset from the actor's `x`, in map pixels. */
float32_t oy; /**< Centre offset from the actor's `y`. Positive is down, matching screen space. */
float32_t oz; /**< Centre offset from the actor's `z`. */
float32_t hx; /**< Half-extent along x. The radius, for a circle or a capsule. */
float32_t hy; /**< Half-extent along y. Ignored for a circle. */
float32_t hz; /**< Half-extent along z. Never 0 after a setter has run; see the extrusion note on this file. */
} akgl_CollisionShape;
Pool collision proxies, and tie one to the life of its actor A proxy is what the broad phase will index: an actor's shape, where it is, and the bounds that follow from those. It is a sixth heap layer, two per actor -- one for the actor itself and headroom for static geometry a game registers that is not tile-aligned. Solid *tiles* deliberately get none: the broad phase will read those out of the tilemap's own array, because one proxy per tile is tens of megabytes to index data that is already a grid. The pool follows the existing convention rather than improving on it. akgl_heap_next_collision_proxy finds a free slot and does not claim it; akgl_collision_proxy_initialize claims it. That is TODO.md item 8's asymmetry, and it is kept on purpose: an acquire abandoned before initialization leaks nothing, because the slot still reads as free, and a sixth pool with its own rule would be worse than one defect with five instances. Fixing item 8 means fixing all five together with every call site audited, and that is its own change. The proxy holds a *copy* of the shape rather than a pointer. An actor's own logic may rewrite its shape mid-step -- a character crouching, a projectile arming -- and a broad phase whose bounds came from a shape that has since changed is a bug that only appears when two things happen in the same frame. akgl_heap_release_actor now releases the actor's proxy. Without it the proxy holds a borrowed `owner` into a slot that has just been zeroed, so the broad phase keeps a registration whose owner reads as a free actor, and the next contact against it is a collision with nothing. Verified by removing the release and watching test_proxy_dies_with_its_actor go red. The tests pin the convention as well as the behaviour: that two acquires with no initialize between them return the same slot is asserted, not merely tolerated, so that changing the convention has to change a test that says why it exists. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:56:21 -04:00
/**
* @brief One shape's registration in the broad phase.
*
* A proxy is what the partitioner indexes. It carries a **copy** of the shape
* rather than a pointer to it, which is deliberate: an actor's own logic may
* legitimately rewrite its shape mid-step -- a character crouching, a projectile
* arming -- and a broad phase whose bounds were computed from a shape that has
* since changed is a source of bugs that only appear when two things happen in
* the same frame. The copy is refreshed once per step, from one place.
*
* Proxies are pool objects. The library creates and destroys them; a game does
* not, and a game that releases an actor gets its proxy released with it.
*/
typedef struct akgl_CollisionProxy {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. First member, as in every pooled type here. */
struct akgl_Actor *owner; /**< The actor this proxy tracks, or `NULL` for static geometry with no actor behind it. Borrowed; no reference is taken. */
akgl_CollisionShape shape; /**< A copy of the owner's shape as of the last refresh. */
SDL_FRect bounds; /**< Planar world bounds in map pixels, computed from #shape at (#x, #y). */
float32_t x; /**< World position #bounds was computed from. */
float32_t y; /**< World position #bounds was computed from. */
float32_t z; /**< World position #bounds was computed from. Not represented in #bounds. */
uint32_t stamp; /**< Sweep serial this proxy was last visited on, so a proxy spanning several cells is reported once. Written by the partitioner. */
Add a pluggable broad phase, and the incremental uniform grid behind it akgl_Partitioner is a record of function pointers and an initializer, the same shape as the render and physics backends. `move` is its own slot rather than remove-then-insert, and that is the whole design: a uniform grid's move is a comparison and a return when the proxy has not left the cells it was in, which is the steady state for a walking actor and the permanent state for a static one. Spelling it as remove-and-insert would turn the incremental grid into the rebuild-every-frame tree that PERFORMANCE.md already measured and rejected. The grid is a dense 128x128 array of cell heads over the world, cells keyed on tile size, with two intrusive chains per entry -- one through the cell so a query can walk it, one through the proxy so removal unlinks a whole span without searching. Everything is an int16_t index rather than a pointer, so the structure is relocatable and clearing it is a memset. A proxy covering more cells than AKGL_COLLISION_GRID_MAX_SPAN spills onto one chain every query walks, which is what makes the cell pool's ceiling provable rather than hopeful. tests/partition.c compares every query against a linear scan over the same proxies and asserts containment in one direction, not equality. That asymmetry is the contract: over-reporting costs a narrowphase call, under-reporting is a wall an actor walks through. The suite is a table over partitioners so the same assertions run against every implementation, which is what the second one landing later will be held to. Three deliberate breaks, and two of them were not caught the first time: - Removing the min-cell rule so a shared-cell pair reports repeatedly: caught. - Dropping the unlink in `move`: **not caught**, initially. Stale entries do not produce wrong query answers, because the bounds re-check filters them out -- they leak the cell pool until the grid stops working, on a timescale no unit test reaches by accident. Counting live pool entries across 200 moves is the only thing that sees it, and now does: 505 entries against an expected 5. - Swapping floorf for a truncating cast: **not caught, and correctly so.** Everything below zero is clamped to cell 0 either way, so today the two are indistinguishable. The comment claiming truncation is a defect was overstating it and now says what is actually true: the clamp is the only thing hiding it, and a negative world origin or a relaxed clamp would make it real with no test standing in front of it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 06:29:02 -04:00
int32_t cx0; /**< Grid cell rectangle currently occupied, inclusive. Written by the uniform grid only, and the thing its `move` compares against. */
int32_t cy0; /**< Grid cell rectangle currently occupied. */
int32_t cx1; /**< Grid cell rectangle currently occupied. */
int32_t cy1; /**< Grid cell rectangle currently occupied. */
int16_t first; /**< Head of this proxy's chain of cell entries, or -1 when it is not in the grid. Written by the uniform grid only. */
Pool collision proxies, and tie one to the life of its actor A proxy is what the broad phase will index: an actor's shape, where it is, and the bounds that follow from those. It is a sixth heap layer, two per actor -- one for the actor itself and headroom for static geometry a game registers that is not tile-aligned. Solid *tiles* deliberately get none: the broad phase will read those out of the tilemap's own array, because one proxy per tile is tens of megabytes to index data that is already a grid. The pool follows the existing convention rather than improving on it. akgl_heap_next_collision_proxy finds a free slot and does not claim it; akgl_collision_proxy_initialize claims it. That is TODO.md item 8's asymmetry, and it is kept on purpose: an acquire abandoned before initialization leaks nothing, because the slot still reads as free, and a sixth pool with its own rule would be worse than one defect with five instances. Fixing item 8 means fixing all five together with every call site audited, and that is its own change. The proxy holds a *copy* of the shape rather than a pointer. An actor's own logic may rewrite its shape mid-step -- a character crouching, a projectile arming -- and a broad phase whose bounds came from a shape that has since changed is a bug that only appears when two things happen in the same frame. akgl_heap_release_actor now releases the actor's proxy. Without it the proxy holds a borrowed `owner` into a slot that has just been zeroed, so the broad phase keeps a registration whose owner reads as a free actor, and the next contact against it is a collision with nothing. Verified by removing the release and watching test_proxy_dies_with_its_actor go red. The tests pin the convention as well as the behaviour: that two acquires with no initialize between them return the same slot is asserted, not merely tolerated, so that changing the convention has to change a test that says why it exists. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:56:21 -04:00
} akgl_CollisionProxy;
Add a pluggable broad phase, and the incremental uniform grid behind it akgl_Partitioner is a record of function pointers and an initializer, the same shape as the render and physics backends. `move` is its own slot rather than remove-then-insert, and that is the whole design: a uniform grid's move is a comparison and a return when the proxy has not left the cells it was in, which is the steady state for a walking actor and the permanent state for a static one. Spelling it as remove-and-insert would turn the incremental grid into the rebuild-every-frame tree that PERFORMANCE.md already measured and rejected. The grid is a dense 128x128 array of cell heads over the world, cells keyed on tile size, with two intrusive chains per entry -- one through the cell so a query can walk it, one through the proxy so removal unlinks a whole span without searching. Everything is an int16_t index rather than a pointer, so the structure is relocatable and clearing it is a memset. A proxy covering more cells than AKGL_COLLISION_GRID_MAX_SPAN spills onto one chain every query walks, which is what makes the cell pool's ceiling provable rather than hopeful. tests/partition.c compares every query against a linear scan over the same proxies and asserts containment in one direction, not equality. That asymmetry is the contract: over-reporting costs a narrowphase call, under-reporting is a wall an actor walks through. The suite is a table over partitioners so the same assertions run against every implementation, which is what the second one landing later will be held to. Three deliberate breaks, and two of them were not caught the first time: - Removing the min-cell rule so a shared-cell pair reports repeatedly: caught. - Dropping the unlink in `move`: **not caught**, initially. Stale entries do not produce wrong query answers, because the bounds re-check filters them out -- they leak the cell pool until the grid stops working, on a timescale no unit test reaches by accident. Counting live pool entries across 200 moves is the only thing that sees it, and now does: 505 entries against an expected 5. - Swapping floorf for a truncating cast: **not caught, and correctly so.** Everything below zero is clamped to cell 0 either way, so today the two are indistinguishable. The comment claiming truncation is a defect was overstating it and now says what is actually true: the clamp is the only thing hiding it, and a negative world origin or a relaxed clamp would make it real with no test standing in front of it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 06:29:02 -04:00
/**
* @brief One proxy's membership of one grid cell.
*
* Two intrusive chains: one through the cell, so a query can walk what is in it,
* and one through the proxy, so removing a proxy unlinks its whole span without
* searching. Everything is an index rather than a pointer, which keeps the
* structure relocatable and makes clearing it a `memset`.
*/
typedef struct akgl_CollisionCell {
uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. */
int16_t proxy; /**< Index into the proxy pool. */
int32_t cell; /**< Which cell, as `(row * columns) + column`. */
int16_t next; /**< Next entry in this cell's chain, or -1. */
int16_t prev; /**< Previous entry in this cell's chain, or -1, so removal is O(1). */
int16_t ownernext; /**< Next entry belonging to the same proxy, or -1. */
} akgl_CollisionCell;
Answer whether two shapes overlap, and how to undo it akgl_collision_test takes two proxies and fills in a contact: a unit normal, a depth, and a point. Three paths, cheapest first. The proxies' bounds reject most pairs in four comparisons and no arithmetic, which is free because the bounds were computed when the proxy was synced. A box against a box is answered in closed form. That is not only cheaper than the general solver, it is *exact*, and the difference shows up where it matters most: an actor resting on a floor wants a normal of precisely (0, -1, 0), and an iterative solver converges to something like (0.0001, -0.99999, 0), which accumulates into a slow sideways creep. A tile game is almost entirely boxes, so this is the path that runs. Everything else goes to ccdMPRPenetration. MPR rather than GJK+EPA because MPR allocates nothing at all, converges in fewer iterations, and its one weakness -- a coarser contact *point* -- is on a field the blocking resolver never reads. EPA stays compiled and available behind the arena for a caller who one day wants an accurate manifold. The header says the point is approximate so nobody builds a damage falloff on it. The normal points out of the second shape and toward the first, so a caller moving `a` along it by `depth` separates the pair. libccd hands back the opposite convention; converting once here saves every resolver from remembering the sign, and getting it backwards would compile, would pass any "do these collide" test, and would drag actors into walls. There is a test that asserts the direction, and inverting the conversion turns it red. AKGL_COLLISION_TEST_PLANAR flattens the normal into the xy plane. The extrusion the setters apply already makes z the most expensive axis, so this is a net for a caller who set a depth by hand -- and the failure it catches is silent: the narrowphase reports a contact, the resolver pushes the actor into the screen, and the actor does not move on screen while remaining inside the floor. It also rescues the degenerate case, two shapes at exactly the same centre, where there is no planar direction at all and a zero-length normal would be a wall that stops nothing. Level authors stack things constantly. Three things the tests found rather than assumed: - The guard has two copies, one per path, and the box one is where it earns its place. Removing both is caught; removing only the iterative one is not, because the iterative solver is seeded from the line between the two centres and so converges to a planar answer on its own for a planar offset. That is measured and written down in the test rather than papered over with a fixture contrived to force it. - A concentric pair has no preferred sign on the degenerate axis, so the test asserts the magnitude of the normal rather than its direction. The first version asserted -1 and failed against an equally correct +1. - The box fast path and the iterative solver are two implementations of one answer, so they are driven over the same arrangements and required to agree on the decision and the axis. A shortcut nothing cross-checks is a shortcut waiting to diverge. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 06:18:09 -04:00
/**
* @brief Force every contact normal into the xy plane.
*
* A safety net rather than a mode. The extrusion the setters apply already makes
* z the most expensive axis to separate on, so a normal along z should be
* unreachable -- but a caller who set a depth by hand has opted out of that
* guarantee, and the failure it produces is silent: the resolver pushes the
* actor into the screen, which moves it nowhere a player can see while leaving
* it inside whatever it hit.
*
* Pass it for a 2D game, which is every game today. When there is a real third
* axis to resolve on, leave it off.
*/
#define AKGL_COLLISION_TEST_PLANAR 0x00000001u
/**
* @brief One overlap, described well enough to undo it.
*
* The narrowphase fills in the geometry. Which actor, which tile, and whether
* either side was a sensor are filled in by the layer above, which is the only
* one that knows.
*/
typedef struct akgl_Contact {
struct akgl_Actor *self; /**< The actor being told about this contact. Filled in by the resolver, not the narrowphase. */
struct akgl_Actor *other; /**< The other actor, or `NULL` when the hit was map geometry with no actor behind it. */
int32_t tilex; /**< Tile column that was hit, or -1 when the other side was not a tile. */
int32_t tiley; /**< Tile row, or -1. */
int32_t tilelayer; /**< Index into the tilemap's layers, or -1. */
int32_t tilegid; /**< Global tile id that was hit, or 0. This is what tells a spike from a floor without a second lookup. */
float32_t nx; /**< Contact normal, unit length. **Points out of the other shape and toward this one**, so moving along it by #depth separates them. */
float32_t ny; /**< Normal along y. Negative means the surface is *below*, since y grows downward. */
float32_t nz; /**< Normal along z. Always 0 when #AKGL_COLLISION_TEST_PLANAR was used. */
float32_t depth; /**< How far along the normal to move to stop overlapping, in map pixels. Never negative. */
float32_t px; /**< A point on the overlap, in map pixels. Approximate for a non-box pair; see below. */
float32_t py; /**< Contact point along y. */
float32_t pz; /**< Contact point along z. */
float32_t dt; /**< Length of the sub-step this contact was found in, in seconds. Filled in by the resolver. */
bool sensor; /**< Either side carries #AKGL_COLLISION_FLAG_SENSOR: report, do not push. Filled in by the resolver. */
bool statichit; /**< The other side does not move -- a tile, or a proxy flagged #AKGL_COLLISION_FLAG_STATIC. */
} akgl_Contact;
/**
* @brief Test two positioned shapes, and describe the overlap if there is one.
*
* Three paths, cheapest first. The proxies' bounds reject most pairs outright.
* A box against a box is answered in closed form -- exact depth, exactly
* axis-aligned normal, no iteration -- which matters because a tile game is
* almost entirely boxes, and because a resting actor wants a normal that is
* precisely `(0, -1, 0)` rather than one converged to within a tolerance, or it
* creeps. Everything else goes to the iterative narrowphase.
*
* @note **The contact point is approximate for anything but a box pair.** The
* iterative solver returns a point on the portal it converged to, which
* for a deep off-centre overlap can sit noticeably away from the deepest
* point. The depth and the normal are not approximate, and the blocking
* resolver uses only those. Do not build a damage falloff on the point.
*
* @param a First proxy. Required.
* @param b Second proxy. Required.
* @param flags Bitwise OR of the `AKGL_COLLISION_TEST_*` values.
* @param dest Receives the contact geometry when @p hit comes back `true`.
* Required. The normal points out of @p b and toward @p a.
* @param hit Receives whether the two overlap. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If any pointer argument is `NULL`.
* @throws AKGL_ERR_COLLISION If the solver could not characterise an
* intersection it found -- a degenerate shape, or the arena running out.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_test(akgl_CollisionProxy *a, akgl_CollisionProxy *b, uint32_t flags, akgl_Contact *dest, bool *hit);
Add a pluggable broad phase, and the incremental uniform grid behind it akgl_Partitioner is a record of function pointers and an initializer, the same shape as the render and physics backends. `move` is its own slot rather than remove-then-insert, and that is the whole design: a uniform grid's move is a comparison and a return when the proxy has not left the cells it was in, which is the steady state for a walking actor and the permanent state for a static one. Spelling it as remove-and-insert would turn the incremental grid into the rebuild-every-frame tree that PERFORMANCE.md already measured and rejected. The grid is a dense 128x128 array of cell heads over the world, cells keyed on tile size, with two intrusive chains per entry -- one through the cell so a query can walk it, one through the proxy so removal unlinks a whole span without searching. Everything is an int16_t index rather than a pointer, so the structure is relocatable and clearing it is a memset. A proxy covering more cells than AKGL_COLLISION_GRID_MAX_SPAN spills onto one chain every query walks, which is what makes the cell pool's ceiling provable rather than hopeful. tests/partition.c compares every query against a linear scan over the same proxies and asserts containment in one direction, not equality. That asymmetry is the contract: over-reporting costs a narrowphase call, under-reporting is a wall an actor walks through. The suite is a table over partitioners so the same assertions run against every implementation, which is what the second one landing later will be held to. Three deliberate breaks, and two of them were not caught the first time: - Removing the min-cell rule so a shared-cell pair reports repeatedly: caught. - Dropping the unlink in `move`: **not caught**, initially. Stale entries do not produce wrong query answers, because the bounds re-check filters them out -- they leak the cell pool until the grid stops working, on a timescale no unit test reaches by accident. Counting live pool entries across 200 moves is the only thing that sees it, and now does: 505 entries against an expected 5. - Swapping floorf for a truncating cast: **not caught, and correctly so.** Everything below zero is clamped to cell 0 either way, so today the two are indistinguishable. The comment claiming truncation is a defect was overstating it and now says what is actually true: the clamp is the only thing hiding it, and a negative world origin or a relaxed clamp would make it real with no test standing in front of it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 06:29:02 -04:00
/** @brief Longest partitioner name, including the terminator. */
#define AKGL_PARTITIONER_MAX_NAME_LENGTH 32
/**
* @brief Visits one proxy a query found. Raise `AKERR_ITERATOR_BREAK` to stop early.
*
* A query is allowed to over-report and forbidden to under-report, so a visitor
* will see proxies that turn out not to overlap. Rejecting those is the
* narrowphase's job and it is cheap; missing one is a wall an actor walks
* through.
*/
typedef akerr_ErrorContext AKERR_NOIGNORE *(*akgl_CollisionVisitFunc)(akgl_CollisionProxy *proxy, void *data);
/** @brief Visits one candidate pair. Raise `AKERR_ITERATOR_BREAK` to stop early. */
typedef akerr_ErrorContext AKERR_NOIGNORE *(*akgl_CollisionPairFunc)(akgl_CollisionProxy *a, akgl_CollisionProxy *b, void *data);
/**
* @brief A pluggable broad phase: a record of function pointers and an initializer.
*
* The same shape as akgl_RenderBackend and akgl_PhysicsBackend, for the same
* reason -- behaviour that varies is a slot to fill, not a branch to add.
*
* @section partitioner_move Why `move` is its own slot
*
* It could be spelled remove-then-insert, and that would throw away the entire
* argument for the structure that is installed by default. A uniform grid's
* `move` is a comparison and a return when the proxy has not left the cells it
* was in, which is the steady state for a walking actor and forever for a static
* one. Collapsing it into remove-and-insert turns the incremental grid into the
* rebuild-every-frame tree that was measured and rejected.
*/
typedef struct akgl_Partitioner {
char name[AKGL_PARTITIONER_MAX_NAME_LENGTH]; /**< Which implementation is installed. Diagnostic, and what the tests use to report which row failed. */
akerr_ErrorContext AKERR_NOIGNORE *(*reset)(struct akgl_Partitioner *self, struct akgl_CollisionWorld *world); /**< Drop every proxy and re-derive the structure's geometry from the world. */
akerr_ErrorContext AKERR_NOIGNORE *(*insert)(struct akgl_Partitioner *self, akgl_CollisionProxy *proxy); /**< Add a proxy at its current bounds. */
akerr_ErrorContext AKERR_NOIGNORE *(*remove)(struct akgl_Partitioner *self, akgl_CollisionProxy *proxy); /**< Take a proxy out. Removing one that is not in is success, not an error. */
akerr_ErrorContext AKERR_NOIGNORE *(*move)(struct akgl_Partitioner *self, akgl_CollisionProxy *proxy); /**< The proxy's bounds changed. See the note above on why this is not remove-then-insert. */
akerr_ErrorContext AKERR_NOIGNORE *(*query)(struct akgl_Partitioner *self, SDL_FRect *area, uint32_t mask, akgl_CollisionVisitFunc visit, void *data); /**< Visit every proxy that may overlap @p area and whose `layermask` intersects @p mask. Each proxy is visited at most once. */
akerr_ErrorContext AKERR_NOIGNORE *(*each_pair)(struct akgl_Partitioner *self, akgl_CollisionPairFunc visit, void *data); /**< Visit every candidate pair exactly once. */
void *state; /**< The implementation's private storage. Never dereferenced outside the file that set it. */
} akgl_Partitioner;
/**
* @brief Broad-phase geometry and the partitioner that indexes it.
*
* One of these exists per world. It is reached by pointer and never embedded by
* value in anything, because the structures behind it are measured in tens of
* kilobytes and `akgl_Tilemap` is already large enough to matter.
*/
typedef struct akgl_CollisionWorld {
akgl_Partitioner partitioner; /**< The broad phase. By value: it is a vtable and a `void *`, not storage. */
float32_t cellwidth; /**< Broad-phase cell width in map pixels. */
float32_t cellheight; /**< Broad-phase cell height. */
float32_t originx; /**< World x the cell grid is measured from. 0 for a Tiled map. */
float32_t originy; /**< World y the cell grid is measured from. */
uint32_t flags; /**< `AKGL_COLLISION_TEST_*` bits applied to every test this world runs. */
uint32_t sweep; /**< Serial of the current pass. Stamped onto proxies so one spanning several cells is reported once. */
uint32_t tests; /**< Narrowphase calls during the last pass. Diagnostic. */
} akgl_CollisionWorld;
/**
* @brief Bring up a collision world with a named partitioner.
*
* @param self The world. Required. Zeroed first.
* @param type `"grid"` for the incremental uniform grid, which is the default
* and what a game should use, or `NULL` for the same. Matched on
* leading characters, like akgl_physics_factory.
* @param cellwidth Cell width in map pixels. Pass the map's tile width. Must be positive.
* @param cellheight Cell height in map pixels. Must be positive.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_VALUE If either cell dimension is not positive.
* @throws AKERR_KEY If @p type names no partitioner. The message quotes it.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_world_init(akgl_CollisionWorld *self, char *type, float32_t cellwidth, float32_t cellheight);
/**
* @brief Install a partitioner by name.
*
* Adding an implementation means adding a name here, not a branch anywhere else.
*
* @param self The partitioner to fill in. Required.
* @param type `"grid"`, or `NULL` for the default. Matched on leading characters.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
* @throws AKERR_KEY If @p type matches nothing.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_partitioner_factory(akgl_Partitioner *self, char *type);
/**
* @brief Install the incremental uniform grid.
*
* The default, and the structure the performance record argues for: cells keyed
* on tile size, static storage, and insert and remove that happen only when a
* proxy crosses a cell boundary. A proxy that has not left its cells costs
* nothing to update, and a static one costs nothing ever.
*
* @param self The partitioner to fill in. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_partitioner_init_grid(akgl_Partitioner *self);
Pool collision proxies, and tie one to the life of its actor A proxy is what the broad phase will index: an actor's shape, where it is, and the bounds that follow from those. It is a sixth heap layer, two per actor -- one for the actor itself and headroom for static geometry a game registers that is not tile-aligned. Solid *tiles* deliberately get none: the broad phase will read those out of the tilemap's own array, because one proxy per tile is tens of megabytes to index data that is already a grid. The pool follows the existing convention rather than improving on it. akgl_heap_next_collision_proxy finds a free slot and does not claim it; akgl_collision_proxy_initialize claims it. That is TODO.md item 8's asymmetry, and it is kept on purpose: an acquire abandoned before initialization leaks nothing, because the slot still reads as free, and a sixth pool with its own rule would be worse than one defect with five instances. Fixing item 8 means fixing all five together with every call site audited, and that is its own change. The proxy holds a *copy* of the shape rather than a pointer. An actor's own logic may rewrite its shape mid-step -- a character crouching, a projectile arming -- and a broad phase whose bounds came from a shape that has since changed is a bug that only appears when two things happen in the same frame. akgl_heap_release_actor now releases the actor's proxy. Without it the proxy holds a borrowed `owner` into a slot that has just been zeroed, so the broad phase keeps a registration whose owner reads as a free actor, and the next contact against it is a collision with nothing. Verified by removing the release and watching test_proxy_dies_with_its_actor go red. The tests pin the convention as well as the behaviour: that two acquires with no initialize between them return the same slot is asserted, not merely tolerated, so that changing the convention has to change a test that says why it exists. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:56:21 -04:00
/*
* The following is part of the internal API. Proxies are created and destroyed
* by the library, not by a game.
*/
/**
* @brief Claim a pooled proxy for an actor's shape and take the reference on it.
*
* Follows the convention every pool here uses: akgl_heap_next_collision_proxy
* finds a free slot and does **not** claim it, and this takes the reference.
* Write the two adjacent, because until the reference is taken the slot is still
* free and the next acquire hands out the same pointer. That is `TODO.md` "Known
* and still open" item 8, it applies to four of the five existing pools, and
* this one does not depart from it -- a sixth convention would be worse than the
* defect.
*
* @param obj The proxy to initialize. Required.
* @param owner The actor it tracks, or `NULL` for static geometry.
* @param shape The shape to copy into it. Required.
* @param x World position of the owner.
* @param y World position of the owner.
* @param z World position of the owner.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p shape is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_proxy_initialize(akgl_CollisionProxy *obj, struct akgl_Actor *owner, akgl_CollisionShape *shape, float32_t x, float32_t y, float32_t z);
/**
* @brief Recompute a proxy's bounds from its owner's current shape and position.
*
* Called once per step per live proxy, before the broad phase is asked anything.
* This is the one place the shape copy is refreshed.
*
* @param obj The proxy. Required.
* @param shape The current shape to copy. Required.
* @param x Current world position of the owner.
* @param y Current world position of the owner.
* @param z Current world position of the owner.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj or @p shape is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_proxy_sync(akgl_CollisionProxy *obj, akgl_CollisionShape *shape, float32_t x, float32_t y, float32_t z);
Add a pluggable broad phase, and the incremental uniform grid behind it akgl_Partitioner is a record of function pointers and an initializer, the same shape as the render and physics backends. `move` is its own slot rather than remove-then-insert, and that is the whole design: a uniform grid's move is a comparison and a return when the proxy has not left the cells it was in, which is the steady state for a walking actor and the permanent state for a static one. Spelling it as remove-and-insert would turn the incremental grid into the rebuild-every-frame tree that PERFORMANCE.md already measured and rejected. The grid is a dense 128x128 array of cell heads over the world, cells keyed on tile size, with two intrusive chains per entry -- one through the cell so a query can walk it, one through the proxy so removal unlinks a whole span without searching. Everything is an int16_t index rather than a pointer, so the structure is relocatable and clearing it is a memset. A proxy covering more cells than AKGL_COLLISION_GRID_MAX_SPAN spills onto one chain every query walks, which is what makes the cell pool's ceiling provable rather than hopeful. tests/partition.c compares every query against a linear scan over the same proxies and asserts containment in one direction, not equality. That asymmetry is the contract: over-reporting costs a narrowphase call, under-reporting is a wall an actor walks through. The suite is a table over partitioners so the same assertions run against every implementation, which is what the second one landing later will be held to. Three deliberate breaks, and two of them were not caught the first time: - Removing the min-cell rule so a shared-cell pair reports repeatedly: caught. - Dropping the unlink in `move`: **not caught**, initially. Stale entries do not produce wrong query answers, because the bounds re-check filters them out -- they leak the cell pool until the grid stops working, on a timescale no unit test reaches by accident. Counting live pool entries across 200 moves is the only thing that sees it, and now does: 505 entries against an expected 5. - Swapping floorf for a truncating cast: **not caught, and correctly so.** Everything below zero is clamped to cell 0 either way, so today the two are indistinguishable. The comment claiming truncation is a defect was overstating it and now says what is actually true: the clamp is the only thing hiding it, and a negative world origin or a relaxed clamp would make it real with no test standing in front of it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 06:29:02 -04:00
/**
* @brief Claim a pooled grid cell entry, pointing it at a proxy and a cell.
*
* The other half of the acquire-then-initialize pair, as for proxies.
*
* @param obj The entry. Required.
* @param proxyidx Index of the proxy in the proxy pool.
* @param cell Cell number, or -1 for the oversized chain.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p obj is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_cell_initialize(akgl_CollisionCell *obj, int16_t proxyidx, int32_t cell);
Add collision shapes, on the character and overridable per actor A shape is a convex volume positioned relative to an actor's origin. It lives on akgl_Character, so every goblin sharing a character shares one shape definition the way they already share speeds and the state-to-sprite map, and an actor may override it with akgl_Actor::shape_override. The flag is not redundant with an empty shape. "This actor deliberately has no collider" and "this actor has not been given one 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. include/akgl/collision.h is a leaf: it names actors and tilemaps as incomplete struct pointers rather than including their headers, because both of those need akgl_CollisionShape by value and a cycle forms otherwise. The `headers` suite compiles it as the first include of a translation unit, so that property is enforced rather than merely intended -- which is why the tilemap types got struct tags two commits ago. The masks are asymmetric and the defaults are the load-bearing part: layermask ACTOR, collidemask STATIC. An actor given a shape and nothing else collides with map geometry and with no other actor. "Everything with a hitbox shoves everything else" would be 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 bit, opting out of it would have been a hunt through every NPC a game spawns. Every shape gets a z depth, because the narrowphase behind this is three dimensional and answers with the *minimum* separating translation. A thin extrusion makes z the cheapest axis, and an actor pushed along z goes nowhere a player can see while remaining inside the floor -- a collision system reporting success while doing nothing. The ratio of 2 is the smallest for which the z overlap of any two shapes the setters build provably exceeds any planar penetration they can reach. The test for that asserts the inequality across a spread of sizes rather than asserting the constant is 2, so a change to the constant fails here rather than in a game. Two things about that test are worth recording, because the first version had both: - It could not fail. TEST_ASSERT expands to FAIL_BREAK, which reports by `break`ing, and the assertion was inside a nested `for` -- so the break left the loop rather than the ATTEMPT block and the failure evaporated. This is the hazard AGENTS.md documents for CATCH; it applies to the whole family. Verified by setting the ratio to 0.5 and watching the suite still pass, then fixing it and watching it report the 512x512 case by name. - tests/collision_arena.c had the same bug in a loop added in the previous commit, and it is fixed here too. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 23:48:05 -04:00
/**
* @brief Build an axis-aligned box from the frame-relative rectangle a game has.
*
* @p body is an offset and a size measured from the actor's position -- the same
* form a game already writes a hitbox in, inset into its sprite frame because
* sprite art does not reach the edges of its cell. It is converted here to the
* centre-and-half-extent form everything downstream uses.
*
* The shape is zeroed first, so every field not named by the arguments ends at a
* known value: no flags, and the default masks of #AKGL_COLLISION_LAYER_ACTOR on
* and #AKGL_COLLISION_LAYER_STATIC responded to. That default is chosen so an
* actor given a shape and nothing else collides with the map and with nothing
* else -- a town full of NPCs does not start shoving itself around because
* somebody gave the townsfolk hitboxes.
*
* @param dest Receives the shape. Required.
* @param body Offset and size in map pixels, relative to the actor's position.
* Required. Both `w` and `h` must be positive.
* @param depth Half-extent along z. Pass 0 for a 2D game and the extrusion is
* chosen for you; see the note on this file for why that matters.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p body is `NULL`.
* @throws AKERR_VALUE If `body->w` or `body->h` is not positive, or @p depth is
* negative.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_shape_box(akgl_CollisionShape *dest, SDL_FRect *body, float32_t depth);
/**
* @brief Build a circle, extruded along z into a cylinder.
*
* A cylinder and not a sphere: a sphere's caps would round away from the plane
* and let a shape slip past a corner in a way a 2D game never expects.
*
* @param dest Receives the shape. Required.
* @param ox Centre offset from the actor's `x`, in map pixels.
* @param oy Centre offset from the actor's `y`.
* @param radius Radius in map pixels. Must be positive.
* @param depth Half-extent along z, or 0 to have it chosen.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest is `NULL`.
* @throws AKERR_VALUE If @p radius is not positive, or @p depth is negative.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_shape_circle(akgl_CollisionShape *dest, float32_t ox, float32_t oy, float32_t radius, float32_t depth);
/**
* @brief Build a capsule: a box with semicircular caps on one axis.
*
* Useful for a character that should slide off a corner rather than catch on it,
* which a box does and a capsule does not.
*
* @param dest Receives the shape. Required.
* @param body Offset and size in map pixels, as for akgl_collision_shape_box.
* Required.
* @param axis #AKGL_COLLISION_SHAPE_CAPSULE_X or #AKGL_COLLISION_SHAPE_CAPSULE_Y.
* @param depth Half-extent along z, or 0 to have it chosen.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p body is `NULL`.
* @throws AKERR_VALUE If either side is not positive, @p depth is negative, @p
* axis is neither capsule kind, or the capped axis is not the longer one
* -- a capsule whose caps are wider than it is long is a circle written
* confusingly, and is refused rather than silently reinterpreted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_shape_capsule(akgl_CollisionShape *dest, SDL_FRect *body, uint8_t axis, float32_t depth);
/**
* @brief The planar bounds a shape occupies with its owner at a given position.
*
* This is what the broad phase indexes on and what a cheap overlap test uses
* before anything more expensive runs. `z` is not represented: the extrusion
* exists to keep the narrowphase honest, not to be searched.
*
* @param shape The shape. Required.
* @param x The owner's `x`.
* @param y The owner's `y`.
* @param dest Receives the bounds in map pixels. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p shape or @p dest is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_shape_bounds(akgl_CollisionShape *shape, float32_t x, float32_t y, SDL_FRect *dest);
/**
* @brief Whether two shapes are allowed to interact, in the given direction.
*
* Asymmetric, and that is the point: @p self responds to @p other when @p
* other's `layermask` intersects @p self's `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.
*
* A shape of kind #AKGL_COLLISION_SHAPE_NONE, or one carrying
* #AKGL_COLLISION_FLAG_DISABLED, interacts with nothing in either direction.
*
* @param self The shape doing the responding. Required.
* @param other The shape being responded to. Required.
* @param dest Receives whether the pair is worth testing. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If any argument is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_shape_interacts(akgl_CollisionShape *self, akgl_CollisionShape *other, bool *dest);
#endif