summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile4
-rw-r--r--all.h19
-rw-r--r--config.h18
-rw-r--r--game.c72
-rw-r--r--player.c282
5 files changed, 352 insertions, 43 deletions
diff --git a/Makefile b/Makefile
index c7cd9e3..0eee797 100644
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,7 @@ LDFLAGS := ./vendor/$(RAYLIB_VER)/lib/libraylib.a -lm
GAME := bin/stalag
HEXDUMP := bin/hexdump
PACKER := bin/packer
-SOURCES := main.c map.c game.c
+SOURCES := main.c map.c game.c player.c
ifeq ($(SYSTEM), linux_amd64)
LDFLAGS += -lX11
@@ -47,7 +47,7 @@ $(PACKER): tools/packer.c
$(CC) -std=c99 -o $(PACKER) tools/packer.c
data: $(PACKER)
- $(PACKER) -p data.pak textures maps
+ $(PACKER) -p data.pak textures maps fonts
mkdirs:
mkdir -p bin
diff --git a/all.h b/all.h
index 40a19f1..ac97717 100644
--- a/all.h
+++ b/all.h
@@ -77,12 +77,28 @@ typedef struct {
// --- Game State ---
+typedef enum {
+ MOVE_NORMAL,
+ MOVE_FLY
+} MovementMode;
+
typedef struct {
Camera camera;
Model *world_models;
int world_model_count;
bool cursor_captured;
bool vsync;
+ Font font_ui;
+
+ MovementMode move_mode;
+ Vector3 pos;
+ Vector3 velocity;
+ bool is_grounded;
+ float yaw;
+ float pitch;
+ float lean_amount;
+ float crouch_amount;
+ float horizontal_speed;
} GameState;
extern GameState game;
@@ -114,4 +130,7 @@ void DrawGame(void);
bool LoadMap(const char *filename);
void UnloadMap(void);
+// Player
+void UpdatePlayer(void);
+
#endif
diff --git a/config.h b/config.h
index 54c015c..b92b308 100644
--- a/config.h
+++ b/config.h
@@ -5,8 +5,24 @@
#define WINDOW_HEIGHT 720
#define WINDOW_TITLE "Stalag"
-#define PLAYER_MOVE_SPEED 400.0f
+#define PLAYER_MOVE_SPEED 200.0f
+#define PLAYER_FLY_SPEED 400.0f
#define PLAYER_ROTATION_SPEED 0.05f
#define PLAYER_MOUSE_SENSITIVITY 0.003f
+#define PLAYER_SPRINT_MULTIPLIER 2.0f
+
+#define PLAYER_GRAVITY 1200.0f
+#define PLAYER_JUMP_FORCE 400.0f
+#define PLAYER_HEIGHT 64.0f
+#define PLAYER_RADIUS 16.0f
+#define PLAYER_EYE_HEIGHT 56.0f
+
+#define PLAYER_LEAN_ANGLE 25.0f
+#define PLAYER_LEAN_SPEED 8.0f
+#define PLAYER_LEAN_PIVOT_DISTANCE 30.0f
+
+#define PLAYER_CROUCH_OFFSET 28.0f
+#define PLAYER_CROUCH_SPEED 12.0f
+#define PLAYER_CROUCH_MOVE_MULTIPLIER 0.5f
#endif
diff --git a/game.c b/game.c
index 7166c70..ad932a8 100644
--- a/game.c
+++ b/game.c
@@ -112,9 +112,18 @@ bool LoadMap(const char *filename) {
float x, y, z;
sscanf(e->properties[j].value, "%f %f %f", &x, &y, &z);
if (strcmp(classname, "info_player_start") == 0) {
- game.camera.position = (Vector3){ x, z, -y };
- game.camera.target = Vector3Add(game.camera.position, (Vector3){0, 0, 1});
- TraceLog(LOG_INFO, "Player spawn set to: %f, %f, %f", game.camera.position.x, game.camera.position.y, game.camera.position.z);
+ game.pos = (Vector3){ x, z, -y };
+ game.camera.position = game.pos;
+ float angle = 0;
+ for (int k = 0; k < e->property_count; k++) {
+ if (strcmp(e->properties[k].key, "angle") == 0) {
+ angle = (float)atof(e->properties[k].value);
+ }
+ }
+ game.yaw = (angle + 90.0f) * DEG2RAD;
+ game.pitch = 0;
+ game.camera.target = Vector3Add(game.pos, (Vector3){sinf(game.yaw), 0, cosf(game.yaw)});
+ TraceLog(LOG_INFO, "Player spawn set to: %f, %f, %f (yaw: %f)", game.pos.x, game.pos.y, game.pos.z, game.yaw);
}
}
}
@@ -219,15 +228,27 @@ void InitGame(void) {
game.world_models = NULL;
game.world_model_count = 0;
+ // Load UI Font
+ size_t font_size = 0;
+ void *font_data = vfs_read("fonts/LiberationSans-Bold.ttf", &font_size);
+ if (font_data) {
+ game.font_ui = LoadFontFromMemory(".ttf", font_data, (int)font_size, 20, NULL, 0);
+ vfs_free(font_data);
+ } else {
+ game.font_ui = GetFontDefault();
+ }
+
LoadMap("maps/demo1.map");
game.cursor_captured = false;
EnableCursor();
+
+ game.move_mode = MOVE_NORMAL;
+ game.velocity = (Vector3){ 0, 0, 0 };
+ game.is_grounded = false;
}
void UpdateGame(void) {
- float dt = GetFrameTime();
-
if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) {
game.cursor_captured = !game.cursor_captured;
if (game.cursor_captured) DisableCursor();
@@ -248,39 +269,7 @@ void UpdateGame(void) {
}
}
- // Manual first-person movement
- Vector3 forward = Vector3Normalize(Vector3Subtract(game.camera.target, game.camera.position));
- Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, game.camera.up));
-
- Vector3 move = { 0 };
- if (IsKeyDown(KEY_W)) move = Vector3Add(move, forward);
- if (IsKeyDown(KEY_S)) move = Vector3Subtract(move, forward);
- if (IsKeyDown(KEY_D)) move = Vector3Add(move, right);
- if (IsKeyDown(KEY_A)) move = Vector3Subtract(move, right);
-
- if (Vector3Length(move) > 0) {
- move = Vector3Scale(Vector3Normalize(move), PLAYER_MOVE_SPEED * dt);
- game.camera.position = Vector3Add(game.camera.position, move);
- game.camera.target = Vector3Add(game.camera.target, move);
- }
-
- if (game.cursor_captured) {
- // Manual rotation
- Vector2 mouseDelta = GetMouseDelta();
- float yaw = -mouseDelta.x * PLAYER_MOUSE_SENSITIVITY;
- float pitch = -mouseDelta.y * PLAYER_MOUSE_SENSITIVITY;
-
- Vector3 view = Vector3Subtract(game.camera.target, game.camera.position);
-
- // Yaw
- view = Vector3RotateByAxisAngle(view, game.camera.up, yaw);
-
- // Pitch
- Vector3 axis = Vector3Normalize(Vector3CrossProduct(view, game.camera.up));
- view = Vector3RotateByAxisAngle(view, axis, pitch);
-
- game.camera.target = Vector3Add(game.camera.position, view);
- }
+ UpdatePlayer();
}
void DrawGame(void) {
@@ -300,7 +289,10 @@ void DrawGame(void) {
int screenHeight = GetScreenHeight();
DrawLine(screenWidth / 2 - 10, screenHeight / 2, screenWidth / 2 + 10, screenHeight / 2, GREEN);
DrawLine(screenWidth / 2, screenHeight / 2 - 10, screenWidth / 2, screenHeight / 2 + 10, GREEN);
- DrawFPS(10, 10);
- DrawText(TextFormat("VSync: %s", game.vsync ? "ON" : "OFF"), 10, 30, 20, GREEN);
+
+ DrawTextEx(game.font_ui, TextFormat("%i FPS", GetFPS()), (Vector2){ 10, 10 }, 20, 2, GREEN);
+ DrawTextEx(game.font_ui, TextFormat("VSync: %s", game.vsync ? "ON" : "OFF"), (Vector2){ 10, 35 }, 20, 2, GREEN);
+ DrawTextEx(game.font_ui, TextFormat("Speed: %.0f", game.horizontal_speed), (Vector2){ 10, 60 }, 20, 2, GREEN);
+
EndDrawing();
}
diff --git a/player.c b/player.c
new file mode 100644
index 0000000..a8c039f
--- /dev/null
+++ b/player.c
@@ -0,0 +1,282 @@
+#include "all.h"
+#include "raymath.h"
+#include <float.h>
+
+static void PlayerRotate(void) {
+ if (!game.cursor_captured) return;
+
+ Vector2 mouseDelta = GetMouseDelta();
+ game.yaw += -mouseDelta.x * PLAYER_MOUSE_SENSITIVITY;
+ game.pitch += mouseDelta.y * PLAYER_MOUSE_SENSITIVITY;
+
+ // Clamp pitch to avoid gimbal lock/flipping (approx 89 degrees)
+ if (game.pitch > 89.0f * DEG2RAD) game.pitch = 89.0f * DEG2RAD;
+ if (game.pitch < -89.0f * DEG2RAD) game.pitch = -89.0f * DEG2RAD;
+}
+
+static bool CheckWorldCollision(Vector3 start, Vector3 end, RayCollision *outCollision) {
+ Vector3 diff = Vector3Subtract(end, start);
+ float maxDist = Vector3Length(diff);
+ if (maxDist < 0.001f) return false;
+
+ Ray ray = { 0 };
+ ray.position = start;
+ ray.direction = Vector3Scale(diff, 1.0f / maxDist);
+
+ RayCollision closest = { 0 };
+ closest.distance = FLT_MAX;
+ closest.hit = false;
+
+ for (int i = 0; i < game.world_model_count; i++) {
+ Model model = game.world_models[i];
+ for (int m = 0; m < model.meshCount; m++) {
+ RayCollision col = GetRayCollisionMesh(ray, model.meshes[m], model.transform);
+ if (col.hit && col.distance < closest.distance && col.distance <= maxDist) {
+ closest = col;
+ }
+ }
+ }
+
+ if (closest.hit) {
+ if (outCollision) *outCollision = closest;
+ return true;
+ }
+ return false;
+}
+
+static void MoveNormal(float dt) {
+ float eyeHeight = PLAYER_EYE_HEIGHT - (game.crouch_amount * PLAYER_CROUCH_OFFSET);
+
+ // Movement vectors based on yaw
+ Vector3 forward = { sinf(game.yaw), 0, cosf(game.yaw) };
+ Vector3 right = { sinf(game.yaw - PI/2.0f), 0, cosf(game.yaw - PI/2.0f) };
+
+ Vector3 moveDir = { 0 };
+ if (IsKeyDown(KEY_W)) moveDir = Vector3Add(moveDir, forward);
+ if (IsKeyDown(KEY_S)) moveDir = Vector3Subtract(moveDir, forward);
+ if (IsKeyDown(KEY_D)) moveDir = Vector3Add(moveDir, right);
+ if (IsKeyDown(KEY_A)) moveDir = Vector3Subtract(moveDir, right);
+
+ float speed = PLAYER_MOVE_SPEED;
+ if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) speed *= PLAYER_SPRINT_MULTIPLIER;
+
+ // Apply crouch speed penalty
+ speed *= Lerp(1.0f, PLAYER_CROUCH_MOVE_MULTIPLIER, game.crouch_amount);
+
+ if (Vector3Length(moveDir) > 0) {
+ moveDir = Vector3Scale(Vector3Normalize(moveDir), speed * dt);
+ }
+
+ // Apply gravity
+ game.velocity.y -= PLAYER_GRAVITY * dt;
+
+ // Jump
+ if (game.is_grounded && IsKeyPressed(KEY_SPACE)) {
+ game.velocity.y = PLAYER_JUMP_FORCE;
+ game.is_grounded = false;
+ }
+
+ // Horizontal movement with sliding collision
+ Vector3 remainingMove = moveDir;
+ for (int iter = 0; iter < 4 && Vector3Length(remainingMove) > 0.001f; iter++) {
+ RayCollision closestHit = { 0 };
+ closestHit.distance = FLT_MAX;
+ bool hitFound = false;
+
+ float heights[] = { -eyeHeight + 10.0f, -eyeHeight / 2.0f, 0.0f };
+ Vector3 currentPos = game.pos;
+
+ for (int i = 0; i < 3; i++) {
+ Vector3 start = Vector3Add(currentPos, (Vector3){0, heights[i], 0});
+ Vector3 dir = Vector3Normalize(remainingMove);
+ float dist = Vector3Length(remainingMove) + PLAYER_RADIUS;
+ Vector3 end = Vector3Add(start, Vector3Scale(dir, dist));
+
+ RayCollision col;
+ if (CheckWorldCollision(start, end, &col)) {
+ // Adjust distance to be relative to the player boundary
+ float adjustedDist = col.distance - PLAYER_RADIUS;
+ if (adjustedDist < closestHit.distance) {
+ closestHit = col;
+ closestHit.distance = adjustedDist;
+ hitFound = true;
+ }
+ }
+ }
+
+ if (hitFound) {
+ // Move as far as possible
+ float moveDist = fmaxf(0, closestHit.distance - 0.1f);
+ Vector3 moveStep = Vector3Scale(Vector3Normalize(remainingMove), moveDist);
+ game.pos.x += moveStep.x;
+ game.pos.z += moveStep.z;
+
+ // Project remaining movement onto the plane of the wall
+ Vector3 slideNormal = { closestHit.normal.x, 0, closestHit.normal.z };
+ if (Vector3Length(slideNormal) > 0.001f) {
+ slideNormal = Vector3Normalize(slideNormal);
+ remainingMove = Vector3Subtract(remainingMove, moveStep);
+ float dot = Vector3DotProduct(remainingMove, slideNormal);
+ remainingMove = Vector3Subtract(remainingMove, Vector3Scale(slideNormal, dot));
+ } else {
+ remainingMove = (Vector3){0, 0, 0};
+ }
+ } else {
+ // No collision found, move the rest of the way
+ game.pos.x += remainingMove.x;
+ game.pos.z += remainingMove.z;
+ break;
+ }
+ }
+
+ // Vertical movement with collision
+ float verticalMove = game.velocity.y * dt;
+ Vector3 vStart = game.pos;
+
+ if (verticalMove < 0) { // Falling/Down
+ Vector3 start = vStart;
+ // Check slightly below feet
+ Vector3 end = Vector3Add(vStart, (Vector3){0, verticalMove - eyeHeight, 0});
+ RayCollision vCol;
+ if (CheckWorldCollision(start, end, &vCol) && vCol.normal.y > 0.5f) {
+ game.pos.y = vCol.point.y + eyeHeight;
+ game.velocity.y = 0;
+ game.is_grounded = true;
+ } else {
+ game.pos.y += verticalMove;
+ game.is_grounded = false;
+ }
+ } else if (verticalMove > 0) { // Jumping/Up
+ Vector3 start = vStart;
+ // Check above head
+ float headHeight = PLAYER_HEIGHT - PLAYER_EYE_HEIGHT;
+ Vector3 end = Vector3Add(vStart, (Vector3){0, verticalMove + headHeight, 0});
+ RayCollision vCol;
+ if (CheckWorldCollision(start, end, &vCol)) {
+ game.pos.y = vCol.point.y - headHeight - 1.0f;
+ game.velocity.y = 0;
+ } else {
+ game.pos.y += verticalMove;
+ }
+ game.is_grounded = false;
+ } else {
+ // Not moving vertically, but check if we are still on ground
+ Vector3 start = vStart;
+ Vector3 end = Vector3Add(vStart, (Vector3){0, -eyeHeight - 2.0f, 0});
+ RayCollision vCol;
+ if (CheckWorldCollision(start, end, &vCol) && vCol.normal.y > 0.5f) {
+ game.is_grounded = true;
+ // Snap to floor if very close
+ if (vCol.distance < eyeHeight + 1.0f) {
+ game.pos.y = vCol.point.y + eyeHeight;
+ }
+ } else {
+ game.is_grounded = false;
+ }
+ }
+}
+
+static void MoveFly(float dt) {
+ // Full 3D movement based on yaw/pitch
+ Vector3 forward = {
+ cosf(game.pitch) * sinf(game.yaw),
+ -sinf(game.pitch),
+ cosf(game.pitch) * cosf(game.yaw)
+ };
+ Vector3 right = { sinf(game.yaw - PI/2.0f), 0, cosf(game.yaw - PI/2.0f) };
+
+ Vector3 move = { 0 };
+ if (IsKeyDown(KEY_W)) move = Vector3Add(move, forward);
+ if (IsKeyDown(KEY_S)) move = Vector3Subtract(move, forward);
+ if (IsKeyDown(KEY_D)) move = Vector3Add(move, right);
+ if (IsKeyDown(KEY_A)) move = Vector3Subtract(move, right);
+
+ // Fly up/down
+ if (IsKeyDown(KEY_SPACE)) move.y += 1.0f;
+ if (IsKeyDown(KEY_LEFT_CONTROL)) move.y -= 1.0f;
+
+ float speed = PLAYER_FLY_SPEED;
+ if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) speed *= PLAYER_SPRINT_MULTIPLIER;
+
+ if (Vector3Length(move) > 0) {
+ move = Vector3Scale(Vector3Normalize(move), speed * dt);
+ game.pos = Vector3Add(game.pos, move);
+ }
+
+ game.velocity = (Vector3){0, 0, 0};
+ game.is_grounded = false;
+}
+
+void UpdatePlayer(void) {
+ float dt = GetFrameTime();
+ Vector3 oldPos = game.pos;
+
+ if (IsKeyPressed(KEY_F)) {
+ game.move_mode = (game.move_mode == MOVE_NORMAL) ? MOVE_FLY : MOVE_NORMAL;
+ TraceLog(LOG_INFO, "Movement mode: %s", (game.move_mode == MOVE_NORMAL) ? "NORMAL" : "FLY");
+ }
+
+ PlayerRotate();
+
+ // Crouching logic
+ bool canStand = true;
+ if (game.crouch_amount > 0.1f) {
+ Vector3 headPos = game.pos;
+ Vector3 headEnd = Vector3Add(headPos, (Vector3){0, PLAYER_HEIGHT - PLAYER_EYE_HEIGHT + PLAYER_CROUCH_OFFSET, 0});
+ RayCollision col;
+ if (CheckWorldCollision(headPos, headEnd, &col)) {
+ canStand = false;
+ }
+ }
+
+ float targetCrouch = 0.0f;
+ if (IsKeyDown(KEY_LEFT_CONTROL) || !canStand) targetCrouch = 1.0f;
+ game.crouch_amount = Lerp(game.crouch_amount, targetCrouch, PLAYER_CROUCH_SPEED * dt);
+
+ // Leaning logic
+ float targetLean = 0.0f;
+ if (IsKeyDown(KEY_Q)) targetLean -= 1.0f;
+ if (IsKeyDown(KEY_E)) targetLean += 1.0f;
+
+ game.lean_amount = Lerp(game.lean_amount, targetLean, PLAYER_LEAN_SPEED * dt);
+
+ if (game.move_mode == MOVE_FLY) {
+ MoveFly(dt);
+ } else {
+ MoveNormal(dt);
+ }
+
+ // Apply lean as a pivot from the neck/waist
+ float leanAngle = game.lean_amount * PLAYER_LEAN_ANGLE * DEG2RAD;
+ Vector3 bodyForward = { sinf(game.yaw), 0, cosf(game.yaw) };
+
+ float currentEyeHeight = PLAYER_EYE_HEIGHT - (game.crouch_amount * PLAYER_CROUCH_OFFSET);
+ float pivotDist = fminf(PLAYER_LEAN_PIVOT_DISTANCE, currentEyeHeight * 0.8f);
+
+ Vector3 neckToEye = { 0, pivotDist, 0 };
+ Vector3 rotatedOffset = Vector3RotateByAxisAngle(neckToEye, bodyForward, leanAngle);
+ Vector3 pivot = Vector3Subtract(game.pos, (Vector3){ 0, pivotDist, 0 });
+
+ // Update camera based on physical pos + visual lean pivot
+ game.camera.position = Vector3Add(pivot, rotatedOffset);
+
+ // Apply roll to up vector
+ Vector3 forward = {
+ cosf(game.pitch) * sinf(game.yaw),
+ -sinf(game.pitch),
+ cosf(game.pitch) * cosf(game.yaw)
+ };
+ Vector3 up = { 0, 1, 0 };
+ up = Vector3RotateByAxisAngle(up, forward, leanAngle);
+ game.camera.up = up;
+
+ game.camera.target = Vector3Add(game.camera.position, forward);
+
+ // Calculate horizontal speed for UI
+ if (dt > 0) {
+ Vector2 velH = { game.pos.x - oldPos.x, game.pos.z - oldPos.z };
+ game.horizontal_speed = Vector2Length(velH) / dt;
+ } else {
+ game.horizontal_speed = 0;
+ }
+}