How to make Player lean when turning

When riding on the skateboard, when the player turns, the board leans to the direction of turn. I want the player to lean as well. I’ve used CFrames on the humanoidrootpart to lean the player but when I do that the player glitches back and forth between the normal position and lean creating a glitchy turn. Any ideas on how to approach this?

With the player lean:

Without player lean:

function SkateboardController:StartMovementLoop(player)
	if MovementConnections[player] then return end
	PlayerInputState[player] = {W = false, S = false}
	PlayerDirections[player] = Vector3.new(0, 0, -1)
	PlayerSpeeds[player] = PlayerSpeeds[player] or 0
	local connection
	connection = RunService.Heartbeat:Connect(function(dt)
		local character = player.Character
		if not character or not PlayerStates[player] then
			connection:Disconnect()
			MovementConnections[player] = nil
			return
		end
		local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
		local humanoid = character:FindFirstChildOfClass("Humanoid")
		if not humanoidRootPart or not humanoid then
			connection:Disconnect()
			MovementConnections[player] = nil
			return
		end
		local inputState = PlayerInputState[player] or {W = false, S = false}
		local dir = PlayerDirections[player] or Vector3.new(0,0,-1)
		if dir.Magnitude == 0 then dir = Vector3.new(0,0,-1) end
		dir = Vector3.new(dir.X, 0, dir.Z)
		if dir.Magnitude > 0 then dir = dir.Unit end
		local grounded = self:IsGrounded(character)
		local currentSpeed = PlayerSpeeds[player] or 0

		if inputState.W and grounded then
			if currentSpeed < maxSkateboardspeed then
				currentSpeed = math.min(maxSkateboardspeed, currentSpeed + accelerationgSpeed * dt)
			end
		elseif inputState.S then
			currentSpeed = math.max(0, currentSpeed - Brake * dt)
		else
			currentSpeed = math.max(0, currentSpeed - coastingdecrease * dt)
		end
		PlayerSpeeds[player] = currentSpeed

		if currentSpeed > 0 then
			local currentLook = humanoidRootPart.CFrame.LookVector
			local desiredDir = dir
			if inputState.W then
				local angleDiff = math.acos(math.clamp(currentLook:Dot(desiredDir), -1, 1))
				local maxTurn = math.rad(SkateboardTurn) * dt
				if angleDiff > maxTurn then
					local axis = currentLook:Cross(desiredDir)
					if axis.Magnitude > 0 then
						axis = axis.Unit
						desiredDir = CFrame.fromAxisAngle(axis, maxTurn) * currentLook
					else
						desiredDir = currentLook
					end
				end
			else
				desiredDir = currentLook
			end
			local moveDelta = desiredDir * currentSpeed * dt
			local newPos = humanoidRootPart.Position + moveDelta
			humanoidRootPart.CFrame = CFrame.lookAt(newPos, newPos + desiredDir)

			local motor = SkateboardWelds[player]
			if motor then
				local turnDir = currentLook:Cross(desiredDir).Y
				local lean = math.clamp(turnDir * 30, -maxLean, maxLean)
				local currentC0 = motor.C0
				
				local targetC0 = CFrame.new(currentC0.Position) * CFrame.Angles(math.rad(90), lean, 0)
				motor.C0 = currentC0:Lerp(targetC0, dt * 5)

				local playerLeanAmount = lean * PlayerLean


				humanoidRootPart.CFrame = CFrame.lookAt( --Player lean code
					humanoidRootPart.Position,
					humanoidRootPart.Position + desiredDir,
					Vector3.new(0,1,0)
				) * CFrame.Angles(0, 0, -playerLeanAmount)
			end

		end

		if currentSpeed < 1 then
			if PlayerStates[player] and not PlayerInputState[player].W then
				self:StopAnimations(character)
				self:PlayAnimation(character, "Idle")
			end
		end
	end)
	MovementConnections[player] = connection
end
3 Likes

So I know if you use the “Platformer” Template, it comes with a script that makes it so when you move forwards, the character leans forwards, BUT also makes it so when you turn, the player leans in that direction, So here is the modified script!

--[[
	CharacterLeanScript - This script makes characters slightly lean in the direction they are moving.

	Each Step the character's Root joint Transform is updated based on the character's velocity.
--]]

local RunService = game:GetService("RunService")
local StarterPlayer = game:GetService("StarterPlayer")


-- Since this script has RunContext of Client, it will run anywhere regardless of its parent.
-- We only want it to run when it's parented to a character so we'll return immediately if it's in StarterCharacterScripts.
if script.Parent == StarterPlayer.StarterCharacterScripts then
	return
end

local character = script.Parent
-- Characters are not replicated atomically so we need to wait for children
local humanoid = character:WaitForChild("Humanoid")
local root = character:WaitForChild("HumanoidRootPart")
local rootJoint = character:WaitForChild("LowerTorso"):WaitForChild("Root")

local ROLL_ANGLE = math.rad(15)
local PITCH_ANGLE = math.rad(-55)
local LEAN_SPEED = 10

local leanCFrame = CFrame.new()

local function onStepped(_: number, deltaTime: number)
	local moveVelocity = humanoid:GetMoveVelocity()
	local relativeVelocity = root.CFrame:VectorToObjectSpace(moveVelocity)

	-- Calculate pitch and roll based on the character's relative velocity
	local pitch = 0
	local roll = 0
	if humanoid.WalkSpeed ~= 0 then
		pitch = math.clamp(relativeVelocity.Z / humanoid.WalkSpeed, -1, 1) * PITCH_ANGLE
		roll = -math.clamp(relativeVelocity.X / humanoid.WalkSpeed, -1, 1) * ROLL_ANGLE
	end

	leanCFrame = leanCFrame:Lerp(CFrame.Angles(pitch, 0, roll), math.min(deltaTime * LEAN_SPEED, 1))
	-- Apply the leaning to the rootJoint's Transform
	rootJoint.Transform = leanCFrame * rootJoint.Transform
end

RunService.Stepped:Connect(onStepped)

Just Pop this in a local script, in Starter Character Scripts, I don’t know if the lean is visible to other players though, But I hope this helped or can be modified to your liking

3 Likes