Removing jitters from movement script

For a long time, I’ve been messing around with what I consider to be the “third person action realistic movement script.”

Examples from other games

emqbs0xrt1j81
giphy
So figgin cool

The goal with this script was 3 things:

  1. Always active shiftlock (or some third person over the shoulder camera with cframe)
  2. Use the default strafing functionality that shiftlock has.

51de1bb96bbccb16fe17409d84a1385c7cbf2acf
The character can backpedal and move side to side

  1. Rotation while idle does not rotate your character, he keeps his feet planted. (I haven’t seen anyone do it, but turn in place IK would be super cool. Someone said they were working on it, but that was a while ago.)

b0a85ac3d75dcb18aeea54d6c9335d11214a1280
Spinning around is unrealistic

Now I have managed to achieve all of this by ripping the CameraScript, but its a tad buggy. If the CameraScript is updated by Roblox or I need to use some other functionality of it, it’s a serious pain to update this. It also runs off old buggy shiftlock code which causes issues with a different element of the game. (Crouching into vents would cause the camera to zoom weirdly next to invisible walls.)


This system is perfect if it weren’t so dang buggy

After returning to this movement system, I decided I wanted to rewrite it to be short, performant, and running off a single script. (Replication was a goal for down the road.) I managed to make this, which works perfectly except I couldn’t manage to get the strafing animations to toggle on:

This was the code. It does a CFrame offset for the camera and toggles rotation when moving.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local player = Players.LocalPlayer
local char = player.Character or player.CharacterAdded:Wait()
local humanoid = char:WaitForChild("Humanoid")
local root = humanoid.RootPart
local cam = workspace.CurrentCamera


local SHOULDER_OFFSET = Vector3.new(2, 1.5, 4)


RunService:BindToRenderStep("MouseLock", Enum.RenderPriority.Character.Value, function()
	UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
end)

RunService:BindToRenderStep("ShoulderCamera", Enum.RenderPriority.Camera.Value, function()
	if not root then return end

	local camRot = cam.CFrame.Rotation

	local basePos = root.Position

	local offset = camRot * SHOULDER_OFFSET

	cam.CFrame = CFrame.new(basePos) * camRot + offset
end)

local alignActive = false
local function enableAlign()
	if alignActive then return end
	alignActive = true
	RunService:BindToRenderStep("CharAlign", Enum.RenderPriority.Character.Value, function()
		if not root or not humanoid then return end

		local moveDir = humanoid.MoveDirection
		if moveDir.Magnitude > 0 then

			local camCF = workspace.CurrentCamera.CFrame
			local camLook = camCF.LookVector
			local camRight = camCF.RightVector


			local forward = moveDir:Dot(Vector3.new(camLook.X, 0, camLook.Z).Unit)
			local sideways = moveDir:Dot(Vector3.new(camRight.X, 0, camRight.Z).Unit)

			if math.abs(forward) > math.abs(sideways) then
				local _, currentY, _ = root.CFrame:ToEulerAnglesYXZ()
				local _, targetY, _ = camCF:ToEulerAnglesYXZ()
				local newY = currentY + (targetY - currentY) * 0.1
				root.CFrame = CFrame.new(root.Position) * CFrame.Angles(0, newY, 0)
			end
		end
	end)
end



local function disableAlign()
	if not alignActive then return end
	alignActive = false
	RunService:UnbindFromRenderStep("CharAlign")
end

local MOVE_THRESHOLD = 1

RunService:BindToRenderStep("MovementCheck", Enum.RenderPriority.Input.Value, function()
	if humanoid.MoveDirection.Magnitude > MOVE_THRESHOLD then
		humanoid.AutoRotate = false
		enableAlign()
	else
		humanoid.AutoRotate = true
		disableAlign()
	end
end)


You can see everything looks great but doesn’t have any side to side strafing or backpedaling

So after a lot of back and forth with GPT5, “we” figured out a system that is 95% there. Somehow this code allows for strafing animations to be active, but there’s a small bug. When transition from idle to moving, the character rotates, however sometimes this rotation tween plays while the player is just walking around casually.

This was the code modified by GPT5. This was about 2 hours back and forth discussing what works and doesn't.
-- LocalScript in StarterPlayerScripts
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local player = Players.LocalPlayer
local char = player.Character or player.CharacterAdded:Wait()
local humanoid = char:WaitForChild("Humanoid")
local root = char:WaitForChild("HumanoidRootPart")
local cam = workspace.CurrentCamera

-- Shoulder offset (right shoulder, tweak as needed)
local SHOULDER_OFFSET = Vector3.new(2, 1.5, 4)
-- X = sideways, Y = up, Z = back

-- Idle threshold (seconds before counting as true idle)
local IDLE_THRESHOLD = 0.5
local idleTimer = 0
local idleConfirmed = false

-- Mouse always locked
RunService:BindToRenderStep("MouseLock", Enum.RenderPriority.Character.Value, function()
	UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
end)

-- Custom camera update
RunService:BindToRenderStep("ShoulderCamera", Enum.RenderPriority.Camera.Value, function()
	if not root then return end

	-- Get camera rotation from mouse
	local camRot = cam.CFrame.Rotation

	-- Base position = character root
	local basePos = root.Position

	-- Offset relative to camera rotation (NOT static world space)
	local offset = camRot * SHOULDER_OFFSET

	-- Place camera
	cam.CFrame = CFrame.new(basePos) * camRot + offset
end)

-- Character rotation logic
local alignActive = false
local function enableAlign()
	if alignActive then return end
	alignActive = true
	RunService:BindToRenderStep("CharAlign", Enum.RenderPriority.Character.Value, function()
		if not root or not humanoid then return end

		local moveDir = humanoid.MoveDirection
		if moveDir.Magnitude > 0 then
			-- Project movement onto camera axes
			local camCF = workspace.CurrentCamera.CFrame
			local _, currentY, _ = root.CFrame:ToEulerAnglesYXZ()
			local _, targetY, _ = camCF:ToEulerAnglesYXZ()
			local newY = currentY + (targetY - currentY) * 0.1
			root.CFrame = CFrame.new(root.Position) * CFrame.Angles(0, newY, 0)
		end
	end)
end

local function disableAlign()
	if not alignActive then return end
	alignActive = false
	RunService:UnbindFromRenderStep("CharAlign")
end

-- Animation-based idle detection
local animator = humanoid:WaitForChild("Animator")
local idleTrack

animator.AnimationPlayed:Connect(function(track)
	if track.Name:lower():find("idle") then
		idleTrack = track
		idleTimer = 0
		idleConfirmed = false

		track.Stopped:Connect(function()
			if idleTrack == track then
				idleTrack = nil
				idleTimer = 0
				idleConfirmed = false
			end
		end)
	end
end)

-- Main movement/idle logic
RunService:BindToRenderStep("MovementCheck", Enum.RenderPriority.Input.Value, function(dt)
	if idleTrack then
		-- count how long idle has been playing
		idleTimer += dt
		if idleTimer >= IDLE_THRESHOLD then
			idleConfirmed = true
		end
	else
		idleTimer = 0
		idleConfirmed = false
	end

	if idleConfirmed then
		-- Idle → let Roblox handle facing
		humanoid.AutoRotate = true
		disableAlign()
	else
		-- Moving/other animations → custom alignment
		humanoid.AutoRotate = false
		enableAlign()
	end
end)


You can see the strafing working now, but sometimes while turning the character does a spinning glitch. This will also just happen randomly while walking in a straight line.

So, what’s goofy in these samples of code? Why can’t the first one strafe and why does the second one have goofy turns despite having redundancy checks?

Thanks everyone.

2 Likes

What’s wrong with just setting it to UseStrafingAnimations from Player.. You’re game looks great, why go for some modified version of what’s already there. ChatGPT is always tryin to remake the wheel.

The toggle for strafing animations is always on. In both of those videos. The requirements for srafing are as follows,

But despite my script adhering to all of these the strafing doesn’t appear. I was not trying to do it the hard way with chat gpt, we were seeing what issues my script had that prevented these requirements from being met. It was mostly me who decided on the alternative solution that did not end up working. This is not me throwing scripts in and saying “fix it.” I am 90% sure the reason my top script doesn’t work is an issue on Roblox’s side.
TLDR; trust me bro if there was an easy way I would have taken it. My goal is a simple script.
[spoiler]Is 2112 a reference to rush? :open_mouth:[/spoiler]

Cough cough… B U M P.
Still prototyping different solutions, if anyone could look at my code that would be really helpful.