State-driven character controller

It’s not really difficult to understand that things as important as character controllers for a game are often subject to constant tweaks  and modifications as the development progresses and new requirements need to be met. Due to this they’re among the easiest components in your game to turn in to a mess of spaghetti code filled exceptions, conditional statements and switches.

But there’s actually a design pattern that can be used to make such components much easier to manage and maintain called the state pattern. It basically allows you to divide all the functionality to different states that handle how the character behaves to commands given by the player or artificial intelligence.

State pattern and finite-state machines are relatively well documented elsewhere in the internet so I won’t be going too deeply in to them in this tutorial. However if you want to know more about them I recommend checking out the explanation at GameDesignPatterns.com.

Download the tutorial Unity package here.

Actors and Character Controllers

Before going further it’s important to fully understand what actors and character controllers actually are and what they’re not.

Actors are usually dynamic characters that move and interact with other Actors and the game environment. They can be animals like sheep that flock among themselves, characters for player to control, friendly NPC’s or even enemy characters.

Character Controllers in the other hand are components within these actors that control how the actors move and interact with the game environment based on commands they’re given. These commands can be given to them either by the player or artificial intelligence.

Due to the broad use of characters it’s generally a good idea to keep character controllers separate from player input handling and potentially very complex artificial intelligence. This allows you to use the same character controller you use to control the actor with both the player controls and artificial intelligence. Not only that it allows the player to switch between actors or even control multiple actors at once with just few lines of code.

Creating the Character Controller

Let’s start with the actual Character Controller named CharControllerBehavior to differentiate it from the one in Unity.  This controller will be simple physics based controller with variables to control the actors movement speed, turning speed, jump power and whether it can double jump or not.

Because it’s physics based it will also need a rigid-body component for us to move it which should have with all rotations frozen to prevent it from falling over. It’ll also have simple ground sensor which simply will detect whether the actor has it’s feet on the ground or not.

But the real meat of the controller is actually in the CurrentState which contains the movement state the actor is currently in. It’s the thing that actually decides if and how the the user can interact in given ways.

CharControllerBehavior.cs


using UnityEngine;

public class CharControllerBehavior : MonoBehaviour
{
    public bool ShowDebug = false;
    public float MovementSpeed = 5.0f;
    public float JumpPower = 3.0f;
    public float TurningSpeed = 10.0f;
    public bool doubleJump = true;

    public Rigidbody RB;
    public GroundSensorBehavior GroundSensor;

    private MovementState _currentState;
    public MovementState CurrentState
    {
        get
        {
            if (_currentState == null)
                CurrentState = new MovementState();

            return _currentState;
        }
        private set { _currentState = value; }
    }

    void Start()
    {
        ChangeState(new OnGroundState());
    }

    public void ChangeState(MovementState newState)
    {
        if (newState == null)
            newState = new MovementState();

        if(ShowDebug)
            Debug.Log("<color=#0066cc>" + gameObject.name + " [Mover] : " + CurrentState.ToString() + " => " + newState.ToString() + "</color>");

        CurrentState = newState;
        CurrentState.Enter(this);
    }

    void Update()
    {
        CurrentState.Update();
    }

    void FixedUpdate()
    {
        CurrentState.FixedUpdate();
    }
}

Movement state

Below is the State which all other movement states will inherit from. It includes all methods a state needs to function. These can be expanded or overridden in inherited states.

Update and Fixed update are basically ways for the state to update itself. In This chase we need Update to process the commands as quickly as possible and Fixed Update to handle physics based movement.

Start and Exit are methods that fire when the current state in CharControllerBehavior changes. Current state will be notified by calling it’s exit method and the new state will be notified through calling it’s Enter method which in this chase also passes it the controller that it’s responsible for. Enter and Exit calls are handy if you for example want to reduce the size of the collider when user is crouching and set it back to normal during exit or use it to tell the Actors animator that the user is now jumping, stunned or dead.

DirInput and RotInput are basically inputs set through the Move and Turn methods in the state. It’s up to the state to either act upon these inputs on update or fixed update or simply ignore them (e.g actor is dead, a sleep or stunned). Even though movement is physics based I prefer to scan inputs on every frame.

MovementState.cs

using UnityEngine;

public class MovementState
{
    private CharControllerBehavior _controller;
    public CharControllerBehavior Controller
    {
        get { return _controller; }
        protected set { _controller = value; }
    }

    public Vector2 DirInput { get; protected set; }
    public float RotInput { get; protected set; }

    public virtual void Update(){}
    public virtual void FixedUpdate(){}
    public virtual void Enter(CharControllerBehavior controller){ _controller = controller; }
    public virtual void Exit(){}

    //Inputs are applied every frame but actual movement should happen in fixed update
    public virtual void Move(Vector2 direction){ DirInput = direction; }
    public virtual void Turn(float turnValue) { RotInput = turnValue; }

    public virtual void Jump(){}
    public virtual void EndJump() { }
}

On Ground State

Here’s the first actual movement state for our controller. Look how it inherits from MovementState which we described above and overrides it’s Fixed update and Jump methods.

Note how the state checks on FixedUpdate whether the actor is airborne through the controllers ground sensor component. If the player is airborne it automatically calls the controller to change to new state called a FallState.

ApplyMovement and ApplyRotation called in the FixedUpdate will handle the simple physics based movement logic based on Directional and Rotational inputs provided through the Move and Turn methods found in the base Movement State class.

If the state is given the command to jump, then the state will immediately tell the controller to change it’s state to JumpState.

Remove the Controller.transform.TransformDirection line if you want the character to move in world-space.

OnGroundState.cs

using UnityEngine;

public class OnGroundState : MovementState
{
    Vector2 prevDir = Vector2.zero; 

    public override void FixedUpdate()
    {
        if (!Controller.GroundSensor.OnGround)
            Controller.ChangeState(new FallState(false));

        ApplyMovement();
        ApplyRotation();
    }

    protected virtual void ApplyMovement()
    {
        if(DirInput.magnitude > 0.0f)
        {
            Vector3 movement = new Vector3(DirInput.x, 0.0f, DirInput.y);
            movement = movement.normalized;
            movement = Controller.transform.TransformDirection(movement);
            movement = movement * Controller.MovementSpeed;
            movement.y = Controller.RB.velocity.y;

            Controller.RB.velocity = movement;
        }
        else
        {
            //simple brake, set horizontal velocity to zero to prevent slipping.
            if (prevDir.magnitude > 0.0f)
                Controller.RB.velocity = new Vector3(0.0f, Controller.RB.velocity.y, 0.0f);
            else
                return;
        }

        prevDir = DirInput;
    }

    protected virtual void ApplyRotation()
    {
        if(RotInput != 0.0f)
            Controller.transform.Rotate(0, RotInput * Controller.TurningSpeed * Time.fixedDeltaTime, 0);
    }

    public override void Jump()
    {
        Controller.ChangeState(new JumpState(Controller.doubleJump));
    }
}

Here’s a simple sensor for detecting whether actor is grounded or not. It’s attached to a transform at the actors feet and it uses Physics.OverlapSphere to see if detects any 3d colliders except for the player.

GroundSensorBehavior.cs

using UnityEngine;

public class GroundSensorBehavior : MonoBehaviour
{
    private bool _onGround = false;
    public bool OnGround
    {
        get { return _onGround; }
        private set { _onGround = value; }
    }

    public float elevation = 0.0f;

    public LayerMask GroundLayer;
    public float Radius = 0.3f;

    void FixedUpdate()
    {
        Collider[] cols = Physics.OverlapSphere(transform.position, 0.2f, GroundLayer);

        for (int i = 0; i < cols.Length; i++)
        {
            if (cols[i].transform.root.tag == "Player")
            {
                continue;
            }
            else
            {
                OnGround = true;
                return;
            }
        }

        OnGround = false;
    }
}

Jumping State and Fall State

Here are the Jump and Fall States that where mentioned before.  The Jump state simply adds vertical velocity to the actors rigidbody for the duration of the jump.  Jump ends when EndJump is called (e.g user releases jump button) which happens automatically if it takes more than 0.3s.

These states also contain simple implementation for double jump where jump state informs the fall-state to allow jumping and where fall-state tells the next jump state to not allow it again.

JumpState.s

using UnityEngine;

public class JumpState : OnGroundState
{
    float MaxJumpTime = 0.3f;
    float JumpTimer = 0.0f;
    bool _doubleJump = false;

    public JumpState(bool doubleJump = false)
    {
        _doubleJump = doubleJump;
    }

    public override void FixedUpdate()
    {
        ApplyJump();
        ApplyRotation();
    }

    public override void Update()
    {
        JumpTimer += Time.deltaTime;
        if (JumpTimer > MaxJumpTime)
            EndJump();
    }

    public override void EndJump()
    {
        Controller.ChangeState(new FallState(_doubleJump));
    }

    void ApplyJump()
    {
        float velY = Controller.JumpPower - Controller.RB.velocity.y;
        Controller.RB.AddForce(Vector3.up * velY, ForceMode.VelocityChange);
    }
}

FallState.cs

using UnityEngine;

public class FallState : OnGroundState
{
    bool _allowJump = false;
    public FallState(bool allowJump = false)
    {
        _allowJump = allowJump;
    }

    public override void FixedUpdate()
    {
        if (Controller.GroundSensor.OnGround)
            Controller.ChangeState(new OnGroundState());
        else
        {
            ApplyRotation();
        }
    }

    public override void Jump()
    {
        if(_allowJump)
        {
            Controller.ChangeState(new JumpState());
        }
    }
}

Controlling the Actor

Now that the actor knows how to move, turn and jump based on what state it’s in it’s time to send it some commands. Below is a simple Controller behavior that takes in CharControllerBehavior which it will then control.

PlayerControllerBehavior.cs

using UnityEngine;

public class PlayerControllerBehavior : MonoBehaviour
{
    public CharControllerBehavior player;

    public void Start()
    {
        Cursor.visible = false;
    }

    public void Update()
    {
        Vector2 movement = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
        float turning = Input.GetAxis("Mouse X");

        if(player)
        {
            player.CurrentState.Move(movement);
            player.CurrentState.Turn(turning);

            if (Input.GetButtonDown("Jump"))
            {
                player.CurrentState.Jump();
            }
            else if (Input.GetButtonUp("Jump"))
            {
                player.CurrentState.EndJump();
            }
        }
    }
}

Notes

  • The physics based movement logic in this tutorial is very bare bones to keep the tutorial as simple as possible.
  • It’s not generally better to use Rigidbody.addForce with velocitychange force mode to control physics based characters because setting velocity directly interferes with external forces (e.g strong wind, knock-back from explosion etc.)
  • Many games move character through animation root-motion instead of physics.
  • State machines are also great for Artificial intelligence among many other things.

Leave a comment