-
Notifications
You must be signed in to change notification settings - Fork 126
State Types
UnityHFSM supports multiple state classes, which can be used in different situations. When implementing custom state classes, it is especially important to know which class to inherit from.
Here's a high-level overview over the state inheritance hierarchy of UnityHFSM, ignoring the fact that it supports generics for a moment:
- The base class of all states is
StateBase
. It's basically an empty state without any functionality. - States that support custom actions, have to implement the
IActionable
interface. - As the implementation to support them is usually the same, there exists the
ActionState
base class. It inherits from theStateBase
class. - Then come the higher-level state classes such as
State
andCoState
that inherit fromActionState
and provide advanced user-customisable behaviour. - To allow for hierarchical state machines, the
StateMachine
class also inherits from theStateBase
class. - The
HybridStateMachine
class is essentially a mixture between theStateMachine
and theState
class. It inherits from theStateMachine
class.
In reality, however, UnityHFSM is a little bit more complicated in order to support generics. The core classes have generic type parameters for the state ID and the event ID types. To reduce boilerplate code, there are also class overloads (that inherit from the generic base classes), filling the type parameters in with common default values (strings).
This means that if you are implementing your own state class, it makes sense to either inherit from the StateBase
or the ActionState
class, depending on whether you need custom actions (e.g. OnFixedUpdate
, OnDamageTaken
, ...). Ideally, you also inherit from the generic versions of these classes, so that you can reuse your state in state machines that use different types for the state IDs.
Simplified inheritance hierarchy without generics (not the reality):
classDiagram
class StateBase
class ActionState
class State
class CoState
class StateMachine
StateBase <|-- ActionState
ActionState <|-- State
ActionState <|-- CoState
StateBase <|-- StateMachine
StateMachine <|-- HybridStateMachine
Actual inheritance hierarchy:
classDiagram
class StateBaseT["StateBase[T]"]
class StateBase["StateBase"]
class ActionStateTT["ActionState[T, T]"]
class ActionStateT["ActionState[T]"]
class ActionState["ActionState"]
class StateTT["State[T, T]"]
class StateT["State[T]"]
class State["State"]
class CoStateTT["CoState[T, T]"]
class CoStateT["CoState[T]"]
class CoState["CoState"]
class StateMachineTTT["StateMachine[T, T, T]"]
class StateMachineTT["StateMachine[T, T]"]
class StateMachineT["StateMachine[T]"]
class StateMachine["StateMachine"]
class HybridStateMachineTTT["HybridStateMachine[T, T, T]"]
class HybridStateMachineTT["HybridStateMachine[T, T]"]
class HybridStateMachineT["HybridStateMachine[T]"]
class HybridStateMachine["HybridStateMachine"]
class IActionableT["IActionable[T]"]
<<Interface>> IActionableT
StateBaseT <|-- StateBase
StateBaseT <|-- ActionStateTT
ActionStateTT <|-- ActionStateT
ActionStateTT <|-- ActionState
ActionStateTT <|-- StateTT
StateTT <|-- StateT
StateTT <|-- State
ActionStateTT <|-- CoStateTT
CoStateTT <|-- CoStateT
CoStateTT <|-- CoState
StateBaseT <|-- StateMachineTTT
StateMachineTTT <|-- StateMachineTT
StateMachineTTT <|-- StateMachineT
StateMachineTTT <|-- StateMachine
StateMachineTTT <|-- HybridStateMachineTTT
HybridStateMachineTTT <|-- HybridStateMachineTT
HybridStateMachineTTT <|-- HybridStateMachineT
HybridStateMachineTTT <|-- HybridStateMachine
ActionStateTT ..|> IActionableT
StateMachineTTT ..|> IActionableT
This diagram can also explain some minor inconveniences found in UnityHFSM. Consider the following example:
new State(onEnter: state => print("Hello World!"));
The function that is passed in actually does not take the state as type State
as its parameter, but as State<string, string>
. This is because State<string, string>
is the class that defines the actual logic, whereas State
merely inherits from this class to fill in the generic type parameters.
The same applies to the other types (states and transitions), too.
- Base class of all states. It's basically an empty state without any functionality.
new StateBase(needsExitTime: false, isGhostState: false);
- Generic type parameters:
StateBase<TStateId>
var state = new StateBase<int>(needsExitTime: false);
var fsm = new StateMachine<int>();
fsm.AddState(5, state);
- Generic overloads:
-
StateBase
=StateBase<string>
-
- Base class of all states that should support custom, user-defined actions.
- Internally, it uses the
ActionStorage
class to store the custom actions.
new ActionState(needsExitTime: false)
.AddAction("OnGameOver", () => print("GameOver"))
.AddAction<float>("OnHit", damage => print($"Damage: {damage}"));
- Generic type parameters:
ActionState<TStateId, TEvent>
- Generic overloads:
-
ActionState
=ActionState<string, string>
-
ActionState<TStateId>
=ActionState<TStateId, string>
-
- Default state type.
- Allows you to define custom logic for when the state enters (i.e. becomes the active state), when the next update (
OnLogic
) occurs or when the state exits. - If the state
needsExitTime
and you can define a custom function to determine when the state can exit (canExit
). - If you don't need
onEnter
/onExit
/canExit
/needsExitTime
, you can simply leave them with their default value (Same applies to the other state and transition classes). - Supports actions.
new State(
onEnter: self => {
// Called when this state becomes the active state.
},
onLogic: self => {
// Called every frame.
},
onExit: self => {
// Called when the state machine transitions to another state.
},
canExit: self => {
// Called while a transition is pending to check whether the state
// can exit.
return false; // Default.
},
needsExitTime: false,
isGhostState: false
);
- Generics and overloads are the same as
ActionState
(State<TStateId, TEvent>
)
-
Like the
State
class but allows you to run a Unity coroutine as its logic / update method. See the README for a longer description and a more advanced example. -
Define a coroutine:
IEnumerator MyCoroutine()
{
// ...
yield return null;
// ...
yield return new WaitForSeconds(2);
}
- Create the
CoState
:
new CoState(
this, // Pass in the MonoBehaviour that should run the coroutine.
MyCoroutine,
onEnter: self => { },
onExit: self => { },
canExit: self => false,
loop: true // If true, the coroutine is run again once completed.
needsExitTime: false,
isGhostState: false
);
- If you don't need
onEnter
/onExit
/needsExitTime
, you can simply leave them with their default value (Same applies to the other state and transition classes).
new CoState(
this,
MyCoroutine
);
- Generics and overloads are same as
ActionState
(CoState<TStateId, TEvent>
)
- State class that allows you to run multiple states in parallel.
new ParallelStates(
new State(onLogic: self => MoveTowardsPlayer()),
new State(onLogic: self => Animate())
);
- By default, when a transition by the parent state machine is requested, it will instantly exit, regardless of whether its child states need exit time. This can be changed by setting
needsExitTime
to true, which will make it wait until any child state callsStateCanExit()
before exiting. If this is undesired or other exit behaviour is wanted, you can set thecanExit
function which determines when theParallelStates
state may exit.
new ParallelStates(
canExit: self => IsPlayerInRange(),
needsExitTime: true,
new State(onLogic: s => MoveTowardsPlayer()),
new State(onLogic: s => Animate())
);
- The states to run can also be added using the
AddState(name, state)
method. The name parameter is only used for debugging purposes:
var state = new ParallelStates();
state.AddState("Move", new State(onLogic: s => MoveTowardsPlayer()));
state.AddState("Animate", new State(onLogic: s => Animate()));
fsm.AddState("FollowPlayer", state);
-
If the states are added in the constructor and you are using the non-generic
ParallelStates
class that uses strings, then the states are implicitly assigned names ("0"
,"1"
, ...). If you add the states in the constructor of a generic version of theParallelStates
class, then the states become nameless, which means that callingGetActiveHierarchyPath()
will not show as much information. -
If all the child states have names, then calling the
GetActiveHierarchyPath()
gives you useful debug information:
// When the "FollowPlayer" state is active...
print(fsm.GetActiveHierarchyPath()); // Prints "FollowPlayer/(Move & Animate)"
-
Calling
GetActiveHierarchyPath()
on a more complex parallel state machine that has nested state machines may return something like this:"/FollowPlayer/(Move/Jump & Animate & Defend/Block)"
-
Generic type parameters:
ParallelStates<TOwnId, TStateId, TEvent>
-
TOwnId
: Type of the name / identifier of theParallelStates
state in the parent state machine. -
TStateId
: Type for the names / identifiers of the child states. These names fulfil no direct function and are only meant for debugging purposes (they appear when callingGetActiveHierarchyPath()
). -
TEvent
: Type for the events / action triggers.
-
-
Generic overloads:
-
ParallelStates
=ParallelStates<string, string, string>
-
ParallelStates<TStateId>
=ParallelStates<TStateId, TStateId, string>
-
ParallelStates<TStateId, TEvent>
=ParallelStates<TStateId, TStateId, TEvent>
-
The base class of all state machines is the StateMachine<TOwnId, TStateId, TEvent>
class.
- Default state machine class.
- See the README for an introduction.
var fsm = new StateMachine(
needsExitTime: false, // Only when used as a nested fsm.
isGhostState: false, // Only when used as a nested fsm.
// Only when used as a nested fsm: If true, the state machine will
// return to its last active state when it enters, instead of its
// original start state.
rememberLastState: false
);
- Generic type parameters:
StateMachine<TOwnId, TStateId, TEvent>
-
TOwnId
: Only relevant for nested state machines: When this state machine is a nested state machine, i.e. it is a state inside a parent state machine,TOwnId
determines the type of its name / identifier in the parent fsm. -
TStateId
: Type of the names / identifiers of states in this state machine. -
TEvent
: Type for the events / action triggers.
-
- Generic overloads:
-
StateMachine
=StateMachine<string, string, string>
-
StateMachine<TStateId>
=StateMachine<TStateId, TStateId, string>
-
StateMachine<TStateId, TEvent>
=StateMachine<TStateId, TStateId, TEvent>
-
- A state machine that allows you to define custom behaviour (for enter / update / logic, and custom actions) like the
State
class. - For more information, see the wiki.
var fsm = new HybridStateMachine(
beforeOnEnter: self => {
// Called before the OnEnter method of the start state when the parent
// state machine switches to this state machine.
},
afterOnEnter: self => {
// Called after the OnEnter method of the start state.
},
beforeOnLogic: self => {
// Called on each OnLogic call before the OnLogic method
// of the active state.
},
afterOnLogic: self => {
// Called after the OnLogic method of the active state.
},
beforeOnExit: self => {
// Called before the OnExit method of the active state when the parent
// state machine switches to another state, exiting this state machine.
},
afterOnExit: self => {
// Called after the OnExit method of the active state.
},
// ... (same as StateMachinee)
);
fsm.AddAction<float>("OnHit", damage => print(damage));
fsm.AddAction("OnGameOver", () => print("Game Over!"));
- Generics and overloads are the same as
StateMachine
(HybridStateMachine<TOwnId, TStateId, TEvent>
)