commit cfcbdd934672703ce6e2106610cb45a22a108496
parent 2a172e5f13f6c512a989033eadf080139e738ab2
Author: amrfti <andrew@kloet.net>
Date: Sun, 28 Dec 2025 16:44:57 -0500
change player controller from sphere to capsule
Diffstat:
5 files changed, 97 insertions(+), 54 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,5 +1,6 @@
CC = cc
CFLAGS = -Wall -Wextra -pedantic -std=c99 -O3
+CFLAGS += -D_POSIX_C_SOURCE=200809L
LDFLAGS = -lraylib -lm
SRC = main.c physics.c
diff --git a/config.h b/config.h
@@ -8,7 +8,7 @@
// physics
#define MAX_SLIDES 4
#define SKIN 0.001f
-#define GRAVITY 45.0f
+#define GRAVITY 40.0f
#define MAX_GROUND_ANGLE_DEG 50.0f
#define GROUND_NORMAL_Y (cosf(MAX_GROUND_ANGLE_DEG * DEG2RAD))
#define DOWN (Vector3){0, -1, 0}
@@ -17,14 +17,12 @@
#define COYOTE_TIME 0.1f
// movement
-#define STEP_HEIGHT 0.4f
-#define STEP_ITERATIONS 2
#define PLAYER_RADIUS 0.3f
+#define PLAYER_HEIGHT 0.8f
#define WALK_SPEED 4.0f
-#define SPRINT_SPEED 7.0f
+#define SPRINT_SPEED 6.0f
#define JUMP_FORCE 10.0f
-#define ACCELERATION 10.0f
-#define FRICTION 10.0f
+#define ACCELERATION 8.0f
#define AIR_ACCELERATION ACCELERATION // could change control in air
// camera
diff --git a/main.c b/main.c
@@ -69,7 +69,6 @@ int main(void) {
Vector3 playerPos = {0, 0, 0};
Vector3 velocity = {0};
- float radius = PLAYER_RADIUS;
bool grounded = false;
while (!WindowShouldClose()) {
@@ -123,10 +122,10 @@ int main(void) {
velocity = (Vector3){0};
}
- Physics_MoveCharacter(&playerPos, &velocity, dt, radius, triangles,
- triCount, &grounded);
+ Physics_MoveCharacter(&playerPos, &velocity, dt, PLAYER_RADIUS,
+ PLAYER_HEIGHT, triangles, triCount, &grounded);
- Vector3 eyePos = Vector3Add(playerPos, (Vector3){0, radius * 2.0f, 0});
+ Vector3 eyePos = Vector3Add(playerPos, (Vector3){0, PLAYER_HEIGHT, 0});
camera.position = eyePos;
camera.target = Vector3Add(eyePos, forward);
diff --git a/physics.c b/physics.c
@@ -6,23 +6,53 @@
#define SQR(x) ((x) * (x))
/*
- * Check for if point p is within the bounds of triangle t with radius r
+ * Find closest point on a line segment [a, b] to point p
*/
-static inline bool TriangleLikelyContains(Vector3 p, float r,
+Vector3 ClosestPointOnSegment(Vector3 p, Vector3 a, Vector3 b) {
+ Vector3 ab = Vector3Subtract(b, a);
+ float t = Vector3DotProduct(Vector3Subtract(p, a), ab);
+ float lenSq = Vector3LengthSqr(ab);
+
+ /* Validate length to avoid division by zero */
+ if (lenSq < EPSILON)
+ return a;
+
+ t = t / lenSq;
+
+ /* Clamp t to [0, 1] */
+ t = fmaxf(0.0f, fminf(1.0f, t));
+
+ return Vector3Add(a, Vector3Scale(ab, t));
+}
+
+/*
+ * Check for if capsule is within bounds of triangle t
+ */
+static inline bool TriangleLikelyContains(Vector3 pos, float height, float r,
const Triangle *t) {
float r2 = r + SKIN;
- if (p.x + r2 < fminf(fminf(t->a.x, t->b.x), t->c.x))
+ /*
+ * AABB check: Triangle must overlap the AABB of the capsule
+ * Capsule Y Range: [pos.y - r, pos.y + height + r]
+ */
+ float minX = fminf(fminf(t->a.x, t->b.x), t->c.x);
+ float maxX = fmaxf(fmaxf(t->a.x, t->b.x), t->c.x);
+ if (pos.x + r2 < minX || pos.x - r2 > maxX)
return false;
- if (p.x - r2 > fmaxf(fmaxf(t->a.x, t->b.x), t->c.x))
- return false;
- if (p.z + r2 < fminf(fminf(t->a.z, t->b.z), t->c.z))
- return false;
- if (p.z - r2 > fmaxf(fmaxf(t->a.z, t->b.z), t->c.z))
+
+ float minZ = fminf(fminf(t->a.z, t->b.z), t->c.z);
+ float maxZ = fmaxf(fmaxf(t->a.z, t->b.z), t->c.z);
+ if (pos.z + r2 < minZ || pos.z - r2 > maxZ)
return false;
- if (p.y + r2 < fminf(fminf(t->a.y, t->b.y), t->c.y))
+
+ float minY = fminf(fminf(t->a.y, t->b.y), t->c.y);
+ float maxY = fmaxf(fmaxf(t->a.y, t->b.y), t->c.y);
+
+ /* Check capsule bottom vs tri top and capsule top vs tri bottom */
+ if ((pos.y + height + r2) < minY)
return false;
- if (p.y - r2 > fmaxf(fmaxf(t->a.y, t->b.y), t->c.y))
+ if ((pos.y - r2) > maxY)
return false;
return true;
@@ -70,12 +100,9 @@ Vector3 ClosestPointOnTriangle(Vector3 p, Vector3 a, Vector3 b, Vector3 c) {
bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float maxDist,
const Triangle *tri, HitInfo *hit) {
Vector3 n = tri->normal;
-
- // intersect with the infinite plane
float denom = Vector3DotProduct(n, dir);
-
if (denom >= -EPSILON)
- return false;
+ return false; /* Backface culling */
float distToPlane = Vector3DotProduct(n, Vector3Subtract(tri->a, pos));
float tPlane = (distToPlane + radius) / denom;
@@ -83,31 +110,24 @@ bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float maxDist,
if (tPlane > maxDist)
return false;
- // determine where the sphere touches the plane
Vector3 planeIntersectionPoint = Vector3Add(pos, Vector3Scale(dir, tPlane));
Vector3 centerOnPlane =
Vector3Subtract(planeIntersectionPoint, Vector3Scale(n, radius));
-
- // find closest point on the actual triangle
Vector3 closestPt =
ClosestPointOnTriangle(centerOnPlane, tri->a, tri->b, tri->c);
-
- // check distance
float distSq = Vector3DistanceSqr(centerOnPlane, closestPt);
- // CASE A: Hit the FACE
+ /* CASE A: Hit the FACE */
if (distSq < EPSILON) {
if (tPlane < 0)
return false;
-
hit->hit = true;
hit->distance = tPlane;
hit->normal = n;
return true;
}
- // CASE B: Sweep against Edge/Vertex
- // Equation: |(pos + dir * t) - closestPt|^2 = radius^2
+ /* CASE B: Hit Edge/Vertex */
Vector3 L = Vector3Subtract(pos, closestPt);
float a = 1.0f;
float b = 2.0f * Vector3DotProduct(L, dir);
@@ -116,19 +136,15 @@ bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float maxDist,
float delta = b * b - 4 * a * c;
if (delta >= 0.0f) {
- float sqrtDelta = sqrtf(delta);
- float tEdge = (-b - sqrtDelta) / 2.0f;
-
+ float tEdge = (-b - sqrtf(delta)) / 2.0f;
if (tEdge >= 0 && tEdge < maxDist) {
hit->hit = true;
hit->distance = tEdge;
-
Vector3 hitPos = Vector3Add(pos, Vector3Scale(dir, tEdge));
hit->normal = Vector3Normalize(Vector3Subtract(hitPos, closestPt));
return true;
}
}
-
return false;
}
@@ -148,18 +164,26 @@ bool SphereCast(Vector3 pos, Vector3 dir, float dist, float radius,
if (!found)
return false;
-
*outHit = best;
return true;
}
-void ResolveOverlaps(Vector3 *pos, float radius, Triangle *tris, int triCount,
- bool *hitFloor) {
+/*
+ * Resolves capsule overlaps. Pos is the center of the BOTTOM sphere.
+ */
+void ResolveOverlaps(Vector3 *pos, float height, float radius, Triangle *tris,
+ int triCount, bool *hitFloor) {
float rSq = SQR(radius);
+ /* Define capsule axis relative to pos */
+ Vector3 capsTopOffset = {0, height, 0};
for (int iter = 0; iter < 2; iter++) {
bool collisionFound = false;
+ /* Calculate current capsule world positions */
+ Vector3 pBottom = *pos;
+ Vector3 pTop = Vector3Add(pBottom, capsTopOffset);
+
for (int i = 0; i < triCount; i++) {
Triangle *t = &tris[i];
@@ -168,11 +192,29 @@ void ResolveOverlaps(Vector3 *pos, float radius, Triangle *tris, int triCount,
* within the bounding box of triangle t then skip it
* TODO: replace with O(1) BVH
*/
- if (!TriangleLikelyContains(*pos, radius, t))
+ if (!TriangleLikelyContains(*pos, height, radius, t))
continue;
- Vector3 closest = ClosestPointOnTriangle(*pos, t->a, t->b, t->c);
- Vector3 pushVec = Vector3Subtract(*pos, closest);
+ /*
+ * 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
+ */
+ closestOnTri = ClosestPointOnTriangle(closestOnSegment, t->a, t->b, t->c);
+
+ /* Calculate push */
+ Vector3 pushVec = Vector3Subtract(closestOnSegment, closestOnTri);
float d2 = Vector3LengthSqr(pushVec);
if (d2 <= 0 || d2 >= rSq + EPSILON)
@@ -192,8 +234,11 @@ void ResolveOverlaps(Vector3 *pos, float radius, Triangle *tris, int triCount,
if (n.y > GROUND_NORMAL_Y) {
pos->y += penetration / n.y;
*hitFloor = true;
- } else
+ } else {
*pos = Vector3Add(*pos, Vector3Scale(n, penetration));
+ pBottom = *pos;
+ pTop = Vector3Add(pBottom, capsTopOffset);
+ }
collisionFound = true;
}
@@ -212,29 +257,29 @@ void SnapToGround(Vector3 *pos, float radius, Triangle *tris, int triCount,
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));
*touchingGround = true;
}
void Physics_MoveCharacter(Vector3 *pos, Vector3 *velocity, float dt,
- float radius, Triangle *tris, int triCount,
- bool *groundedOut) {
+ float radius, float height, Triangle *tris,
+ int triCount, bool *groundedOut) {
static float groundedTimer = 0.0f;
Vector3 moveStep = Vector3Scale(*velocity, dt);
*pos = Vector3Add(*pos, moveStep);
groundedTimer -= dt;
-
if (groundedTimer < 0)
groundedTimer = 0;
-
bool effectivelyGrounded = (groundedTimer > 0);
bool touchingGround = false;
- ResolveOverlaps(pos, radius, tris, triCount, &touchingGround);
+ ResolveOverlaps(pos, height, radius, tris, triCount, &touchingGround);
if (effectivelyGrounded && velocity->y <= 0) {
SnapToGround(pos, radius, tris, triCount, &touchingGround);
diff --git a/physics.h b/physics.h
@@ -25,8 +25,8 @@ bool SphereCastTriangle(Vector3 pos, Vector3 dir, float radius, float dist,
bool SphereCast(Vector3 pos, Vector3 dir, float dist, float radius,
Triangle *tris, int triCount, HitInfo *outHit);
-void ResolveOverlaps(Vector3 *pos, float radius, Triangle *tris, int triCount,
- bool *hitFloor);
+void ResolveOverlaps(Vector3 *pos, float radius, float height, Triangle *tris,
+ int triCount, bool *hitFloor);
bool CheckGrounded(Vector3 pos, float radius, Triangle *tris, int triCount);
@@ -34,5 +34,5 @@ void SnapToGround(Vector3 *pos, float radius, Triangle *tris, int triCount,
bool *hitFloor);
void Physics_MoveCharacter(Vector3 *pos, Vector3 *velocity, float dt,
- float radius, Triangle *tris, int triCount,
- bool *groundedOut);
+ float radius, float height, Triangle *tris,
+ int triCount, bool *groundedOut);