Simple FSM Framework Part 1: The Making

I remember a project which I lead that heavily used Playmaker for its systems. This was the time when Playmaker was still in its first couple of months and I got pretty excited when I tried it. For those of you who are not familiar, Playmaker is a Unity asset that helps you create FSM graphs and add functionality without code. It got so popular that it has become the leading “visual scripting” asset of Unity.

Short version of the story: that project became a disgusting pile of mess. The messiness of FSM wiring is way worse than spaghetti code. At least code could be refactored. It was so bad that I vow not to primarily use visual scripting to run the complex systems of a game ever again.

A part of Defender unit FSM. Looks like spaghetti, yum!
This is not part of that project but you get the idea.

However, I didn’t hate Playmaker. It showed a good data model on how a finite state machine works that can be applied to games. So I made my own bare bones FSM framework based from it so that I can still make FSM graphs through code. Used wisely, FSMs can be a great way to organize simple behavior. I’ve been using this framework ever since. I’m still using Playmaker but only as a visual reference. All of my FSMs are now in code.

I’m going to show you how I did it in this post. There will be no github repository for this because honestly, I’m too lazy to extract it from my library. My intent is to have you read code so you can make your own FSM framework and hopefully improve it on your own.

High Level Design

My design is based on Playmaker’s model. These are the main concepts:

  • FsmAction – Represents an action that is executed/executing on a single state
  • Events – It is that thing that causes state change. This can be easily represented as strings.
  • FsmState – Represents a state in the FSM. It has a collection of actions and transitions.
  • Transitions – Represents the link to another state. This can be easily represented as a pair of event (string) and FsmState.
  • Fsm – Contains a certain set of FsmState instances that comprises a single FSM graph. Provides methods to send events and thus changes the current state.

FsmAction Code

An FsmAction is just a base class for concrete actions to implement. The way the framework is used is that there will be a collection of predefined action classes that can be used such as Move, Rotate, TimedWait, etc. At the same time, custom actions could also be implemented. Obvious examples are actions that are specific to a game.

public abstract class FsmAction {

    private readonly FsmState owner;

    public FsmAction(FsmState owner) {
        this.owner = owner;
    }

    public FsmState GetOwner() {
        return owner;
    }

    public virtual void OnEnter() {
        // may or may not be implemented by deriving class
    }

    public virtual void OnUpdate() {
        // may or may not be implemented by deriving class
    }

    public virtual void OnExit() {
        // may or may not be implemented by deriving class
    }

}

This is pretty straightforward. The most important here are the three methods. OnEnter() is invoked when a state has been entered. OnUpdate() is called repeatedly every frame while on a certain state. OnExit() is invoked when the current state will be changed to a new one. The member FsmState is needed so that actions can send events to it (change the current state).

Why is it not an interface? The intent is that actions should be small modular classes deriving from FsmAction. An existing big class, say some kind of big manager, can’t just implement an interface and use that big class as an FsmAction. This also prevents usage of MonoBehaviour classes as FsmAction instances.

FsmState Code

FsmState is just a collection of actions and transitions to other states. It provides the methods appropriate to setup states.

public class FsmState {

    private readonly string name;
    private readonly Fsm owner;

    // Transitions are just pairs of event and FsmState
    // Events are just strings
    private readonly Dictionary<string, FsmState> transitionMap = new Dictionary<string, FsmState>();

    private readonly List<FsmAction> actionList = new List<FsmAction>();

    public FsmState(string name, Fsm owner) {
        this.name = name;
        this.owner = owner;
    }

    public string Name {
        get {
            return name;
        }
    }

    public void AddTransition(string eventId, FsmState destinationState) {
        // can't have two transitions for the same event
        Assertion.Assert(!transitionMap.ContainsKey(eventId), string.Format("The state {0} already contains a transition for event {1}.", this.name, eventId));
        transitionMap[eventId] = destinationState;
    }

    public FsmState GetTransition(string eventId) {
        // May return null if there's no such transition
        return transitionMap.Find(eventId);
    }

    public void AddAction(FsmAction action) {
        Assertion.Assert(!actionList.Contains(action), "The state already contains the specified action.");
        Assertion.Assert(action.GetOwner() == this, "The owner of the action should be this state.");
        actionList.Add(action);
    }

    public IEnumerable<FsmAction> GetActions() {
        return actionList;
    }

    public void SendEvent(string eventId) {
        // Just delegate to the owner Fsm
        this.owner.SendEvent(eventId);
    }

}

Fsm Code

Fsm is like the central hub of the framework. You make an instance of this class then it provides methods to create FsmState instances. Those FsmState instances can then be setup to your liking (actions and transitions).

public class Fsm {

    private readonly string name;

    private FsmState currentState;

    // Collection of all states using the state name as key
    private readonly Dictionary<string, FsmState> stateMap = new Dictionary<string, FsmState>();

    public Fsm(string name) {
        this.name = name;
        currentState = null;
    }

    public string Name {
        get {
            return name;
        }
    }

    public FsmState AddState(string name) {
        // state names should be unique
        Assertion.Assert(!stateMap.ContainsKey(name), "The FSM already contains a state with the specified name: " + name);

        FsmState newState = new FsmState(name, this);
        stateMap[name] = newState;
        return newState;
    }

    private delegate void StateActionProcessor(FsmAction action);

    private void ProcessStateActions(FsmState state, StateActionProcessor actionProcessor) {
        FsmState stateOnInvoke = this.currentState;

        IEnumerable<FsmAction> actions = state.GetActions();
        foreach(FsmAction action in actions) {
            actionProcessor(action);

            if(this.currentState != stateOnInvoke) {
                // this means that the action processing caused a state change
                // we don't continue with the rest of the actions
                break;
            }
        }
    }

    public void Start(string stateName) {
        FsmState state = stateMap.Find(stateName);
        Assertion.AssertNotNull(state);
        ChangeToState(state);
    }

    private void ChangeToState(FsmState state) {
        if(this.currentState != null) {
            // if there's an active current state, we exit that first
            ExitState(this.currentState);
        }

        this.currentState = state;
        EnterState(this.currentState);
    }

    private void EnterState(FsmState state) {
        ProcessStateActions(state, delegate(FsmAction action) {
            action.OnEnter();
        });
    }

    private void ExitState(FsmState state) {
        FsmState currentStateOnInvoke = this.currentState;

        ProcessStateActions(state, delegate(FsmAction action) {
            action.OnExit();
            if(this.currentState != currentStateOnInvoke) {
                // this means that the action's OnExit() causes the FSM to change state
                // note that states should not change state on exit
	        throw new Exception("State cannot be changed on exit of the specified state.");
            }
        });
    }

    public void Update() {
        if(this.currentState == null) {
            return;
        }

        ProcessStateActions(this.currentState, delegate(FsmAction action) {
            action.OnUpdate();
        });
    }

    public FsmState GetCurrentState() {
        return this.currentState;
    }

    public void SendEvent(string eventId) {
        if(currentState == null) {
            Debug.LogWarning(string.Format("Fsm {0} does not have a current state. Check if it was started.", this.name));
            return;
        }

        FsmState transitionState = this.currentState.GetTransition(eventId);
        if(transitionState == null) {
            #if UNITY_EDITOR
            // log only in Unity Editor since it lags the game even if done in build
            Debug.LogWarning(string.Format("The current state {0} has no transtion for event {1}.", this.currentState.GetName(), eventId));
            #endif
        } else {
            ChangeToState(transitionState);
        }
    }

}

The only thing special here is ProcessStateActions(). It is the common algorithm for traversing state actions. I made it this way so I don’t have to repeat this pattern whenever I traverse state actions. As you can see here, it is invoked in EnterState(), ExitState(), and Update().

This is pretty much it. The FSM framework is done.

How to use?

Let’s just say we’re making a runner game and we are developing the main runner character. Let’s say the following would be its FSM behavior graph:

RunnerFsm

This is self explanatory. That starting state would be the character running. When Jump event triggers, like the player presses the jump button, it goes to jumping state. From jumping, if it hits the ground, it goes back to running state. If it hits an obstacle, then it dies.

Their are many ways to use the framework. The most common one is to use it in a MonoBehaviour. This is how it is then set up:

public class RunnerCharacter : MonoBehaviour {

    [SerializeField]
    private float moveSpeed;

    private Fsm fsm = new Fsm("RunnerCharacter");

    private void Awake() {
        PrepareFsm();
    }

    // Only keeping constant of the starting state since it will be needed for starting the FSM
    private const string RUNNING = "Running";

    // Events
    private const string JUMP = "Jump";
    private const string HIT_GROUND = "HitGround";
    private const string HIT_OBSTACLE = "HitObstacle";

    private void PrepareFsm() {
        // States
        FsmState running = fsm.AddState(RUNNING);
        FsmState jumping = fsm.AddState("Jumping");
        FsmState die = fsm.AddState("Die");

        // Actions

        // Running
        {
            MoveConstantly move = new MoveConstantly(running);
            move.Init(this.transform, this.moveSpeed);
            running.AddAction(move);

            TriggerAnimator animationTrigger = new TriggerAnimator(running, "Run");
            running.AddAction(animationTrigger);
        }

        // Jumping
        {
            JumpAction jump = new JumpAction(jumping);
            jumping.AddAction(jump);

            TriggerAnimator animationTrigger = new TriggerAnimator(running, "Jump");
            running.AddAction(animationTrigger);
        }

        // Die
        {
            TriggerAnimator animationTrigger = new TriggerAnimator(running, "Die");
            running.AddAction(animationTrigger);
        }

        // Transitions
        running.AddTransition(JUMP, jumping);
        running.AddTransition(HIT_OBSTACLE, die);

        jumping.AddTransition(HIT_GROUND, running);

        // Auto start the FSM (sometimes it may be started else where)
        // Start() can be invoked anywhere like respawning a character
        this.fsm.Start(RUNNING);
    }

    private void Update() {
        // Process input
        if(Input.GetKeyDown(KeyCode.Space)) {
            this.fsm.SendEvent(JUMP);
        }

        // Don't forget to update
        this.fsm.Update();
    }

    private void OnCollisionEnter(Collision collision) {
        if(...) {
            // Collided with ground
            this.fsm.SendEvent(HIT_GROUND);
        }

        if(...) {
            // Collided with obstacle
            this.fsm.SendEvent(HIT_OBSTACLE);
        }
    }

}

I usually make a method for preparing the FSM. I usually name it PrepareFsm() and invoked on Awake(). In PrepareFsm(), I perform 3 preparations which are the states, actions, and transitions. I make the states first because those instances are needed when adding transitions. The classes MoveConstantly, JumpAction, and TriggerAnimator are defined FsmAction classes.

Events are natural candidates to become constants. They can be made as public if you want to expose the sending of events from other parts of the program. However, I don’t recommend that.

Every time you want to change the state, you just have to call Fsm.SendEvent() as can be seen here in Update() and OnCollisionEnter().

A common use case is to determine what the current state is. This can be done by caching the added state in a member variable instead of a local one. The cached state can then be compared with the state returned with Fsm.GetCurrentState().

private FsmState running;

private void PrepareFsm() {
    // States
    this.running = fsm.AddState(RUNNING); // Cache in member variable
    FsmState jumping = fsm.AddState("Jumping");
    FsmState die = fsm.AddState("Die");

    ...
}

private void Update() {
    // Process input
    if (Input.GetKeyDown(KeyCode.Space)) {
        if (this.fsm.GetCurrentState() == this.running) {
            // Jump only when on running state
            this.fsm.SendEvent(JUMP);
        }
    }

    this.fsm.Update();
}

This looks more tedious than Playmaker.

It is but it’s more useful for me. The reason I made the framework is so that I don’t have to use Playmaker if I ever do need FSMs. It forces me to ground my work using code. I’m afraid that if I start using Playmaker, it will creep out of control and will eventually become unmanageable. With FSMs done in code, I can apply a lot of tricks to make them manageable. Or if the FSM itself is messy, I could opt to refactor the whole thing without dealing with a Playmaker graph.

Advertisements

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s