top of page

Making Spiders With Procedural Animation




For my next challenge, I thought it would be cool to make a spider-like insect walk by using procedural animation. Before starting, I did a bit of research, and found out that in the newer versions of Unity, there is a downloadable package specifically made for Inverse Kinematics, which would be very useful while getting the spider to work.


So the first thing I did was download the "Animation Rigging" package in the Unity Registry tab from the Package Manager window.



Doing so gave me a new drop down menu called "Animation Rigging" which has all the tools needed to setup the inverse kinematics.









Before I could go on, I needed a a rigged leg to work with. So in Blender, I made a simple leg with 3 separate segments, and added a bone to each of them, allowing the leg to be bent where each segment ends. I then exported it as and FBX, and imported it to Unity.



The next few steps are setting up inverse kinematics on the leg...


While the object with all the bones is selected, go to the "Animation Rigging" dropdown menu and select "Bone Renderer Setup". This will detect the bones in the prefab, and add the "Bone Renderer" script to the object.



While that same object is selected (the object with all the bones), go back to the "Animation Rigging" dropdown and select "Rig Setup". This should do a few things:

  1. Add an "Animator" Component to the object.

  2. Add a "Rig Builder" Component to the object.

  3. Create a child object called "Rig 1" and add the "Rig" Component to it.



On the new "Rig 1" object, I created an empty game object child, and called it "FootIK", and add the "Two Bone IK Constraint" Component. Here, we can set up the rig by adding the Root, Mid and Tip bones, while also setting a Target, and Hint transforms to guide the leg in the right direction.



Although, there is a slight problem. The leg has 3 bones, not 2. This was done on purpose, since the leg only needs 2 out of the 3 bones to have IK to work. The last segment of the leg would always be facing downward, towards the grounds, while the other bones will move around.



Knowing this, I simply added the second last bone to the "Tip" slot, right-clicked the component, and selected "Auto Setup From Tip Transform". This will then automatically get the rest of the bines from the rest of the leg, while also creating 2 more gameobjects, "FootIK_target" and "FootIK_hint" and inserts them into their proper slot on the component. These objects will be used to guide the IK movement in the proper direction.



Now with everything setup, I just needed to position the target and hint transforms in the right position, and the IK was working as it should.



The next thing to work on was making these legs move on their own. First, I made a spider-like insect using a block, and 6 legs.



The main idea is in the following rules:

  1. Stick the foot to the ground.

  2. Raycast to the ground from the body.

  3. When the distance between the foot and the desired position (raycast point) is too large, set the position of the foot to the desired position.

  4. Repeat.



I made a script called "LegIKController", and started implementing the functionality.

In Update(), I raycast from a transform attached to the body, straight down, and make sure I am only searching for objects with the tag "Floor". Set the desired position to the hit point.


//Raycast to find the best floor position for the foot
        RaycastHit hit;
        if (Physics.Raycast(desiredPos.position, transform.rotation * Vector3.down, out hit, 100, 1 << LayerMask.NameToLayer("Floor")))
        {
            if (hit.collider)
            {
                bestDesiredPos = hit.point;
            }
        }


The target transform is where the tip bone will always be. Although the rig thinks the tip bone is the second last bone. Meaning if we just set the target position to the desired position, half the leg will be in the floor, which is obviously undesirable. To resolve this, I calculate the distance between the second last, and tip bone, and add the difference to the desired position. The next 2 lines of code take care of this.


        //Calculate the difference between the last 2 bones.
        lastBoneDiff = midJoint.position - tip.position;

        //Set the target to the foot position, and offset it by the pos of the first joint.
        m_target.position = FootPos.position + lastBoneDiff;


I also make sure to always set the foot position to the world position, instead of the local position. This also allows me to only set the "worldFootPos" variable to move the foot of the leg.


//Set the target to the foot position, and offset it by the pos of the first joint.
        m_target.position = FootPos.position + lastBoneDiff;


Next, I check the distance between the current foot position and the desired position, and if the result is too large, start a lerping coroutine.


if (Vector3.Distance(bestDesiredPos, FootPos.position) >= maxLerpDistance)
        {
            if (!isLerping)
            {
                isLerping = true;
                StartCoroutine(MoveLeg());
            }
        }


For the coroutine, I simply lerped from the original foot position to the new desired position, and added a sine wave to the y position to make it look like its actually walking.


    IEnumerator MoveLeg()
    {
        float waitTime = lerpSpeed;
        float elapsedTime = 0;

        Vector3 originalPos = worldFootPos;
        Vector3 newPos = bestDesiredPos;

        while(elapsedTime < waitTime)
        {
            //Lerp the pos of the leg
            Vector3 pos = Vector3.Lerp(originalPos, newPos, elapsedTime / waitTime);
            //Add sine wave to the y of the new pos
            pos.y += Mathf.Sin(elapsedTime / waitTime * Mathf.PI);

            worldFootPos = pos;

            elapsedTime += Time.deltaTime;
            yield return null;
        }

        isLerping = false;

        yield return null;
    }


And that's most of the leg functionality implemented. I just needed to take care of the main body, and how it will react when crawling over obstacles and up walls. I made a script called "SpiderController" and added the following code in the Update() function.


    void Update()
    {
        //Ryacst to the floor form the spiders position
        RaycastHit hit;
        if (Physics.Raycast(transform.position, transform.rotation * Vector3.down, out hit, 100, 1 << LayerMask.NameToLayer("Floor")))
        {
            //Lerp the rotation
            transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation, Time.deltaTime * lerpSpeed);

            //Offset the position by the height variable
            Vector3 pos;
            pos = (hit.normal * height) + hit.point;

            //Lerp the position
            transform.position = Vector3.Lerp(transform.position, pos, Time.deltaTime * lerpSpeed);
        }

        if (isMoving)
        {
            transform.Translate(transform.forward * Time.deltaTime * moveSpeed, Space.World);
        }
    }

This piece of code does a few simple things.

  1. Set the rotation of the body to the normal of whatever is underneath it.

  2. Set the body a certain height above the ground.

  3. Lerp the position, so it doesn't jitter to its new position when discovering an obstacle.

  4. And finally, move the spider in it's forward direction.


Complete LegIKController.cs Script


public class LegIKController : MonoBehaviour
{
    bool isLerping;
    Vector3 lastBoneDiff;
    Vector3 worldFootPos;
    Vector3 bestDesiredPos;

    [Tooltip("The position of the foot (tip bone)")]
    [SerializeField] Transform FootPos;
    [Tooltip("The IK target")]
    [SerializeField] Transform m_target;

    [Tooltip("The bone before the tip")]
    [SerializeField] Transform midJoint;
    [Tooltip("The tip bone")]
    [SerializeField] Transform tip;

    [Tooltip("The desired position for the leg")]
    [SerializeField] Transform desiredPos;

    [Tooltip("The distance between the foot and the desired pos for the foot to lerp.")]
    public float maxLerpDistance = 1.5f;
    [Tooltip("The lerp speed of the leg movement.")]
    [SerializeField] float lerpSpeed = 0.5f;

    private void Start()
    {
        worldFootPos = FootPos.position;
    }

    // Update is called once per frame
    void Update()
    {
        //Raycast to find the best floor position for the foot
        RaycastHit hit;
        if (Physics.Raycast(desiredPos.position, transform.rotation * Vector3.down, out hit, 100, 1 << LayerMask.NameToLayer("Floor")))
        {
            if (hit.collider)
            {
                bestDesiredPos = hit.point;
            }
        }

        //Always set the foot position to the world pos
        FootPos.position = worldFootPos;

        //Calculate the difference between the last 2 bones.
        lastBoneDiff = midJoint.position - tip.position;

        //Set the target to the foot position, and offset it by the pos of the first joint.
        m_target.position = FootPos.position + lastBoneDiff;

        if (Vector3.Distance(bestDesiredPos, FootPos.position) >= maxLerpDistance)
        {
            if (!isLerping)
            {
                isLerping = true;
                StartCoroutine(MoveLeg());
            }
        }
    }

    IEnumerator MoveLeg()
    {
        float waitTime = lerpSpeed;
        float elapsedTime = 0;

        Vector3 originalPos = worldFootPos;
        Vector3 newPos = bestDesiredPos;

        while(elapsedTime < waitTime)
        {
            //Lerp the pos of the leg
            Vector3 pos = Vector3.Lerp(originalPos, newPos, elapsedTime / waitTime);
            //Add sine wave to the y of the new pos
            pos.y += Mathf.Sin(elapsedTime / waitTime * Mathf.PI);

            worldFootPos = pos;

            elapsedTime += Time.deltaTime;
            yield return null;
        }

        isLerping = false;

        yield return null;
    }

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawSphere(bestDesiredPos, 0.5f);
    }
}


Complete SpiderController.cs Script


public class SpiderController : MonoBehaviour
{
    [SerializeField] float height = 1.0f;
    [SerializeField] float moveSpeed = 3.0f;
    [SerializeField] float lerpSpeed = 3.0f;

    [SerializeField] bool isMoving;

    // Update is called once per frame
    void Update()
    {
        //Ryacst to the floor form the spiders position
        RaycastHit hit;
        if (Physics.Raycast(transform.position, transform.rotation * Vector3.down, out hit, 100, 1 << LayerMask.NameToLayer("Floor")))
        {
            //Lerp the rotation
            transform.rotation = Quaternion.Lerp(transform.rotation, Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation, Time.deltaTime * lerpSpeed);

            //Offset the position by the height variable
            Vector3 pos;
            pos = (hit.normal * height) + hit.point;

            //Lerp the position
            transform.position = Vector3.Lerp(transform.position, pos, Time.deltaTime * lerpSpeed);
        }

        if (isMoving)
        {
            transform.Translate(transform.forward * Time.deltaTime * moveSpeed, Space.World);
        }
    }
}


And that is basically it. There is plenty of room for improvement, but they work pretty well, and the result is quite fun to watch.





bottom of page