diff --git a/src/entity.zig b/src/entity.zig new file mode 100644 index 00000000..e03fb85c --- /dev/null +++ b/src/entity.zig @@ -0,0 +1,136 @@ +const std = @import("std"); + +const JsonElement = @import("json.zig").JsonElement; +const renderer = @import("renderer.zig"); +const settings = @import("settings.zig"); +const utils = @import("utils.zig"); +const vec = @import("vec.zig"); +const Vec3d = vec.Vec3d; +const Vec3f = vec.Vec3f; + +pub const ClientEntity = struct { + interpolatedValues: utils.GenericInterpolation(6) = undefined, + + width: f64, + height: f64, +// TODO: +// public final EntityType type; + + pos: Vec3d = undefined, + rot: Vec3f = undefined, + + id: u32, + name: []const u8, + + pub fn init(self: *ClientEntity) void { + self.interpolatedValues.init(); + } + + pub fn getRenderPosition(self: *ClientEntity) Vec3d { + return Vec3d{.x = self.pos.x, .y = self.pos.y + self.height/2, .z = self.pos.z}; + } + + pub fn updatePosition(self: *ClientEntity, pos: [3]f64, vel: [3]f64, time: i16) void { + self.interpolatedValues.updatePosition(pos, vel, time); + } + + pub fn update(self: *ClientEntity, time: i16, lastTime: i16) void { + self.interpolatedValues.update(time, lastTime); + self.pos.x = self.interpolatedValues.outPos[0]; + self.pos.y = self.interpolatedValues.outPos[1]; + self.pos.z = self.interpolatedValues.outPos[2]; + self.rot.x = @floatCast(f32, self.interpolatedValues.outPos[3]); + self.rot.y = @floatCast(f32, self.interpolatedValues.outPos[4]); + self.rot.z = @floatCast(f32, self.interpolatedValues.outPos[5]); + } +}; + +pub const ClientEntityManager = struct { + var lastTime: i16 = 0; + var timeDifference: utils.TimeDifference = utils.TimeDifference{}; + pub var entities: std.ArrayList(ClientEntity) = undefined; + pub var mutex: std.Thread.Mutex = std.Thread.Mutex{}; + + pub fn init() void { + entities = std.ArrayList(ClientEntity).init(renderer.RenderStructure.allocator); // TODO: Use world allocator. + } + + pub fn deinit() void { + entities.deinit(); + } + + pub fn clear() void { + entities.clearRetainingCapacity(); + timeDifference = utils.TimeDifference{}; + } + + pub fn update() void { + mutex.lock(); + defer mutex.unlock(); + var time = @intCast(i16, std.time.milliTimestamp() & 65535); + time -%= timeDifference.difference; + for(entities.items) |*ent| { + ent.update(time, lastTime); + } + lastTime = time; + } + + pub fn addEntity(json: JsonElement) !void { + mutex.lock(); + defer mutex.unlock(); + var ent = try entities.addOne(); + ent.* = ClientEntity{ + .id = json.get(u32, "id", std.math.maxInt(u32)), + // TODO: +// CubyzRegistries.ENTITY_REGISTRY.getByID(json.getString("type", null)), + .width = json.get(f64, "width", 1), + .height = json.get(f64, "height", 1), + .name = json.get([]const u8, "name", 1), + }; + ent.init(); + } + + pub fn removeEntity(id: u32) void { + mutex.lock(); + defer mutex.unlock(); + for(entities.items) |*ent, i| { + if(ent.id == id) { + entities.swapRemove(i); + break; + } + } + } + + pub fn serverUpdate(time: i16, data: []const u8) !void { + mutex.lock(); + defer mutex.unlock(); + timeDifference.addDataPoint(time); + std.debug.assert(data.len%(4 + 24 + 12 + 24) == 0); + var remaining = data; + while(remaining.len != 0) { + const id = std.mem.readIntBig(u32, remaining[0..4]); + remaining = remaining[4..]; + const pos = [_]f64 { + @bitCast(f64, std.mem.readIntBig(u64, remaining[0..8])), + @bitCast(f64, std.mem.readIntBig(u64, remaining[8..16])), + @bitCast(f64, std.mem.readIntBig(u64, remaining[16..24])), + @floatCast(f64, @bitCast(f32, std.mem.readIntBig(u32, remaining[24..28]))), + @floatCast(f64, @bitCast(f32, std.mem.readIntBig(u32, remaining[28..32]))), + @floatCast(f64, @bitCast(f32, std.mem.readIntBig(u32, remaining[32..36]))), + }; + remaining = remaining[36..]; + const vel = [_]f64 { + @bitCast(f64, std.mem.readIntBig(u64, remaining[0..8])), + @bitCast(f64, std.mem.readIntBig(u64, remaining[8..16])), + @bitCast(f64, std.mem.readIntBig(u64, remaining[16..24])), + 0, 0, 0, + }; + remaining = remaining[24..]; + for(entities.items) |*ent| { + if(ent.id == id) { + ent.updatePosition(pos, vel, time); + } + } + } + } +}; \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index e6ed3abe..9a0eddd6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,6 +3,7 @@ const std = @import("std"); const assets = @import("assets.zig"); const blocks = @import("blocks.zig"); const chunk = @import("chunk.zig"); +const entity = @import("entity.zig"); const game = @import("game.zig"); const graphics = @import("graphics.zig"); const renderer = @import("renderer.zig"); @@ -245,6 +246,9 @@ pub fn main() !void { try renderer.init(); defer renderer.deinit(); + entity.ClientEntityManager.init(); + defer entity.ClientEntityManager.deinit(); + network.init(); try renderer.RenderStructure.init(); diff --git a/src/settings.zig b/src/settings.zig index 472f38f3..0ec7bf8a 100644 --- a/src/settings.zig +++ b/src/settings.zig @@ -3,6 +3,8 @@ pub const defaultPort: u16 = 47649; pub const connectionTimeout = 60000; +pub const entityLookback: i16 = 100; + pub const version = "0.12.0"; pub const highestLOD: u5 = 5; diff --git a/src/utils.zig b/src/utils.zig index 9d27d2f1..4eb189b2 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -334,4 +334,173 @@ pub const ThreadPool = struct { defer self.loadList.mutex.unlock(); return self.loadList.size; } +}; + +pub fn GenericInterpolation(comptime elements: comptime_int) type { + const frames: usize = 8; + return struct { + lastPos: [frames][elements]f64, + lastVel: [frames][elements]f64, + lastTimes: [frames]i16, + frontIndex: u32, + currentPoint: i32, + outPos: [elements]f64, + outVel: [elements]f64, + + pub fn initPosition(self: *@This(), initialPosition: *[elements]f64) void { + std.mem.copy(f64, &self.outPos, initialPosition); + std.mem.set([elements]f64, &self.lastPos, self.outPos); + std.mem.set(f64, &self.outVel, 0); + std.mem.set([elements]f64, &self.lastVel, self.outVel); + self.frontIndex = 0; + self.currentPoint = -1; + } + + pub fn init(self: *@This(), initialPosition: *[elements]f64, initialVelocity: *[elements]f64) void { + std.mem.copy(f64, &self.outPos, initialPosition); + std.mem.set([elements]f64, &self.lastPos, self.outPos); + std.mem.copy(f64, &self.outVel, initialVelocity); + std.mem.set([elements]f64, &self.lastVel, self.outVel); + self.frontIndex = 0; + self.currentPoint = -1; + } + + pub fn updatePosition(self: *@This(), pos: *[elements]f64, vel: *[elements]f64, time: i16) void { + self.frontIndex = (self.frontIndex + 1)%frames; + std.mem.copy(f64, &self.lastPos[self.frontIndex], pos); + std.mem.copy(f64, &self.lastVel[self.frontIndex], vel); + self.lastTimes[self.frontIndex] = time; + } + + fn evaluateSplineAt(_t: f64, tScale: f64, p0: f64, _m0: f64, p1: f64, _m1: f64) [2]f64 { + // https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Unit_interval_(0,_1) + const t = _t/tScale; + const m0 = _m0*tScale; + const m1 = _m1*tScale; + const t2 = t*t; + const t3 = t2*t; + const a0 = p0; + const a1 = m0; + const a2 = -3*p0 - 2*m0 + 3*p1 - m1; + const a3 = 2*p0 + m0 - 2*p1 + m1; + return [_]f64 { + a0 + a1*t + a2*t2 + a3*t3, // value + (a1 + 2*a2*t + 3*a3*t2)/tScale, // first derivative + }; + } + + fn interpolateCoordinate(self: *@This(), i: u32, t: f64, tScale: f64) void { + if(self.outVel[i] == 0 and self.lastVel[self.currentPoint][i] == 0) { + self.outPos += (self.lastPos[self.currentPoint][i] - self.outPos[i])*t/tScale; + } else { + // Use cubic interpolation to interpolate the velocity as well. + const newValue = evaluateSplineAt(t, tScale, self.outPos[i], self.outVel[i], self.lastPos[self.currentPoint][i], self.lastVel[self.currentPoint][i]); + self.outPos = newValue[0]; + self.outVel = newValue[1]; + } + } + + fn determineNextDataPoint(self: *@This(), time: i16, lastTime: *i16) void { + if(self.currentPoint != -1 and self.lastTimes[self.currentPoint] -% time <= 0) { + // Jump to the last used value and adjust the time to start at that point. + lastTime.* = self.lastTimes[self.currentPoint]; + std.mem.copy(f64, &self.outPos, &self.lastPos[self.currentPoint]); + std.mem.copy(f64, &self.outVel, &self.lastVel[self.currentPoint]); + self.currentPoint = -1; + } + + if(self.currentPoint == -1) { + // Need a new point: + var smallestTime: i16 = std.math.maxInt(i16); + var smallestIndex: i32 = -1; + for(self.lastTimes) |_, i| { + // ↓ Only using a future time value that is far enough away to prevent jumping. + if(self.lastTimes[i] -% time >= 50 and self.lastTimes[i] -% time < smallestTime) { + smallestTime = self.lastTimes[i] -% time; + smallestIndex = i; + } + } + self.currentPoint = smallestIndex; + } + } + + pub fn update(self: *@This(), time: i16, _lastTime: i16) void { + var lastTime = _lastTime; + self.determineNextDataPoint(time, &lastTime); + + var deltaTime = @intToFloat(f64, time -% lastTime)/1000; + if(deltaTime < 0) { + std.log.err("Experienced time travel. Current time: {} Last time: {}", .{time, lastTime}); + deltaTime = 0; + } + + if(self.currentPoint == -1) { + for(self.outPos) |*pos, i| { + // Just move on with the current velocity. + pos.* += self.outVel[i]*deltaTime; + // Add some drag to prevent moving far away on short connection loss. + self.outVel[i] *= std.math.pow(f64, 0.5, deltaTime); + } + } else { + const tScale = @intToFloat(f64, self.lastTimes[self.currentPoint] -% lastTime)/1000; + const t = deltaTime; + for(self.outPos) |_, i| { + self.interpolateCoordinate(i, t, tScale); + } + } + } + + pub fn updateIndexed(self: *@This(), time: i16, _lastTime: i16, indices: []u16, coordinatesPerIndex: comptime_int) void { + var lastTime = _lastTime; + self.determineNextDataPoint(time, &lastTime); + + var deltaTime = @intToFloat(f64, time -% lastTime)/1000; + if(deltaTime < 0) { + std.log.err("Experienced time travel. Current time: {} Last time: {}", .{time, lastTime}); + deltaTime = 0; + } + + if(self.currentPoint == -1) { + for(indices) |i| { + const index = i*coordinatesPerIndex; + var j: u32 = 0; + while(j < coordinatesPerIndex): (j += 1) { + // Just move on with the current velocity. + self.outPos[index + j] += self.outVel[index + j]*deltaTime; + // Add some drag to prevent moving far away on short connection loss. + self.outVel[index + j] *= std.math.pow(f64, 0.5, deltaTime); + } + } + } else { + const tScale = @intToFloat(f64, self.lastTimes[self.currentPoint] -% lastTime)/1000; + const t = deltaTime; + for(indices) |i| { + const index = i*coordinatesPerIndex; + var j: u32 = 0; + while(j < coordinatesPerIndex): (j += 1) { + self.interpolateCoordinate(index + j, t, tScale); + } + } + } + } + }; +} + +pub const TimeDifference = struct { + difference: i16 = 0, + firstValue: bool = true, + + pub fn addDataPoint(self: *TimeDifference, time: i16) void { + const currentTime = @intCast(i16, std.time.milliTimestamp() & 65535); + const timeDifference = currentTime -% time; + if(self.firstValue) { + self.difference = timeDifference; + self.firstValue = false; + } + if(timeDifference -% self.difference > 0) { + self.difference +%= 1; + } else if(timeDifference -% self.difference < 0) { + self.difference -%= 1; + } + } }; \ No newline at end of file