**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:

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:

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:

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:
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:
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:
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:
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:

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 