spherecast

wip first person shooter engine
Download | Log | Files | Refs

commit 56a7e3e6c9f8fb07d8be9e4f895068a733362346
parent 1ef521e3f19c49ae5b61f95c3c6794565fee909c
Author: amrfti <andrew@kloet.net>
Date:   Wed,  7 Jan 2026 07:26:40 -0500

Rebase player, still some issues with ground detection on the edges of
slopes. Reports crazy normals if the player stands on an edge.

Diffstat:
Mconfig.h | 4++--
Mmain.c | 27+++++++++++++--------------
Mphysics.c | 148+++++++++++++++++++++++++++++++------------------------------------------------
Mphysics.h | 5+++++
Mplayer.c | 94++++++++++++++++++++++++++++++++++++-------------------------------------------
Mplayer.h | 6+++---
Atypes.h | 11+++++++++++
7 files changed, 135 insertions(+), 160 deletions(-)

diff --git a/config.h b/config.h @@ -9,7 +9,7 @@ #define MAX_SLIDES 4 #define SKIN 0.001f #define GRAVITY 50.0f -#define MAX_GROUND_ANGLE_DEG 50.0f +#define MAX_GROUND_ANGLE_DEG 60.0f #define GROUND_NORMAL_Y (cosf(MAX_GROUND_ANGLE_DEG * DEG2RAD)) #define DOWN (Vector3){0, -1, 0} #define UP (Vector3){0, 1, 0} @@ -36,7 +36,7 @@ #define GROUND_FRICTION 10.0f #define AIR_FRICTION 0.5f -#define JUMP_FORCE 10.0f +#define JUMP_FORCE 1.0f #define SLIDE_FRICTION 1.5f #define SLIDE_CONTROL 8.0f diff --git a/main.c b/main.c @@ -7,6 +7,7 @@ #include "config.h" #include "physics.h" #include "player.h" +#define SQR(x) ((x) * (x)) Triangle *BuildTriangleCache(Model *model, int *outCount) { int total = 0; @@ -45,19 +46,19 @@ Triangle *BuildTriangleCache(Model *model, int *outCount) { return tris; } -void UpdatePlayerCamera(Camera3D *c, Player p, float deltaTime) { +void UpdatePlayerCamera(Camera3D *c, Player *p, float deltaTime) { static float smoothHeight = 0.0f; - smoothHeight += (p.height - smoothHeight) * CROUCH_LERP_SPEED * deltaTime; + smoothHeight += (p->height - smoothHeight) * CROUCH_LERP_SPEED * deltaTime; - Vector3 eyePos = {p.pos.x, p.pos.y + smoothHeight, p.pos.z}; + Vector3 eyePos = {p->pos.x, p->pos.y + smoothHeight, p->pos.z}; c->position = eyePos; - Vector3 forward = {cosf(p.rot.y) * sinf(p.rot.z), sinf(p.rot.y), - cosf(p.rot.y) * cosf(p.rot.z)}; + Vector3 forward = {cosf(p->rot.y) * sinf(p->rot.z), sinf(p->rot.y), + cosf(p->rot.y) * cosf(p->rot.z)}; c->target = Vector3Add(eyePos, forward); - Vector3 targetUp = Vector3RotateByAxisAngle(UP, forward, p.rot.x); + Vector3 targetUp = Vector3RotateByAxisAngle(UP, forward, p->rot.x); float upLerpSpeed = 0.1f; c->up = Vector3Lerp(c->up, targetUp, upLerpSpeed); } @@ -83,9 +84,9 @@ int main(void) { static enum Input input; player.vel = Vector3Zero(); player.pos = Vector3Zero(); - player.mode = MOVE_AIR; player.height = PLAYER_HEIGHT; player.radius = PLAYER_RADIUS; + player.grounded = false; while (!WindowShouldClose()) { float dt = GetFrameTime(); @@ -111,17 +112,16 @@ int main(void) { if (IsKeyDown(KEY_SPACE)) input |= INPUT_JUMP; - if (IsKeyPressed(KEY_R)) { + if (IsKeyPressed(KEY_R)) player.pos = player.vel = Vector3Zero(); - } PlayerUpdate(&player, input, dt); + player.vel.y -= GRAVITY * dt; PlayerApplyFriction(&player, dt); PlayerApplyAcceleration(&player, input, dt); player.pos = Vector3Add(player.pos, Vector3Scale(player.vel, dt)); - PlayerCollide(&player, triangles, triCount, dt); - - UpdatePlayerCamera(&camera, player, dt); + ResolveCollisions(&player, triangles, triCount); + UpdatePlayerCamera(&camera, &player, dt); BeginDrawing(); ClearBackground(RAYWHITE); @@ -139,8 +139,7 @@ int main(void) { player.vel.z), 10, 30, 20, BLACK); - DrawText(TextFormat("Grounded: %b", player.mode == MOVE_GROUND), 10, 50, 20, - BLACK); + DrawText(TextFormat("Grounded: %b", player.grounded), 10, 50, 20, BLACK); EndDrawing(); } diff --git a/physics.c b/physics.c @@ -1,9 +1,9 @@ #include <float.h> #include <raylib.h> -#include <stdio.h> #include "config.h" #include "physics.h" +#include "player.h" #define SQR(x) ((x) * (x)) @@ -173,107 +173,92 @@ bool SphereCast(Vector3 pos, Vector3 dir, float dist, float radius, /* * Resolves capsule overlaps. Pos is the center of the BOTTOM sphere. */ -void ResolveOverlaps(Vector3 *pos, Vector3 *vel, float height, float radius, - Triangle *tris, int triCount, GroundState *state) { - float rSq = SQR(radius); - /* Define capsule axis relative to pos */ - Vector3 capsTopOffset = {0, height, 0}; +void ResolveCollisions(Player *p, Triangle *tris, int count) { + float rSq = SQR(p->radius); + Vector3 capsTopOffset = {0, p->height, 0}; - for (int iter = 0; iter < 2; iter++) { + HitInfo hit; + if (p->mode != MOVE_SLIDE && p->vel.y <= 0 && + SphereCast(p->pos, DOWN, GROUND_SNAP_DIST, p->radius, tris, count, + &hit)) { + p->pos = Vector3Add(p->pos, Vector3Scale(DOWN, hit.distance)); + p->grounded = true; + } + + /* Four iterations should be max: ceil, floor, wall1, wall2 */ + for (int iter = 0; iter < 4; iter++) { bool collisionFound = false; - /* Calculate current capsule world positions */ - Vector3 pBottom = *pos; + Vector3 pBottom = p->pos; Vector3 pTop = Vector3Add(pBottom, capsTopOffset); + Vector3 midPoint = Vector3Scale(Vector3Add(pBottom, pTop), 0.5f); - for (int i = 0; i < triCount; i++) { + for (int i = 0; i < count; i++) { Triangle *t = &tris[i]; - /* - * Broad phase AABB check. If the position isn't even - * within the bounding box of triangle t then skip it - * TODO: replace with O(1) BVH - */ - if (!TriangleLikelyContains(*pos, height, radius, t)) - continue; - - /* - * Find the point on the triangle closest to the - * capsule's central segment Heuristic: Check closest - * point from the capsule's true center - */ - Vector3 midPoint = Vector3Scale(Vector3Add(pBottom, pTop), 0.5f); Vector3 closestOnTri = ClosestPointOnTriangle(midPoint, t->a, t->b, t->c); - - /* Find the point on the capsule segment closest to that - * triangle point */ Vector3 closestOnSegment = ClosestPointOnSegment(closestOnTri, pBottom, pTop); - /* - * Re-refine the point on the triangle based on the - * specific segment point This helps when the capsule is - * tall and the triangle is near the feet or head - */ + // Re-refine closestOnTri = ClosestPointOnTriangle(closestOnSegment, t->a, t->b, t->c); - /* Calculate push */ Vector3 pushVec = Vector3Subtract(closestOnSegment, closestOnTri); float d2 = Vector3LengthSqr(pushVec); - // Added check for very small d2 to prevent division by zero in - // normalization - if (d2 <= EPSILON || d2 >= rSq + EPSILON) + /* Early exit if no collision or invalid math */ + if (d2 <= EPSILON || d2 >= rSq) continue; float d = sqrtf(d2); - // Robust normalization Vector3 n = Vector3Scale(pushVec, 1.0f / d); - float penetration = radius - d + SKIN; - /* - * TODO: ALL slopes should be treated as "steep/slippery" when - * sliding. Gotta figure out where to handle that logic as collision - * doesn't receive player state. - */ - if (n.y > GROUND_NORMAL_Y) { - /* GROUND CASE */ - *state = GROUND_GROUNDED; + /* Add small skin width to prevent floating point sinking */ + float penetration = p->radius - d + SKIN; + /* Check if the surface is "Walkable" (Flat floor or manageable slope) */ + if (n.y > GROUND_NORMAL_Y) { /* - Apply Vertical Depenetration logic. - Slope definition: penetration_vertical = penetration_normal / - cos(theta) n.y is cos(theta). - */ - float verticalPush = penetration / n.y; + * If we are on walkable ground, we want to push the player STRICTLY UP. + * hypotenuse = adjacent / cos(theta) + * vertical_move = penetration / n.y + */ - // Safety: Don't push up if the slope is too crazy (math breakdown) - if (verticalPush < radius * 2.0f) { - pos->y += verticalPush; - } else { - // Fallback to normal push if vertical push is astronomical - *pos = Vector3Add(*pos, Vector3Scale(n, penetration)); - } + p->grounded = true; + float verticalPush = penetration / n.y; + p->pos.y += verticalPush; - /* Only kill velocity if we are falling into the ground */ - /* Allow jumping UP a slope. */ - if (vel->y < 0) - vel->y = 0; } else { - /* WALL/CEILING CASE */ - - /* 1. Frictionless slide (remove velocity component into wall) */ - float vn = Vector3DotProduct(*vel, n); - if (vn < 0.0f) { - *vel = Vector3Subtract(*vel, Vector3Scale(n, vn)); - } - - /* 2. Push out along normal */ - *pos = Vector3Add(*pos, Vector3Scale(n, penetration)); + /* + * If it is a steep slope, wall, or ceiling, push along the normal + * to prevent the player from walking up walls. + */ + p->pos = Vector3Add(p->pos, Vector3Scale(n, penetration)); } - pBottom = *pos; + pBottom = p->pos; pTop = Vector3Add(pBottom, capsTopOffset); + midPoint = Vector3Scale(Vector3Add(pBottom, pTop), 0.5f); + + float vn = Vector3DotProduct(p->vel, n); + + /* Only act if checking velocity moving INTO the wall/floor */ + if (vn < 0.0f) { + + if (n.y > GROUND_NORMAL_Y && p->mode != MOVE_SLIDE) { + p->groundedTimer = COYOTE_TIME; + + /* + * To prevent sliding on slopes, we zero out the Y velocity. + * This stops gravity from accumulating and sliding you down. + */ + p->vel.y = 0.0f; + p->grounded = true; + } else { + /* Remove the velocity moving into the wall (Slide along wall) */ + p->vel = Vector3Subtract(p->vel, Vector3Scale(n, vn)); + } + } collisionFound = true; } @@ -282,20 +267,3 @@ void ResolveOverlaps(Vector3 *pos, Vector3 *vel, float height, float radius, break; } } - -/* - * Snaps the player to the ground, this is useful so we don't get - * micro air time while running down slopes or climbing over the top. - */ -void SnapToGround(Vector3 *pos, float radius, Triangle *tris, int triCount, - GroundState *state) { - HitInfo hit; - if (!SphereCast(*pos, DOWN, GROUND_SNAP_DIST, radius, tris, triCount, &hit)) - return; - - if (hit.normal.y < GROUND_NORMAL_Y) - return; - - *pos = Vector3Add(*pos, Vector3Scale(DOWN, hit.distance)); - *state = GROUND_GROUNDED; -} diff --git a/physics.h b/physics.h @@ -1,5 +1,6 @@ #pragma once +#include "player.h" #include <raylib.h> #include <raymath.h> @@ -22,6 +23,8 @@ typedef struct { Vector3 ClosestPointOnTriangle(Vector3 p, Vector3 a, Vector3 b, Vector3 c); +Vector3 ClosestPointOnSegment(Vector3 p, Vector3 a, Vector3 b); + bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float dist, const Triangle *tri, HitInfo *hit); @@ -31,6 +34,8 @@ bool SphereCast(Vector3 pos, Vector3 dir, float dist, float radius, void ResolveOverlaps(Vector3 *pos, Vector3 *vel, float radius, float height, Triangle *tris, int triCount, GroundState *state); +void ResolveCollisions(Player *p, Triangle *tris, int count); + void SnapToGround(Vector3 *pos, float radius, Triangle *tris, int triCount, GroundState *state); diff --git a/player.c b/player.c @@ -5,7 +5,6 @@ #include <stdio.h> #include "config.h" -#include "physics.h" #include "player.h" static PlayerIntent GetIntent(enum Input i) { @@ -17,8 +16,7 @@ static PlayerIntent GetIntent(enum Input i) { } void PlayerApplyAcceleration(Player *p, enum Input i, float dt) { - Vector3 forward = {cosf(p->rot.y) * sinf(p->rot.z), sinf(p->rot.y), - cosf(p->rot.x) * cosf(p->rot.z)}; + Vector3 forward = {sinf(p->rot.z), 0.0f, cosf(p->rot.z)}; Vector3 right = {sinf(p->rot.z - PI / 2.0f), 0.0f, cosf(p->rot.z - PI / 2.0f)}; @@ -39,7 +37,6 @@ void PlayerApplyAcceleration(Player *p, enum Input i, float dt) { Vector3 wishDir = Vector3Add(Vector3Scale(forward, input.z), Vector3Scale(right, input.x)); - wishDir.y = 0.0f; wishDir = Vector3Normalize(wishDir); float currentSpeed = Vector3DotProduct(p->vel, wishDir); @@ -55,69 +52,57 @@ void PlayerApplyAcceleration(Player *p, enum Input i, float dt) { p->vel = Vector3Add(p->vel, Vector3Scale(wishDir, accelSpeed)); } -void PlayerCollide(Player *p, Triangle *tris, int triCount, float dt) { - GroundState groundState = GROUND_AIRBORNE; - ResolveOverlaps(&p->pos, &p->vel, p->height, p->radius, tris, triCount, - &groundState); - - if (p->mode != MOVE_AIR && p->vel.y < 0) { - SnapToGround(&p->pos, p->radius, tris, triCount, &groundState); - } else { - p->vel.y -= GRAVITY * dt; - } - - if (groundState == GROUND_GROUNDED) - p->groundedTimer = COYOTE_TIME; -} - /* * This function does nothing but set/unset player states */ void PlayerUpdateMode(Player *p, PlayerIntent in) { - bool grounded = p->groundedTimer > 0; - switch (p->mode) { - case MOVE_GROUND: - if (!grounded) { - p->mode = MOVE_AIR; + case MOVE_WALK: + if (in.sprint) { + p->mode = MOVE_SPRINT; + break; + } + + if (in.crouch) { + p->mode = MOVE_CROUCH; + break; + } + + if (in.jump && p->grounded) { + p->vel.y += JUMP_FORCE; break; } + break; - if (in.move && in.sprint && in.crouch && Vector3Length(p->vel) > 4.0f) { + case MOVE_SPRINT: + if (in.move && in.crouch && Vector3Length(p->vel) > 4.0f) { p->mode = MOVE_SLIDE; break; } - if (in.crouch) { - p->mode = MOVE_CROUCH; + if (!in.sprint) { + p->mode = MOVE_WALK; break; } - if (in.jump) { - p->mode = MOVE_AIR; + if (in.jump && p->grounded) { p->vel.y += JUMP_FORCE; break; } + break; case MOVE_CROUCH: if (!in.crouch) { - p->mode = MOVE_GROUND; + p->mode = MOVE_WALK; break; } break; case MOVE_SLIDE: - if (!in.crouch || Vector3Length(p->vel) < 3.0f) { - p->mode = MOVE_GROUND; - break; - } - break; - - case MOVE_AIR: - if (grounded && p->vel.y < 0) { - p->mode = MOVE_GROUND; + if (!in.crouch || Vector3Length(p->vel) < 1.0f) { + p->mode = MOVE_WALK; break; } break; @@ -126,14 +111,20 @@ void PlayerUpdateMode(Player *p, PlayerIntent in) { void PlayerHandleMovement(Player *p, PlayerIntent in) { p->rot.x = 0; + p->height = PLAYER_HEIGHT; switch (p->mode) { - case MOVE_GROUND: - printf("STATE GROUND\n"); - p->maxSpeed = in.sprint ? SPRINT_SPEED : WALK_SPEED; - p->accel = in.sprint ? SPRINT_ACCEL : WALK_ACCEL; - p->height = in.crouch ? PLAYER_HEIGHT / 2 : PLAYER_HEIGHT; + case MOVE_WALK: + printf("STATE WALK\n"); + p->maxSpeed = WALK_SPEED; + p->accel = WALK_ACCEL; + break; + + case MOVE_SPRINT: + printf("STATE SPRINT\n"); + p->maxSpeed = SPRINT_SPEED; + p->accel = SPRINT_ACCEL; break; case MOVE_CROUCH: @@ -148,19 +139,20 @@ void PlayerHandleMovement(Player *p, PlayerIntent in) { p->maxSpeed = SLIDE_SPEED; p->accel = SLIDE_ACCEL; p->height = PLAYER_HEIGHT / 2; - p->rot.x = PI / 12; + p->rot.x = PI / 24; break; - case MOVE_AIR: - printf("STATE AIR\n"); - p->maxSpeed = AIR_SPEED; - p->accel = AIR_ACCEL; - break; + if (!p->grounded) { + p->maxSpeed = AIR_SPEED; + p->accel = AIR_ACCEL; + } } } void PlayerUpdate(Player *p, enum Input i, float dt) { + p->groundedTimer = Clamp(p->groundedTimer - dt, 0, COYOTE_TIME); + p->grounded = (p->groundedTimer > 0); PlayerIntent intent = GetIntent(i); @@ -170,7 +162,7 @@ void PlayerUpdate(Player *p, enum Input i, float dt) { void PlayerApplyFriction(Player *p, float dt) { float friction = AIR_FRICTION; - if (p->mode == MOVE_GROUND || p->mode == MOVE_CROUCH) + if (p->mode == MOVE_WALK || p->mode == MOVE_SPRINT || p->mode == MOVE_CROUCH) friction = GROUND_FRICTION; Vector3 horiz = {p->vel.x, 0, p->vel.z}; diff --git a/player.h b/player.h @@ -1,6 +1,5 @@ #pragma once -#include "physics.h" #include <raylib.h> enum Input { @@ -13,7 +12,7 @@ enum Input { INPUT_CROUCH = 1 << 6, }; -typedef enum { MOVE_GROUND, MOVE_AIR, MOVE_CROUCH, MOVE_SLIDE } MoveMode; +typedef enum { MOVE_WALK, MOVE_SPRINT, MOVE_CROUCH, MOVE_SLIDE } MoveMode; typedef struct { bool move; @@ -40,11 +39,12 @@ typedef struct { Vector3 pos; MoveMode mode; + bool grounded; float groundedTimer; } Player; -void PlayerCollide(Player *p, Triangle *tris, int triCount, float dt); +// void PlayerCollide(Player *p, Triangle *tris, int triCount, float dt); void PlayerUpdateState(Player *p, enum Input i, float dt); diff --git a/types.h b/types.h @@ -0,0 +1,11 @@ +#pragma once +#include <raylib.h> + +typedef struct { + Vector3 a, b, c; + Vector3 normal; + Vector3 min; + Vector3 max; +} Triangle; + +typedef enum { GROUND_AIRBORNE = 0, GROUND_GROUNDED } GroundState;