commit 1ef521e3f19c49ae5b61f95c3c6794565fee909c
parent cfcbdd934672703ce6e2106610cb45a22a108496
Author: amrfti <andrew@kloet.net>
Date: Wed, 31 Dec 2025 16:31:29 -0500
Add more detailed player control. Starting to encapsulate data better.
Added a slide for when the player is running and crouches anddddd
probably a bunch of other stuff I forgot.
Diffstat:
| M | Makefile | | | 2 | +- |
| M | config.h | | | 38 | +++++++++++++++++++++++++++++--------- |
| M | main.c | | | 142 | +++++++++++++++++++++++++++++++------------------------------------------------ |
| M | physics.c | | | 110 | ++++++++++++++++++++++++++++++++++++++++++------------------------------------- |
| M | physics.h | | | 17 | ++++++++--------- |
| A | player.c | | | 195 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | player.h | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
7 files changed, 403 insertions(+), 158 deletions(-)
diff --git a/Makefile b/Makefile
@@ -3,7 +3,7 @@ CFLAGS = -Wall -Wextra -pedantic -std=c99 -O3
CFLAGS += -D_POSIX_C_SOURCE=200809L
LDFLAGS = -lraylib -lm
-SRC = main.c physics.c
+SRC = main.c physics.c player.c
OBJ = $(SRC:.c=.o)
EXE = main
diff --git a/config.h b/config.h
@@ -8,29 +8,49 @@
// physics
#define MAX_SLIDES 4
#define SKIN 0.001f
-#define GRAVITY 40.0f
+#define GRAVITY 50.0f
#define MAX_GROUND_ANGLE_DEG 50.0f
#define GROUND_NORMAL_Y (cosf(MAX_GROUND_ANGLE_DEG * DEG2RAD))
#define DOWN (Vector3){0, -1, 0}
#define UP (Vector3){0, 1, 0}
-#define GROUND_SNAP_DIST 0.5f
+#define GROUND_SNAP_DIST 0.1f
#define COYOTE_TIME 0.1f
// movement
#define PLAYER_RADIUS 0.3f
#define PLAYER_HEIGHT 0.8f
+
+#define DECELERATION 3.0f
+#define STOP_SPEED 0.01f
#define WALK_SPEED 4.0f
-#define SPRINT_SPEED 6.0f
+#define CROUCH_SPEED WALK_SPEED
+#define AIR_SPEED 4.0f
+#define SPRINT_SPEED 1.5f * WALK_SPEED
+#define SLIDE_SPEED SPRINT_SPEED
+
+#define WALK_ACCEL 8.0f
+#define AIR_ACCEL 0.25f * WALK_ACCEL
+#define SPRINT_ACCEL 1.5f * WALK_ACCEL
+#define CROUCH_ACCEL 0.5f * WALK_SPEED
+#define SLIDE_ACCEL 0.1f * WALK_SPEED
+
+#define GROUND_FRICTION 10.0f
+#define AIR_FRICTION 0.5f
#define JUMP_FORCE 10.0f
-#define ACCELERATION 8.0f
-#define AIR_ACCELERATION ACCELERATION // could change control in air
+#define SLIDE_FRICTION 1.5f
+
+#define SLIDE_CONTROL 8.0f
// camera
-#define BASE_FOV 70.0f
-#define SPRINT_FOV 85.0f
+#define BASE_FOV 90.0f
+#define SPRINT_FOV 100.0f
+
#define MAX_PITCH 1.5f
+
#define FOV_LERP_SPEED 30.0f
-#define ROLL_FACTOR 6.0f
#define ROLL_LERP_SPEED 20.0f
-#define MOUSE_SENSITIVITY 0.003f
#define SPRINT_FOV_LERP_SPEED 10.0f
+#define CROUCH_LERP_SPEED 10.0f
+
+#define ROLL_FACTOR 1.0f
+#define MOUSE_SENSITIVITY 0.003f
diff --git a/main.c b/main.c
@@ -6,12 +6,7 @@
#include "config.h"
#include "physics.h"
-
-#define UNGROUND_VELOCITY_EPSILON 0.5f
-
-float camYaw = 0.0f;
-float camPitch = 0.0f;
-float camRoll = 0.0f;
+#include "player.h"
Triangle *BuildTriangleCache(Model *model, int *outCount) {
int total = 0;
@@ -50,6 +45,23 @@ Triangle *BuildTriangleCache(Model *model, int *outCount) {
return tris;
}
+void UpdatePlayerCamera(Camera3D *c, Player p, float deltaTime) {
+ static float smoothHeight = 0.0f;
+ smoothHeight += (p.height - smoothHeight) * CROUCH_LERP_SPEED * deltaTime;
+
+ 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)};
+
+ c->target = Vector3Add(eyePos, forward);
+
+ Vector3 targetUp = Vector3RotateByAxisAngle(UP, forward, p.rot.x);
+ float upLerpSpeed = 0.1f;
+ c->up = Vector3Lerp(c->up, targetUp, upLerpSpeed);
+}
+
int main(void) {
InitWindow(WIDTH, HEIGHT, "Sphere Character Controller");
DisableCursor();
@@ -67,94 +79,49 @@ int main(void) {
int triCount = 0;
Triangle *triangles = BuildTriangleCache(&model, &triCount);
- Vector3 playerPos = {0, 0, 0};
- Vector3 velocity = {0};
- bool grounded = false;
+ static Player player;
+ static enum Input input;
+ player.vel = Vector3Zero();
+ player.pos = Vector3Zero();
+ player.mode = MOVE_AIR;
+ player.height = PLAYER_HEIGHT;
+ player.radius = PLAYER_RADIUS;
while (!WindowShouldClose()) {
float dt = GetFrameTime();
+ Vector2 md = GetMouseDelta();
- Vector2 mouseDelta = GetMouseDelta();
- camYaw -= mouseDelta.x * MOUSE_SENSITIVITY;
- camPitch -= mouseDelta.y * MOUSE_SENSITIVITY;
- camPitch = Clamp(camPitch, -MAX_PITCH, MAX_PITCH);
-
- Vector3 forward = {cosf(camPitch) * sinf(camYaw), sinf(camPitch),
- cosf(camPitch) * cosf(camYaw)};
-
- Vector3 right = {sinf(camYaw - PI / 2.0f), 0.0f, cosf(camYaw - PI / 2.0f)};
+ player.rot.z -= md.x * MOUSE_SENSITIVITY;
+ player.rot.y -= md.y * MOUSE_SENSITIVITY;
+ player.rot.y = Clamp(player.rot.y, -MAX_PITCH, MAX_PITCH);
- Vector3 input = {0};
+ input = 0;
if (IsKeyDown(KEY_W))
- input.z += 1;
+ input |= INPUT_FORWARD;
if (IsKeyDown(KEY_S))
- input.z -= 1;
- if (IsKeyDown(KEY_D))
- input.x += 1;
+ input |= INPUT_BACKWARD;
if (IsKeyDown(KEY_A))
- input.x -= 1;
-
- bool sprinting = IsKeyDown(KEY_LEFT_SHIFT);
- float speed = sprinting ? SPRINT_SPEED : WALK_SPEED;
-
- Vector3 targetVel = {0};
-
- if (Vector3Length(input) > 0) {
- input = Vector3Normalize(input);
- Vector3 wishDir = Vector3Add(Vector3Scale(forward, input.z),
- Vector3Scale(right, input.x));
- wishDir.y = 0;
- wishDir = Vector3Normalize(wishDir);
-
- targetVel.x = wishDir.x * speed;
- targetVel.z = wishDir.z * speed;
- }
-
- float accel = grounded ? ACCELERATION : AIR_ACCELERATION;
- velocity.x = Lerp(velocity.x, targetVel.x, accel * dt);
- velocity.z = Lerp(velocity.z, targetVel.z, accel * dt);
-
- if (grounded && IsKeyPressed(KEY_SPACE))
- velocity.y = JUMP_FORCE;
+ input |= INPUT_LEFT;
+ if (IsKeyDown(KEY_D))
+ input |= INPUT_RIGHT;
+ if (IsKeyDown(KEY_C))
+ input |= INPUT_CROUCH;
+ if (IsKeyDown(KEY_LEFT_SHIFT))
+ input |= INPUT_SPRINT;
+ if (IsKeyDown(KEY_SPACE))
+ input |= INPUT_JUMP;
if (IsKeyPressed(KEY_R)) {
- playerPos = (Vector3){0, 0, 0};
- velocity = (Vector3){0};
+ player.pos = player.vel = Vector3Zero();
}
- Physics_MoveCharacter(&playerPos, &velocity, dt, PLAYER_RADIUS,
- PLAYER_HEIGHT, triangles, triCount, &grounded);
-
- Vector3 eyePos = Vector3Add(playerPos, (Vector3){0, PLAYER_HEIGHT, 0});
-
- camera.position = eyePos;
- camera.target = Vector3Add(eyePos, forward);
-
- Vector3 forwardDir =
- Vector3Normalize(Vector3Subtract(camera.target, camera.position));
- Vector3 rightDir =
- Vector3Normalize(Vector3CrossProduct(forwardDir, (Vector3){0, 1, 0}));
-
- float lateralSpeed = Vector3DotProduct(velocity, rightDir);
- float targetRoll = lateralSpeed * ROLL_FACTOR / SPRINT_SPEED;
- camRoll = Lerp(camRoll, targetRoll, dt * ROLL_LERP_SPEED);
-
- camera.fovy =
- Lerp(camera.fovy,
- sprinting && Vector3Length(input) > 0 ? SPRINT_FOV : BASE_FOV,
- dt * SPRINT_FOV_LERP_SPEED);
-
- float rad = camRoll * DEG2RAD;
-
- Vector3 rotatedUp = {
- UP.x * cosf(rad) +
- (forwardDir.y * UP.z - forwardDir.z * UP.y) * sinf(rad),
- UP.y * cosf(rad) +
- (forwardDir.z * UP.x - forwardDir.x * UP.z) * sinf(rad),
- UP.z * cosf(rad) +
- (forwardDir.x * UP.y - forwardDir.y * UP.x) * sinf(rad)};
+ PlayerUpdate(&player, input, dt);
+ PlayerApplyFriction(&player, dt);
+ PlayerApplyAcceleration(&player, input, dt);
+ player.pos = Vector3Add(player.pos, Vector3Scale(player.vel, dt));
+ PlayerCollide(&player, triangles, triCount, dt);
- camera.up = Vector3Normalize(rotatedUp);
+ UpdatePlayerCamera(&camera, player, dt);
BeginDrawing();
ClearBackground(RAYWHITE);
@@ -164,15 +131,16 @@ int main(void) {
EndMode3D();
- DrawText(TextFormat("Pos: %.2f %.2f %.2f", playerPos.x, playerPos.y,
- playerPos.z),
+ DrawText(TextFormat("Pos: %.2f %.2f %.2f", player.pos.x, player.pos.y,
+ player.pos.z),
10, 10, 20, BLACK);
- DrawText(
- TextFormat("Vel: %.4f %.4f %.4f", velocity.x, velocity.y, velocity.z),
- 10, 30, 20, BLACK);
+ DrawText(TextFormat("Vel: %.4f %.4f %.4f", player.vel.x, player.vel.y,
+ player.vel.z),
+ 10, 30, 20, BLACK);
- DrawText(TextFormat("Grounded: %d", grounded), 10, 50, 20, BLACK);
+ DrawText(TextFormat("Grounded: %b", player.mode == MOVE_GROUND), 10, 50, 20,
+ BLACK);
EndDrawing();
}
diff --git a/physics.c b/physics.c
@@ -1,7 +1,9 @@
-#include "physics.h"
-#include "config.h"
#include <float.h>
-#include <raymath.h>
+#include <raylib.h>
+#include <stdio.h>
+
+#include "config.h"
+#include "physics.h"
#define SQR(x) ((x) * (x))
@@ -171,8 +173,8 @@ 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, float height, float radius, Triangle *tris,
- int triCount, bool *hitFloor) {
+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};
@@ -196,20 +198,22 @@ void ResolveOverlaps(Vector3 *pos, float height, float radius, Triangle *tris,
continue;
/*
- * Find the point on the triangle closest to the capsule's central
- * segment Heuristic: Check closest point from the capsule's true center
+ * 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 */
+ /* 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 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);
@@ -217,29 +221,60 @@ void ResolveOverlaps(Vector3 *pos, float height, float radius, Triangle *tris,
Vector3 pushVec = Vector3Subtract(closestOnSegment, closestOnTri);
float d2 = Vector3LengthSqr(pushVec);
- if (d2 <= 0 || d2 >= rSq + EPSILON)
+ // Added check for very small d2 to prevent division by zero in
+ // normalization
+ if (d2 <= EPSILON || d2 >= rSq + EPSILON)
continue;
float d = sqrtf(d2);
+ // Robust normalization
Vector3 n = Vector3Scale(pushVec, 1.0f / d);
-
float penetration = radius - d + SKIN;
/*
- * If it is ground, only push UP.
- * We convert the normal penetration into vertical penetration:
- * dist_vertical = dist_normal / cos(theta)
- * n.y is actually cos(theta) between Up and Normal.
+ * 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) {
- pos->y += penetration / n.y;
- *hitFloor = true;
+ /* GROUND CASE */
+ *state = GROUND_GROUNDED;
+
+ /*
+ Apply Vertical Depenetration logic.
+ Slope definition: penetration_vertical = penetration_normal /
+ cos(theta) n.y is cos(theta).
+ */
+ float verticalPush = 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));
+ }
+
+ /* 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));
- pBottom = *pos;
- pTop = Vector3Add(pBottom, capsTopOffset);
}
+ pBottom = *pos;
+ pTop = Vector3Add(pBottom, capsTopOffset);
+
collisionFound = true;
}
@@ -253,7 +288,7 @@ void ResolveOverlaps(Vector3 *pos, float height, float radius, Triangle *tris,
* micro air time while running down slopes or climbing over the top.
*/
void SnapToGround(Vector3 *pos, float radius, Triangle *tris, int triCount,
- bool *touchingGround) {
+ GroundState *state) {
HitInfo hit;
if (!SphereCast(*pos, DOWN, GROUND_SNAP_DIST, radius, tris, triCount, &hit))
return;
@@ -262,34 +297,5 @@ void SnapToGround(Vector3 *pos, float radius, Triangle *tris, int triCount,
return;
*pos = Vector3Add(*pos, Vector3Scale(DOWN, hit.distance));
- *touchingGround = true;
-}
-
-void Physics_MoveCharacter(Vector3 *pos, Vector3 *velocity, float dt,
- 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, height, radius, tris, triCount, &touchingGround);
-
- if (effectivelyGrounded && velocity->y <= 0) {
- SnapToGround(pos, radius, tris, triCount, &touchingGround);
- velocity->y = 0;
- } else {
- velocity->y -= GRAVITY * dt;
- }
-
- if (touchingGround)
- groundedTimer = COYOTE_TIME;
-
- *groundedOut = effectivelyGrounded;
+ *state = GROUND_GROUNDED;
}
diff --git a/physics.h b/physics.h
@@ -1,9 +1,12 @@
#pragma once
+#include <raylib.h>
#include <raymath.h>
-#include <stdbool.h>
+
#define MAX_ITERS 3
+typedef enum { GROUND_AIRBORNE = 0, GROUND_GROUNDED } GroundState;
+
typedef struct {
bool hit;
float distance;
@@ -25,14 +28,10 @@ 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, float height, Triangle *tris,
- int triCount, bool *hitFloor);
-
-bool CheckGrounded(Vector3 pos, float radius, Triangle *tris, int triCount);
+void ResolveOverlaps(Vector3 *pos, Vector3 *vel, float radius, float height,
+ Triangle *tris, int triCount, GroundState *state);
void SnapToGround(Vector3 *pos, float radius, Triangle *tris, int triCount,
- bool *hitFloor);
+ GroundState *state);
-void Physics_MoveCharacter(Vector3 *pos, Vector3 *velocity, float dt,
- float radius, float height, Triangle *tris,
- int triCount, bool *groundedOut);
+void ApplyGravity(Vector3 *velocity, float dt);
diff --git a/player.c b/player.c
@@ -0,0 +1,195 @@
+#include <float.h>
+#include <raylib.h>
+#include <raymath.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "config.h"
+#include "physics.h"
+#include "player.h"
+
+static PlayerIntent GetIntent(enum Input i) {
+ return (PlayerIntent){
+ .move = i & (INPUT_FORWARD | INPUT_BACKWARD | INPUT_LEFT | INPUT_RIGHT),
+ .sprint = i & INPUT_SPRINT,
+ .crouch = i & INPUT_CROUCH,
+ .jump = i & INPUT_JUMP};
+}
+
+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 right = {sinf(p->rot.z - PI / 2.0f), 0.0f,
+ cosf(p->rot.z - PI / 2.0f)};
+
+ Vector3 input = Vector3Zero();
+ if (i & INPUT_FORWARD)
+ input.z += 1;
+ if (i & INPUT_BACKWARD)
+ input.z -= 1;
+ if (i & INPUT_LEFT)
+ input.x -= 1;
+ if (i & INPUT_RIGHT)
+ input.x += 1;
+
+ if (Vector3Length(input) == 0.0f)
+ return;
+
+ Vector3 wishDir =
+ Vector3Add(Vector3Scale(forward, input.z), Vector3Scale(right, input.x));
+
+ wishDir.y = 0.0f;
+ wishDir = Vector3Normalize(wishDir);
+
+ float currentSpeed = Vector3DotProduct(p->vel, wishDir);
+
+ float addSpeed = p->maxSpeed - currentSpeed;
+ if (addSpeed <= 0.0f)
+ return;
+
+ float accelSpeed = p->accel * p->maxSpeed * dt;
+ if (accelSpeed > addSpeed)
+ accelSpeed = addSpeed;
+
+ 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;
+ break;
+ }
+
+ if (in.move && in.sprint && in.crouch && Vector3Length(p->vel) > 4.0f) {
+ p->mode = MOVE_SLIDE;
+ break;
+ }
+
+ if (in.crouch) {
+ p->mode = MOVE_CROUCH;
+ break;
+ }
+
+ if (in.jump) {
+ p->mode = MOVE_AIR;
+ p->vel.y += JUMP_FORCE;
+ break;
+ }
+ break;
+
+ case MOVE_CROUCH:
+ if (!in.crouch) {
+ p->mode = MOVE_GROUND;
+ 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;
+ break;
+ }
+ break;
+ }
+}
+
+void PlayerHandleMovement(Player *p, PlayerIntent in) {
+ p->rot.x = 0;
+
+ 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;
+ break;
+
+ case MOVE_CROUCH:
+ printf("STATE CROUCH\n");
+ p->maxSpeed = CROUCH_SPEED;
+ p->accel = CROUCH_ACCEL;
+ p->height = PLAYER_HEIGHT / 2;
+ break;
+
+ case MOVE_SLIDE:
+ printf("STATE SLIDE\n");
+ p->maxSpeed = SLIDE_SPEED;
+ p->accel = SLIDE_ACCEL;
+ p->height = PLAYER_HEIGHT / 2;
+ p->rot.x = PI / 12;
+ break;
+
+ case MOVE_AIR:
+ printf("STATE AIR\n");
+ p->maxSpeed = AIR_SPEED;
+ p->accel = AIR_ACCEL;
+ break;
+ }
+}
+
+void PlayerUpdate(Player *p, enum Input i, float dt) {
+ p->groundedTimer = Clamp(p->groundedTimer - dt, 0, COYOTE_TIME);
+
+ PlayerIntent intent = GetIntent(i);
+
+ PlayerUpdateMode(p, intent);
+ PlayerHandleMovement(p, intent);
+}
+
+void PlayerApplyFriction(Player *p, float dt) {
+ float friction = AIR_FRICTION;
+ if (p->mode == MOVE_GROUND || p->mode == MOVE_CROUCH)
+ friction = GROUND_FRICTION;
+
+ Vector3 horiz = {p->vel.x, 0, p->vel.z};
+ float speed = Vector3Length(horiz);
+
+ if (speed < STOP_SPEED) {
+ p->vel = (Vector3){0, p->vel.y, 0};
+ return;
+ }
+
+ float control = speed < STOP_SPEED ? STOP_SPEED : speed;
+ float drop = control * friction * dt;
+
+ float newSpeed = speed - drop;
+ if (newSpeed < 0)
+ newSpeed = 0;
+
+ horiz = Vector3Scale(horiz, newSpeed / speed);
+
+ p->vel.x = horiz.x;
+ p->vel.z = horiz.z;
+}
diff --git a/player.h b/player.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "physics.h"
+#include <raylib.h>
+
+enum Input {
+ INPUT_FORWARD = 1 << 0,
+ INPUT_BACKWARD = 1 << 1,
+ INPUT_LEFT = 1 << 2,
+ INPUT_RIGHT = 1 << 3,
+ INPUT_JUMP = 1 << 4,
+ INPUT_SPRINT = 1 << 5,
+ INPUT_CROUCH = 1 << 6,
+};
+
+typedef enum { MOVE_GROUND, MOVE_AIR, MOVE_CROUCH, MOVE_SLIDE } MoveMode;
+
+typedef struct {
+ bool move;
+ bool sprint;
+ bool crouch;
+ bool jump;
+} PlayerIntent;
+
+/*
+ * NOTE: Players will eventually be serialized and networked so everything
+ * in here should be strictly relevant for interactivity on other clients.
+ * For example camera data like fov shouldn't be stored here as it's either
+ * irrelevant data on remote clients or it can be derived from other details
+ * like velocity or player state.
+ */
+typedef struct {
+ float height;
+ float radius;
+
+ float maxSpeed;
+ float accel;
+ Vector3 vel;
+ Vector3 rot;
+ Vector3 pos;
+
+ MoveMode mode;
+
+ float groundedTimer;
+} Player;
+
+void PlayerCollide(Player *p, Triangle *tris, int triCount, float dt);
+
+void PlayerUpdateState(Player *p, enum Input i, float dt);
+
+void PlayerApplyAcceleration(Player *p, enum Input i, float dt);
+
+void PlayerUpdate(Player *p, enum Input i, float dt);
+
+void PlayerHandleMovement(Player *p, PlayerIntent in);
+
+void PlayerApplyFriction(Player *p, float dt);