How we did the tutorial system in Academia: School Simulator, Part 1: SSM

I’ve been a game developer for more than a decade now. I can say that one of the technically challenging features to do is the guided tutorial. Tutorials are generally hard because they tend to temporarily break the normal functioning of the game’s systems. Later on, those systems should revert to functioning normally and seamlessly. Managing this temporary breaking of rules is not easy.

I’m always amazed with games that have elaborate tutorials. I always want to know how they did it while being scared on how to approach it to our own game. But alas, details about making tutorials are very few. I reckon that each game have different systems and making tutorials on the game should respect those systems. Thus, tutorials systems tend to be different from game to game, at least to the ones that I’ve worked with. However, I was able to design and implement some kind of a generic solution for Academia that can be reused for other games (with some limitations of course).

Simple Tutorial System

Academia’s tutorial is a linear guided tutorial with step by step progression. You can see it here in action. Basically, the game holds the player’s hand teaching him/her what to click through the game. The game ensures that the player does not stray out of the guide by blocking other UI or disabling interactions that the player is not meant to use yet. If the player can stray away, that’s considered a bug and we fix that by disabling the things that the player used to stray off the path. The game dictates what the player should focus on and what can only be interacted with one at a time.

By making it linear with step by step progression, it’s easier to turn it into a reusable framework. However, it sacrifices flexibility for the player. The player can’t play with anything else while in the “tutorial mode”. This is fine for us. If the player chooses to play the tutorial mode, they are choosing to stick to the guided tour. That’s how we rationalize it.

Backstory

I implemented a hardcoded tutorial back in December 2018. I remember getting tired of doing game shows because every time someone wants to play the game, we have to guide the player on what to do at the beginning. I’m not a very talkative person so explaining things wears me out. Imagine saying the same things for 30-50 times throughout the day.

We were scheduled to show the game at Taipei Game Show back in January 2019. I thought that the game should have a guided tutorial so I don’t have to guide the players every time. If someone wants to play, I should just say “Please go ahead. Sit and click Start Game.” A tutorial should give me energy to answer other questions.

I made the on-boarding tutorial “prototype” using our FSM framework. I made a system that enables/disables UI elements. I also made another one that turns on or off blinking of UI elements as way to bring attention on what to interact. I’ll explain how I made these in part 2. By using our Signals system, I can lock all UI, unlock/lock a certain one, or turn on/off blinking of a button. I used the FSM framework to put them all together in a single MonoBehaviour.

A state in this FSM is really a single step in the tutorial progression. The player’s interaction then is the transition to the next state (which is the next tutorial step). The actions in each state then are something like:

  • Unlock button A
  • Turn on blinking on button A
  • Show tutorial message explaining A

On the next state, it can be something like:

  • Lock button A
  • Turn off blinking on button A
  • Hide tutorial message explaining A
  • Unlock button B
  • Turn on blinking on button B
  • Show tutorial message explaining B

This quick and dirty prototype worked really well. I had a great time in Taipei Game Show even though the weather was terrible and I got the worst colds ever. I was always sneezing but I didn’t worry too much because the game explained itself. My girlfriend was with me at the time. She also didn’t have much problem guiding players through the game.

This version of the tutorial was in the game ever since until around February 2021 when I was finally assigned the task to implement the proper tutorial. It’s not just the on-boarding this time. There are other tutorial topics as well.

Sequence State Machine

I took a hard look at my prototype FSM tutorial to see what I can reuse or just to guide me on how to create the system that could accommodate all tutorials. While studying the code, I realized that the thing doesn’t really need an FSM. The steps are linear anyway. There’s no branching of tutorial steps. I thought, hey I could make a subset of FSM out of this. It’s like an FSM but the states are arranged in sequence maintained in a list. There’s no need for events and transitions since the transition is to just go to the next state in the sequence. There’s probably an existing term for this but I don’t know what it is and I needed the name for the classes that I’m about to write. So I called it Sequence State Machine or SSM for short.

The implementation is mostly the same for our FSM framework except that there’s no need for events and transitions. The elements are:

  • SsmAction – Atomic actions that does something.
  • SsmState – Contains the list of SsmAction instances. Different combinations of actions can be mixed and matched.
  • Ssm – Contains the list of SsmStates. It controls the state machine. Start the state machine means setting the first state as the current state. Has the method Next() that moves the current state to the next state in the list.

This is what an SSM action looks like:

public abstract class SsmAction {
    public virtual void OnEnter() {
        // Executes on enter of state
    }

    public virtual void OnUpdate() {
        // Executes while the owner state is the current state
    }

    public virtual void OnExit() {
        // Executes when current state changes and the owner state was the 
        // previous current state
    }
}

It’s very similar to our implementation of FsmAction only that it doesn’t need a reference to its owner state because there are no more events to send to trigger a transition. This is used as the base class for classes that act as the actual action. SsmState is implemented this way:

public class SsmState {
    private readonly string name;
    private readonly List<SsmAction> actions = new List<SsmAction>();

    public SsmState(string name) {
        this.name = name;
    }

    public string Name {
        get {
            return this.name;
        }
    }

    public IReadOnlyList<SsmAction> Actions {
        get {
            return this.actions;
        }
    }

    public void AddAction(SsmAction action) {
        Assertion.Assert(!this.actions.Contains(action), "The state already contains the specified action.");
        this.actions.Add(action);
    }
}

Nothing much here. It really just manages the list of SsmAction instances. The name property is good for debugging.

Finally, the SSM implementation looks like this:

public class Ssm {
    private readonly string name;
    private readonly List<SsmState> states = new List<SsmState>();
    
    private int currentIndex;

    public Ssm(string name) {
        this.name = name;
    }
    
    public SsmState AddState(string name) {
        SsmState state = new SsmState(name);
        this.states.Add(state);
        return state;
    }

    /// <summary>
    /// Starts the SSM by setting the first state as the current state.
    /// </summary>
    public void Start() {
        this.currentIndex = 0;
        ChangeToCurrentState();
    }

    private void ChangeToCurrentState() {
        // See if there was a previous state
        // We call OnExit() on that.
        int previousIndex = this.currentIndex - 1;
        if (previousIndex >= 0) {
            ExitState(this.states[previousIndex]);
        }
        
        // We save this so we know if the state has changed
        int indexOnInvoke = this.currentIndex;

        IReadOnlyList<SsmAction> actions = this.states[this.currentIndex].Actions;
        int count = actions.Count;
        for (int i = 0; i < count; ++i) {
            actions[i].OnEnter();

            if (this.currentIndex != indexOnInvoke) {
                // This means that the current action has caused a state change on invoke of OnEnter()
                // We don't continue with the rest of the actions
                break;
            }
        }
    }

    private void ExitState(SsmState state) {
        // We save this so we know if the state has changed
        int indexOnInvoke = this.currentIndex;
        
        IReadOnlyList<SsmAction> actions = state.Actions;
        int count = actions.Count;
        for (int i = 0; i < count; ++i) {
            actions[i].OnExit();

            if (this.currentIndex != indexOnInvoke) {
                // This means that the current action has caused a state change on invoke of OnEnter()
                // We throw exception as OnExit() should not cause a state change
                throw new Exception("State change is not allowed in OnExit()");
            }
        }
    }

    public void Update() {
        // We save this so we know if the state has changed
        int indexOnInvoke = this.currentIndex;

        IReadOnlyList<SsmAction> actions = this.states[this.currentIndex].Actions;
        int count = actions.Count;
        for (int i = 0; i < count; ++i) {
            actions[i].OnUpdate();

            if (this.currentIndex != indexOnInvoke) {
                // This means that the current action has caused a state change on invoke of OnEnter()
                // We don't continue with the rest of the actions
                break;
            }
        }
    }
    
    /// <summary>
    /// Returns whether or not there are more states to transition to
    /// </summary>
    public bool HasMore {
        get {
            return this.currentIndex + 1 < this.states.Count;
        }
    }

    public string Name {
        get {
            return this.name;
        }
    }

    /// <summary>
    /// Transitions to the next state
    /// </summary>
    public void Next() {
        if (!this.HasMore) {
            // There are no more states to transition to
            throw new Exception("No more state to transition to. Already at the last.");
        }

        ++this.currentIndex;
        ChangeToCurrentState();
    }

    public SsmState Current {
        get {
            return this.states[this.currentIndex];
        }
    }
}

The usage of this class should explain how it works:

Ssm ssm;

private void Awake() {
    PrepareSsm();
}

private void PrepareSsm() {
    this.ssm = new Ssm("CookInstantNoodles");

    // Let's just say that the SsmAction classes are already implemented here
    SsmState step1 = this.ssm.AddState("Step1");
    step1.AddAction(new HeatPot());
    step1.AddAction(new NextStateWhenWaterIsBoiling(ssm));

    SsmState step2 = this.ssm.AddState("Step2");
    step2.AddAction(new AddNoodles());
    step2.AddAction(new NextState(ssm));

    SsmState step3 = this.ssm.AddState("Step3");
    step3.AddAction(new AddSeasoningPacket());
    step3.AddAction(new NextState(ssm));

    SsmState step4 = this.ssm.AddState("Step4");
    step4.AddAction(new KeepStirring());
    step4.AddAction(new NextStateAfterMinutes(ssm, 5));

    SsmState step5 = this.ssm.AddState("Step5");
    step5.AddAction(new Serve());

    // Auto start SSM
    this.ssm.Start();
}

private void Update() {
    // Runs OnUpdate() actions of the current state
    this.ssm.Update();
}

This is a very contrived example but this is probably one of the best uses of SSM, executing steps when cooking. Preparing the SSM is straightforward. States are added, then actions are added to each state. The SSM is then automatically started. Assume that the actions NextStateWhenWaterIsBoiling, NextState, and NextStateAfterMinutes calls Ssm.Next() at some point which moves the current state to the next one. Ssm.Update() is then called in Update() so that the OnUpdate() of the actions of the current state would be executed.

As for its usage in the tutorial system, imagine that each state is a tutorial step and the actions are common actions used in tutorials like LockUiItem, HighlightItem, ShowTutorialMessage, DispatchSignal, etc. SSM can be used to manage the tutorial steps and the actions for each tutorial step. Ssm.Next() can be invoked when the player does the thing that we want him/her to do in each step. The tutorial is finished when the SSM has reached the last state. How cool is that?

Using SSM, we can compose tutorials by hand using code… or can we make an editor? This post is long already. I’m saving that for part 2. 🙂

If you like my posts, follow me on Twitter!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s