Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save MrChickenRocket/19d21c1786503b3c8a7b685368db64de to your computer and use it in GitHub Desktop.
Save MrChickenRocket/19d21c1786503b3c8a7b685368db64de to your computer and use it in GitHub Desktop.
Kinematic animated objects for roblox. Tag anchored server objects with "Kinematic" and the motion and physics code is magic'd away.
if (script:IsDescendantOf(game.ReplicatedFirst) == false) then
error(script.Name .. "needs to be in ReplicatedFirst")
end
local CollectionService = game:GetService("CollectionService")
local kinematicObjects = {}
local function AddInstance(target)
local instance = nil
local model = nil
if (target:IsA("Model")) then
instance = target.PrimaryPart
model = target
else
instance = target
model = nil
end
local record = {}
record.instance = instance
record.model = model
record.target = target
--grab all the parts that need their velocity set
record.parts = {}
for key,value in record.target:GetDescendants() do
if (value:IsA("BasePart") and value.Anchored == true) then
table.insert(record.parts, value)
end
end
record.currentCFrame = instance.CFrame
record.targetCFrame = instance.CFrame
record.lastCFrame = instance.CFrame
kinematicObjects[target] = record
end
local function RemoveInstance(instance)
local record = kinematicObjects[instance]
if (record == nil) then
return
end
kinematicObjects[instance] = nil
if (record) then
record.target:Destroy()
end
end
local remoteEvent = game.ReplicatedStorage:WaitForChild("KinematicData")
remoteEvent.OnClientEvent:Connect(function(data)
--We got a remote event
if (data.id == "add") then
data.instance.Parent = data.parent
AddInstance(data.instance)
elseif (data.id == "move") then
for key,rec in data.data do
local instance = rec.i
if (instance) then
local kinematicRecord = kinematicObjects[instance]
if (rec.p) then
local posx, posy, posz = string.unpack("fff", rec.p)
kinematicRecord.pos = CFrame.new(Vector3.new(posx, posy, posz))
end
if (rec.a) then
local axisx, axisy, axisz, angle = string.unpack("ffff", rec.a)
kinematicRecord.rot = CFrame.fromAxisAngle(Vector3.new(axisx, axisy, axisz), angle)
end
end
end
elseif (data.id == "del") then
local instance = data.i
RemoveInstance(instance)
end
end)
function SmoothLerp(variableA, variableB, fraction, deltaTime)
local f = 1.0 - math.pow(1.0 - fraction, deltaTime)
if (type(variableA) == "number") then
return ((1-f) * variableA) + (variableB * f)
end
return variableA:Lerp(variableB, f)
end
local function Stepped(deltaTime)
local smoothFactor = script:GetAttribute("smoothfactor") or 0.99
for instance,record in kinematicObjects do
if (record.pos == nil or record.rot == nil) then
return
end
record.targetCFrame = record.pos * record.rot
record.currentCFrame = SmoothLerp(record.currentCFrame, record.targetCFrame, smoothFactor, deltaTime)
if (record.model) then
record.model:PivotTo(record.currentCFrame)
else
record.instance.CFrame = record.currentCFrame
end
local posDelta = record.currentCFrame.Position - record.lastCFrame.Position
local rotDelta = record.currentCFrame.Rotation * record.lastCFrame.Rotation:Inverse()
local x,y,z = rotDelta:ToEulerAngles()
local angleDelta = Vector3.new(x,y,z)
record.lastCFrame = record.currentCFrame
if (record.model) then
for key,part in record.parts do
part.AssemblyLinearVelocity = posDelta / deltaTime
part.AssemblyAngularVelocity = angleDelta / deltaTime
end
else
record.instance.AssemblyLinearVelocity = posDelta / deltaTime
record.instance.AssemblyAngularVelocity = angleDelta / deltaTime
end
end
end
local function Setup()
game["Run Service"].PreSimulation:Connect(Stepped)
end
Setup()
-- Kinematic Objects script, courtesty of MCR Christmas 2022
--
-- This tool is designed to greatly simplify coding certain physics interactions in roblox such as elevators and other moving platforms
-- To use: Tag a server instance (part or model) with "Kinematic" and then just cframe it with whatever motion you want each frame
-- the scripts will take care of replication, smooth motion on the client, as well as providing accurate velocity and angular velocity data
-- Each kinematic instance uses 0 bandwidth when idle, and up to ~1kb/s when moving
--
-- Limitaions:
--
-- Changes in appearance to the server kinematic parts are not replicated (it's a clone!)
-- Kinematically animated objects do not wake up sleeping physics objects
-- There is no facility to teleport a kinematic part - they can only interpolate
-- The client is currently using smoothing to smooth towards the current position, versus true interpolation.
--
-- Possible expansions:
--
-- Because the list of parts is caluclated per player, this could easily be updated to be used for instancing objects just for specific players
-- or only updating the position/velocity of players if they are within range
--
-- It should be possible to flag parts of a server rig/model to be only simulated locally on a client. eg: floppy tails
--
--
-- Technical notes:
-- Okay, so this has ballooned into a big bag of black magic roblox tricks :)
--
-- 1) Server parts are moved under a "camera" called DoNotReplicate, which prevents them from replicating while still having collision on the server
-- Note: tTis trick only works for anchored parts, if the part is simulated this does not work
--
-- 2) Server parts are cloned, and sent to each client individually via their playerGui folder (And a screenGui with ResetOnSpawn = false!)
-- Note: An event is sent after the cloning, which because of replication order will not "fire" until the replication is complete
--
-- 3) The velocity and angular velocity of parts are calculated per frame based on their previous cframes (server kinematics)
--
-- 4) Updates to position and angle are packed using string.pack and do not get sent if there are no changes
-- Note 1: Parts are identidfied by putting a reference to the instance directly in the table sent over the remote event.
-- Roblox is able to resolve this on the client if the instance is still present.
-- Note 2: The replication rate is at 20hz, this can be changed to massively increase the number of parts
--
if (script:IsDescendantOf(game.ServerScriptService) == false) then
error(script.Name .. "needs to be in ServerScriptService")
end
local CollectionService = game:GetService("CollectionService")
local doNotReplicate = Instance.new("Camera")
doNotReplicate.Name = "DoNotReplicate"
doNotReplicate.Parent = game.Workspace
local remoteEvent = Instance.new("RemoteEvent")
remoteEvent.Name = "KinematicData"
remoteEvent.Parent = game.ReplicatedStorage
local playerRecords = {}
local kinematicObjects = {}
local timeOfNextUpdate = 0
local serverHz = 20
local function SendInstanceToPlayer(playerRecord, kinematicRecord)
if (playerRecord.replicatedInstances[kinematicRecord.target] ~= nil) then
return
end
--print("Sending instance to player ", kinematicRecord)
local clone = kinematicRecord.cloneModel:Clone()
clone.Name = clone.Name .. "_replicated"
local record = {}
record.sourceRecord = kinematicRecord
record.clone = clone
playerRecord.replicatedInstances[kinematicRecord.target] = record
clone.Parent = playerRecord.kinematicGui
remoteEvent:FireClient(playerRecord.player, {id = "add", instance = clone, parent = kinematicRecord.parent} )
end
local function BuildPacketForPlayer(playerRecord)
local epsilon = 0.001
local data = {}
local send = false
for key,record in playerRecord.replicatedInstances do
local write = false
local rec = {}
rec.i = record.clone
local p = record.sourceRecord.instance.CFrame.Position
local axis, angle = record.sourceRecord.instance.CFrame:ToAxisAngle()
if (record.p == nil or record.p:FuzzyEq(p,epsilon) == false) then
rec.p = string.pack("fff", p.x, p.y, p.z)
write = true
end
record.p = p
if (record.a == nil or record.axis:FuzzyEq(axis,epsilon) == false or math.abs(record.angle - angle) > epsilon) then
rec.a = string.pack("ffff", axis.x, axis.y, axis.z, angle)
write = true
end
record.angle = angle
record.axis = axis
if (write == true) then
table.insert(data, rec)
send = true
end
end
if (send == true) then
remoteEvent:FireClient(playerRecord.player, { id = "move", data = data })
end
end
local function RemoveInstance(instance)
local record = kinematicObjects[instance]
if (record == nil) then
return
end
for _,playerRecord in playerRecords do
local rec = playerRecord.replicatedInstances[instance]
if (rec) then
remoteEvent:FireClient(playerRecord.player, { id = "del", i = rec.clone })
playerRecord.replicatedInstances[instance] = nil
end
end
record.cloneModel:Destroy()
kinematicObjects[instance] = nil
end
local function ClearTags(instance)
CollectionService:RemoveTag(instance, "Kinematic")
for key,value in instance:GetDescendants() do
CollectionService:RemoveTag(value, "Kinematic")
--Remove any scripts
--if (value:IsA("Script") and value.RunContext==Enum.RunContext.Server or value.RunContext == Enum.RunContext.Legacy) then
if (value:IsA("Script")) then
value:Destroy()
end
end
end
local function AddInstance(target)
if (target:IsA("BasePart") == false and target:IsA("Model") == false) then
warn("Kinematic tags must be applied to baseparts and models only:", target)
return
end
local instance = nil
local model = nil
if (target:IsA("Model")) then
if (target.PrimaryPart == nil) then
warn("Kinematic - No primarypart for ", target)
return
end
instance = target.PrimaryPart
model = target
else
instance = target
model = nil
end
--Store data
local record = {}
record.target = target
record.parent = target.Parent
record.instance = instance
record.model = model
record.lastCFrame = instance.CFrame
--Make a copy for cloning (we might remove clientOnly parts from the original)
record.cloneModel = target:Clone()
ClearTags(record.cloneModel)
kinematicObjects[target] = record
--set initial instance values
instance.Anchored = true
instance.AssemblyAngularVelocity = Vector3.zero
instance.AssemblyLinearVelocity = Vector3.zero
--pull it out of the world
target.Parent = doNotReplicate
--Capture the destroy
target.Destroying:Connect(function()
RemoveInstance(target)
end)
--Replicate the waldo
for key,playerRecord in playerRecords do
SendInstanceToPlayer(playerRecord, record)
end
end
local function Stepped(deltaTime)
local list = CollectionService:GetTagged("Kinematic")
for _,target in list do
if (kinematicObjects[target] == nil) then
AddInstance(target)
end
end
for instance,record in kinematicObjects do
local currentCFrame = record.instance.CFrame
local posDelta = currentCFrame.Position - record.lastCFrame.Position
local rotDelta = currentCFrame.Rotation * record.lastCFrame.Rotation:Inverse()
local x,y,z = rotDelta:ToEulerAngles()
local angleDelta = Vector3.new(x,y,z)
record.lastCFrame = currentCFrame
record.instance.AssemblyLinearVelocity = posDelta / deltaTime
record.instance.AssemblyAngularVelocity = angleDelta / deltaTime
end
timeOfNextUpdate+=deltaTime
if (timeOfNextUpdate > 1/serverHz) then
timeOfNextUpdate = math.fmod(timeOfNextUpdate, 1/serverHz)
for key,playerRecord in playerRecords do
BuildPacketForPlayer(playerRecord)
end
end
end
local function Setup()
game.Players.PlayerAdded:Connect(function(player)
local playerRecord = {}
playerRecord.replicatedInstances = {}
playerRecord.player = player
playerRecords[player.UserId] = playerRecord
--Create a place to put their stuff
local instance = Instance.new("ScreenGui")
instance.ResetOnSpawn = false
instance.Name = "Kinematics"
instance.Parent = playerRecord.player.PlayerGui
playerRecord.kinematicGui = instance
for key,kinematicRecord in kinematicObjects do
SendInstanceToPlayer(playerRecord, kinematicRecord)
end
end)
game.Players.PlayerRemoving:Connect(function(player)
playerRecords[player.UserId] = nil
end)
game["Run Service"].PreSimulation:Connect(Stepped)
end
Setup()
@Gargafield
Copy link

Please make this a git repo or a wally package.

@coasterteam
Copy link

coasterteam commented Dec 19, 2022

Very awesome project, using this in another project to convert the painful physics elevators to this hybrid of client physics + server ownership.

One issue that has sprung up is the fact that on respawn, the replicated instances disappear.

I have worked up a somewhat hacky solution to it, by parenting the replicated instances to a ScreenGui in a PlayerGui that has it's "ResetOnSpawn" property set to false, the instance isn't "deleted" by the server. (It's working after respawn, which is good!)

@MrChickenRocket
Copy link
Author

Oh! That's really clever, I'll make sure to fix that.

@MrChickenRocket
Copy link
Author

Very awesome project, using this in another project to convert the painful physics elevators to this hybrid of client physics + server ownership.

One issue that has sprung up is the fact that on respawn, the replicated instances disappear.

I have worked up a somewhat hacky solution to it, by parenting the replicated instances to a ScreenGui in a PlayerGui that has it's "ResetOnSpawn" property set to false, the instance isn't "deleted" by the server. (It's working after respawn, which is good!)

Updated to do the same thing. Cheers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment