I'm hitting a wall with saving and loading persistent data in Unreal Engine, especially with references that become invalid.

I’m working on a strategy game (campaign/battle style), and I have a bunch of interconnected AActor-based objects like ARegion, AArmy, and a CampaignPawn that holds the world together. Everything works great… until I try to save and reload the campaign.

Here’s the kicker:

When I destroy the CampaignPawn (like when saving or switching maps), every ARegion or AArmy that holds a pointer to it now has an invalid reference.
ARegion and AArmytheir internal pointers to other classes also break if I try to duplicate or serialize the objects, to load later.

So this:

UPROPERTY(BlueprintReadWrite) TArray<ARegion*> NeighborRegions;

Becomes this:

UPROPERTY(BlueprintReadWrite) TArray<ARegion*> NeighborRegions; UPROPERTY(BlueprintReadWrite) TArray NeighborRegionIDs;

Just to make the relationships save/load safely.

Now I’m doing this everywhere. Army needs to store the region it’s in? That’s a pointer plus an ID. Region needs to store neighboring regions? Same deal. Every single object that references something else now has to carry a stable FGUID, and use that to reconnect during load.

It’s doubling my data. It’s tedious. And it only exists for saving. Game logic runs on the pointers , which are useless after load unless I re-hook them all manually.

Using only FGUIDs and looking things up via a registry/map: Feels super alien to game logic. Now I’m resolving IDs every time I want a neighbor or location. Gross.
Moving everything to UObjects: Doesn’t help. The internal references still break unless I rebuild them manually, which is a mess and needs and extra variable FGUIDs for everysingle reference you use to another class inside a class so to respawn them correctly.

You need two loops - in the first one you spawn everything that is missing (all the actors you referenced) and call UObject::Rename on them so that the objects have the old path.

In the second loop - you deserialize all the data for them. Then the references will work as if nothing happened. :wink:

1 Like

I dont understand how renaming it would reconnect the references to the classes that are now being spawned from scratch when loading back the campaign?
Could you give me some tips here?
I think even if you rename them the old name, the old data is now invalid, so it wont work…?
Is there a tuts for this, or any examples?

Something like this:

Save actor data:

FActorSaveData(Actor->GetClass(), Actor->GetPathName() /*<-ActorPath*/, Actor->GetActorTransform(), ActorBinaryData, ComponentsBinaryData);

Load:

CurrentActor = UGameplayStatics::BeginDeferredActorSpawnFromClass(this, LoadedData.ActorClass, FTransform(), ESpawnActorCollisionHandlingMethod::AlwaysSpawn);

CurrentActor->FinishSpawning(FTransform());

FString IDName;// old actor path (old reference)

LoadedData.ActorPath.Split(".", nullptr, &IDName, ESearchCase::IgnoreCase, ESearchDir::FromEnd);
CurrentActor->Rename(*IDName);

It just works (c)*

*If you first create an object and then “recall” references to it.
If the opposite is true, the magic won’t work.

1 Like

Ah so you are using the old path as the way to identify the references to the objects, is that correct? is the path the only thing reliable for this? I tried something like this before using GetFName() and it didnt even allow me to rename the object. will try this now,a nd get back to you, thanks

Yes, because initially all references are already unique for objects.

You can come up with as many methods as you like, but the reference is the path (under the hood), and that is what we ultimately need.

remark
Be careful with Rename: if suddenly there are two objects with the same name, the game will crash. :sweat_smile:

1 Like

So im saving my URegionObj* (UObject with the data of the game regions).
Then when i load im doing this:

TextureReaderRef->All_Regions[i]->Rename(*GameInstance->Saved_All_Regions[i]->GetPathName());

So this gives the new spawned regions the name of the old Object that was saved in the game instance. Which is quite cool, and might just work.

Is this the correct thing to do?

Though im not doing exactly what you did so far, dont see yet the reason for it:

LoadedData.ActorPath.Split(“.”, nullptr, &IDName, ESearchCase::IgnoreCase, ESearchDir::FromEnd);

Is this really necessary or its just for aesthetic purposes? To have a cleaner name?

It was named as RegionObj_2444 before as you can see:


̶Y̶e̶s̶,̶ ̶t̶h̶a̶t̶’̶s̶ ̶e̶x̶a̶c̶t̶l̶y̶ ̶h̶o̶w̶ ̶i̶t̶’̶s̶ ̶i̶n̶t̶e̶n̶d̶e̶d̶.̶

UPD

Wait, are you setting the object name by copying it from another object? I think I’m sensing a crash here.

Like you have two arrays of objects, or am I misunderstanding?

Most likely yes, I wouldn’t add code that can be done without… But I don’t remember exactly. In any case, you can check for yourself). :upside_down_face:

1 Like

also use FObjectAndNameAsStringProxyArchive

FMemoryWriter MemoryWriter(Data, true);
FObjectAndNameAsStringProxyArchive Ar(MemoryWriter, false);
Ar.ArIsSaveGame = true;
Ar.ArNoDelta = true;
Target->SerializeScriptProperties(Ar);
1 Like

Yeah, i was just getting intrigued with it.
So this is what im doing.
Campaign level ends and battle level is about to start.
So I save the Regions in Saved_All_Regions, then save also the Armies (that are contained in each region), and save also the Buildings that are in each region.
Then after the battle is finished i need to restore back the campaign as it was before the battle.
So im not very sure, but from what i understood from your explanation, i need to rename the Newly spawned regions with the Old name (GetPathName()), so I go on the GameInstance get all the Saved_All_Regions and just assign the new respawned regions (All_Regions) with the old names.
Isn’t this renaming what is going to allow the armies to identify the region they were in?
This is super confusing to me.

Edit:
So for example the Regions have this data witht references to Armies inside it:
So how am i supposed to respawn the regions, if when you change levels and respawn the armies and regions you lose the references to the NeighboringRegions and also the CurrentArmiesInRegion…?
:brain::skull:

UCLASS()
class TOPSYMBOL_API ARegion : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ARegion();

	UPROPERTY(BlueprintReadWrite)
		TArray<FVector2D> BorderTiles;

	UPROPERTY(BlueprintReadWrite)
		TArray<URegionObj*> NeighborRegions;

	UPROPERTY(BlueprintReadWrite)
		TArray<UArmyObj*> CurrentArmiesInRegion;

	UPROPERTY(BlueprintReadWrite)
		FColor Color;

};

Would this also keep the references of the objects alive? Cant wrap my head around this. Is there some tuts or example for me to follow that explains how this works?

It seems we put different meanings into the words “save/load”. :melting_face:
I mean the object serialization that
Auran13
is talking about.

It is after this that the “magic recovery references” occurs.

It is not enough to simply rename the objects you reference (but it is necessary), you also need to deserialize (load) the objects that contain those references.

This

1 Like

yes BUT,

It resolves an object reference by its name, so the name must be consistent and of course it must exist.

so as @PREDALIEN said you need 2 loops,

  1. too spawn/create/rename all objects and then
  2. to deserialize all objects knowning that their references exist and are properly named.

it is a weird system, in that i dont know why they dont use a FGuid over a FName because where it can fail is loading different levels, if you have level1 with Crate_1 and level2 with Crate_1 it could get confused.

so you have to handle that situation (ie save persistant data and level data seperately)

there is a presentation and a GitHub here too

this one looks more interesting because it uses an FStructureArchive which allows you to debug your save data to Json, but i havent learned it yet.

1 Like

The object path has the level name in it (if we talk about actors), so this shouldn’t be a problem.

2 Likes

im pretty sure FObjectAndNameAsStringProxyArchive just uses the ObjectName not ObjectPath but could be wrong, thats why Rename() works

Your intuition is failing you)

2 Likes

you are wise Sensei!

2 Likes

Thanks sensei.
So far im trying something that might be stupid but its working.
Though its only for transition between Campaign Level and Battle Level.
So before I change level,
I duplicate the UObjects using:

	TMap<UArmyObj*, UArmyObj*> Old_New_Army_Map;
	for (int32 i = 0; i < CampaignMapManager->All_Armies.Num(); ++i) {
		UArmyObj* Original = CampaignMapManager->All_Armies[i];
		UArmyObj* SavedArmy = DuplicateObject<UArmyObj>(Original, GameInstance);
		Old_New_Army_Map.Add(CampaignMapManager->All_Armies[i], SavedArmy);

This duplicated the UObject perfectly into the GameInstance. The DuplicateObject is very convenient because you dont need to build the whole project with all the variables.
Then I use the TMap Old_New_Army_Map, to keep track of what the new duplicate armies (backup) correspond to the armies in the Campaign (about to become invalid).
Before I launch the Battle level, I fix all the references in the duplicated UObjects. This is easy because im creatin a TMap. So In the duplicated UObjects you have references to Armies and Regions, you fix those references using the TMap.

Then when you load back the Campaign, do something similar (though some exceptions may be necessary), Spawn all the regions, armies, using again TMaps, that relate the GameInstance UObjects, with the newly respawned UObjects. And then fix them by iterating through them.
And voilá it is working. I did this a bit by intuition… so im not sure…
Is this good enough? Is there something im missing? Do you recommend I do it some other way, or maybe even use SPUD ?
Let me know before i do my whole project this way, only to shoot foot later :foot::water_pistol:

The joke is starting to drag on… :zany_face:

This will only work while the game is running. It won’t be possible to implement a save system this way.
And I’m not sure (haven’t tested) that the references in the duplicated objects will lead to where you expect…

Is it worth making two similar systems, where one of them only does half the work?

UPD

Serialization does this automatically (if you do Rename before)

1 Like