/** * @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 #include #include #include #include struct akgl_Actor; struct akgl_Tilemap; /** @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. */ } akgl_CollisionProxy; /** * @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); /* * 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 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