Crouching and Sprinting with the Character Controller and Animator

Before we start, it's recommended that you have a Character fully set up in the Animator.

I have, in the above example:

  • A Blend Tree with two parameters and two Blend Trees attached.

  • A crouching and a standing Blend Tree, with idle, forward and backward animations for each.

  • A sprint animation state on the standing Blend Tree.

Set your character up before continuing.

As we develop more sophisticated characters in our games, we may need to traverse between multiple different animation states. For this, we can use a programming structure called a finite state machine. These finite states could be crouching, sprinting, carrying an object, pushing/pulling an object, etc. Essentially they describe a discreet state our character can be in (and therefore not in any other states).

We can express these different states using an enum.

As we mentioned previously on the Gitbook, enums are a method of creating your own variable with your own discreet options that it can contain. It's like inventing your own boolean, but instead of only storing either true or false, you define what it can contain. Let's take a look at the syntax:

   enum animStates 
    {
        normal, sprinting, crouching
    }
    animStates currentState = animStates.normal;

As you can see, we make an enum using the enum keyword, then name the enum, and finally define the different values that the enum can contain (normal, sprinting and crouching).

Then, we need to make an instance of the enum to put one of those states in, and set it to one of the states. We set it to the 'normal' state in my example.

Next, we need a way to transition between these states. We are going to use a 'toggle' system here, meaning that when certain conditions are met we are going to enter one of the states (like sprinting or crouching).

We could simply transition our states with keyboard input:

if (Input.GetKeyDown(KeyCode.E))
{
    currentState = animStates.sprinting;
}

if (Input.GetKeyDown(KeyCode.R))
{
    currentState = animStates.crouching;
}

if (Input.GetKeyDown(KeyCode.T))
{
    currentState = animStates.normal;
}

This is fine, but not particularly useful in a games context. This is because the conditions to move between states is usually more complicate than that. Let's take sprinting for example. We should stop sprinting when our character stops moving, or when we crouch. Let's take a look at more sophisticated way to transition between our states:

//Turn crouch on.
        if (Input.GetKeyDown(KeyCode.C) && currentState != animStates.crouching)
        {
            currentState = animStates.crouching;
        }
        
//Turn crouch off.
        else if (Input.GetKeyDown(KeyCode.C) && currentState == animStates.crouching)
        {
            currentState = animStates.normal;
        }

//Turn sprint on.
        else if (Input.GetKeyDown(KeyCode.E)  && currentState != animStates.sprinting)
        {
            currentState = animStates.sprinting;
        }
        
//Turn sprint off.
        else if (currentState == animStates.sprinting && Input.GetAxis("Vertical") <= 0)
        {
            currentState = animStates.normal;
        }

Here we have ways to move between our different animation states that's more appropriate for a game character. Specifically then:

  • We only enter crouch state when we are tapping the key 'C' and aren't already crouching.

  • If we tap the 'C' key when we are crouching, it'll stand us up (normal state) instead.

  • If we tap the 'E' key and we're not already sprinting, we enter the sprint state.

  • Finally, if we are in sprint state, but we aren't moving forwards, we can return to the normal state.

As you can see, a finite state machine is a sensible way to arrange our animations when we can only be in one state at any given time.

Next, we need to run code depending on which state we are in. Of course, this can be done with if() statements, but a more effective syntax is the switch operator:

switch (currentState)
{
    case animStates.crouching:
        //run some code.
        break;
    case animStates.sprinting:
        //run some code.
        break;
    case animStates.normal:
        //run some code.
        break;
}

The syntax of a switch is a little unusual, as it is different to an if() statement and other simple logic operations. The first thing we do after declaring the switch keyword is put a variable to check in some parenthesis:

switch (currentState)
{
//Do some code.
}

Then, in braces, we need to state if the case is in a specific state, we run some code. This is the case syntax:

    case animStates.crouching:
        //run some code.
        break;

We use the keyword case followed by essentially the state here is essentially saying "if my variable from earlier has this specific thing in it, run code". That code is declared where my comments are in the example, as in under the case declaration and before the break keyword.

For example, if we wanted to add to some number over time using in this case, we would do this:

 case animStates.crouching:
        score++;//Here, we're adding +1 every frame we are in this state, to some variable called score. This is pointless and just for reference.
        break;

What do we need to do in each state then? Let's run through this logically:

  • If I'm in the crouching state, I need to slow my character down, reduce the height and centre point of the Character Controller collider, and potentially change some float in the Animator.

  • If I'm in the sprinting state, I need to increase my character's speed, return the height and centre point of the collider to normal, and potentially modify the animator.

  • If I'm in the sprinting state, I want to return my character's speed to normal, as well as the collider.

What's worth noting here though is I want to make these changes happen gradually instead of instantly. It's easy to do this instantly, but making a variable transform into another over time is a little harder. Let's look at how you might do it instantly:

// At the top of the class...

float speedMod = 1;

//Then, inside a case in a switch.

speedMod = 2;

What if we wanted to make our player speed transition from 1 to 2 gradually, while in that state? For that, we can use the function called MoveTowards().

MoveTowards() is a function of the Mathf class. The full call is therefore Mathf.MoveTowards(). The function requires 3 arguments:

  • What are number currently is.

  • What you want the number to be.

  • How much to add-subtract each function call.

This is similar to the Mathf.Lerp() function, but we add to or subtract from our number linearly, as in the same amount every time we call the function.

Let's look at it in action:

// At the top of the class...

float speedMod = 1;

//Then, inside a case in a switch.

speedMod = Mathf.MoveTowards(speedMod, 2, 0.1f);

When we are in the state, we will make playerSpeed increase or decrease from whatever it is, towards 2 (the second argument) until we reach 2. We can move towards our target by a maximum of 0.1f each Update using the example code.

Let's implement something similar in our code:

// At the top of the class...

float speedMod = 1;

//Then, in our Update()...

switch (currentState)
{
    case animStates.crouching:
        speedMod = Mathf.MoveTowards(speedMod, 0.8f, 0.1f);
        break;
    case animStates.sprinting:
        speedMod = Mathf.MoveTowards(speedMod, 2, 0.1f);
        break;
    case animStates.normal:
        speedMod = Mathf.MoveTowards(speedMod, 1, 0.1f);
        break;
}

//Then, when calling the SimpleMove() function...

cc.SimpleMove(moveDir * speedMod);

Each of our states will gradually increase or decrease our speedMod variable over time.

Next, let's set up a float to transition between 0 and -1 in a Blend Tree. In this example, I have a blend float for managing whether I am crouching or not, called BlendY. BlendY of 0 would be standing, and BlendY of -1 means we are crouching.

We would add this like so:

// At the top of the class...

float crouchAnimManager = 0;

//Then, in our Update()...

switch (currentState)
{
    case animStates.crouching:
        speedMod = Mathf.MoveTowards(speedMod, 0.8f, 0.1f);
        crouchAnimManager = Mathf.MoveTowards(crouchAnimManager, -1, 0.01f);
        break;
    case animStates.sprinting:
        speedMod = Mathf.MoveTowards(speedMod, 2, 0.1f);
        crouchAnimManager = Mathf.MoveTowards(crouchAnimManager, 0, 0.01f);
        break;
    case animStates.normal:
        speedMod = Mathf.MoveTowards(speedMod, 1, 0.1f);
        crouchAnimManager = Mathf.MoveTowards(crouchAnimManager, 0, 0.01f);
        break;
}

//Later on in our Update()...

cc.SetFloat("BlendY", crouchAnimManager);

If we wanted to add a sprint to our Blend Tree, we could add it on an axis at a threshold of 2.

The problem here is we can't get to a threshold of 2 by default. Our Input.GetAxis() return values only range from -1 to 1. We therefore need to create a float to multiply our Input.GetAxis() return value by to get it past 1 and ultimately to 2.

// At the top of the class...

float animationSpeedMod = 1;

//Then, in our Update()...

switch (currentState)
{
    case animStates.crouching:
        speedMod = Mathf.MoveTowards(speedMod, 0.8f, 0.1f);
        crouchAnimManager = Mathf.MoveTowards(crouchAnimManager, -1, 0.01f);
        animationSpeedMod = Mathf.MoveTowards(animationSpeedMod, 1, 0.01f); 
        break;
    case animStates.sprinting:
        speedMod = Mathf.MoveTowards(speedMod, 2, 0.1f);
        crouchAnimManager = Mathf.MoveTowards(crouchAnimManager, 0, 0.01f);
        animationSpeedMod = Mathf.MoveTowards(animationSpeedMod, 2, 0.01f);
        break;
    case animStates.normal:
        speedMod = Mathf.MoveTowards(speedMod, 1, 0.1f);
        crouchAnimManager = Mathf.MoveTowards(crouchAnimManager, 0, 0.01f);
        animationSpeedMod = Mathf.MoveTowards(animationSpeedMod, 1, 0.01f);
        break;
}

//Later on in our Update()...

cc.SetFloat("BlendX", Input.GetAxis("Vertical") * animationSpeedMod);

Finally, we would ideally change the size and center of the Character Controller's collider. This is because as our character's silhouette gets smaller when we crouch, we would need to make our collider more in line with the size of the model.

We can access the center property using:

// Presuming we've already set up a Character Controller variable called cc.
cc.center

We can access the height property using:

// Presuming we've already set up a Character Controller variable called cc.
cc.height

You can easily change the height property with the MoveTowards() function as previous:

// Gradually set it to standing height.
cc.height = Mathf.MoveTowards(cc.height, 1.78f, 0.01f);

//Gradually set it to crouching height.
cc.height = Mathf.MoveTowards(cc.height, 1.3f, 0.01f);

The center is a little harder to set as it is a Vector3. The syntax looks like this:

// Change the center to the default.
cc.center = Vector3.MoveTowards(cc.center, new Vector3(0, 0.9f, 0), 0.01f);

//Change the center for a more crouched position.
cc.center = Vector3.MoveTowards(cc.center, new Vector3(0, 0.65f, 0), 0.01f);

Combining this up should allow you to build a Character Controller with three discreet states, and custom animation and movement speed for each!

Reference Script

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class Character : MonoBehaviour
{
    //Components we need.
    CharacterController cc;
    Animator anim;

    //Our movement direction.
    Vector3 moveDir;

    //Used to manage animations or speed of movement.
    float speedMod = 1;
    float crouchAnimManager = 0;
    float animationSpeedMod = 1;

    enum animStates 
    {
        normal, 
        sprinting, 
        crouching
    }
    animStates currentState = animStates.normal;

    // Start is called before the first frame update
    void Start()
    {
        cc = GetComponent<CharacterController>();
        anim = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        //Turn crouch on.
        if (Input.GetKeyDown(KeyCode.C) && currentState != animStates.crouching)
        {
            currentState = animStates.crouching;
        }
        
        //Turn crouch off.
        else if (Input.GetKeyDown(KeyCode.C) && currentState == animStates.crouching)
        {
            currentState = animStates.normal;
        }

        //Turn sprint on.
        else if (Input.GetKeyDown(KeyCode.E)  && currentState != animStates.sprinting)
        {
            currentState = animStates.sprinting;
        }
        
        //Turn sprint off.
        else if (currentState == animStates.sprinting && Input.GetAxis("Vertical") <= 0)
        {
            currentState = animStates.normal;
        }

        switch (currentState)
        {
            case animStates.sprinting:
                crouchAnimManager = Mathf.MoveTowards(crouchAnimManager, 0, 0.01f);
                animationSpeedMod = Mathf.MoveTowards(animationSpeedMod, 2, 0.01f);
                cc.center = Vector3.MoveTowards(cc.center, new Vector3(0, 0.9f, 0), 0.01f);
                cc.height = Mathf.MoveTowards(cc.height, 1.78f, 0.01f);
                speedMod = Mathf.MoveTowards(speedMod, 2, 0.1f);
                break;

            case animStates.crouching:
                crouchAnimManager = Mathf.MoveTowards(crouchAnimManager, -1, 0.01f);
                animationSpeedMod = Mathf.MoveTowards(animationSpeedMod, 1, 0.01f);
                cc.center = Vector3.MoveTowards(cc.center, new Vector3(0, 0.65f, 0), 0.01f);
                cc.height = Mathf.MoveTowards(cc.height, 1.3f, 0.01f);
                speedMod = Mathf.MoveTowards(speedMod, 0.8f, 0.1f);
                break;

            case animStates.normal:
                crouchAnimManager = Mathf.MoveTowards(crouchAnimManager, 0, 0.01f);
                animationSpeedMod = Mathf.MoveTowards(animationSpeedMod, 1, 0.01f);
                cc.center = Vector3.MoveTowards(cc.center, new Vector3(0, 0.9f, 0), 0.01f);
                cc.height = Mathf.MoveTowards(cc.height, 1.78f, 0.01f);
                speedMod = Mathf.MoveTowards(speedMod, 1, 0.1f);
                break;
        }

        //Control our Blend Trees in the Animator.
        anim.SetFloat("BlendY", crouchAnimManager);
        anim.SetFloat("BlendX", Input.GetAxis("Vertical") * animationSpeedMod);

        //Get keyboard input to move forward/backward.
        moveDir = new Vector3(0, 0, Input.GetAxis("Vertical"));

        //Rotate character using Mouse X movement.
        transform.Rotate(0, Input.GetAxis("Mouse X"), 0);

        //Make our movement vector relative to character's orientation.
        moveDir = transform.rotation * moveDir;
      
        //Move the character, modifying the speed by the speedMod variable.
        cc.SimpleMove(moveDir * speedMod);
    }
}

Last updated