What am I missing about SaveGame?

I have created a USaveGame subclass in blueprint, called BP_SaveGame.
This subclass has two properties, a FString, and an UObject subclass that I have defined in C++.
Both are marked “SaveGame” in the blueprint.
The properties of the C++ object are also marked SaveGame (as well as BluprintReadWrite and EditableAnywhere)
The properties on the C++ object are TArray<>s of FStructs, which in turn also have their properties marked as SaveGame.

In blueprint, I create a BP_SaveGame class instance. I then create the UObject subclass, and put a bunch of data into the TArrays<> in C++ code, and then I assign this object to the property in the BP_SaveGame.
I save the SaveGame to a slot using Async Save Game to Slot.
However, when this SaveGame object is actually saved, neither the property that contains the string nor the property that contains the UObject subclass are actually serialized.
The data is not in the archive .sav file on disk, and the PreSave(Platform) callback doesn’t get called on my object instance.
When I load the SaveGame object back, it comes back as the right class (BP_SaveGame) but of course contains no property values.

Why are these properties on the BP_SaveGame not getting serialized?

From the blueprint that actually tries to save the state:

From the BP_SaveGame, the highlighted Put Character Data function:

The property definition that contains the Character Save:

Slurping the data into the UCarriedInventory object:


UCLASS(Blueprintable, ClassGroup=("Character Sheet"), meta=(BlueprintSpawnableComponent))
class SCAPES_API UCharacterSheet : public UActorComponent {
...
  UFUNCTION(BlueprintCallable, Category = "Stats and Skills|Inventory")
    void SaveInventory(UCarriedInventory* ToInv);



void UCharacterSheet::SaveInventory(UCarriedInventory* ToInv) {
  ToInv->Inventory.Reset();
  ToInv->CharacterStats.Reset();
  for (auto& it : Equipped) {
    if (it.Value.Contents.StackCount > 0) {
      it.Value.Contents.SaveConsumption();
     ToInv->Inventory.Add(it.Value);
    }
  }
  for (auto& it : Carried) {
    if (it.Contents.StackCount > 0) {
      ToInv->Inventory.Add(it);
    }
  }
  for (auto& it : Stats) {
    if (it.Value->Consumed != 0.0f) {
      ToInv->CharacterStats.Add({
        it.Key,
        it.Value->Consumed
      });
    }
  }
}


The UCarriedInventory object itself:


UCLASS(Blueprintable, Category="Stats and Skills|Inventory")
class SCAPES_API UCarriedInventory : public UObject {
GENERATED_BODY()
public:
UCarriedInventory();

UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Stats and Skills|Inventory", SaveGame)
TArray<FEquipmentSlot> Inventory;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Stats and Skills|Inventory", SaveGame)
TArray<FSavedStatConsumption> CharacterStats;

void PreSave(ITargetPlatform const *TargetPlatform) override;
void PostLoad() override;
};


The default save system in engine can’t save object references. Only primitive values are supported there.

First: Thanks for the answer!
Second: WTH!?
Third: You’d think they would have mentioned that in the documentation …

The source says this:


bool UGameplayStatics::SaveGameToMemory(USaveGame* SaveGameObject, TArray<uint8>& OutSaveData )
{
if (SaveGameObject)
{
FMemoryWriter MemoryWriter(OutSaveData, true);

FSaveGameHeader SaveHeader(SaveGameObject->GetClass());
SaveHeader.Write(MemoryWriter);

// Then save the object state, replacing object refs and names with strings
FObjectAndNameAsStringProxyArchive Ar(MemoryWriter, false);
SaveGameObject->Serialize(Ar);

return true; // Not sure if there's a failure case here.
}

return false;
}

I imagine what’s going on is that it saves a reference to whatever the object name is at save time, and that object doesn’t exist at load time, and thus the load doesn’t find a reference to restore. If that’s the case, I’m going to have to figure out how to make this work for real with inline object references.

I quit using MemoryWriter and ProxyArchive because that problem and some other issues, I built a custom serializer that can support the “SaveGame” tag and array of object references;

The default system is a problem because once you figure out how to record object IDs, you find out that the engine’s internal pool of objects can recycle those IDs for newly created objects and the problems go on and on…

I found a simpler solution :slight_smile:

I can write objects to memory using FObjectWriter, and read them back using FObjectReader, and then serialize to the savegame object as byte arrays.
I don’t need the whole dynamic object stuff from a structured archive, nor from the linker loader, I just load data into a pre-determined object hierarchy I create as default subobjects on construction, so this works well!


USaveGameMine::USaveGameMine()
{
  MyObject = CreateDefaultSubobject<UMyObjectClass>(this, FName(TEXT("MyObject")));
}

void USaveGameMine::Serialize(FArchive& Ar)
{
  TArray<uint8> mem;
  if (Ar.IsSaving()) {
    FObjectWriter wr(MyObject, mem);
    uint8 v = 0;
    if ((int)mem.Num() < 1) {
      UE_LOG(LogBlueprintUserMessages, Error, TEXT("Bad size of serialized archive: %d"), (int)mem.Num());
      Ar << v;
      return;
    }
    v = 1;
    Ar << v;
    uint32 sz = mem.Num();
    Ar << sz;
    Ar.Serialize(&mem[0], mem.Num());
  }
  else if (Ar.IsLoading()) {
     uint8 ver = 0;
    Ar << ver;
    if (ver != 1) {
      UE_LOG(LogBlueprintUserMessages, Error, TEXT("Bad version number in character save: %d"), (int)ver);
      return;
    }
    uint32 size = 0;
    Ar << size;
    if (size < 1 || size > MAX_CHARACTER_SIZE) {
      UE_LOG(LogBlueprintUserMessages, Error, TEXT("Bad size in character save: %d"), (int)size);
      return;
    }
    mem.InsertUninitialized(0, size);
    Ar.Serialize(&mem[0], size);
    FObjectReader rd(MyObject, mem);
  }
  else {
    Super::Serialize(Ar);
  }
}


I have some more code to deal with more properties of the savegame and also set some status flags based on success/failure, but this should illustrate the concept well enough.

I don’t know about today, but when trying to use that with outdated or corrupt save data, the game would crash on load.

Yeah, that happens :slight_smile:
That’s why I have the version number in there.
There’s really no way to load corrupt data safely, unless you end up tagging and CRC-checking and length-prefixing each and every field of each and every object.
Which, sure, is totally possible, but also slow and bulky!

If you plan publishing on console, certification will be a problem, if not then I think it’s fine.