A lightweight user-friendly finite state machine to easily implement both simple and complex behaviors. Supports hierarchical states, custom and conditional transitions.
Tested on unity 2020.3 and contains assembly definition for compiling to separate assembly file for performance reason.
Important! Requires installed Conditions
This repository can be installed as unity module directly from git url. In this way new line should be added to Packages/manifest.json
:
"com.goodcat.fsm": "https://github.com/GoodCatGames/FSM.git"
If you can't / don't want to use unity modules, code can be downloaded as sources archive from Releases
page.
public class Unit : MonoBehaviour, IUnit
{
...
public FSM Fsm { get; private set; }
private void Awake()
{
const string stateStandName = "Stand";
var stateRotateToTarget = new StateRotateToTarget();
var stateMoveToTarget = new StateMoveToTarget();
var conditionTargetWithinRange = new ConditionTargetWithinRange(this);
Fsm = new FSM();
// States
Fsm.StatesCollection
.Add(stateMoveToTarget)
.AddHierarchical(stateStandName) // Hierarchical state
.StatesCollection // Go inside Stand state
.Add(stateRotateToTarget)
// Create StateSimple (if you don't want to create an inheritor of the State class)
.Add("Shoot", // Name state
() => Debug.Log("Shoot start!"), // OnEnableAction
ShootIfYouCan, // OnUpdateAction
() => Debug.Log("Shoot end!")) // OnDisableAction
.SetStartState(stateRotateToTarget) // Set StartState in Stand
.GetParentStatesCollection() // Go back to FSM Root
.SetStartState(stateMoveToTarget); // Set StartState in FSM Root
// Transitions
_fsm.StatesCollection.Transitions
.From(stateMoveToTarget).ToStateHierarchical(stateStandName) // variable stateMoveToTarget is used here
.Set(conditionTargetWithinRange)
.FromStateHierarchical(stateStandName).To<StateMoveToTarget>() // generic StateMoveToTarget is used here
.Set(conditionTargetWithinRange.Not())
.StatesCollection.GetStateHierarchical(stateStandName).StatesCollection // Go inside Stand state
.Transitions
.From<StateRotateToTarget>().To("Shoot")
.Set(IsLookAtTarget) // It is not necessary to create a Condition class
.From("Shoot").To<StateRotateToTarget>()
.Set(() => IsLookAtTarget() == false);
Fsm.Initialize();
}
private void Update() => Fsm.Update();
...
}
public class ConditionTargetWithinRange : ICondition
{
private readonly IUnit _unit;
public ConditionTargetWithinRange(IUnit unit) => _unit = unit;
public bool IsTrue() => (_unit.TargetCurrent.position - _unit.Owner.position).sqrMagnitude < _unit.Range * _unit.Range;
}
public interface IUnit
{
Transform Owner { get; }
Transform TargetCurrent { get; }
float Range { get; }
void ShootIfYouCan();
bool IsLookAtTarget();
}
- FSM can be in one and only one State at a time (StateMoveToTarget, StateShoot, etc)
- OnEnable() - called when entering the state
- OnUpdate() - called on every call to Fsm.Update()
- OnDisable() - called when exiting the state
// Complex state implementation
public class StateMoveToTarget : State
{
...
protected override void OnEnable() => StartMoveAnimation();
protected override bool OnUpdate()
{
// ... movement code
return false;
}
protected override void OnDisable() => EndMoveAnimation();
}
// Simple state implementation
var stateShoot = new StateSimple("Shoot", // Name state
() => Debug.Log("Shoot start!"), // OnEnableAction
() => ShootIfYouCan(), // OnUpdateAction
() => Debug.Log("Shoot end!")); // OnDisableAction
// Setting states in fsm
Fsm.StatesCollection
.Add(stateMoveToTarget)
.Add(stateShoot);
- There can be one and only one Transition from StateA to StateB.
- Transition is actually a condition that is checked every time Fsm.Update() is called, but before OnUpdate() of the State class is called.
- The condition can be compound.
For example, the Transition condition from StateRotateToTarget to StateShoot: CurrentWeaponDoesNotRequireTurningToTarget OR AlreadyLookingAtTheTarget.
Fsm.StatesCollection.Transitions
.From<StateRotateToTarget>().To<StateShoot>()
.Set(conditionIsNeedRotateToTarget.Not().Or(conditionLookAtTarget));
Look Conditions
- The hierarchical state must contain other nested states.
- One of these states must be set to start.
- When transition To StateHierarchical is triggered, this starting state is entered.
- StateHierarchical can never be Fsm.StateCurrent.
- If StateCurrent is in StateHierarchical when calling Fsm.Update() transition From StateHierarchical is checked first and only then transition From StateCurrent is checked.
Fsm.StatesCollection
.Add(stateMoveToTarget)
.AddHierarchical("Stand") // Hierarchical state
.StatesCollection // Go inside Stand state
.Add(stateRotateToTarget)
.Add(stateShoot);
- To track the state change, just add to the GameObject with
MonoBehaviour: IFsmContainer
componentFsmDebugView: MonoBehaviour
- To understand why there is a transition to any state.
a) Find the condition for the transition to the state of interest to you
.From<StateRotateToTarget>().To<StateShoot>()
.Set(conditionIsNeedRotateToTarget.Not().Or(conditionLookAtTarget))
b) Then output the results of condition checks to the console using the Conditions Debug(…)
method.
To do this it is enough to modernize the code
.From<StateRotateToTarget>().To<StateShoot>()
.Set(conditionIsNeedRotateToTarget.Not().Debug("conditionIsNeedRotateToTargetNot")
Or(conditionLookAtTarget).Debug("conditionLookAtTarget"))
Now, when making the transition from StateRotateToTarget to StateShoot, we will see messages in the console, for example, of the following form:
// Unity Log
conditionIsNeedRotateToTargetNot - False
conditionLookAtTarget - True
Then the transition occurred due to the conditionLookAtTarget condition.
The software is released under the terms of the MIT license.
No personal support or any guarantees.