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?