hello, i’m making a stand system. The stand is supposed to follow my character and i’m using the spring force formula so it isn’t completely linear. It mostly works but for some reason it starts stuttering after a while for any framerate above 120. Here’s the snipped of code that handles that.
local FOLLOW_THRESHOLD = 2
local STIFFNESS = 80
local DAMPING = 18
local MAX_SPEED = 27
local velocity = Vector3.zero
RunService.Heartbeat:Connect(function(dt)
local standCF = Stand:GetPivot()
local charCF = Character:GetPivot()
local target = (charCF * CFrame.new(3, -1, 2)).Position
local rotation = charCF - charCF.Position
local toTarget = target - standCF.Position
local distance = toTarget.Magnitude
if distance > FOLLOW_THRESHOLD or velocity.Magnitude > 0.01 then
local springForce = toTarget * STIFFNESS - velocity * DAMPING
velocity = velocity + springForce * dt
local speed = velocity.Magnitude
if speed > MAX_SPEED then
velocity = velocity * (MAX_SPEED / speed)
end
end
if velocity.Magnitude < 0.05 then
velocity = Vector3.zero
end
local newPos = standCF.Position + velocity * dt * 8
Stand:PivotTo(CFrame.new(newPos) * rotation)
end)
I’ve tried tweaking every variable, scaling them, multiplying just dt with them but it didn’t fix anything
it isn’t a speed issue because it was fluctuating between 15 and 16. Just to be sure i even capped it at 10 but it didn’t change anything.
the issue is the * 8 in your position update - you’re applying dt twice which breaks framerate independence above 120fps
here’s the fixed script:
local FOLLOW_THRESHOLD = 2
local STIFFNESS = 160
local DAMPING = 22
local MAX_SPEED = 55
local velocity = Vector3.zero
local FIXED_DT = 1 / 120
local accumulator = 0
RunService.Heartbeat:Connect(function(dt)
local standCF = Stand:GetPivot()
local charCF = Character:GetPivot()
local target = (charCF * CFrame.new(3, -1, 2)).Position
local rotation = charCF - charCF.Position
accumulator += dt
while accumulator >= FIXED_DT do
local toTarget = target - standCF.Position
local distance = toTarget.Magnitude
if distance > FOLLOW_THRESHOLD or velocity.Magnitude > 0.01 then
local springForce = toTarget * STIFFNESS - velocity * DAMPING
velocity += springForce * FIXED_DT
local speed = velocity.Magnitude
if speed > MAX_SPEED then
velocity *= MAX_SPEED / speed
end
end
if velocity.Magnitude < 0.05 then
velocity = Vector3.zero
end
accumulator -= FIXED_DT
end
local newPos = Stand:GetPivot().Position + velocity * FIXED_DT
Stand:PivotTo(CFrame.new(newPos) * rotation)
end)
does the Stand model have any Anchored = false parts in it
is anything else (server script, another local script) also moving the Stand
also try switching Heartbeat to RenderStepped - Heartbeat fires after physics but before render, so at high FPS you can read the same character position multiple times in a row which causes the spring to micro-stutter. RenderStepped fires right before the frame draws which is what you want for purely visual things like a stand following you
does the stutter happen when you’re standing completely still, or only while moving?
can you temporarily add a print(dt) inside the Heartbeat and tell me if the values look normal or are spiking a lot above 120fps?
local FOLLOW_THRESHOLD = 2
local STIFFNESS = 160
local DAMPING = 22
local MAX_SPEED = 55
local velocity = Vector3.zero
local FIXED_DT = 1 / 120
local accumulator = 0
RunService.Heartbeat:Connect(function(dt)
local hrp = Character:FindFirstChild("HumanoidRootPart")
if not hrp then return end
local charCF = hrp.CFrame
local predictedPos = charCF.Position + hrp.AssemblyLinearVelocity * dt
local target = (CFrame.new(predictedPos) * (charCF - charCF.Position) * CFrame.new(3, -1, 2)).Position
local rotation = charCF - charCF.Position
accumulator += dt
while accumulator >= FIXED_DT do
local standPos = Stand:GetPivot().Position
local toTarget = target - standPos
local distance = toTarget.Magnitude
if distance > FOLLOW_THRESHOLD or velocity.Magnitude > 0.01 then
local springForce = toTarget * STIFFNESS - velocity * DAMPING
velocity += springForce * FIXED_DT
local speed = velocity.Magnitude
if speed > MAX_SPEED then
velocity *= MAX_SPEED / speed
end
end
if velocity.Magnitude < 0.05 then
velocity = Vector3.zero
end
accumulator -= FIXED_DT
end
local newPos = Stand:GetPivot().Position + velocity * FIXED_DT
Stand:PivotTo(CFrame.new(newPos) * rotation)
end)
the * dt on the prediction was slightly overshooting, fixed it to use 1/240 to match exactly one physics step:
local FOLLOW_THRESHOLD = 2
local STIFFNESS = 160
local DAMPING = 22
local MAX_SPEED = 55
local velocity = Vector3.zero
local FIXED_DT = 1 / 120
local accumulator = 0
RunService.Heartbeat:Connect(function(dt)
local hrp = Character:FindFirstChild("HumanoidRootPart")
if not hrp then return end
local charCF = hrp.CFrame
local predictedPos = charCF.Position + hrp.AssemblyLinearVelocity * (1/240)
local target = (CFrame.new(predictedPos) * (charCF - charCF.Position) * CFrame.new(3, -1, 2)).Position
local rotation = charCF - charCF.Position
accumulator += dt
while accumulator >= FIXED_DT do
local standPos = Stand:GetPivot().Position
local toTarget = target - standPos
local distance = toTarget.Magnitude
if distance > FOLLOW_THRESHOLD or velocity.Magnitude > 0.01 then
local springForce = toTarget * STIFFNESS - velocity * DAMPING
velocity += springForce * FIXED_DT
local speed = velocity.Magnitude
if speed > MAX_SPEED then
velocity *= MAX_SPEED / speed
end
end
if velocity.Magnitude < 0.05 then
velocity = Vector3.zero
end
accumulator -= FIXED_DT
end
local newPos = Stand:GetPivot().Position + velocity * FIXED_DT
Stand:PivotTo(CFrame.new(newPos) * rotation)
end)
The problem isn’t the model since i changed it to a default rig and it didn’t fix the problem. I can’t send you the place with the model in it since it wasn’t made by just me
Hey, found three bugs in the original code that were all hitting you at once. Here’s the fixed version:
local RunService = game:GetService("RunService")
local Character = script.Parent
local Stand = workspace:WaitForChild("Stand")
local StandRoot = Stand:WaitForChild("StandHumanoidRootPart")
StandRoot.Anchored = true
local FOLLOW_THRESHOLD = 2
local STIFFNESS = 160
local DAMPING = 22
local MAX_SPEED = 55
local FIXED_DT = 1 / 120
local FLOOR_OFFSET = 3.15
local floorFilter = RaycastParams.new()
floorFilter.FilterDescendantsInstances = {Stand, Character}
floorFilter.FilterType = Enum.RaycastFilterType.Exclude
local function getFloorY(pos)
local ray = workspace:Raycast(pos + Vector3.new(0, 0.5, 0), Vector3.new(0, -25, 0), floorFilter)
return ray and ray.Position.Y or -math.huge
end
local velocity = Vector3.zero
local accumulator = 0
local physicsPos = StandRoot.CFrame.Position
local lastPhysicsPos = physicsPos
RunService.Heartbeat:Connect(function(dt)
local hrp = Character:FindFirstChild("HumanoidRootPart")
if not hrp then return end
local charCF = hrp.CFrame
local target = (charCF * CFrame.new(3, -1, 2)).Position
local rotation = charCF - charCF.Position
accumulator += dt
while accumulator >= FIXED_DT do
lastPhysicsPos = physicsPos
local toTarget = target - physicsPos
local distance = toTarget.Magnitude
if distance > FOLLOW_THRESHOLD or velocity.Magnitude > 0.01 then
local springForce = toTarget * STIFFNESS - velocity * DAMPING
velocity += springForce * FIXED_DT
local speed = velocity.Magnitude
if speed > MAX_SPEED then
velocity *= MAX_SPEED / speed
end
end
if velocity.Magnitude < 0.05 then
velocity = Vector3.zero
end
physicsPos = physicsPos + velocity * FIXED_DT
local minY = getFloorY(physicsPos) + FLOOR_OFFSET
if physicsPos.Y < minY then
physicsPos = Vector3.new(physicsPos.X, minY, physicsPos.Z)
if velocity.Y < 0 then
velocity = Vector3.new(velocity.X, 0, velocity.Z)
end
end
accumulator -= FIXED_DT
end
local alpha = accumulator / FIXED_DT
local renderPos = lastPhysicsPos:Lerp(physicsPos, alpha)
StandRoot.CFrame = CFrame.new(renderPos) * rotation
end)
You could try playing around with the render priority stuff, like BindToRenderStep. Maybe do the priority as Enum.RenderPriority.Camera.Value -/+ 1 (or try other enum values)
I know why this is happening, but as far as I know there isn’t a simple fix for this exact scenario unless you’re open to moving part of the update into the control of the Roblox physics engine. Your problem is different than the usual problem of trying to sync something relative to the camera, for which using RunService.PreRender or BindToRenderStep is the fix.
In your case, the problem is that the character is being moved by the Roblox physics engine, and the camera is tracking the player, but your “Stand” character is doing it’s own physics calculations and it doesn’t have access to the actual timesteps used to update the character, only an approximate overall dt which isn’t good enough for pixel-perfect following.
Just for giggles, try this version below, to which I’ve added a correction factor for the linear velocity only (does not fix rotational jitter you see when rotating your camera, see note below):
local FOLLOW_THRESHOLD = 2
local STIFFNESS = 80
local DAMPING = 18
local MAX_SPEED = 27
local velocity = Vector3.zero
-- Initialize previous character position
local lastCharCF = Players.LocalPlayer.Character:GetPivot()
RunService.Heartbeat:Connect(function(dt)
local charCF = Players.LocalPlayer.Character:GetPivot()
local standCF = Stand:GetPivot()
-- Correction Factor
local speedFromDisplacementChar = (charCF.Position - lastCharCF.Position).Magnitude/dt
local speedFromSimulation = Players.LocalPlayer.Character.HumanoidRootPart.AssemblyLinearVelocity.Magnitude
if speedFromSimulation > 0.01 then
dt *= speedFromDisplacementChar/speedFromSimulation
end
-- End Correction Factor
local target = (charCF * CFrame.new(3, -1, 2)).Position
local rotation = charCF - charCF.Position
local toTarget = target - standCF.Position
local distance = toTarget.Magnitude
if distance > FOLLOW_THRESHOLD or velocity.Magnitude > 0.01 then
local springForce = toTarget * STIFFNESS - velocity * DAMPING
velocity = velocity + springForce * dt
local speed = velocity.Magnitude
if speed > MAX_SPEED then
velocity = velocity * (MAX_SPEED / speed)
end
end
if velocity.Magnitude < 0.05 then
velocity = Vector3.zero
end
local newPos = standCF.Position + velocity * dt
Stand:PivotTo(CFrame.new(newPos) * rotation)
-- Save previous character position
lastCharCF = charCF
end)
Fixing the stutter of the part you see when rotating the camera is a separate issue, I think mostly coming from how the positional update is driven by a spring system, but the rotation update is just copying character rotation.
Personally, I think I’d solve this whole issue a different way, by using Roblox physics constraints on the Stand character (AlignPosition and AlignOrientation), and just use your spring calculations to set the target attachments for those constraints, rather than using CFraming to move the character directly. That way, all of the position and rotation updates for both player avatar and Stand character would happen in sync, with the same time steps in the Roblox physics simulation step.