/** * @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 #include #include /** * @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); }