State Machines and Boss Fights
c#


Posted by fariel on 03/12/17 3:31 PM

Ever seen a really awesome boss fight and asked yourself "how did they do that?" Perhaps you've had a great idea for a boss and don't know where to begin programming it. Well, today I'm going to talk about how we use simple state machines in our enemies and boss fights to give believable and predictable behaviours to our favorite things to make. 

So, what is a state machine? Google defines it as "a device that can be in one of a set number of stable conditions depending on its previous condition and on the present values of its inputs." What this means is that a state machine has a series of states, or in our case behaviors, that it can switch between. For example, we might call our state machine "Boss" and its states will be "FirstAttack," "SecondAttack," and "Move."

Since this example is going to be based in Unity, we're going to use C# to show you how to write your own state machine behaviour for whatever you want. The first thing you need is an enumeration, which uses the enum type. Enumerations are, quite literally, just a list of data with names. You can find out more about enumerations in C# by clicking this sentence.

enum BossState {
    FirstAttack,
    SecondAttack,
    Move,
    COUNT
}

This creates an enum named BossState that can have values equal to FirstAttack, SecondAttack, and Move. You might notice that the last option in the enum is "COUNT." So long as COUNT is the last option in the enum, its value will be how long the enum is. This will let us select a random one later. The rest of these will be our states later on. The next piece of this puzzle is to add a variable to hold the current state of our state machine.

BossState currentState = BossState.Move;

Now we have everything we need to start and control our state machine. Let's write a function that will control it. There are a few ways we can do this. We can make it so we tell the function what state to go to. We could also make it pick a random state from the list. Or, we could make it cycle through in a given order. I'll leave two of these up to your imagination, because we're going to go with the random state option.

    void GoToNextState() {
        // First, pick a random state to go into.
        BossState nextState = (BossState)Random.Range(0, (int)BossState.COUNT);
        // Get the name of the chosen state into a string, then add "State" to the end of it. 
        string nextStateString = nextState.ToString() + "State";
        // Get the name of the CURRENT state into a string and add "State" to the end of it, just like above.
        string lastStateString = currentState.ToString() + "State";
        // Update our current state to reflect what we're doing.
        currentState = nextState;
        // Stop our last state just in case we did something funky, then start our new state.
        StopCoroutine(lastStateString);
        StartCoroutine(nextStateString);
    }

The comments in this code should help you to understand what is going on. The only new thing that has been introduced is in the last two lines of the code: Coroutines. These are essentially functions that can be run over time, instead of instantly. You can view Unity's documentation on coroutines by clicking this sentence.

Coroutines are incredibly helpful. They can be used for things like an effect that plays when something takes damage, something that needs to happen on a fixed time interval, or many other crazy and intuitive things. Here, we use them for our states. Every state has a coroutine associated with it that will be called when that state is selected. In our previous example, we created a string with the name of the next state to use. Then we started the next state by using Unity's StartCoroutine function, which can also accept a string. Let's move on to our Move state.

    IEnumerator MoveState() {
        // We will move for three seconds
        float timeToMove = 3f;
        // While three seconds have not elapsed....
        while(timeToMove > 0f) {
            // Subtract the time that has passed since our last frame from the time remaining
            timeToMove -= Time.deltaTime;
            // Then move to the right some.
            transform.Translate(transform.right * Time.deltaTime);
            // Then wait until the next frame so we can keep moving for the entire duration of the Move state.
            yield return null;
        }
        // Once we have moved for our three seconds, we end up down here.
        // We can then call the next state function!
        GoToNextState();
    }

Again, the comments should help in understanding what is going on. Coroutines are kind of crazy. First, they have a return type of IEnumerator. You can read more about C#'s IEnumerator class by clicking here. This means that they have to return something. The call to "yield" makes it wait right there until the next frame. Then, we have the option of returning something immediately afterward. In our case we return "null" but there are other options, too, such as WaitForSeconds(float). This causes it to wait for an amount of time rather than returning next frame and can be useful for scheduling repetitive tasks. 

I'll leave you to figure out how to write your own FirstAttack and SecondAttack coroutines. 

You can extend this as much as you like by adding to the enum that we created in the first step. The only catch is that every value in the enum must also have an IEnumerator with the same name and the word "State" added to the end. 

Do you think you can extend this state machine so that it won't pick the same state twice in a row? How about so you can tell it what state to go to? Feel free to leave questions below, and thanks for reading. 

    enum BossState {
        FirstAttack,
        SecondAttack,
        Move,
        COUNT
    }
    BossState currentState = BossState.Move;
    // Use this for initialization
    void Start () {
        GoToNextState();
}
    void GoToNextState() {
        // First, pick a random state to go into.
        BossState nextState = (BossState)Random.Range(0, (int)BossState.COUNT);
        // Get the name of the chosen state into a string, then add "State" to the end of it. 
        string nextStateString = nextState.ToString() + "State";
        // Get the name of the CURRENT state into a string and add "State" to the end of it, just like above.
        string lastStateString = currentState.ToString() + "State";
        // Update our current state to reflect what we're doing.
        currentState = nextState;
        // Stop our last state just in case we did something funky, then start our new state.
        StopCoroutine(lastStateString);
        StartCoroutine(nextStateString);
    }
    IEnumerator FirstAttackState() {
        yield return null;
        Debug.Log("First Attack");
        GoToNextState();
    }
    IEnumerator SecondAttackState() {
        yield return null;
        Debug.Log("Second Attack");
        GoToNextState();
    }
    IEnumerator MoveState() {
        // We will move for three seconds
        float timeToMove = 3f;
        // While three seconds have not elapsed....
        while(timeToMove > 0f) {
            // Subtract the time that has passed since our last frame from the time remaining
            timeToMove -= Time.deltaTime;
            // Then move to the right some.
            transform.Translate(transform.right * Time.deltaTime);
            // Then wait until the next frame so we can keep moving for the entire duration of the Move state.
            yield return null;
        }
        // Once we have moved for our three seconds, we end up down here.
        // We can then call the next state function!
        GoToNextState();
    }


Back to tutorials
There aren't any comments yet. Log in to post one.