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.