Files
voxel-game/src/Player.zig

540 lines
18 KiB
Zig

const Player = @This();
const std = @import("std");
const c = @import("const.zig");
const ctx = @import("AppContext.zig");
const glfw = @import("zglfw");
const math = @import("math.zig");
const vm = @import("vecmath");
const Blocks = @import("assets/Blocks.zig");
const Chunks = @import("Chunks.zig");
const Game = @import("Game.zig");
const Iterator2 = math.Iterator2;
const AxisState = enum {
none,
positive,
negative,
both_positive,
both_negative,
pub fn getComponentFloat(self: AxisState) f32 {
return switch (self) {
.none => 0,
.positive, .both_positive => 1,
.negative, .both_negative => -1,
};
}
pub fn getComponentInt(self: AxisState) i32 {
return switch (self) {
.none => 0,
.positive, .both_positive => 1,
.negative, .both_negative => -1,
};
}
pub fn getComponentIntFrac(self: AxisState) i32 {
return switch (self) {
.none => 0,
.positive, .both_positive => std.math.maxInt(i32),
.negative, .both_negative => -std.math.maxInt(i32),
};
}
};
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,
negative_key: glfw.Key,
pub fn init(positive_key: glfw.Key, negative_key: glfw.Key) Axis {
return .{
.positive_key = positive_key,
.negative_key = negative_key,
};
}
pub fn onKeyDown(self: *Axis, key_code: glfw.Key) void {
if (key_code == self.positive_key) {
switch (self.state) {
.none => self.state = .positive,
.negative => self.state = .both_positive,
else => unreachable,
}
} else if (key_code == self.negative_key) {
switch (self.state) {
.none => self.state = .negative,
.positive => self.state = .both_negative,
else => unreachable,
}
}
}
pub fn onKeyUp(self: *Axis, key_code: glfw.Key) void {
if (key_code == self.positive_key) {
switch (self.state) {
.positive => self.state = .none,
.both_positive, .both_negative => self.state = .negative,
else => unreachable,
}
} else if (key_code == self.negative_key) {
switch (self.state) {
.negative => self.state = .none,
.both_positive, .both_negative => self.state = .positive,
else => unreachable,
}
}
}
pub fn getComponentFloat(self: Axis) f32 {
return self.state.getComponentFloat();
}
pub fn getComponentInt(self: Axis) i32 {
return self.state.getComponentInt();
}
pub fn getComponentIntFrac(self: Axis) i32 {
return self.state.getComponentIntFrac();
}
};
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_sv: vm.Vector2Int,
},
air: struct {
horizontal_velocity_sv: vm.Vector2Int,
vertical_velocity_sv: i32,
},
flight: void,
};
position_sv: vm.Vector3Int,
pitch_turns: f32,
yaw_turns: 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.0003,
vertical_fov_deg: f32 = 80,
movement_state: MovementState,
maybe_raycast_hit: ?Chunks.RaycastHit = null,
block_index: usize = 0,
pub const blocks = [9][:0]const u8{
"Bricks.json",
"ChiseledStoneBricks.json",
"Cobblestone.json",
"DiamondBlock.json",
"GoldBlock.json",
"IronBlock.json",
"OakPlanks.json",
"SmoothStone.json",
"StoneBricks.json",
};
pub const camera_height_vx = 1.62;
pub const horizontal_speed_sv = sv(11.0);
pub const vertical_speed_sv = sv(7.49);
pub const min_pitch_turns = -0.25;
pub const max_pitch_turns = 0.25;
pub const collision_half_width_sv = sv(0.4);
pub const collision_height_sv = sv(1.8);
pub const speed_sv = sv(5.612);
pub const step_up_sv = sv(0.53125);
pub const step_down_sv = sv(0.53125);
pub const jump_velocity_sv = sv(6.1237245);
pub const gravity_sv = sv(15);
pub const vertical_velocity_cap_sv = sv(30);
pub const ground_acceleration_sv = sv(56.12);
pub const air_acceleration_sv = sv(56.12);
pub const air_speed_limit_sv = sv(0.5612);
pub const raycast_length_sv = sv(5.0);
pub fn init(position_sv: vm.Vector3Int, pitch_turns: f32, yaw_turns: f32) Player {
return .{
.position_sv = position_sv,
.pitch_turns = pitch_turns,
.yaw_turns = yaw_turns,
.movement_state = .flight,
};
}
pub fn onKeyDown(self: *Player, key: glfw.Key) void {
self.x_input.onKeyDown(key);
self.y_input.onKeyDown(key);
self.z_input.onKeyDown(key);
const key_code = @intFromEnum(key);
if (key_code >= @intFromEnum(glfw.Key.one) and key_code <= @intFromEnum(glfw.Key.nine)) {
self.block_index = @intCast(key_code - @intFromEnum(glfw.Key.one));
}
}
pub fn onKeyUp(self: *Player, key: glfw.Key) void {
self.x_input.onKeyUp(key);
self.y_input.onKeyUp(key);
self.z_input.onKeyUp(key);
}
pub fn onMouseMove(self: *Player, dx: f32, dy: f32) void {
self.pitch_turns = std.math.clamp(self.pitch_turns - dy * self.mouse_sensitivity, min_pitch_turns, max_pitch_turns);
self.yaw_turns = @mod(self.yaw_turns - dx * self.mouse_sensitivity, 1);
}
pub fn onMouseDown(self: *Player, button: glfw.MouseButton, game: *Game) void {
if (self.maybe_raycast_hit) |raycast_hit| {
switch (button) {
.left => {
game.chunks.setVoxelAt(
raycast_hit.voxel,
.air,
&game.blocks,
game.descriptor_pool,
game.per_batch_descriptor_set_layout,
) catch |err| {
std.log.err("Error while destroying voxel {f}: {}", .{ raycast_hit.voxel, err });
};
},
.right => blk: {
const target_vx = raycast_hit.voxel.add(raycast_hit.side.getSignVector());
const id = game.blocks.getOrLoad(blocks[self.block_index]) catch |err| {
std.log.err("Error while placing voxel at {f}: {}", .{ target_vx, err });
break :blk;
};
game.chunks.setVoxelAt(
target_vx,
id,
&game.blocks,
game.descriptor_pool,
game.per_batch_descriptor_set_layout,
) catch |err| {
std.log.err("Error while placing voxel at {f}: {}", .{ target_vx, err });
};
},
else => {},
}
}
}
pub fn update(self: *Player, dt: f32, game: *Game) void {
const swapchain = ctx.swapchain;
const materials = ctx.materials;
const gui = ctx.gui;
defer self.resetAllButtons();
const raycast_origin_sv = self.position_sv
.add(.init(0, 0, @intFromFloat(@round(c.sv_per_vx * camera_height_vx))));
const raycast_ray_sv = vm.Vector3
.init(0, raycast_length_sv, 0)
.rotate(.mulQuaternion(
.initRotation(.XY, self.yaw_turns),
.initRotation(.YZ, self.pitch_turns),
))
.round()
.asInt();
self.maybe_raycast_hit = game.chunks.raycast(raycast_origin_sv, raycast_ray_sv);
// --- GATHER INPUTS -------------------------------------------------------
const horizontal_input_vector_frac = blk: {
var ret = vm.Vector2
.init(self.x_input.getComponentFloat(), self.y_input.getComponentFloat())
.rotate(.initRotation(self.yaw_turns));
const len_squared = ret.lenSquared();
if (len_squared > 1) {
ret = ret.divScalar(@sqrt(len_squared));
}
break :blk math.asIntFrac2(ret);
};
const vertical_input_vector_frac = self.z_input.getComponentIntFrac();
// --- STATE TRANSITIONS ---------------------------------------------------
switch (self.movement_state) {
.ground => |ground| {
if (self.jump_input.isPressed()) {
self.movement_state = .{
.air = .{
.horizontal_velocity_sv = ground.horizontal_velocity_sv,
.vertical_velocity_sv = jump_velocity_sv,
},
};
}
},
else => {},
}
// --- STATE HANDLING ------------------------------------------------------
switch (self.movement_state) {
.ground => {},
.air => {},
.flight => {
const horizontal_velocity_sv = math.mulFrac2(horizontal_input_vector_frac, horizontal_speed_sv);
const vertical_velocity_sv = math.mulFrac(vertical_input_vector_frac, vertical_speed_sv);
var horizontal_displacement_sv = math.mulIntFloat2(horizontal_velocity_sv, dt);
var vertical_displacement_sv = math.mulIntFloat(vertical_velocity_sv, dt);
var position_sv = self.position_sv;
var min_sv = position_sv.add(.init(-collision_half_width_sv, -collision_half_width_sv, 0));
var max_sv = position_sv.add(.init(collision_half_width_sv, collision_half_width_sv, collision_height_sv));
if (vertical_displacement_sv > 0) {
if (game.chunks.sweepCastUp(min_sv, max_sv, vertical_displacement_sv)) |hit| {
vertical_displacement_sv -= hit.projected_distance_sv;
}
position_sv = .add(position_sv, .init(0, 0, vertical_displacement_sv));
min_sv = .add(min_sv, .init(0, 0, vertical_displacement_sv));
max_sv = .add(max_sv, .init(0, 0, vertical_displacement_sv));
}
var i: usize = 0;
while (i < 2) : (i += 1) {
if (game.chunks.sweepCastHorizontal(min_sv, max_sv, horizontal_displacement_sv)) |hit| {
const adjustment = math.mulFrac2(hit.normal_frac.dropZ(), hit.projected_distance_sv);
horizontal_displacement_sv = .add(
horizontal_displacement_sv,
adjustment,
);
} else {
break;
}
}
position_sv = .add(position_sv, horizontal_displacement_sv.withZ(0));
if (vertical_displacement_sv < 0.0) {
min_sv = .add(min_sv, horizontal_displacement_sv.withZ(0));
max_sv = .add(max_sv, horizontal_displacement_sv.withZ(0));
if (game.chunks.sweepCastDown(min_sv, max_sv, -vertical_displacement_sv)) |hit| {
vertical_displacement_sv += hit.projected_distance_sv;
}
position_sv = .add(position_sv, .init(0, 0, vertical_displacement_sv));
}
self.position_sv = position_sv;
},
}
// --- GUI -----------------------------------------------------------------
const extent = swapchain.extent;
const framebuffer_size = vm.Vector2.init(
@floatFromInt(extent.width),
@floatFromInt(extent.height),
);
// --- CROSSHAIR ---
{
const crosshair_half_extent_px = 8;
const crosshair_half_width_px = 1;
gui.pushBox(.{
.background_color = .init(1, 1, 1, 0.5),
.border_color = .init(0, 0, 0, 1),
.position_sspx = framebuffer_size.mulScalar(0.5).add(.init(-crosshair_half_extent_px, -crosshair_half_width_px)),
.size_px = .init(2.0 * crosshair_half_extent_px, 2.0 * crosshair_half_width_px),
.border_width_px = 0,
.border_radius_px = 0,
});
gui.pushBox(.{
.background_color = .init(1, 1, 1, 0.5),
.border_color = .init(0, 0, 0, 1),
.position_sspx = framebuffer_size.mulScalar(0.5).add(.init(-crosshair_half_width_px, -crosshair_half_extent_px)),
.size_px = .init(2.0 * crosshair_half_width_px, 2.0 * crosshair_half_extent_px),
.border_width_px = 0,
.border_radius_px = 0,
});
}
// --- COORDINATES ---
{
const margin: f16 = 8;
const font_size: f32 = 16;
// NOTE We have to do the number formatting semi-manually, because zig's
// std fmt is weird with signed numbers
var number_buf: [9]u8 = undefined; // sign ('-' or ' ') and 8 hex digits
gui.pushBox(.{
.background_color = .init(0, 0, 0, 0.75),
.border_color = .init(0, 0, 0, 1),
.position_sspx = .init(0, 0),
.size_px = .init(164, 4 * margin + 3 * font_size),
.border_width_px = 0,
.border_radius_px = 0,
});
gui.printLine("X {s}", .{formatHexCoord(self.position_sv.x, &number_buf)}, .{
.position_sspx = .init(margin, margin + font_size),
.font_id = @enumFromInt(0),
.cap_height_px = font_size,
.color = .init(1, 1, 1, 1),
});
gui.printLine("Y {s}", .{formatHexCoord(self.position_sv.y, &number_buf)}, .{
.position_sspx = .init(margin, 2 * (margin + font_size)),
.font_id = @enumFromInt(0),
.cap_height_px = font_size,
.color = .init(1, 1, 1, 1),
});
gui.printLine("Z {s}", .{formatHexCoord(self.position_sv.z, &number_buf)}, .{
.position_sspx = .init(margin, 3 * (margin + font_size)),
.font_id = @enumFromInt(0),
.cap_height_px = font_size,
.color = .init(1, 1, 1, 1),
});
}
// --- HOTBAR ---
{
const block_selection_outline_width_px: f32 = 4;
const block_image_size_px: f32 = 48;
const block_image_gap_px: f32 = 8;
const block_image_margin_px: f32 = 8;
const hotbar_width_px: f32 = @as(f32, @floatFromInt(blocks.len)) * block_image_size_px + @as(f32, @floatFromInt(blocks.len - 1)) * block_image_gap_px;
const hotbar_left_px: f32 = 0.5 * (framebuffer_size.x - hotbar_width_px);
const hotbar_top_px: f32 = framebuffer_size.y - block_image_margin_px - block_image_size_px;
gui.pushBox(.{
.background_color = .init(0, 0, 0, 0.75),
.border_color = .init(0, 0, 0, 1),
.position_sspx = .init(hotbar_left_px - block_image_margin_px, hotbar_top_px - block_image_margin_px),
.size_px = .init(hotbar_width_px + 2 * block_image_margin_px, block_image_size_px + 2 * block_image_margin_px),
.border_width_px = 4,
.border_radius_px = 0,
});
var cx: f32 = hotbar_left_px;
for (blocks, 0..) |filename, i| {
defer cx += block_image_size_px + block_image_gap_px;
const id = game.blocks.getOrLoad(filename) catch |err| {
std.log.err("Error while rendering hotbar block {s}: {}", .{ filename, err });
continue;
};
const definition = &game.blocks.array.items[@intFromEnum(id)];
const material_id = definition.walls.positive_z.material;
const material = &materials.array.items[@intFromEnum(material_id)];
if (i == self.block_index) {
gui.pushBox(.{
.background_color = .initScalar(0),
.border_color = .init(1, 1, 1, 1),
.position_sspx = vm.Vector2.init(cx, hotbar_top_px).sub(.initScalar(block_selection_outline_width_px)),
.size_px = .initScalar(block_image_size_px + 2 * block_selection_outline_width_px),
.border_width_px = block_selection_outline_width_px,
.border_radius_px = block_selection_outline_width_px,
});
}
const texture_id = material.base_color_texture;
gui.pushImage(.{
.position_sspx = .init(cx, hotbar_top_px),
.size_px = .initScalar(block_image_size_px),
.texture_id = texture_id,
});
}
}
}
fn formatHexCoord(coord: i32, buf: *[9]u8) []const u8 {
buf[0] = ' ';
const coord_abs = @abs(coord);
_ = std.fmt.bufPrint(buf[1..9], "{X:>8}", .{coord_abs}) catch unreachable;
if (coord < 0) {
const i = std.mem.findScalarLast(u8, buf, ' ') orelse unreachable;
buf[i] = '-';
}
return buf[0..];
}
fn resetAllButtons(self: *Player) void {
inline for (@typeInfo(Player).@"struct".fields) |field| {
if (field.type == Button) {
@field(self, field.name).reset();
}
}
}
inline fn sv(comptime vx: comptime_float) comptime_int {
return @intFromFloat(@round(vx * c.sv_per_vx));
}