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);