[Bug] Nested state machines don't invoke StateChanged event on the root state machine
UnityHFSM Version: 2.1.0
When a state machine is nested inside another state machine, the StateMachine.StateChanged event of the parent (root) machine will not be called for any state changes inside the nested machine.
Not quite sure if this is intended behavior or not, thus labelling this as an issue for now.
If this indeed is intended behavior, I'd recommend adding a note to the StateChanged event XML comment. Furthermore, a HierarchyChanged event could be implemented to allow users to get feedback on state changes without polling.
My reasoning for this is that I don't want to keep polling the StateMachine.GetActiveHierarchyPath() method each frame, since it generates garbage with the string concatenations.
Example script that showcases the behavior:
using UnityEngine;
using UnityHFSM;
namespace UnityHFSMTest
{
/// <summary>
/// This example demonstrates how the <see cref="StateMachine.StateChanged"/> event does NOT get triggered when a nested state machine changes its state.
/// </summary>
public class UnityHFSMStateChangedTest : MonoBehaviour
{
private StateMachine _rootFsm;
private void Awake()
{
_rootFsm = new StateMachine();
// ----- Root States -----
// State A: Normal state that waits for one second before transitioning to the next state.
_rootFsm.AddState("State A",
onEnter: _ =>
{
print("Enter state A");
},
onLogic: state =>
{
if (state.timer.Elapsed > 1)
state.fsm.StateCanExit();
},
needsExitTime: true
);
// State B: A state machine, that contains two states (X and Y).
StateMachine stateBFsm = new(needsExitTime: true);
stateBFsm.AddState("Nested X",
onEnter: _ =>
{
print("Enter state B-X");
},
onLogic: state =>
{
if (state.timer.Elapsed > 1)
state.fsm.StateCanExit();
},
needsExitTime: true
);
stateBFsm.AddState("Nested Y",
onEnter: _ =>
{
print("Enter state B-Y");
},
onLogic: state =>
{
if (state.timer.Elapsed > 1)
state.fsm.StateCanExit();
},
needsExitTime: true
);
// Add state B transitions. Nested Y is an exit transition.
stateBFsm.AddTransition(new Transition("Nested X", "Nested Y"));
stateBFsm.AddExitTransition(new Transition("Nested Y", ""));
// Add the state machine to the root FSM.
_rootFsm.AddState("State B", stateBFsm);
// ----- Root Transitions -----
// Alternate between "state A" and "state B".
_rootFsm.AddTransition(new Transition("State A", "State B"));
_rootFsm.AddTransition(new Transition("State B", "State A"));
}
private void OnEnable() => _rootFsm.StateChanged += OnStateChanged;
private void OnDisable() => _rootFsm.StateChanged -= OnStateChanged;
private void Start() => _rootFsm.Init();
private void Update() => _rootFsm.OnLogic();
private void OnStateChanged(StateBase<string> newState)
{
print($"StateChanged @: {newState.name}");
}
}
}
Expected output: "StateChanged @ stateName" Debug.Log call for each state change.
Actual output:
Debug.Log call for only "State A" and "State B" (the highest-up states in the hierarchy):
If this is the intended behavior and you give me the thumbs-up, I can submit a PR to clarify the event docs and potentially implement the HierarchyChanged event.
@japsuu see also this: https://github.com/Inspiaaa/UnityHFSM/issues/29
Hi @japsuu,
Thanks for the bug report. As @tomsseisums has mentioned, this is the current intended behaviour of this feature. One of the main reasons why there is no such event for state changes in child state machines, is that the use of generics makes it very hard to implement for the general case. An event in C# needs a fixed type, which means that the parameter type used for the new state also has to be fixed. This becomes problematic for state hierarchies, as each child state machine can use a different type for the state IDs.
However, there are solutions:
- A 'hierarchy changed' event could be restricted to state machines that use the same type as the root state machine.
- An implementation via an interface with a generic method is able to support arbitrary types. This is similar to the way that the new visitor pattern /
IStateVisitor/StateMachineWalkerare able to traverse arbitrarily-typed state hierarchies.
Furthermore, I have an implementation in mind that should be very efficient. When the hierarchy changed event is not used, it should not cause any negative effect of performance.
However, the most important question regarding the design of this event and a possible improvement of the existing StateChanged event is how this feature is intended to be used in real-world scenarios. I believe the most important thing is understanding the practical applications and needs that would drive this feature's design. For example, is the event primarily for logging state transitions, handling some form of global state change, or perhaps for triggering specific behaviours in other parts of the game when a state change occurs, even at deeper levels of the hierarchy?
If you have any concrete use cases in mind, I'd love to hear more about them before finalizing the design. This will help ensure the solution fits real-world needs and is both efficient and flexible.