Enum

This page is part of the Game Manager example.

The enum-based system in this example is in the GameManagerEnum script:

using Animancer;
using System;
using UnityEngine;
using UnityEngine.UI;

public sealed class GameManagerEnum : MonoBehaviour
{
    [SerializeField] private Transform _Camera;
    [SerializeField] private float _IntroductionOrbitSpeed = 45;
    [SerializeField] private float _IntroductionOrbitRadius = 3;
    [SerializeField] private Vector3 _ReadyCameraPosition = new Vector3(0.25f, 1, -2);
    [SerializeField] private Vector3 _ReadyCameraRotation = Vector3.zero;
    [SerializeField] private float _CameraTurnSpeedFactor = 5;
    [SerializeField] private Text _Text;
    [SerializeField] private Image _FadeImage;
    [SerializeField] private float _FadeSpeed = 2;
    [SerializeField] private Events.GolfHitController _Golfer;
    [SerializeField] private Rigidbody _Ball;

    public enum State
    {
        Introduction,
        Ready,
        Action,
        FadeOut,
        FadeIn,
    }

    private State _CurrentState;

    public State CurrentState
    {
        get => _CurrentState;
        set
        {
            _CurrentState = value;
            OnEnterState();
        }
    }

    private void Awake()
    {
        _FadeImage.gameObject.SetActive(false);
        OnEnterState();
    }

    private void OnEnterState()
    {
        switch (_CurrentState)
        {
            case State.Introduction:
                _Text.gameObject.SetActive(true);
                _Text.text = "Welcome to the Game Manager example\nClick to start playing";
                _FadeImage.gameObject.SetActive(false);
                _Golfer.EndSwing();
                break;

            case State.Ready:
                _Camera.position = _ReadyCameraPosition;
                _Camera.eulerAngles = _ReadyCameraRotation;
                _Text.gameObject.SetActive(true);
                _Text.text = "Click to hit the ball";
                _FadeImage.gameObject.SetActive(false);
                _Golfer.enabled = true;
                break;

            case State.Action:
                _Text.gameObject.SetActive(true);
                _FadeImage.gameObject.SetActive(false);
                _Golfer.enabled = false;
                break;

            case State.FadeOut:
                _Text.gameObject.SetActive(false);
                _FadeImage.gameObject.SetActive(true);
                _FadeImage.color = new Color(0, 0, 0, 0);
                break;

            case State.FadeIn:
                _Camera.position = _ReadyCameraPosition;
                _Camera.eulerAngles = _ReadyCameraRotation;
                _Text.gameObject.SetActive(false);
                _FadeImage.gameObject.SetActive(true);
                _FadeImage.color = new Color(0, 0, 0, 1);
                _Golfer.ReturnToReady();
                break;
        }
    }

    private void Update()
    {
        switch (_CurrentState)
        {
            case State.Introduction:
                var euler = _Camera.eulerAngles;
                euler.y += _IntroductionOrbitSpeed * Time.deltaTime;
                _Camera.eulerAngles = euler;

                var lookAt = _Golfer.transform.position;
                lookAt.y += 1;
                _Camera.position = lookAt - _Camera.forward * _IntroductionOrbitRadius;

                if (Input.GetMouseButtonUp(0))
                    CurrentState = State.Ready;

                break;

            case State.Ready:
                if (_Golfer.CurrentState != Events.GolfHitController.State.Ready)
                    CurrentState = State.Action;
                break;

            case State.Action:
                _Text.text = $"Wait for the ball to stop\nCurrent Speed: {_Ball.velocity.magnitude:0.00}m/s";

                var targetRotation = Quaternion.LookRotation(_Ball.position - _Camera.position);
                _Camera.rotation = Quaternion.Slerp(_Camera.rotation, targetRotation, _CameraTurnSpeedFactor * Time.deltaTime);

                if (_Golfer.CurrentState == Events.GolfHitController.State.Idle &&
                    _Ball.IsSleeping())
                    CurrentState = State.FadeOut;
                break;

            case State.FadeOut:
                {
                    var color = _FadeImage.color;
                    color.a = Mathf.MoveTowards(color.a, 1, _FadeSpeed * Time.deltaTime);
                    _FadeImage.color = color;

                    if (color.a == 1)
                        CurrentState = State.FadeIn;

                    break;
                }

            case State.FadeIn:
                {
                    var color = _FadeImage.color;
                    color.a = Mathf.MoveTowards(color.a, 0, _FadeSpeed * Time.deltaTime);
                    _FadeImage.color = color;

                    if (color.a == 0)
                        CurrentState = State.Ready;

                    break;
                }
        }
    }
}

That's quite a long script compared to most of the other examples, so let's break down its individual parts.

Serialized Fields

We will mention each of the Serialized Fields again as we need them, but first let's look at them all together:

[SerializeField] private Transform _Camera;
[SerializeField] private float _IntroductionOrbitSpeed = 45;
[SerializeField] private float _IntroductionOrbitRadius = 3;
[SerializeField] private Vector3 _ReadyCameraPosition = new Vector3(0.25f, 1, -2);
[SerializeField] private Vector3 _ReadyCameraRotation = Vector3.zero;
[SerializeField] private float _CameraTurnSpeedFactor = 5;
[SerializeField] private Text _Text;
[SerializeField] private Image _FadeImage;
[SerializeField] private float _FadeSpeed = 2;
[SerializeField] private Events.GolfHitController _Golfer;
[SerializeField] private Rigidbody _Ball;

Having everything in a single class like this means that we can't easily tell which fields are used by which states and members often need long names like _IntroductionOrbitSpeed to try to make it clear what they are used for. If we later wanted to rename the Introduction state to something like Intro or Orbit, then we would need to also rename those fields to avoid causing confusion.

State Enum

The first thing we need for an enum-based state machine is an enum to define all the different states that it can be in:

public sealed class GameManagerEnum : MonoBehaviour
{
    ...

    public enum State
    {
        /// <summary>Camera orbiting the player, waiting for the player to click.</summary>
        Introduction,

        /// <summary>Waiting for player input to hit the ball.</summary>
        Ready,

        /// <summary>Waiting the the character to hit the ball and the ball to stop.</summary>
        Action,

        /// <summary>Fading the screen to black.</summary>
        FadeOut,

        /// <summary>Fading the screen back in after resetting the ball.</summary>
        FadeIn,
    }
}
  • Since this enum will only be used in conjunction with the GameManagerEnum class, we declared it as a Nested Type inside that class. It is public so other classes could still access it using GameManagerEnum.State, but this makes it clear that the enum is specifically associated with the GameManagerEnum class. Otherwise we should have given it a more descriptive name such as GameManagerState.
  • Putting Documentation Comments on each of the enum values is helpful because it allows your IDE to display a description of the value when you hover over it elsewhere in your code.

Current State

Now that we have defined all the states the game can be in (or at least some of them, you could always add more later), we can start setting them up to actually do stuff.

First we need a field to keep track of the state the game is currently in:

private State _CurrentState;

The default value for an enum is the first value in its declaration (unless you set a different one), so the current state will start as Introduction.

We will want to do different things when the game enters each state, so we can start with an OnEnterState method that uses a switch statement to choose which case to run depending on the _CurrentState:

private void OnEnterState()
{
    switch (_CurrentState)
    {
        case State.Introduction:
            break;
        case State.Ready:
            break;
        case State.Action:
            break;
        case State.FadeOut:
            break;
        case State.FadeIn:
            break;
        default:
            break;
    }
}

As noted in the Flow Control section, Visual Studio allows you to simply type switch and press Tab twice to insert a default snippet, then put _CurrentState in the parenthesis and press Enter and it will automatically insert cases for every value in the State enum.

In this case, we don't want to do anything in the default case so you can either leave it empty or remove it.

With that method defined, we will want to call it first when the scene is loaded:

private void Awake()
{
    OnEnterState();
}

And then again any time we change the _CurrentState. C# doesn't have any way to automatically run code whenever you modify a Field, but it does have Properties which can be used to wrap a field:

public State CurrentState
{
    get => _CurrentState;
    set
    {
        _CurrentState = value;
        OnEnterState();
    }
}

This means that any time we would want to set the _CurrentState field, we need to instead set the CurrentState property.

We will also have an Update method containing a switch statement just like OnEnterState.

Introduction State

The first thing we want to do when the game starts and enters the Introduction state is show some text to tell the player what to do, so we have a Serialized Field to reference a UI Text component (which we assign using the Inspector) and have OnEnterStateset itstextproperty if the current state isIntroduction`:

[SerializeField] private Text _Text;

private void OnEnterState()
{
    switch (_CurrentState)
    {
        case State.Introduction:
            _Text.text = "Welcome to the Game Manager example\nClick to start playing";

The \n is a special character sequence. \ is an "escape character" which indicates that the next character is special and n indicates a line break so that Welcome to the Game Manager example will be on one line and Click to start playing on a separate line.

We could have simply entered that text on the component itself in the Inspector, but most of the other states are going to be changing the text so this way we avoid having some text defined in the Inspector and some in the script. So if you want to modify some of the displayed text, you only have to look in the script.

We are reusing the GolfHitControllerAnimancerHybrid script from the Golf Events example for the character in this game, but it was designed to start the character in their Ready pose as they are about to hit the ball which we don't want during the Introduction so we disable that component in the scene (using the checkbox in the component header next to its name):

Then add another serialized field to reference it so we can call its EndSwing method to put the character into their Idle state:

// GolfHitControllerAnimancerHybrid inherits from GolfHitController.
[SerializeField] private Events.GolfHitController _Golfer;

private void OnEnterState()
{
    switch (_CurrentState)
    {
        case State.Introduction:
            _Text.text = "Welcome to the Game Manager example\nClick to start playing";
            _Golfer.EndSwing();
            break;

Note that the GolfHitController actually has its own enum-based state machine system with 3 states: Ready, Swing, and Idle. It also s normally handles its own input (waiting for a mouse click to hit the ball), but we don't actually want it to do that during our Introduction and it won't because we already disabled it.

Every case must have a statement indicating what happens after it ends since only one section of the switch will execute, usually a break:

Alternatively, you can use return to end the entire method, throw to throw an exception, or goto to jump to a specific label or even another case.

More specifically, you can actually have multiple cases in a single section and only need one ending statement for the whole section. For example, if we wanted to do the same thing in both the Introduction and Ready state we could write:

private void OnEnterState()
{
    switch (_CurrentState)
    {
        case State.Introduction:
        case State.Ready:
            // Do this if the current state is Introduction or Ready.
            break;

The official documentation for C# switch statements is much more detailed.

Now that we are actually in the Introduction state, we want to make the camera constantly orbit around the player so we have some serialized fields to reference the camera's Transform and define the orbit:

[SerializeField] private Transform _Camera;
[SerializeField] private float _IntroductionOrbitSpeed = 45;
[SerializeField] private float _IntroductionOrbitRadius = 3;

Then an Update method with another switch based on the current state:

private void Update()
{
    switch (_CurrentState)
    {
        case State.Introduction:

We start by simply adding to the Y axis of the camera's Euler Angles to make it rotate. Since we want the camera speed to look consistent regardless of whether the game is currently running at a high frame rate or not, we multiply the speed by Time.deltaTime:

            var euler = _Camera.eulerAngles;
            euler.y += _IntroductionOrbitSpeed * Time.deltaTime;
            _Camera.eulerAngles = euler;

If we ran the game now, the camera would just be looking around in a circle without moving rather than orbiting around a point:

Next, we need to calculate the point in space which the camera will be orbiting around. In a real game you might have a reference to the character's hip bone or even add an extra GameObject to the character specifically to use as the target for the camera to look at, but for the sake of simplicity we are just using the character's position moved up 1 meter:

            var lookAt = _Golfer.transform.position;
            lookAt.y += 1;

Then we can simply subtract the direction the camera is now pointing (_Camera.forward) multiplied by how far away we want it to be (_IntroductionOrbitRadius) from that point and set the camera to that position:

            _Camera.position = lookAt - _Camera.forward * _IntroductionOrbitRadius;

That's good enough for this example, but for a real game you could get a lot more fancy and might consider using Unity's Cinemachine package which is a proper system for managing camera movements and other details.

And finally, we check if the player clicked the mouse to move on to the Ready state and break out of this switch case:

            if (Input.GetMouseButtonUp(0))
                CurrentState = State.Ready;

            break;

That's all for the Introduction state:

Ready State

Then entering the Ready state, we want to put the camera behind the character so we just add some more serialized fields and use them to set the camera's position and eulerAngles:

[SerializeField] private Vector3 _ReadyCameraPosition = new Vector3(0.25f, 1, -2);
[SerializeField] private Vector3 _ReadyCameraRotation = Vector3.zero;

private void OnEnterState()
{
    switch (_CurrentState)
    {
        ...

        case State.Ready:
            _Camera.position = _ReadyCameraPosition;
            _Camera.eulerAngles = _ReadyCameraRotation;

The point of the serialized fields is to be able to set them using the Inspector, but for this example we know what values we want so we just assign them as defaults. That way, if you use this script on another object it will start with those values and serialize them with that object so you can edit them in the Inspector if you want to.

We also want to change the displayed text:

            _Text.text = "Click to hit the ball";

And since the GolfHitController was disabled for the Introduction, we now need to enable it so it can wait for a mouse click to hit the ball:

            _Golfer.enabled = true;
            break;

With the GolfHitController enabled and waiting for input on its own, our Ready state doesn't really need to do anything in Update except check if the character has moved out of their Ready state so that we can move onto the game's Action state:

private void Update()
{
    switch (_CurrentState)
    {
        ...

        case State.Ready:
            if (_Golfer.CurrentState != Events.GolfHitController.State.Ready)
                CurrentState = State.Action;
            break;

And there's our Ready state:

Action State

When entering the Action state and the character is starting to swing their golf club, all we need to do is disable the GolfHitController so that it doesn't keep checking for input and doing its own thing:

private void OnEnterState()
{
    switch (_CurrentState)
    {
        ...

        case State.Action:
            _Golfer.enabled = false;
            break;

This time we want the UI _Text to show the speed of the ball in real-time, so we do it in Update instead of OnEnterState:

private void Update()
{
    switch (_CurrentState)
    {
        ...

        case State.Action:
            _Text.text = $"Wait for the ball to stop\nCurrent Speed: {_Ball.velocity.magnitude:0.00}m/s";

Here is a quick explanation of what that line is doing if you are unfamiliar with String Interpolation:

String interpolation is a fairly recent addition to the C# language:

  • The $ symbol before the quotes "" indicates that it is an interpolated string.
  • The first part of the string is just a regular string literal.
  • Then the { and } indicate that the code inside them needs to be evaluated and the result inserted into the string. In this case, that's the _Ball.velocity.magnitude (the speed the ball is moving at).
  • But that's a float which could have anywhere from 0 to 7 decimal places, which would not look very good as the number rapidly changes and pushes the rest of the text around on screen. So we follow the value with : and a Numeric Format String to indicate how we want the number to be formatted. In this case, we use 0.00 to indicate that we want the number to always be followed by two decimal places. For example, 1.5 would show up as 1.50 and 3.14159265359 would show up as 3.14.
  • Then the last part is another regular string literal to add m/s after the number (the standard units to indicate that the speed is measured in meters per second).

Without string interpolation, that line would look like this:

_Text.text = "Wait for the ball to stop\nCurrent Speed: " + _Ball.velocity.magnitude.ToString("0.00") + "m/s";

String interpolation often only saves a few characters, but in this case the difference is more notable since it avoids the need to manually call ToString to specify the number format.

After updating the text, we want to make the camera look towards the ball so that we can see where it goes after the character hits it. Immediately snapping the camera to look at the ball would be very jarring when it gets hit and whenever it bounces, so instead we have a field to determine how closely we want the camera to track it:

[SerializeField] private float _CameraTurnSpeedFactor = 5;

Back in the Update method, we need to calculate the target rotation that the camera is trying to reach by getting the difference between it's position and the ball's position and passing the result into Quaternion.LookRotation:

private void Update()
{
    switch (_CurrentState)
    {
        case State.Action:
            ...

            var targetRotation = Quaternion.LookRotation(_Ball.position - _Camera.position);

Then we use Quaternion.Slerp to rotate from the current _Camera.rotation to the targetRotation a small amount every frame calculated by _CameraTurnSpeedFactor * Time.deltaTime:

            _Camera.rotation = Quaternion.Slerp(_Camera.rotation, targetRotation, _CameraTurnSpeedFactor * Time.deltaTime);

That gives us a gradual movement which will automatically slow down as it nears the target and speed up if the target gets further away. That's good enough for this example, but it doesn't give us any control over how quickly it starts moving or allow the camera to have any momentum to dampen sudden changes in the target value so real games will usually use more complex tracking algorithms. That's also why we called it _CameraTurnSpeedFactor rather than just _CameraTurnSpeed, because it's not a constant speed but rather a multiplier.

The character started swinging their golf club as we entered the Action state, so that has still been happening while we have been rotating the camera and we just need to check if it's done (which causes the character to return to their Idle state):

            if (_Golfer.CurrentState == Events.GolfHitController.State.Idle &&

If it is done, then we also want to check if the ball has stopped moving. We could check if the velocity is zero, but that could theoretically still happen if an update happened to occur with the ball in the air at the very top of its arc after being hit (or bouncing) directly upwards or if the ball is stationary but somehow still rotating. Those are very unlikely scenarios, but still possible and we can easily check that it has properly stopped moving using Rigidbody.IsSleeping:

                _Ball.IsSleeping())

Note that the ball's Rigidbody has its Anguler Drag set to 10 to make it stop rolling very quickly for this example, but that likely wouldn't be the case in a real game.

If those are both true, then we can move on to the FadeOut state:

                CurrentState = State.FadeOut;
            break;

And here it is in Action (pun intended):

Fade Out State

After the ball stops, we want to fade the screen out (to black), reset everything to the Ready state, then fade back in. There are lots of different ways we could do that, but one of the easiest is just to set up a UI Image component so that it stretches across the whole screen and we can simply change its color over time:

Note how the Anchors which define the corners of the RectTransform in screen-space are set to Min 0 and Max 1 on both X and Y, which is what causes it to fill the screen regardless of screen size.

So now we need one serialized field to reference that Image component and one to specify how fast we want the fade to occur:

[SerializeField] private Image _FadeImage;
[SerializeField] private float _FadeSpeed = 2;

Rather than wasting rendering time on an Image that is fully transpacent most of the time, we should deactivate it when we don't need it. So we want it active during FadeOut and FadeIn, but inactive during all other states, but here we start to see one of the disadvantages of an enum-based system: their inflexibility that makes them harder to maintain as your requirements change throughout development.

  • We could make an OnExitState method for the CurrentState setter to call, but that's a lot of effort just for a few SetActive calls. That might be worth it in a real game where there might be more things to put in that method, but not for this example.
  • We could just deactivate the Image when entering the Ready state since that's what comes after FadeOut, but that might not be true anymore if we add any new states. For example, we could add a GameOver screen later on.
  • So instead, we set the _FadeImage to active or inactive on entering any state and while we're at it, we can do the same for the _Text since we don't want to show it while fading either:
private void OnEnterState()
{
    switch (_CurrentState)
    {
        case State.Introduction:
            _Text.gameObject.SetActive(true);
            _FadeImage.gameObject.SetActive(false);
            ...

        case State.Ready:
            _Text.gameObject.SetActive(true);
            _FadeImage.gameObject.SetActive(false);
            ...

        case State.Action:
            _Text.gameObject.SetActive(true);
            _FadeImage.gameObject.SetActive(false);
            ...

        case State.FadeOut:
            _Text.gameObject.SetActive(false);
            _FadeImage.gameObject.SetActive(true);
            ...

        case State.FadeIn:
            _Text.gameObject.SetActive(false);
            _FadeImage.gameObject.SetActive(true);
            ...
    }
}

Repeating code like that is generally a bad programming practice, but it is quick and easy to set up, which is the main advantage of the enum-based approach in the first place.

When entering the FadeOut state, we also need to make sure that it starts fully transparent:

            _FadeImage.color = new Color(0, 0, 0, 0);

Every Update in the FadeOut state, we move the color of the _FadeImage towards solid black using Mathf.MoveTowards:

private void Update()
{
    switch (_CurrentState)
    {
        ...

        case State.FadeOut:
            {
                var color = _FadeImage.color;
                color.a = Mathf.MoveTowards(color.a, 1, _FadeSpeed * Time.deltaTime);
                _FadeImage.color = color;

Unlike Mathf.Lerp (or the Quaternion.Slerp we used earlier), MoveTowards applies a constant speed over time (if the third parameter remains constant with regard to Time.deltaTime). Since the a (alpha) value we are fading will go from 0 (fully transparent) to 1 (fully opaque) a _FadeSpeed of 2 will make it take 0.5 seconds to fade out.

After updating the fade, we just need to check if it has reached the target value to move on to the FadeIn state:

                if (color.a == 1)
                    CurrentState = State.FadeIn;

                break;
            }

And here's what it looks like (seizure warning, it's a very short video that flashes every time it loops):

Fade In State

Entering the FadeIn state deactivates the displayed _Text and activating the _FadeImage as shown above (in case we somehow got into this state without being in FadeOut first, such as if we wanted to start with the screen black and fade in before the Introduction):

private void OnEnterState()
{
    switch (_CurrentState)
    {
        ...

        case State.FadeIn:
            _Text.gameObject.SetActive(false);
            _FadeImage.gameObject.SetActive(true);
            ...
    }
}

We also want to reset the camera to the Ready position, make sure the _FadeImage is fully black, and return the character to their Ready state as well:

            _Camera.position = _ReadyCameraPosition;
            _Camera.eulerAngles = _ReadyCameraRotation;
            _FadeImage.color = new Color(0, 0, 0, 1);
            _Golfer.ReturnToReady();
            break;

Then we can Update the FadeIn in basically the same way as we did the FadeOut, except that we are moving the color.a towards 0 instead of 1:

private void Update()
{
    switch (_CurrentState)
    {
        ...

        case State.FadeIn:
            {
                var color = _FadeImage.color;
                color.a = Mathf.MoveTowards(color.a, 0, _FadeSpeed * Time.deltaTime);
                _FadeImage.color = color;

                if (color.a == 0)
                    CurrentState = State.Ready;

                break;
            }

And that's out last state done (seizure warning, it's a very short video that flashes every time it loops):

So here's all of them together in sequence:

Next

Now that we have gone through the implementation of the enum-based system, you can move on to the Game Manager FSM page to learn how to implement the same mechanics using Animancer's Finite Staate Machine system or skip to the Comparison page for an explanation of the differences between the two approaches.