Weird stutter during motion

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.

1 Like

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)
1 Like

i tried removing the “*8” first and then your solution but neither worked

1 Like

can you share a few things:

  • is this a LocalScript or a regular Script
  • 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

RunService.RenderStepped:Connect(function(dt)
end) 
1 Like
  • it’s a local script;
  • rn only the root part is anchored but earlier everything was anchored and the result was the same;
  • no other script that moves the stand;
  • i tried both heartbeat and renderstepped but it doesn’t change anything
1 Like
  • 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 stutter only happens when the character is moving. If the character stops and the stand is still catching up it’ll stop stuttering;
  • The values go between 0.005 and 0.008 they’re pretty stable
1 Like

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)
1 Like

now it started stuttering on 120 and 60 too, even if way less than the higher framerate

External Media
1 Like

Could you just send the game file to me

1 Like

i just need to see the stand model

1 Like

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

1 Like

Is it okay if I use another model from the toolbox?

1 Like

ye of course. Also the code is literally just that one above you can copy paste it

1 Like

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)

the movement is still stuttery, though it doesn’t clip through the floor anymore

1 Like

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)

Good luck!

1 Like

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.

1 Like

I genuinely gave up made it linear. Thank you all for your help though