TRAVERSAL like procedural footplanting

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.

5 Likes

I’m adding this exact module to my game and I have made it to work nicely for me today. I hope you have not just used the module out-of-the-box. I’ll modify it to straighten the legs a second after stopping. Just gonna bump this right now so anyone else can post a suggestion for now

1 Like

Really? i still couldnt really figure it out but i also didnt really do much to it after this post. Can i see some more insight on what you did?

1 Like

I almost did it, but found that cddik is not very good, so I moved to a new one, which I thought was much better, but couldn’t twist legs. I can say I did make them able to twist when turning, but they currently can’t take steps while resetting their position. You can also check this post, which explains how to take steps realistically (also check its linked post).