184 lines
6.8 KiB
C
184 lines
6.8 KiB
C
|
|
/**
|
||
|
|
* @file collision_shape.c
|
||
|
|
* @brief Building collision shapes, and the one invariant that keeps 2D flat.
|
||
|
|
*
|
||
|
|
* Nothing here touches the narrowphase. A shape is data, and this file is the
|
||
|
|
* only place that decides what a well-formed one looks like -- which is why the
|
||
|
|
* extrusion rule lives in the setters rather than at the point of use, where
|
||
|
|
* every caller would have to remember it.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akgl/collision.h>
|
||
|
|
#include <akgl/error.h>
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Fill in the depth, unless the caller asked for a specific one.
|
||
|
|
*
|
||
|
|
* Called last by every setter, once `hx` and `hy` are known. See collision.h for
|
||
|
|
* why the ratio is what it is; the short version is that a thin extrusion makes
|
||
|
|
* z the cheapest axis to separate on, and an actor pushed along z goes nowhere a
|
||
|
|
* player can see while falling through the floor.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *shape_extrude(akgl_CollisionShape *dest, float32_t depth)
|
||
|
|
{
|
||
|
|
float32_t largest = 0.0f;
|
||
|
|
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL shape reference");
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (depth < 0.0f), AKERR_VALUE,
|
||
|
|
"Shape depth %f is negative", depth);
|
||
|
|
|
||
|
|
if ( depth > 0.0f ) {
|
||
|
|
dest->hz = depth;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
largest = dest->hx;
|
||
|
|
if ( dest->hy > largest ) {
|
||
|
|
largest = dest->hy;
|
||
|
|
}
|
||
|
|
dest->hz = AKGL_COLLISION_DEPTH_RATIO * largest;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Zero a shape and give it the defaults every setter shares.
|
||
|
|
*
|
||
|
|
* The masks are the interesting part. An actor that has been given a shape and
|
||
|
|
* nothing else collides with map geometry and with no other actor, because
|
||
|
|
* "everything with a hitbox shoves everything else" is a surprising default for
|
||
|
|
* a town full of scenery and a painful one to discover after the fact. Opting
|
||
|
|
* in to actor-versus-actor is one added bit; opting out of it would have been a
|
||
|
|
* hunt through every NPC a game spawns.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *shape_defaults(akgl_CollisionShape *dest, uint8_t kind)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL shape reference");
|
||
|
|
|
||
|
|
memset(dest, 0x00, sizeof(akgl_CollisionShape));
|
||
|
|
dest->kind = kind;
|
||
|
|
dest->layermask = AKGL_COLLISION_LAYER_ACTOR;
|
||
|
|
dest->collidemask = AKGL_COLLISION_LAYER_STATIC;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akgl_collision_shape_box(akgl_CollisionShape *dest, SDL_FRect *body, float32_t depth)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL shape reference");
|
||
|
|
FAIL_ZERO_RETURN(errctx, body, AKERR_NULLPOINTER, "NULL rectangle reference");
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (body->w <= 0.0f), AKERR_VALUE,
|
||
|
|
"Box width %f is not positive", body->w);
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (body->h <= 0.0f), AKERR_VALUE,
|
||
|
|
"Box height %f is not positive", body->h);
|
||
|
|
|
||
|
|
PASS(errctx, shape_defaults(dest, AKGL_COLLISION_SHAPE_BOX));
|
||
|
|
|
||
|
|
// Top-left-and-size in, centre-and-half-extent out. Done once here so that
|
||
|
|
// nothing on the frame path has to do it per test.
|
||
|
|
dest->hx = body->w / 2.0f;
|
||
|
|
dest->hy = body->h / 2.0f;
|
||
|
|
dest->ox = body->x + dest->hx;
|
||
|
|
dest->oy = body->y + dest->hy;
|
||
|
|
|
||
|
|
PASS(errctx, shape_extrude(dest, depth));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akgl_collision_shape_circle(akgl_CollisionShape *dest, float32_t ox, float32_t oy, float32_t radius, float32_t depth)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL shape reference");
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (radius <= 0.0f), AKERR_VALUE,
|
||
|
|
"Circle radius %f is not positive", radius);
|
||
|
|
|
||
|
|
PASS(errctx, shape_defaults(dest, AKGL_COLLISION_SHAPE_CIRCLE));
|
||
|
|
dest->ox = ox;
|
||
|
|
dest->oy = oy;
|
||
|
|
dest->hx = radius;
|
||
|
|
dest->hy = radius;
|
||
|
|
PASS(errctx, shape_extrude(dest, depth));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akgl_collision_shape_capsule(akgl_CollisionShape *dest, SDL_FRect *body, uint8_t axis, float32_t depth)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL shape reference");
|
||
|
|
FAIL_ZERO_RETURN(errctx, body, AKERR_NULLPOINTER, "NULL rectangle reference");
|
||
|
|
FAIL_NONZERO_RETURN(
|
||
|
|
errctx,
|
||
|
|
((axis != AKGL_COLLISION_SHAPE_CAPSULE_X) && (axis != AKGL_COLLISION_SHAPE_CAPSULE_Y)),
|
||
|
|
AKERR_VALUE,
|
||
|
|
"Capsule axis %u is neither AKGL_COLLISION_SHAPE_CAPSULE_X nor _Y",
|
||
|
|
(unsigned int)axis
|
||
|
|
);
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (body->w <= 0.0f), AKERR_VALUE,
|
||
|
|
"Capsule width %f is not positive", body->w);
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (body->h <= 0.0f), AKERR_VALUE,
|
||
|
|
"Capsule height %f is not positive", body->h);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* The capped axis has to be the longer one. A capsule whose caps are wider
|
||
|
|
* than the body is long is a circle with extra steps, and accepting it would
|
||
|
|
* mean the shape a caller described and the shape the narrowphase built were
|
||
|
|
* different -- silently, and only for some sizes.
|
||
|
|
*/
|
||
|
|
if ( axis == AKGL_COLLISION_SHAPE_CAPSULE_X ) {
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (body->w <= body->h), AKERR_VALUE,
|
||
|
|
"An x-axis capsule is %fx%f; the capped axis must be the longer one",
|
||
|
|
body->w, body->h);
|
||
|
|
} else {
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (body->h <= body->w), AKERR_VALUE,
|
||
|
|
"A y-axis capsule is %fx%f; the capped axis must be the longer one",
|
||
|
|
body->w, body->h);
|
||
|
|
}
|
||
|
|
|
||
|
|
PASS(errctx, shape_defaults(dest, axis));
|
||
|
|
dest->hx = body->w / 2.0f;
|
||
|
|
dest->hy = body->h / 2.0f;
|
||
|
|
dest->ox = body->x + dest->hx;
|
||
|
|
dest->oy = body->y + dest->hy;
|
||
|
|
PASS(errctx, shape_extrude(dest, depth));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akgl_collision_shape_bounds(akgl_CollisionShape *shape, float32_t x, float32_t y, SDL_FRect *dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_ZERO_RETURN(errctx, shape, AKERR_NULLPOINTER, "NULL shape reference");
|
||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL rectangle reference");
|
||
|
|
|
||
|
|
dest->x = x + shape->ox - shape->hx;
|
||
|
|
dest->y = y + shape->oy - shape->hy;
|
||
|
|
dest->w = shape->hx * 2.0f;
|
||
|
|
dest->h = shape->hy * 2.0f;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akgl_collision_shape_interacts(akgl_CollisionShape *self, akgl_CollisionShape *other, bool *dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "NULL shape reference");
|
||
|
|
FAIL_ZERO_RETURN(errctx, other, AKERR_NULLPOINTER, "NULL other shape reference");
|
||
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL result reference");
|
||
|
|
|
||
|
|
*dest = false;
|
||
|
|
if ( (self->kind == AKGL_COLLISION_SHAPE_NONE) ||
|
||
|
|
(other->kind == AKGL_COLLISION_SHAPE_NONE) ) {
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
if ( ((self->flags & AKGL_COLLISION_FLAG_DISABLED) != 0) ||
|
||
|
|
((other->flags & AKGL_COLLISION_FLAG_DISABLED) != 0) ) {
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
// One direction only. The mirrored question is asked separately, and may
|
||
|
|
// legitimately have a different answer.
|
||
|
|
*dest = ((other->layermask & self->collidemask) != 0);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|