From 04ae797196476e53c075d52ac3ab69f085eedfc4 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Fri, 12 Dec 2025 01:24:09 +0100 Subject: [PATCH] Bad collisions --- src/Game.zig | 16 +- src/Player.zig | 463 +++++++++++++++++++++++++++++++++++++++++-- src/assets/Chunk.zig | 18 +- 3 files changed, 468 insertions(+), 29 deletions(-) diff --git a/src/Game.zig b/src/Game.zig index 687f0f8..05ad90a 100644 --- a/src/Game.zig +++ b/src/Game.zig @@ -56,7 +56,7 @@ player: Player, const max_textures = 1024; const max_point_lights = 1024; const max_directional_lights = 4; -const chunk_descriptor_pool = 512; +const chunk_descriptor_pool = 1024; const camera_near_plane = 0.1; @@ -180,25 +180,25 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain try vertex_buffer.write(engine, .{ .elements = &.{ .init( - .init(-0.5, -0.5, 0), + .init(0, 0, 0), .init(0, 1), .init(0, 0, 1), .init(1, 0, 0, -1), ), .init( - .init(0.5, -0.5, 0), + .init(1, 0, 0), .init(1, 1), .init(0, 0, 1), .init(1, 0, 0, -1), ), .init( - .init(-0.5, 0.5, 0), + .init(0, 1, 0), .init(0, 0), .init(0, 0, 1), .init(1, 0, 0, -1), ), .init( - .init(0.5, 0.5, 0), + .init(1, 1, 0), .init(1, 0), .init(0, 0, 1), .init(1, 0, 0, -1), @@ -534,8 +534,8 @@ pub fn init(allocator: std.mem.Allocator, engine: *Engine, swapchain: *Swapchain const world_seed = engine.random.int(u64); std.log.info("Using world seed 0x{x:0<16}", .{world_seed}); var it = Iterator2(i16).init(.{ - .min = .{ -8, -8 }, - .max = .{ 7, 7 }, + .min = .{ -12, -12 }, + .max = .{ 12, 12 }, }); while (it.next()) |chunk_coords2| { const chunk_coords3 = chunk_coords2 ++ [_]i16{0}; @@ -697,7 +697,7 @@ pub fn deinit(self: *Game) void { } pub fn update(self: *Game, dt: f32) void { - self.player.update(dt); + self.player.update(dt, &self.chunks); self.render() catch |err| { std.log.err("Failed to render: {s}", .{@errorName(err)}); diff --git a/src/Player.zig b/src/Player.zig index 909c840..441a9cf 100644 --- a/src/Player.zig +++ b/src/Player.zig @@ -4,6 +4,8 @@ const std = @import("std"); const glfw = @import("zglfw"); const math = @import("math.zig"); +const Chunk = @import("assets/Chunk.zig"); +const Iterator2 = math.Iterator2; const Vector2 = math.Vector2; const Vector3 = math.Vector3; @@ -23,6 +25,17 @@ const AxisState = enum { } }; +const ButtonState = struct { + current: bool = false, + pressed_this_frame: bool = false, + released_this_frame: bool = false, + + pub fn reset(self: *ButtonState) void { + self.pressed_this_frame = false; + self.released_this_frame = false; + } +}; + const Axis = struct { state: AxisState = .none, positive_key: glfw.Key, @@ -72,6 +85,58 @@ const Axis = struct { } }; +const Button = struct { + state: ButtonState = .{}, + key: glfw.Key, + + pub fn init(key: glfw.Key) Button { + return .{ + .key = key, + }; + } + + pub fn onKeyDown(self: *Button, key_code: glfw.Key) void { + if (key_code == self.key) { + self.state.current = true; + self.state.pressed_this_frame = true; + } + } + + pub fn onKeyUp(self: *Button, key_code: glfw.Key) void { + if (key_code == self.key) { + self.state.current = false; + self.state.released_this_frame = true; + } + } + + pub fn reset(self: *Button) void { + self.state.reset(); + } + + pub inline fn isDown(self: Button) bool { + return self.state.current; + } + + pub inline fn isPressed(self: Button) bool { + return self.state.pressed_this_frame; + } + + pub inline fn isReleased(self: Button) bool { + return self.state.released_this_frame; + } +}; + +const MovementState = union(enum) { + ground: struct { + horizontal_velocity: Vector2, + }, + air: struct { + horizontal_velocity: Vector2, + vertical_velocity: f32, + }, + flight: void, +}; + position: Vector3, pitch_rad: f32, yaw_rad: f32, @@ -79,10 +144,13 @@ yaw_rad: f32, x_input: Axis = .init(.d, .a), y_input: Axis = .init(.w, .s), z_input: Axis = .init(.space, .left_shift), +jump_input: Button = .init(.space), mouse_sensitivity: f32 = 0.002, vertical_fov_deg: f32 = 80, +movement_state: MovementState, + pub const camera_height = 1.62; pub const horizontal_speed = 11.0; @@ -91,11 +159,26 @@ pub const vertical_speed = 7.49; pub const min_pitch_rad = -0.5 * std.math.pi; pub const max_pitch_rad = 0.5 * std.math.pi; +pub const half_width = 0.4; +pub const height = 1.8; +pub const speed = 5.612; +pub const step_up = 0.53125; +pub const step_down = 0.53125; +pub const jump_velocity = 6.1237245; +pub const gravity = 15; +pub const vertical_velocity_cap = 30; +pub const ground_acceleration = 56.12; +pub const air_acceleration = 56.12; +pub const air_speed_limit = 0.5612; + +const distance_from_surface = 0.0009765625; + pub fn init(position: Vector3, pitch_rad: f32, yaw_rad: f32) Player { return .{ .position = position, .pitch_rad = pitch_rad, .yaw_rad = yaw_rad, + .movement_state = .flight, }; } @@ -116,20 +199,376 @@ pub fn onMouseMove(self: *Player, dx: f32, dy: f32) void { self.yaw_rad = @mod(self.yaw_rad - dx * self.mouse_sensitivity, std.math.tau); } -pub fn update(self: *Player, dt: f32) void { - var horizontal_input_vector = Vector2 - .init(self.x_input.getComponent(), self.y_input.getComponent()) - .rotate(self.yaw_rad); - const horizontal_input_vector_len_squared = horizontal_input_vector.lenSquared(); - if (horizontal_input_vector_len_squared > 1) { - horizontal_input_vector = horizontal_input_vector.divScalar(horizontal_input_vector_len_squared); - } +pub fn update(self: *Player, dt: f32, chunks: *const std.AutoHashMapUnmanaged([3]i16, Chunk)) void { + defer self.resetAllButtons(); + + // --- GATHER INPUTS ------------------------------------------------------- + + const horizontal_input_vector = blk: { + var ret = Vector2 + .init(self.x_input.getComponent(), self.y_input.getComponent()) + .rotate(self.yaw_rad); + const len_squared = ret.lenSquared(); + if (len_squared > 1) { + ret = ret.divScalar(@sqrt(len_squared)); + } + break :blk ret; + }; const vertical_input_vector = self.z_input.getComponent(); - const horizontal_velocity = horizontal_input_vector.mulScalar(horizontal_speed); - const vertical_velocity = vertical_input_vector * vertical_speed; + // --- STATE TRANSITIONS --------------------------------------------------- - const displacement = horizontal_velocity.asVector3(vertical_velocity).mulScalar(dt); + switch (self.movement_state) { + .ground => |ground| { + if (self.jump_input.isPressed()) { + self.movement_state = .{ + .air = .{ + .horizontal_velocity = ground.horizontal_velocity, + .vertical_velocity = jump_velocity, + }, + }; + } + }, + else => {}, + } - self.position = .add(self.position, displacement); + // --- STATE HANDLING ------------------------------------------------------ + + switch (self.movement_state) { + .ground => {}, + .air => {}, + .flight => { + const horizontal_velocity = horizontal_input_vector.mulScalar(horizontal_speed); + const vertical_velocity = vertical_input_vector * vertical_speed; + + var horizontal_displacement = horizontal_velocity.mulScalar(dt); + var vertical_displacement = vertical_velocity * dt; + + if (vertical_displacement > 0.0) { + if (sweepCastUp(self.position, vertical_displacement, chunks)) |hit| { + vertical_displacement = hit.projected_distance - distance_from_surface; + } + } + + var i: usize = 0; + while (i < 2) : (i += 1) { + if (sweepCastHorizontal(self.position, horizontal_displacement, chunks)) |hit| { + horizontal_displacement = .add( + horizontal_displacement, + hit.normal + .asVector2() + .mulScalar(hit.projected_distance + distance_from_surface), + ); + } + } + + if (vertical_displacement < 0.0) { + if (sweepCastDown(self.position, -vertical_displacement, chunks)) |hit| { + vertical_displacement = -(hit.projected_distance - distance_from_surface); + } + } + + const displacement = horizontal_displacement.asVector3(vertical_displacement); + self.position = .add(self.position, displacement); + }, + } +} + +const SweepHit = struct { + normal: Vector3, + projected_distance: f32, +}; + +fn sweepCastDown(origin: Vector3, distance: f32, chunks: *const std.AutoHashMapUnmanaged([3]i16, Chunk)) ?SweepHit { + const min_origin = origin.add(.init(-half_width, -half_width, 0.0)); + const max_origin = origin.add(.init(half_width, half_width, height)); + + const min_x = blockCoord(.border_up, min_origin.getX()); + const min_y = blockCoord(.border_up, min_origin.getY()); + const max_x = blockCoord(.border_down, max_origin.getX()); + const max_y = blockCoord(.border_down, max_origin.getY()); + + const start_z = blockCoord(.next_down_with_border, min_origin.getZ()); + const end_z = blockCoord(.border_down, min_origin.getZ() - distance); + + var z: i32 = start_z; + while (z >= end_z) : (z -= 1) { + const fz = @as(f32, @floatFromInt(z)) + 1.0; + var it = Iterator2(i32).init(.{ + .min = .{ min_x, min_y }, + .max = .{ max_x, max_y }, + }); + while (it.next()) |xy| { + const x, const y = xy; + if (!isSolid(chunks, x, y, z)) continue; + + return .{ + .projected_distance = distance - (min_origin.getZ() - fz), + .normal = .unit_z, + }; + } + } + + return null; +} + +fn sweepCastUp(origin: Vector3, distance: f32, chunks: *const std.AutoHashMapUnmanaged([3]i16, Chunk)) ?SweepHit { + const min_origin = origin.add(.init(-half_width, -half_width, 0.0)); + const max_origin = origin.add(.init(half_width, half_width, height)); + + const min_x = blockCoord(.border_up, min_origin.getX()); + const min_y = blockCoord(.border_up, min_origin.getY()); + const max_x = blockCoord(.border_down, max_origin.getX()); + const max_y = blockCoord(.border_down, max_origin.getY()); + + const start_z = blockCoord(.next_up_with_border, max_origin.getZ()); + const end_z = blockCoord(.border_up, max_origin.getZ() + distance); + + var z: i32 = start_z; + while (z <= end_z) : (z += 1) { + const fz = @as(f32, @floatFromInt(z)); + var it = Iterator2(i32).init(.{ + .min = .{ min_x, min_y }, + .max = .{ max_x, max_y }, + }); + while (it.next()) |xy| { + const x, const y = xy; + if (!isSolid(chunks, x, y, z)) continue; + + return .{ + .projected_distance = distance - (fz - max_origin.getZ()), + .normal = .negate(.unit_z), + }; + } + } + + return null; +} + +fn sweepCastHorizontal(origin: Vector3, ray: Vector2, chunks: *const std.AutoHashMapUnmanaged([3]i16, Chunk)) ?SweepHit { + const min_origin = origin.add(.init(-half_width, -half_width, 0)); + const max_origin = origin.add(.init(half_width, half_width, height)); + + const min_z = blockCoord(.border_up, min_origin.getZ()); + const max_z = blockCoord(.border_down, max_origin.getZ()); + + var closest_hit: ?SweepHit = null; + var closest_hit_distance = std.math.inf(f32); + + const dydx = ray.getY() / ray.getX(); + const dxdy = ray.getX() / ray.getY(); + + // Positive X + if (ray.getX() > 0.0) { + const x0 = max_origin.getX(); + const y0 = min_origin.getY(); + const y1 = max_origin.getY(); + + const start_x = blockCoord(.next_up_with_border, x0); + const end_x = blockCoord(.border_up, x0 + ray.getX()); + + var x: i32 = start_x; + px: while (x <= end_x) : (x += 1) { + const fx = @as(f32, @floatFromInt(x)); + const min_y = blockCoord(.border_up, (fx - x0) * dydx + y0); + const max_y = blockCoord(.border_down, (fx - x0) * dydx + y1); + var it = Iterator2(i32).init(.{ + .min = .{ min_y, min_z }, + .max = .{ max_y, max_z }, + }); + while (it.next()) |yz| { + const y, const z = yz; + if (!isSolid(chunks, x, y, z)) continue; + + const dx = fx - x0; + const dy = dydx * dx; + const distance = Vector2.init(dx, dy).len(); + + if (distance >= closest_hit_distance) { + break :px; + } + + closest_hit = .{ + .projected_distance = ray.getX() - dx, + .normal = .negate(.unit_x), + }; + closest_hit_distance = distance; + } + } + } + + // Negative X + if (ray.getX() < 0.0) { + const x0 = min_origin.getX(); + const y0 = min_origin.getY(); + const y1 = max_origin.getY(); + + const start_x = blockCoord(.next_down_with_border, x0); + const end_x = blockCoord(.border_down, x0 + ray.getX()); + + var x: i32 = start_x; + nx: while (x >= end_x) : (x -= 1) { + const fx = @as(f32, @floatFromInt(x)) + 1.0; + const min_y = blockCoord(.border_up, (fx - x0) * dydx + y0); + const max_y = blockCoord(.border_down, (fx - x0) * dydx + y1); + var it = Iterator2(i32).init(.{ + .min = .{ min_y, min_z }, + .max = .{ max_y, max_z }, + }); + while (it.next()) |yz| { + const y, const z = yz; + if (!isSolid(chunks, x, y, z)) continue; + + const dx = fx - x0; + const dy = dydx * dx; + const distance = Vector2.init(dx, dy).len(); + + if (distance >= closest_hit_distance) { + break :nx; + } + + closest_hit = .{ + .projected_distance = -(ray.getX() - dx), + .normal = .unit_x, + }; + closest_hit_distance = distance; + } + } + } + + // Positive Y + if (ray.getY() > 0.0) { + const y0 = max_origin.getY(); + const x0 = min_origin.getX(); + const x1 = max_origin.getX(); + + const start_y: i32 = @intFromFloat(@ceil(y0)); + const end_y: i32 = @intFromFloat(@floor(y0 + ray.getY())); + + var y: i32 = start_y; + py: while (y <= end_y) : (y += 1) { + const fy = @as(f32, @floatFromInt(y)); + const min_x: i32 = @intFromFloat(@floor(fy - y0) * dxdy + x0); + const max_x: i32 = @intFromFloat(@ceil((fy - y0) * dxdy + x1 - 1.0)); + var it = Iterator2(i32).init(.{ + .min = .{ min_x, min_z }, + .max = .{ max_x, max_z }, + }); + while (it.next()) |xz| { + const x, const z = xz; + if (!isSolid(chunks, x, y, z)) continue; + + const dy = fy - y0; + const dx = dxdy * dy; + const distance = Vector2.init(dx, dy).len(); + + if (distance >= closest_hit_distance) { + break :py; + } + + closest_hit = .{ + .projected_distance = ray.getY() - dy, + .normal = .negate(.unit_y), + }; + closest_hit_distance = distance; + } + } + } + + // Negative Y + if (ray.getY() < 0.0) { + const y0 = min_origin.getY(); + const x0 = min_origin.getX(); + const x1 = max_origin.getX(); + + const start_y = blockCoord(.next_down_with_border, y0); + const end_y = blockCoord(.border_down, y0 + ray.getY()); + + var y: i32 = start_y; + py: while (y >= end_y) : (y -= 1) { + const fy = @as(f32, @floatFromInt(y)) + 1.0; + const min_x: i32 = @intFromFloat(@floor(fy - y0) * dxdy + x0); + const max_x: i32 = @intFromFloat(@ceil((fy - y0) * dxdy + x1 - 1.0)); + var it = Iterator2(i32).init(.{ + .min = .{ min_x, min_z }, + .max = .{ max_x, max_z }, + }); + while (it.next()) |xz| { + const x, const z = xz; + if (!isSolid(chunks, x, y, z)) continue; + + const dy = fy - y0; + const dx = dxdy * dy; + const distance = Vector2.init(dx, dy).len(); + + if (distance >= closest_hit_distance) { + break :py; + } + + closest_hit = .{ + .projected_distance = -(ray.getY() - dy), + .normal = .unit_y, + }; + closest_hit_distance = distance; + } + } + } + + return closest_hit; +} + +fn resetAllButtons(self: *Player) void { + inline for (@typeInfo(Player).@"struct".fields) |field| { + if (field.type == Button) { + @field(self, field.name).reset(); + } + } +} + +fn isSolid(chunks: *const std.AutoHashMapUnmanaged([3]i16, Chunk), x: i32, y: i32, z: i32) bool { + const chunk_x = std.math.cast(i16, @divFloor(x, Chunk.chunk_size)) orelse return true; + const chunk_y = std.math.cast(i16, @divFloor(y, Chunk.chunk_size)) orelse return true; + const chunk_z = std.math.cast(i16, @divFloor(z, Chunk.chunk_size)) orelse return true; + if (chunks.get(.{ chunk_x, chunk_y, chunk_z })) |chunk| { + const local_x: u4 = @intCast(@mod(x, Chunk.chunk_size)); + const local_y: u4 = @intCast(@mod(y, Chunk.chunk_size)); + const local_z: u4 = @intCast(@mod(z, Chunk.chunk_size)); + return chunk.blocks[local_z][local_y][local_x] != .air; + } else { + return false; + } +} + +const RoundingMode = enum { + border_down, + border_up, + next_down_with_border, + next_down_without_border, + next_up_with_border, + next_up_without_border, +}; + +inline fn blockCoord(comptime rounding_mode: RoundingMode, coord: f32) i32 { + return switch (rounding_mode) { + .border_down => @intFromFloat(@ceil(coord - 1.0)), + .border_up => @intFromFloat(@floor(coord)), + .next_down_with_border => @intFromFloat(@floor(coord - 1.0)), + .next_down_without_border => @intFromFloat(@ceil(coord - 2.0)), + .next_up_with_border => @intFromFloat(@ceil(coord)), + .next_up_without_border => @intFromFloat(@floor(coord + 1.0)), + }; +} + +inline fn blockCoord2(comptime rounding_mode: RoundingMode, coord: Vector2) [2]i32 { + return switch (rounding_mode) { + .border_down => @as(Vector2.Vector, @intFromFloat(@ceil(coord.sub(.one).vector))), + .border_up => @as(Vector2.Vector, @intFromFloat(@floor(coord.vector))), + }; +} + +inline fn blockCoord3(comptime rounding_mode: RoundingMode, coord: Vector3) [3]i32 { + return switch (rounding_mode) { + .border_down => @as(Vector3.Vector, @intFromFloat(@ceil(coord.sub(.one).vector))), + .border_up => @as(Vector3.Vector, @intFromFloat(@floor(coord.vector))), + }; } diff --git a/src/assets/Chunk.zig b/src/assets/Chunk.zig index 9d6d255..e136541 100644 --- a/src/assets/Chunk.zig +++ b/src/assets/Chunk.zig @@ -110,13 +110,13 @@ pub fn refresh(self: *Chunk, engine: *Engine, blocks: *const Blocks, neighbors: for (row, 0..) |id, ix| { const fx: f32 = @floatFromInt(ix); const block: *const Blocks.Definition = &blocks.array.items[id.toInt()]; - const center = Vector3.add(self.origin, .init(fx, fy, fz)); + const origin = Vector3.add(self.origin, .init(fx, fy, fz)); inline for (@typeInfo(voxels.Orientation).@"enum".fields) |field| { const side = @field(voxels.Orientation, field.name); const material = @field(block.walls, field.name).material; if (material != .empty and self.getOpposite(side, ix, iy, iz, blocks, neighbors) == .empty) { - const matrix = getMatrix(side, center); + const matrix = getMatrix(side, origin); try uniforms.append(temp_allocator, .init(matrix, matrix, material)); } } @@ -209,44 +209,44 @@ fn getOpposite(self: *const Chunk, comptime side: voxels.Orientation, x: usize, }; } -fn getMatrix(comptime side: voxels.Orientation, center: Vector3) Matrix4x4 { +fn getMatrix(comptime side: voxels.Orientation, origin: Vector3) Matrix4x4 { return switch (side) { // zig fmt: off .positive_x => .init( 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, - center.getX() + 0.5, center.getY(), center.getZ(), 1, + origin.getX() + 1, origin.getY(), origin.getZ(), 1, ), .negative_x => .init( 0, -1, 0, 0, 0, 0, 1, 0, -1, 0, 0, 0, - center.getX() - 0.5, center.getY(), center.getZ(), 1, + origin.getX(), origin.getY() + 1, origin.getZ(), 1, ), .positive_y => .init( -1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, - center.getX(), center.getY() + 0.5, center.getZ(), 1, + origin.getX() + 1, origin.getY() + 1, origin.getZ(), 1, ), .negative_y => .init( 1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, - center.getX(), center.getY() - 0.5, center.getZ(), 1, + origin.getX(), origin.getY(), origin.getZ(), 1, ), .positive_z => .init( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, - center.getX(), center.getY(), center.getZ() + 0.5, 1, + origin.getX(), origin.getY(), origin.getZ() + 1, 1, ), .negative_z => .init( 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, - center.getX(), center.getY(), center.getZ() - 0.5, 1, + origin.getX() + 1, origin.getY() + 1, origin.getZ(), 1, ), // zig fmt: on };