Inspiaaa / UnityHFSM

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

Flowgraph Generation #53

Open sandsalamand opened 1 month ago

sandsalamand commented 1 month ago

This pull request adds an extension class which generates an AnimatorController graph from a given StateMachine.

For example, it converts code like this:

Into this:

The ChasingMacro StateMachine expands into this when double-clicked:

Usage:

fsm.Init();
#if UNITY_EDITOR
fsm.PrintToAnimator(animatorName: "EnemyHFSM");
#endif

Details:

The animator is placed in the Assets/DebugAnimators/ folder by default. This can be changed through an optional argument to PrintToAnimator().

The first time an AnimatorController graph is generated, the states will be in auto-generated positions. You will need to organize them manually. On subsequent calls to PrintToAnimator(), existing state positions will be respected and unchanged.

In order to access some important fields of StateMachine, I needed to change a few fields from private to internal. The alternative would be embedding the PrintToAnimator code directly inside StateMachine in a giant #if UNITY_EDITOR block.

sandsalamand commented 1 month ago

Addresses #52

sandsalamand commented 1 month ago

You can also get some runtime debugging by assigning the auto-generated AnimatorController to an Animator component in your scene, and then calling this code after you call fsm.OnLogic()

string stateName = fsm.GetActiveNestedStateName();
int id = Animator.StringToHash(stateName);
if (stateMachineDebugAnimator.HasState(0, id))
    stateMachineDebugAnimator.Play(stateName);
/// <summary>
/// Gets the active state name, even if it's a nested state of the supplied root state machine
/// </summary>
public static string GetActiveNestedStateName<TOwnId, TStateId, TEvent>(this StateMachine<TOwnId, TStateId, TEvent> rootStateMachine)
{
    var nestedStateMachine = rootStateMachine.ActiveState as StateMachine<TOwnId, TStateId, TEvent>;
    int emergencyEscape = 100;
    while (nestedStateMachine is not null && emergencyEscape > 0)
    {
        var possibleNestedStateMachine = nestedStateMachine.ActiveState as StateMachine<TOwnId, TStateId, TEvent>;
        if (possibleNestedStateMachine is not null)
            nestedStateMachine = possibleNestedStateMachine;
        else
            return nestedStateMachine.ActiveStateName.ToString();

        emergencyEscape--;
    }

    return rootStateMachine.ActiveStateName.ToString();
}