A tile layer with a `collidable` boolean custom property is collision geometry. Before this a game had to hard-code a layer index -- both examples do, because akgl_TilemapLayer does not retain the name Tiled wrote -- and that index changes the moment somebody reorders layers in the editor. Solid tiles are **not** given proxies. At the maximum map size that is a quarter of a million per layer, tens of megabytes of index to describe data that is already a dense grid sitting in the tilemap. The world keeps a borrowed pointer and reads layers[i].data[] over whatever cell range a query covers: nine array reads for a 32-pixel actor on 16-pixel tiles, nothing to build at level load, and nothing to maintain per frame. Static geometry that is not tile-aligned is still an ordinary proxy with AKGL_COLLISION_FLAG_STATIC; both mechanisms exist and tiles use the free one because there are a hundred thousand of them. Four public queries come with it, all of which answer without resolving: solid_at, box_blocked, query_box and settle. They are what a game reaches for when it wants to know rather than to be pushed -- a ledge probe ahead of a walking enemy, a check that a doorway is clear -- and the sidescroller cannot drop its hand-rolled collision without them. akgl_collision_settle is the one worth naming. 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, so settling walks a shape up a tile at a time and refuses loudly rather than searching forever. Two defects found by writing the tests: - The fixture put an akgl_Tilemap on the stack and segfaulted before the first assertion. It is about 26 MB -- the layer and tileset arrays are sized for the worst case the format allows -- and tilemap.h says so. It is static now, with a comment saying why, because the next person to write a map fixture will reach for a local first as well. - The far-edge nudge used AKGL_COLLISION_EPSILON, which is 1e-6. That is a sensible tolerance on a unit vector and a meaningless one on a map coordinate: `float` carries about seven significant digits, so at a coordinate of 144 the smallest representable step is around 1.5e-5 and `144.0f - 1e-6f` is exactly 144.0f. The nudge did nothing, a box resting flush on the floor read as inside it, and every move it tried looked blocked -- an actor standing on the ground unable to walk. Two different quantities were sharing one constant; the tile one is now its own, at a thousandth of a pixel, which is what the sidescroller example independently arrived at. Both breaks verified: removing the nudge and ignoring the `collidable` bit each turn the suite red with the symptom named. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
613 lines
34 KiB
C
613 lines
34 KiB
C
/**
|
|
* @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;
|
|
struct akgl_CollisionWorld;
|
|
|
|
/** @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;
|
|
|
|
/**
|
|
* @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. */
|
|
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. */
|
|
} akgl_CollisionProxy;
|
|
|
|
/**
|
|
* @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;
|
|
|
|
/**
|
|
* @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);
|
|
|
|
/** @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. */
|
|
|
|
struct akgl_Tilemap *tilesource;/**< The map whose solid tile layers are static geometry, or `NULL` for none. Borrowed. */
|
|
uint32_t tilelayers; /**< Which of that map's layers are solid, copied from its `collidablelayers`. */
|
|
uint32_t tilelayermask; /**< The collision layer solid tiles are treated as being on. #AKGL_COLLISION_LAYER_STATIC by default. */
|
|
} 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);
|
|
|
|
/**
|
|
* @brief Make a map's solid layers count as collision geometry.
|
|
*
|
|
* @section collision_tiles Tiles are read, not registered
|
|
*
|
|
* A map's solid tiles are **not** given proxies. At the maximum map size that
|
|
* would be a quarter of a million of them per layer -- tens of megabytes of
|
|
* index, to describe data that is already a dense grid sitting in the tilemap.
|
|
* Instead the world keeps a borrowed pointer to the map and reads
|
|
* `layers[i].data[]` directly over whatever cell range a query covers, which is
|
|
* nine array reads for a 32-pixel actor on 16-pixel tiles and costs nothing to
|
|
* maintain when a level loads.
|
|
*
|
|
* 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.
|
|
*
|
|
* @param self The world. Required.
|
|
* @param map The map, or `NULL` to detach the one already bound.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p self is `NULL`.
|
|
* @throws AKERR_VALUE If the map's tile dimensions are not positive.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_bind_tilemap(akgl_CollisionWorld *self, struct akgl_Tilemap *map);
|
|
|
|
/**
|
|
* @brief Is the tile under this point solid?
|
|
*
|
|
* A point query against the bound map's collidable layers, and nothing else --
|
|
* it does not consult proxies. Off the map is **not** solid: a game that wants
|
|
* an edge of the world puts one there, and a pit that kills the player has to be
|
|
* a pit rather than a wall.
|
|
*
|
|
* @param self The world. Required.
|
|
* @param x World x in map pixels.
|
|
* @param y World y in map pixels.
|
|
* @param dest Receives whether a solid tile covers that point. Required.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If @p self or @p dest is `NULL`.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_solid_at(akgl_CollisionWorld *self, float32_t x, float32_t y, bool *dest);
|
|
|
|
/**
|
|
* @brief Would this rectangle overlap anything solid?
|
|
*
|
|
* Tiles and proxies both. This is the query a game reaches for when it wants an
|
|
* answer without a response: a ledge probe ahead of a walking enemy, a check
|
|
* that a spawn point is clear, a door that only opens when nothing is standing
|
|
* in it.
|
|
*
|
|
* @param self The world. Required.
|
|
* @param box The rectangle, in map pixels. Required.
|
|
* @param mask Which collision layers count. #AKGL_COLLISION_LAYER_ALL for
|
|
* everything, #AKGL_COLLISION_LAYER_STATIC for map geometry only.
|
|
* @param dest Receives the answer. Required.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If any pointer argument is `NULL`.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_box_blocked(akgl_CollisionWorld *self, SDL_FRect *box, uint32_t mask, bool *dest);
|
|
|
|
/**
|
|
* @brief Visit every proxy that may overlap a rectangle.
|
|
*
|
|
* The broad phase, exposed. Tiles are not proxies and so are not visited; use
|
|
* akgl_collision_box_blocked when the question includes map geometry.
|
|
*
|
|
* @param self The world. Required.
|
|
* @param box The rectangle, in map pixels. Required.
|
|
* @param mask Which collision layers to report.
|
|
* @param visit Called once per candidate. Required.
|
|
* @param data Passed through to @p visit.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If any pointer argument is `NULL`.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_query_box(akgl_CollisionWorld *self, SDL_FRect *box, uint32_t mask, akgl_CollisionVisitFunc visit, void *data);
|
|
|
|
/**
|
|
* @brief Lift a shape out of the geometry it was placed inside.
|
|
*
|
|
* Resolution stops a shape *entering* geometry and has nothing to say about one
|
|
* that started inside it -- what it does instead is refuse every move, so an
|
|
* actor spawned in a wall is simply stuck. That is not a hypothetical: level
|
|
* authors place a character on a step and Tiled rounds the object to a position
|
|
* that overlaps the tile below it.
|
|
*
|
|
* This walks the shape up a tile at a time until it is clear, and refuses rather
|
|
* than searching forever if it cannot be. Call it once, after a map load, for
|
|
* anything the map placed.
|
|
*
|
|
* @param self The world. Required.
|
|
* @param shape The shape being placed. Required.
|
|
* @param x Position in map pixels. Read and written.
|
|
* @param y Position in map pixels. Read and written; this is what moves.
|
|
* @param maxsteps How many tiles to try before giving up. 0 uses a sensible default.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER If any pointer argument is `NULL`.
|
|
* @throws AKERR_VALUE If the shape is still inside geometry after @p maxsteps.
|
|
* The message carries the position, because the answer is almost always
|
|
* to move the object in the level rather than to raise the limit.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_settle(akgl_CollisionWorld *self, akgl_CollisionShape *shape, float32_t *x, float32_t *y, int maxsteps);
|
|
|
|
/*
|
|
* 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);
|
|
|
|
/**
|
|
* @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);
|
|
|
|
/**
|
|
* @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
|