Unity3D Tutorial: State Machine

Hello to all new readers and welcome back to old ones!
Today I will try to teach you all how to do a State Machine.

Remember that those tutorials expects you to have some kind of programming experience, and they reflect my experience and may not agree with everyone.

What is a State Machine?

Also commonly called a finite-state machine (FSM), a state machine is a concept of something that can be in one state of a finite number of them.

Hard to understand? No worries, it’s way simpler than you think, a common example is a traffic light. A traffic light can only ever be in one state at a given time, commonly red, yellow or green, this mean it is a state machine with 3 states! Well, you could also consider turned off to be a state.
So, starting to understand now?

Why would I ever need that?

If you’ve been making games, even simple ones, for a while you probably already used it!
Remember those switch cases you used once? They might have been a very crude state machine.

If you used the Animator at Unity3D guess what? It’s also a state machine! You can actually use it to do your own FSM but I’ve found it lacking in some points.

Imagine a character in a game, let’s say they can be dead, idle, walking, running or swimming. That is already a state machine! Your character can be at any of those given states at any time.

How do I do it?

For this tutorial I’m gonna make a simple sphere that has three states:

  • Stopped: It will not move.
  • 4Dir: It will move as a topdown rpg game, in four directions.
  • Car: It will move like a racing game, acceleration/reverse, and turning left/right.

First let’s think of what we need:

  • We need a way to identify what state we are in, and what state we would like to go. I prefer this to be in a human readable for easier of use.
  • We need our states themselves, obviously.
  • And finally we need something to control the states, like setting the current state, asking to change states, so a controller of sorts.

So let’s tackle the first item:

public enum SphereStateEnum {
    Stopped,
    4Dir,
    Car
}

This mean that we can assign names to the states and ask them to change by simply using this enum, and it’s easy to read what is going on.

But… Can you find the problem with that? It’s not actually in this project, but try to increase the scope.

If you can’t then it’s fine, the answer is in the next line.

Enums can’t be extended, overrided or modified later in any way! For example in my game TinyAttack I use state machines for the AI controller of enemies, but not all enemies have the same states.
Some bosses don’t move, so they don’t use the walking state, some enemies can’t fly, don’t block, don’t sleep, don’t chase, etc.
This mean that if I used an enum, that enum needs to contain every single state in the game! Creating a huge list that in every use case is too much.

So how do we fix that? We create a fake enum! It’s a class that acts exactly like an enum and can be extended:

public class AIStateNames {
    public static readonly AIStateNames NullStateID = new AIStateNames(0); // Use this ID to represent a non-existing State in your system
    /// <summary>This character is spawning.</summary>
    public static readonly AIStateNames Spawning = new AIStateNames(1);
    /// <summary>Nothing to do...</summary>
    public static readonly AIStateNames Idle = new AIStateNames(2);
    /// <summary>We walking to somewhere.</summary>
    public static readonly AIStateNames Moving = new AIStateNames(3);
    /// <summary>We are chasing someone.</summary>
    public static readonly AIStateNames Chasing = new AIStateNames(4);
    /// <summary>We are at combat range, but not attacking.</summary>
    public static readonly AIStateNames Combat = new AIStateNames(5);
    /// <summary>We are doing an attack!</summary>
    public static readonly AIStateNames Attacking = new AIStateNames(6);
    /// <summary>Ded.</summary>
    public static readonly AIStateNames Dead = new AIStateNames(7);

    public int Value { get; protected set; }

    protected AIStateNames(int internalValue) {
        this.Value = internalValue;
    }
}

Now in the case of my Whale boss in TinyAttack, it uses completely different states than normal monsters, so I can just do this:

public class WhaleStateNames : AIStateNames {
    public static readonly WhaleStateNames Stage1 = new WhaleStateNames(8);
    public static readonly WhaleStateNames Stage2 = new WhaleStateNames(9);
    public static readonly WhaleStateNames Stage3 = new WhaleStateNames(10);
    public static readonly WhaleStateNames Stage4 = new WhaleStateNames(11);
    public static readonly WhaleStateNames Stage5 = new WhaleStateNames(12);

    protected WhaleStateNames(int internalValue) : base(internalValue) {
    }
}

And done, now the Whale have 5 new states that no one enemy will ever need to know about.

And this is how you use them:

AIStateNames.Spawning
AIStateNames.Idle
AIStateNames.Walking
WhaleStateNames.Stage1
WhaleStateNames.Stage2
WhaleStateNames.Stage3

Just like a normal enum. And if you are curious about the int, it’s how enum works internally, and as long as you never have two states with the same number in the same controller then it’s fine.

So using our new system in this project it will be:

public class SphereStateNames {
    public static readonly SphereStateNames NullStateID = new SphereStateNames(0); // Use this ID to represent a non-existing State in your system
    /// <summary>We can't move.</summary>
    public static readonly SphereStateNames Stopped = new SphereStateNames(1);
    /// <summary>We moving like an old rpg.</summary>
    public static readonly SphereStateNames FourDir = new SphereStateNames(2);
    /// <summary>We moving like a racecar.</summary>
    public static readonly SphereStateNames Car = new SphereStateNames(3);

    public int Value { get; protected set; }

    protected SphereStateNames(int internalValue) {
        this.Value = internalValue;
    }
}

Now we can make the states, first let’s make an abstract state to represent what states are usually able to do:

public abstract class SphereState {

    protected SphereController _controller;

    public virtual SphereStateNames StateName {
        get {
            return SphereStateNames.NullStateID;
        }
    }

    public SphereState(SphereController caller) {
        _controller = caller;
    }

    public virtual void OnBegin(SphereStateNames previous) {
    }

    public virtual void OnEnd(SphereStateNames next) {
    }

    public virtual void MainUpdate() {
    }

    public virtual void ReceiveInput(float xAxis, float yAxis) {
    }
}

That defines that a State have a name and a controller (which we will create later). It also contains:

OnBegin: This method will be called as soon as the sphere changes to this state, it’s parameter is the name of the previous state the sphere was in.

OnEnd: Like the previous one, but called when you are going out of this state, it’s parameter is the name of the next state the sphere is going in.
This function is called before OnBegin.

MainUpdate: We will not use it in this example, but usually it’s helpful to have a method that runs synced to the update time of the engine.

ReceiveInput: This method receives the horizontal and vertical input of the player, since we will move the sphere around those are necessary.

Let’s now create the first state, the Stopped one, at this state the sphere will do nothing:

public class SphereStateStopped : SphereState {

    public override SphereStateNames StateName {
        get {
            return SphereStateNames.Stopped;
        }
    }

    public SphereStateStopped(SphereController caller) : base(caller) {
    }

    public override void OnBegin(SphereStateNames previous) {
        base.OnBegin(previous);

        _controller.ChangeSphereColor(Color.red);
    }
}

It’s very basic, we only modify it’s name and make it change the color so we can see at which state the sphere is at.

Now to the FourDir state:

public class SphereStateFourDir : SphereState {

    private float _speed = 4f;

    public override SphereStateNames StateName {
        get {
            return SphereStateNames.FourDir;
        }
    }

    public SphereStateFourDir(SphereController caller) : base(caller) {
    }

    public override void OnBegin(SphereStateNames previous) {
        base.OnBegin(previous);

        _controller.ChangeSphereColor(Color.green);
    }

    public override void ReceiveInput(float xAxis, float yAxis) {
        float rotation = _controller.Sphere.transform.rotation.eulerAngles.z;

        if (xAxis > 0) {
            rotation = 0f;
        } else if (xAxis < 0) {
            rotation = 180f;
        }

        if (yAxis > 0) {
            rotation = 90f;
        } else if (yAxis < 0) {
            rotation = 270f;
        }

        _controller.SetRotationSphere(Quaternion.Euler(0f, 0f, rotation));

        _controller.TranslateSphere(xAxis * _speed * Time.deltaTime, yAxis * _speed * Time.deltaTime, Space.World);
    }
}

Besides the same thing we did previously (name and color), this one finally moves the sphere. It basically moves it up/down and left/right like you would expect from a WASD topdown movement.

The last state is the car one:

public class SphereStateCar : SphereState {

    private float _accel = 5f;
    private float _maxVel = 5f;

    private float _velocity = 0f;

    public override SphereStateNames StateName {
        get {
            return SphereStateNames.Car;
        }
    }

    public SphereStateCar(SphereController caller) : base(caller) {
    }

    public override void OnBegin(SphereStateNames previous) {
        base.OnBegin(previous);

        _velocity = 0f;

        _controller.ChangeSphereColor(Color.blue);
    }

    public override void ReceiveInput(float xAxis, float yAxis) {
        Quaternion newFacing = _controller.Sphere.transform.rotation;

        newFacing = Quaternion.Euler(0f, 0f, newFacing.eulerAngles.z - xAxis * 90f * Time.deltaTime);

        _controller.SetRotationSphere(newFacing);

        if (yAxis != 0) {
            _velocity += _accel * Time.deltaTime;
            Mathf.Clamp(_velocity, -_maxVel, _maxVel);
        } else {
            if (_velocity < 0f) {
                _velocity += _maxVel * 2f * Time.deltaTime;

                Mathf.Clamp(_velocity, -_maxVel, 0f);
            } else if (_velocity > 0f) {
                _velocity -= _maxVel * 2f * Time.deltaTime;

                Mathf.Clamp(_velocity, 0f, _maxVel);
            }
        }

        _controller.TranslateSphere(yAxis * _velocity * Time.deltaTime, 0f);
    }
}

This state also have an acceleration, velocity and max velocity in addition to the name and color. It will move the car forward/back and allow you to rotate it.

We now have all the states, now we need the controller to actually use them:

public class SphereController : MonoBehaviour {

    public GameObject Sphere;

    private SphereState _currentState;
    private List<SphereState> _listOfStates = new List<SphereState>();

    private Material _material;

    void Awake() {
        _material = GetComponent<Renderer>().material;

        AddStates();
    }

    // Use this for initialization
    void Start () {
        GoToState(SphereStateNames.Stopped);
    }
    
    // Update is called once per frame
    void Update () {
        if (Input.GetKeyDown(KeyCode.Space)) {
            if (_currentState.StateName == SphereStateNames.Stopped) {
                GoToState(SphereStateNames.FourDir);
            } else if (_currentState.StateName == SphereStateNames.FourDir) {
                GoToState(SphereStateNames.Car);
            } else {
                GoToState(SphereStateNames.Stopped);
            }

            return;
        }

        float xAxis, yAxis;

        xAxis = Input.GetAxis("Horizontal");
        yAxis = Input.GetAxis("Vertical");

        if (xAxis <= -0.15) {
            xAxis = -1;
        } else if (xAxis >= 0.15) {
            xAxis = 1;
        } else {
            xAxis = 0f;
        }

        if (yAxis <= -0.15) {
            yAxis = -1;
        } else if (yAxis >= 0.15) {
            yAxis = 1;
        } else {
            yAxis = 0f;
        }

        _currentState.ReceiveInput(xAxis, yAxis);
    }

    void AddStates() {
        _listOfStates.Add(new SphereStateStopped(this));
        _listOfStates.Add(new SphereStateFourDir(this));
        _listOfStates.Add(new SphereStateCar(this));
    }

    public void GoToState(SphereStateNames newStateName) {
        foreach (SphereState nState in _listOfStates) {
            if (nState.StateName == newStateName) {
                if (_currentState != null) {
                    _currentState.OnEnd(newStateName);
                }

                _currentState = nState;
                _currentState.OnBegin(_currentState != null ? _currentState.StateName : SphereStateNames.NullStateID);

                return;
            }
        }

        Debug.LogWarning("SphereController::GoToState a state with the given name was not found.");
    }

    public void ChangeSphereColor(Color value) {
        _material.SetColor("_Color", value);
    }

    public void TranslateSphere(float xAxis, float yAxis, Space relativeTo = Space.Self) {
        Sphere.transform.Translate(xAxis, yAxis, 0f, relativeTo);
    }

    public void SetRotationSphere(Quaternion facing) {
        Sphere.transform.rotation = facing;
    }
}

For a finite state machine the two most important properties are: _currentState and _listOfStates, the first one will always point to the current state and the second one holds a list of possible states we can be at. With those two you are able to do your own state machine!

AddStates: This method will create the states this controller are able to use.

GoToState: This method changes the current state to the one which name matches the provided in it’s parameter.

Update: This will change the current sphere state if you press Space, and redirect the input (after some cleaning) to the current state.

ChangeSphereColor, TranslateSphere, SetRotationSphere: Those are just methods to help the state control the sphere.

And with those you should have a functional controllable sphere!

And that’s it! Wasn’t it simple? Feel free to play around with your newfound knowledge.

Full Project Download:

You can download the project I used to test here.

Thank you for reading my tutorial!

I hope this tutorial was of help to you, and if possible a donation of 1$ may be of great help on aiding the development of future tutorials:

 

https://www.patreon.com/TinyBirdGames

Thank you very much!

Liked it? Take a second to support TinyBird Games on Patreon!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.