Announcement

Collapse
No announcement yet.

Spawning Actors from Serialized Data

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    Spawning Actors from Serialized Data

    I'm trying to create a custom save and load system for my game. I've never done this in UE4. I'm creating a "quicksave" and "quickload" functionality and I want to have the option to export the data to an external file so that the game can be restored from it later.

    So, normally if I was going to do this with a standard IOStream binary file, I'd probably just figure out some way to encode each instanced object into binary. Each object would be responsible for knowing how to save itself and load itself. The binary output would be some sort of unique identifier which could be matched to a class type, followed by all of the binary data necessary to restore that instance of object. Somewhere, I'd have a lookup table which matches the unique identifiers with classes so that I spawn the correct ones.

    My novice approach to UE4 is somewhat similar, but a bit different as well. For starters, I don't need to save every single actor in the world, I just need to save the dynamic actors and the current world state. When I reset the world, I go through and call "destroy" on all dynamically spawned actors. Then I need to go through and recreate all of the actors I have saved in my binary data stream.

    I've got everything working, except for being able to store the instanced actor class. Somehow, I need to be able to save the actor class or a unique identifier which can be used to get the class. I basically need to be able to be able to store and recall the actors UClass* pointer, but the pointer is a reference to a memory address rather than a static signature. I tried using Actor->GetName() to get an FString and storing that FString in the binary file, but once I have the class FString in there, I need to be able to convert that into a UClass* again.

    Is there an easy way to do this with minimal coding and upkeep?
    Should I manually create a hash table with integer ID's and associate classes to those ID's? (This is prone to human error...)
    Is there a way to serialize an object based off of a UPROPERTY metatag, such as "SaveGame"? without having to write a custom saver and loader function for each actor class?

    #2
    Okay, I've made some headway on this for anyone who is interested. Unless I'm doing something really wrong here, I find that reading serial data back in is pretty hard.

    Click image for larger version

Name:	FileStream.png
Views:	1
Size:	20.2 KB
ID:	1111619

    So, this is the scheme I've come up with. In case anyone is new to binary I/O, which is used to both write files and write data to the network, here's a general idea on how serialization works. Basically, it's a really long array of bytes. The bytes have absolutely no meaning without context or interpretation, and it can easily be messed up. In the SAMPLE Serial Stream above, I created a bunch of serialized variables. As we all know, a 32 bit integer consumes 4 bytes of data. Same with a float. If we serialize a FVector, we're serializing the X,Y,Z components which are floats, so that takes up 4*3 = 12 bytes. I just learned this today, but we can ALSO serialize an array of bytes which can already be serialized. To do this, just store those bytes as a TArray<uint8>. It's important to use the unsigned integer because the signed integer uses the last bit for storing the sign, which we don't want (or else data loss!).

    Alright, so let's pretend that we know how to serialize data out to a file. We close down our game. Then we open up our game again, and we want to read in the file data. We want to read the file data, interpret it, and then use the interpreted values to restore the game to its prior state. The first question we absolutely must answer is, "What am I reading?! What does this data mean and who does it belong to?"

    I decided to create a unique class signature value at the beginning of each block of data. This tells me, "Hey, here's a unique ID lookup you can use to figure out which class this data belongs to!". To read the data, I would then create an instance of that class. Then, I'd tell that instanced object, "Hey, here's a data block with your data! You know how to read it, so read it in!". The object will then read in the data block, and as it reads the data, it progresses the read pointer to the next byte until it has read all of its own data.

    The easy part is reading data which you know exists. For example, maybe you store the position and orientation of every object as a FVector followed by an FRotator. You know that this will progress your read pointer by 6*3*4 bytes (72b). Then, your class needs to read in a TArray<uint8> which can contain a variable length of data. Maybe this is something like the list of monsters your character knows about, or a list of buildings your colony has built. The point is, you have no idea how long this block of bytes is going to be and you need to know when to stop interpreting the data block as internal data. If you stop at the wrong position, you may accidentally interpret the next object's binary data as your own, and then your read pointer is going to be completely off. So, I decided the smartest thing to do is give ourselves a hint at how large this variable datablock is going to be by encoding some metadata (as an int32) right before we start reading the data block. We can then either parse this data block right now, or save it and interpret it later and move on to the next object.

    One of the other issues I had is that I needed my objects to know how to read and write their data block to a serial stream of bytes. It's easy when you have classes with known properties, but what happens when you introduce inheritance to the mix? I decided polymorphism is the solution here:
    Click image for larger version

Name:	FileStream2.png
Views:	1
Size:	20.1 KB
ID:	1111620

    So, I have a "ABaseCreature" which inherits from "AActor". This is the base class which all of my other creature classes will inherit from. The base class has some common properties which every creature has, so I want the base class to know how to serialize those blocks of data. However, the base class is kind of abstract, meaning we won't ever have instances of it, so what we really want is to let all of the classes which derive from the base class to implement their own serialization. But we don't want to re-implement the serialization of the base class, we just want to serialize the deltas between the base class and the inheriting class. This is where polymorphism shines. I discovered that I needed to create a struct to hold the base creature data because when you use the UE4 serializer, it can be used to either read data in from a serial stream, or write data to a serial stream. How do you read/write something like the actor position directly? You can't. You need to know if you're loading or saving your data, and whether the serializer is reading or writing is context sensitive (which reminds me a lot of network coding!). Anyways, the base creature class knows how to load and save itself. It does this by writing the base class specific data into the preset creature record fields. However, we are creating objects which inherit from the BaseCreature class, and those inheriting objects will certainly have additional property fields, and we have no idea how many properties such and inheriting class will have. That means the data block is going to be variable in length, depending on what class is writing to it. Since we're using polymorphism, we HAVE to use the "FCreatureRecord" struct, but we have to be able to support more data than what we anticipate. So, I created a TArray<uint8> of unspecified length to support this. I figure that a inheriting class can fill this block with data it knows how to interpret, and it can be responsible for putting in the meta data flags if it needs to. Basically, we're reserving a block of binary data in the record we're saving.

    The part I haven't figured out yet though, is how to get blueprints to implement the serialization step. If I have my "ABaseCreature" class, and I create an "AWizard : ABaseCreature" class in code, and then in blueprints I creature "BP_Wizard" which inherits from "AWizard", then the BP_Wizard is going to have to know how to serialize itself correctly. I'm not sure that a blueprint knows how to do serialization. I might end up having to do some tricky BS with polling all UPROPERTY tags for "SaveGame" flags or something, but that sounds like a recipe for things going sideways really fast.

    There's gotta be an easier way to do this, right??? I get the feeling I'm working much harder on this than I should be.

    Comment


      #3
      Okay, I got it working!!! I spent the whole weekend trying to figure this out.

      It turns out, there IS a much easier way.

      So, the super undocumented secret to serializing any data to or from an object based on whether the CPF_SaveGame flag is set, is super simple: When you are creating your FMemoryWriter, you MUST have the ArIsSaveGame flag set to true. By default, it's false. IF you forget to set this flag, the Memory Writer will try to serialize your ENTIRE object, and it will loop through EVERY property and try to serialize it. I was getting crashes when the serializer was trying to serialize a particle system component, static mesh component, etc. and this lead me to believe my initial approach was wrong.

      I went off on a wild goose chase down some rabbit hole trying to create my own custom serialization method in blueprints using the "BlueprintImplementableEvent" and letting objects save and load from a TArray<uint8>. While it's not necessarily a bad idea, it can become very hard to maintain. I found that one of the huge advantages to doing it manually though is I can pack my serialized data down into something like 50 bytes per object. You can get things SUPER compacted if you know the exact size of your data and the data type, and where you lay it out in memory. My smallest actor instance was reduced to 31 bytes.

      Here's a huge word of caution for anyone else: if you are silly and decide to override the Serialize(FArchive& Ar) method for your class, MAKE SUPER SURE YOU CALL THE SUPER(Ar) method in the base class!!! IF you forget to do this, the engine loading event WILL load your blueprint off of your overriden serialize method, and ANY default values your class had WILL BE WIPED!!! I discovered this the hard way... thank god for backups.

      For anyone else who stumbles on this post when doing serialization, here is my relevant code.

      Pickup.h
      Code:
      USTRUCT()
      struct FPickupRecord
      {
      	GENERATED_USTRUCT_BODY()
      
      	int32 ClassID;
      	FVector Position;
      	FRotator Rotation;
      
      	//extra data for pickup specific implementations
      	TArray<uint8> Data;
      
      	//might be able to do an internal serialize here...
      	FORCEINLINE FArchive& Serialize(FArchive& Ar)
      	{
      		//Ar << ClassName;
      		Ar << ClassID;
      		Ar << Position;
      		Ar << Rotation;
      		Ar << Data;
      		return Ar;
      	}
      
      
      };
      
      /*A pickup is a generic type of object which gets spawned in the world and can be picked up by creatures.*/
      UCLASS()
      class APickup : public AActor
      {
      	GENERATED_BODY()
      
              //Gets you the hash value for this class
      	int32 GetClassID();
      
      	FPickupRecord SaveToRecord();
      	void LoadFromRecord(FPickupRecord Record);
      
      private:
      	void SetClassID(int32 ClassID);
      };
      Pickup.cpp
      Code:
      int32 APickup::GetClassID()
      {
      	//When we serialize our objects, we're going to want an identifier we can use to identify what class is serialized.
      	//We're going to be encoding this into a 32 bit integer using bit masks. The first 16 bits are going to be the unique
      	//creature ID. Bits 16-32 are going to be the class ID, which is a hard coded static constant.
      	//[ClassID][CreatureID]
      	//[32-16][15-0]
      	return CategoryID | PickupID;
      }
      
      void APickup::SetClassID(int32 ClassID)
      {
      	PickupID = 0x0000FFFF & ClassID;
      	CategoryID = 0xFFFF0000 & ClassID;
      }
      
      FPickupRecord APickup::SaveToRecord()
      {
      	FPickupRecord Ret;
      
      	Ret.ClassID = GetClassID();
      	Ret.Position = GetActorLocation();
      	Ret.Rotation = GetActorRotation();
      
      	FMemoryWriter DataWriter = FMemoryWriter(Ret.Data);
      	DataWriter.ArIsSaveGame = true;
      	Serialize(DataWriter);
      	return Ret;
      }
      
      void APickup::LoadFromRecord(FPickupRecord Record)
      {
      	SetClassID(Record.ClassID);	//so we can properly save again
      	SetActorLocation(Record.Position);
      	SetActorRotation(Record.Rotation);
      
      	FMemoryReader Reader = FMemoryReader(Record.Data);
      	Serialize(Reader);
      }

      AS1_PreludeState.h:
      Code:
      UCLASS()
      class AS1_PreludeState : public AGameState
      {
      	GENERATED_BODY()
      
      public:
      	AS1_PreludeState(const FObjectInitializer& ObjectInitializer);
      
      	UFUNCTION(BlueprintCallable, Category = "GameState")
      	void SaveGamestate();
      
      	UFUNCTION(BlueprintCallable, Category = "GameState")
      	void RestoreGamestate();
      
      	UFUNCTION(BlueprintCallable, Category = "Level")
      	void QuickSave();
      
      	UFUNCTION(BlueprintCallable, Category = "Level")
      	void QuickLoad();
      
      	/*Goes through the level and destroys every actor it finds*/
      	UFUNCTION(BlueprintCallable, Category = "GameState")
      	void ClearAllActors();
      
      	virtual void Serialize(FArchive& Ar) override;
      
      	int32 NumRecords = 0;
      	TArray<FCreatureRecord> CreatureRecords;
      	
      	int32 NumPickups = 0;
      	TArray<FPickupRecord> PickupRecords;
      
      	// This is a lookup of UClass pointers created at runtime.
      	// Usage: Use this to lookup UClass* pointers from a stored hash value.
      	// Key - The class ID (hash value)
      	// Value - The pointer to the class pointer
      	TMap<int32, UClass*> ClassDB;
      };
      AS1_PreludeState.cpp:
      Code:
      void AS1_PreludeState::QuickSave()
      {
      	USBSaveGame* SaveGameInstance = Cast<USBSaveGame>(UGameplayStatics::CreateSaveGameObject(USBSaveGame::StaticClass()));
      	SaveGameInstance->PlayerName = TEXT("Hello World");
      	TArray<AActor*> AllPickups;
      	UGameplayStatics::GetAllActorsOfClass(GetWorld(), APickup::StaticClass(), AllPickups);
      	for (int32 a = 0; a < AllPickups.Num(); a++)
      	{
      		APickup* CurItem = (APickup*)AllPickups[a];
      		SaveGameInstance->AllPickups.Add(CurItem);
      	}
      	UGameplayStatics::SaveGameToSlot(SaveGameInstance, SaveGameInstance->SaveSlotName, SaveGameInstance->UserIndex);
      }
      
      void AS1_PreludeState::QuickLoad()
      {
      	USBSaveGame* LoadGameInstance = Cast<USBSaveGame>(UGameplayStatics::CreateSaveGameObject(USBSaveGame::StaticClass()));
      	LoadGameInstance = Cast<USBSaveGame>(UGameplayStatics::LoadGameFromSlot(LoadGameInstance->SaveSlotName, LoadGameInstance->UserIndex));
      	FString PlayerNameDisplay = LoadGameInstance->PlayerName;
      
      	//destroy all pickups
      	{
      		TArray<AActor*> AllPickups;
      		UGameplayStatics::GetAllActorsOfClass(GetWorld(), APickup::StaticClass(), AllPickups);
      		for (int32 a = 0; a < AllPickups.Num(); a++)
      		{
      			AllPickups[a]->Destroy();
      		}
      	}
      
      	TArray<APickup*> AllPickups = LoadGameInstance->AllPickups;
      
      	for (int32 a = 0; a < AllPickups.Num(); a++)
      	{
      		APickup* NewPickup = GetWorld()->SpawnActor<APickup>(AllPickups[a]->GetClass(), AllPickups[a]->GetActorLocation(), AllPickups[a]->GetActorRotation());
      	}
      }
      
      void AS1_PreludeState::ClearAllActors()
      {
      	//destroy all creatures
      	TArray<AActor*> AllCreatures;
      	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ABaseCreature::StaticClass(), AllCreatures);
      	for (int a = 0; a < AllCreatures.Num(); a++)
      	{
      		AllCreatures[a]->Destroy();
      	}
      
      	//destroy all pickups
      	TArray<AActor*> AllPickups;
      	UGameplayStatics::GetAllActorsOfClass(GetWorld(), APickup::StaticClass(), AllPickups);
      	for (int32 a = 0; a < AllPickups.Num(); a++)
      	{
      		AllPickups[a]->Destroy();
      	}
      
      	CreatureRecords.Empty();
      	PickupRecords.Empty();
      }
      
      void AS1_PreludeState::SaveGamestate()
      {
      	PickupRecords.Empty();
      	CreatureRecords.Empty();
      	ClassDB.Empty();
      
      	TArray<AActor*> AllCreatures;
      	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ABaseCreature::StaticClass(), AllCreatures);
      	NumRecords = AllCreatures.Num();
      
      	//go through all active creatures and save their state to some records
      	for (int a = 0; a < AllCreatures.Num(); a++)
      	{
      		ABaseCreature* CurrentCreature = Cast<ABaseCreature>(AllCreatures[a]);
      		
      		//save the class ID
      		int32 UID = CurrentCreature->GetClassID();
      		if (!ClassDB.Contains(UID))
      		{
      			ClassDB.Add(UID);
      			ClassDB[UID] = CurrentCreature->GetClass();
      		}
      
      		FCreatureRecord NewRecord = CurrentCreature->SaveToRecord();
      		CreatureRecords.Add(NewRecord);
      		//ActiveActors[a]->Serialize(Ar);
      	}
      
      
      	TArray<AActor*> AllPickups;
      	UGameplayStatics::GetAllActorsOfClass(GetWorld(), APickup::StaticClass(), AllPickups);
      	NumPickups = AllPickups.Num();
      	//Create a list of pickup records
      	for (int32 a = 0; a < AllPickups.Num(); a++)
      	{
      		APickup* CurrentPickup = Cast<APickup>(AllPickups[a]);
      		int32 UID = CurrentPickup->GetClassID();
      		if (!ClassDB.Contains(UID))
      		{
      			ClassDB.Add(UID);
      			ClassDB[UID] = CurrentPickup->GetClass();
      		}
      		FPickupRecord NewRecord = CurrentPickup->SaveToRecord();
      		PickupRecords.Add(NewRecord);
      	}
      
      	//We're going to be writing to this block of binary data!
      	FMemoryWriter MyWriter = FMemoryWriter(BinaryData);
      	MyWriter.ArIsSaveGame = true;
      
      	//we call this->serialize which knows how to serialize itself. We're passing the "writer" as the archive which gets serialized to.
      	//when this is complete, "BinaryData" will have all of the serialized data from ourself!
      	Serialize(MyWriter);
      	
      
      }
      void AS1_PreludeState::RestoreGamestate()
      {
      	//destroy everything dynamic in the world
      	ClearAllActors();
      	
      	//This sets the context to READ from a binary block of data.
      	FMemoryReader MyReader = FMemoryReader(BinaryData);
      	MyReader.Seek(0);
      
      	//We are going to take the data in the block of binary data, deserialize it, and fill our class with the result.
      	//our class knows how to serialize and deserialize itself, so this shouldn't be a problem.
      	Serialize(MyReader);
      
      	UWorld* World = GetWorld();
      
      	//Restore saved creatures
      	for (int a = 0; a < NumRecords; a++)
      	{
      		//TODO: Spawn actors from class 
      		
      		UClass* SpawnedClass = ClassDB[CreatureRecords[a].ClassID];
      		FVector Pos = CreatureRecords[a].Position;
      		FRotator Rot = CreatureRecords[a].Rotation;
      
      		ABaseCreature* newActor = World->SpawnActorDeferred<ABaseCreature>(SpawnedClass, Pos, Rot);
      		if (newActor)
      		{
      			newActor->LoadFromRecord(CreatureRecords[a]);
      			//newActor->SpawnDefaultController();
      			FTransform XForm;
      			XForm.SetLocation(Pos);
      			XForm.SetRotation(Rot.Quaternion());
      			newActor->FinishSpawning(XForm);
      
      			//TODO: check to see if its the wizard...
      			World->GetFirstPlayerController()->Possess(newActor);
      		}
      	}
      
      	//Restore saved pickups
      	for (int32 a = 0; a < NumPickups; a++)
      	{
      		UClass* SpawnedPickupClass = ClassDB[PickupRecords[a].ClassID];
      		APickup* NewPickup = World->SpawnActorDeferred<APickup>(SpawnedPickupClass, PickupRecords[a].Position, PickupRecords[a].Rotation);
      		if (NewPickup)
      		{
      			NewPickup->LoadFromRecord(PickupRecords[a]);
      			FTransform XForm = FTransform(PickupRecords[a].Rotation, PickupRecords[a].Position, FVector(1, 1, 1));
      			NewPickup->FinishSpawning(XForm);
      		}
      	}
      
      }
      
      void AS1_PreludeState::Serialize(FArchive& Ar)
      {
      
      	//this is the metadata that we want to save/load! 
      	Ar << ZombieSpawner;
      	Ar << NumRecords;
      	Ar << NumPickups;
      
      
      	CreatureRecords.SetNum(NumRecords);
      	//go through all active creatures and serialize them
      	for (int a = 0; a < NumRecords; a++)
      	{
      		CreatureRecords[a].Serialize(Ar);
      	}
      
      	PickupRecords.SetNum(NumPickups);
      	for (int32 a = 0; a < NumPickups; a++)
      	{
      		PickupRecords[a].Serialize(Ar);
      	}
      }
      With this, any blueprints which inherit from the APickup class will automatically be saved and serialized. The data which gets saved to disk is going to be based on whether the blueprint creator set the "CPF_SavedGame" flag to true or false. We don't need to know beforehand what variables a child object has because they all get loaded into a TArray<uint8> stored in our data record, and the serializer worries about being able to correctly save and load that data. We know that if a user sets CPF_SaveGame to true, that variable data will live as a serialized steam in our TArray<uint8> data variable.

      I hope this is helpful.

      Comment


        #4
        Alright, I've made a huge improvement to my approach... again!

        One of the potential annoyances is that with my approach above, I'd leave a lot of room for human error. I have simplified things very much and enhanced the saving system!

        1) I have a lot of base classes which will need to be able to save themselves. They all pretty much behave the same way, but there needs to be room for a class to save itself in the standard way, and then add any of its own special save code. One option is to create a base class for all my base classes. The thought crossed my mind, but it's actually a pretty dumb idea. All of the objects in UE4 already inherit from UObject, and all Actors inherit from AActor. Why would I want to create an extra layer of abstraction between AActor and my own classes? Just to get access to two functions which every base class should implement? The correct answer (if you haven't guess already) is to create an interface! So, I created "ISaveable", which I can apply to any classes which should want to save themselves. Each base class is responsible for implementing two methods, "SaveToRecord" and "LoadFromRecord".

        ISaveable.h:
        Code:
        #pragma once
        
        #include "ISaveable.generated.h"
        
        USTRUCT()
        struct FSaveDataRecord
        {
        	GENERATED_USTRUCT_BODY()
        
        		UPROPERTY(SaveGame)
        		UClass* ActorClass;
        
        	UPROPERTY(SaveGame)
        		FTransform ActorTransform;
        
        	UPROPERTY(SaveGame)
        		FString ActorName;
        
        	//extra data for actor specific implementations
        	UPROPERTY(SaveGame)
        		TArray<uint8> Data;
        
        	//might be able to do an internal serialize here...
        	FORCEINLINE FArchive& Serialize(FArchive& Ar)
        	{
        		Ar << ActorClass;
        		Ar << ActorTransform;
        		Ar << ActorName;
        		Ar << Data;
        		return Ar;
        	}
        };
        
        struct FSaveGameArchive : public FObjectAndNameAsStringProxyArchive
        {
        	FSaveGameArchive(FArchive& InInnerArchive, bool bInLoadIfFindFails)
        		: FObjectAndNameAsStringProxyArchive(InInnerArchive, bInLoadIfFindFails)
        	{
        		ArIsSaveGame = true;
        	}
        };
        
        UINTERFACE(Blueprintable)
        class USaveable : public UInterface
        {
        	GENERATED_UINTERFACE_BODY()
        };
        
        class ISaveable
        {
        	GENERATED_IINTERFACE_BODY()
        
        public:
        	virtual FSaveDataRecord SaveToRecord();
        	virtual void LoadFromRecord(FSaveDataRecord Record);
        };
        2) I have drastically simplified the way I save my game state. Because I'm now using interfaces for all of my base classes, and all of my base classes can load and save themselves without knowing what child classes inherit from them, I can save most properties which have CPF_SaveGame checked. Let's look at my new code (which is a much better explanation than words):

        MyCustomGameState.cpp
        Code:
        void AS1_PreludeState::ClearAllActors()
        {
        	//destroy all transient actors! They were scummy anyways...
        	for (int32 a = 0; a < TransientActors.Num();)
        	{
        		TransientActors[a]->Destroy();
        	}
        	TransientActors.Empty();
        
        	//destroy all creatures
        	TArray<AActor*> AllCreatures;
        	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ABaseCreature::StaticClass(), AllCreatures);
        	for (int a = 0; a < AllCreatures.Num(); a++)
        	{
        		AllCreatures[a]->Destroy();
        	}
        
        	//destroy all pickups
        	TArray<AActor*> AllPickups;
        	UGameplayStatics::GetAllActorsOfClass(GetWorld(), APickup::StaticClass(), AllPickups);
        	for (int32 a = 0; a < AllPickups.Num(); a++)
        	{
        		AllPickups[a]->Destroy();
        	}
        
        	//destroy all active spells
        	TArray<AActor*> AllSpells;
        	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ABaseSpell::StaticClass(), AllSpells);
        	for (int32 a = 0; a < AllSpells.Num(); a++)
        	{
        		AllSpells[a]->Destroy();
        	}
        
        	ObjectRecords.Empty();
        
        }
        
        void AS1_PreludeState::SaveActors(TSubclassOf<AActor> ActorClass)
        {
        	TArray<AActor*> FoundActors;
        	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ActorClass, FoundActors);
        	for (int32 a = 0; a < FoundActors.Num(); a++)
        	{
        		//does this actor implement the ISaveable interface? If so, let's call it!
        		ISaveable* SaveInterface = Cast<ISaveable>(FoundActors[a]);
        		if (SaveInterface)
        		{
        			ObjectRecords.Add(SaveInterface->SaveToRecord());
        		}
        	}
        
        }
        
        void AS1_PreludeState::SaveGamestate()
        {
        	ObjectRecords.Empty();
        
        	//We're going to save all of our dynamic actors via their base class. Any inheriting actors will get their properties
        	//saved. This will populate an array of object records.
        	SaveActors(ABaseCreature::StaticClass());
        	SaveActors(APickup::StaticClass());
        	SaveActors(ABaseSpell::StaticClass());
        
        	//We're going to be writing to this block of binary data!
        	TArray<uint8> BinaryDataBlock;
        	FMemoryWriter MyWriter = FMemoryWriter(BinaryData);
        	MyWriter.ArIsSaveGame = true;
        
        	FSaveGameArchive Ar(MyWriter, false);
        
        	//we call this->serialize which knows how to serialize itself. We're passing the "writer" as the archive which gets serialized to.
        	//when this is complete, "BinaryData" will have all of the serialized data from ourself!
        	Serialize(Ar);
        	
        	//now we want to write our serialized data to a file so that we can restore from it.
        }
        void AS1_PreludeState::RestoreGamestate()
        {
        	//destroy everything dynamic in the world
        	ClearAllActors();
        	
        	//This sets the context to READ from a binary block of data.
        	FMemoryReader MyReader = FMemoryReader(BinaryData);
        	FSaveGameArchive Ar(MyReader, true);
        
        	//We are going to take the data in the block of binary data, deserialize it, and fill our class with the result.
        	//our class knows how to serialize and deserialize itself, so this shouldn't be a problem.
        	Serialize(Ar);
        
        	UWorld* World = GetWorld();
        
        	//Restore all objects from the saved records
        	for (int a = 0; a < ObjectRecords.Num(); a++)
        	{
        		UClass* SpawnedClass = ObjectRecords[a].ActorClass;
        		FVector Pos = ObjectRecords[a].ActorTransform.GetLocation();
        		FRotator Rot = FRotator::ZeroRotator;
        
        		FActorSpawnParameters SpawnParams;
        		FName ActorName = FName();
        		ActorName.AppendString(ObjectRecords[a].ActorName);
        		SpawnParams.Name = ActorName;
        
        
        		//We want to use deferred spawning because this lets any logic in an actors construction script to run.
        		AActor* newActor = World->SpawnActorDeferred<AActor>(SpawnedClass, ObjectRecords[a].ActorTransform);
        		//AActor* newActor = World->SpawnActor<AActor>(SpawnedClass,ObjectRecords[a].ActorTransform, SpawnParams);
        
        		if (newActor)
        		{
        			//check to make sure the actor implements our save interface
        			ISaveable* LoadInterface = Cast<ISaveable>(newActor);
        			if (LoadInterface)
        			{
        				//Each object knows how to load itself!
        				//note: the wizard.cpp implementation version should let the first player controller possess it.
        				LoadInterface->LoadFromRecord(ObjectRecords[a]);
        			}
        
        			newActor->FinishSpawning(ObjectRecords[a].ActorTransform);
        		}
        
        	}
        }
        One personal annoyance I have is that the "SpawnActorDeferred" does not accept an FActorSpawnParameters variable, so if you want to set the name of your object using the deferred spawning method, you're out of luck. The internal name is private, so you never get access to it.


        3) How do objects load themselves? It's been reduced to a very small amount of code now:

        BaseCreature.cpp:
        Code:
        FSaveDataRecord ABaseCreature::SaveToRecord()
        {
        	FSaveDataRecord NewRecord = FSaveDataRecord();
        	NewRecord.ActorClass = GetClass();
        	NewRecord.ActorTransform = GetActorTransform();
        	NewRecord.ActorName = GetName();
        
        	//store any properties which have CPF_SaveGame checked and store that in a binary data array
        	FMemoryWriter Writer = FMemoryWriter(NewRecord.Data);
        	Writer.ArIsSaveGame = true;
        	Serialize(Writer);
        	
        	return NewRecord;
        }
        
        void ABaseCreature::LoadFromRecord(FSaveDataRecord Record)
        {
        	FMemoryReader Reader = FMemoryReader(Record.Data);
        	Serialize(Reader);
        
        	//should controller possess creature here?
        }

        There are still a few gotchas that I'm trying to figure out:

        I don't know how to save a component reference (anything deriving from UActorComponent). The engine doesn't seem to know how to serialize a UActorComponent* pointer.
        I don't know how to save a blackboard state. Particularly blackboard values which are references to existing objects in game.
        I don't know how to save and restore a particular state in an animation blueprint. Lets say I have a zombie which is crawling out of the grave and his animation sequence lasts for about 7 seconds. If I delete all actors and reload my zombie, he's going to be reloading from the grave crawling sequence, even though he might be doing something totally different like lurching towards the player.

        Comment


          #5
          Thank you, this thread is very useful to me. I googgling for clear example for a long time. Cheer

          Comment


            #6
            Scenario: I have a zombie character which uses a behavior tree and blackboard for its AI, and is also driven by an animation blueprint. I want to save the precise game state and be able to restore it to the precise moment the game was saved. Think of this like taking a camera picture once, and then at any time, rewinding to that picture. Every time you resume from that snapshot, you want the game state to play out exactly the same way, as if it was a deterministic universe, as if I was rewinding a movie back to a set time frame and then hitting play. The number of times I rewind my game to a previous point in game time should have no effect on the outcome.

            Take a close look at this screenshot:
            Click image for larger version

Name:	Blackboard.jpg
Views:	1
Size:	394.2 KB
ID:	1112026

            You'll notice that there are many different data types in the blackboard that need to be saved and restored. Of particular interest is "WanderTarget" and "SensedEnemy" because these are object references. This is particularly difficult to handle because every time I load the game, I go through the whole level and delete all dynamic actors. Then, I restore the level by spawning a list of dynamic actors from a series of saved records which contain the actor information. The problem is that if an actor stores a *reference* to another dynamic actor, that reference will point to a destroyed actor and will be invalid. We can't save the name of an actor either, because when you spawn a new actor, the name is unique. For example, "Zombie_C_0" becomes "Zombie_C_1". We also don't want to mess around with storing memory addresses. Let's just assume that creating an actor will never guarantee that it will have the same memory address. So, the crucial question is, "How do I save a reference to an instanced actor and then restore that reference during a load sequence?"

            I have solved this problem. It works, but it's messy. Here's the concept:

            Way back in my database developer days, database table records would have unique auto incrementing integer index values. If a table wanted to reference another record, you would put an integer reference in the other table. Then, you'd run a SQL query to lookup the appropriate row based off of the index value. The index value would be like a unique reference pointer... Here's a super basic example:

            Click image for larger version

Name:	DataTable.jpg
Views:	1
Size:	34.0 KB
ID:	1112027
            The "Inventory Table" above has a list of inventory records. The RecordID is a unique record identifer, and each record contains the number of inventory items followed by an indexed reference to the item ID. If we wanted to match an item in the inventory to some item properties, we'd do a "join" lookup between InventoryTable.ItemID and ItemTable.ID; We'd get the corresponding properties for that record, which in this case is just the name of the record. One super important thing to note about this system is that we can change the internal properties of our item table without affecting the record keeping data in the inventory table. Maybe we want to change the name "Mushroom" to "Poison Mushroom"? No problem! No data loss! You might start to see where I'm going with this...

            So, let's apply this same principle to saving and loading references. What if... every dynamic object we create has a unique ID assigned to it during creation time? What if, we can use this ID to match it to an instanced object? What if... our data record just saved the object ID? When we restore an object, we reassign it the saved ID? Then, to restore a pointer reference, we just use our lookup table? Here's a rough design of what this looks like:

            Click image for larger version

Name:	DataTable2.jpg
Views:	1
Size:	66.9 KB
ID:	1112032

            So, what we're doing is creating a TMap<int32, ABaseCreature*> database to act as our lookup. When we create a new creature, we want to add it to this database as a reference. We can then "dereference" an integer to get access to the instanced object. This part is super simple:
            Code:
            ABaseCreature* AZombieGameState::LookupCreatureID(int32 CreatureID)
            {
            	if (CreatureDB.Contains(CreatureID))
            	{
            		return CreatureDB[CreatureID];
            	}
            	
            	return NULL;
            }
            So, when you go to save a creature, you also save the ID you assigned to the creature.
            When you go to restore a creature, you also restore the ID you assigned and you also add it to this reference lookup database.
            Note that you really only need this lookup database when you're loading and saving. Create the database from a list of active creatures by assigning them all ID's at 'save time'. Restore the creature to the database each time you spawn it, until you've restored every creature.

            AFTER you have restored every single object to your game, you want to go through and do a "post fixup" step, where you go through and repair broken object references to point to the newly spawned objects. It is SUPER important that you do this step after all objects have been restored, because there's a chance that an object may have a reference pointing to an object which doesn't exist yet. Here's that section of code for reference:
            Code:
            void AZombieGameState::PostFixupReferences()
            {
            	TArray<AActor*> AllCreatures;
            	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ABaseCreature::StaticClass(), AllCreatures);
            	for (int32 a = 0; a < AllCreatures.Num(); a++)
            	{
            		ABaseCreature* CurCreature = Cast<ABaseCreature>(AllCreatures[a]);
            		CurCreature->PostFixupReferences(this);
            	}
            }
            Note that I let each creature figure out how to fix their own references. I decided to only repair references for my creatures because they're the only instanced objects which break noticeably if references are null.

            When it comes to saving and restoring the blackboard state of a creature, I do that by creating an AI state record which contains a list of all the blackboard keys I'm interested in saving to disk. You'll note that I had two object references, so I store those as int32 values which are just the ID lookups I mentioned earlier:

            Code:
            FMonsterRecord AMonsterController::SaveBlackboard()
            {
            	FMonsterRecord Ret = FMonsterRecord();
            	
            	//BlackboardComp->SetValue<UBlackboardKeyType_Enum>(BBK_ActionType, (uint8)EActionType::Spawning);
            	//BlackboardComp->GetValueAsEnum(FName("ActionState"));
            	
            	//Ret.ActionType = BlackboardComp->GetValue<UBlackboardKeyType_Enum>(BBK_ActionType);
            	Ret.IsValid = true;
            	Ret.ActionState = (uint8)BlackboardComp->GetValueAsEnum(FName("ActionState"));
            	Ret.PriorState = (uint8)BlackboardComp->GetValueAsEnum(FName("PriorState"));
            	Ret.State = (uint8)BlackboardComp->GetValueAsEnum(FName("State"));
            	Ret.OverrideActionType = (uint8)BlackboardComp->GetValueAsEnum(FName("OverrideActionType"));
            	Ret.ActionType = (uint8)BlackboardComp->GetValueAsEnum(FName("ActionType"));
            	ABaseCreature* WanderTarget = (ABaseCreature*)BlackboardComp->GetValueAsObject(FName("WanderTarget"));
            	if (WanderTarget != NULL)
            		Ret.WanderTargetID = WanderTarget->CreatureID;
            	ABaseCreature* SensedEnemy = (ABaseCreature*)BlackboardComp->GetValueAsObject(FName("SensedEnemy"));
            	if (SensedEnemy != NULL)
            		Ret.SensedEnemyID = SensedEnemy->CreatureID;
            
            	return Ret;
            }
            
            void AMonsterController::LoadBlackboard(FMonsterRecord Record)
            {
            	BlackboardComp->SetValue<UBlackboardKeyType_Enum>(BBK_ActionState, Record.ActionState);
            	BlackboardComp->SetValue<UBlackboardKeyType_Enum>(BBK_PriorState, Record.PriorState);
            	BlackboardComp->SetValue<UBlackboardKeyType_Enum>(BBK_State, Record.State);
            	BlackboardComp->SetValue<UBlackboardKeyType_Enum>(BBK_OverrideActionType, Record.OverrideActionType);
            	BlackboardComp->SetValue<UBlackboardKeyType_Enum>(BBK_ActionType, Record.ActionType);
            
            	ListReferenceID.Empty();
            
            	int32 WanderTargetID = -1;
            	int32 SensedEnemyID = -1;
            	WanderTargetID = Record.WanderTargetID;
            	SensedEnemyID = Record.SensedEnemyID;
            	ListReferenceID.Add(WanderTargetID);
            	ListReferenceID.Add(SensedEnemyID);
            }
            
            void AMonsterController::PostFixupReferences(AZombieGameState* ZGS)
            {
            	if (ZGS != NULL && ListReferenceID.Num() > 0)
            	{
            		//use our list of loaded ID's and do a reverse lookup
            		if (ListReferenceID[0] != -1)
            		{
            			ABaseCreature* WanderTarget = ZGS->LookupCreatureID(ListReferenceID[0]);
            			BlackboardComp->SetValueAsObject(FName("WanderTarget"), WanderTarget);
            			//BlackboardComp->SetValue<UBlackboardKeyType_Object>(BBK_WanderTarget, WanderTarget);
            		}
            		if (ListReferenceID[1] != -1)
            		{
            			ABaseCreature* SensedEnemy = ZGS->LookupCreatureID(ListReferenceID[1]);
            			BlackboardComp->SetValueAsObject(FName("SensedEnemy"), SensedEnemy);
            			//BlackboardComp->SetValue<UBlackboardKeyType_Object>(BBK_SensedEnemy, SensedEnemy);
            		}
            	}
            }
            One other thing to note is that the MonsterController (derives from AAIController) is not actually created until AFTER a creature finishes spawning. So, I have to override the "PostInitializeComponents()" method in my base creature in order to load the blackboard from a record:
            Code:
            void ABaseCreature::PostInitializeComponents()
            {
            	Super::PostInitializeComponents();
            	
            	if (MonsterRecord.IsValid)
            	{
            		AMonsterController* AMC = Cast<AMonsterController>(Controller);
            		if (AMC != NULL)
            		{
            			AMC->LoadBlackboard(MonsterRecord);
            		}
            	}
            }

            Now, I can save and restore my game as many times as I want, and no matter how many variations of the instanced actors I get, so long as I maintain their ID's, I can repair any object references they have. I can restore an AI to its exact state in the behavior tree and have it resume its behavior as usual.

            The last challenge, and this is going to be hard because there is no documentation... is to figure out how to restore an animation blueprint to a particular state in the state machine graph. Not only do we have to restore to a particular animation state, but we also have to restore to a particular frame within that animation. Here's a screenshot to illustrate the problem:
            Click image for larger version

Name:	AnimState.jpg
Views:	1
Size:	250.1 KB
ID:	1112043

            So, the moment my zombie is spawned, he enters into a "Spawning" state where it plays an animation which has him crawling out of a grave. This animation takes about 15 seconds to complete, so it's a bit lengthy. As you can see from the graph, one of the problems is that if the character is in a different animation and behavior state, we HAVE to visit the spawning node first. The transition logic immediately pops us out of it, but the zombie will still play 2-3 frames of the animation before transitioning, so there is a visible pop between animation sequences. Not good. And, if we save the game when the zombie is half way into the animation sequence and then restore the game, we start the animation sequence all the way from the beginning instead of resuming from where we left off. I don't know how to solve this problem yet. A design work around would be to put save points only at points in the level where the player can't see this problem.

            Anyways, you should now have enough to restore 95% of a game to a prior state
            Attached Files

            Comment


              #7
              This is pretty awesome! I love the use of the interface as a method of allowing specific actors to implement their own custom saving functionality! ...

              Comment


                #8
                The only thing I'm not super clear on with your "new" implementation is the interaction with the Save Game Object and "UGameplayStatics::SaveGameToSlot". Are you still using this functionality, or was it removed altogether? If the latter, how are you actually producing a .sav file on the disk? Another thing that springs to mind is the use of GameState as a serialized object. My understanding of game states is that they were inherently transient and would exist only for the duration of a match, (primarily for tracking and replicating online stats relevant to that particular game session).
                Last edited by UnexpectedSquirt; 01-03-2017, 08:17 AM.

                Comment


                  #9
                  Originally posted by AJQuick View Post
                  The only thing I'm not super clear on with your "new" implementation is the interaction with the Save Game Object and "UGameplayStatics::SaveGameToSlot". Are you still using this functionality, or was it removed altogether? If the latter, how are you actually producing a .sav file on the disk? Another thing that springs to mind is the use of GameState as a serialized object. My understanding of game states is that they were inherently transient and would exist only for the duration of a match, (primarily for tracking and replicating online stats relevant to that particular game session).
                  Yes, I'm still using the "SaveGameToSlot" method. It's a pretty basic method which is only useful for saving game data to disk in a binary format. If you wanted to save other things, such as use preferences or profiles, you'd probably want to revisit the way you save data.

                  Before I started using the SaveGameToSlot method, I was manually creating my own binary data file. I think I was using the same file read / write operations SaveGameToSlot uses (UE4 has a file handling library I think). If the engine doesn't have support for file IO, I can always just use the native C++ fstream libraries and output my own files The important thing to note is that reading / writing to a binary file is relatively easy, but the hard part is figuring out what data to write to file and how to read it back in. Writing game data to a file is easy. Reading a game data file and restoring a game is not. So, when you read in a file, you are pretty much reading blocks of data and then trying to interpret what that data means. Data in binary format is not typed, so if you read in four bytes, it could be anything from an integer to a float, to a masked bool, etc. So, one of the important things to do is include the data type before the data so that you know what you're reading in. You're pretty much saying, "This next block of 4 bytes is going to be a float." or "This next block of data is going to be a struct of type XYZ." or "This next block of data is going to be an array of floats, and the array length is 24." Anyways, I found that I could create some very small files if I manually serialized the file data, but it was also a bit user unfriendly. The "easy" thing to do is just mark a variable with meta data indicating that it should be saved to file. Oh hey... UE4 already has that markup They also have a file IO system that knows how to serialize and deserialize any data type, and they've got a handy method for it.

                  When it comes to GameState, I think of it as a global chunk of data that all actors and all clients may need to access. If you pretend that you have a multiplayer game, you are going to have two connected clients. In order to have a meaningful game, they both need to be synchronized and viewing the exact same game state. So, if I see a zombie on one client, the other client should see it as well. If a new zombie is spawned, it needs to be spawned in both clients. If a wizard throws a fireball, both clients need to see that. It's also super helpful to think about what is and is not deterministic. If I throw a fireball and it has a simple physics trajectory, the only thing which is not deterministic is the initial release velocity. So, if two connected clients see a fireball being launched, the only data they would need to see is the initial launch velocity. Everything afterwards is deterministic, so no further data about the fireball would need to be replicated. If you wanted to take a snapshot of the game, save it to file, annihilate the game, and then restore it from the snapshot, you'd have to capture every single dynamically instanced actor and its state and save it to a persistent data store. So, I kind of see the game state as a global manager of all non-static objects which a connected client or load game file would need to know about in order to restore the game to the exact same state. So, to answer your question: Yes, the game state contains instanced objects. If you're going to save the game and restore from a file, you need to need to save every instanced actor. When you load a game, you're going to be respawning all of those actors and loading their current state from a file. Maybe these instanced actors could be managed by the "GameInstance" class? I've never used it.



                  Small update for those of you trying to save the AI state:
                  I ended up completely abandoning behavior trees and blackboards. The AI logic is now scripted in C++ and blackboards are now a struct (which is super simple to save/load). Eventually, I'm going to create a weighted decision graph to run all of my AI so that it "learns", which is not something a behavior tree could ever support.

                  Comment


                    #10
                    [MENTION=25999]Slayemin[/MENTION] I bookmarked this months back and still haven't had chance to go through it properly. Just wanted to say thanks for writing all this up, it looks like some excellent info on a topic that seems to me to have been rather overlooked by Epic.

                    Comment


                      #11
                      Thanks a lot for this info [MENTION=25999]Slayemin[/MENTION], I was able to get my serialization saving stuff up and running much faster than I would have if I hadn't found this thread.

                      I do have one issue though: variables of my actor's components are not serializing their values. For instance, if I have a ship with a component that gives it health, the variables on the ship are stored in my serialization, while the health variables are not. I am just running ship.Serialize() as shown in this thread. Do I need to run serialization for each component of my actors?

                      Comment


                        #12
                        Originally posted by brethar2 View Post
                        Thanks a lot for this info [MENTION=25999]Slayemin[/MENTION], I was able to get my serialization saving stuff up and running much faster than I would have if I hadn't found this thread.

                        I do have one issue though: variables of my actor's components are not serializing their values. For instance, if I have a ship with a component that gives it health, the variables on the ship are stored in my serialization, while the health variables are not. I am just running ship.Serialize() as shown in this thread. Do I need to run serialization for each component of my actors?
                        The general principle is: "Everything should know how to serialize and deserialize itself".

                        I'm not 100% sure if components are serialized or not, or if they're just initialized to their default values. It would be something to look into. Do components know how to serialize themselves? If not, then you'll have to come up with some scheme to grab the data you want to save from a component and store it in some object which is easier to serialize, and then in the deserialization stage, you'd read those objects back into the component properties. My gut intuition is to say that components know how to serialize and deserialize themselves because every object in UE4 is made of components, and the editor itself calls the same serialization methods to save your project, so if you change a default value on an actor within its component, that value gets serialized somewhere, so it should work... So... I think if you have a custom component for ship health, you may need to override the serialization method to include your additional variables or at least check to see if the serialization method is being invoked on your save method.

                        One other thing to check: Make sure that for every variable you want to save, you have the "SaveGame" flag checked. UE4 will loop through every property in your actor and check for this flag to see if it needs to write it to a file.

                        When you think you have serialized an object, open up the serialized file in a hex editor and look at the data to see if the data you expected to get serialized actually did get saved. You should see a plain text string which describes the variable name and the variable type, followed by the binary data value for that variable. If you don't see an expected data value in your savefile, then it means that it wasn't saved properly and you need to look more closely at the save process. If you do see the variable, but aren't seeing it in an loaded object, then you've got a problem with the deserialization steps and that's where you should begin looking.

                        Comment


                          #13
                          Dam, mods punish me for this but i've got to bump this up. This thread contains so many information about serialization that should have its own wiki page. While Rama's a Epic's guides are good and clear I felt like they are not complete, and leave room for a lot of human error. Thank you very much Slayemin for sharing! You helped me a lot!

                          Comment


                            #14
                            I just wanted to post a significant update which improves on serializing game data. I spent the last week trying to simplify and reduce my save game system so that it's easier to use, uses less data, and is more elegant in its design.

                            Changes:
                            1) I don't use an object manager anymore in the game state.
                            2) I keep track of save file versions for build compatibility
                            3) I support saving pointers and lists of pointers
                            4) You're not required to "pollute" your game objects with savegame meta data


                            I will share all of my code files, and then talk about design and implementation details. I have designed these code segments to be project independent, so I have this working in a blank project with starter content. Each of the following are source code files attached to this post (too much to copy and paste).

                            CustomGameState.h
                            CustomGameState.cpp

                            ISaveable.h
                            ISaveable.cpp

                            TestSaveObj.h
                            TestSaveObj.cpp

                            MySaveGame.h
                            MySaveGame.cpp

                            Important Note: You will need to use C++ in your project in order to use this. I tried really hard to minimize the amount of code required and use the editor / blueprints as much as possible for ease of use -- for now, you'll still need to write C++.

                            In this version, I spent a lot of time trying to eliminate and reduce my save system as much as possible. In my previous design, I was maintaining an object database during game play. The idea was that any time you spawn an object, you have to tell the game state about it and it had an object manager on the backend which would assign that object a unique ID from a pool of integers, and then inject the object into a hash table (TMap). When it came time to save the game state, I just saved the contents of the object database. Since I tracked every object and it had a unique ID associated with it, I could convert object references to integers, which would be useful for serializing pointers. There were a lot of problems with this design approach:

                            1) It creates a lot of variable pollution in the game state. I had to have a unique ID pool and the supporting code infrastructure to manage access to it. I also maintained a game object database.

                            2) If an object isn't registered with the game state upon spawning, it won't get saved. It requires users to make sure that they add the object to the game state so that it can be tracked and managed. It's a potential source for human error (forgetting!) and also adds a lot of extra code bloat. Any time an object gets spawned, it has to get the game state, cast it to the right game state, and add itself. Yuck.

                            3) It consumes memory.

                            4) The design was too tightly coupled.

                            Usage:
                            The updated system completely gets rid of all of this. The only data structure I still have is an array of object records. This is empty for 99.9% of the game and only gets populated during saving and loading, and after saving or loading is complete, it gets emptied. It's "transient", but not. On the blueprint side, I expose two functions: SaveGameState() and LoadGameState(). From the blueprint designer side, a user just needs to worry about calling these two functions and it will "magically" work.

                            If you look at "CustomGameState.cpp", you can see the implementations for each of these functions. You'll have to touch each of these functions lightly. Within the SaveGameState(), you'll have to create a list of function calls for SaveActors(), with the class of actors to save. You'll probably also want to modify the SaveGameInstance values to modify the savegame slot name and profile name. One thing I didn't implement is the current level the game is saved within, so you may want to do that if you have multiple save games with different levels.

                            If you look at the "SaveActors()" method, you'll see that it gets a list of all actors of that class type and then it tries to make an interface call on them. If the actor implements the interface, its "SaveToRecord()" method is called. This is super important!!! Every actor you want to save, will have to implement this interface. The reason is because actors may contain pointers or lists of pointers, and I can't serialize a pointer directly, so the actor will have to implement a schema for converting pointers to something that can be serialized (more on this below).

                            The "LoadGameState()" method is 99% self contained. The only thing you may want to modify is the name of the savegame slot which gets loaded. This function is where most of the magic happens and I think it's very elegant. This is where I implement a two pass deserialization scheme. In the first pass, I load all of the actors from the saved file and then spawn them in the world. The only thing which is missing is any pointer links. I can't relink pointers until every actor is spawned because a spawned actor could be pointing to another actor which hasn't been spawned yet -- so, I need a second pass. In the second pass, I go through every spawned object and call its "Relink Pointers" method and pass it a list of all spawned objects.

                            Serializing Pointers and lists of pointers in an actor:
                            Pointers are hard. Let's review pointers briefly. A pointer is a variable which contains an address to memory, which contains another variable type. This usually contains a memory address which is located on the heap, and usually is an instanced object. Pointers themselves are variables with an address, so you can also have pointers to pointers. So, a pointer value could be a memory address such as 0x0552FC30, which corresponds to the memory address of an object. The critical thing to note is that every time you run your game, without changing any code, the address of the object being pointed to will be different!! So, if you are dumb, you will serialize the pointed to memory address and think that it contains a valid reference to the same object. Completely wrong! We should assume that the memory address of an object is random, so serializing random numbers is fruitless. The technique I came up with (and turns out to be sort of common) is to assign each object a unique object ID (sometimes called an OID in other reference materials), and then when you find a pointer which points to an object, instead of serializing the pointer, you serialize the Object ID. When you restore a pointer, you have a unique integer which corresponds to an object in memory, so you just use the OID to lookup the memory address of an object and then relink the pointer.

                            An object will often have more than one pointer. It could have dozens of pointers, and it could have lists of pointers. The key thing to note about lists (arrays) is that the length can be variable. If you look at my "FSaveDataRecord" struct located in "ISaveable.h", you can see: TArray<uint32> PointerList;
                            As far as the save file is concerned, it's just a list of integers which it saves and it neither knows nor cares about what those integers mean. You get to decide what those integers represent, and that decision gets made per interface call. If you have an actor which contains five fixed pointers, you can add five integers representing the OID of each pointer. If you have a mix of pointers and arrays of pointers, you can add extra numbers to the array to act as meta data. In my case, I save the array count of a pointer list, followed by a list of OID's. When you load the pointers back in, you know how many you are going to load before moving onto the next item in the array. If you use this meta data schema, take great precaution and care to test your implementation for correctness -- I find this is a high source for a lot of human error.

                            If you look at TestSaveObj.cpp and my implementation of "SaveToRecord()" interface call, you can see examples illustrating each of these techniques.

                            I made an important discovery as well: Every single object in Unreal Engine has an assigned unique ID. It's a uint32 stored in UObjectBase. To access it, just call "GetUniqueID" on any object in UE4. I think they use an ID pool on the backend so unique ID's are recycled -- they aren't GUIDs. For the purposes of saving and loading games, this is perfect and helps avoid variable pollution. This is what I use for my OID and allowed me to delete my own implementation of an ID pool in game state.

                            Possible Improvements:
                            Most likely, out of my own ignorance, I don't know how Unreal Engine serializes pointer references. I know they do it somewhere, and it's probably really easy and elegant and probably happens automatically. For now, I'm stuck with implementing a two pass loading scheme and forcing users to implement a save/load interface. There's gotta be a better way to do this such that it's transparent to the user. I'm sure it's a solved problem for a lot of people and teams, I just don't know how other people serialize their pointers so I'm stuck with my janky solution. If you have a better way, PLEASE COMMENT!

                            If you're a glutton for punishment, have a lot of time on your hands, or improving serialization is your focus for the next few months, you could move away from the FObjectAndNameAsStringProxyArchive for the savegame file. If you look at the resulting savegame file in a hex editor, you'll see that it lists objects by strings followed by values. You could implement a raw binary format where you minimize the amount of metadata which gets serialized. This could potentially shrink the size of your savegame files by some unknown percentage, which is probably significant. For most people, this would be a waste of time and a source of bugs, but if you have a strong command of serialization, binary data manipulation, and data compression, you could potentially turn a 500kb savegame file to 5kb? This might open up some new capabilities when it comes to multiplayer games and game state synchronization. If someone joins a game in progress, you could serialize the entire game state into a tiny data packet and send it to them. Alternatively, if a multiplayer client gets disconnected from a game and then reconnects at a later time, you can gracefully resume the game by sending them a game state data packet. Again, this might be a waste of time depending on the size of your game state data file -- improving 500kb to 5kb is nothing, but improving 250mb to 25mb might be valuable.

                            If I come up with any further improvements or learn something important, I'll add another comment. For now, this should be a really good starting point for anyone who wants to implement save games which contain level state information.

                            Comment


                              #15
                              woooo amazing

                              Comment

                              Working...
                              X