Inspiaaa / UnityHFSM

A simple yet powerful class-based hierarchical finite state machine for Unity
MIT License
951 stars 110 forks source link

[Bug] Nested state machines don't invoke StateChanged event on the root state machine #49

Open japsuu opened 3 weeks ago

japsuu commented 3 weeks ago

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): image

japsuu commented 3 weeks ago

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.