You can write your topic however you want, but you need to answer these questions:
-
What do you want to achieve? I want to achieve smooth movement for this Mech model and make sure the LeftFoot and RightFoot MeshParts inside the Mech always align properly with the ground (even/uneven). If anyone here can take a look and suggest improvements I’d really appreciate it. There’s something about the movement that just feels off right now.
-
What is the issue?
I noticed it works fine
when walking on the baseplate, but when we change the surface to another part and walk off it, the Mech model starts “floating,” as you can see
- What solutions have you tried so far? None, because I’m still learning scripting and don’t know much yet
Here is the script:
local RunService = game:GetService(“RunService”)
local ReplicatedStorage = game:GetService(“ReplicatedStorage”)
– RemoteEvents setup
local toggleCombat = ReplicatedStorage:FindFirstChild(“ToggleCombatMode”) or Instance.new(“RemoteEvent”, ReplicatedStorage)
toggleCombat.Name = “ToggleCombatMode”
local toggleEngine = ReplicatedStorage:FindFirstChild(“ToggleEngine”) or Instance.new(“RemoteEvent”, ReplicatedStorage)
toggleEngine.Name = “ToggleEngine”
– References
local mech = script.Parent
local seat = mech:FindFirstChildOfClass(“VehicleSeat”)
local humanoid = mech:FindFirstChildOfClass(“Humanoid”)
local walkAnim = mech:FindFirstChild(“WalkAnim”)
local animator = humanoid and humanoid:FindFirstChildOfClass(“Animator”)
local rootPart = humanoid and humanoid.RootPart or mech.PrimaryPart
local leftFoot = mech:FindFirstChild(“LeftFoot”)
local rightFoot = mech:FindFirstChild(“RightFoot”)
if not (seat and humanoid and walkAnim and animator and rootPart and leftFoot and rightFoot) then
warn(“Missing critical parts”)
return
end
– Enable humanoid states
humanoid:SetStateEnabled(Enum.HumanoidStateType.Running, true)
humanoid:SetStateEnabled(Enum.HumanoidStateType.FallingDown, true)
humanoid:SetStateEnabled(Enum.HumanoidStateType.GettingUp, true)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Seated, true)
seat.Anchored = false
rootPart.Anchored = false
– Load animation
local walkTrack = animator:LoadAnimation(walkAnim)
– Force setup
local velocityForce = Instance.new(“LinearVelocity”)
velocityForce.Name = “MechMover”
velocityForce.Attachment0 = Instance.new(“Attachment”, rootPart)
velocityForce.RelativeTo = Enum.ActuatorRelativeTo.World
velocityForce.VelocityConstraintMode = Enum.VelocityConstraintMode.Vector
velocityForce.MaxForce = 1e7
velocityForce.VectorVelocity = Vector3.zero
velocityForce.Parent = rootPart
local moveSpeed = 25
local turnSpeed = math.rad(50)
– Sounds
local startSound = Instance.new(“Sound”, rootPart)
startSound.SoundId = “rbxassetid://7948751841”
local runningSound = Instance.new(“Sound”, rootPart)
runningSound.SoundId = “rbxassetid://8626621990”
runningSound.Looped = true
runningSound.Volume = 0.7
local stopSound = Instance.new(“Sound”, rootPart)
stopSound.SoundId = “rbxassetid://317671259”
local footstepSoundIds = {
“rbxassetid://6620598807”,
“rbxassetid://6620595244”
}
local function playFootstepSound(foot)
local soundId = footstepSoundIds[math.random(1, #footstepSoundIds)]
local sound = Instance.new(“Sound”, foot)
sound.SoundId = soundId
sound.Volume = 2
sound.Pitch = math.random(70, 85) / 100
sound:Play()
game.Debris:AddItem(sound, 2)
end
– Footstep control
local lastFootstepTime = 0
local FOOTSTEP_INTERVAL = 0.7
local lastFootIndex = 1
– Ground detection
local RAY_LENGTH = 1.5
local GROUNDED_THRESHOLD = 0.1
local COYOTE_TIME = 0.03
local grounded = false
local groundedTimer = 0
local lastGroundedTime = 0
local function castGroundRay(origin, mechIgnoreList)
local directions = {
Vector3.new(0, -1, 0),
Vector3.new(0.2, -1, 0),
Vector3.new(-0.2, -1, 0),
Vector3.new(0, -1, 0.2),
Vector3.new(0, -1, -0.2)
}
local params = RaycastParams.new()
params.FilterDescendantsInstances = mechIgnoreList
params.FilterType = Enum.RaycastFilterType.Blacklist
params.IgnoreWater = true
for _, dir in ipairs(directions) do
local result = workspace:Raycast(origin, dir * RAY_LENGTH, params)
if result and result.Instance and (result.Instance.CanCollide or result.Instance:IsA("Terrain")) then
return true
end
end
return false
end
local function checkFootCollisions(part)
local params = OverlapParams.new()
params.FilterDescendantsInstances = {mech}
params.FilterType = Enum.RaycastFilterType.Blacklist
local touchingParts = workspace:GetPartsInPart(part, params)
for _, p in ipairs(touchingParts) do
if (p:IsA("BasePart") and p.CanCollide) or p:IsA("Terrain") then
return true
end
end
return false
end
local function updateGrounded(dt)
local ignore = {mech}
local rayOrigin = rootPart.Position + Vector3.new(0, 4, 0)
local rayGrounded = castGroundRay(rayOrigin, ignore)
local left = checkFootCollisions(leftFoot) or castGroundRay(leftFoot.Position + Vector3.new(0, 0.5, 0), ignore)
local right = checkFootCollisions(rightFoot) or castGroundRay(rightFoot.Position + Vector3.new(0, 0.5, 0), ignore)
local floorMat = humanoid and humanoid.FloorMaterial ~= Enum.Material.Air
local groundedNow = rayGrounded or left or right or floorMat
if groundedNow then
groundedTimer = 0
grounded = true
lastGroundedTime = tick()
else
groundedTimer += dt
if groundedTimer > GROUNDED_THRESHOLD and tick() - lastGroundedTime > COYOTE_TIME then
grounded = false
end
end
end
– State variables
local engineStarted = false
local lastEngineToggle = 0
local engineCooldown = 1
local waistPart = mech:FindFirstChild(“Waist”)
local waistMotor = waistPart and waistPart:FindFirstChildWhichIsA(“Motor6D”)
local originalC0 = waistMotor and waistMotor.C0
local inCombat = false
local lookVector = nil
local startSoundEndedConn
local horizontalVelocity = Vector3.zero
local velocityDampingSpeed = 6
local lastHumanoidState = nil
local lastIsMoving = false
– TURNING HOLD & COOLDOWN VARIABLES (NEW)
local turnHoldTime = 0 – How long turn key held
local turnHoldThreshold = 1 – Seconds to hold before turn starts
local lastTurnDirection = 0 – -1 for left, 1 for right, 0 for none
local turnCooldown = 0.5 – cooldown between turns (seconds)
local turnCooldownTimer = 0 – timer counting down cooldown
local canTurn = false – whether we can turn now
– Remote Events
toggleCombat.OnServerEvent:Connect(function(player, toggle, camVec)
if seat.Occupant and seat.Occupant.Parent == player.Character then
inCombat = toggle
lookVector = camVec
end
end)
toggleEngine.OnServerEvent:Connect(function(player)
if seat.Occupant and seat.Occupant.Parent == player.Character then
local now = tick()
if now - lastEngineToggle < engineCooldown then return end
lastEngineToggle = now
engineStarted = not engineStarted
seat:SetAttribute("EngineStarted", engineStarted)
if engineStarted then
startSound:Play()
if startSoundEndedConn then startSoundEndedConn:Disconnect() end
startSoundEndedConn = startSound.Ended:Connect(function()
if engineStarted then runningSound:Play() end
end)
else
if startSoundEndedConn then startSoundEndedConn:Disconnect() end
runningSound:Stop()
velocityForce.VectorVelocity = Vector3.zero
velocityForce.MaxForce = 0
stopSound:Play()
end
toggleEngine:FireAllClients(engineStarted)
end
end)
– Main loop
RunService.Heartbeat:Connect(function(dt)
local isMoving = false
if engineStarted and seat.Occupant then
updateGrounded(dt)
local steer = seat.Steer
local steerDir = 0
if steer > 0.05 then
steerDir = 1
elseif steer < -0.05 then
steerDir = -1
else
steerDir = 0
end
-- TURNING HOLD & COOLDOWN LOGIC (UPDATED)
if steerDir ~= 0 then
if steerDir ~= lastTurnDirection then
turnHoldTime = 0
canTurn = false
end
if turnCooldownTimer <= 0 then
turnHoldTime = turnHoldTime + dt
if turnHoldTime >= turnHoldThreshold then
canTurn = true
end
end
else
turnHoldTime = 0
canTurn = false
end
lastTurnDirection = steerDir
if turnCooldownTimer > 0 then
turnCooldownTimer = math.max(turnCooldownTimer - dt, 0)
end
if canTurn then
local turnAngle = steerDir * turnSpeed * dt
rootPart.CFrame = rootPart.CFrame * CFrame.Angles(0, turnAngle, 0)
end
-- Start cooldown timer when turning stops
if not canTurn and lastIsMoving and lastTurnDirection == 0 then
if turnCooldownTimer == 0 then
turnCooldownTimer = turnCooldown
end
end
-- Movement logic
local throttle = seat.Throttle
if throttle ~= 0 and grounded then
local forward = Vector3.new(rootPart.CFrame.LookVector.X, 0, rootPart.CFrame.LookVector.Z).Unit
horizontalVelocity = horizontalVelocity:Lerp(forward * (moveSpeed * throttle), dt * 10)
isMoving = true
else
horizontalVelocity = horizontalVelocity:Lerp(Vector3.zero, dt * velocityDampingSpeed)
end
-- Apply force
if grounded then
velocityForce.MaxForce = 1e7
velocityForce.VectorVelocity = Vector3.new(horizontalVelocity.X, 0, horizontalVelocity.Z)
if lastHumanoidState ~= Enum.HumanoidStateType.Running then
humanoid:ChangeState(Enum.HumanoidStateType.Running)
lastHumanoidState = Enum.HumanoidStateType.Running
end
else
velocityForce.MaxForce = 1e7
velocityForce.VectorVelocity = Vector3.new(horizontalVelocity.X, -200, horizontalVelocity.Z)
if lastHumanoidState ~= Enum.HumanoidStateType.Freefall then
humanoid:ChangeState(Enum.HumanoidStateType.Freefall)
lastHumanoidState = Enum.HumanoidStateType.Freefall
end
end
-- Fix: Set isTurning properly
local isTurning = canTurn or (steerDir ~= 0 and turnHoldTime > 0)
-- Animation control
if (isMoving or isTurning) and not lastIsMoving then
if not walkTrack.IsPlaying then
walkTrack:Play()
end
elseif not (isMoving or isTurning) and lastIsMoving then
if walkTrack.IsPlaying then
walkTrack:Stop()
end
end
-- Footsteps
if (isMoving or isTurning) and grounded and tick() - lastFootstepTime > FOOTSTEP_INTERVAL then
local foot = (lastFootIndex == 1) and leftFoot or rightFoot
lastFootIndex = 3 - lastFootIndex
if foot and foot.Parent then
playFootstepSound(foot)
end
lastFootstepTime = tick()
end
lastIsMoving = (isMoving or isTurning)
-- Waist aiming
if waistMotor and originalC0 then
if inCombat and lookVector then
local mechForward = Vector3.new(rootPart.CFrame.LookVector.X, 0, rootPart.CFrame.LookVector.Z).Unit
local camDir = Vector3.new(lookVector.X, 0, lookVector.Z).Unit
local dot = mechForward:Dot(camDir)
local cross = mechForward:Cross(camDir)
local angle = math.acos(math.clamp(dot, -1, 1))
if cross.Y < 0 then angle = -angle end
local pos = originalC0.Position
local baseRot = originalC0 - pos
local yawRotation = CFrame.Angles(0, angle, 0)
local targetC0 = CFrame.new(pos) * yawRotation * baseRot
waistMotor.C0 = waistMotor.C0:Lerp(targetC0, dt * 7)
else
waistMotor.C0 = waistMotor.C0:Lerp(originalC0, dt * 5)
end
end
else
velocityForce.MaxForce = 0
velocityForce.VectorVelocity = Vector3.zero
if walkTrack.IsPlaying then walkTrack:Stop() end
if lastHumanoidState ~= Enum.HumanoidStateType.Seated then
humanoid:ChangeState(Enum.HumanoidStateType.Seated)
lastHumanoidState = Enum.HumanoidStateType.Seated
end
lastIsMoving = false
end
end)