Inspiaaa / UnityHFSM

A simple yet powerful class-based hierarchical finite state machine for Unity
MIT License
1.05k stars 121 forks source link

StateCanExit always calls changing state of base statemachine. #14

Closed JKot-Coder closed 8 months ago

JKot-Coder commented 2 years ago

I implement a simple chain of states like:

A => submachine state B => submachine state C => D

using UnityEngine;
using FSM;

public class Test : MonoBehaviour
{
    StateMachine stateMachine = new StateMachine();

    private class SimpleState : StateBase
    {
        public SimpleState(bool needsExitTime) : base(needsExitTime) { }
        public override void OnEnter() { Debug.Log("Enter: " + name); }
        public override void OnLogic()
        {
            if (Input.GetMouseButtonUp(0))
            {
                Debug.Log("Can exit: " + name);
                fsm.StateCanExit();
            }
        }
    }

    void Start()
    {
        var submachine = new StateMachine(needsExitTime: true);

        submachine.AddState("Submachine state B", new SimpleState(true));
        submachine.AddState("Submachine state C", new SimpleState(true));

        submachine.AddTransition("Submachine state B", "Submachine state C");

        submachine.SetStartState("Submachine state B");

        stateMachine.AddState("A", new SimpleState(true));
        stateMachine.AddState("D", new SimpleState(true));
        stateMachine.AddState("Submachine", submachine);

        stateMachine.AddTransition("A", "Submachine");
        stateMachine.AddTransition("Submachine", "D");

        stateMachine.SetStartState("A");

        stateMachine.Init();
    }

    void Update()
    {
        stateMachine.OnLogic();
    }
}

What I expected: Changing state one by one every mouse click.

What I get:

Enter: A Mouse click. Can exit: A

Enter: Submachine state B Mouse click. Can exit: Submachine state B

Enter: Submachine state C

The problem is this: the state of the root state machine instantly changes to "D". This happens because StateCanExit always propagates StateCanExit to the root state machine without any mechanism defining this behavior.

Enter: D

burakkurkcu commented 1 year ago

I think we have this problem as well, currently we implemented a workaround for this by separating sub machine as states to the main state machine, instead of using sub machine as a state.

jszung commented 1 year ago

I have this issue also. Lets say there is a statemachine similar to above containing state A, SubstateMachine, and State E, where the states B and C and D are in the Substate machine. Normal transitions are defined in the base state machine from A->Substate and Substate->E. Substate transitions are defined from B->C and C-D. You would think that once the substate machine's transitions are complete (B->C and C->D) then base state transtion Substate->E will occur. But what happens is once substate B finishes, the base state transition of Substate->E will fire and force state C to simply exit immediately, skipping the rest of the Substates, and then continue to state E. This essentially makes the use of substate machines useless unless you do NOT define a normal transition in the base state machine from the Substate->E and use a trigger transition instead called from the last substate. This is a nasty bug to track down and am looking into a fix for it but if the the creator Inspiaaa could add some insight or a quick fix that would be great.

JKot-Coder commented 1 year ago

@jszung You can take a look at this commit https://github.com/JKot-Coder/UnityHFSM/commit/76214843ce7436387b31bd9f9ca3e6a89d44606a, it works fine for me. After this fix, you should explicitly call fsm.StateCanExit() from the SubstateMachine to toggle the state of the high-level state machine.

Inspiaaa commented 1 year ago

Thanks for pointing out this issue. I have been working on a fix for this problem in another branch, but have not merged it into the master branch yet, although I intend to in the future.

In the meantime, you can already use it by cloning the feature/exit-states branch (https://github.com/Inspiaaa/UnityHFSM/tree/feature/exit-states) which introduces two new features.

The main problem in the current version is that you can only define transitions between two states within one state machine, but cannot define the exit condition that leads to a transition "up the hierarchy"; it is implicitly assumed.

This branch aims to fix the issue by introducing the concept of an "exit state", a state in which the state machine is allowed to exit up the hierarchy. In @jszung 's example, we would declare D the exit state. Once the state machine SubstateMachine enters D and has a transition pending itself (to E), it will exit to E. This system also respects the needsExitTime behaviour of the exit state: when the exit state can safely transition, the state machine can exit.

You can mark a state as an exit state in the constructor:

var state = new State(isExitState: true);

or using the shortcut methods:

subStateMachine.AddState("D", isExitState: true);

For this to work, the subStateMachine needs its needsExitTime property set to true.

For another example, you can have a look at the tests: https://github.com/Inspiaaa/UnityHFSM/blob/feature/exit-states/Tests/TestHierarchicalTiming.cs

Unrelated to the issue above, this branch also introduces "ghost states". These are states that the state machine does not want to remain in and seeks to exit as soon as possible. Once the fsm enters a ghost state, it will immediately try to exit if there is an outgoing transition (of course respecting the needsExitTime behaviour of the ghost state). This means that multiple transitions can occur in one OnLogic call.

States can be marked as ghost states in a similar fashion:

var state = new State(isGhostState: true);

An example can be found in the tests: https://github.com/Inspiaaa/UnityHFSM/blob/feature/exit-states/Tests/TestGhostStates.cs

Let me know if this helps.

niwho commented 1 year ago

version 1.9

stateMachine.AddTransition("Submachine", "D", (t) =>
{
        return submachine.ActiveStateName == "Submachine state C";
});

add condition can resolve the problem. the condition prevent root machine pending until submachine entering "Submachine state C". so there is nothing to fix in my opinion.

ps:I prefer using trigger in most situation

Inspiaaa commented 8 months ago

After working some more on this issue, I have developed a new, more versatile solution. It takes a slightly different approach to the "exit states" that I had proposed and implemented previously.

In my new solution, instead of declaring in which states the parent FSM may exit and move to the next state, you can explicitly define the exit transitions themselves. This allows you to leverage the full power of the existing transition and timing systems.

This new feature is part of the recent 2.0 release. For an installation guide, please see the README.

For usage examples and documentation you can take a look at the README, the quick feature overview, or the advanced guard AI tutorial.