so,in the past few weeks ive been trying to make a TRAVERSAL inspired movement system. (heres the game TRAVERSAL - Roblox)
i used an already existing module (R6 IKPF (Inverse Kinematics Procedural Footplanting)) and tried to modify it to my liking which did not come out how i wanted it.
first of all im already using a direction based animation system to animate the arms,torso and head im just using the procedural animations to animate the legs.
the problem is that when switching directions or stopping the legs twitch instead of smoothly shifting,and the legs dont reset when idle despite my attempts to do so. i have tried fixing these on a few occasions but im just way too new to luau to do so by myself (i also attempted using the help of ai which did not work)
im gonna put the scripts in question below.
local player = game.Players.LocalPlayer
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local Workspace = game.Workspace
local ProceduralAnimator = require(script.ProceduralAnimator)
local CCDIKController = require(script.CCDIKController)
local function createFakeLegs(character, Torso, RealLeftLeg, RealRightLeg)
local function newFakeLeg()
local fakeLeg = Instance.new("Part")
fakeLeg.Transparency = 1
fakeLeg.Size = Vector3.new(0.1, 0.1, 0.1)
fakeLeg.CanCollide = false
fakeLeg.CanQuery = false
fakeLeg.CanTouch = false
fakeLeg.Parent = character
return fakeLeg
end
local FakeUpperLLeg = newFakeLeg()
local FakeUpperRLeg = newFakeLeg()
local LeftHip = Instance.new("Motor6D")
LeftHip.Name = "Fake Left Hip"
LeftHip.C1 = CFrame.new(0, 0.3, 0)
LeftHip.C0 = CFrame.new(-0.5, -0.95, 0)
LeftHip.Part0 = Torso
LeftHip.Part1 = FakeUpperLLeg
LeftHip.Parent = Torso
local LeftLeg = Instance.new("Motor6D")
LeftLeg.Name = "Fake Left Leg"
LeftLeg.C1 = CFrame.new(0, 0.6, 0)
LeftLeg.C0 = CFrame.new(0, -0.15, 0)
LeftLeg.Part0 = FakeUpperLLeg
LeftLeg.Part1 = RealLeftLeg
LeftLeg.Parent = Torso
local RightHip = Instance.new("Motor6D")
RightHip.Name = "Fake Right Hip"
RightHip.C1 = CFrame.new(0, 0.3, 0)
RightHip.C0 = CFrame.new(0.5, -0.95, 0)
RightHip.Part0 = Torso
RightHip.Part1 = FakeUpperRLeg
RightHip.Parent = Torso
local RightLeg = Instance.new("Motor6D")
RightLeg.Name = "Fake Right Leg"
RightLeg.C1 = CFrame.new(0, 0.6, 0)
RightLeg.C0 = CFrame.new(0, -0.15, 0)
RightLeg.Part0 = FakeUpperRLeg
RightLeg.Part1 = RealRightLeg
RightLeg.Parent = Torso
return {LeftHip, LeftLeg}, {RightHip, RightLeg}, FakeUpperLLeg, FakeUpperRLeg
end
local function giveCharacterIK(character)
local Torso = character:WaitForChild("Torso")
local RealRightLeg = character:WaitForChild("Right Leg")
local RealLeftLeg = character:WaitForChild("Left Leg")
local function makeEndEffector(part)
local att = Instance.new("Attachment")
att.Name = "EndEffector"
att.Position = Vector3.new(0, -0.9, 0)
att.Parent = part
return att
end
local RightEffector = makeEndEffector(RealRightLeg)
local LeftEffector = makeEndEffector(RealLeftLeg)
local LeftLegData, RightLegData, FakeUpperLLeg, FakeUpperRLeg = createFakeLegs(character, Torso, RealLeftLeg, RealRightLeg)
local function createHingeConstraints(upLeg, realLeg)
local att0 = Instance.new("Attachment")
att0.Position = Vector3.new(0, -0.35, 0)
att0.Parent = upLeg
local att1 = Instance.new("Attachment")
att1.Position = Vector3.new(0, 0.4, 0)
att1.Parent = realLeg
local hinge = Instance.new("HingeConstraint")
hinge.Attachment0 = att0
hinge.Attachment1 = att1
hinge.LowerAngle = -140
hinge.UpperAngle = -15
hinge.Parent = upLeg
end
createHingeConstraints(FakeUpperLLeg, RealLeftLeg)
createHingeConstraints(FakeUpperRLeg, RealRightLeg)
local function createBallSocketConstraint(name, torso, fakeLeg, xOffset)
local attTorso = Instance.new("Attachment")
attTorso.Position = Vector3.new(xOffset, -0.95, 0)
attTorso.Parent = torso
local attLeg = Instance.new("Attachment")
attLeg.Position = Vector3.new(0, 0.3, 0)
attLeg.Parent = fakeLeg
local ballSocket = Instance.new("BallSocketConstraint")
ballSocket.Name = name
ballSocket.LimitsEnabled = true
ballSocket.TwistLimitsEnabled = false
ballSocket.UpperAngle = 20
ballSocket.Attachment0 = attTorso
ballSocket.Attachment1 = attLeg
ballSocket.Parent = torso
end
createBallSocketConstraint("RightBallSocketConstraint", Torso, FakeUpperRLeg, 0.5)
createBallSocketConstraint("LeftBallSocketConstraint", Torso, FakeUpperLLeg, -0.5)
Torso:WaitForChild("Right Hip").Enabled = false
Torso:WaitForChild("Left Hip").Enabled = false
local rightLegController = CCDIKController.new(RightLegData)
rightLegController.UseLastMotor = true
rightLegController:GetConstraints()
rightLegController:GetConstraintsFromMotor(RightLegData[1], "RightBallSocketConstraint")
local leftLegController = CCDIKController.new(LeftLegData)
leftLegController.UseLastMotor = true
leftLegController:GetConstraints()
leftLegController:GetConstraintsFromMotor(LeftLegData[1], "LeftBallSocketConstraint")
local leftStepAttach = Instance.new("Attachment")
leftStepAttach.Name = "LeftStepAttach"
leftStepAttach.Position = Vector3.new(-0.5, -2.8, 0.1)
leftStepAttach.Parent = Torso
local rightStepAttach = Instance.new("Attachment")
rightStepAttach.Name = "RightStepAttach"
rightStepAttach.Position = Vector3.new(0.5, -2.8, 0.1)
rightStepAttach.Parent = Torso
local rightHipAttach = Instance.new("Attachment")
rightHipAttach.Name = "RightHipAttach"
rightHipAttach.Position = Vector3.new(0.5, -0.9, 0)
rightHipAttach.Parent = Torso
local leftHipAttach = Instance.new("Attachment")
leftHipAttach.Name = "LeftHipAttach"
leftHipAttach.Position = Vector3.new(-0.5, -0.9, 0)
leftHipAttach.Parent = Torso
local r6Legs = {
rightLeg = {
CurrentCycle = 0,
CCDIKController = rightLegController,
HipAttachment = rightHipAttach,
FootAttachment = rightStepAttach,
},
leftLeg = {
CurrentCycle = math.pi,
CCDIKController = leftLegController,
HipAttachment = leftHipAttach,
FootAttachment = leftStepAttach,
}
}
local params = RaycastParams.new()
params.FilterDescendantsInstances = {character}
params.IgnoreWater = true
local humanoidRootPart = character:WaitForChild("HumanoidRootPart")
local rootMotor = humanoidRootPart:WaitForChild("RootJoint")
local animator = ProceduralAnimator.new(humanoidRootPart, r6Legs, rootMotor, params)
local runSound = humanoidRootPart:FindFirstChild("Running")
if runSound then
runSound.Volume = 0
end
local footStepSoundObj = script:FindFirstChild("FootStepSound")
if footStepSoundObj and footStepSoundObj.Value then
animator:ConnectFootStepSound(footStepSoundObj.Value)
end
local maxSpeed = 0.02 -- slower speed for heavy movement
local swayAmount = 0
local smoothedVelocity = Vector3.new(0, 0, 0)
local smoothingFactor = 8 -- how quickly velocity smooths out
local isIdle = false
local function updateLegCycles(dt)
local currentVelocity = humanoidRootPart.Velocity
smoothedVelocity = smoothedVelocity:Lerp(currentVelocity, math.clamp(smoothingFactor * dt, 0, 1))
local horizontalSpeed = Vector3.new(smoothedVelocity.X, 0, smoothedVelocity.Z).Magnitude
local cycleSpeed = math.clamp(horizontalSpeed / 6, 0, 1) * maxSpeed
if horizontalSpeed < 0.05 then
isIdle = true
local torsoLookVector = Torso.CFrame.LookVector
local torsoYaw = math.atan2(torsoLookVector.X, torsoLookVector.Z)
local outwardAngle = math.rad(15) -- slight outward angle
-- Left leg target rotation
local targetCFrameLeft = CFrame.new(-0.5, -0.95, 0) * CFrame.Angles(0, torsoYaw - outwardAngle, 0)
-- Right leg target rotation
local targetCFrameRight = CFrame.new(0.5, -0.95, 0) * CFrame.Angles(0, torsoYaw + outwardAngle, 0)
-- Smoothly interpolate leg Motor6D C0 toward the target (only legs)
r6Legs.leftLeg[1].C0 = r6Legs.leftLeg[1].C0:Lerp(targetCFrameLeft, 0.15)
r6Legs.rightLeg[1].C0 = r6Legs.rightLeg[1].C0:Lerp(targetCFrameRight, 0.15)
else
if isIdle then
isIdle = false
r6Legs.leftLeg.CurrentCycle = 0
r6Legs.rightLeg.CurrentCycle = math.pi
end
r6Legs.leftLeg.CurrentCycle = (r6Legs.leftLeg.CurrentCycle + cycleSpeed * dt) % (math.pi * 2)
r6Legs.rightLeg.CurrentCycle = (r6Legs.rightLeg.CurrentCycle + cycleSpeed * dt) % (math.pi * 2)
end
end
local function updateFootPlacement()
local function placeFoot(attachment)
local origin = attachment.Parent.Position + Vector3.new(0, 1, 0)
local direction = Vector3.new(0, -1.5, 0)
local result = Workspace:Raycast(origin, direction, params)
if result then
attachment.WorldPosition = result.Position + Vector3.new(0, 0.01, 0)
end
end
placeFoot(r6Legs.leftLeg.FootAttachment)
placeFoot(r6Legs.rightLeg.FootAttachment)
end
local animationConnection
animationConnection = RunService.Heartbeat:Connect(function(dt)
updateLegCycles(dt)
updateFootPlacement()
if not isIdle then
animator:Animate(dt)
end
end)
local humanoid = character:WaitForChild("Humanoid")
humanoid.StateChanged:Connect(function(oldState, newState)
if newState == Enum.HumanoidStateType.Landed then
local vel = humanoidRootPart.AssemblyLinearVelocity
humanoidRootPart.AssemblyLinearVelocity = Vector3.new(vel.X, 0, vel.Z)
end
end)
humanoid.Died:Connect(function()
if animationConnection then
animationConnection:Disconnect()
animationConnection = nil
end
end)
humanoidRootPart.AncestryChanged:Connect(function(_, parent)
if not parent and animationConnection then
animationConnection:Disconnect()
animationConnection = nil
end
end)
end
local function givePlayerIK(player)
player.CharacterAdded:Connect(giveCharacterIK)
if player.Character then
giveCharacterIK(player.Character)
end
end
Players.PlayerAdded:Connect(givePlayerIK)
for _, player in pairs(Players:GetPlayers()) do
givePlayerIK(player)
end
local RunService = game:GetService("RunService")
local ProceduralAnimatorClass = {}
ProceduralAnimatorClass.__index = ProceduralAnimatorClass
local CF = CFrame.new
local ANGLES = CFrame.Angles
local x_and_y = Vector3.new(1, 0, 1)
local TAU = 2 * math.pi
local DOWN = 10 * Vector3.new(0, -1, 0)
local Signal = require(script.Parent.Signal)
function ProceduralAnimatorClass.new(RootPart, Legs, RootMotor, raycastParams)
local self = setmetatable({}, ProceduralAnimatorClass)
self.RootPart = RootPart
self.RaycastParams = raycastParams
self.RootMotor = RootMotor
self.RootMotorC1Store = RootMotor and RootMotor.C1 or nil
self.WaistCycle = 0
self.Legs = Legs
self.DefaultStride = 1.2
self.CycleSpeed = 6
self.DefaultStrideOffset = 0
self.DefaultStrideCF = CFrame.new(0, 0, -self.DefaultStride / 2)
self.MovementDirectionXZ = Vector3.new(1, 0, 1)
self.rootVelocityMagnitude = 0
self.FootStep = Signal.new()
self.MaxSpeed = 20
self.EngineSound = nil
self.FootStepSound = nil
self.RandomNumGenerator = Random.new()
self.WalkBounce = 0.1
self.SwayX = -1 * 2
return self
end
function ProceduralAnimatorClass:MoveLegs(stepCycle, dt)
if self.rootVelocityMagnitude > 0.1 then
for _, Leg in pairs(self.Legs) do
local strideCF = Leg.StrideCF or self.DefaultStrideCF
local strideOffset = Leg.StrideOffset or self.DefaultStrideOffset
local raycastParams = self.RaycastParams
Leg.CurrentCycle = (Leg.CurrentCycle + stepCycle) % 360
local cycle = Leg.CurrentCycle
local IKTolerance = Leg.IKTolerance or 0
local hip = Leg.HipAttachment.WorldPosition
local ground = Leg.FootAttachment.WorldPosition
local desiredPos = (CF(ground, ground + self.MovementDirectionXZ) * ANGLES(-cycle, 0, 0) * strideCF).p
local offset = (desiredPos - hip)
local raycastResult = workspace:Raycast(hip, offset.Unit * (offset.Magnitude + strideOffset), raycastParams)
local footPos = raycastResult and raycastResult.Position or (hip + offset.Unit * (offset.Magnitude + strideOffset))
Leg.CCDIKController:CCDIKIterateUntil(footPos, IKTolerance)
if not Leg.TouchGround and raycastResult then
self.FootStep:Fire(raycastResult)
end
Leg.TouchGround = raycastResult ~= nil
end
else
local torsoLookVector = self.RootPart.CFrame.LookVector
local torsoYaw = math.atan2(torsoLookVector.X, torsoLookVector.Z)
local outwardAngle = math.rad(15)
for _, Leg in pairs(self.Legs) do
local strideCF = Leg.StrideCF or self.DefaultStrideCF
local strideOffset = Leg.StrideOffset or self.DefaultStrideOffset
local raycastParams = self.RaycastParams
local IKTolerance = Leg.IKTolerance or 0
local hip = Leg.HipAttachment.WorldPosition
local desiredPos = Leg.FootAttachment.WorldPosition + DOWN
-- side sign: right leg positive, left leg negative
local side = (Leg.HipAttachment.Position.X > 0) and 1 or -1
local outwardRot = CFrame.Angles(0, outwardAngle * side, 0)
-- rotate desiredPos to face torso forward plus outward angle
local torsoRotationCF = CFrame.Angles(0, torsoYaw, 0)
desiredPos = (CFrame.new(desiredPos) * torsoRotationCF * outwardRot).Position
local offset = (desiredPos - hip)
local raycastResult = workspace:Raycast(hip, offset.Unit * (offset.Magnitude + strideOffset), raycastParams)
local footPos = raycastResult and raycastResult.Position or (hip + offset.Unit * (offset.Magnitude + strideOffset))
Leg.CCDIKController:CCDIKIterateOnce(footPos, IKTolerance)
if not Leg.TouchGround and raycastResult then
self.FootStep:Fire(raycastResult)
end
Leg.TouchGround = raycastResult ~= nil
end
end
end
function ProceduralAnimatorClass:Animate(dt)
local dt10 = math.min(dt * 10, 1)
local rootpart = self.RootPart
local rootvel0 = rootpart.Velocity
local rootVelocity = rootvel0 * x_and_y
local rootVelocityMagnitude = rootVelocity.Magnitude
self.rootVelocityMagnitude = rootVelocityMagnitude
if self.EngineSound then
self.EngineSound.PlaybackSpeed = (rootVelocityMagnitude / self.MaxSpeed) + 0.6
end
if rootVelocityMagnitude > 0.1 then
self.MovementDirectionXZ = self.MovementDirectionXZ:Lerp(rootVelocity.unit, dt10 * 0.5)
end
local relativizeToHumanoidSpeed = rootVelocityMagnitude / 16
local stepCycle = relativizeToHumanoidSpeed * dt * self.CycleSpeed
self:MoveLegs(stepCycle, dt)
end
function ProceduralAnimatorClass:ConnectFootStepSound(sound)
self.FootStep:Connect(function(raycastResult)
local soundPositionAttachment = Instance.new("Attachment")
soundPositionAttachment.WorldPosition = raycastResult.Position
soundPositionAttachment.Parent = workspace.Terrain
local footStepSound = sound:Clone()
footStepSound.PlaybackSpeed = self.RandomNumGenerator:NextNumber(0.7, 1)
local reverbEffect = Instance.new("ReverbSoundEffect")
reverbEffect.Density = 0.8
reverbEffect.DecayTime = 1
reverbEffect.Parent = footStepSound
footStepSound.PlayOnRemove = true
footStepSound.Parent = soundPositionAttachment
soundPositionAttachment:Destroy()
end)
end
function ProceduralAnimatorClass:StartEngineSound(sound)
local engineSound = sound:Clone()
engineSound.Parent = self.RootPart
engineSound.Looped = true
engineSound:Play()
end
function ProceduralAnimatorClass:InitDragDebug()
for _, Leg in pairs(self.Legs) do
Leg.CCDIKController:InitDragDebug()
end
end
function ProceduralAnimatorClass:Destroy()
if self.FootStep then
self.FootStep:Destroy()
end
self = nil
end
return ProceduralAnimatorClass
there are a few more scripts in the module that i dont understand but these two are the only ones that i modified.
any help is greatly appreciated.