Character stuttering problem

I’ve been working on this Crossy Road game and have recently ran into an issue where when moving around, the players character can stutter or quickly move back and forth between their previous and next position (this is much more apparent on lower end hardware/mobile).

Considering this type of game where you need to move quickly, a chance of this happening can confuse a player and ruin a run.

Place link:

Player movement LocalScript:

local UserInputService = game:GetService("UserInputService")
local TweenService = game:GetService("TweenService")
local Workspace = game:GetService("Workspace")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local remotesFolder = ReplicatedStorage.remotes
local playerFolder = ReplicatedStorage.clientModels.player
local modulesFolder = ReplicatedStorage.modules

local lilypadModule = require(modulesFolder.lilypadModule)
local logModule = require(modulesFolder.logModule)
local playerModule = require(modulesFolder.playerModule)

local localPlayer = Players.LocalPlayer
local mouse = localPlayer:GetMouse()

local character = localPlayer.Character or script.Parent
local playerCharacterMesh = character:WaitForChild("actualCharacter")
local bottomAttachment = playerCharacterMesh:FindFirstChild("BottomAttachment", true)
local cameraBodyPosition = character:WaitForChild("cameraFollowPart"):WaitForChild("BodyPosition")

type directions = "front" | "back" | "left" | "right"
type DetectorMap = {
	FrontDetector: BasePart,
	BackDetector: BasePart,
	LeftDetector: BasePart,
	RightDetector: BasePart,
	MiddleDetector: BasePart,
}
local detectors: DetectorMap = {}
for _, name in ipairs({"Front", "Back", "Left", "Right", "Middle"}) do
	local detector = playerFolder.Detector:Clone()
	detector.Name = name .. "Detector"
	detector.Parent = playerCharacterMesh
	detectors[detector.Name] = detector
end

local keyMap = {
	[Enum.KeyCode.W] = "front", [Enum.KeyCode.Up] = "front",
	[Enum.KeyCode.S] = "back", [Enum.KeyCode.Down] = "back",
	[Enum.KeyCode.A] = "left", [Enum.KeyCode.Left] = "left",
	[Enum.KeyCode.D] = "right", [Enum.KeyCode.Right] = "right"
}

local swipeMap = {
	[Enum.SwipeDirection.Up] = "front",
	[Enum.SwipeDirection.Down] = "back",
	[Enum.SwipeDirection.Left] = "left",
	[Enum.SwipeDirection.Right] = "right"
}

local distance = 8
local jumpHeight = 6
local moveTime = 0.14
local squishTime = 0.2

local directionVectors = {
	front = Vector3.new(0, 0, -distance),
	back = Vector3.new(0, 0, distance),
	left = Vector3.new(-distance, 0, 0),
	right = Vector3.new(distance, 0, 0),
}

local yRotations = {
	front = 0,
	back = -180,
	left = 90,
	right = -90,
}

local moveCooldowns = {}
local moveCooldownTime = moveTime

local squishTweenInfo = TweenInfo.new(squishTime, Enum.EasingStyle.Back)

local initialPlayerSize = playerCharacterMesh.Size
local initialPlayerPositionY = playerCharacterMesh.Position.Y
local initialBottomAttachmentPositionY = bottomAttachment.Position.Y

local squishedSize = Vector3.new(initialPlayerSize.X + 0.5, initialPlayerSize.Y - 3, initialPlayerSize.Z)
local sizeDiffY = (initialPlayerSize.Y - squishedSize.Y) / 2

local CAMERA_OFFSET = Vector3.new(-6, 0, -24)
local MARKER_BOUNDS = 36

local isMoving = false
local isSquished = false

local moveQueue = {}

local facingDirection = "back"
local currentY
local currentLogMarker

--initial camera position on playerjoin
cameraBodyPosition.Position = playerCharacterMesh.Position + CAMERA_OFFSET

remotesFolder.respawnEvent.OnClientEvent:Connect(function()
	cameraBodyPosition.Position = Vector3.new(0, 7, 28.5) + CAMERA_OFFSET
	cameraBodyPosition.Parent.CFrame = CFrame.new(-5.99, 6.025, -3.962)

	facingDirection = "back"
	logModule.setFacingDirection(facingDirection)
	table.clear(moveQueue)

	playerCharacterMesh.Size = initialPlayerSize
	playerCharacterMesh.Transparency = 0
end)

function keyDownSquish(mode: boolean)
	if playerModule.isPlayerDead(localPlayer) then return end
	if logModule.getCurrentLog() then return end
	
	if mode then
		if isSquished then return end
		isSquished = true

		TweenService:Create(if playerCharacterMesh:FindFirstChild("playerClone") then playerCharacterMesh.playerClone else playerCharacterMesh, squishTweenInfo, {
			Size = squishedSize,
			Position = Vector3.new(playerCharacterMesh.Position.X, playerCharacterMesh.Position.Y - sizeDiffY, playerCharacterMesh.Position.Z)
		}):Play()

		TweenService:Create(bottomAttachment, squishTweenInfo, {
			Position = Vector3.new(playerCharacterMesh.Position.X, -squishedSize.Y / 2, playerCharacterMesh.Position.Z)
		}):Play()
	else
		if not isSquished then return end
		isSquished = false

		TweenService:Create(playerCharacterMesh, squishTweenInfo, {
			Size = initialPlayerSize,
			Position = Vector3.new(playerCharacterMesh.Position.X, currentY or initialPlayerPositionY, playerCharacterMesh.Position.Z)
		}):Play()

		TweenService:Create(bottomAttachment, squishTweenInfo, {
			Position = Vector3.new(playerCharacterMesh.Position.X, initialBottomAttachmentPositionY, playerCharacterMesh.Position.Z)
		}):Play()
	end
end

function moveCharacter(direction: directions)
	if character.actualCharacter.Position.Y <= 0 then return end
	if not direction or isMoving then return end 
	if playerModule.isPlayerDead(localPlayer) then return end

	isMoving = true

	local detectorToUse = detectors[string.gsub(direction, "^%l", string.upper) .. "Detector"]
	local cameraNextPosition = directionVectors[direction]
	local targetY = yRotations[direction]
	local targetPosition

	if facingDirection ~= direction and targetY then
		facingDirection = direction
		logModule.setFacingDirection(facingDirection)
		playerModule.rotatePlayer(targetY)
	end 

	local marker = playerModule.getMarker(detectorToUse, currentLogMarker)
	if marker then
		if marker:GetAttribute("Unusable") or math.abs(marker.Position.X) > MARKER_BOUNDS then
			remotesFolder.rotateEvent:FireServer(direction)
			isMoving = false
			return
		end

		local markerParent = marker.Parent.Parent
		if markerParent then
			local moveDirection = markerParent:GetAttribute("MoveDirection")
			local moveSpeed = markerParent:GetAttribute("MoveSpeed")

			if moveDirection and moveSpeed then
				currentLogMarker = marker
				
				local logVelocity = moveDirection.Unit * moveSpeed
				targetPosition = marker.Position + (logVelocity * moveTime)
			else
				currentLogMarker = nil
				targetPosition = marker.Position
			end
		end
	end

	if not targetPosition then
		isMoving = false
		logModule.stopFollowingLog()
		return
	end

	logModule.stopFollowingLog()
	lilypadModule.disconnect()

	remotesFolder.logEvent:FireServer(false)

	local nextRowAttachment = playerModule.getNextRowAttachment(detectorToUse)
	if nextRowAttachment then
		local desiredY = nextRowAttachment.WorldPosition.Y + (initialPlayerSize.Y / 2)
		currentY = desiredY
		targetPosition = Vector3.new(targetPosition.X, desiredY, targetPosition.Z)
	else
		currentY = nil
		targetPosition = Vector3.new(targetPosition.X, initialPlayerPositionY, targetPosition.Z)
	end

	local yDiff = math.abs(targetPosition.Y - playerCharacterMesh.Position.Y)
	local adjustedJumpHeight = math.max(jumpHeight, yDiff + 2)
	local midPoint = playerCharacterMesh.Position:Lerp(targetPosition, 0.5) + Vector3.new(0, adjustedJumpHeight, 0)

	local jumpUp = TweenService:Create(playerCharacterMesh, TweenInfo.new(moveTime / 2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out), {
		Position = midPoint
	})
	local jumpDown = TweenService:Create(playerCharacterMesh, TweenInfo.new(moveTime / 2, Enum.EasingStyle.Sine, Enum.EasingDirection.In), {
		Position = targetPosition
	})

	cameraBodyPosition.Position += cameraNextPosition

	jumpUp:Play()
	jumpUp.Completed:Wait()
	jumpDown:Play()
	jumpDown.Completed:Wait()

	isMoving = false
	
	if not marker:GetAttribute("Unusable") then
		remotesFolder.movementEvent:FireServer(direction, playerCharacterMesh.Position)
		remotesFolder.rotateEvent:FireServer(direction)
	end

	if #moveQueue > 0 then
		local nextDirection = table.remove(moveQueue, 1)
		task.defer(function()
			moveCharacter(nextDirection)
		end)
	end

	for _, v in Workspace:GetPartBoundsInBox(detectors.MiddleDetector.CFrame, detectors.MiddleDetector.Size) do
		if v.Name == "LogMarker" then continue end

		if v.Name == "Lilypad" then
			lilypadModule.lilypadInteraction(v)
		elseif string.find(v.Name:lower(), "log") then
			remotesFolder.logEvent:FireServer(true, v)
			logModule.startFollowingLog(v)
		end
	end
end

function canMove(key: directions)
	local now = tick()

	if moveCooldowns[key] and now < moveCooldowns[key] then
		return false
	end

	moveCooldowns[key] = now + moveCooldownTime
	return true
end

function addMoveToQueue(direction: string)
	if isMoving and not playerModule.isPlayerDead(localPlayer) then
		if #moveQueue < 1 then
			table.insert(moveQueue, direction)
			return true
		end
	end
	return false
end

playerCharacterMesh.Changed:Connect(function(property: string)
	if property ~= "Position" then return end

	local characterPosition = playerCharacterMesh.Position
	detectors.FrontDetector.Position = characterPosition - Vector3.new(0, 0, distance)
	detectors.BackDetector.Position = characterPosition + Vector3.new(0, 0, distance)
	detectors.LeftDetector.Position = characterPosition - Vector3.new(distance, 0, 0)
	detectors.RightDetector.Position = characterPosition + Vector3.new(distance, 0, 0)
	detectors.MiddleDetector.Position = characterPosition
end)

--keyboard
UserInputService.InputBegan:Connect(function(input, gameProcessedEvent)
	if playerModule.getPlayerDevice() == "mobile" then return end
	if playerModule.isPlayerDead(localPlayer) then return end
	if gameProcessedEvent or isMoving then return end
	if not keyMap[input.KeyCode] then return end
	keyDownSquish(true)
end)
UserInputService.InputEnded:Connect(function(input, gameProcessedEvent)
	if playerModule.getPlayerDevice() == "mobile" then return end
	if playerModule.isPlayerDead(localPlayer) then return end
	if gameProcessedEvent then return end

	local direction = keyMap[input.KeyCode]
	if not direction then return end

	if addMoveToQueue(direction) or not canMove(direction) then return end

	keyDownSquish(false)
	moveCharacter(direction)
end)

--mouse
mouse.Button1Down:Connect(function()
	if playerModule.getPlayerDevice() == "mobile" then return end
	if playerModule.isPlayerDead(localPlayer) then return end
	if isMoving then return end
	keyDownSquish(true)
end)
mouse.Button1Up:Connect(function()
	if playerModule.getPlayerDevice() == "mobile" then return end
	if playerModule.isPlayerDead(localPlayer) then return end

	if addMoveToQueue("front") or not canMove("front") then return end

	keyDownSquish(false)
	moveCharacter("front")
end) 

--mobile
UserInputService.TouchStarted:Connect(function(input, gameProcessedEvent)
	if playerModule.getPlayerDevice() == "pc" then return end
	if playerModule.isPlayerDead(localPlayer) then return end
	if gameProcessedEvent or isMoving then return end
	keyDownSquish(true)
end)
UserInputService.TouchTap:Connect(function(touchPosition, gameProcessedEvent)
	if playerModule.getPlayerDevice() == "pc" then return end
	if playerModule.isPlayerDead(localPlayer) then return end
	if gameProcessedEvent then return end

	if addMoveToQueue("front") or not canMove("front") then return end

	keyDownSquish(false)
	moveCharacter("front")
end)
UserInputService.TouchSwipe:Connect(function(input, touches, gameProcessedEvent)
	if playerModule.getPlayerDevice() == "pc" then return end
	if playerModule.isPlayerDead(localPlayer) then return end
	if gameProcessedEvent then return end

	local direction = swipeMap[input]
	if not direction then return end

	if addMoveToQueue(direction) or not canMove(direction) then return end

	keyDownSquish(false)
	moveCharacter(direction)
end)

Movement validation ServerScript:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local Workspace = game:GetService("Workspace")
local ServerScriptService = game:GetService("ServerScriptService")

local modulesFolder = ServerScriptService.modules
local playerModule = require(modulesFolder.playerModule)

local remotesFolder = ReplicatedStorage.remotes

local barrierLeft = Workspace.barrierLeft
local barrierRight = Workspace.barrierRight

local environmentFolder = Workspace.environment
local groundFolder = environmentFolder.ground

local pendingChecks = {}
local movementAmount = 8
local lastPosition

local movementVector3s = {
	front = Vector3.new(0, 0, -movementAmount),
	back  = Vector3.new(0, 0, movementAmount),
	left  = Vector3.new(-movementAmount, 0, 0),
	right = Vector3.new(movementAmount, 0, 0),
}

local directionCFrames = {
	front = CFrame.Angles(0, math.rad(0), 0),
	back  = CFrame.Angles(0, math.rad(180), 0),
	left  = CFrame.Angles(0, math.rad(90), 0),
	right = CFrame.Angles(0, math.rad(-90), 0),
}

function setPlayerPosition(character: model, position: Vector3)
	if not character or not position then return end
	character.actualCharacter.Position = position
	character.PrimaryPart.Position = position
end

local falsePositive
remotesFolder.log2Event.OnServerEvent:Connect(function(player: Player, result: Instance)
	if pendingChecks[player] then
		task.cancel(pendingChecks[player])
		pendingChecks[player] = nil
	end

	if result and string.find(result.Name:lower(), "log") then return end
	playerModule.killPlayer(player, player.Character, true)

	warn(falsePositive)
end)

remotesFolder.movementEvent.OnServerEvent:Connect(function(player: Player, direction: string, targetPosition: Vector3)
	local character = player.Character
	if not character or not targetPosition then return end

	local currentRow

	setPlayerPosition(character, targetPosition)

	if not character:GetAttribute("onLog") and not character:GetAttribute("Init") and character.PrimaryPart.Position.Y > 0 then
		if lastPosition and targetPosition and (lastPosition - targetPosition).Magnitude > 16.1 then
			if not pendingChecks[player] then 
				remotesFolder.log2Event:FireClient(player)
				falsePositive = (lastPosition - targetPosition).Magnitude
				--exploiter prevention if no response in 5 seconds
				pendingChecks[player] = task.delay(5, function() 
					pendingChecks[player] = nil
					playerModule.killPlayer(player, player.Character, true)
				end)
			end
		end
	end

	for _, v in Workspace:GetPartsInPart(character.PrimaryPart) do
		if string.find(v.Name:lower(), "marker") and v.Parent.Parent:GetAttribute("GroundType") then
			currentRow = v.Parent.Parent
		end

		if not v:GetAttribute("Unusable") then continue end
		setPlayerPosition(character, lastPosition)
		return
	end

	if character:GetAttribute("isDead") then 
		setPlayerPosition(character, lastPosition)
		return 
	end

	if direction == "front" or direction == "back" then
		barrierLeft.Position += movementVector3s[direction]
		barrierRight.Position += movementVector3s[direction]
	end

	if character:GetAttribute("Init") then
		character:SetAttribute("Init", false)
	end

	lastPosition = character.PrimaryPart.Position

	if character.PrimaryPart.Position.Y <= 0 then
		playerModule.killPlayer(player, character)
		return
	end

	if not currentRow then return end

	local number = string.match(currentRow.Name, "%d+")
	if not number then return end

	number = tonumber(number)

	--remove markers from 4 rows behind the player
	local lastReachableRow = "ground".. (number - 4) 
	local lastReachableGround = groundFolder:FindFirstChild("ground".. (number - 4))
	if lastReachableGround then
		local markersGroup = lastReachableGround:FindFirstChild("Markers")
		if markersGroup then
			markersGroup:Destroy() 
		end
	end
end)

remotesFolder.rotateEvent.OnServerEvent:Connect(function(player, direction)
	local character = player.Character
	if not character then return end

	local rotation = directionCFrames[direction]
	if not rotation then return end

	character.actualCharacter.CFrame = CFrame.new(character.actualCharacter.Position.X, character.actualCharacter.Position.Y, character.actualCharacter.Position.Z) * rotation
end)

It looks like you need to add a bigger cooldown between movements. The jumping animation restarts too soon, so it looks like your teleporting around unnaturally. Let me know if you think this isn’t the case!