Inspiaaa / UnityHFSM

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

StateMachine<T> and StateMachine needs a common StateMachine base type #27

Closed tomsseisums closed 5 months ago

tomsseisums commented 1 year ago
private StateMachine fsm;

Awake()
{
    // Fails, because this is StateMachine<T> and StateMachine<T> is not derived from StateMachine
    fsm = new HybridStateMachine(); 
}

or


private StateMachine fsm;

Awake()
{
    var nestedFSM = new HybridStateMachine();
    nestedFSM.AddState("INITIAL");

    fsm = new StateMachine();
    fsm.AddState("NESTED", nestedFSM);
}

Update()
{
    switch (fsm.ActiveState) {
        case StateMachine { name: "NESTED" } nestedFSM:
            // do something...
            // fails again, because HybridStateMachine isn't derived from StateMachine
            break;
    }
}
tomsseisums commented 1 year ago

Right, I remember another aspect to this, while technically we can use IStateMachine, AddState does not support IStateMachine.

And also IStateMachine is super limited if used to expose StateMachine for public use, it could make use Trigger and OnAction methods at least. Or give us two types, like IActionableStateMachine that derives from IStateMachine and exposes Trigger/OnAction.

tomsseisums commented 1 year ago

I guess a better example is this: image image image image

tomsseisums commented 1 year ago

And here the IStateMachine issue: image image

tomsseisums commented 1 year ago

And also IStateMachine is super limited if used to expose StateMachine for public use, it could make use Trigger and OnAction methods at least. Or give us two types, like IActionableStateMachine that derives from IStateMachine and exposes Trigger/OnAction.

Right, we have ITriggerable and IActionable, but no way to have both-in-one excluding other public methods.

Inspiaaa commented 1 year ago

Hi @tomsseisums,

Thanks for providing so much information!

The issue you are talking about is one of the minor inconveniences of the way that UnityHFSM is implemented internally regarding its support for generics.

StateMachine, StateMachine<T>, HybridStateMachine and other overloads of these classes do actually share a common base class - it's just not directly apparent.

The "actual" StateMachine implementation is in the generic version of the class: StateMachine<TOwnId, TStateId, TEvent>. The other overloads that require less parameters inherit from this base class and help you avoid boilerplate code when working with the defaults (strings).

As HybridStateMachine extends the behaviour of StateMachine, it simply inherits from this class. As it also has to support generics, it inherits from the aforementioned generic version of StateMachine.

If UnityHFSM didn't support generics, the inheritance hierarchy would therefore look like this:

classDiagram
    class StateMachine
    class HybridStateMachine

    StateMachine <|-- HybridStateMachine

With generics, it looks like this:

classDiagram
    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"]

    StateMachineTTT <|-- StateMachineTT
    StateMachineTTT <|-- StateMachineT
    StateMachineTTT <|-- StateMachine

    StateMachineTTT <|-- HybridStateMachineTTT

    HybridStateMachineTTT <|-- HybridStateMachineTT
    HybridStateMachineTTT <|-- HybridStateMachineT
    HybridStateMachineTTT <|-- HybridStateMachine

In other words, using StateMachine<TOwnId, TStateId, TEvent> as the type in your examples should fix the bugs.

As to the IStateMachine interface, I've explored different alternatives and reached the conclusion that the current implementation covers all practical conceivable cases at this point in time, without introducing additional complexity. As you have noticed, the IStateMachine interface was not intended to be used "publicly". Instead it creates a minimal layer of abstraction between the nested state machines, that allows each nested state machine to use its own generic types.

For more information on the use of generics, please check out the WIP README in the release branch for the 2.0 release or this issue.

I hope I could clarify this.

Inspiaaa commented 5 months ago

I have now written documentation on this topic which you can find in the wiki here.