Skip to content

Advanced Concepts

Niklas Lindblad edited this page Jun 5, 2025 · 1 revision

How the machine ticks

Here we'll dive deep into how the machine works.

States

A machine contains a set of states. Each state is unique and each state is only part of one machine's set of states. These states are the states that has been added via the builder. Note that child states a state could contain are not included in this set.

The state is stored as an array within the machine. The state at slot 0 is also the default state.

Enter

Update

Exit

Destroy

Transitions

The machine also contains two sets of linked transitions.

One of the sets map a transition from one member of the set of states, to a member of a set of states. The source and destination state can be the same state.

The second maps a transition to a member of our set of states. This is our Any transition, where all other members of the set of states are mapped as an accepted source for the transition.

Life cycle

A machine can only be in one state at a time, or no state at all if the machine is disabled.

Enable

When the machine is enabled it enters its' default state. If configured to do so it can instead enter the last active state from when it was last enabled.

Depending on configuration, enabling an already enabled machine will either do nothing, or reset the machine. That is, treat the enable as a Disable followed by an Enable call.

Disable

When the machine is disabled it exits its' current state, resulting in no states being active.

Update

The update is the most complex, involving the following steps:

Evaluating transitions

All transitions pertaining to this state are evaluated. This means, it will go through all transitions where this state is the source state and test if it can pass through. If it can, it does, leaving the active state and entering the destination state. If none of the tests pass, it will also test all of the any transitions as well, excluding those where the destination is the active state.

This step repeats until no transitions pass the test or for a maximum of eight times.

This means the first transition it encounters that it can pass through it will, meaning the order of transitions matter as you add them. Specific transitions also have a higher precedence over any transitions.

Update the state

The now active state gets its' Update tick, the details of which depend entirely on the state in question.

Evaluating transitions

Since the state update may cause a change, we test all transitions again. Doing this test both before and after ensures the machine is both up to date with external factors, but can also immediately react to internal factors. This allows for it to quickly reflect any changes immediately to the user or to other parts of the game.

Destroy

When destroyed, the machine will call for all the states in its' set of states to be destroyed.

This allows states to hold and manage memory, like NativeArrays, RenderTextures, or other resources whose lifetime falls within the lifetime of that state. This can come with a few caveats, explained in Managing memory.

Machines in tandem

Update, FixedUpdate, LateUpdate. Unity has a few different ways to make things tick! A machine only has the one. As such, we can't have a state that does one thing in FixedUpdate, and something different in Update. As a practical example:

In our machine, we have a state that moves the character using physics. We also have another state that moves a particle towards that character every LateUpdate. We can't do physics in LateUpdate, but the particle looks choppy in FixedUpdate.

FSMForUnity don't support machines more than one kind of Update, but it's a fully solvable problem.

The solution here is to have multiple machines, each being updated by a different Unity event. For this case, we have one physics machine and one fx machine. They act independently, but as they share data (the Rigidbody) they will still work in tandem to produce the desired results. Having machines work in tandem allows them to react to each other, which can be utilized for reactive and emergent results.

Nesting machines

The ability to group up a set of states into one little bundle is quite handy.

Executing multiple states in parallel

Performance and profiling

FSMForUnity will automatically propagate profiling markers for your states, allowing you to profile the states as the machine runs.

To paint a broad picture, while the performance overhead of this state machine is low, it is not suitable for simulations on a grand scale. If your use case involves hundreds or thousands of these machines, for instance for an RTS game, consider a data oriented approach instead.

Below are some performance considerations to take when Using FSMForUnity:

Transitions

Transitions are evaluated frequently, often twice per machine update. If writing performance sensitive code try and limit the amount of processing required to evaluate a transition, and try to minimize the number of transitions have a given state as its' source state.

The machine are able to step through multiple states via a chain of transitions in a single Update call. While this makes it responsive make sure to avoid having any infinite cycles of transitions. Not only would this make the machine erratic as it repeatedly jump between these states, but it also adds unnecessary performance overhead.

States

By segmenting your code into a states in a machine you will be reducing the amount of processing needed. Try to see if you can utilize Enter/Exit as well to reduce the amount of processing going on in Update.

Tick rate

Some things are just expensive, maybe you have a machine that is an AI responsible for making complex decisions. The choice here is to tick the machine less frequently. Coming to a decision once every 10 frames is sometimes an acceptable compromise.

Managing memory

In C# we generally rely on the garbage collector to clean up after us. There are some Unity-specific constructs that fall outside of this, and sometimes we want our states to be managing this. Below are some sample cases, explaining the utility and pitfalls of some use cases.

Using NativeArray and non-scene Objects

Say you have a state that does some intense calculations as part of a job. In this case it makes sense to store the results of these calculations in a NativeArray, and the state acts upon these values each tick. Rather than having something else manage that memory, you can do as follows:

private NativeArray<int> myArray;

void IFSMState.Destroy()
{
    myArray.Dispose();
}

With this you ensure the array is never leaked, as long as you destroy the machine.

This works similarly for other non-scene objects:

private RenderTexture myTexture;

void IFSMState.Destroy()
{
    Object.Destroy(myTexture);
}

Using instantiated Prefabs

Maybe you have some particle you want to play when in a given state. It makes sense to instantiate it from a prefab, and destroy it when we're destroying the machine. Something like:

private ParticleSystem instance;

public Constructor(ParticleSystem prefab)
{
    instance = Object.Instantiate(prefab);
}

public void Enter()
{
    instance.Play();
}

public void Exit()
{
    instance.Stop();
}

void IFSMState.Destroy()
{
    Object.Destroy(instance.gameObject);
}

Unfortunately this will lead to some errors when exiting play mode. This is because Unity is also managing your prefab instance. The object may already be destroyed by unity by the time we're exiting this state.

To ensure this doesn't happen, use hide flags to tell Unity you're responsible for destroying this object.

Procedural machines