From 5987f9a44f1ad788fa4ef629990968f019732aed Mon Sep 17 00:00:00 2001 From: duck Date: Mon, 16 Jun 2025 01:49:58 +0500 Subject: [PATCH] Time travel! --- src/debug_scene.zig | 195 +++++++++++++++++++++++++------------ src/game.zig | 6 +- src/graph.zig | 2 +- src/graphics/transform.zig | 3 + src/math.zig | 22 ++++- src/time.zig | 27 ++++- 6 files changed, 185 insertions(+), 70 deletions(-) diff --git a/src/debug_scene.zig b/src/debug_scene.zig index db1b7d2..af10455 100644 --- a/src/debug_scene.zig +++ b/src/debug_scene.zig @@ -56,41 +56,59 @@ const PLANE_MESH_DATA = [_]f32{ pub const WorldTime = struct { time: Time, - delta: f32, + last_time: Time, view_unresolved: f32, view_timescale: f32, }; +pub const PlayerState = union(enum) { + idle, + moving: struct { + from_position: @Vector(3, f32), + from_time: Time, + to_time: Time, + }, +}; + pub const Player = struct { mesh: Graphics.Mesh, - transform: Graphics.Transform, - velocity: @Vector(2, f32), + transform: Transform, + position: @Vector(2, i32), + state: PlayerState, + + move_units: f32, + idle_timescale: f32, + moving_timescale: f32, }; pub const Environment = struct { mesh: Graphics.Mesh, + transform: Transform, }; pub fn init(controller: *Controller, graphics: *Graphics) !void { controller.addResource(Player{ .mesh = try graphics.loadMesh(@ptrCast(&CUBE_MESH_DATA)), - .transform = Graphics.Transform{ - .position = .{ 0, 0, 1 }, - }, - .velocity = .{ 0, 0 }, + .transform = .{}, + .position = .{ 0, 0 }, + .state = .idle, + .move_units = 0.125, + .moving_timescale = 1.0, + .idle_timescale = 0.0625, }); controller.addResource(Environment{ .mesh = try graphics.loadMesh(@ptrCast(&PLANE_MESH_DATA)), + .transform = .{ + .position = .{ 0, 0, -1 }, + .scale = @splat(5), + }, }); controller.addResource(WorldTime{ - .time = .{ .clock = 0 }, - .delta = 0.0, + .time = Time.ZERO, + .last_time = Time.ZERO, .view_unresolved = 0.0, - .view_timescale = 1.0, + .view_timescale = 0.0625, }); - graphics.camera.transform = .{ - .position = .{ 0, 0, 10 }, - }; } pub fn deinit() void {} @@ -101,89 +119,133 @@ pub fn updateReal( controller: *Controller, ) void { world_time.view_unresolved += real_time.delta; - controller.queue(updateWorld); + controller.queue(.{ + updateWorld, + updatePlayerTransform, + updateCamera, + Controller.Option.ordered, + }); } pub fn updateWorld( world_time: *WorldTime, controller: *Controller, + player: *Player, ) void { - if (world_time.view_unresolved <= 0.000001) { - return; + if (world_time.view_unresolved <= 0) return; + + var real_delta = world_time.view_unresolved; + var world_delta = Time.durationFromUnits(real_delta * world_time.view_timescale); + + switch (player.state) { + .moving => |move| { + if (move.to_time.clock > world_time.time.clock) { + world_delta = @min(world_delta, move.to_time.clock - world_time.time.clock); + real_delta = Time.unitsFromDuration(world_delta) / world_time.view_timescale; + } + }, + .idle => {}, } - // This can later be clamped to schedule several updates per frame - const real_delta = world_time.view_unresolved; - const world_delta = real_delta * world_time.view_timescale; - world_time.time.tick(real_delta); + if (world_delta == 0) return; + + world_time.last_time = world_time.time; + + world_time.time.clock += world_delta; world_time.view_unresolved -= real_delta; - world_time.delta = world_delta; - controller.queue(update); + controller.queue(.{ + updatePlayer, + updateWorld, + Controller.Option.ordered, + }); } -pub fn update( +pub fn updatePlayer( player: *Player, - // mouse: *Game.Mouse, keyboard: *Game.Keyboard, - graphics: *Graphics, - real_time: *Game.Time, world_time: *WorldTime, ) void { - const MAX_VELOCITY = 12.0; - const TIME_TO_REACH_MAX_VELOCITY = 1.0 / 8.0; + switch (player.state) { + .idle => {}, + .moving => |move| { + if (world_time.time.past(move.to_time)) { + player.state = .idle; + world_time.view_timescale = player.idle_timescale; + } else return; + }, + } - var velocity_target: @Vector(2, f32) = .{ 0, 0 }; + var delta: @Vector(2, i32) = .{ 0, 0 }; if (keyboard.keys.is_pressed(sdl.SCANCODE_W)) { - velocity_target[1] += MAX_VELOCITY; + delta[1] += 1; } if (keyboard.keys.is_pressed(sdl.SCANCODE_S)) { - velocity_target[1] -= MAX_VELOCITY; + delta[1] -= 1; } if (keyboard.keys.is_pressed(sdl.SCANCODE_D)) { - velocity_target[0] += MAX_VELOCITY; + delta[0] += 1; } if (keyboard.keys.is_pressed(sdl.SCANCODE_A)) { - velocity_target[0] -= MAX_VELOCITY; + delta[0] -= 1; } - velocity_target = math.limitLength(velocity_target, MAX_VELOCITY); - player.velocity = math.stepVector(player.velocity, velocity_target, MAX_VELOCITY / TIME_TO_REACH_MAX_VELOCITY * world_time.delta); - player.transform.position[0] += player.velocity[0] * world_time.delta; - player.transform.position[1] += player.velocity[1] * world_time.delta; - - const target_position = player.transform.position + - @Vector(3, f32){ player.velocity[0], player.velocity[1], 0 } * - @as(@Vector(3, f32), @splat(1.0 / MAX_VELOCITY)) * - @Vector(3, f32){ 0.0, 0.0, 0.0 }; + if (delta[0] != 0 or delta[1] != 0) { + updatePlayerTransform(player, world_time); + player.state = .{ .moving = .{ + .from_position = player.transform.position, + .from_time = world_time.time, + .to_time = world_time.time.offset(player.move_units * math.lengthInt(delta)), + } }; + player.position[0] += delta[0]; + player.position[1] += delta[1]; + world_time.view_timescale = player.moving_timescale; + } +} +pub fn updateCamera( + graphics: *Graphics, + player: *Player, + real_time: *Game.Time, +) void { graphics.camera.transform.position = math.lerpTimeLn( graphics.camera.transform.position, - target_position + @Vector(3, f32){ 0.0, -2.0, 5.0 }, + player.transform.position + @Vector(3, f32){ 0.0, -2.0, 5.0 }, real_time.delta, -25, ); - { // Rotate camera toward player + const ORIGIN_DIR = @Vector(3, f32){ 0.0, 0.0, -1.0 }; + const INIT_ROTATION = Transform.rotationByAxis(.{ 1.0, 0.0, 0.0 }, std.math.pi * 0.5); - const ORIGIN_DIR = @Vector(3, f32){ 0.0, 0.0, -1.0 }; - const INIT_ROTATION = Transform.rotationByAxis(.{ 1.0, 0.0, 0.0 }, std.math.pi * 0.5); + const ROTATED_DIR = Transform.rotateVector(ORIGIN_DIR, INIT_ROTATION); - const ROTATED_DIR = Transform.rotateVector(ORIGIN_DIR, INIT_ROTATION); + const target_rotation = Transform.combineRotations( + INIT_ROTATION, + Transform.rotationToward( + ROTATED_DIR, + player.transform.position - graphics.camera.transform.position, + .{ .normalize_to = true }, + ), + ); + graphics.camera.transform.rotation = Transform.normalizeRotation(math.slerpTimeLn( + graphics.camera.transform.rotation, + target_rotation, + real_time.delta, + -2, + )); +} - const target_rotation = Transform.combineRotations( - INIT_ROTATION, - Transform.rotationToward( - ROTATED_DIR, - target_position - graphics.camera.transform.position, - .{ .normalize_to = true }, - ), - ); - graphics.camera.transform.rotation = Transform.normalizeRotation(math.slerpTimeLn( - graphics.camera.transform.rotation, - target_rotation, - real_time.delta, - -2, - )); +pub fn updatePlayerTransform(player: *Player, world_time: *WorldTime) void { + switch (player.state) { + .idle => player.transform.position = .{ @floatFromInt(player.position[0]), @floatFromInt(player.position[1]), 0.0 }, + .moving => |move| { + const to_position = @Vector(3, f32){ @floatFromInt(player.position[0]), @floatFromInt(player.position[1]), 0.0 }; + player.transform.position = math.lerp( + move.from_position, + to_position, + world_time.time.progress(move.from_time, move.to_time), + ); + }, } } @@ -191,9 +253,16 @@ pub fn draw( player: *Player, env: *Environment, graphics: *Graphics, + world_time: *WorldTime, ) !void { - try graphics.drawMesh(env.mesh, Graphics.Transform{ - .scale = .{ 10, 10, 10 }, + env.transform.rotation = Transform.combineRotations( + env.transform.rotation, + Transform.rotationByAxis(.{ 0, 0, 1 }, world_time.time.unitsSince(world_time.last_time) * std.math.pi), + ); + try graphics.drawMesh(env.mesh, env.transform); + try graphics.drawMesh(env.mesh, Transform{ + .position = .{ 0, 0, -0.5 }, + .scale = @splat(5), }); try graphics.drawMesh(player.mesh, player.transform); } diff --git a/src/game.zig b/src/game.zig index 31ca101..36f1517 100644 --- a/src/game.zig +++ b/src/game.zig @@ -68,9 +68,11 @@ pub fn run(self: *Self) GameError!void { var current_time: sdl.Time = undefined; if (sdl.GetCurrentTime(¤t_time)) { const time = self.graph.getResource(Time).?; - time.delta = @as(f32, @floatFromInt(current_time - time.now)) * 0.000000001; + if (time.now != 0) { + time.delta = @as(f32, @floatFromInt(current_time - time.now)) * 0.000000001; + } time.now = current_time; - } + } else return error.SdlError; var controller = try self.graph.getController(); controller.queue(.{ diff --git a/src/graph.zig b/src/graph.zig index 31cd387..d3624cf 100644 --- a/src/graph.zig +++ b/src/graph.zig @@ -15,7 +15,7 @@ pub const Controller = @import("graph/controller.zig"); const MAX_SYSTEM_REQUESTS = 8; const DEFAULT_SYSTEM_CAPACITY = 16; const DEFAULT_CONTROLLERS = 2; -const DEFAULT_DUDS_PER_CONTROLLER = 4; +const DEFAULT_DUDS_PER_CONTROLLER = 32; const ResourceMap = std.AutoArrayHashMapUnmanaged(utils.Hash, Resource); const SystemQueue = std.ArrayListUnmanaged(System); diff --git a/src/graphics/transform.zig b/src/graphics/transform.zig index a314711..0a86198 100644 --- a/src/graphics/transform.zig +++ b/src/graphics/transform.zig @@ -181,6 +181,9 @@ pub fn extractNormal(vector: Position) struct { Position, f32 } { @setFloatMode(.optimized); const length = @sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]); + if (length < 1e-15) { + return .{ @Vector(3, f32){ 0, 0, -1 }, 1 }; + } return .{ vector / @as(Position, @splat(length)), length }; } diff --git a/src/math.zig b/src/math.zig index 7b9928a..e9e0fd5 100644 --- a/src/math.zig +++ b/src/math.zig @@ -10,8 +10,14 @@ pub inline fn lerpTime(a: anytype, b: anytype, t: f32, comptime f: f32) @TypeOf( pub fn lerpTimeLn(a: anytype, b: anytype, t: f32, lnf: f32) @TypeOf(a, b) { @setFloatMode(.optimized); - const a_factor = @exp(lnf * t); - const b_factor = 1.0 - a_factor; + return lerp(b, a, @exp(lnf * t)); +} + +pub fn lerp(a: anytype, b: anytype, f: f32) @TypeOf(a, b) { + @setFloatMode(.optimized); + + const a_factor = 1.0 - f; + const b_factor = f; switch (@typeInfo(@TypeOf(a, b))) { .float => return a_factor * a + b_factor * b, @@ -88,3 +94,15 @@ pub fn dot(a: anytype, b: anytype) f32 { return @reduce(.Add, a * b); } + +pub fn lengthInt(vector: anytype) f32 { + @setFloatMode(.optimized); + + return @sqrt(@as(f32, @floatFromInt(dotInt(vector, vector)))); +} + +pub fn dotInt(a: anytype, b: anytype) (@typeInfo(@TypeOf(a)).vector.child) { + @setFloatMode(.optimized); + + return @reduce(.Add, a * b); +} diff --git a/src/time.zig b/src/time.zig index 67c22cc..baa4a7d 100644 --- a/src/time.zig +++ b/src/time.zig @@ -1,20 +1,43 @@ const TimeType = u64; const TIME_UNIT: TimeType = 1 << 32; const TIME_MULT = 1.0 / @as(f32, @floatFromInt(TIME_UNIT)); +const Time = @This(); + +pub const ZERO = Time{ .clock = 0 }; clock: TimeType, -const Time = @This(); - pub fn tick(self: *Time, units: f32) void { self.clock += durationFromUnits(units); } +pub fn past(self: *Time, goal: Time) bool { + return self.clock >= goal.clock; +} + +pub fn offset(self: Time, units: f32) Time { + return Time{ + .clock = self.clock + durationFromUnits(units), + }; +} + pub fn unitsSince(self: *Time, from: Time) f32 { if (from.clock > self.clock) return 0; return @as(f32, @floatFromInt(self.clock - from.clock)) * TIME_MULT; } +pub fn progress(self: *Time, from: Time, to: Time) f32 { + if (from.clock > to.clock) return 1.0; + if (self.clock > to.clock) return 1.0; + + const duration = to.clock - from.clock; + return @as(f32, @floatFromInt(self.clock - from.clock)) / @as(f32, @floatFromInt(duration)); +} + +pub fn unitsFromDuration(duration: TimeType) f32 { + return @as(f32, @floatFromInt(duration)) * TIME_MULT; +} + pub fn durationFromUnits(units: f32) TimeType { return @intFromFloat(@as(f32, @floatFromInt(TIME_UNIT)) * units); }