Camera Stuttering for Objects Moved with Physics

Context


I have a rather odd issue that I have been trying to solve for the past month. I have talked to many different people about the issue, but no one yet has seemed to provide a solution. I have also searched the DevForum quite a lot trying to see if someone has a similar issue, which they did, but the solutions given to them simply did not work for me.

In my game, I am trying to make an object move via BodyMovers which is controlled by user input. This system works fine, however, I started to notice that the object looked like it was stuttering. After doing more investigation into this, I realized the issue was with the Camera and not the object.

Now if I were to just set the camera to a defined position offset from the object, the stuttering is solved. However, I want my camera to smoothly follow the object, which introduced the stuttering issue. I am using a SmoothDamp function to move the camera, and yes the SmoothDamp function works as it is expected to in other situations (no issue with the function).

[Vector3] SmoothDamp(current: Vector3, target: Vector3, velocity: Vector3, smoothTime: number, maxSpeed: number, deltaTime: number)

For the purposes of this post, I have set up a reproduction place where I move a single Part via BodyVelocity. The network owner of the Part is set to the client and all works as expected minus the horrible stuttering.

Another important thing to note is I am linearly interpolating the values that are fed into the BodyVelocity.

function module.Lerpf(a, b, c)
	return clamp(a + (b - a) * c, min(a,b), max(a,b))
end

Failed Solutions


These appeared to be the best posts that matched my specific issue, however the solutions given did not solve my issue even though it seemed to solve other’s issues.

My Code


--SERVICES
local UserInput = game:GetService("UserInputService")
local RunService = game:GetService("RunService")

--MODULES
local Util = require(script:WaitForChild("Util"))

--VARIABLES
local Player = game:GetService("Players").LocalPlayer
local RootPart = workspace:WaitForChild("Root")

local Camera = workspace.CurrentCamera

--CONFIGURATION

local moveDir = 0
local activeSpeed = 0
local maxSpeed = 20
local speedAcc = 4

local BV = nil
local lastPhysicsDt, lastRenderDt = 0, 0
local lastCFrame = nil

--MODULE
local Controller = {Active = false, CONN_KeyPress = nil, CONN_KeyRelease = nil}

function Controller.Start()
	if Controller.Active then return end
	Controller.Active = true
	
	Camera.CameraType = Enum.CameraType.Scriptable
	Camera.CameraSubject = RootPart
	Camera.Focus = RootPart.CFrame
	
	BV = RootPart:WaitForChild("BodyVelocity")
	
	--INPUT LISTENERS
	Controller.CONN_KeyPress = UserInput.InputBegan:Connect(function(input, gpe)
		if gpe then return end
		if input.UserInputType == Enum.UserInputType.Keyboard then
			
			if input.KeyCode == Enum.KeyCode.A then
				moveDir -= 1
			elseif input.KeyCode == Enum.KeyCode.D then
				moveDir += 1
			end
			
		end
	end)
	
	Controller.CONN_KeyRelease = UserInput.InputEnded:Connect(function(input, gpe)
		if gpe then return end
		if input.UserInputType == Enum.UserInputType.Keyboard then

			if input.KeyCode == Enum.KeyCode.A then
				moveDir += 1
			elseif input.KeyCode == Enum.KeyCode.D then
				moveDir -= 1
			end

		end
	end)
	
	--UPDATE LISTENERS
	Controller.CONN_Stepped = RunService.Stepped:Connect(function(_, deltaTime)
		lastPhysicsDt = deltaTime
	end)
	RunService:BindToRenderStep("Control", Enum.RenderPriority.Camera.Value + 3, Controller.Update)
	
end

function Controller.Stop()
	if not Controller.Active then return end
	Controller.CONN_KeyPress:Disconnect()
	Controller.CONN_KeyPress = nil
	Controller.CONN_KeyRelease:Disconnect()
	Controller.CONN_KeyRelease = nil
	RunService:UnbindFromRenderStep("Control")
	Controller.Active = false
end

function Controller.Update(deltaTime)
	
	local sumDelta = lastRenderDt + lastPhysicsDt
	
	activeSpeed = Util.Lerpf(activeSpeed, moveDir * maxSpeed, speedAcc * lastPhysicsDt)
	BV.Velocity = Vector3.new(0, 0, activeSpeed)
	
	if not lastCFrame or not (lastCFrame == RootPart.CFrame) then
		local target = (RootPart.CFrame * CFrame.new(-20, 0, 0)).Position
		local pos = Util.SmoothDamp(Camera.CFrame.Position, target, Vector3.new(), 0.04, math.huge, sumDelta)
		
		Camera.CFrame = CFrame.new(pos) * CFrame.Angles(0, -math.pi / 2, 0)
		
		lastCFrame = RootPart.CFrame
		lastRenderDt = 0
	else
		lastRenderDt = deltaTime
	end
	
end

return Controller

Video


Reproduction File


StutterIssue.rbxl (29.6 KB)

Camera.CFrame = CFrame.new(pos*deltaTime) * CFrame.Angles(0, -math.pi / 2, 0)

That completely broke it, as shown in the video:
https://gyazo.com/10d1d1f591648d72dcb745c4c28dbbf9