How to Add Foot Alternating Procedural Animation

Hello, I'm new to Unity and coding, and I'm working on a procedural foot placement animation using inverse kinematics. I'm stuck on making the foot alternate after each step. Can you help me with this, please

using UnityEngine;

public class footIK : MonoBehaviour
{
    // Debug flags
    [SerializeField] private bool groundDebug;
    [SerializeField] private bool IKDebug;

    // References
    [Header("References")]
    [SerializeField] private GameObject Steppartical;
    [SerializeField] private Transform StepparticalParent;

    // Ground check parameters
    [Header("GroundCheck")]
    [SerializeField] private LayerMask groundLayer;
    [SerializeField] private Transform groundRaycastOrigin;
    [SerializeField] private float groundRaycastDistance = 0.1f, groundRaycastRadius = 0.1f;

    // Body movement parameters
    [Header("Body")]
    [SerializeField] private Transform Body;
    [SerializeField] private float BodyHight;
    [SerializeField] private float BodyMoveSpeed;

    // Foot IK Visuals parameters
    [Header("Foot IK Visuals")]
    [SerializeField] private float FootSpeed;
    [SerializeField] private float StepHight;
    [SerializeField] private float timeBetweenSteps;

    // Foot IK parameters
    [Header("Foot IK")]
    [SerializeField] private Transform LeftFootTarget;
    [SerializeField] private Transform RightFootTarget;
    [SerializeField] private Transform IKRaycastOrigin;
    [SerializeField] private Transform LeftFootRaycastOrigin;
    [SerializeField] private Transform RightFootRaycastOrigin;
    [SerializeField] private float StepDistance;
    [SerializeField] private float StepLength;
    [SerializeField] private float MaxFootReach;
    [SerializeField] private float FootYOffset;
    [SerializeField] private LayerMask IKLayer;

    bool isGrounded;
    RaycastHit Groundhit;

    Vector3 oldPosR, oldPosL;
    Vector3 NewPosR, NewPosL;
    Vector3 FTTargetPosR, FTTargetPosL;
    float targetStepLength;

    // Update is called once per frame
    void Update()
    {
        FootIK(LeftFootRaycastOrigin, LeftFootTarget, ref NewPosR, ref oldPosR, ref FTTargetPosR);
        FootIK(RightFootRaycastOrigin, RightFootTarget, ref NewPosL, ref oldPosL, ref FTTargetPosL);

        // Perform spherecast downwards to check for ground contact
        isGrounded = Physics.SphereCast(groundRaycastOrigin.position, groundRaycastRadius, Vector3.down, out Groundhit, groundRaycastDistance, groundLayer);

        // Calculate and update the position of the Body based on foot positions
        Vector3 bodyTarget = ((FTTargetPosR + FTTargetPosL) / 2) + (Vector3.up * BodyHight) - (Body.forward * StepLength);

        if (isGrounded)
            Body.position = Vector3.Lerp(Body.position, bodyTarget, BodyMoveSpeed * Time.deltaTime);
    }

    // Initialize variables
    void Start()
    {
        NewPosL = FTTargetPosL;
        NewPosR = FTTargetPosR;
        oldPosL = FTTargetPosL;
        oldPosR = FTTargetPosR;

        targetStepLength = StepLength;
    }

    // Function to perform Foot IK
    void FootIK(Transform RayOrigin, Transform foot, ref Vector3 NewPos, ref Vector3 oldPos, ref Vector3 targetPos)
    {
        // Raycast forward to find the adjusted position for the foot
        RaycastHit FTHitForward;
        bool FTRayForward = Physics.Raycast(RayOrigin.position, RayOrigin.forward, out FTHitForward, targetStepLength, IKLayer);

        Vector3 FTHipPos;

        if (FTRayForward)
        {
            FTHipPos = FTHitForward.point; // Set position to the forward hit position
        }
        else
        {
            FTHipPos = RayOrigin.position + targetStepLength * IKRaycastOrigin.forward; // Set position to default forward position
        }

        // Raycast downward to find the adjusted position for the foot on the ground
        RaycastHit FTHitDown;
        RaycastHit FTIDLE;
        bool FTRayDown = Physics.Raycast(FTHipPos, Vector3.down, out FTHitDown, MaxFootReach, IKLayer);
        bool FTRayIDL = Physics.Raycast(RayOrigin.position, Vector3.down, out FTIDLE, MaxFootReach, IKLayer);

        // If foot isn't on the ground, go to a default state
        if (FTRayDown)
        {
            targetPos = Vector3.up * FootYOffset + FTHitDown.point;
            targetStepLength = StepLength;
        }
        else
        {
            targetPos = Vector3.up * (FootYOffset - MaxFootReach) + FTHipPos;
            targetStepLength = targetStepLength - 0.01f; // Decrease step length

            Debug.Log(targetStepLength);
        }

        float ftDist = Vector3.Distance(oldPos, targetPos);

        // Check if the character is moving
        if (InputManager.Manager.GetMovementInput().magnitude > 0)
        {
            if (ftDist > StepDistance)
            {
                oldPos = NewPos;
                NewPos = targetPos;

                // Spawn particle underneath feet
                if (FTRayDown)
                {
                    GameObject instant = Instantiate(Steppartical, targetPos, Quaternion.Euler(Steppartical.transform.rotation.eulerAngles), StepparticalParent);
                    ParticleSystem PS = instant.GetComponent<ParticleSystem>();
                    PS.Play();
                    float PSduration = PS.duration + PS.startLifetime;                 
                    Destroy(instant, PSduration);
                }
                else
                {
                    NewPos = RayOrigin.position - (MaxFootReach * Vector3.up) + (FootYOffset * Vector3.up);
                }
            }
        }
        else
        {
            // If not moving, set to idle position
            if (FTRayIDL)
            {
                NewPos = Vector3.up * FootYOffset + FTIDLE.point;
                oldPos = NewPos;
            }
            else
            {
                NewPos = RayOrigin.position - (MaxFootReach * Vector3.up) + (FootYOffset * Vector3.up);
                oldPos = NewPos;
            }
        }

        // Move foot to the calculated position
        foot.position = Vector3.Lerp(oldPos, NewPos, FootSpeed);

        // Debug visualization
        if (IKDebug)
        {
            Debug.DrawLine(IKRaycastOrigin.position, FTHipPos, Color.red); // Draw line for the adjusted position
            Debug.DrawLine(FTHipPos , FTHitDown.point, Color.red); // Draw line for the adjusted downward position
        }
    }
}

heres a video of my code so far in play
https://www.youtube.com/watch?v=9B5njXeZdLg

You're getting close.

You need to have a flag for each leg that is being moved, and a calculation of "how far behind the intended direction" each foot is at any given time. If the foot is behind the body, then it's more likely to need to take a step. While a leg is in motion, it needs set the flag while it completes its own stride, hopefully ending up being ahead of the body in the intended direction. When a leg is planted on the ground, the flag clears to say it's supporting the body. However, before a leg can start a stride from the "behind" position to the "ahead" position, the whole body has to decide if it has enough other legs already ahead of the body.

For a biped, you should always have a foot on the ground unless you're running, and when you're running, you can't start a leg striding forward until the other leg is almost done with its stride. For a spider, you can decide that the spider needs five legs firmly touching surfaces before it's allowed to move a leg forward. When the spider realizes it's free to move a leg, it should choose the one that is most "behind" the direction of travel from its neutral pose.

1 Like

updated the code and removed some unnecessary bits

using UnityEngine;

public class footIK : MonoBehaviour
{
    // Debug flags
    [SerializeField] private bool groundDebug;
    [SerializeField] private bool IKDebug;

    // References
    [Header("References")]
    [SerializeField] private GameObject Steppartical;
    [SerializeField] private Transform StepparticalParent;

    // Ground check parameters
    [Header("GroundCheck")]
    [SerializeField] private LayerMask groundLayer;
    [SerializeField] private Transform groundRaycastOrigin;
    [SerializeField] private float groundRaycastDistance = 0.1f, groundRaycastRadius = 0.1f;

    // Body movement parameters
    [Header("Body")]
    [SerializeField] private Transform Body;
    [SerializeField] private float BodyHight;
    [SerializeField] private float BodyMoveSpeed;

    // Foot IK Visuals parameters
    [Header("Foot IK Visuals")]
    [SerializeField] private float FootSpeed;

    // Foot IK parameters
    [Header("Foot IK")]
    [SerializeField] private Transform LeftFootTarget;
    [SerializeField] private Transform RightFootTarget;
    [SerializeField] private Transform IKRaycastOrigin;
    [SerializeField] private Transform LeftFootRaycastOrigin;
    [SerializeField] private Transform RightFootRaycastOrigin;
    [SerializeField] private float StepDistance;
    [SerializeField] private float StepLength;
    [SerializeField] private float MaxFootReach;
    [SerializeField] private float FootYOffset;
    [SerializeField] private LayerMask IKLayer;

    Vector3 NewPosR, NewPosL;
    Vector3 FTTargetPosR, FTTargetPosL;

    // Update is called once per frame
    void Update()
    {
        FootIK(LeftFootRaycastOrigin, LeftFootTarget, ref NewPosR, ref FTTargetPosR);
        FootIK(RightFootRaycastOrigin, RightFootTarget, ref NewPosL, ref FTTargetPosL);

        // Calculate and update the position of the Body based on foot positions
        Vector3 bodyTarget = ((FTTargetPosR + FTTargetPosL) / 2) + (Vector3.up * BodyHight) - (Body.forward * StepLength);

        Body.position = Vector3.Lerp(Body.position, bodyTarget, BodyMoveSpeed * Time.deltaTime);
    }

    // Initialize variables
    void Start()
    {
        NewPosL = FTTargetPosL;
        NewPosR = FTTargetPosR;
    }

    // Function to perform Foot IK
    void FootIK(Transform RayOrigin, Transform foot, ref Vector3 NewPos, ref Vector3 targetPos)
    {  
        // Raycast forward to find the adjusted position for the foot
        RaycastHit FTHitForward;
        bool FTRayForward = Physics.Raycast(RayOrigin.position, RayOrigin.forward, out FTHitForward, StepLength, IKLayer);

        Vector3 FTHipPos;

        if (FTRayForward)
        {
            FTHipPos = FTHitForward.point; // Set position to the forward hit position
        }
        else
        {
            FTHipPos = RayOrigin.position + StepLength * IKRaycastOrigin.forward; // Set position to default forward position
        }

        // Raycast downward to find the adjusted position for the foot on the ground
        RaycastHit FTHitDown;
        RaycastHit FTIDLE;
        bool FTRayDown = Physics.Raycast(FTHipPos, Vector3.down, out FTHitDown, MaxFootReach, IKLayer);
        bool FTRayIDL = Physics.Raycast(RayOrigin.position, Vector3.down, out FTIDLE, MaxFootReach, IKLayer);

        // If foot isn't on the ground, go to a default state
        if (FTRayDown)
        {
            targetPos = Vector3.up * FootYOffset + FTHitDown.point;
        }
        else
        {
            targetPos = Vector3.up * (FootYOffset - MaxFootReach) + FTHipPos;
        }

        float ftDist = Vector3.Distance(NewPos, targetPos);

        // Check if the character is moving
        if (InputManager.Manager.GetMovementInput().magnitude > 0)
        {
            if (ftDist > StepDistance)
            {
                NewPos = targetPos;

                // Spawn particle underneath feet
                if (FTRayDown)
                {
                    GameObject instant = Instantiate(Steppartical, targetPos, Quaternion.Euler(Steppartical.transform.rotation.eulerAngles), StepparticalParent);
                    ParticleSystem PS = instant.GetComponent<ParticleSystem>();
                    PS.Play();
                    float PSduration = PS.duration + PS.startLifetime;                   
                    Destroy(instant, PSduration);
                }
                else
                {
                    NewPos = RayOrigin.position - (MaxFootReach * Vector3.up) + (FootYOffset * Vector3.up);
                }
            }
        }
        else
        {
            // If not moving, set to idle position
            if (FTRayIDL)
            {
                NewPos = Vector3.up * FootYOffset + FTIDLE.point;
            }
            else
            {
                NewPos = RayOrigin.position - (MaxFootReach * Vector3.up) + (FootYOffset * Vector3.up);
            }
        }

        // Move foot to the calculated position
        foot.position = NewPos;

        // Debug visualization
        if (IKDebug)
        {
            Debug.DrawLine(IKRaycastOrigin.position, FTHipPos, Color.red); // Draw line for the adjusted position
            Debug.DrawLine(FTHipPos , FTHitDown.point, Color.red); // Draw line for the adjusted downward position
        }
    }
}

alright i know its been a bit, I've been busy with school and extra curricular activity but I think I've figured it out

using UnityEngine;

public class footIK : MonoBehaviour
{
    // Debug flags
    [SerializeField] private bool groundDebug;
    [SerializeField] private bool IKDebug;

    // References
    [Header("References")]
    [SerializeField] private GameObject Steppartical;
    [SerializeField] private Transform StepparticalParent;

    // Ground check parameters
    [Header("GroundCheck")]
    [SerializeField] private LayerMask groundLayer;
    [SerializeField] private Transform groundRaycastOrigin;
    [SerializeField] private float groundRaycastDistance = 0.1f, groundRaycastRadius = 0.1f;

    // Body movement parameters
    [Header("Body")]
    [SerializeField] private Transform Body;
    [SerializeField] private float BodyHight;
    [SerializeField] private float BodyMoveSpeed;

    // Foot IK Visuals parameters
    [Header("Foot IK Visuals")]
    [SerializeField] private float FootSpeed;
    [SerializeField] private float StepHight;

    // Foot IK parameters
    [Header("Foot IK")]
    [SerializeField] private Transform LeftFootTarget;
    [SerializeField] private Transform RightFootTarget;
    [SerializeField] private Transform IKRaycastOrigin;
    [SerializeField] private Transform LeftFootRaycastOrigin;
    [SerializeField] private Transform RightFootRaycastOrigin;
    [SerializeField] private float StepDistance;
    [SerializeField] private float StepLength;
    [SerializeField] private float MaxFootReach;
    [SerializeField] private float FootYOffset;
    [SerializeField] private LayerMask IKLayer;

    bool isGrounded;
    RaycastHit Groundhit;

    Vector3 oldPosR, oldPosL;
    Vector3 NewPosR, NewPosL;
    Vector3 CurrentPosL, CurrentPosR;
    Vector3 FTTargetPosR, FTTargetPosL;
    float LLerp = 0f, RLerp = .5f;
    bool RightFootStep = true, LeftFootStep = false;

    // Update is called once per frame
    void Update()
    {
        FootIK(LeftFootRaycastOrigin, LeftFootTarget, ref NewPosR, ref oldPosR, ref FTTargetPosR, LeftFootStep, ref RightFootStep, ref LLerp, ref CurrentPosR);
        FootIK(RightFootRaycastOrigin, RightFootTarget, ref NewPosL, ref oldPosL, ref FTTargetPosL, RightFootStep, ref LeftFootStep, ref RLerp,ref CurrentPosL);

        // Perform spherecast downwards to check for ground contact
        isGrounded = Physics.SphereCast(groundRaycastOrigin.position, groundRaycastRadius, Vector3.down, out Groundhit, groundRaycastDistance, groundLayer);

        // Calculate and update the position of the Body based on foot positions
        Vector3 bodyTarget = ((FTTargetPosR + FTTargetPosL) / 2) + (Vector3.up * BodyHight) - (Body.forward * StepLength);

        if (isGrounded)
            Body.position = Vector3.Lerp(Body.position, bodyTarget, BodyMoveSpeed * Time.deltaTime);
    }

    // Initialize variables
    void Start()
    {
        FootIK(LeftFootRaycastOrigin, LeftFootTarget, ref NewPosR, ref oldPosR, ref FTTargetPosR, LeftFootStep, ref RightFootStep, ref LLerp, ref CurrentPosR);
        FootIK(RightFootRaycastOrigin, RightFootTarget, ref NewPosL, ref oldPosL, ref FTTargetPosL, RightFootStep, ref LeftFootStep, ref RLerp, ref CurrentPosL);

        NewPosL = NewPosR = oldPosL = oldPosR = FTTargetPosL;
    }

    // Function to perform Foot IK
    void FootIK(Transform RayOrigin, Transform foot, ref Vector3 NewPos, ref Vector3 oldPos, ref Vector3 targetPos,bool AltFTStep, ref bool FTStep,ref float lerp, ref Vector3 currentPos)
    {
        // Raycast forward to find the adjusted position for the foot
        RaycastHit FTHitForward;
        bool FTRayForward = Physics.Raycast(RayOrigin.position, RayOrigin.forward, out FTHitForward, StepLength, IKLayer);

        Vector3 FTHipPos;

        if (FTRayForward)
        {
            FTHipPos = FTHitForward.point; // Set position to the forward hit position
        }
        else
        {
            FTHipPos = RayOrigin.position + StepLength * IKRaycastOrigin.forward; // Set position to default forward position
        }

        // Raycast downward to find the adjusted position for the foot on the ground
        RaycastHit FTHitDown;
        RaycastHit FTIDLE;
        bool FTRayDown = Physics.Raycast(FTHipPos, Vector3.down, out FTHitDown, MaxFootReach, IKLayer);
        bool FTRayIDL = Physics.Raycast(RayOrigin.position, Vector3.down, out FTIDLE, MaxFootReach, IKLayer);

        // If foot isn't on the ground, go to a default state
        if (FTRayDown)
        {
            targetPos = Vector3.up * FootYOffset + FTHitDown.point;
        }
        else
        {
            targetPos = Vector3.up * (FootYOffset - MaxFootReach) + FTHipPos;
        }

        float ftDist = Vector3.Distance(oldPos, targetPos);

        // Check if the character is moving
        if (FTRayDown && !AltFTStep)
        {
            if (ftDist > StepDistance && lerp > 1)
            {
                FTStep = true;

                NewPos = targetPos;

                // Spawn particle underneath feet
                GameObject instant = Instantiate(Steppartical, targetPos, Quaternion.Euler(Steppartical.transform.rotation.eulerAngles), StepparticalParent);
                ParticleSystem PS = instant.GetComponent<ParticleSystem>();
                PS.Play();
                float PSduration = PS.duration + PS.startLifetime;
                Destroy(instant, PSduration);
            }
            else
            {
                FTStep = false;
            }

            if (lerp < 1)
            {
                currentPos = Vector3.Lerp(foot.position, NewPos, lerp);

                lerp += Time.deltaTime * FootSpeed;
            }
            else
            {
                oldPos = NewPos;

                lerp = 0;
            }

            foot.position = currentPos;
        }

        // Debug visualization
        if (IKDebug)
        {
            Debug.DrawLine(IKRaycastOrigin.position, FTHipPos, Color.red); // Draw line for the adjusted position
            Debug.DrawLine(FTHipPos, FTHitDown.point, Color.red); // Draw line for the adjusted downward position
        }
    }
}

theres a few bugs like going up the stairs looks weird and going down slopes looks off plus jumping but I think that can be sorted out easily

nvm it doesnt work but im on to something

using UnityEngine;

public class footIK : MonoBehaviour
{
    // Debug flags
    [Header("Debug")]
    [SerializeField] private bool groundDebug; // Enable ground debug visualization
    [SerializeField] private bool ikDebug; // Enable IK debug visualization

    // References
    [Header("References")]
    [SerializeField] private GameObject stepParticle; // Particle effect for footstep
    [SerializeField] private Transform stepParticleParent; // Parent object for step particles
    [SerializeField] private PlayerMovement movement; // Reference to the player movement script

    // Ground check parameters
    [Header("Ground Check")]
    [SerializeField] private LayerMask groundLayer; // Layer mask for ground objects
    [SerializeField] private Transform groundRaycastOrigin; // Origin of ground raycasts
    [SerializeField] private float groundRaycastDistance = 0.1f; // Distance of ground raycasts
    [SerializeField] private float groundRaycastRadius = 0.1f; // Radius of ground raycasts

    // Body movement parameters
    [Header("Body")]
    [SerializeField] private Transform body; // Reference to the body transform
    [SerializeField] private float bodyHeight; // Height of the body above the ground
    [SerializeField] private float bodyMoveSpeed; // Speed of body movement

    // Foot IK Visuals parameters
    [Header("Foot IK Visuals")]
    [SerializeField] private float footSpeed; // Speed of foot movement
    [SerializeField] private float stepHeight; // Height of footstep

    // Foot IK parameters
    [Header("Foot IK")]
    [SerializeField] private Transform leftFootTarget; // Target transform for left foot
    [SerializeField] private Transform rightFootTarget; // Target transform for right foot
    [SerializeField] private Transform ikRaycastOrigin; // Origin of IK raycasts
    [SerializeField] private Transform leftFootRaycastOrigin; // Origin of raycast for left foot
    [SerializeField] private Transform rightFootRaycastOrigin; // Origin of raycast for right foot
    [SerializeField] private float stepDistance; // Distance threshold for footstep
    [SerializeField] private float stepLength; // Length of each step
    [SerializeField] private float maxFootReach; // Maximum reach of the foot
    [SerializeField] private float footYOffset; // Vertical offset of the foot
    [SerializeField] private LayerMask ikLayer; // Layer mask for IK objects

    // Private variables
    private bool isGrounded; // Whether the player is grounded
    private RaycastHit groundHit; // Information about the ground hit

    private Vector3 oldPosRight, oldPosLeft; // Previous positions of the feet
    private Vector3 newPosRight, newPosLeft; // New positions of the feet
    private Vector3 currentPosLeft, currentPosRight; // Current positions of the feet
    private Vector3 footTargetPosRight, footTargetPosLeft; // Target positions for the feet
    private float lerpLeft = 0f, lerpRight = .5f; // Lerp values for smooth foot movement
    private bool rightFootStep = true, leftFootStep = false; // Flags for alternating footstep

    // Update is called once per frame
    void Update()
    {
        // Perform foot IK for left and right feet
        FootIK(leftFootRaycastOrigin, leftFootTarget, ref newPosRight, ref oldPosRight, ref footTargetPosRight, leftFootStep, ref rightFootStep, ref lerpLeft, ref currentPosRight);
        FootIK(rightFootRaycastOrigin, rightFootTarget, ref newPosLeft, ref oldPosLeft, ref footTargetPosLeft, rightFootStep, ref leftFootStep, ref lerpRight, ref currentPosLeft);

        // Perform ground check
        isGrounded = Physics.SphereCast(groundRaycastOrigin.position, groundRaycastRadius, Vector3.down, out groundHit, groundRaycastDistance, groundLayer);

        // Calculate and update the position of the body based on foot positions
        Vector3 bodyTarget = ((footTargetPosRight + footTargetPosLeft) / 2) + (Vector3.up * bodyHeight) - (body.forward * stepLength);

        // Move the body towards the calculated target position
        if (isGrounded)
            body.position = Vector3.Lerp(body.position, bodyTarget, bodyMoveSpeed * Time.deltaTime);
    }

    // Start is called before the first frame update
    void Start()
    {
        // Initialize foot positions
        FootIK(leftFootRaycastOrigin, leftFootTarget, ref newPosRight, ref oldPosRight, ref footTargetPosRight, leftFootStep, ref rightFootStep, ref lerpLeft, ref currentPosRight);
        FootIK(rightFootRaycastOrigin, rightFootTarget, ref newPosLeft, ref oldPosLeft, ref footTargetPosLeft, rightFootStep, ref leftFootStep, ref lerpRight, ref currentPosLeft);

        // Set initial foot positions
        newPosLeft = newPosRight = oldPosLeft = oldPosRight = footTargetPosLeft;
    }

    // Function to perform Foot IK
    void FootIK(Transform rayOrigin, Transform foot, ref Vector3 newPos, ref Vector3 oldPos, ref Vector3 targetPos, bool altFootStep, ref bool footStep, ref float lerp, ref Vector3 currentPos)
    {
        // Raycast forward to find the adjusted position for the foot
        RaycastHit hitForward;
        bool rayForward = Physics.Raycast(rayOrigin.position, rayOrigin.forward, out hitForward, stepLength, ikLayer);
        Vector3 hipPos = rayForward ? hitForward.point : rayOrigin.position + stepLength * ikRaycastOrigin.forward;

        // Raycast downward to find the adjusted position for the foot on the ground
        RaycastHit hitDown;
        bool rayDown = Physics.Raycast(hipPos, Vector3.down, out hitDown, maxFootReach, ikLayer);

        // Calculate target position for the foot
        targetPos = rayDown ? Vector3.up * footYOffset + hitDown.point : Vector3.up * (footYOffset - maxFootReach) + hipPos;

        // Check if foot has moved beyond step distance threshold
        float footDistance = Vector3.Distance(oldPos, targetPos);
        if (footDistance > stepDistance)
            newPos = targetPos;

        // Smoothly move the foot towards the target position
        if (lerp < 1 && !altFootStep)
        {
            footStep = true;
            currentPos = Vector3.Lerp(foot.position, newPos, lerp);
            lerp += Time.deltaTime * footSpeed;
        }
        else
        {
            footStep = false;
            oldPos = newPos;
            lerp = 0;
        }

        // Calculate vertical offset for footstep
        float yStep = movement.movement.magnitude >= 1 ? Mathf.Sin(lerp) * stepHeight : 0;
        foot.position = currentPos + Vector3.up * yStep;

        // Debug visualization
        if (ikDebug)
        {
            Debug.DrawLine(ikRaycastOrigin.position, hipPos, Color.red); // Draw line for the adjusted position
            Debug.DrawLine(hipPos, hitDown.point, Color.red);
        }
    }
}

I figured it out :)

the feet just jitter a bit when its moving up and down

using UnityEngine;

public class footIK : MonoBehaviour
{
    // Debug flags
    [Header("Debug")]
    [SerializeField] private bool groundDebug; // Enable ground debug visualization
    [SerializeField] private bool ikDebug; // Enable IK debug visualization

    // References
    [Header("References")]
    [SerializeField] private PlayerMovement movement; // Reference to the player movement script

    // Ground check parameters
    [Header("Ground Check")]
    [SerializeField] private LayerMask groundLayer; // Layer mask for ground objects
    [SerializeField] private Transform groundRaycastOrigin; // Origin of ground raycasts
    [SerializeField] private float groundRaycastDistance = 0.1f; // Distance of ground raycasts
    [SerializeField] private float groundRaycastRadius = 0.1f; // Radius of ground raycasts

    // Body movement parameters
    [Header("Body")]
    [SerializeField] private Transform body; // Reference to the body transform
    [SerializeField] private float bodyHeight; // Height of the body above the ground
    [SerializeField] private float bodyMoveSpeed; // Speed of body movement

    // Foot IK Visuals parameters
    [Header("Foot IK Visuals")]
    [SerializeField] private float footSpeed; // Speed of foot movement
    [SerializeField] private float stepHeight; // Height of footstep

    // Foot IK parameters
    [Header("Foot IK")]
    [SerializeField] private Transform leftFootTarget; // Target transform for left foot
    [SerializeField] private Transform rightFootTarget; // Target transform for right foot
    [SerializeField] private Transform ikRaycastOrigin; // Origin of IK raycasts
    [SerializeField] private Transform leftFootRaycastOrigin; // Origin of raycast for left foot
    [SerializeField] private Transform rightFootRaycastOrigin; // Origin of raycast for right foot
    [SerializeField] private float stepDistance; // Distance threshold for footstep
    [SerializeField] private float stepLength; // Length of each step
    [SerializeField] private float maxFootReach; // Maximum reach of the foot
    [SerializeField] private float footYOffset; // Vertical offset of the foot
    [SerializeField] private LayerMask ikLayer; // Layer mask for IK objects

    // Private variables
    private bool isGrounded; // Whether the player is grounded
    private RaycastHit groundHit; // Information about the ground hit

    private Vector3 oldPosRight, oldPosLeft; // Previous positions of the feet
    private Vector3 newPosRight, newPosLeft; // New positions of the feet
    private Vector3 currentPosLeft, currentPosRight; // Current positions of the feet
    private Vector3 footTargetPosRight, footTargetPosLeft; // Target positions for the feet
    private float lerpLeft = 0f, lerpRight = .5f; // Lerp values for smooth foot movement
    private bool rightFootStep = true, leftFootStep = false; // Flags for alternating footstep

    // Update is called once per frame
    void Update()
    {
        // Perform foot IK for left and right feet
        FootIK(leftFootRaycastOrigin, leftFootTarget, ref newPosRight, ref oldPosRight, ref footTargetPosRight, leftFootStep, ref rightFootStep, ref lerpLeft, ref currentPosRight);
        FootIK(rightFootRaycastOrigin, rightFootTarget, ref newPosLeft, ref oldPosLeft, ref footTargetPosLeft, rightFootStep, ref leftFootStep, ref lerpRight, ref currentPosLeft);

        // Perform ground check
        isGrounded = Physics.SphereCast(groundRaycastOrigin.position, groundRaycastRadius, Vector3.down, out groundHit, groundRaycastDistance, groundLayer);

        // Calculate and update the position of the body based on foot positions
        Vector3 bodyTarget = ((footTargetPosRight + footTargetPosLeft) / 2) + (Vector3.up * bodyHeight) - (body.forward * stepLength);

        // Move the body towards the calculated target position
        if (isGrounded)
            body.position = Vector3.Lerp(body.position, bodyTarget, bodyMoveSpeed * Time.deltaTime);
    }

    // Start is called before the first frame update
    void Start()
    {
        // Initialize foot positions
        FootIK(leftFootRaycastOrigin, leftFootTarget, ref newPosRight, ref oldPosRight, ref footTargetPosRight, leftFootStep, ref rightFootStep, ref lerpLeft, ref currentPosRight);
        FootIK(rightFootRaycastOrigin, rightFootTarget, ref newPosLeft, ref oldPosLeft, ref footTargetPosLeft, rightFootStep, ref leftFootStep, ref lerpRight, ref currentPosLeft);

        // Set initial foot positions
        newPosLeft = newPosRight = oldPosLeft = oldPosRight = footTargetPosLeft;
    }

    // Function to perform Foot IK
    void FootIK(Transform rayOrigin, Transform foot, ref Vector3 newPos, ref Vector3 oldPos, ref Vector3 targetPos, bool altFootStep, ref bool footStep, ref float lerp, ref Vector3 currentPos)
    {
        // Raycast forward to find the adjusted position for the foot
        RaycastHit hitForward;
        bool rayForward = Physics.Raycast(rayOrigin.position, rayOrigin.forward, out hitForward, stepLength, ikLayer);
        Vector3 hipPos = rayForward ? hitForward.point : rayOrigin.position + stepLength * ikRaycastOrigin.forward;

        // Raycast downward to find the adjusted position for the foot on the ground
        RaycastHit hitDown;
        bool rayDown = Physics.Raycast(hipPos, Vector3.down, out hitDown, maxFootReach, ikLayer);

        // Calculate target position for the foot
        targetPos = rayDown ? Vector3.up * footYOffset + hitDown.point : Vector3.up * (footYOffset - maxFootReach) + hipPos;

        // Check if foot has moved beyond step distance threshold
        float footDistance = Vector3.Distance(oldPos, targetPos);
        if (footDistance > stepDistance)
            newPos = targetPos;

        // Smoothly move the foot towards the target position
        if (lerp < 1 && !altFootStep)
        {
            footStep = true;
            currentPos = Vector3.Lerp(foot.position, newPos, lerp);
            lerp += Time.deltaTime * footSpeed;
        }
        else
        {
            footStep = false;
            oldPos = newPos;
            lerp = 0;
        }

        // Calculate vertical offset for footstep
        float yStep = movement.movement.magnitude >= 1 ? (0.5f*(1 + Mathf.Sin(2 * Mathf.PI * lerp))) * stepHeight : 0;
        foot.position = currentPos + Vector3.up * yStep;

        // Debug visualization
        if (ikDebug)
        {
            Debug.DrawLine(ikRaycastOrigin.position, hipPos, Color.red); // Draw line for the adjusted position
            Debug.DrawLine(rayOrigin.position, hitDown.point, Color.red);
        }
    }
}

all done

First time really responding on a unity forum post so sorry if this necroposts or something
I’ve found a really good alternative to be (at least for 2 legs) calculating the average position between the left and right foot. If the average is too far away from the body, take a step with the leg that’s furthest away from the body
This consistently provides 2 alternating feet because when the furthest leg moves forwards the average also moves forwards. Stepping is 100% consistent. I’ve found offsetting the ray direction by the velocity of the rigidbody to make up for speed is good and makes it look like it’s running

Tip: I would only compare the X and Z. I’ve found the Y has way too much influence

Thanks for the tip! This is yielding much better results than my existing solution. For anyone confused by the meaning of “average position” like I was, I believe this is what they’re talking about.

    Vector3 rightDifference = (rightFoot.transform.position - bodyTransform.position);
    rightDifference.y = 0;

    Vector3 leftDifference = (leftFoot.transform.position - bodyTransform.position);
    leftDifference.y = 0;

    float avgDistance = (rightDifference.magnitude + leftDifference.magnitude)/2;

    //if avgDistance is greater than threshold, move furthest foot from body
1 Like