GC FSM - Event-driven, hierarchical finite state machines in blueprint

I see. What is happening here is that the when an event is triggered it is queued in all FSMs that are currently available and are in the target policy. The problem is that when the main FSM is in the Traveling state, the Combat sub-FSM has not yet been created, so it can’t get the event. In fact you have:



LogGCFSM (VeryVerbose) FSM Adventure (in state Traveling, class AdventureState_Traveling_C): event ToCombat queued (0 matching events discarded from queue)
LogGCFSM (VeryVerbose) FSM Adventure (in state Traveling, class AdventureState_Traveling_C): event NPCTurn queued (0 matching events discarded from queue)


When the Combat sub-FSM gets created, it’s too late and it won’t get the event.

(Compare with the different case, when you later you trigger the Stop event. In that case you see



LogGCFSM (VeryVerbose) FSM Adventure (in state Combat, submachine Combat): event Stop queued (0 matching events discarded from queue)
LogGCFSM (VeryVerbose) FSM Adventure/Combat/Combat (in state Player's Turn, class AdventureState_Combat_C): event Stop queued (0 matching events discarded from queue)


and the Stop event is thus properly queued in the Combat sub-FSM, although it won’t handle it.)

Although a bit surprising, the behaviour now makes sense to me. I can’t classify this as a bug, although I agree that your use-case is legit. I’m not sure what would be the best course of action to make it work, though. I can’t just queue all parent-FSM events to the sub-FSM when it’s being created, since the original target policy may forbid it and that information is not longer available.

To solve your issue, you can either:

  1. wait for a tick before triggering the second event (ugly, but gives the sub-FSM the time to appear and get the event)
  2. use a bool to select the first state to enter in the Combat FSM. Although maybe less elegant, this approach as the advantage that you never enter the “Player’s Turn” state when the NPC’s turn is expected to be the first, so in the end it might actually be more correct (for some definition of “correct”).

Edit: actually the “To Combat” event is superfluous. Just use the NPCTurn and and PlayerTurn event right in the Traveling state.

Just an idea for save/reload: Some kind of functionality that makes it possible to go directly to a state only when the FSM is launched might work? So in order to use it again, the FSM must be stopped and restarted. That might be enough of a deterrent to prevent bypassing the graph structure willy-nilly…

It was about knowledge separation (I assume that a lot more states will be added into both adventure and combat state machines so I’ve tried to minimize dependencies). I think not using submachine at all is better than add-boolean fix.

I’ve just added initial “Idle” state to avoid possible issues in future.

At the end I think I will try delay fix or just leave it as is (simple flat SM) for now.
Thanks a lot for the help!

My two cents :slight_smile: If your SM has a chain of states each of which changes game state somehow it can be incorrect to just skip to the last one when game is loaded (assuming the game state is complex). Or you will need to carefully duplicate logic in each state somehow (thinking about which state game is allowed to be saved in etc.). I would just trigger events in the same way both during the game and when game is loaded to ensure game state consistency.

Well, if your states are modifying game properties, it’s easier and more efficient to just save and restore those properties directly, isn’t it? :wink:

Re-triggering states might work in some narrow cases, but it’s not a general solution, since the path to a state is not generally unique. You cannot observe the machine’s current state and infer how it got there, except in very limited cases (e.g., each state has one parent and the FSM is acyclic). And there are plenty of situations where re-triggering each parent state produces incorrect results, too. Plus, if your states are doing performance-hungry things on entry/exit, like loading/unloading sublevels, it’s really not a good idea to force the game to do an entire sequence of those tasks if it only needs the last one.

Agreed, I think I am just using plugin for different kind of stuff. :slight_smile:

Will this plugin support serialization at some point? I am trying to write it into the plugin myself but the code is not really serialization friendly right now. The approach I’m trying right now is to make everything a UPROP and non-transient for the time being and then I have to recreate all the state and FSM objects on load and then somehow invoke the correct entry points to recreate the BP stack.

Okay so I ended up writing a custom serialization approach where I take advantage of the replication features (activeNodeGuid) to “replicate” the state stack on deserialization.

Sounds interesting, mind describing it a little?

I still think the plugin could really benefit from a built-in way of restoring states from savegames. Hopefully the author is looking into it.

I’m also interested…

Yes, I confirm that I’m looking into that. The idea of exploiting the network replication features, which include the activeNodeGuid, is precisely what I thinking about.

So I’m using FJsonObjectConverter (and some customizations I made to it) for runtime serialization. I serialize saves to a .json and can load saves from said json. The file is pretty human readable right now so I’ll be sharing excerpts from a save.

As part of this custom saving logic I wrote, I’m serializing almost all INNER objects of an object I serialize. This as a result catches the GCFSMRoot_0 that gamecentric creates for all objects with at least one statemachine. Here is the GCFSMRootState subobject on one of my actors. The json serialization stuff I’m doing catches all UPROPERTIES and subobjects. I initially added UPROPERTY to all the GCFSM and GCFSMState classes and while I deserialized those things okay, getting the latent executors to work was nigh impossible so I went with the replication approach instead. I stopped serializing the subobjects of GCFSMRootState, and instead added a custom serialization pass that serializes the FSM entry point names and the activeNodeGuid of that FSM. It looks kinda ugly as you can see below but it works.



{
    "objectClass": "/Script/GCFSM.GCFSMRootState",
    "objectName": "GCFSMRootState_0",
    "objectFlags": 0,
    "fSMs_serialized":
    {
        "Root":
        {
            "48CC44D249CBCE21EE881B9ED0D9183F":
            {
                "fSMs_serialized":
                {
                    "PlacedGraph":
                    {
                        "5765360444F49C21837FD3B0C8B69D0A":
                        {
                            "fSMs_serialized":
                            {
                                "OperationalGraph":
                                {
                                    "16A2C4A1417F200B30BAD2B1EC2BED3D":
                                    {
                                        "fSMs_serialized":
                                        {
                                            "OperationalOnGraph":
                                            {
                                                "48181F2347D84A8C6ED67C9AC7C65A05":
                                                {
                                                    "fSMs_serialized":
                                                    {
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "contextObject": "/Game/Maps/RND/Protomap_01/UEDPIE_0_Protomap_01_Map.Protomap_01_Map:PersistentLevel.CS_LaborShelter_C_2",
    "rootState": "/Game/Maps/RND/Protomap_01/UEDPIE_0_Protomap_01_Map.Protomap_01_Map:PersistentLevel.CS_LaborShelter_C_2.GCFSMRootState_0",
    "time": 1869.96630859375,
    "_stateName": "<root>"
}


To get the state machine started in our code base I have to have the blueprint context ready to go. So this is how I start my recursive launch of any FSMs under that were started under our StateMachineComponent.



void PostActorConstruction(ULevel* LevelToLoadInto, TSharedPtr<FJsonObject> ActorJson)
{
    auto ActorObjectName = ActorJson->GetStringField(JsonField::ObjectName);
    if (auto Actor = FindObject<AActor>(LevelToLoadInto, *ActorObjectName))
    {
        // start fsm
        if (auto RootState = Cast<UGCFSMRootState>(UGCFSMUtilities::GetRootStateFromContext(Actor)))
        {
            RootState->StartTicker(); // #note: custom code i added to GCFSMRootState

            TSharedPtr<FJsonObject> RootStateJson;
            auto InnersJson = ActorJson->GetArrayField(JsonField::InnerObjects);
            for (auto JsonValue : InnersJson)
            {
                auto InnerObjectJson = JsonValue->AsObject();
                if (InnerObjectJson->GetStringField(JsonField::ObjectName).Equals(RootState->GetName()))
                {
                    RootStateJson = InnerObjectJson;
                    break;
                }
            }

            // we found this rootstate in json so we need to launch its fsms
            if (RootStateJson.IsValid())
            {
                // this next part is a bit of a hack.. only starts the statemachinecomponent's fsm
                if (auto StateMachineComponent = Actor->FindComponentByClass<UBBStateMachineComponent>())
                {
                    bLoadingFSM = true;
                    LaunchStateFSMsFromJson(RootState, RootStateJson, StateMachineComponent->RootState);
                    bLoadingFSM = false;
                }
            }
        }


This is how I recursively launch the state machine.



// BEGIN RECURSIVE FSM LOADING

    void LaunchStateFSMsFromJson(UObject* StateOrContext, TSharedPtr<FJsonObject> StateJson, UObject* BlueprintContext = nullptr);

    void EnterStateFromFSMJson(UGCFSM* FSM, TSharedPtr<FJsonObject> FSMJson)
    {
        TArray<FString> ActiveNodeGuidBuffer;
        if (FSMJson->Values.GetKeys(ActiveNodeGuidBuffer) > 0)
        {
            FGuid ActiveNodeGuid;
            FGuid::Parse(ActiveNodeGuidBuffer[0], ActiveNodeGuid);

            FSM->ReplicatedExitState(NAME_None); // #note the state machine could have automatically entered the correct state or the wrong state, i just back out of whatever state it may have entered before entering the state i want to enter
            FSM->ReplicatedEnterState(ActiveNodeGuid);

            LaunchStateFSMsFromJson(FSM->GetActiveState(), FSMJson->GetObjectField(ActiveNodeGuidBuffer[0]));
        }
    }


    void LaunchStateFSMsFromJson(UObject* StateOrContext, TSharedPtr<FJsonObject> StateJson, UObject* BlueprintContext/* = nullptr*/)
    {
        auto FSMsJson = StateJson->GetObjectField(JsonField::InnerFSMs);

        for (auto FSMJsonPair : FSMsJson->Values)
        {
            FName FSMName = *FSMJsonPair.Key;
            UGCFSMUtilities::LaunchFSM(StateOrContext, FSMName, EGCFSMReplicationOptions::NonReplicated, BlueprintContext);

            UGCFSMState* ActiveState = Cast<UGCFSMState>(StateOrContext);
            if (ActiveState == nullptr)
            {
                ActiveState = UGCFSMUtilities::GetRootStateFromContext(StateOrContext);
            }

            if (ActiveState)
            {
                if (UGCFSM* FSM = ActiveState->GetFSM(FSMName))
                {
                    EnterStateFromFSMJson(FSM, FSMJsonPair.Value->AsObject());
                }
            }
        }
    }

// END RECURSIVE FSM LOADING


Here is how I add my GCFSMRootState when the object gets loaded. Also commented out above it is the old attempt to just reload the FSM in the exact state that it was in before.



bool LoadNativePropertiesFromJsonObject(UObject* Object, TSharedPtr<FJsonObject> JsonObject)
{
// note: removed because this depends on serializing out the BP VM
//     if (auto FSM = Cast<UGCFSM>(Object))
//     {
//         auto& LatentExecutor = FSM->latentExecutor;
//         auto LatentExecutorJson = JsonObject->GetObjectField(TEXT("latentExecutor"));
//         LatentExecutor.target = FindImportedObject<UObject>(Object, LatentExecutorJson->GetStringField(TEXT("target")));
//         LatentExecutor.function = FindImportedObject<UFunction>(Object, LatentExecutorJson->GetStringField(TEXT("function")));
//         LatentExecutor.linkage = static_cast<int32>(LatentExecutorJson->GetNumberField(TEXT("linkage")));
//         if (UProperty* EventNameProperty = LatentExecutor.function->FindPropertyByName(TEXT("CallFunc_EnterState_eventName")))
//         {
//             LatentExecutor.eventName = // this memory lives in the BP VM
//             *LatentExecutor.eventName = *LatentExecutorJson->GetStringField(TEXT("eventName"));    
//         }
//         if (UProperty* DeltaTimeProperty = LatentExecutor.function->FindPropertyByName(TEXT("CallFunc_EnterState_deltaTime")))
//         {
//             LatentExecutor.deltaTime = // this memory lives in the BP VM
//             *LatentExecutor.deltaTime = LatentExecutorJson->GetNumberField(TEXT("deltaTime"));    
//         }
//     } else
    if (auto RootState = Cast<UGCFSMRootState>(Object))
    {
        UGCFSMUtilities::AddRootStateForContext(RootState->GetOuter(), RootState);
    }


If you can’t tell, this is a lot of custom code. Some engine modifications for my JSON stuff. A decent amount of GCFSM changes.

Here’s how I convert the whole thing to JSON.



// BEGIN RECURSIVE FSM SAVING

    void ConvertStateToJson(const UGCFSMState*, TSharedRef<FJsonObject>);


    void ConvertFSMToJson(const UGCFSM* FSM, TSharedRef<FJsonObject> FSMJson)
    {
        if (auto ActiveState = Cast<UGCFSMState>(FSM->GetActiveState()))
        {
            auto StateJson = MakeShared<FJsonObject>();
            ConvertStateToJson(ActiveState, StateJson);
            FSMJson->SetObjectField(FSM->GetActiveNodeGuid().ToString(), StateJson);
        }
    }


    void ConvertStateToJson(const UGCFSMState* State, TSharedRef<FJsonObject> StateJson)
    {
        auto FSMsJson = MakeShared<FJsonObject>();
        StateJson->SetObjectField(JsonField::InnerFSMs, FSMsJson);

        for (auto FSMPair : State->GetMappedFSMs())
        {
            auto FSMJson = MakeShared<FJsonObject>();
            ConvertFSMToJson(FSMPair.Value, FSMJson);
            FSMsJson->SetObjectField(FSMPair.Key.ToString(), FSMJson);
        }
    }

// END RECURSIVE FSM SAVING

bool NativePropertiesSerialization(const UObject* Object, TSharedRef<FJsonObject> OutJsonObject)
{
//    // #note: this is commented out because we can't serialize the latentExecutor until we serialize the whole BP VM stack on which it relies
//     if (auto FSM = Cast<UGCFSM>(Object))
//     {
//         auto& LatentExecutor = FSM->latentExecutor;
//         auto LatentExecutorJson = MakeShared<FJsonObject>();
//         LatentExecutorJson->SetField(TEXT("target"), GetObjectReferenceAsValue(LatentExecutor.target));
//         LatentExecutorJson->SetField(TEXT("function"), GetObjectReferenceAsValue(LatentExecutor.function));
//         LatentExecutorJson->SetField(TEXT("linkage"), MakeShared<FJsonValueNumber>(LatentExecutor.linkage));
//         LatentExecutorJson->SetField(TEXT("eventName"), MakeShared<FJsonValueString>(LatentExecutor.eventName->ToString()));
//         LatentExecutorJson->SetField(TEXT("deltaTime"), MakeShared<FJsonValueNumber>(*LatentExecutor.deltaTime));
//         OutJsonObject->SetField(TEXT("latentExecutor"), MakeShared<FJsonValueObject>(LatentExecutorJson));
//     }

    if (const UGCFSMState* RootState = Cast<UGCFSMRootState>(Object))
    {
        ConvertStateToJson(RootState, OutJsonObject);
    }

    return true;
}


I’m probably leaving out a bunch of stuff, but I think I covered the most important parts.

Hi @Cobryis, that is very clever. However, there are a few things that I see troublesome in your approach and could be improved. First, the FSM is launched and then the correct state is restored. This means that you need to put a “dummy” state at the beginning of your FSM in order to avoid the FSM start doing something that it shouldn’t be doing. Second, correct me if I’m wrong, it seems that you approach only works for top-level FSMs, but each FSM may have nested FSMs, which will not be restored.
For those who are interested, I already have a working implementation of a more generic approach to save/restore the state of all FSMs of an object. The approach is meant to be easily integrated in any serialization scheme. I have introduced a new function “MakeFSMSnapshot” that creates a opaque snapshot object that represent the state of all running FSMs. The snapshot object can used as-is or be converted to/from a string representation for serialization/deserialization. Then you have a “RestoreFSMSnapshot” function to restore all the FSMs in the snapshot to their respective saved state (automatically starting the FSMs that aren’t currently running).
I still have to finalize the interface and do the final and regressions tests. There are a lot of corner cases and, although I’m sure I will miss something, I’d like to cover the most common use-cases right in the first release. I hope I will be able to submit it next week.

That sounds awesome, thank you @gamecentric ! :rolleyes:

  1. I would have preferred to just launch straight into the correct state. But your blueprint vm code was confusing (for me at least as someone who has never written code like that) so I gave up on trying to just have the blueprint launch immediately into the correct state (for the the sake of time). Right now it doesn’t really matter (for our project) if the FSM enters the wrong state first because it will eventually enter the right state and THEN all the properties I serialized out will be dumped on top of any changes made from state changes.

  2. No, my approach works for all nested FSMs. We have a deeply hierarchical FSM (thanks to your plugin) and it all launches correctly (look at my recursive functions again and the JSON data).

  3. I’m sure whatever solution you provide will be far superior.

Looking forward to this. Thanks for the discussion and updates!

New version has been submitted. Should be available for download in a few days.

…And it’s online! Documentation for the new feature at Saving and Restoring FSMs – gamecentric Feedbacks are welcome!

Looks great! I’ll try it out ASAP.

One question: If an FSM is currently running and then has a snapshot restored that puts it in a different state, is the current state’s OnExit event fired, or no?