-
-
Save MrChickenRocket/19d21c1786503b3c8a7b685368db64de to your computer and use it in GitHub Desktop.
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() |
Please make this a git repo or a wally package.
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!)
Oh! That's really clever, I'll make sure to fix that.
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.
You can check out a place file with this here:
https://www.roblox.com/games/11817429464/KinematicModuleV2