A code-only, simple and easy to use Finite State Machine for your Unity Projects!
player-fsm-example.mp4
openupm add com.macawls.oceanfsm
- Open the package manager window
- Click the plus icon
- Choose
"Add package from git URL..."
- Use the link below.
https://github.com/Macawls/OceanFsm.git
Add the following to your manifest.json.
{
"scopedRegistries": [
{
"name": "OpenUPM",
"url": "https://package.openupm.com",
"scopes": [
"com.macawls.oceanfsm"
]
}
],
"dependencies": {
// Replace with latest version or version of your choice
"com.macawls.oceanfsm": "{version}"
}
}
After adding the package, you can use the import button from the package manager window to inspect the samples.
The samples were created from the Default 3D Unity template.
To add functionality to a state, you can override the following methods.
virtual void OnInitialize() { } // called once
virtual void OnEnter() { }
virtual void OnExit() { }
virtual void OnUpdate(float deltaTime) { } // depends on how you decide to update the state machine
All states have to inherit from the State<T>
class.
The generic reference type T
is used to associate your states and state machines.
You can use whatever you want, but I recommend using an interface to keep things tidy.
Lets define IDoor
as our reference type.
public interface IDoor
{
void Close();
void Open();
}
public class Door : MonoBehaviour, IDoor
{
private IPollingMachine<IDoor> _mFsm;
// Instance of IDoor passed to constructor of the builder, because Door implements IDoor
_mFsm = new PollingBuilder<IDoor>(this)
...
}
Here, all the states will have access to instance which implements IDoor
using the Runner
property. Like so.
class Closed : State<IDoor>
{
public override void OnEnter()
{
Runner.Close();
}
}
States also have a Machine
property. If the machine is not castable to IAutonomousStateMachine<T>
, it will be null.
class Closed : State<IDoor>
{
public override void OnUpdate()
{
if (PlayerIsNearby() && Input.GetKeyDown(unlockKey))
{
Machine.ChangeState<Open>(() => {
Debug.Log("Player has opened the door");
});
}
}
}
For a simple use case for inheritance, suppose we have a base class called PlayerStateBase where when we enter a new state, we'd like to play a specific animation. It would look something like this.
[Serializable]
public abstract class PlayerStateBase : State<IPlayer>
{
[SerializeField] private AnimationClip stateAnimation;
public override void Enter()
{
Runner.PlayAnimation(stateAnimation);
}
}
[Serializable]
public class Jump : PlayerStateBase
{
// whatever fields you need
public override void Enter()
{
base.Enter(); // play the animation
// other logic
}
}
Since all states have to inherit from State<T>
class, you can use the State Generator
tool to generate a state boilerplate class for you.
You'll find it under the Tools
dropdown menu.
There are two types at the moment :)
- States are responsible for transitioning to other states.
- The machine can receive commands to transition to a specific state.
- Can freely transition to any state.
- The machine holds an internal dictionary of states and their transitions.
- Transitions are triggered by a condition/predicate.
- States cannot transition by themselves.
There are two builders to aid in creating state machines.
private IPollingMachine<IDoor> _mFsm;
private void Awake()
{
var closed = new Closed();
var open = new Open();
_mFsm = new PollingBuilder<IDoor>(this)
.SetStartingState(nameof(Closed))
.AddTransition(closed, open, () => {
return PlayerIsNearby && Input.GetKeyDown(KeyCode.Space) && !_mIsLocked)
})
.AddTransition(open, closed, () => {
return PlayerIsNearby() && Input.GetKeyDown(KeyCode.Space)
}, onTransition: () => {
Debug.Log("Closing the door"); // optional
})
.Build();
}
private IAutonomousMachine<IPlayer> _mFsm;
private void Awake()
{
_mFsm = new AutonomousBuilder<IDoor>(this)
.SetStartingState(nameof(Idle))
.AddState(new Idle())
.AddState(new Walk())
.AddState(new Jump())
.Build();
}
Currently, only the AutonomousStateMachine
supports commands.
Commands are useful for triggering actions or responding to events from outside the state machine. Conditions and the actions are optional.
private void Awake()
{
_mFsm = new AutonomousBuilder<IPlayer>(this)
.SetInitialState(nameof(Idle))
.AddStates(idle, run, jump)
.Build();
_mFsm.AddCommand("Jump")
.SetTargetState<Jump>()
.SetCondition(() => _mFsm.CurrentState is Run && IsGrounded)
.OnSuccess(() => Debug.Log("Hi mom")) // visual fx for example
.OnFailure(() => Debug.Log("depression")); // negative sound effect for example
}
private void OnJump(InputAction.CallbackContext ctx)
{
if (ctx.performed)
{
_mFsm.ExecuteCommand("Jump");
}
}
It is completely up to you how you want to run the state machine. The key methods are:
void Start();
void Stop();
void Update(float deltaTime);
void Evaluate(); // only for Polling State machines
Important to note that the state machine will not run until you call Start()
if you're using the Polling
state machine, I would recommend calling Evaluate()
in Late Update.
Evaluate()
will continuously check all transitions of the current state. If a transition is met, it will change to the new state.
private void OnEnable()
{
_mFsm.Start();
}
private void OnDisable()
{
_mFsm.Stop();
}
private void Update()
{
_mFsm.Update(Time.deltaTime);
}
private void LateUpdate()
{
_mFsm.Evaluate();
}
I've found that if it's a simple entity with a few states and transitions, the Polling
state machine is good.
For example a door, checkpoint, traffic light, treasure chest etc.
If it's an entity that is fairly complex and or reacts to external input , the Autonomous
state machine is the way to go.
Something like a player, enemy, NPC, UI system etc.
The Autonomous
one is easier to use and more flexible. Most of the time I recommend using it.