Should I clear arrays of pointers before saving struct to GameSave?

My “game” asks questions to the user and records the user’s accuracy. Questions may relate to multiple topics or tags. Some questions relate to multiple topics, are the same question in a different format, etc. I record all variations under a parent class that contains all variations.

When the user started an exercise, I pull all the questions related to the topics/tags in the selected exercise and copy them to a TMap. I then populate various TArrays with pointers to the questions in the TMap to organise them by their recorded accuracy under each variation/topic/etc. All this info is inside a struct that is used while the exercise is active. When it is finished, I pull the results from the TMap and save them to a summary struct elsewhere.

Now, when the user exits mid-question, I save the interim struct so I can resume the exercise. I know that the pointers will be rubbish so when I reload I reprocess the arrays.

My question is, should I leave the pointers inside the TArraysor clear them before saving the struct that contains them?

At the moment I am not getting crashes but they are raw pointers. What would be the case if they are TSharedPtr?

Thanks a lot!
M

How are you saving that struct?

If you are using an array of struct pointers such as TArray<FSomeStruct*> Array, this is not supported by the reflection system, so the compiler won’t even let you mark it as an UPROPERTY. When using “automatic” savegame serialization, only UProperties are serialized, so that array would be skipped.

If your question class is an UObject class, then they are not exactly raw pointers. If your array looks like TArray<UQuestion*> Array and is marked as UPROPERTY, then the pointers will be serialized as string references. The game will try to resolve them upon reload, which is generally inconsistent (might end up null or point to a different object), but shouldn’t cause low level problems (crashes). Just don’t rely on it and reprocess the arrays as you did. You can mark those properties as “Transient” to skip saving them.

I actually need to test this properly. However, I have a few questions as it seems my understanding is obviously lacking. (I am starting to think I should have done everything in C++!).

What is considereed automatic? The way I have implemented my state holder UObjects is that they return the structs that contain the data through a function that is blueprintcallable. The GameSave, currently in blueprint, has a setter function for all the structs inside. So, when I want to save I just get the structs from all the UObjects and pass it to the setter function in the GameSave and then write it to disk (blueprint also).

So, my question is:
Since I am setting the GameSave structs variables in blueprint, when I set them the values that not marked UPROPERTY() do not get copied? I thought it just copies all the memory, even what is not “visible”.

Also, what about if you have a USTRUCT(BlueprintType) that contains other USTRUCT(BlueprintType) that contain fields that have not been marked as UPROPERTIES. If I set the “parent” struct in my blueprint function. Will it not copy the non UPROPERTIES?

The reason why I am not marking them UPROPERTIES is because I only manipulate them in C++ so I didn’t want them to clutter. I don’t know if that makes sense now…

Thanks a lot,
M

By automatic I mean using a SaveGame object subclass and calling SaveDataToSlot or something like that. What this does is iterate all the UProperties in the SaveGame object and serialize them (if they differ from defaults).

All blueprint properties are UProperties, so if your SaveGame subclass is a blueprint class and your structs are blueprint structs, you don’t have anything to worry about.

From what you just said, I gather your SaveGame is a blueprint class, and contains struct properties that were declared natively. These structs forcibly have USTRUCT(…) otherwise they wouldn’t even be visible in blueprints. However if the properties within those structs are not prefixed with UPROPERTY, they will not be serialized by default, unless you provide a custom serialization method and inform the compiler via some obscure TStructOpsTypeTraitsBase2 shenanigans. I don’t know much about that but here’s some more info.

When you set struct variable = other struct variable, in c++ or blueprints, yes it does a full memory copy (or calls the copy operator if applicable). UProperties do not matter in this case. Serialization is different though as stated above.

Same rules apply. When assigning to variables or passing as function argument, “parent” struct would be fully copied as a single memory block, including embedded child structs. Of course that’s only true if they are actually embedded structs, not if they are pointers. Pointers would be copied as-is but whatever they point to is not copied.
As for serialization, embedded structs will be serialized if they are marked as UPROPERTY, and their properties will be serialized if they are also marked as UPROPERTY. Unless you provide custom serialization as mentioned above.

Hey, I really appreciate your answer!

I still got a few questions to clarify my understanding and how some edge cases would be affected (TSharedPtrs, Refs, etc) that I would like to ask if that is OK with you.

I will write them up tomorrow to ensure they are clear and I don’t waste your time.

Cheers,
M

Hi @Chatouille, apologies for the delay. I ended up following this guide which uses FWCSaveGameArchive:

I was able to save sub-objects using the GetObjectsWithOuter(). Everything works as long as I keep the root object in GameStateBase.

However, I wanted to use GameInstance as I wanted to preserve all my data between levels.

As far as I know you cannot serialise TransientPackage and GameInstance is under Engine\Transient so when I restore it all pointers are null.

So, my question is:

How do I get around this? Do, I need to store everything in the GameMode and move it to the GameInstance before switching levels or is there some way around it?

Like changing the outer before saving, setting some other flag, or, some other magic?

I’ve tried serialising intermediate objects and restoring the children but they all fail when unserialise. I guess they store the whole path?

Thanks a lot!
M

What are you using GetObjectsWithOuter() with ? The entire current World ?
GameInstance is not part of the world, which is also why it preserves data between levels.

What’s the Outer for your objects ? That’s for you to specify when you create them (unless they are actors, then outer = world).

It shouldn’t matter if stuff is in TransientPackage or not. If I understand this plugin correctly you first call Actor/ObjectSaver with all the actors/objects you need, which will serialize and store them into TArray<FObjectRecord>, then you call SaveGameToSlot which will save the TArray to disk. Inter-dependencies within saved objects should resolve fine, external dependencies won’t.

Also as a side note, that system uses the same serializer as SaveGame so the same rules as previously discussed apply - only UProperties are serialized. Additionally, it will only save UProperties marked with the “SaveGame” flag.

Gonna need more info - what are you saving, what are you restoring, what are the error messages when they fail ?


I think you should be able to create all your objects with Outer=GameInstance, and then serialize them using GetObjectsWithOuter(GameInstance). Avoid serializing the GameInstance itself. If I get this right you need to emplace GameInstance into the PersistentOuters array (say, at index 0) before saving. And before restoring, also emplace the GameInstance into PersistentOuters at the same index, so the deserializer can resolve it with actual object.

Recap (again this is from my understanding, could be wrong)
Saving :

  • create save game object
  • set SaveGame.PersistentOuters[0] = GameInstance
  • gather objects to save via GetObjectsWithOuter(GameInstance)
  • call UObjectArraySaver with gathered objects
  • SaveGameToSlot

Restoring :

  • SaveGame = LoadGameFromSlot
  • set SaveGame.PersistentOuters[0] = GameInstance
  • call UObjectsPreloader
  • call UObjectDataLoader

Hi @Chatouille,

Thanks a lot for all your help. This is going to be a long one. I will put the final solution in a git repo though so it can be used in the future.

The project I am using now is just a minimal project with complex test objects (see below). My idea was to have a master object that contains all the pointers and logic required to manipulate/manage all the other objects. In this sample project, the class UMasterStateObj contains a pointer to various complex UObjects. The UMasterStateObj is constructed in the GameInstance.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Kismet/KismetArrayLibrary.h"
#include "Math/UnrealMathUtility.h"
#include "Engine/DataTable.h"
#include "UObject/Class.h"
#include "Misc/DateTime.h"
#include "SavingTest.h"

#include "GenericPlatform/GenericPlatformMath.h"


#include "MyObjectA.generated.h"


#define DISPLAY_TIME 7.0f

UENUM(BlueprintType)
enum class E_CPP_LevelName : uint8
{
  Level_Unknown UMETA(DisplayName = "Level_Unknown"),
  Level_1 UMETA(DisplayName = "Level_1"),
  Level_2 UMETA(DisplayName = "Level_2"),
  Level_3 UMETA(DisplayName = "Level_3"),
  Level_4 UMETA(DisplayName = "Level_4"),
  Level_5 UMETA(DisplayName = "Level_5"),
  Level_6 UMETA(DisplayName = "Level_6"),
  Level_7 UMETA(DisplayName = "Level_7"),
  Level_8 UMETA(DisplayName = "Level_8"),
  //Level_All UMETA(DisplayName = "Level_All"), //for total

};



USTRUCT(BlueprintType)
struct FSCPP_Plain : public FTableRowBase
{
  GENERATED_BODY()

    FSCPP_Plain() {}


  FSCPP_Plain(int val, FString ff)
    : testInt(val),
    testStr(ff)
  { }

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    int testInt = -1;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    FString testStr = "notSet";


  void InitWithDummy(int val, FString ss) {
    testInt = val;
    testStr = "FSCPP_Plain" + ss;
  }


  void PrintContent() {
    if (GEngine)
    {
      GEngine->AddOnScreenDebugMessage(-1, DISPLAY_TIME, FColor::Red, TEXT("----FSCPP_Plain------"));
      UE_LOG(LogSavingTest, Error, TEXT("-----FSCPP_Plain-----"));

      GEngine->AddOnScreenDebugMessage(-1, DISPLAY_TIME, FColor::Yellow, FString::Printf(TEXT("testInt %i"), testInt));
      UE_LOG(LogSavingTest, Error, TEXT("testInt %i"), testInt);

      GEngine->AddOnScreenDebugMessage(-1, DISPLAY_TIME, FColor::Yellow, FString::Printf(TEXT("testStr %s"), *testStr));
      UE_LOG(LogSavingTest, Error, TEXT("testStr %s"), *testStr);

      GEngine->AddOnScreenDebugMessage(-1, DISPLAY_TIME, FColor::Red, TEXT("------FSCPP_Plain----"));
      UE_LOG(LogSavingTest, Error, TEXT("----FSCPP_Plain------"));

    }

  }

};


USTRUCT(BlueprintType)
struct FSCPP_ComplexStruct : public FTableRowBase
{
  GENERATED_BODY()

    FSCPP_ComplexStruct() {
    ArrayEnum = TArray< E_CPP_LevelName>();
    PlainArray = TArray< FSCPP_Plain>();
  }

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    TArray< E_CPP_LevelName> ArrayEnum;


  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    TArray< FSCPP_Plain> PlainArray;


  void InitWithDummy(FString subPlain) {
    for (int i = 0; i < 1; i++) {
      PlainArray.Add(FSCPP_Plain(i, "FSCPP_Plain" + subPlain));

    }

    ArrayEnum = { {E_CPP_LevelName::Level_3}, {E_CPP_LevelName::Level_3}, {E_CPP_LevelName::Level_3} };
  }


  void PrintContent() {
    if (GEngine)
    {
      GEngine->AddOnScreenDebugMessage(-1, DISPLAY_TIME, FColor::Red, TEXT("----FSCPP_ComplexStruct------"));
      UE_LOG(LogSavingTest, Display, TEXT("-----FSCPP_ComplexStruct-----"));

      for (auto& ee : ArrayEnum) {
        GEngine->AddOnScreenDebugMessage(-1, DISPLAY_TIME, FColor::Yellow, FString::Printf(TEXT("ArrayEnum %i"), ee));
        UE_LOG(LogSavingTest, Display, TEXT("ArrayEnum %i"), ee);

      }
      /*  GEngine->AddOnScreenDebugMessage(-1, DISPLAY_TIME, FColor::Red, TEXT("----------"));
        UE_LOG(LogSavingTest, Display, TEXT("----------"));*/

      for (auto& ee : PlainArray) {
        ee.PrintContent();
      }
      GEngine->AddOnScreenDebugMessage(-1, DISPLAY_TIME, FColor::Red, TEXT("-----FSCPP_ComplexStruct-----"));
      UE_LOG(LogSavingTest, Display, TEXT("-----FSCPP_ComplexStruct-----"));

    }
  }
};


UCLASS(Blueprintable, BlueprintType)
class SAVINGTEST_API UMyObjectBB : public UObject
{
  GENERATED_BODY()

public:

  UMyObjectBB() {}

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    FSCPP_Plain plainStruct;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    FSCPP_ComplexStruct ComplexStruct;

  UFUNCTION(BlueprintCallable)
    void InitWithDummy(int val, FString sub);

  UFUNCTION(BlueprintCallable)
    void PrintContent();


};


/**
 *
 */
UCLASS(Blueprintable, BlueprintType)
class SAVINGTEST_API UMyObjectA : public UObject
{
  GENERATED_BODY()


public:
  UMyObjectA() {}


  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    FSCPP_Plain plainStruct;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    FSCPP_ComplexStruct ComplexStruct;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    UMyObjectBB* ObjBB;

  UFUNCTION(BlueprintCallable)
    void PrintContent();

  UFUNCTION(BlueprintCallable)
    void InitWithDummy(int val, FString sub);
};


UCLASS(Blueprintable, BlueprintType)
class SAVINGTEST_API UMyObjectBIG : public UObject
{
  GENERATED_BODY()

public:

  UMyObjectBIG() {
    ObjBBSubArray = TArray<UMyObjectBB*>();
    ObjASubArray = TArray<UMyObjectA*>();
  }


  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    FSCPP_Plain plainStruct;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    FSCPP_ComplexStruct ComplexStruct;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    UMyObjectA* ObjASub;


  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    UMyObjectBB* ObjBBSub;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    TArray<UMyObjectBB*> ObjBBSubArray;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    TArray<UMyObjectA*> ObjASubArray;

  UFUNCTION(BlueprintCallable)
    void PrintContent();

  UFUNCTION(BlueprintCallable)
    void InitWithDummy();



};




UCLASS(Blueprintable, BlueprintType)
class SAVINGTEST_API UMasterStateObj : public UObject
{
  GENERATED_BODY()

public:

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    UMyObjectBIG* ObjectBig;


  UMasterStateObj() {

  }

  UFUNCTION(BlueprintCallable)
    void Init() {
    ObjectBig = NewObject<UMyObjectBIG>();
    //    ObjectBig = NewObject<UMyObjectBIG>(this);

    ObjectBig->InitWithDummy();
  }

  UFUNCTION(BlueprintCallable)
    void PrintContent() {
    GEngine->AddOnScreenDebugMessage(-1, DISPLAY_TIME, FColor::Red, TEXT("+++----UMasterStateObj------"));
    UE_LOG(LogSavingTest, Error, TEXT("+++----UMasterStateObj------"));
    if (ObjectBig)
      ObjectBig->PrintContent();
    GEngine->AddOnScreenDebugMessage(-1, DISPLAY_TIME, FColor::Red, TEXT("+++----UMasterStateObj------"));
    UE_LOG(LogSavingTest, Error, TEXT("+++----UMasterStateObj------"));
  }

};

In USaveGameC.h I added a function that uses the object given and saves all subobjects. I was making sure that each object created uses this as outer so they can be found from the parent object.

void USaveGameC::UObjectSaverAndSubObj(UObject* SaveObject)
{
  UObjectSaver(SaveObject);

  TArray<UObject*> SubSaveObjects;

  GetObjectsWithOuter(SaveObject, SubSaveObjects, true); //,EObjectFlags::RF_Transient);

  UE_LOG(LogSavingTest, Error, TEXT("NUMEBER SUB OBJ %i"), SubSaveObjects.Num());


  UObjectArraySaver(SubSaveObjects);
}

Unfortunately, I broke a lot of the blueprints in the GameInstance experimenting, but as per the tutorial, I call SaveData() on my GameInstance and then UObjectSaverAndSubObj() on the master object inside. I then wrote it to the SaveGame with SaveGameToSlot . That worked and through the debugger, I could see that the variables were being written OK. To load I reversed the order, called UObjectPreloder() and then UObjectDataLoader() and finally LoadData() on the GameInstance. However, before even calling any of these functions after calling LoadGameFromSlot you get the error:

LogLinker:Warning: Can't find file '/Engine/Transient'

I debugged the process and as soon as it is loaded the data is zeroed. It does work if you don’t stop the game. I guess because the data is still loaded. I have also tried not doing Save/LoadData() on the GameInstance. But, I guess the same problem as everything seems to appear under Engine\Transient path.

Before I forget, I also ticked the box for SaveGame inside the GameInstance too (for UMasterStateObj).

I think you should be able to create all your objects with Outer=GameInstance, and then serialize them using GetObjectsWithOuter(GameInstance). Avoid serializing the GameInstance itself. If I get this right you need to emplace GameInstance into the PersistentOuters array (say, at index 0) before saving. And before restoring, also emplace the GameInstance into PersistentOuters at the same index, so the deserializer can resolve it with the actual object.

As far as I know, since my ‘‘master’’ object is inside the GameInstance and all subobjects use this when I serialise it adds GameInstance as the persistent outer (I saw it when debugging). I haven’t tried emplacing it myself though, but I will give it a try. I have tried both serialising and not doing it for the GameInstance. It does work serialising the GameStateBase though.

One thing I noticed is that the path of the GameInstance appears as 'Engine\Transient', so all objects constructed inside seem to fall under that. If you notice above, I’ve tried creating the object inside the ‘‘master’’ object with and without this. But, neither works. If I create it without this, then it gets the TransientPackage which also gives the same error. I am not sure if UObjects with TransientPackage as outer can be serialised…

What is interesting, is that if I do the exact same process in GameStateBase then it works perfectly. But, I checked the path and it does not below to Transient. I can’t remember, but it has a different root.

Alternatively, I could save everything in the GameStateBase, and before level transition write it to the game instance (maybe to disk) and then copy it back to the GameStateBase once the GameModeBase creates it. I got no idea what happens with the outers in this case though. But, do I need to explicitly move them to the new GameStateBase before saving again?

Finally, am I structuring things correctly? I mean, putting these persistent state objects inside the GameInstance? Or, should they be placed somewhere else? How are you supposed to handle persistent data during level transitions?

I suspect the error has not much to do with the issue.

When using SaveGameToSlot / LoadGameFromSlot, it’s only the UProperties in SaveGame class that are serialized / loaded. Those properties are :
TArray<FObjectRecord> ObjectRecords
TArray<UObject*> TempObjects
TArray<UObject*> PersistentOuters

TempObjects and PersistentOuters should probably not be serialized. They will just serialize as string refs, and cause the sort of errors you are seeing upon LoadGameFromSlot. You should mark those two UProperties as Transient to avoid saving/reloading them.


You say you call SaveData() on GameInstance. How exactly ? What do you do with the resulting array of bytes ? Do you create your own FObjectRecord and add it to the ObjectRecords array ? Doing so will result in creating a new GameInstance upon preload, which is not a good idea. Creating a new gamestate isn’t a good idea either, though there’s a better chance it would appear to work (but not really).


I still need more info to help more unfortunately. You showed how your saved objects are structured but the problem isn’t at that level. It’s most likely at the intermediate level which I can’t see.


If you create objects with Outer = GameState, and “move” these objects to GameInstance upon level change, either the objects will be destroyed anyways, or the engine is gonna crash on map change. Persistent objects should be stored in GameInstance that is definitely the right way to go.


I’m gonna try to get a simplified example working.

I practically copied the USaveGameC from the tutorial so they are not marked as SaveGame. Not sure if they get serialised though. (see below)

UCLASS(Blueprintable)
class SAVINGTEST_API USaveGameC : public USaveGame
{
  GENERATED_BODY()

public:
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<uint8> MasteObjectSave;
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<uint8> GIData;
  // All object data in one array
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<FObjectRecord> ObjectRecords;

  // used for temp loading objects before serializing but after loading
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<UObject*> TempObjects;

  // outers that are part of the map or otherwise preloaded so won't be in the list of TempObjects
  UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<UObject*> PersistentOuters;

// functions
}

Regarding the TArray I was keeping it in the blueprint that extends the USaveGameC cpp implementation. In all honesty, I would have preferred to keep everything in C++ but the tutorial has a blueprint section so I replicated it in the same way. I do think it’s pretty messy so after getting a working example I was going to port it back to C++.

I still haven’t had a chance to try what you said. I got rudely interrupted by a meeting!

Will report back soon.
Cheers!

So, implemented what you suggested.

Changed the variables as per your instruction:

  UPROPERTY(EditAnywhere, BlueprintReadWrite, Transient)
    TArray<UObject*> TempObjects;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, Transient)
    TArray<UObject*> PersistentOuters;

And set the PersistentOuters[0] = GameInstance before calling

Saving and Loading work if I don’t close the game. If I do loading after restarting the game MasterStateObj has no content.

The error I get is it fails at finding Engine/Transient (see logs).

Output below so you see the paths (All the outputs appear as errors, I just used that as it comes out in Red and it’s easy to see. They are not errors per say):

Saving:

LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1 0
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0 1
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0 2
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectBB_0 3
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1 4
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0.MyObjectBB_0 5
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1.MyObjectBB_0 6
LogSavingTest: Display: ------UObjectDataLoader
LogSavingTest: Error: LoadData
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1
LogSavingTest: Error: LoadData
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0
LogSavingTest: Error: LoadData
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0
LogSavingTest: Error: LoadData
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectBB_0
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectBB_0
LogSavingTest: Error: LoadData
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1
LogSavingTest: Error: LoadData
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0.MyObjectBB_0
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0.MyObjectBB_0
LogSavingTest: Error: LoadData
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1.MyObjectBB_0
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1.MyObjectBB_0

Loading:

As soon as I call LoadGameFromSlot() I get

LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'
LogUObjectGlobals: Warning: Failed to find object 'Object None.None'

And, after

LogSavingTest: Display: ------UObjectsPreloader
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1 0
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0 1
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0 2
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectBB_0 3
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1 4
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0.MyObjectBB_0 5
LogSavingTest: Display: Complete Load UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1.MyObjectBB_0 6
LogSavingTest: Display: ------UObjectDataLoader
LogSavingTest: Error: LoadData
LogLinker: Warning: Failed to load '/Engine/Transient': Can't find file.
LogUObjectGlobals: Warning: Failed to find object 'Object /Engine/Transient.UnrealEdEngine_0.MyGameInstance_C_0.MasterStateObj_1.MyObjectBIG_0'
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1
LogSavingTest: Error: LoadData
LogLinker: Warning: Failed to load '/Engine/Transient': Can't find file.
LogLinker: Warning: Failed to load '/Engine/Transient': Can't find file.
LogLinker: Warning: Failed to load '/Engine/Transient': Can't find file.
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0
LogSavingTest: Error: LoadData
LogLinker: Warning: Failed to load '/Engine/Transient': Can't find file.
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0
LogSavingTest: Error: LoadData
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectBB_0
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectBB_0
LogSavingTest: Error: LoadData
LogLinker: Warning: Failed to load '/Engine/Transient': Can't find file.
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1
LogSavingTest: Error: LoadData
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0.MyObjectBB_0
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_0.MyObjectBB_0
LogSavingTest: Error: LoadData
LogSavingTest: Display: --Complete LoadData UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1.MyObjectBB_0
LogSavingTest: Display: --Complete UObjectDataLoader UObject /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1.MyObjectBIG_0.MyObjectA_1.MyObjectBB_0

Also, I thought I may include the GameInstance Blueprint:

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 :

  1. Bunch of logs Failed to find ‘None.None’
  2. 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.

Great! Thanks a lot for all your help!

I just noticed the difference in the Intances, I didn’t know that PIE increased them.

So, the big question I have is. Should I be doing this in another way?

I mean, surely other developers run into similar issues. I know that my ‘‘game’’ is not really a game in terms of the data I am manipulating, but is there a better way to go about saving stuff that does not cause this kind of problem?

Or, should I code around these limitations?

Cheers!

@Chatouille

Hey, I wanted to thank you for all your help with this question.

Before I close it, I was going to ask if you had any suggestions on how to achieve the same functionality without the path problem that arises from changing maps or PIE increasing the instance.

So far, I’ve thought about two solutions. However, I am not super keen on them due to the increase in complexity:

  1. Use a local/external database solution for the persistent data and load it to the GameInstance objects on start.
  2. Do a manual copy of the structs within the objects to a specific SaveGame file. Then on load pass the data to the GameInstance Objects and reinitialise.

The downside of both is that no pointers are saved. Also, the data needs to be flattened or modified.

Have to say that I am not very keen on either…

Anyway, thanks a lot for your help!

Well I can think of several options…


Option 1 : save differently. In the original post you mentioned how you were recreating the objects upon reloading as you knew pointers would be rubbish. This is kinda the same issue. Instead of trying to store objects and pointers, store the data required to recreate those objects. I believe this is the standard go-to solution when other developers need to do this kind of thing.


Option 2 : use GameState. Don’t move your objects to the GameInstance when switching levels. Save them before map load, let them be destroyed, and reload them after map load. This might only work if the map and gamestate are always the same though.


Option 3 : upgrade the serializer to fix paths dynamically. The way FObjectAndNameAsStringArchive works is really simple, nothing to be scared about. Whenever it encounters an object pointer to save, it saves object->GetPathName() instead. Upon loading, it reads that string and uses FindObject/LoadObject to find the actual object.

image

With a bit of additional code, after spawning the master object, you could get its path (eg. /Engine/Transient.UnrealEdEngine_0:MyGameInstance_C_1.MasterStateObj_1), then replace the old root path with that new root path before using FindObject.


Option 4 : as a matter of fact a while ago I did experiment with making my own serializer to do this sort of things. It might do exactly what you’re trying to do. It’s a bit rushed, not documented, and barely tested though so there could be bugs remaining. But not the bugs already discussed here. Actors and components deserialization are a pain and probably not very solid, but for UObjects I’ve had satisfying results.

The system works differently - it takes a single “root” object as input and serializes that object along with all its subobjects into a file. Subobjects are discovered automatically while traversing and serializing UObject* properties. It’s designed to properly resolve all internal references upon reloading, while references to external objects will obviously suffer the usual caveats.

The outer of the root object is intentionally not serialized as it would typically be an external reference which the user (developper) should specify upon reloading.

Usage looks like this :

But usage should be easily adaptable, such as putting the resulting array of bytes into a SaveGame property rather than directly saving to file.

I feel like an idiot for not looking into the code inside the Farchive. I thought the loading happen inside the Engine in some obscure location.

Doing option 4 seems the best way to go about it. I think that should do it.

Thanks a lot for this. Hopefully, I can return the favour in the future when I am a bit more clued up on Unreal.

I’ve you are ever in London I’ll buy you a beer!
Cheers!

@Chatouille
Hey, apologies for revisiting this. I built a small library to incorporate all the functionality and I noticed that the recursion does not work with arrays. I think they get skipped when serialising and when reloading you get an error as follows:

LogTemp: [DeserializeObject] Deserializing into $ROOT$
LogTemp: [DeserializeObject] Deserializing into $ROOT$.MyObjectBIG_0
LogTemp: [DeserializeObject] Deserializing into $ROOT$.MyObjectBIG_0.MyObjectA_0
LogTemp: [DeserializeObject] Deserializing into $ROOT$.MyObjectBIG_0.MyObjectA_0.MyObjectBB_0
LogTemp: [DeserializeObject] Deserialized $ROOT$.MyObjectBIG_0.MyObjectA_0.MyObjectBB_0
LogTemp: [DeserializeObject] Deserialized $ROOT$.MyObjectBIG_0.MyObjectA_0
LogClass: Error: Failed loading tagged ArrayProperty /Script/SavingTest.MyObjectBIG:ObjASubArray. Read 41B, expected 78B.
LogTemp: [DeserializeObject] Deserialized $ROOT$.MyObjectBIG_0
LogTemp: [DeserializeObject] Deserialized $ROOT$

Where MyObjectBIG is a struct like:

UCLASS(Blueprintable, BlueprintType)
class SAVINGTEST_API UMyObjectBIG : public UObject
{
  GENERATED_BODY()

public:

  UMyObjectBIG() {
    ObjASubArray = TArray<UMyObjectA*>();
  }

  UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)
    TArray<UMyObjectA*> ObjASubArray;

  UFUNCTION(BlueprintCallable)
    void PrintContent();

  UFUNCTION(BlueprintCallable)
    void InitWithDummy();
};

I suspect I need to check before Obj->Serialize() and use get all subObjects. But, if you happen to know a bit more I would appreciate it.

Thanks!

Found a serious issue caused by array reallocation - this is probably the cause of your bug as well.

Added this part in SerializeObject to fix

Lemme know how it goes

Great! I will report back!