From ce4492d449a18103648e74d34f471ef6a200dd1e Mon Sep 17 00:00:00 2001 From: Tiago Ferreira Date: Wed, 25 Feb 2026 23:22:44 +0000 Subject: [PATCH 1/2] feat: naive player fp physics (AABB based) (#36) --- CMakeLists.txt | 2 + README.md | 7 +- src/main.c | 147 +++++++++++++++++--------------------- src/physics.c | 111 +++++++++++++++++++++++++++++ src/physics.h | 22 ++++++ src/player.c | 190 +++++++++++++++++++++++++++++++++++++++++++++++++ src/player.h | 33 +++++++++ 7 files changed, 427 insertions(+), 85 deletions(-) create mode 100644 src/physics.c create mode 100644 src/physics.h create mode 100644 src/player.c create mode 100644 src/player.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3180dde..6943d9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,8 @@ add_executable(blocks WIN32 src/camera.c src/main.c src/map.c + src/physics.c + src/player.c src/rand.c src/save.c src/shader.c diff --git a/README.md b/README.md index 0bb8746..ae27605 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,17 @@ To build locally, add [SDL_shadercross](https://github.com/libsdl-org/SDL_shader ### Controls -- `WASDEQ` to move +- `WASD` to move +- `Space` to jump +- `F5` to toggle first person/freecam controller - `Escape` to unfocus - `Left Click` to break a block - `Middle Click` to select a block - `Right Click` to place a block - `Scroll` to change blocks - `F11` to toggle fullscreen -- `LControl` to move quickly +- `LControl` to sprint +- `E/Q` to move up/down in freecam ### Passes diff --git a/src/main.c b/src/main.c index b3092f5..8950545 100644 --- a/src/main.c +++ b/src/main.c @@ -4,6 +4,7 @@ #include "camera.h" #include "check.h" +#include "player.h" #include "save.h" #include "shader.h" #include "world.h" @@ -13,7 +14,6 @@ static const int PLAYER_ID = 0; static const float ATLAS_WIDTH = 512.0f; static const int ATLAS_MIP_LEVELS = 4; static const float BLOCK_WIDTH = 16.0f; -static const float PLAYER_SPEED = 0.01f; static const float PLAYER_SENSITIVITY = 0.1f; static const float PLAYER_REACH = 10.0f; static const int SHADOW_RESOLUTION = 4096.0f; @@ -51,40 +51,19 @@ static SDL_GPUTexture* shadow_texture; static SDL_GPUSampler* linear_sampler; static SDL_GPUSampler* nearest_sampler; static camera_t shadow_camera; -static camera_t player_camera; +static player_t player; static world_query_t player_query; -static block_t player_block; static Uint64 ticks1; static Uint64 ticks2; -static void move_player(float dt) +static void update_shadow_camera() { - float speed = PLAYER_SPEED; - float dx = 0.0f; - float dy = 0.0f; - float dz = 0.0f; - const bool* state = SDL_GetKeyboardState(NULL); - dx += state[SDL_SCANCODE_D]; - dx -= state[SDL_SCANCODE_A]; - dy += state[SDL_SCANCODE_E] || state[SDL_SCANCODE_SPACE]; - dy -= state[SDL_SCANCODE_Q] || state[SDL_SCANCODE_LSHIFT]; - dz += state[SDL_SCANCODE_W]; - dz -= state[SDL_SCANCODE_S]; - if (state[SDL_SCANCODE_LCTRL]) - { - speed *= 10.0f; - } - dx *= speed * dt; - dy *= speed * dt; - dz *= speed * dt; - camera_move(&player_camera, dx, dy, dz); - player_query = world_raycast(&player_camera, PLAYER_REACH); camera_init(&shadow_camera, CAMERA_TYPE_ORTHO); shadow_camera.ortho = SHADOW_ORTHO; shadow_camera.far = SHADOW_FAR; - shadow_camera.x = SDL_floor(player_camera.x / CHUNK_WIDTH) * CHUNK_WIDTH; + shadow_camera.x = SDL_floor(player.camera.x / CHUNK_WIDTH) * CHUNK_WIDTH; shadow_camera.y = SHADOW_Y; - shadow_camera.z = SDL_floor(player_camera.z / CHUNK_WIDTH) * CHUNK_WIDTH; + shadow_camera.z = SDL_floor(player.camera.z / CHUNK_WIDTH) * CHUNK_WIDTH; shadow_camera.pitch = SHADOW_PITCH; shadow_camera.yaw = SHADOW_YAW; camera_update(&shadow_camera); @@ -104,31 +83,27 @@ static void save_or_load_player(bool save) data; if (save) { - data.x = player_camera.x; - data.y = player_camera.y; - data.z = player_camera.z; - data.pitch = player_camera.pitch; - data.yaw = player_camera.yaw; - data.block = player_block; + data.x = player.camera.x; + data.y = player.camera.y; + data.z = player.camera.z; + data.pitch = player.camera.pitch; + data.yaw = player.camera.yaw; + data.block = player.block; save_set_player(PLAYER_ID, &data, sizeof(data)); } else { - camera_init(&player_camera, CAMERA_TYPE_PERSPECTIVE); - player_block = BLOCK_YELLOW_TORCH; - player_camera.x = -200.0f; - player_camera.y = 50.0f; - player_camera.z = 0.0f; - if (!save_get_player(PLAYER_ID, &data, sizeof(data))) + player_init(&player); + if (save_get_player(PLAYER_ID, &data, sizeof(data))) { - return; + player.block = data.block; + player.camera.x = data.x; + player.camera.y = data.y; + player.camera.z = data.z; + player.camera.pitch = data.pitch; + player.camera.yaw = data.yaw; } - player_block = data.block; - player_camera.x = data.x; - player_camera.y = data.y; - player_camera.z = data.z; - player_camera.pitch = data.pitch; - player_camera.yaw = data.yaw; + player_update_grounded(&player); } } @@ -609,8 +584,9 @@ SDL_AppResult SDLCALL SDL_AppInit(void** appstate, int argc, char** argv) save_init(SAVE_PATH); world_init(device); save_or_load_player(false); - world_update(&player_camera); - move_player(0.0f); + world_update(&player.camera); + player_query = world_raycast(&player.camera, PLAYER_REACH); + update_shadow_camera(); ticks2 = SDL_GetTicks(); ticks1 = 0; return SDL_APP_CONTINUE; @@ -739,7 +715,7 @@ static bool resize(int width, int height) SDL_Log("Failed to create composite texture: %s", SDL_GetError()); return false; } - camera_resize(&player_camera, width, height); + camera_resize(&player.camera, width, height); return true; } @@ -769,8 +745,8 @@ static void render_sky(SDL_GPUCommandBuffer* cbuf, SDL_GPURenderPass* pass) { SDL_PushGPUDebugGroup(cbuf, "sky"); SDL_BindGPUGraphicsPipeline(pass, sky_pipeline); - SDL_PushGPUVertexUniformData(cbuf, 0, player_camera.proj, 64); - SDL_PushGPUVertexUniformData(cbuf, 1, player_camera.view, 64); + SDL_PushGPUVertexUniformData(cbuf, 0, player.camera.proj, 64); + SDL_PushGPUVertexUniformData(cbuf, 1, player.camera.view, 64); SDL_DrawGPUPrimitives(pass, 36, 1, 0, 0); SDL_PopGPUDebugGroup(cbuf); } @@ -783,7 +759,7 @@ static void render_opaque(SDL_GPUCommandBuffer* cbuf, SDL_GPURenderPass* pass) SDL_PushGPUDebugGroup(cbuf, "opaque"); SDL_BindGPUGraphicsPipeline(pass, opaque_pipeline); SDL_BindGPUFragmentSamplers(pass, 0, &atlas_binding, 1); - world_render(&player_camera, cbuf, pass, WORLD_FLAGS_OPAQUE | WORLD_FLAGS_LIGHT); + world_render(&player.camera, cbuf, pass, WORLD_FLAGS_OPAQUE | WORLD_FLAGS_LIGHT); SDL_PopGPUDebugGroup(cbuf); } @@ -837,8 +813,8 @@ static void render_ssao(SDL_GPUCommandBuffer* cbuf) SDL_GPUTexture* read_textures[2] = {0}; read_textures[0] = voxel_texture; read_textures[1] = position_texture; - int groups_x = (player_camera.width + 8 - 1) / 8; - int groups_y = (player_camera.height + 8 - 1) / 8; + int groups_x = (player.camera.width + 8 - 1) / 8; + int groups_y = (player.camera.height + 8 - 1) / 8; SDL_PushGPUDebugGroup(cbuf, "ssao"); SDL_BindGPUComputePipeline(compute_pass, ssao_pipeline); SDL_BindGPUComputeStorageTextures(compute_pass, 0, read_textures, 2); @@ -859,8 +835,8 @@ static void render_blur(SDL_GPUCommandBuffer* cbuf) } SDL_GPUTexture* read_textures[1]; read_textures[0] = ssao_texture; - int groups_x = (player_camera.width + 8 - 1) / 8; - int groups_y = (player_camera.height + 8 - 1) / 8; + int groups_x = (player.camera.width + 8 - 1) / 8; + int groups_y = (player.camera.height + 8 - 1) / 8; SDL_PushGPUDebugGroup(cbuf, "blur"); SDL_BindGPUComputePipeline(compute_pass, blur_pipeline); SDL_BindGPUComputeStorageTextures(compute_pass, 0, read_textures, 1); @@ -888,14 +864,14 @@ static void render_composite(SDL_GPUCommandBuffer* cbuf) read_textures[4] = position_texture; read_samplers.texture = shadow_texture; read_samplers.sampler = linear_sampler; - int groups_x = (player_camera.width + 8 - 1) / 8; - int groups_y = (player_camera.height + 8 - 1) / 8; + int groups_x = (player.camera.width + 8 - 1) / 8; + int groups_y = (player.camera.height + 8 - 1) / 8; SDL_PushGPUDebugGroup(cbuf, "composite"); SDL_BindGPUComputePipeline(compute_pass, composite_pipeline); SDL_BindGPUComputeStorageTextures(compute_pass, 0, read_textures, 5); SDL_BindGPUComputeSamplers(compute_pass, 0, &read_samplers, 1); SDL_PushGPUComputeUniformData(cbuf, 0, &shadow_camera.matrix, 64); - SDL_PushGPUComputeUniformData(cbuf, 1, player_camera.position, 12); + SDL_PushGPUComputeUniformData(cbuf, 1, player.camera.position, 12); SDL_DispatchGPUCompute(compute_pass, groups_x, groups_y, 1); SDL_EndGPUComputePass(compute_pass); SDL_PopGPUDebugGroup(cbuf); @@ -915,7 +891,7 @@ static void render_depth(SDL_GPUCommandBuffer* cbuf) } SDL_PushGPUDebugGroup(cbuf, "depth"); SDL_BindGPUGraphicsPipeline(pass, depth_pipeline); - world_render(&player_camera, cbuf, pass, WORLD_FLAGS_TRANSPARENT); + world_render(&player.camera, cbuf, pass, WORLD_FLAGS_TRANSPARENT); SDL_PopGPUDebugGroup(cbuf); SDL_EndGPURenderPass(pass); } @@ -932,9 +908,9 @@ static void render_transparent(SDL_GPUCommandBuffer* cbuf, SDL_GPURenderPass* pa SDL_PushGPUDebugGroup(cbuf, "transparent"); SDL_BindGPUGraphicsPipeline(pass, transparent_pipeline); SDL_PushGPUFragmentUniformData(cbuf, 1, &shadow_camera.matrix, 64); - SDL_PushGPUFragmentUniformData(cbuf, 2, player_camera.position, 12); + SDL_PushGPUFragmentUniformData(cbuf, 2, player.camera.position, 12); SDL_BindGPUFragmentSamplers(pass, 0, sampler_bindings, 3); - world_render(&player_camera, cbuf, pass, WORLD_FLAGS_TRANSPARENT | WORLD_FLAGS_LIGHT); + world_render(&player.camera, cbuf, pass, WORLD_FLAGS_TRANSPARENT | WORLD_FLAGS_LIGHT); SDL_PopGPUDebugGroup(cbuf); } @@ -946,7 +922,7 @@ static void render_raycast(SDL_GPUCommandBuffer* cbuf, SDL_GPURenderPass* pass) } SDL_PushGPUDebugGroup(cbuf, "raycast"); SDL_BindGPUGraphicsPipeline(pass, raycast_pipeline); - SDL_PushGPUVertexUniformData(cbuf, 0, player_camera.matrix, 64); + SDL_PushGPUVertexUniformData(cbuf, 0, player.camera.matrix, 64); SDL_PushGPUVertexUniformData(cbuf, 1, player_query.current, 12); SDL_DrawGPUPrimitives(pass, 36, 1, 0, 0); SDL_PopGPUDebugGroup(cbuf); @@ -986,13 +962,13 @@ static void render_ui(SDL_GPUCommandBuffer* cbuf) SDL_GPUTextureSamplerBinding read_textures[1] = {0}; read_textures[0].texture = atlas_texture; read_textures[0].sampler = nearest_sampler; - Sint32 index = block_get_index(player_block, DIRECTION_NORTH); - int groups_x = (player_camera.width + 8 - 1) / 8; - int groups_y = (player_camera.height + 8 - 1) / 8; + Sint32 index = block_get_index(player.block, DIRECTION_NORTH); + int groups_x = (player.camera.width + 8 - 1) / 8; + int groups_y = (player.camera.height + 8 - 1) / 8; SDL_PushGPUDebugGroup(cbuf, "ui"); SDL_BindGPUComputePipeline(compute_pass, ui_pipeline); SDL_BindGPUComputeSamplers(compute_pass, 0, read_textures, 1); - SDL_PushGPUComputeUniformData(cbuf, 0, player_camera.size, 8); + SDL_PushGPUComputeUniformData(cbuf, 0, player.camera.size, 8); SDL_PushGPUComputeUniformData(cbuf, 1, &index, 4); SDL_DispatchGPUCompute(compute_pass, groups_x, groups_y, 1); SDL_EndGPUComputePass(compute_pass); @@ -1003,11 +979,11 @@ static void render_swapchain(SDL_GPUCommandBuffer* cbuf, SDL_GPUTexture* swapcha { SDL_GPUBlitInfo info = {0}; info.source.texture = composite_texture; - info.source.w = player_camera.width; - info.source.h = player_camera.height; + info.source.w = player.camera.width; + info.source.h = player.camera.height; info.destination.texture = swapchain_texture; - info.destination.w = player_camera.width; - info.destination.h = player_camera.height; + info.destination.w = player.camera.width; + info.destination.h = player.camera.height; info.load_op = SDL_GPU_LOADOP_DONT_CARE; info.filter = SDL_GPU_FILTER_NEAREST; SDL_BlitGPUTexture(cbuf, &info); @@ -1035,12 +1011,12 @@ static void render() SDL_SubmitGPUCommandBuffer(cbuf); return; } - if ((width != player_camera.width || height != player_camera.height) && !resize(width, height)) + if ((width != player.camera.width || height != player.camera.height) && !resize(width, height)) { SDL_SubmitGPUCommandBuffer(cbuf); return; } - camera_update(&player_camera); + camera_update(&player.camera); render_shadow(cbuf); render_gbuffer(cbuf); render_ssao(cbuf); @@ -1060,20 +1036,20 @@ SDL_AppResult SDLCALL SDL_AppIterate(void* appstate) ticks1 = ticks2; if (SDL_GetWindowRelativeMouseMode(window)) { - move_player(dt); + player_move(&player, dt, SDL_GetKeyboardState(NULL)); + player_query = world_raycast(&player.camera, PLAYER_REACH); save_or_load_player(true); } - world_update(&player_camera); + update_shadow_camera(); + world_update(&player.camera); render(); return SDL_APP_CONTINUE; } static void rotate_player(float pitch, float yaw) { - pitch *= -PLAYER_SENSITIVITY; - yaw *= PLAYER_SENSITIVITY; - camera_rotate(&player_camera, pitch, yaw); - player_query = world_raycast(&player_camera, PLAYER_REACH); + player_rotate(&player, pitch, yaw, PLAYER_SENSITIVITY); + player_query = world_raycast(&player.camera, PLAYER_REACH); } static void break_block() @@ -1088,24 +1064,24 @@ static void select_block() { if (player_query.block != BLOCK_EMPTY) { - player_block = player_query.block; + player.block = player_query.block; } } static void place_block() { - if (player_query.block != BLOCK_EMPTY) + if (player_query.block != BLOCK_EMPTY && !player_overlaps_block(&player, player_query.previous)) { - world_set_block(player_query.previous, player_block); + world_set_block(player_query.previous, player.block); } } static void change_block(int dy) { static const int COUNT = BLOCK_COUNT - BLOCK_EMPTY - 1; - int block = player_block - (BLOCK_EMPTY + 1) + dy; + int block = player.block - (BLOCK_EMPTY + 1) + dy; block = (block + COUNT) % COUNT; - player_block = block + BLOCK_EMPTY + 1; + player.block = block + BLOCK_EMPTY + 1; } SDL_AppResult SDLCALL SDL_AppEvent(void* appstate, SDL_Event* event) @@ -1126,6 +1102,11 @@ SDL_AppResult SDLCALL SDL_AppEvent(void* appstate, SDL_Event* event) SDL_SetWindowRelativeMouseMode(window, false); SDL_SetWindowFullscreen(window, false); } + else if (event->key.scancode == SDL_SCANCODE_F5) + { + player_toggle_controller(&player); + SDL_Log("Controller mode: %s", player_controller_name(player.controller)); + } else if (event->key.scancode == SDL_SCANCODE_F11) { if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) diff --git a/src/physics.c b/src/physics.c new file mode 100644 index 0000000..49076e7 --- /dev/null +++ b/src/physics.c @@ -0,0 +1,111 @@ +#include + +#include "physics.h" + +static const float PHYSICS_EPSILON = 0.001f; + + +static void resolve_step(const physics_aabb_t* aabb, float* x, float* y, float* z, int axis, float step, physics_is_solid_fn is_solid) +{ + // Step-by-step AABB provides a bit more accuracy + float start[3] = {*x, *y, *z}; + float low = 0.0f; + float high = 1.0f; + for (int i = 0; i < 8; i++) + { + float t = (low + high) * 0.5f; + float next[3] = {start[0], start[1], start[2]}; + next[axis] += step * t; + if (physics_is_colliding(aabb, next[0], next[1], next[2], is_solid)) + { + high = t; + } + else + { + low = t; + } + } + if (axis == 0) + { + *x = start[0] + step * low; + } + else if (axis == 1) + { + *y = start[1] + step * low; + } + else + { + *z = start[2] + step * low; + } +} + + +bool physics_is_colliding(const physics_aabb_t* aabb, float x, float y, float z, physics_is_solid_fn is_solid) +{ + int min_x = SDL_floorf(x + aabb->min_x + PHYSICS_EPSILON); + int max_x = SDL_floorf(x + aabb->max_x - PHYSICS_EPSILON); + int min_y = SDL_floorf(y + aabb->min_y + PHYSICS_EPSILON); + int max_y = SDL_floorf(y + aabb->max_y - PHYSICS_EPSILON); + int min_z = SDL_floorf(z + aabb->min_z + PHYSICS_EPSILON); + int max_z = SDL_floorf(z + aabb->max_z - PHYSICS_EPSILON); + for (int bx = min_x; bx <= max_x; bx++) + { + for (int by = min_y; by <= max_y; by++) + { + for (int bz = min_z; bz <= max_z; bz++) + { + if (is_solid(bx, by, bz)) return true; + } + } + } + return false; +} + +bool physics_move_axis(const physics_aabb_t* aabb, float* x, float* y, float* z, int axis, float delta, float step_size, physics_is_solid_fn is_solid) +{ + // Tiny jitter check prevent small float issues and additional unnecessary collision work. + if (SDL_fabsf(delta) <= SDL_FLT_EPSILON) + { + return false; + } + int steps = (int) SDL_ceilf(SDL_fabsf(delta) / step_size); + steps = SDL_max(steps, 1); + float step = delta / steps; + for (int i = 0; i < steps; i++) + { + float next_x = (*x) + ((axis == 0)? step : 0); + float next_y = (*y) + ((axis == 1)? step : 0); + float next_z = (*z) + ((axis == 2)? step : 0); + + if (physics_is_colliding(aabb, next_x, next_y, next_z, is_solid)) + { + resolve_step(aabb, x, y, z, axis, step, is_solid); + return true; + } + + *x = next_x; + *y = next_y; + *z = next_z; + } + return false; +} + +bool physics_overlaps_block(const physics_aabb_t* aabb, float x, float y, float z, const int block_position[3]) +{ + float aabb_min_x = x + aabb->min_x + PHYSICS_EPSILON; + float aabb_max_x = x + aabb->max_x - PHYSICS_EPSILON; + float aabb_min_y = y + aabb->min_y + PHYSICS_EPSILON; + float aabb_max_y = y + aabb->max_y - PHYSICS_EPSILON; + float aabb_min_z = z + aabb->min_z + PHYSICS_EPSILON; + float aabb_max_z = z + aabb->max_z - PHYSICS_EPSILON; + float block_min_x = block_position[0]; + float block_max_x = block_position[0] + 1.0f; + float block_min_y = block_position[1]; + float block_max_y = block_position[1] + 1.0f; + float block_min_z = block_position[2]; + float block_max_z = block_position[2] + 1.0f; + return + (aabb_max_x > block_min_x && aabb_min_x < block_max_x) && + (aabb_max_y > block_min_y && aabb_min_y < block_max_y) && + (aabb_max_z > block_min_z && aabb_min_z < block_max_z); +} diff --git a/src/physics.h b/src/physics.h new file mode 100644 index 0000000..5e48937 --- /dev/null +++ b/src/physics.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +typedef bool (*physics_is_solid_fn)(int x, int y, int z); + +typedef struct physics_aabb +{ + float min_x; + float max_x; + float min_y; + float max_y; + float min_z; + float max_z; +} +physics_aabb_t; + + + +bool physics_is_colliding(const physics_aabb_t* aabb, float x, float y, float z, physics_is_solid_fn is_solid); +bool physics_move_axis(const physics_aabb_t* aabb, float* x, float* y, float* z, int axis, float delta, float step_size, physics_is_solid_fn is_solid); +bool physics_overlaps_block(const physics_aabb_t* aabb, float x, float y, float z, const int block_position[3]); diff --git a/src/player.c b/src/player.c new file mode 100644 index 0000000..e670202 --- /dev/null +++ b/src/player.c @@ -0,0 +1,190 @@ +#include + +#include "block.h" +#include "camera.h" +#include "physics.h" +#include "player.h" +#include "world.h" + +static const float MOVE_SPEED = 5.0f; +static const float SPRINT_MULTIPLIER = 1.8f; +static const float AIR_ACCEL = 6.0f; +static const float GRAVITY = 24.0f; +static const float JUMP_SPEED = 8.5f; +static const float FREECAM_SPEED = 0.01f; +static const float FREECAM_FAST_MULTIPLIER = 10.0f; +static const float COLLISION_STEP = 0.1f; +static const float GROUND_CHECK_OFFSET = 0.002f; +static const float PLAYER_COLLISION_RADIUS = 0.3f; +static const float PLAYER_COLLISION_HEIGHT = 1.8f; +static const float PLAYER_EYE_OFFSET = 1.62f; + +static physics_aabb_t player_aabb(void) { + return (physics_aabb_t){ + .min_x = -PLAYER_COLLISION_RADIUS, + .max_x = PLAYER_COLLISION_RADIUS, + .min_y = -PLAYER_EYE_OFFSET, + .max_y = PLAYER_COLLISION_HEIGHT - PLAYER_EYE_OFFSET, + .min_z = -PLAYER_COLLISION_RADIUS, + .max_z = PLAYER_COLLISION_RADIUS, + }; +} + +static bool is_block_solid(int x, int y, int z) { + int position[3] = {x, y, z}; + return block_is_solid(world_get_block(position)); +} + +static void move_physics(player_t *player, float dt_ms, const bool *state) { + const physics_aabb_t aabb = player_aabb(); + float dt = SDL_min(dt_ms * 0.001f, 0.05f); + float input_x = state[SDL_SCANCODE_D] - state[SDL_SCANCODE_A]; + float input_z = state[SDL_SCANCODE_W] - state[SDL_SCANCODE_S]; + float length = SDL_sqrtf(input_x * input_x + input_z * input_z); + if (length > SDL_FLT_EPSILON) { + input_x /= length; + input_z /= length; + } + + float speed = MOVE_SPEED; + if (state[SDL_SCANCODE_LCTRL]) { + speed *= SPRINT_MULTIPLIER; + } + + float sy = SDL_sinf(player->camera.yaw); + float cy = SDL_cosf(player->camera.yaw); + float target_x = (cy * input_x + sy * input_z) * speed; + float target_z = (sy * input_x - cy * input_z) * speed; + + if (player->on_ground) { + player->velocity[0] = target_x; + player->velocity[2] = target_z; + } else { + float blend = SDL_min(1.0f, AIR_ACCEL * dt); + player->velocity[0] += (target_x - player->velocity[0]) * blend; + player->velocity[2] += (target_z - player->velocity[2]) * blend; + } + + bool jump_down = state[SDL_SCANCODE_SPACE]; + if (jump_down && !player->jump_was_down && player->on_ground) { + player->velocity[1] = JUMP_SPEED; + player->on_ground = false; + } + player->jump_was_down = jump_down; + + // Before anything "time" check delta for early return (another guard against precision issues/jitter) + // we still need to process input up to this point tho. + if (dt <= SDL_FLT_EPSILON) { + return; + } + + player->velocity[1] -= GRAVITY * dt; + float x = player->camera.x; + float y = player->camera.y; + float z = player->camera.z; + + bool hit_x = physics_move_axis(&aabb, &x, &y, &z, 0, player->velocity[0] * dt, COLLISION_STEP, is_block_solid); + bool hit_y = physics_move_axis(&aabb, &x, &y, &z, 1, player->velocity[1] * dt, COLLISION_STEP, is_block_solid); + bool hit_z = physics_move_axis(&aabb, &x, &y, &z, 2, player->velocity[2] * dt, COLLISION_STEP, is_block_solid); + + player->camera.x = x; + player->camera.y = y; + player->camera.z = z; + + if (hit_x) { + player->velocity[0] = 0.0f; + } + if (hit_z) { + player->velocity[2] = 0.0f; + } + if (hit_y) { + if (player->velocity[1] < 0.0f) { + player->on_ground = true; + } + player->velocity[1] = 0.0f; + } else { + player->on_ground = false; + } +} + +static void move_freecam(player_t *player, float dt_ms, const bool *state) { + float speed = FREECAM_SPEED; + float dx = state[SDL_SCANCODE_D] - state[SDL_SCANCODE_A]; + float dy = (state[SDL_SCANCODE_E] || state[SDL_SCANCODE_SPACE]) - + (state[SDL_SCANCODE_Q] || state[SDL_SCANCODE_LSHIFT]); + float dz = state[SDL_SCANCODE_W] - state[SDL_SCANCODE_S]; + if (state[SDL_SCANCODE_LCTRL]) { + speed *= FREECAM_FAST_MULTIPLIER; + } + camera_move(&player->camera, dx * speed * dt_ms, dy * speed * dt_ms, + dz * speed * dt_ms); + SDL_zerop(player->velocity); + player->jump_was_down = state[SDL_SCANCODE_SPACE]; + player->on_ground = false; +} + +void player_init(player_t *player) { + SDL_zerop(player); + camera_init(&player->camera, CAMERA_TYPE_PERSPECTIVE); + player->camera.x = -200.0f; + player->camera.y = 50.0f; + player->camera.z = 0.0f; + player->block = BLOCK_YELLOW_TORCH; + player->controller = PLAYER_CONTROLLER_FP; +} + +void player_set_controller(player_t *player, player_controller_t controller) { + if (player->controller == controller) { + return; + } + player->controller = controller; + SDL_zerop(player->velocity); + player->jump_was_down = false; + if (controller == PLAYER_CONTROLLER_FP) { + player_update_grounded(player); + } else { + player->on_ground = false; + } +} + +void player_toggle_controller(player_t *player) { + if (player->controller == PLAYER_CONTROLLER_FP) { + player_set_controller(player, PLAYER_CONTROLLER_FREECAM); + } else { + player_set_controller(player, PLAYER_CONTROLLER_FP); + } +} + +const char *player_controller_name(player_controller_t controller) { + if (controller == PLAYER_CONTROLLER_FREECAM) { + return "freecam"; + } else { + return "first_person"; + } +} + +void player_rotate(player_t *player, float pitch, float yaw, + float sensitivity) { + camera_rotate(&player->camera, pitch * -sensitivity, yaw * sensitivity); +} + +void player_move(player_t *player, float dt_ms, const bool *keyboard_state) { + if (player->controller == PLAYER_CONTROLLER_FREECAM) { + move_freecam(player, dt_ms, keyboard_state); + } else { + move_physics(player, dt_ms, keyboard_state); + } +} + +bool player_overlaps_block(const player_t *player, const int position[3]) { + const physics_aabb_t aabb = player_aabb(); + return physics_overlaps_block(&aabb, player->camera.x, + player->camera.y, player->camera.z, position); +} + +void player_update_grounded(player_t *player) { + const physics_aabb_t aabb = player_aabb(); + player->on_ground = physics_is_colliding( + &aabb, player->camera.x, player->camera.y - GROUND_CHECK_OFFSET, + player->camera.z, is_block_solid); +} diff --git a/src/player.h b/src/player.h new file mode 100644 index 0000000..ec11552 --- /dev/null +++ b/src/player.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "block.h" +#include "camera.h" + +typedef enum player_controller +{ + PLAYER_CONTROLLER_FP, + PLAYER_CONTROLLER_FREECAM, +} +player_controller_t; + +typedef struct player +{ + camera_t camera; + float velocity[3]; + bool on_ground; + bool jump_was_down; + block_t block; + player_controller_t controller; +} +player_t; + +void player_init(player_t* player); +void player_set_controller(player_t* player, player_controller_t controller); +void player_toggle_controller(player_t* player); +const char* player_controller_name(player_controller_t controller); +void player_rotate(player_t* player, float pitch, float yaw, float sensitivity); +void player_move(player_t* player, float dt_ms, const bool* keyboard_state); +bool player_overlaps_block(const player_t* player, const int position[3]); +void player_update_grounded(player_t* player); From 08f86ade1ec68534b43e1ac2ff10fc6d542a903b Mon Sep 17 00:00:00 2001 From: Jaan Soulier Date: Tue, 3 Mar 2026 22:46:32 -0500 Subject: [PATCH 2/2] refactor physics --- CMakeLists.txt | 1 - README.md | 8 +- src/main.c | 131 +++------------- src/physics.c | 111 -------------- src/physics.h | 22 --- src/player.c | 405 ++++++++++++++++++++++++++++++------------------- src/player.h | 26 ++-- 7 files changed, 291 insertions(+), 413 deletions(-) delete mode 100644 src/physics.c delete mode 100644 src/physics.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6943d9d..8feae46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,6 @@ add_executable(blocks WIN32 src/camera.c src/main.c src/map.c - src/physics.c src/player.c src/rand.c src/save.c diff --git a/README.md b/README.md index ae27605..385694e 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ Tiny Minecraft clone in C and HLSL using the new SDL3 GPU API - Procedural world generation - Asynchronous chunk loading -- Blocks and sprites - Persistent worlds +- Physics - Directional shadows - Clustered dynamic lighting -- Basic transparency +- Blocks and sprites ### Building @@ -51,7 +51,7 @@ To build locally, add [SDL_shadercross](https://github.com/libsdl-org/SDL_shader - `WASD` to move - `Space` to jump -- `F5` to toggle first person/freecam controller +- `F5` to toggle fly - `Escape` to unfocus - `Left Click` to break a block - `Middle Click` to select a block @@ -59,7 +59,7 @@ To build locally, add [SDL_shadercross](https://github.com/libsdl-org/SDL_shader - `Scroll` to change blocks - `F11` to toggle fullscreen - `LControl` to sprint -- `E/Q` to move up/down in freecam +- `EQ` to move up and down (fly only) ### Passes diff --git a/src/main.c b/src/main.c index 8950545..784ebb8 100644 --- a/src/main.c +++ b/src/main.c @@ -14,8 +14,6 @@ static const int PLAYER_ID = 0; static const float ATLAS_WIDTH = 512.0f; static const int ATLAS_MIP_LEVELS = 4; static const float BLOCK_WIDTH = 16.0f; -static const float PLAYER_SENSITIVITY = 0.1f; -static const float PLAYER_REACH = 10.0f; static const int SHADOW_RESOLUTION = 4096.0f; static const float SHADOW_Y = 30.0f; static const float SHADOW_ORTHO = 300.0f; @@ -52,61 +50,9 @@ static SDL_GPUSampler* linear_sampler; static SDL_GPUSampler* nearest_sampler; static camera_t shadow_camera; static player_t player; -static world_query_t player_query; static Uint64 ticks1; static Uint64 ticks2; -static void update_shadow_camera() -{ - camera_init(&shadow_camera, CAMERA_TYPE_ORTHO); - shadow_camera.ortho = SHADOW_ORTHO; - shadow_camera.far = SHADOW_FAR; - shadow_camera.x = SDL_floor(player.camera.x / CHUNK_WIDTH) * CHUNK_WIDTH; - shadow_camera.y = SHADOW_Y; - shadow_camera.z = SDL_floor(player.camera.z / CHUNK_WIDTH) * CHUNK_WIDTH; - shadow_camera.pitch = SHADOW_PITCH; - shadow_camera.yaw = SHADOW_YAW; - camera_update(&shadow_camera); -} - -static void save_or_load_player(bool save) -{ - struct - { - float x; - float y; - float z; - float pitch; - float yaw; - block_t block; - } - data; - if (save) - { - data.x = player.camera.x; - data.y = player.camera.y; - data.z = player.camera.z; - data.pitch = player.camera.pitch; - data.yaw = player.camera.yaw; - data.block = player.block; - save_set_player(PLAYER_ID, &data, sizeof(data)); - } - else - { - player_init(&player); - if (save_get_player(PLAYER_ID, &data, sizeof(data))) - { - player.block = data.block; - player.camera.x = data.x; - player.camera.y = data.y; - player.camera.z = data.z; - player.camera.pitch = data.pitch; - player.camera.yaw = data.yaw; - } - player_update_grounded(&player); - } -} - static bool create_atlas() { char path[512] = {0}; @@ -583,10 +529,8 @@ SDL_AppResult SDLCALL SDL_AppInit(void** appstate, int argc, char** argv) set_window_icon(BLOCK_GRASS); save_init(SAVE_PATH); world_init(device); - save_or_load_player(false); + player_save_or_load(&player, PLAYER_ID, false); world_update(&player.camera); - player_query = world_raycast(&player.camera, PLAYER_REACH); - update_shadow_camera(); ticks2 = SDL_GetTicks(); ticks1 = 0; return SDL_APP_CONTINUE; @@ -596,7 +540,7 @@ void SDLCALL SDL_AppQuit(void* appstate, SDL_AppResult result) { SDL_HideWindow(window); world_free(); - save_or_load_player(true); + player_save_or_load(&player, PLAYER_ID, true); save_free(); SDL_ReleaseGPUSampler(device, linear_sampler); SDL_ReleaseGPUSampler(device, nearest_sampler); @@ -719,6 +663,19 @@ static bool resize(int width, int height) return true; } +static void update_shadow_camera() +{ + camera_init(&shadow_camera, CAMERA_TYPE_ORTHO); + shadow_camera.ortho = SHADOW_ORTHO; + shadow_camera.far = SHADOW_FAR; + shadow_camera.x = SDL_floor(player.camera.x / CHUNK_WIDTH) * CHUNK_WIDTH; + shadow_camera.y = SHADOW_Y; + shadow_camera.z = SDL_floor(player.camera.z / CHUNK_WIDTH) * CHUNK_WIDTH; + shadow_camera.pitch = SHADOW_PITCH; + shadow_camera.yaw = SHADOW_YAW; + camera_update(&shadow_camera); +} + static void render_shadow(SDL_GPUCommandBuffer* cbuf) { SDL_GPUDepthStencilTargetInfo depth_info = {0}; @@ -916,14 +873,14 @@ static void render_transparent(SDL_GPUCommandBuffer* cbuf, SDL_GPURenderPass* pa static void render_raycast(SDL_GPUCommandBuffer* cbuf, SDL_GPURenderPass* pass) { - if (player_query.block == BLOCK_EMPTY) + if (player.query.block == BLOCK_EMPTY) { return; } SDL_PushGPUDebugGroup(cbuf, "raycast"); SDL_BindGPUGraphicsPipeline(pass, raycast_pipeline); SDL_PushGPUVertexUniformData(cbuf, 0, player.camera.matrix, 64); - SDL_PushGPUVertexUniformData(cbuf, 1, player_query.current, 12); + SDL_PushGPUVertexUniformData(cbuf, 1, player.query.current, 12); SDL_DrawGPUPrimitives(pass, 36, 1, 0, 0); SDL_PopGPUDebugGroup(cbuf); } @@ -1036,9 +993,8 @@ SDL_AppResult SDLCALL SDL_AppIterate(void* appstate) ticks1 = ticks2; if (SDL_GetWindowRelativeMouseMode(window)) { - player_move(&player, dt, SDL_GetKeyboardState(NULL)); - player_query = world_raycast(&player.camera, PLAYER_REACH); - save_or_load_player(true); + player_move(&player, dt); + player_save_or_load(&player, PLAYER_ID, true); } update_shadow_camera(); world_update(&player.camera); @@ -1046,44 +1002,6 @@ SDL_AppResult SDLCALL SDL_AppIterate(void* appstate) return SDL_APP_CONTINUE; } -static void rotate_player(float pitch, float yaw) -{ - player_rotate(&player, pitch, yaw, PLAYER_SENSITIVITY); - player_query = world_raycast(&player.camera, PLAYER_REACH); -} - -static void break_block() -{ - if (player_query.block != BLOCK_EMPTY) - { - world_set_block(player_query.current, BLOCK_EMPTY); - } -} - -static void select_block() -{ - if (player_query.block != BLOCK_EMPTY) - { - player.block = player_query.block; - } -} - -static void place_block() -{ - if (player_query.block != BLOCK_EMPTY && !player_overlaps_block(&player, player_query.previous)) - { - world_set_block(player_query.previous, player.block); - } -} - -static void change_block(int dy) -{ - static const int COUNT = BLOCK_COUNT - BLOCK_EMPTY - 1; - int block = player.block - (BLOCK_EMPTY + 1) + dy; - block = (block + COUNT) % COUNT; - player.block = block + BLOCK_EMPTY + 1; -} - SDL_AppResult SDLCALL SDL_AppEvent(void* appstate, SDL_Event* event) { switch (event->type) @@ -1093,7 +1011,7 @@ SDL_AppResult SDLCALL SDL_AppEvent(void* appstate, SDL_Event* event) case SDL_EVENT_MOUSE_MOTION: if (SDL_GetWindowRelativeMouseMode(window)) { - rotate_player(event->motion.yrel, event->motion.xrel); + player_rotate(&player, event->motion.yrel, event->motion.xrel); } break; case SDL_EVENT_KEY_DOWN: @@ -1105,7 +1023,6 @@ SDL_AppResult SDLCALL SDL_AppEvent(void* appstate, SDL_Event* event) else if (event->key.scancode == SDL_SCANCODE_F5) { player_toggle_controller(&player); - SDL_Log("Controller mode: %s", player_controller_name(player.controller)); } else if (event->key.scancode == SDL_SCANCODE_F11) { @@ -1130,20 +1047,20 @@ SDL_AppResult SDLCALL SDL_AppEvent(void* appstate, SDL_Event* event) { if (event->button.button == SDL_BUTTON_LEFT) { - break_block(); + player_break_block(&player); } else if (event->button.button == SDL_BUTTON_MIDDLE) { - select_block(); + player_select_block(&player); } else if (event->button.button == SDL_BUTTON_RIGHT) { - place_block(); + player_place_block(&player); } } break; case SDL_EVENT_MOUSE_WHEEL: - change_block(event->wheel.y); + player_change_block(&player, event->wheel.y); break; } return SDL_APP_CONTINUE; diff --git a/src/physics.c b/src/physics.c deleted file mode 100644 index 49076e7..0000000 --- a/src/physics.c +++ /dev/null @@ -1,111 +0,0 @@ -#include - -#include "physics.h" - -static const float PHYSICS_EPSILON = 0.001f; - - -static void resolve_step(const physics_aabb_t* aabb, float* x, float* y, float* z, int axis, float step, physics_is_solid_fn is_solid) -{ - // Step-by-step AABB provides a bit more accuracy - float start[3] = {*x, *y, *z}; - float low = 0.0f; - float high = 1.0f; - for (int i = 0; i < 8; i++) - { - float t = (low + high) * 0.5f; - float next[3] = {start[0], start[1], start[2]}; - next[axis] += step * t; - if (physics_is_colliding(aabb, next[0], next[1], next[2], is_solid)) - { - high = t; - } - else - { - low = t; - } - } - if (axis == 0) - { - *x = start[0] + step * low; - } - else if (axis == 1) - { - *y = start[1] + step * low; - } - else - { - *z = start[2] + step * low; - } -} - - -bool physics_is_colliding(const physics_aabb_t* aabb, float x, float y, float z, physics_is_solid_fn is_solid) -{ - int min_x = SDL_floorf(x + aabb->min_x + PHYSICS_EPSILON); - int max_x = SDL_floorf(x + aabb->max_x - PHYSICS_EPSILON); - int min_y = SDL_floorf(y + aabb->min_y + PHYSICS_EPSILON); - int max_y = SDL_floorf(y + aabb->max_y - PHYSICS_EPSILON); - int min_z = SDL_floorf(z + aabb->min_z + PHYSICS_EPSILON); - int max_z = SDL_floorf(z + aabb->max_z - PHYSICS_EPSILON); - for (int bx = min_x; bx <= max_x; bx++) - { - for (int by = min_y; by <= max_y; by++) - { - for (int bz = min_z; bz <= max_z; bz++) - { - if (is_solid(bx, by, bz)) return true; - } - } - } - return false; -} - -bool physics_move_axis(const physics_aabb_t* aabb, float* x, float* y, float* z, int axis, float delta, float step_size, physics_is_solid_fn is_solid) -{ - // Tiny jitter check prevent small float issues and additional unnecessary collision work. - if (SDL_fabsf(delta) <= SDL_FLT_EPSILON) - { - return false; - } - int steps = (int) SDL_ceilf(SDL_fabsf(delta) / step_size); - steps = SDL_max(steps, 1); - float step = delta / steps; - for (int i = 0; i < steps; i++) - { - float next_x = (*x) + ((axis == 0)? step : 0); - float next_y = (*y) + ((axis == 1)? step : 0); - float next_z = (*z) + ((axis == 2)? step : 0); - - if (physics_is_colliding(aabb, next_x, next_y, next_z, is_solid)) - { - resolve_step(aabb, x, y, z, axis, step, is_solid); - return true; - } - - *x = next_x; - *y = next_y; - *z = next_z; - } - return false; -} - -bool physics_overlaps_block(const physics_aabb_t* aabb, float x, float y, float z, const int block_position[3]) -{ - float aabb_min_x = x + aabb->min_x + PHYSICS_EPSILON; - float aabb_max_x = x + aabb->max_x - PHYSICS_EPSILON; - float aabb_min_y = y + aabb->min_y + PHYSICS_EPSILON; - float aabb_max_y = y + aabb->max_y - PHYSICS_EPSILON; - float aabb_min_z = z + aabb->min_z + PHYSICS_EPSILON; - float aabb_max_z = z + aabb->max_z - PHYSICS_EPSILON; - float block_min_x = block_position[0]; - float block_max_x = block_position[0] + 1.0f; - float block_min_y = block_position[1]; - float block_max_y = block_position[1] + 1.0f; - float block_min_z = block_position[2]; - float block_max_z = block_position[2] + 1.0f; - return - (aabb_max_x > block_min_x && aabb_min_x < block_max_x) && - (aabb_max_y > block_min_y && aabb_min_y < block_max_y) && - (aabb_max_z > block_min_z && aabb_min_z < block_max_z); -} diff --git a/src/physics.h b/src/physics.h deleted file mode 100644 index 5e48937..0000000 --- a/src/physics.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -typedef bool (*physics_is_solid_fn)(int x, int y, int z); - -typedef struct physics_aabb -{ - float min_x; - float max_x; - float min_y; - float max_y; - float min_z; - float max_z; -} -physics_aabb_t; - - - -bool physics_is_colliding(const physics_aabb_t* aabb, float x, float y, float z, physics_is_solid_fn is_solid); -bool physics_move_axis(const physics_aabb_t* aabb, float* x, float* y, float* z, int axis, float delta, float step_size, physics_is_solid_fn is_solid); -bool physics_overlaps_block(const physics_aabb_t* aabb, float x, float y, float z, const int block_position[3]); diff --git a/src/player.c b/src/player.c index e670202..d4ead23 100644 --- a/src/player.c +++ b/src/player.c @@ -2,189 +2,282 @@ #include "block.h" #include "camera.h" -#include "physics.h" #include "player.h" +#include "save.h" #include "world.h" -static const float MOVE_SPEED = 5.0f; -static const float SPRINT_MULTIPLIER = 1.8f; -static const float AIR_ACCEL = 6.0f; +typedef struct aabb +{ + float min[3]; + float max[3]; +} +aabb_t; + +static const float PHYSICS_EPSILON = 0.001f; +static const float WALK_SPEED = 5.0f; +static const float SPRINT_SPEED = 9.0f; +static const float SENSITIVITY = 0.1f; +static const float REACH = 10.0f; +static const float AIR_ACCELERATION = 6.0f; static const float GRAVITY = 24.0f; static const float JUMP_SPEED = 8.5f; -static const float FREECAM_SPEED = 0.01f; -static const float FREECAM_FAST_MULTIPLIER = 10.0f; +static const float FLY_SPEED = 0.01f; +static const float FLY_FAST_SPEED = 0.1f; static const float COLLISION_STEP = 0.1f; -static const float GROUND_CHECK_OFFSET = 0.002f; -static const float PLAYER_COLLISION_RADIUS = 0.3f; -static const float PLAYER_COLLISION_HEIGHT = 1.8f; -static const float PLAYER_EYE_OFFSET = 1.62f; - -static physics_aabb_t player_aabb(void) { - return (physics_aabb_t){ - .min_x = -PLAYER_COLLISION_RADIUS, - .max_x = PLAYER_COLLISION_RADIUS, - .min_y = -PLAYER_EYE_OFFSET, - .max_y = PLAYER_COLLISION_HEIGHT - PLAYER_EYE_OFFSET, - .min_z = -PLAYER_COLLISION_RADIUS, - .max_z = PLAYER_COLLISION_RADIUS, - }; +static const float GROUND_OFFSET = 0.002f; +static const float COLLISION_RADIUS = 0.3f; +static const float COLLISION_HEIGHT = 1.8f; +static const float EYE_OFFSET = 1.62f; + +static aabb_t get_aabb() +{ + return (aabb_t) {{-COLLISION_RADIUS, -EYE_OFFSET, -COLLISION_RADIUS}, + {COLLISION_RADIUS, COLLISION_HEIGHT - EYE_OFFSET, COLLISION_RADIUS}}; } -static bool is_block_solid(int x, int y, int z) { - int position[3] = {x, y, z}; - return block_is_solid(world_get_block(position)); +static bool is_solid(const float position[3]) +{ + int index[3] = {position[0], position[1], position[2]}; + return block_is_solid(world_get_block(index)); } -static void move_physics(player_t *player, float dt_ms, const bool *state) { - const physics_aabb_t aabb = player_aabb(); - float dt = SDL_min(dt_ms * 0.001f, 0.05f); - float input_x = state[SDL_SCANCODE_D] - state[SDL_SCANCODE_A]; - float input_z = state[SDL_SCANCODE_W] - state[SDL_SCANCODE_S]; - float length = SDL_sqrtf(input_x * input_x + input_z * input_z); - if (length > SDL_FLT_EPSILON) { - input_x /= length; - input_z /= length; - } - - float speed = MOVE_SPEED; - if (state[SDL_SCANCODE_LCTRL]) { - speed *= SPRINT_MULTIPLIER; - } - - float sy = SDL_sinf(player->camera.yaw); - float cy = SDL_cosf(player->camera.yaw); - float target_x = (cy * input_x + sy * input_z) * speed; - float target_z = (sy * input_x - cy * input_z) * speed; - - if (player->on_ground) { - player->velocity[0] = target_x; - player->velocity[2] = target_z; - } else { - float blend = SDL_min(1.0f, AIR_ACCEL * dt); - player->velocity[0] += (target_x - player->velocity[0]) * blend; - player->velocity[2] += (target_z - player->velocity[2]) * blend; - } - - bool jump_down = state[SDL_SCANCODE_SPACE]; - if (jump_down && !player->jump_was_down && player->on_ground) { - player->velocity[1] = JUMP_SPEED; - player->on_ground = false; - } - player->jump_was_down = jump_down; - - // Before anything "time" check delta for early return (another guard against precision issues/jitter) - // we still need to process input up to this point tho. - if (dt <= SDL_FLT_EPSILON) { - return; - } - - player->velocity[1] -= GRAVITY * dt; - float x = player->camera.x; - float y = player->camera.y; - float z = player->camera.z; - - bool hit_x = physics_move_axis(&aabb, &x, &y, &z, 0, player->velocity[0] * dt, COLLISION_STEP, is_block_solid); - bool hit_y = physics_move_axis(&aabb, &x, &y, &z, 1, player->velocity[1] * dt, COLLISION_STEP, is_block_solid); - bool hit_z = physics_move_axis(&aabb, &x, &y, &z, 2, player->velocity[2] * dt, COLLISION_STEP, is_block_solid); - - player->camera.x = x; - player->camera.y = y; - player->camera.z = z; - - if (hit_x) { - player->velocity[0] = 0.0f; - } - if (hit_z) { - player->velocity[2] = 0.0f; - } - if (hit_y) { - if (player->velocity[1] < 0.0f) { - player->on_ground = true; +static bool is_colliding(const aabb_t *aabb, const float position[3]) +{ + int min[3]; + int max[3]; + for (int i = 0; i < 3; i++) + { + min[i] = SDL_floorf(position[i] + aabb->min[i] + PHYSICS_EPSILON); + max[i] = SDL_floorf(position[i] + aabb->max[i] - PHYSICS_EPSILON); } - player->velocity[1] = 0.0f; - } else { - player->on_ground = false; - } + for (int bx = min[0]; bx <= max[0]; bx++) + for (int by = min[1]; by <= max[1]; by++) + for (int bz = min[2]; bz <= max[2]; bz++) + { + float location[3] = {bx, by, bz}; + if (is_solid(location)) + { + return true; + } + } + return false; } -static void move_freecam(player_t *player, float dt_ms, const bool *state) { - float speed = FREECAM_SPEED; - float dx = state[SDL_SCANCODE_D] - state[SDL_SCANCODE_A]; - float dy = (state[SDL_SCANCODE_E] || state[SDL_SCANCODE_SPACE]) - - (state[SDL_SCANCODE_Q] || state[SDL_SCANCODE_LSHIFT]); - float dz = state[SDL_SCANCODE_W] - state[SDL_SCANCODE_S]; - if (state[SDL_SCANCODE_LCTRL]) { - speed *= FREECAM_FAST_MULTIPLIER; - } - camera_move(&player->camera, dx * speed * dt_ms, dy * speed * dt_ms, - dz * speed * dt_ms); - SDL_zerop(player->velocity); - player->jump_was_down = state[SDL_SCANCODE_SPACE]; - player->on_ground = false; +static void bisect(const aabb_t* aabb, float position[3], int axis, float step) +{ + float start[3] = {position[0], position[1], position[2]}; + float lower = 0.0f; + float upper = 1.0f; + for (int i = 0; i < 8; i++) + { + float t = (lower + upper) * 0.5f; + float location[3] = {start[0], start[1], start[2]}; + location[axis] += step * t; + if (is_colliding(aabb, location)) + { + upper = t; + } + else + { + lower = t; + } + } + position[axis] = start[axis] + step * lower; +} + +static bool move(const aabb_t* aabb, float position[3], int axis, float delta) +{ + if (SDL_fabsf(delta) <= SDL_FLT_EPSILON) + { + return false; + } + int steps = SDL_ceilf(SDL_fabsf(delta) / COLLISION_STEP); + steps = SDL_max(steps, 1); + float step = delta / steps; + for (int i = 0; i < steps; i++) + { + float location[3] = {position[0], position[1], position[2]}; + location[axis] += step; + if (is_colliding(aabb, location)) + { + bisect(aabb, position, axis, step); + return true; + } + SDL_memcpy(position, location, 12); + } + return false; } -void player_init(player_t *player) { - SDL_zerop(player); - camera_init(&player->camera, CAMERA_TYPE_PERSPECTIVE); - player->camera.x = -200.0f; - player->camera.y = 50.0f; - player->camera.z = 0.0f; - player->block = BLOCK_YELLOW_TORCH; - player->controller = PLAYER_CONTROLLER_FP; +void player_save_or_load(player_t* player, int id, bool save) +{ + struct + { + float x; + float y; + float z; + float pitch; + float yaw; + block_t block; + } + data; + if (save) + { + data.x = player->camera.x; + data.y = player->camera.y; + data.z = player->camera.z; + data.pitch = player->camera.pitch; + data.yaw = player->camera.yaw; + data.block = player->block; + save_set_player(id, &data, sizeof(data)); + return; + } + camera_init(&player->camera, CAMERA_TYPE_PERSPECTIVE); + player->camera.x = -200.0f; + player->camera.y = 50.0f; + player->camera.z = 0.0f; + player->controller = PLAYER_CONTROLLER_WALK; + player->block = BLOCK_YELLOW_TORCH; + if (save_get_player(id, &data, sizeof(data))) + { + player->block = data.block; + player->camera.x = data.x; + player->camera.y = data.y; + player->camera.z = data.z; + player->camera.pitch = data.pitch; + player->camera.yaw = data.yaw; + } + player->query = world_raycast(&player->camera, REACH); } -void player_set_controller(player_t *player, player_controller_t controller) { - if (player->controller == controller) { - return; - } - player->controller = controller; - SDL_zerop(player->velocity); - player->jump_was_down = false; - if (controller == PLAYER_CONTROLLER_FP) { - player_update_grounded(player); - } else { - player->on_ground = false; - } +void player_toggle_controller(player_t* player) +{ + player->controller++; + player->controller %= PLAYER_CONTROLLER_COUNT; } -void player_toggle_controller(player_t *player) { - if (player->controller == PLAYER_CONTROLLER_FP) { - player_set_controller(player, PLAYER_CONTROLLER_FREECAM); - } else { - player_set_controller(player, PLAYER_CONTROLLER_FP); - } +void player_rotate(player_t* player, float pitch, float yaw) +{ + camera_rotate(&player->camera, pitch * -SENSITIVITY, yaw * SENSITIVITY); + player->query = world_raycast(&player->camera, REACH); } -const char *player_controller_name(player_controller_t controller) { - if (controller == PLAYER_CONTROLLER_FREECAM) { - return "freecam"; - } else { - return "first_person"; - } +void player_move(player_t* player, float dt) +{ + const bool* keys = SDL_GetKeyboardState(NULL); + if (player->controller == PLAYER_CONTROLLER_WALK) + { + const aabb_t aabb = get_aabb(); + dt = SDL_min(dt * 0.001f, 0.05f); + float input_x = keys[SDL_SCANCODE_D] - keys[SDL_SCANCODE_A]; + float input_z = keys[SDL_SCANCODE_W] - keys[SDL_SCANCODE_S]; + float length = SDL_sqrtf(input_x * input_x + input_z * input_z); + if (length > SDL_FLT_EPSILON) + { + input_x /= length; + input_z /= length; + } + float speed = keys[SDL_SCANCODE_LCTRL] ? SPRINT_SPEED : WALK_SPEED; + float sy = SDL_sinf(player->camera.yaw); + float cy = SDL_cosf(player->camera.yaw); + float target_x = (cy * input_x + sy * input_z) * speed; + float target_z = (sy * input_x - cy * input_z) * speed; + if (player->is_on_ground) + { + player->velocity[0] = target_x; + player->velocity[2] = target_z; + } + else + { + float blend = SDL_min(1.0f, AIR_ACCELERATION * dt); + player->velocity[0] += (target_x - player->velocity[0]) * blend; + player->velocity[2] += (target_z - player->velocity[2]) * blend; + } + if (keys[SDL_SCANCODE_SPACE] && player->is_on_ground) + { + player->velocity[1] = JUMP_SPEED; + player->is_on_ground = false; + } + if (dt <= SDL_FLT_EPSILON) + { + return; + } + player->velocity[1] -= GRAVITY * dt; + bool hits[3]; + for (int i = 0; i < 3; i++) + { + hits[i] = move(&aabb, player->camera.position, i, player->velocity[i] * dt); + } + if (hits[0]) + { + player->velocity[0] = 0.0f; + } + if (hits[2]) + { + player->velocity[2] = 0.0f; + } + if (hits[1]) + { + if (player->velocity[1] < 0.0f) + { + player->is_on_ground = true; + } + player->velocity[1] = 0.0f; + } + else + { + player->is_on_ground = false; + } + player->query = world_raycast(&player->camera, REACH); + } + else + { + float speed = keys[SDL_SCANCODE_LCTRL] ? FLY_FAST_SPEED : FLY_SPEED; + float dx = keys[SDL_SCANCODE_D] - keys[SDL_SCANCODE_A]; + float dy = (keys[SDL_SCANCODE_E] || keys[SDL_SCANCODE_SPACE]) - (keys[SDL_SCANCODE_Q] || keys[SDL_SCANCODE_LSHIFT]); + float dz = keys[SDL_SCANCODE_W] - keys[SDL_SCANCODE_S]; + camera_move(&player->camera, dx * speed * dt, dy * speed * dt, dz * speed * dt); + } } -void player_rotate(player_t *player, float pitch, float yaw, - float sensitivity) { - camera_rotate(&player->camera, pitch * -sensitivity, yaw * sensitivity); +void player_place_block(const player_t* player) +{ + if (player->query.block == BLOCK_EMPTY) + { + return; + } + const aabb_t aabb = get_aabb(); + for (int i = 0; i < 3; i++) + { + float min = player->camera.position[i] + aabb.min[i] + PHYSICS_EPSILON; + float max = player->camera.position[i] + aabb.max[i] - PHYSICS_EPSILON; + if (max <= player->query.previous[i] || min >= player->query.previous[i] + 1.0f) + { + world_set_block(player->query.previous, player->block); + break; + } + } } -void player_move(player_t *player, float dt_ms, const bool *keyboard_state) { - if (player->controller == PLAYER_CONTROLLER_FREECAM) { - move_freecam(player, dt_ms, keyboard_state); - } else { - move_physics(player, dt_ms, keyboard_state); - } +void player_select_block(player_t* player) +{ + if (player->query.block != BLOCK_EMPTY) + { + player->block = player->query.block; + } } -bool player_overlaps_block(const player_t *player, const int position[3]) { - const physics_aabb_t aabb = player_aabb(); - return physics_overlaps_block(&aabb, player->camera.x, - player->camera.y, player->camera.z, position); +void player_break_block(const player_t* player) +{ + if (player->query.block != BLOCK_EMPTY) + { + world_set_block(player->query.current, BLOCK_EMPTY); + } } -void player_update_grounded(player_t *player) { - const physics_aabb_t aabb = player_aabb(); - player->on_ground = physics_is_colliding( - &aabb, player->camera.x, player->camera.y - GROUND_CHECK_OFFSET, - player->camera.z, is_block_solid); +void player_change_block(player_t* player, int dy) +{ + static const int COUNT = BLOCK_COUNT - BLOCK_EMPTY - 1; + int block = player->block - (BLOCK_EMPTY + 1) + dy; + block = (block + COUNT) % COUNT; + player->block = block + BLOCK_EMPTY + 1; } diff --git a/src/player.h b/src/player.h index ec11552..07ce1e1 100644 --- a/src/player.h +++ b/src/player.h @@ -4,30 +4,32 @@ #include "block.h" #include "camera.h" +#include "world.h" typedef enum player_controller { - PLAYER_CONTROLLER_FP, - PLAYER_CONTROLLER_FREECAM, + PLAYER_CONTROLLER_WALK, + PLAYER_CONTROLLER_FLY, + PLAYER_CONTROLLER_COUNT, } player_controller_t; typedef struct player { camera_t camera; + player_controller_t controller; float velocity[3]; - bool on_ground; - bool jump_was_down; + bool is_on_ground; + world_query_t query; block_t block; - player_controller_t controller; } player_t; -void player_init(player_t* player); -void player_set_controller(player_t* player, player_controller_t controller); +void player_save_or_load(player_t* player, int id, bool save); void player_toggle_controller(player_t* player); -const char* player_controller_name(player_controller_t controller); -void player_rotate(player_t* player, float pitch, float yaw, float sensitivity); -void player_move(player_t* player, float dt_ms, const bool* keyboard_state); -bool player_overlaps_block(const player_t* player, const int position[3]); -void player_update_grounded(player_t* player); +void player_rotate(player_t* player, float pitch, float yaw); +void player_move(player_t* player, float dt); +void player_place_block(const player_t* player); +void player_select_block(player_t* player); +void player_break_block(const player_t* player); +void player_change_block(player_t* player, int dy);