Skip to content

Feature Overview

Inspiaaa edited this page Sep 18, 2023 · 3 revisions

Quick overview over features and usage

Creating a state machine

var fsm = new StateMachine();

Adding states

fsm.AddState("Running", new State(
    onLogic: state => { }
));

If you want to add an instance of the State class, you can also use the shortcut methods that help reduce the amount of boilerplate code:

fsm.AddState("Running", onLogic: state => { });

Adding an empty state:

fsm.AddState("Running");
// Same as
fsm.AddState("Running", new StateBase<string>(needsExitTime: false));

Setting the start state

Whenever the state machine is initialised / its OnEnter method is called (e.g. in a nested state machine), then it transitions to the start state.

By default the first state you add is implicitly the start state, but this can be explicitly overwritten:

fsm.SetStartState("Idle");

This method can also be called before adding the actual start state:

fsm.SetStartState("Idle");
fsm.AddState("Idle");

Initialising the state machine

fsm.Init();

Getting the active state

StateBase<string> state = fsm.ActiveState;
string name = fsm.ActiveStateName;

Getting the nested active states

// E.g. "/Move/Jump"
string name = fsm.GetActiveHierarchyPath();

Adding transitions

These are checked on every OnLogic call of the state machine (i.e. a polling-based approach).

fsm.AddTransition(new TransitionAfter("Running", "Idle", 2));

If you want to add an instance of the Transition class, you can, once again, use the shortcut methods:

fsm.AddTransition("Running", "Jump",
    transition => Input.GetKeyDown(KeyCode.Space));

Adding unconditional transitions:

fsm.AddTransition("Shoot", "Reload");
fsm.AddTransition("Shoot", "Reload", forceInstantly: true);

// Same as
fsm.AddTransition(new TransitionBase<string>("Shoot", "Reload", forceInstantly: true));

You can also have multiple transitions from one state to another (this also applies to the other transitions types, like trigger transitions):

// Example here: The state machine will transition from the "MainMenu" state
// to the "Game" state after 10 seconds or if the player presses
// the space key.
fsm.AddTransition(new TransitionAfter("MainMenu", "Game", 10));
fsm.AddTransition(new Transition("MainMenu", "Game", t => Input.GetKeyDown(KeyCode.Space)));

The transitions are checked / executed when you call

fsm.OnLogic();

The transitions are checked in the order they are added to the state machine. The first transition added will be checked first.

Trigger transitions

These are transitions that are event based and only checked when a specific event is triggered.

fsm.AddTriggerTransition("OnHit",
    new Transition("Alive", "Dead", t => health < 0));

There are also shortcut methods again that can be used:

fsm.AddTriggerTransition("OnHit", "Alive", "Dead");
// Or
fsm.AddTriggerTransition("OnHit", "Alive", "Dead", t => health < 0);

To fire an event, run:

fsm.Trigger("OnHit");

If you are using a hierarchical state machine, the event will propagate down the hierarchy. This means that e.g. the when the OnHit event is triggered in the top state machine, any nested state machine can use this trigger.

If you only want to activate an event locally without activating it a nested state machine, you can use:

fsm.TriggerLocally("OnHit");

Transitions from any

UnityHFSM also lets you define transitions that can occur from any state. These are also checked on every OnLogic call. (The state machine will not try to transition to the target state when it is already in that state.)

The equivalent in Unity's Animator is the Any State:

fsm.AddTransitionFromAny(new Transition("", "Dead", t => health < 0));

Using the shortcut methods:

fsm.AddTransitionFromAny("Dead", t => health < 0);

Trigger transitions from any

These work the same as "transitions from any" and "trigger transitions". They are transitions that are only checked (and executed) when an event is triggered and can transition from any state to a target state.

fsm.AddTriggerTransitionFromAny("OnHit",
    new Transition("", "Dead", t => health < 0));
fsm.AddTriggerTransitionFromAny("OnHit", "Dead");
fsm.AddTriggerTransitionFromAny("OnHit", "Dead", forceInstantly: true);
fsm.AddTriggerTransitionFromAny("OnHit", "Dead", t => health < 0);

These are also only checked when an event is activated with:

fsm.Trigger("OnHit");

Two way transitions

Two way transitions are transitions that go from a source to a target state when a condition is true, and from the target to the source state when the condition is false:

fsm.AddTwoWayTransition("Idle", "Shoot", t => isInRange);

// Same as
fsm.AddTransition("Idle", "Shoot", t => isInRange);
fsm.AddTransition("Shoot", "Idle", t => ! isInRange);
fsm.AddTwoWayTransition("Idle", "Chase", t => distanceToPlayer < 10);

// Changes state if the player is in range and it has been
// in one state for at least 2 seconds. (E.g. to prevent quick state 
// changes when distanceToPlayer = 9.99)
fsm.AddTwoWayTransition(
    new TransitionAfter(
        "Idle", 
        "Chase", 
        2, 
        t => distanceToPlayer < 10
    )
);

Two way trigger transitions

Same as two way transitions, but can, similarly to trigger transitions, only transition when an event occurs.

// E.g. item in a game shop
fsm.AddTwoWayTriggerTransition(
    "OnCoinsChange", 
    new Transition(
        "TooExpensive", 
        "Affordable",
        t => coins > 100
    )
);

Using the shortcut methods:

fsm.AddTwoWayTriggerTransition(
    "OnCoinsChange"
    "TooExpensive", 
    "Affordable",
    t => coins > 100);

Callbacks when transitions succeed

Most transition types allow you to provide callbacks that get called before or after a transition succeeds.

fsm.AddTransition(
    new Transition(
        "A", 
        "B", 
        onTransition: transition => Debug.Log("Before"),
        afterTransition: transition => Debug.Log("After")
    )
);

Shortcut methods are also supported:

fsm.AddTransition("A", "B", onTransition: t => Debug.Log("Before"));

Changing states directly

UnityHFSM also allows you to transition to a state without using transitions.

fsm.RequestStateChange("Idle");
fsm.RequestStateChange("Idle", forceInstantly: true);

If the current state needsExitTime, the target state becomes the "pending state". If multiple transitions should have happened, but couldn't because the currently active state had not exited yet, only the most recent transition will actually take place. The other transitions that were overwritten will be forgotten.

Actions (Custom events)

To let the states define more custom events beside OnEnter, OnLogic and OnExit, you can use the action system. Actions are like the builtin events, but they are defined by the user.

fsm.AddState(
    "Alive",
    new State().AddAction("OnHit", () => PlayHitAnimation())
);

Then to call an action:

fsm.OnAction("OnHit");

When you call an action, only the active state will be informed and, if the state defines this action, will run this action. Unlike transitions you may only define one action per event type.

You can also add multiple actions:

fsm.AddState(
    "Running",
    new State()
        .AddAction("OnFixedUpdate", () => { })
        .AddAction("OnLateUpdate", () => { })
);

Example usage for a MonoBehaviour:

void FixedUpdate()
{
    fsm.OnAction("OnFixedUpdate");
}

Passing a parameter

fsm.AddState(
    "Alive",
    new State()
        .AddAction<float>("OnHit", (float damage) => { })
        .AddAction<Collision2D>("OnCollision", collision => { })
);

Running the actions:

fsm.OnAction<float>("OnHit", 5);
fsm.OnAction<Collision2D>("OnCollision", null);

Passing multiple parameters

Although you can theoretically only pass one parameter to an action, you can use tuples and especially named tuples to get around this limitation.

fsm.AddState("Alive",
    new State()
        .AddAction<(float, string)>(
            "OnDamage",
            ((float damage, string damageSource) data) => {
                Print($"Received {data.damage} from {data.damageSource}");
            }
        )
);

Running the actions:

fsm.OnAction<(float, string)>("OnDamage", (2, "Enemy"));
// Or
fsm.OnAction<(float damage, string damageSource)>(
    "OnDamage",
    (damage: 2, damageSource: "Enemy"));
// Or
fsm.OnAction<(float damage, string damageSource)>("OnDamage", (2, "Enemy"));

Ghost states

Ghost states are states that the state machine does not want to remain in and will try to exit as soon as possible. That means that once the fsm enters a ghost state, it will instantly review all of its outgoing transitions to check if it can switch states. The "ghost state behaviour" is supported by every state type by simply setting the isGhostState field (in the constructor).

Ghost states are useful, because they can help you organise your state machine and manage transitions, as you sometimes want to introduce a "temporary" / "passing" state without introducing a one frame delay.

fsm.AddState("GhostState", new State(isGhostState: true));
// Using shortcut methods:
fsm.AddState("GhostState", isGhostState: true);

Example:

If you have two states, Idle and Patrol and you don't know which state to start in upfront, you can add a ghost state that manages this choice at runtime.

stateDiagram-v2
  direction LR
  [*] --> Start
  Start --> Idle
  Start --> Patrol
fsm.AddState("Start", isGhostState: true);
fsm.AddState("Idle");
fsm.AddState("Patrol");

fsm.SetStartState("Start");

// If the distance to the player > 10, the fsm will transition to the Idle state.
fsm.AddTransition("Start", "Idle", t => distanceToPlayer > 10);

// Because the state machine reviews the transitions in the order they were added,
// it will first check the first transition.
// If the condition is false, the fsm will then try the second transition,
// which is guaranteed to work.
fsm.AddTransition("Start", "Patrol");

State Change Timing

The state change timing system in UnityHFSM is actually very simple and efficient.

If a state does not need exit time (needsExitTime = false), then a transition can instantly happen. Otherwise, the target state of the transition is internally stored in a variable called pendingState and the responsibility moves to the active state to tell the state machine that it can exit, by calling the fsm.StateCanExit() method.

When a transition should happen but has to wait because the active state needsExitTime, the state machine calls the active state's OnExitRequest() method. If it can instantly exit, it is the state's responsibility to call fsm.StateCanExit() in the OnExitRequest method. Otherwise it has to call the fsm.StateCanExit() some time later.

Hierarchical State Change Timing (Exit Transitions)

When a nested fsm does not need exit time, it can instantly exit in order to transition to the next state in the root fsm. This means that the exit time behaviour of the nested fsm's active state is ignored.

To prevent this, set needsExitTime = true in the nested fsm. Then, so-called exit transitions determine when the nested state machine can exit. These behave like normal transitions, but essentially lead out of the state machine to the parent level ("vertical transition").

Exit transitions are only checked if the state machine should exit, i.e. the parent state machine has a pending transition. If the exit transition succeeds but the nested fsm's active state needs exit time, then the nested state machine will only exit, once its active state is ready (fsm.StateCanExit()), unless the transition is forced with forceInstantly = true, then it is instant.

var nested = new StateMachine(needsExitTime: true);
nested.AddState("A");
nested.AddState("B");
// ...

// The nested fsm can only exit when it is in the "B" state and
// the variable x equals 0.
move.AddExitTransition("B", t => x == 0);

Exit transitions can also be defined for all states (AddExitTransitionFromAny), as trigger transitions (AddExitTriggerTransition), or as both (AddExitTriggerTransitionFromAny) and are fully supported by shortcut methods.