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:
| M | config.h | | | 4 | ++-- |
| M | main.c | | | 27 | +++++++++++++-------------- |
| M | physics.c | | | 148 | +++++++++++++++++++++++++++++++------------------------------------------------ |
| M | physics.h | | | 5 | +++++ |
| M | player.c | | | 94 | ++++++++++++++++++++++++++++++++++++------------------------------------------- |
| M | player.h | | | 6 | +++--- |
| A | types.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;