How to create footlocking? / Alt title: how to create IK?

How to create footlocking like in this video?

this code doesnt even work, why?

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local LocalPlayer = Players.LocalPlayer

-- Utility to clean up old IKControls and target parts
local function cleanupOld(character)
    for _, obj in character:GetChildren() do
        if obj:IsA("IKControl") or (obj:IsA("Part") and (obj.Name == "LeftFoot_IKTarget" or obj.Name == "RightFoot_IKTarget" or obj.Name == "Left Leg_IKTarget" or obj.Name == "Right Leg_IKTarget")) then
            obj:Destroy()
        end
    end
end

local function getNeck(character)
    -- Works for both R15 and R6
    local head = character:FindFirstChild("Head")
    if not head then return nil end
    for _, obj in head:GetChildren() do
        if obj:IsA("Motor6D") and obj.Name == "Neck" then
            return obj
        end
    end
    return nil
end

local function lookAtNeck(neck, head, torso, targetPos)
    -- Calculate the look-at C0 for the neck joint
    local headPos = head.Position
    local torsoPos = torso.Position
    local dir = (targetPos - headPos)
    if dir.Magnitude < 0.1 then return end
    dir = dir.Unit

    -- Save original C0/C1 if not already
    if not neck.OriginC0 then
        neck.OriginC0 = neck.C0
        neck.OriginC1 = neck.C1
    end

    -- Calculate relative position in torso space
    local relative = torso.CFrame:PointToObjectSpace(targetPos)
    local yaw = math.atan2(-relative.X, -relative.Z)
    local pitch = math.atan2(relative.Y, math.sqrt(relative.X^2 + relative.Z^2))

    -- Clamp pitch/yaw to reasonable values (to avoid unnatural twisting)
    local maxYaw = math.rad(70)
    local maxPitch = math.rad(40)
    yaw = math.clamp(yaw, -maxYaw, maxYaw)
    pitch = math.clamp(pitch, -maxPitch, maxPitch)

    -- Apply rotation to original C0
    neck.C0 = neck.OriginC0 * CFrame.Angles(pitch, yaw, 0)
end

local currentRenderStepped = nil

local function setupFootLock(character)
    cleanupOld(character)

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid then return end
    humanoid.AutoRotate = false

    local hrp = character:FindFirstChild("HumanoidRootPart")
    if not hrp then return end

    -- Detect rig type
    local isR15 = character:FindFirstChild("LeftFoot") and character:FindFirstChild("RightFoot")
    local leftFoot = isR15 and character:FindFirstChild("LeftFoot") or character:FindFirstChild("Left Leg")
    local rightFoot = isR15 and character:FindFirstChild("RightFoot") or character:FindFirstChild("Right Leg")
    local leftChainRoot = isR15 and character:FindFirstChild("LeftUpperLeg") or character:FindFirstChild("Left Leg")
    local rightChainRoot = isR15 and character:FindFirstChild("RightUpperLeg") or character:FindFirstChild("Right Leg")
    if not (leftFoot and rightFoot and leftChainRoot and rightChainRoot) then return end

    -- Create invisible parts to act as foot lock targets
    local function createTargetPart(foot)
        local part = Instance.new("Part")
        part.Size = Vector3.new(0.2, 0.2, 0.2)
        part.Transparency = 1
        part.Anchored = true
        part.CanCollide = false
        part.Name = foot.Name .. "_IKTarget"
        part.CFrame = foot.CFrame
        part.Parent = character
        return part
    end

    local leftTarget = createTargetPart(leftFoot)
    local rightTarget = createTargetPart(rightFoot)

    -- Set up IKControls for both feet
    local function setupIK(foot, target, chainRoot)
        local ik = Instance.new("IKControl")
        ik.Type = Enum.IKControlType.Position
        ik.EndEffector = foot
        ik.Target = target
        ik.ChainRoot = chainRoot
        ik.Parent = humanoid
        ik.Enabled = true
        return ik
    end

    local leftIK = setupIK(leftFoot, leftTarget, leftChainRoot)
    local rightIK = setupIK(rightFoot, rightTarget, rightChainRoot)

    -- Update target parts to stay under feet when standing still
    local function updateTargets()
        leftTarget.CFrame = leftFoot.CFrame
        rightTarget.CFrame = rightFoot.CFrame
    end

    -- Get neck joint for head look-at
    local neck = getNeck(character)
    local head = character:FindFirstChild("Head")
    local upperTorso = character:FindFirstChild("UpperTorso") or character:FindFirstChild("Torso")

    -- Mouse reference
    local mouse = LocalPlayer:GetMouse()

    -- Disconnect previous RenderStepped if exists
    if currentRenderStepped then
        currentRenderStepped:Disconnect()
        currentRenderStepped = nil
    end

    currentRenderStepped = RunService.RenderStepped:Connect(function()
        if not LocalPlayer.Character or not LocalPlayer.Character:FindFirstChild("HumanoidRootPart") then return end
        local camera = workspace.CurrentCamera
        if not camera then return end

        -- Set HRP's orientation to match the camera's Y-axis rotation (ignore X/Z tilt)
        local hrpPos = hrp.Position
        local camCFrame = camera.CFrame
        -- Extract yaw from camera's CFrame
        local _, camYaw, _ = camCFrame:ToEulerAnglesYXZ()
        -- Only update if yaw is different to avoid jitter
        local currentYaw = select(2, hrp.CFrame:ToEulerAnglesYXZ())
        if math.abs(currentYaw - camYaw) > 1e-4 then
            hrp.CFrame = CFrame.new(hrpPos) * CFrame.Angles(0, camYaw, 0)
        end

        -- If not moving, update foot targets to lock feet in place
        if humanoid.MoveDirection.Magnitude < 0.1 then
            updateTargets()
            leftIK.Enabled = true
            rightIK.Enabled = true
        else
            leftIK.Enabled = false
            rightIK.Enabled = false
        end

        -- Make the head/neck look at the mouse's 3D hit position
        if neck and head and upperTorso and mouse then
            local mouseHit = mouse.Hit
            if mouseHit and typeof(mouseHit) == "CFrame" then
                local targetPos = mouseHit.Position
                lookAtNeck(neck, head, upperTorso, targetPos)
            end
        end
    end)
end

-- Listen for character spawn
LocalPlayer.CharacterAdded:Connect(function(character)
    if currentRenderStepped then
        currentRenderStepped:Disconnect()
        currentRenderStepped = nil
    end
    setupFootLock(character)
end)

if LocalPlayer.Character then
    setupFootLock(LocalPlayer.Character)
end

I losttttttttttttttttttttttttttt hope…

I finally found the solution:
In order to create footlocking, Keep a copy of the original Motor6D CFrames at all times.
Add the calculated offset to the original CFrames every frame.
Modify the Motor6Ds (Neck, Waist, Root, etc.) every frame based on the logic similar to the provided example.

local function footlocking(dt)
    local diffCFrame = currentHRPCFrame:ToObjectSpace(lastHRPCFrame)
    local _,X,_ = diffCFrame:ToEulerAnglesYXZ()
    finalDir = (finalDir + -X)
    local dtVal = 1 + (snappyness * dt * 60)
    local dtValWalk = 1 + ((snappyness * 0.8) * dt * 60)
    if HumanoidRootPart.AssemblyLinearVelocity.Magnitude > 0.1 then
        finalDir = math.clamp(finalDir/dtValWalk,-2.4,2.4) -- when player isnt standing still
    else
        if snap then
            if math.abs(finalDir) > 0.1 then -- we work our way to the target
                finalDir = math.clamp(finalDir/dtVal,-1,1)
            else
                snap = false
            end
        end
        
        if math.abs(finalDir) > maxDiffBeforeSnap then
            if maxDiffBeforeSnap < 2 then
                snap = true
                finalDir = math.clamp(finalDir/dtVal,-1,1)
            end
        end
    end

    if isScoped then
        finalCFrames.Waist *= CFrame.fromEulerAnglesYXZ(0,finalDir,0)
    else
        finalCFrames.Neck *= CFrame.fromEulerAnglesYXZ(0,finalDir*0.25,0)
        finalCFrames.Waist *= CFrame.fromEulerAnglesYXZ(0,finalDir*0.75,0)
    end
    
    finalCFrames.Root += CFrame.fromEulerAnglesYXZ(0,-finalDir,0)
    lastHRPCFrame = currentHRPCFrame
end

references:

welds.Neck = waitForChild(waitForChild(Character, "Head"), "Neck")
welds.LS = waitForChild(waitForChild(Character, "LeftUpperArm"), "LeftShoulder")
welds.RS = waitForChild(waitForChild(Character, "RightUpperArm"), "RightShoulder")
welds.Waist = waitForChild(waitForChild(Character, "UpperTorso"), "Waist")
welds.Root = waitForChild(waitForChild(Character, "LowerTorso"), "Root")

On character spawn, cache the original C0/C1 of Neck, Waist, Root (and optionally Shoulders if needed).
Every frame, calculate the required offset (finalDir) using the player’s movement and camera.
Apply the offset to the original C0/C1 and set them on the Motor6Ds.
Clean up and restore original C0/C1 on character removal.
Use RunService.RenderStepped for per-frame updates.
my function now:

local function footlockLoop(character, welds)
    local HumanoidRootPart = character:FindFirstChild("HumanoidRootPart")
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not HumanoidRootPart or not humanoid then return end

    local lastHRPCFrame = HumanoidRootPart.CFrame
    local finalDir = 0
    local snap = false
    local snappyness = 0.18
    local maxDiffBeforeSnap = 1.2
	local isScoped = character:FindFirstChild("IsScoped")

    renderConn = RunService.RenderStepped:Connect(function(dt)
        local currentHRPCFrame = HumanoidRootPart.CFrame
        local diffCFrame = currentHRPCFrame:ToObjectSpace(lastHRPCFrame)
        local _,X,_ = diffCFrame:ToEulerAnglesYXZ()
        finalDir = (finalDir + -X)
        local dtVal = 1 + (snappyness * dt * 60)
        local dtValWalk = 1 + ((snappyness * 0.8) * dt * 60)
        if HumanoidRootPart.AssemblyLinearVelocity.Magnitude > 0.1 then
            finalDir = math.clamp(finalDir/dtValWalk,-2.4,2.4)
        else
            if snap then
                if math.abs(finalDir) > 0.1 then
                    finalDir = math.clamp(finalDir/dtVal,-1,1)
                else
                    snap = false
                end
            end
            if math.abs(finalDir) > maxDiffBeforeSnap then
                if maxDiffBeforeSnap < 2 then
                    snap = true
                    finalDir = math.clamp(finalDir/dtVal,-1,1)
                end
            end
        end

        -- Apply offsets to Motor6Ds, always relative to original C0
        if isScoped.Value == true then
            if welds.Waist and originalCFrames.Waist then
                welds.Waist.C0 = originalCFrames.Waist.C0 * CFrame.fromEulerAnglesYXZ(0,finalDir,0)
            end
        else
            if welds.Neck and originalCFrames.Neck then
                welds.Neck.C0 = originalCFrames.Neck.C0 * CFrame.fromEulerAnglesYXZ(0,finalDir*0.25,0)
            end
            if welds.Waist and originalCFrames.Waist then
                welds.Waist.C0 = originalCFrames.Waist.C0 * CFrame.fromEulerAnglesYXZ(0,finalDir*0.75,0)
            end
        end
        if welds.Root and originalCFrames.Root then
            welds.Root.C0 = originalCFrames.Root.C0 * CFrame.fromEulerAnglesYXZ(0,-finalDir,0)
        end

        lastHRPCFrame = currentHRPCFrame
    end)
end
1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.