i managed to make a client-side predictive system for kinematics of vehicles
although it’s not for cars, it’s for aircraft, conceptually they should be the same, if anything, aircraft demand higher precision since they fly faster than any car on land
how it works:
the cosmetic plane model that exists in the workspace on clients does not exist on the server
each client controls their own plane, and sends updates to the server regarding the plane’s kinematics (CFrame, linear velocity, linear acceleration, and angular velocity) to the server through an unreliable remote event, 15 times a second
the server receives those values and store them. since this information is inaccurate as considerable time has elapsed since it was sent, so if the server wants to know a more precise approximation of the aircraft’s CFrame, it will take those outdated values along with the elapsed time to calculate (approximate) the current CFrame using basic Calculus
dPos = 1/2 * acceleration * dt^2 + velocity * dt
dRot = angularvelocity * dt
(i did not consider angular acceleration because its usually small enough, whereas linear acceleration can be huge on aircraft, for cars it usually isn’t)
note that dt
is not framerate, it is the elapsed time since the kinematic information was last updated from the client
now to replicate the kinematic movement of the aircraft onto other clients, not just the client piloting it, the server sends all the stored kinematic values across all other clients, also at 15 Hz (the rate is arbitrary, you can increase or lower it depending on the strain on the network, although i wouldn’t recommend exceeding 20 Hz)
the client uses the same method to approximate the cframe of other players’ aircraft, the only difference this time is that the elapsed time dt
may be much larger (~2x larger), but that’s just the nature of multiplayer games
and to achieve the illusion of smooth movement, the client replicating the plane’s movement assigns the approximated CFrame value to the plane’s physical Model instance every frame in a RunService
connection.
local Kinematics = {}
Kinematics.__index = Kinematics
Kinematics.new = function(cframe: CFrame, vel: Vector3?, localAngVel: Vector3?, acc: Vector3?)
return setmetatable({
CFrame = cframe,
LinearVelocity = vel or Vector3.zero,
LinearAcceleration = acc or Vector3.zero,
AngularVelocity = localAngVel or Vector3.zero, --LOCAL, NOT WORLDSPACE!!!
AtTime = Timer.GetTime(), --Timer is a module that syncs time between server and client
Instance = nil:: typeof(script.ValuesTemplate)?,
}, Kinematics)
end
function Kinematics:GetCFrameApprox(actualCf: CFrame?, atTime: number?, linearLerp: number?, rotationalLerp: number?): CFrame & number
local self: KinematicInfo = self
local atTime = atTime or Timer.GetTime() --time of evaluation
local dt = atTime - self.AtTime
--print(dt)
local finalPos do
local dPos = self.LinearVelocity * dt + .5 * self.LinearAcceleration * dt^2
local shouldBePos = self.CFrame.Position + dPos
if actualCf then
--a * t + b * (1 - t)
--actualCf is just the cosmetic model's CFrame, just smoothing out movement
local distToActual = shouldBePos - actualCf.Position
shouldBePos = actualCf.Position + distToActual * (linearLerp or 0.1)
end
finalPos = shouldBePos
end
local finalRot do
--local rx, ry, rz = self.CFrame:ToEulerAnglesYXZ()
local dRot = self.AngularVelocity * dt
dRot = CFrame.fromEulerAnglesYXZ(dRot.X, dRot.Y, dRot.Z)
finalRot = self.CFrame * dRot
if actualCf then
finalRot = actualCf:Lerp(finalRot, rotationalLerp or .1).Rotation
end
end
return CFrame.new(finalPos) * finalRot.Rotation, dt
end
snipet from the client:
self._update = RunService.Heartbeat:Connect(function(deltaTime)
local actualCf = self.Aircraft.Model.PrimaryPart.CFrame
local approx_cf = self.KinematicInfo:GetCFrameApprox(actualCf)
self.Aircraft.Model.PrimaryPart.CFrame = approx_cf
self.Aircraft.Model.PrimaryPart.AssemblyLinearVelocity = self.KinematicInfo.LinearVelocity
self.Aircraft.Model.PrimaryPart.AssemblyAngularVelocity = self.KinematicInfo.CFrame:VectorToWorldSpace(self.KinematicInfo.AngularVelocity)
end)
basically this is ditching roblox’s default replication system and making your own
it isn’t perfect, it’s a prediction after all, but imo it works pretty well; as you can see the two clients seems to agree where and when collisions occur, even though the two planes are travelling at hundreds of studs per second