Once you have made your State Type, you can create your StateMachine
. There are a various ways of doing that with different advantages and disadvantages depending on your needs. The two most common ones are:
- A Serialized Field if you want it visible in the Inspector.
- Otherwise, a Readonly Field lets you safely make it
public
.
Serialized Field
It's often very useful to declare a StateMachine
as a Serialized Field because:
- It allows you to set the first state in the Inspector.
- It allows you to view the
Current State
at any time in the Inspector.
The recommended implementation looks like this:
[DefaultExecutionOrder(-10000)]
public class Character : MonoBehaviour
{
[Serializable] public class MyStateMachine : StateMachine<MyState> { }
[SerializeField] private MyStateMachine _StateMachine;
public MyStateMachine StateMachine => _StateMachine;
private void Awake()
{
_StateMachine.InitializeAfterDeserialize();
}
}
Breakdown
Unity can't serialize generic types directly so you need to declare a non-generic [Serializable]
class which inherits from StateMachine
(it doesn't have to be a nested class, but it will usually not be directly used in too many places so declaring it next to where it's used can be convenient):
[Serializable] public class MyStateMachine : StateMachine<MyState> { }
// This wouldn't be serialized by Unity.
//[SerializeField] private StateMachine<MyState> _StateMachine;
Note that the state type must also be serializable for this to work. That means it needs to have a [Serializable]
attribute of its own or inherit from UnityEngine.Object
(generally via MonoBehaviour
, StateBehaviour
, or ScriptableObject
). Basically, if you can make a MyState
field show up in the Inspector, then it can also be used in a serializable state machine.
Then you can use that class as the serialized field type:
[SerializeField] private MyStateMachine _StateMachine;
Add a public property to allow other scripts to access it:
public MyStateMachine StateMachine => _StateMachine;
You could just make the field public
, but then other scripts would be able to set it to null
or assign a different StateMachine
which would likely be unintentional and cause a bug unless you specifically plan for it. Utilizing proper encapsulation only takes a bit of effort upfront and can save you from wasting a lot of effort fixing bugs later on, especially if you're working in a team.
In a non-serialized state machine (such as if you use the Readonly Field approach), telling it to enter the first state would call OnStateEnter
on it as you would expect. Unfortunately, with a serialized state machine it isn't possible for the system to automatically call OnStateEnter
on the first state you set in the Inspector so you need to call InitializeAfterDeserialize
in your script on startup. It's important to call that method before anything changes it's state (because that would let it change without calling OnStateEnter
on the first state and then would call it again on the changed state). The easiest way to do that is usually to give your class a [DefaultExecutionOrder]
attribute like so:
[DefaultExecutionOrder(-10000)]// Initialize the StateMachine before anything uses it.
public class Character : MonoBehaviour
{
...
private void Awake()
{
StateMachine.InitializeAfterDeserialize();
}
}
Serialized Properties
You can use the field
keyword to apply the [SerializeField]
attribute to the backing field of an auto-property so that you can make it read-only without needing a separate field as well:
[DefaultExecutionOrder(-10000)]
public class Character : MonoBehaviour
{
[Serializable] public class MyStateMachine : StateMachine<MyState> { }
[field: SerializeField] public MyStateMachine StateMachine { get; private set; }
private void Awake()
{
_StateMachine.InitializeAfterDeserialize();
}
}
The name of the backing field is generated by the compiler and looks like <StateMachine>k__BackingField
in this case. That means two things:
- The serialized data is slightly larger than a regular field due to its length, which is unlikely to have any notable impact on performance, but is worth knowing.
- Unity 2019 will display that ugly name in the Inspector, but newer versions of Unity will instead display it as
State Machine
like you would expect.
Readonly Field
If you don't need the StateMachine
to be visible in the Inspector or if your state type isn't serializable anyway, then you can just use a public readonly
field to make it safely accessible:
public readonly StateMachine<MyState> StateMachine = new StateMachine<MyState>();
private void Awake()
{
StateMachine.TrySetState(firstState);
}
Other Approaches
Here are some of the other approaches that could be used and a summary of their problems:
Create in Awake
[SerializeField]
private State _Idle;
public StateMachine<State> StateMachine { get; private set; }
private void Awake()
{
StateMachine = new StateMachine<State>(_Idle);
}
If another script executes before this Awake
method gets called, the StateMachine
would still be null.
Initialize in Field Initializer
[SerializeField]
private State _Idle;
public readonly StateMachine<State>
StateMachine = new StateMachine<State>(_Idle);
That gives a compiler error because Field Initializers can't access instance fields (_Idle
is an instance field).
Initialize in Constructor
[SerializeField]
private State _Idle;
public readonly StateMachine<State> StateMachine;
public MyClass()
{
StateMachine = new StateMachine<State>(_Idle);
}
That would compile and run, but won't actually work as intended because Constructors are executed immediately when the object is created which is before Unity's Serialization system can deserialize its fields, meaning that the _Idle
field will still be null
.
Default States
A regular StateMachine<TState>
doesn't keep track of any states other than the CurrentState
, but it is often useful to have a particular state that it can return to by default if nothing else is active so the StateMachine<TState>.WithDefault
class allows you to do that:
- It inherits from the base
StateMachine<TState>
, meaning it has the usualCurrentState
property and methods for Changing States. - It has a
DefaultState
property. If there is noCurrentState
when you set the default, the state machine will immediately enter that state. - It also has a
ForceSetDefaultState
callback which is useful for End Events if you want to return to the default state when an animation ends.
The recommended initialization patterns are basically the same as the regular ones explained above.
- If used in a Serialized Field, it will show both the
Current State
andDefault State
in the Inspector. - Otherwise, if you initialize it entirely in code you can simply set its
DefaultState
property which will enter that state if it doesn't already have one:
public sealed class Character : MonoBehaviour
{
[SerializeField]
private AnimancerComponent _Animancer;
public AnimancerComponent Animancer => _Animancer;
[SerializeField]
private CharacterState _Idle;
public readonly StateMachine<CharacterState>.WithDefault
StateMachine = new StateMachine<CharacterState>.WithDefault();
private void Awake()
{
StateMachine.DefaultState = _Idle;
}
}
Then other states can use its ForceSetDefaultState
callback for their End Events:
public abstract class CharacterState : StateBehaviour
{
[SerializeField]
private Character _Character;
public Character Character => _Character;
}
public sealed class AttackState : CharacterState
{
[SerializeField]
private AnimationClip _Animation;
private void OnEnable()
{
var animancerState = Character.Animancer.Play(_Animation);
animancerState.Events.OnEnd = Character.StateMachine.ForceSetDefaultState;
}
}