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)