A simple yet powerful hierarchical finite state machine for the Unity game engine. It is scalable and customisable by being class-based, but also supports functions (or lambdas) for fast prototyping.
Examples
Simple State Machine
Here's a simple state machine for an enemy spy in your game.
As you can see the enemy will try to stay outside of the player's scanning range while extracting intel. When the player goes too far away, it will follow the player again.
The idea:
-
Initialise the state machine
-
Add states:
fsm.AddState( new State( onEnter, onLogic, onExit ));
-
Add transitions
fsm.AddTransition( new Transition( from, to, condition ));
-
Run the state machine
void Update { fsm.OnLogic() }
Initialising the state machine
using System.Collections; using System.Collections.Generic; using UnityEngine; using FSM; // Import the required classes for the state machine public class EnemyController : MonoBehaviour { private StateMachine fsm; public float playerScanningRange = 4f; public float ownScanningRange = 6f; void Start() { fsm = new StateMachine(this); } }
Adding states
float DistanceToPlayer() { // Or however you have your scene and player configured return (transform.position - PlayerController.Instance.transform.position).magnitude; } void MoveTowardsPlayer(float speed) { // Or however you have your scene and player configured Vector3 player = PlayerController.Instance.transform.position; transform.position += (player - transform.position).normalized * speed * Time.deltaTime; } void Start() { fsm = new StateMachine(this); // Empty state without any logic fsm.AddState("ExtractIntel", new State()); fsm.AddState("FollowPlayer", new State( onLogic: (state) => MoveTowardsPlayer(1) )); fsm.AddState("FleeFromPlayer", new State( onLogic: (state) => MoveTowardsPlayer(-1) )); // This configures the entry point of the state machine fsm.SetStartState("FollowPlayer"); // Initialises the state machine and must be called before OnLogic() is called fsm.OnEnter() }
Although this example is using lambda expressions for the states' logic, you can of course just pass normal functions.
Adding transitions
void Start() { // ... fsm.AddTransition(new Transition( "ExtractIntel", "FollowPlayer", (transition) => DistanceToPlayer() > ownScanningRange )); fsm.AddTransition(new Transition( "FollowPlayer", "ExtractIntel", (transition) => DistanceToPlayer() < ownScanningRange )); fsm.AddTransition(new Transition( "ExtractIntel", "FleeFromPlayer", (transition) => DistanceToPlayer() < playerScanningRange )); fsm.AddTransition(new Transition( "FleeFromPlayer", "ExtractIntel", (transition) => DistanceToPlayer() > playerScanningRange )); }
Initialising and running the state machine
void Start() { // ... fsm.OnEnter(); } void Update() { fsm.OnLogic(); }
Hierarchical State Machine
Because StateMachine inherits from StateBase, it can be treated as a normal state, therefore allowing for the nesting of state machines together with states.
Expanding on the previous example
So that you can see a visual difference, the enemy should be spinning when it enters the "SendData" state
The idea:
-
Create a separate state machine for the nested states (States in Extract Intel)
-
Add the nested states to the new state machine
-
Add the new state machine to the main state machine as a normal state
Separate FSM for the ExtractIntel state
void Start() { // This is the main state machine fsm = new StateMachine(this); // This is the nested state machine StateMachine extractIntel = new StateMachine(this, needsExitTime: false); fsm.AddState("ExtractIntel", extractIntel); // ... }
Adding States and Transitions
void Start() { fsm = new StateMachine(this); StateMachine extractIntel = new StateMachine(this, needsExitTime: false); fsm.AddState("ExtractIntel", extractIntel); extractIntel.AddState("SendData", new State( onLogic: (state) => { // When the state has been active for more than 5 seconds, // notify the fsm that the state can cleanly exit if (state.timer > 5) state.fsm.StateCanExit(); // Make the enemy turn at 100 degrees per second transform.rotation = Quaternion.Euler(transform.eulerAngles + new Vector3(0, 0, Time.deltaTime * 100)); }, // This means the state won't instantly exit when a transition should happen // but instead the state machine waits until it is given permission to change state needsExitTime: true )); extractIntel.AddState("CollectData", new State( onLogic: (state) => {if (state.timer > 5) state.fsm.StateCanExit();}, needsExitTime: true )); // A transition without a condition extractIntel.AddTransition(new Transition("SendData", "CollectData")); extractIntel.AddTransition(new Transition("CollectData", "SendData")); extractIntel.SetStartState("CollectData"); // ... }
What is fsm.StateCanExit() and needsExitTime?
When needsExitTime is set to false, the state can exit any time (because of a transition), regardless of its state (Get it? :) ). If it is set to true this cannot happen (unless a transition has the forceInstantly property). This is very useful when you do not want an action to be interrupted before it has ended, like in this case.
But when is the right time for the state machine to finally change states? This is where the fsm.StateCanExit() method comes in and another argument for the State constructor: canExit. fsm.StateCanExit() notifies the state machine that the state can cleanly exit.
-
When a transition should happen, the state machine calls
activeState.RequestExit(), this calling thecanExitfunction. If the state can exit, thecanExitfunction has to callfsm.StateCanExit()and if not, it doesn't callfsm.StateCanExit(). -
If the state couldn't exit when
canExitwas called, the active state has to notify the state machine at a later point in time, that it can exit, by calling thefsm.StateCanExit()method.
State Change Patterns
The state machine supports two ways of changing states:
-
Using transitions as described earlier
fsm.AddTransition( new Transition( from, to, condition ));
-
Calling the
RequestStateChangemethodfsm.RequestStateChange(state, forceInstantly: false);
Example
fsm.AddState("FollowPlayer", new State( onLogic: (state) => { MoveTowardsPlayer(1); if (DistanceToPlayer() < ownScanningRange) fsm.RequestStateChange("ExtractIntel") }));
Unity Coroutines
By using the CoState class you can run coroutines. This class handles the following things automatically:
-
Starting the Coroutine
-
Running the Coroutine again once it has completed
-
Terminating the Coroutine on state exit
As a result of a limitation of the C# language, you can sadly not use lambda expressions to define IEnumerators (=> Coroutines)
In this example, we can replace the SendData state with a more advanced one, which makes the spy turn in one direction for two seconds, and the in the other direction for the same duration.
IEnumerator SendData(CoState state) { while (state.timer.Elapsed < 2) { transform.rotation = Quaternion.Euler(transform.eulerAngles + new Vector3(0, 0, Time.deltaTime * 100)); // Wait until the next frame yield return null; } while (state.timer.Elapsed < 4) { transform.rotation = Quaternion.Euler(transform.eulerAngles - new Vector3(0, 0, Time.deltaTime * 100)); yield return null; } state.timer.Reset(); // Because needsExitTime is true, we have to tell the FSM when it can // safely exit the state state.fsm.StateCanExit(); yield break; } void Start() { // ... extractIntel.AddState("SendData", new CoState( onLogic: SendData, needsExitTime: true )); // ... }
Class based architecture
Because the states, transitions and the state machine itself are implemented in a object oriented manner, custom state and transition classes can be created. By inheriting from the common base classes (StateBase, TransitionBase), custom states and transitions can be developed.
This is also how CoState, TransitionAfter, ... have been implemented internally.
Creating your own states
Simply inherit from the base class StateBase and override the methods you need
class CustomSendData : StateBase { // Important: The constructor must call StateBase's constructor (here: base(...)) // because it declares whether the state needsExitTime public CustomSendData() : base(needsExitTime: false) { // Optional initialisation code here } public override void OnEnter() { // Write your code for OnEnter here // If you don't have any, you can just leave this entire method override out } public override void OnLogic() { // The MonoBehaviour can be accessed from inside the state with this.mono or simply mono this.mono.transform.rotation = Quaternion.Euler( this.mono.transform.eulerAngles + new Vector3(0, 0, Time.deltaTime * 100)); } } void Start() { // ... extractIntel.AddState("SendData", new CustomSendData()); // ... }
More documentation coming soon...


