Ok I got a simpler example working, although I ran into some issues I had to fix, and noticed some other potential issues that are not easily fixable in this system - it is kinda flawed unless you can guarantee one super-object (like the Master) that always has the same absolute path.
So I set up the following classes :
#pragma once
#include "CoreMinimal.h"
#include "MyObjectA.generated.h"
UCLASS(Blueprintable, BlueprintType)
class UMyObjectA : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(SaveGame) int32 SomeInt;
UPROPERTY(SaveGame) FString SomeStr;
UPROPERTY(SaveGame) UMyObjectA* SubObject;
UFUNCTION(BlueprintCallable)
void Print(const FString& Indent)
{
UE_LOG(LogTemp, Log, TEXT("%s--BEGIN PRINT %s"), *Indent, *GetFullName());
UE_LOG(LogTemp, Log, TEXT("%s SomeInt = %i"), *Indent, SomeInt);
UE_LOG(LogTemp, Log, TEXT("%s SomeStr = \"%s\""), *Indent, *SomeStr);
UE_LOG(LogTemp, Log, TEXT("%s SubObject = (%p) %s"), *Indent, SubObject, SubObject ? *SubObject->GetFullName() : TEXT("null"));
if (SubObject)
SubObject->Print(Indent+" ");
UE_LOG(LogTemp, Log, TEXT("%s--END PRINT %s"), *Indent, *GetFullName());
}
};
#pragma once
#include "CoreMinimal.h"
#include "MyObjectA.h"
#include "MasterStateObj.generated.h"
UCLASS(Blueprintable, BlueprintType)
class UMasterStateObj : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(SaveGame) UMyObjectA* DirectRef;
UPROPERTY(SaveGame) TArray<UMyObjectA*> ArrayRefs;
UFUNCTION(BlueprintCallable)
void Print()
{
UE_LOG(LogTemp, Log, TEXT("--BEGIN PRINT %s"), *GetFullName());
UE_LOG(LogTemp, Log, TEXT(" DirectRef (%p) %s"), DirectRef, DirectRef ? *DirectRef->GetFullName() : TEXT("null"));
if (DirectRef)
DirectRef->Print(" ");
UE_LOG(LogTemp, Log, TEXT(" ArrayRefs.Num = %i"), ArrayRefs.Num());
for (int32 i = 0; i < ArrayRefs.Num(); i++)
{
UE_LOG(LogTemp, Log, TEXT(" [%i] = (%p) %s"), i, ArrayRefs[i], ArrayRefs[i] ? *ArrayRefs[i]->GetFullName() : TEXT("null"));
if (ArrayRefs[i])
ArrayRefs[i]->Print(" ");
}
UE_LOG(LogTemp, Log, TEXT("--END PRINT %s"), *GetFullName());
}
UFUNCTION(BlueprintCallable)
void CreateDummyData()
{
DirectRef = NewObject<UMyObjectA>(this);
DirectRef->SomeInt = 41;
DirectRef->SomeStr = "First object";
// also reference our first object from the array
ArrayRefs = { DirectRef };
{
UMyObjectA* Dummy = NewObject<UMyObjectA>(this);
Dummy->SomeInt = 42;
Dummy->SomeStr = "Second object";
ArrayRefs.Add(Dummy);
}
{
UMyObjectA* Dummy = NewObject<UMyObjectA>(this);
Dummy->SomeInt = 43;
Dummy->SomeStr = "Third object";
ArrayRefs.Add(Dummy);
Dummy->SubObject = NewObject<UMyObjectA>(Dummy);
Dummy->SubObject->SomeInt = 431;
Dummy->SubObject->SomeStr = "Subobject of third object";
}
}
};
Added this to my game instance
Saving with the following code
Output log
Loading with the following code :
So the first two issues :
- Bunch of logs Failed to find ‘None.None’
- Master was deserializing fine, but all subobjects logged “Unable to find Outer for object (invalid array object)”
Those issues are from the same source. During save, the TempObjects array is filled with objects, and then serialized to disk as string references when calling SaveGameToSlot. Upon loading, those string references fail to load so it logs “failed to find None.None”, and the TempObject array is now filled with None objects.
When loading objects, the code appends newly loaded objects into TempObjects array. Subobjects store an index (OuterID) to reference their Outer within the TempObjects array. That array is filled with None objects so it fails.
Tagging TempObjects as Transient fixes those issues. However it’s still important to realize the loading mechanism here is heavily dependent on the order of the objects. You MUST have the outer-most objects before the dependent objects. GetObjectsWithOuter()
does not guarantee that order. But I’d say as long as you are not getting any “Unable to find Outer for object (invalid array object)” you look fine.
After fixing this, second problem : launching a PIE session to save, and then launching another PIE session to restore, was not working.
The reason for this is, every PIE session increases the counter for GameInstance and so changes its name.
As you can see in your own logs, when you save, your objects have parent path :
/Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0
And when you try to load, the parent path is :
/Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1
So all the references now fail to load.
In theory this could be a non-issue in standalone game.
But in PIE it’s gonna be a PITA and I don’t know how to avoid this.
Launch editor, save (with GameInstance_0), restart editor, try to load (with GameInstance_0) and now it works…
Lastly, as you may have noticed in the blueprint code, I also had to re-assign the GameInstance->MasterObj variable, because there’s nothing to save that. Restoring objects is one thing. Restoring variables that are supposed to point to those objects, is another.
As mentioned earlier, including GameInstance in the serialized objects is probably not a good idea. Using a single master object is good as it makes it easy to restore, like I did.
The reason it works much better with GameState, is because it keeps the same full path across PIE sessions :
/Game/UEDPIE_0_TestMap.TestMap:PersistentLevel.BP_GameState_C_0
None of those parts change across PIE sessions, unless you change map, then all hell will break loose again.