This is a lightweight Finite State Machine for your C# projects
โถ๏ธ High performanceโถ๏ธ Supports transitionsโถ๏ธ Abstracted from the game engine
Requires C# 8+ version
Supports installation as a Unity module via a git link in the PackageManager
https://github.com/MeeXaSiK/FiniteStateMachine.git
or direct editing of Packages/manifest.json
:
"com.nighttraincode.fsm": "https://github.com/MeeXaSiK/FiniteStateMachine.git",
You can also clone the code into your Unity project.
TInitializer
is the class to which the states will belong.
The namespace you need is using NTC.FiniteStateMachine
.
public readonly StateMachine<Sample> StateMachine = new StateMachine<Sample>();
public StateMachine<Sample> StateMachine { get; } = new StateMachine<Sample>();
Create some states for an entity and declare the methods you need: OnEnter()
, OnRun()
, OnExit()
.
Method | Info |
---|---|
OnEnter() |
Called once upon entering the state. |
OnRun() |
Execution is determined by update methods. |
OnExit() |
Called once when exiting the state. |
For example, let's create two states:
The AwaitingState
is necessary for an entity to wait for some action.
You also need to implement the TInitializer
property.
Let me remind you that the TInitializer
is the class to which the state belongs.
In this case, the initializer is Sample
:
public class AwaitingState : IState<Sample>
{
private readonly IFollower _follower;
public AwaitingState(IFollower follower, Sample sample)
{
_follower = follower;
Initializer = sample;
}
public Sample Initializer { get; }
public void OnEnter()
{
_follower.StopFollow();
}
}
The FollowingState
is necessary for the entity to follow a target:
public class FollowingState : IState<Sample>
{
private readonly IFollower _follower;
private readonly Func<Transform> _getTarget;
public FollowingState(IFollower follower, Func<Transform> getTarget, Sample sample)
{
_follower = follower;
_getTarget = getTarget;
Initializer = sample;
}
public Sample Initializer { get; }
public void OnRun()
{
Transform target = _getTarget.Invoke();
if (target != null)
{
_follower.Follow(target.position);
}
}
}
This Finite State Machine is abstracted from any game engine, but I'll show you how it works with an example in Unity:
[RequireComponent(typeof(IFollower))]
public class Sample : MonoBehaviour
{
[SerializeField] private Health health;
public StateMachine<Sample> StateMachine { get; } = new StateMachine<Sample>();
public Transform Target { get; set; }
private AwaitingState _awaitingState;
private FollowingState _followingState;
private void Awake()
{
InstallStates();
}
private void InstallStates()
{
var follower = GetComponent<IFollower>();
_awaitingState = new AwaitingState(follower, this);
_followingState = new FollowingState(follower, GetTarget, this);
}
private Transform GetTarget()
{
return Target;
}
}
You can add states using the AddStates
method or the class constructor.
StateMachine.AddStates(_awaitingState, _followingState);
StateMachine = new StateMachine<Sample>(_awaitingState, _followingState);
Warning! You can only add states to the
StateMachine
once!
We have methods for binding transitions such as AddTransition
and AddAnyTransition
.
Method | Info |
---|---|
AddTransition<TStateFrom, TStateTo>(Func<bool> condition) |
Takes two states as arguments, from which state we want to go to the second state and the transition condition. |
AddAnyTransition<TStateTo>(Func<bool> condition) |
Takes as arguments the one state we want to switch to and the transition condition. |
Let's imagine a situation:
We want to switch from the AwatingState
to the FollowingState
if a target is found. We also want to switch back to the AwaitingState
if the target is lost. AddTransition
method will help us with this, let's add two transitions:
From AwaitingState
to FollowingState
StateMachine.AddTransition<AwaitingState, FollowingState>(condition: () => Target != null);
From FollowingState
to AwaitingState
StateMachine.AddTransition<FollowingState, AwaitingState>(condition: () => Target == null);
We will also add a transition to the AwaitingState
from any state.
Why might this be needed? If the entity is not alive, then it is logical that it will not be able to move and will have to switch to the idle state.
Add transition from any state to idle AwaitingState
StateMachine.AddAnyTransition<AwaitingState>(condition: () => health.IsAlive == false);
For the launch of StateMachine
you need to set first state and call the Run()
method in Update()
:
private void Start()
{
StateMachine.SetState<AwaitingState>();
}
private void Update()
{
StateMachine.Run();
}
using NTC.FiniteStateMachine;
using UnityEngine;
[RequireComponent(typeof(IFollower))]
public class Sample : MonoBehaviour
{
[SerializeField] private Health health;
public StateMachine<Sample> StateMachine { get; } = new StateMachine<Sample>();
public Transform Target { get; set; }
private AwaitingState _awaitingState;
private FollowingState _followingState;
private void Awake()
{
InstallStates();
BindTransitions();
BindAnyTransitions();
StateMachine.SetState<AwaitingState>();
}
private void Update()
{
StateMachine.Run();
}
private void InstallStates()
{
var follower = GetComponent<IFollower>();
_awaitingState = new AwaitingState(follower, this);
_followingState = new FollowingState(follower, GetTarget, this);
StateMachine.AddStates(_awaitingState, _followingState);
}
private Transform GetTarget()
{
return Target;
}
private void BindTransitions()
{
StateMachine.AddTransition<AwaitingState, FollowingState>(condition: () => Target != null);
StateMachine.AddTransition<FollowingState, AwaitingState>(condition: () => Target == null);
}
private void BindAnyTransitions()
{
StateMachine.AddAnyTransition<AwaitingState>(condition: () => health.IsAlive == false);
}
}
If you want to set states manually, you can disable TransitionsEnabled
in the StateMachine
:
StateMachine.TransitionsEnabled = false;
Then you can change state of StateMachine
by method SetState<TState>()
:
StateMachine.SetState<TState>();
Also if you don't want the StateMachine
to choose the state in the update method, you can use the method SetStateByTransitions()
when you need:
StateMachine.SetStateByTransitions();