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


So figgin cool
The goal with this script was 3 things:
- Always active shiftlock (or some third person over the shoulder camera with cframe)
- Use the default strafing functionality that shiftlock has.
The character can backpedal and move side to side
- 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.)
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.

