Glitchy movements in crossy road type game

So, I have this LocalScript that controls the movement for the player (movement is replicated on the server just without tween animations) and right now I just can’t figure out how to stop the player from glitching a bit while stepping on the lilypads. The character also can float above or clip into it if they squish themselves (by holding down a movement button like wasd) before the lilypad has finished its sinking animations.

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 remotes = ReplicatedStorage.remotes

local player = script.Parent:WaitForChild("actualCharacter")
local bottomAttachment = player:FindFirstChild("BottomAttachment", true)
local playerHead = script.Parent:WaitForChild("Head")
local playerTorso = script.Parent:WaitForChild("Torso")
local humanoid = script.Parent:WaitForChild("Humanoid")
local bodyPosition = player.Parent:WaitForChild("cameraFollowPart"):WaitForChild("BodyPosition")

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

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

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

local moveCooldowns = {}
local moveCooldownTime = moveTime

local moveTweenInfo = TweenInfo.new(moveTime, Enum.EasingStyle.Quart)
local rotateTweenInfo = TweenInfo.new(rotateTime, Enum.EasingStyle.Back)
local squishTweenInfo = TweenInfo.new(squishTime, Enum.EasingStyle.Back)

local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {barrierLeft, barrierRight, player.Parent}
raycastParams.FilterType = Enum.RaycastFilterType.Exclude

local initialPlayerSize = player.Size
local initialPlayerPositionY = player.Position.Y

local viewGridSpace = false
local isMoving = false
local isSquishing = false

local platformOffset = Vector3.new()

local facingDirection = "back"
local currentPlatform = nil
local connection

function keyDownSquish(mode: boolean, x, z)
	if not x then x = 0 end
	if not z then z = 0 end

	local currentY = player.Position.Y

	isSquishing = true

	if mode then
		local newSize = Vector3.new(initialPlayerSize.X + 0.5, initialPlayerSize.Y - 3, initialPlayerSize.Z)
		local sizeDiffY = (initialPlayerSize.Y - newSize.Y) / 2

		tweenService:Create(player, squishTweenInfo, {
			Size = newSize,
			Position = Vector3.new(player.Position.X, currentY - sizeDiffY, player.Position.Z)
		}):Play()
	else
		tweenService:Create(player, squishTweenInfo, {
			Size = initialPlayerSize,
			Position = Vector3.new(player.Position.X + x, currentY, player.Position.Z + z)
		}):Play()

		task.delay(squishTime, function()
			isSquishing = false
		end)
	end
end

function shortestRotation(currentY, targetY)
	local delta = (targetY - currentY) % 360

	if delta > 180 then
		delta = delta - 360
	end

	return delta
end

function handleLilypadInteraction(hitPart)
	currentPlatform = hitPart
	platformOffset = player.Position - hitPart.Position

	local tweenInfo = TweenInfo.new(0.2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
	local originalPosition = hitPart.Position
	local sinkPosition = originalPosition - Vector3.new(0, 0.8, 0)

	local sinkTween = tweenService:Create(hitPart, tweenInfo, {Position = sinkPosition})
	local riseTween = tweenService:Create(hitPart, tweenInfo, {Position = originalPosition})

	connection = RunService.RenderStepped:Connect(function()
		if currentPlatform and currentPlatform:IsDescendantOf(Workspace.environment.obstacles) then
			if isSquishing then 
				player.Position = Vector3.new(player.Position.X, currentPlatform.Position.Y + platformOffset.Y - 1.5, player.Position.Z)
			else
				player.Position = Vector3.new(player.Position.X, currentPlatform.Position.Y + platformOffset.Y, player.Position.Z)
			end
		end
	end)

	sinkTween:Play()
	sinkTween.Completed:Wait()
	riseTween:Play()
	riseTween.Completed:Wait()

	if connection then
		connection:Disconnect()
	end	
end

function moveCharacter(direction: string)
	if not direction or isMoving then return end

	if connection then
		connection:Disconnect()
	end

	isMoving = true

	local targetPosition
	local cameraNextPosition

	if direction == "front" then
		targetPosition = player.Position + Vector3.new(0, 0, -distance)
		cameraNextPosition = Vector3.new(0, 0, -distance)
	elseif direction == "back" then
		targetPosition = player.Position + Vector3.new(0, 0, distance)
		cameraNextPosition = Vector3.new(0, 0, distance)
	elseif direction == "left" then
		targetPosition = player.Position + Vector3.new(-distance, 0, 0)
		cameraNextPosition = Vector3.new(-distance, 0, 0)
	elseif direction == "right" then
		targetPosition = player.Position + Vector3.new(distance, 0, 0)
		cameraNextPosition = Vector3.new(distance, 0, 0)
	end

	if targetPosition then
		local raycastResult = workspace:Raycast(targetPosition + Vector3.new(0, 5, 0), Vector3.new(0, -10, 0), raycastParams)
		if raycastResult then
			local attachment = raycastResult.Instance:FindFirstChild("Attachment")
			if attachment and bottomAttachment then
				local desiredY = attachment.WorldPosition.Y - (bottomAttachment.WorldPosition.Y - player.Position.Y)
				targetPosition = Vector3.new(targetPosition.X, desiredY, targetPosition.Z)
			else
				targetPosition = Vector3.new(targetPosition.X, initialPlayerPositionY, targetPosition.Z)
			end
		end

		local yDiff = math.abs(targetPosition.Y - player.Position.Y)
		local adjustedJumpHeight = math.max(jumpHeight, yDiff + 2) --clearance for steeper jump e.g: falling into water
		local midPoint = player.Position:Lerp(targetPosition, 0.5) + Vector3.new(0, adjustedJumpHeight, 0)

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

		jumpUp:Play()
		bodyPosition.Position += cameraNextPosition

		if facingDirection ~= direction then
			local targetY
			if direction == "front" then
				targetY = 0
			elseif direction == "back" then
				targetY = -180
			elseif direction == "left" then
				targetY = 90
			elseif direction == "right" then
				targetY = -90
			end

			if targetY then
				local rotationDelta = shortestRotation(player.Orientation.Y, targetY)
				tweenService:Create(player, rotateTweenInfo, {
					Orientation = player.Orientation + Vector3.new(0, rotationDelta, 0)
				}):Play()
			end

			facingDirection = direction
		end

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

		isMoving = false

		local raycastResult = workspace:Raycast(player.Position + Vector3.new(0, 5, 0), Vector3.new(0, -10, 0), raycastParams)
		if raycastResult then
			local hitPart = raycastResult.Instance
			if hitPart and hitPart.Name == "lilypad" then
				handleLilypadInteraction(hitPart)
			else
				currentPlatform = nil
			end
		end

		remotes.movementEvent:FireServer(direction, player.Position)
		remotes.rotateEvent:FireServer(direction)
	else
		isMoving = false
	end
end

bodyPosition.Position = playerTorso.Position + Vector3.new(-6, 0, -24)
player.Orientation = Vector3.new(0, -180, 0)
playerHead.Orientation = Vector3.new(0, -180, 0)
playerTorso.Orientation = Vector3.new(0, -180, 0)
humanoid.WalkSpeed = 0
humanoid.JumpPower = 0

--commences the squishing
userInputService.InputBegan:Connect(function(input, gameProcessedEvent)
	if not userInputService.KeyboardEnabled then return end
	if gameProcessedEvent then return end
	if isMoving then return end

	local key = input.KeyCode
	if key == Enum.KeyCode.W or key == Enum.KeyCode.Up 
		or key == Enum.KeyCode.S or key == Enum.KeyCode.Down
		or key == Enum.KeyCode.A or key == Enum.KeyCode.Left 
		or key == Enum.KeyCode.D or key == Enum.KeyCode.Right then
		keyDownSquish(true)
	end
end)

function canMove(key)
	local now = tick()

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

	moveCooldowns[key] = now + moveCooldownTime
	return true
end

--actually moves the character
userInputService.InputEnded:Connect(function(input, gameProcessedEvent)
	if not userInputService.KeyboardEnabled then return end
	if gameProcessedEvent then return end
	if isMoving then return end

	local key = input.KeyCode
	if (key == Enum.KeyCode.W or key == Enum.KeyCode.Up) and canMove("front") then	
		keyDownSquish(false, 0, -distance)
		moveCharacter("front")
	elseif (key == Enum.KeyCode.S or key == Enum.KeyCode.Down) and canMove("down") then
		keyDownSquish(false, 0, distance)
		moveCharacter("back")
	elseif (key == Enum.KeyCode.A or key == Enum.KeyCode.Left) and canMove("left") then
		keyDownSquish(false, -distance, 0)
		moveCharacter("left")
	elseif (key == Enum.KeyCode.D or key == Enum.KeyCode.Right) and canMove("right") then
		keyDownSquish(false, distance, 0)
		moveCharacter("right")
	end
end)

--commences the squishing for mouse
mouse.Button1Down:Connect(function()
	if not userInputService.KeyboardEnabled then return end
	if isMoving then return end

	keyDownSquish(true)
end)

--releases the squishing and does movement for mouse
mouse.Button1Up:Connect(function()
	if not userInputService.KeyboardEnabled then return end
	if isMoving then return end

	if not canMove("front") then return end
	keyDownSquish(false, 0, -distance)
	moveCharacter("front")
end) 

--mobile stuff
userInputService.TouchStarted:Connect(function(input, gameProcessedEvent)
	if gameProcessedEvent then return end
	if isMoving then return end

	keyDownSquish(true)
end)

userInputService.TouchTap:Connect(function(touchPosition, gameProcessedEvent)
	if gameProcessedEvent then return end
	if isMoving then return end

	if not canMove("front") then return end
	keyDownSquish(false, 0, -distance)
	moveCharacter("front")

	remotes.movementEvent:FireServer("front")
	remotes.rotateEvent:FireServer("front")
end)

userInputService.TouchSwipe:Connect(function(input)
	if input == Enum.SwipeDirection.Up then
		keyDownSquish(false, 0, -distance)
		moveCharacter("front")
	elseif input == Enum.SwipeDirection.Down then
		keyDownSquish(false, 0, distance)
		moveCharacter("back")
	elseif input == Enum.SwipeDirection.Left then
		keyDownSquish(false, -distance, 0)
		moveCharacter("left")
	elseif input == Enum.SwipeDirection.Right then
		keyDownSquish(false, distance, 0)
		moveCharacter("right")
	end
end)

if viewGridSpace then
	local part = Instance.new("Part")
	part.Size = Vector3.new(8, 10.5, 8)
	part.Anchored = false
	part.CanCollide = false
	part.CastShadow = false
	part.Transparency = 0.7
	part.Name = "GridSpacePart"
	part.Parent = player.Parent

	local weld = Instance.new("WeldConstraint")
	weld.Part0 = player.Parent:WaitForChild("GridSpacePart")
	weld.Part1 = player
	weld.Parent = part

	RunService.Heartbeat:Connect(function()
		part.Position = player.Position
		part.CFrame = CFrame.new(part.Position) * CFrame.Angles(0, 0, 0)
	end)
end
1 Like

I think to make a proper fix for this would be a bit of a hassle, as you’d maybe have to lerp to move the character according to where the lilypad is, which depends on the timing of the player starting to squish and won’t be the same every time.
Personally I’d probably do a bit of a hacky trick by basically making it so that the lilypad and character has two different tweens for when the character jumps on the lilypad, then if the player wants to start squishing before both tween are finished, then you cancel the character’s tween and teleport it to where it will normally end up, and then start the squish tween while the lilypad tween is still running. Unless the teleporting/floating is very obvious, then I think this can end up looking quite nice, and if someone is playing the game slowly, then it wouldn’t affect them and wouldn’t look hacky at all.
An alternative to fix it without floating and teleporting and not having to use lerp or something alike, could be a debounce so you can’t squish and start the moving before the lilypad tween is done, while this looks good, I think it’d be extremely annoying having to wait for that after 5min of playing the game.

1 Like