Game Saving In C++

Hiya Guys,

I have spent the past couple days re-writing my game serialization so it supports objects at runtime. I’ve got this entirely working within the Editor but last night I thought I had needed to get ready for a playtest, I packaged my build and the game is executing in a different order.

All of the game data is being read correctly however I did not realise the order of executions changed in a packaged game.

Could anyone help out with this problem? Below I have included all the code of the relevant class.

void AClimbingInCPPGameMode::SetSlotName(FString NewSlotName)
{
// Ignore empty name
if (NewSlotName.Len() == 0)
{
return;
}

CurrentSlotName = NewSlotName;

}

void AClimbingInCPPGameMode::WriteSaveGame()
{
Debug::Print(“Running write save game”);
Debug::Print(CurrentSlotName);
auto GameInstance = Cast(UGameplayStatics::GetGameInstance(GetWorld()));
if (GameInstance == nullptr)
{
// Warn about failure to save?
Debug::Print(“FAILURE TO SAVE : GAME STATE == NULL”);
return;
}
if (UGameplayStatics::DoesSaveGameExist(CurrentSlotName, 0))
{
if (CurrentSaveGame)
{
CurrentSaveGame->SavedActors.Empty();
}
}
// Iterate the entire world of actors
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
if (Actor->Implements())
{
FActorSaveData ActorData;
ActorData.ActorName = Actor->GetFName();
ActorData.Transform = Actor->GetActorTransform();
ActorData.ActorClass = Actor->GetClass();
FString LevelName;
LevelName = GetWorld()->GetMapName();
// Pass the array to fill with data from Actor
FMemoryWriter MemWriter(ActorData.ByteData);

		FObjectAndNameAsStringProxyArchive Ar(MemWriter, true);
		// Find only variables with UPROPERTY(SaveGame)
		Ar.ArIsSaveGame = true;
		// Converts Actor's SaveGame UPROPERTIES into binary array
		Actor->Serialize(Ar);
		//Debug::Print("Object added to save data: " + ActorData.ActorName.ToString());
		CurrentSaveGame->SavedActors.Add(ActorData);
		CurrentSaveGame->LevelName = LevelName;
	}
}
if (UGameplayStatics::DoesSaveGameExist(CurrentSlotName, 0))
{
	UGameplayStatics::SaveGameToSlot(CurrentSaveGame, CurrentSlotName, 0);
	UpdateCurrentSaveGameData();
	Debug::Print("Game saved");
}

}

void AClimbingInCPPGameMode::LoadSaveGame()
{
FString LevelName;
LevelName = GetWorld()->GetMapName();
auto GameInstance = Cast(UGameplayStatics::GetGameInstance(GetWorld()));
// Update slot name first if specified, otherwise keeps default name
FString ProjectPath = FPlatformMisc::ProjectDir();
SavePath = ProjectPath + “Saved/SaveGames/” +GameInstance->CurrentlyLoadedSaveState;
CurrentSlotName = SavePath + LevelName;
Debug::Print(SavePath + LevelName + “: This is the current save slot”, 10,10);
if (UGameplayStatics::DoesSaveGameExist(CurrentSlotName, 0))
{
CurrentSlotName.RemoveFromStart(“Current”, ESearchCase::CaseSensitive);
auto LoadedSave = UGameplayStatics::LoadGameFromSlot(CurrentSlotName, 0);
CurrentSaveGame = Cast(LoadedSave);
if (!CurrentSaveGame)
{
Debug::Print(“Failed to load SaveGame Data.”);
return;
}
TArray allActors;
for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It;
FName ActorName = Actor->GetFName();
allActors.Add(ActorName);
}

	for (FActorSaveData& ActorData : CurrentSaveGame->SavedActors)
	{
		if (!allActors.Contains(ActorData.ActorName))
		{
			if (!ActorData.ActorName.ToString().Contains("ClimbCharacter"))
			{
				AActor* newlySpawned = GetWorld()->SpawnActor(ActorData.ActorClass);
				newlySpawned->SetActorTransform(ActorData.Transform);
				Debug::Print( "Name of Actor" +newlySpawned->GetName() + "Location" + newlySpawned->GetActorLocation().ToString(),FMath::RandRange(0, 100000000),10);
			}
		}
	}
	// Iterate the entire world of actors
	for (FActorIterator It(GetWorld()); It; ++It)
	{
		AActor* Actor = *It;
		// Only interested in our 'gameplay actors'
		if (Actor->Implements<USaveInterface>())
		{
			for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
			{
				if (ActorData.ActorName == Actor->GetFName())
				{
					Actor->SetActorTransform(ActorData.Transform);
					
					FMemoryReader MemReader(ActorData.ByteData);
					FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
					Ar.ArIsSaveGame = true;
					// Convert binary array back into actor's variables
					Actor->Serialize(Ar);

					ISaveInterface::Execute_OnActorLoaded(Actor);
					break;
				}
			}
		}
	}

	//OnSaveGameLoaded.Broadcast(CurrentSaveGame);
}
else
{
	NewSaveGameData(CurrentSlotName, GetWorld()->GetMapName(), GameInstance->CurrentlyLoadedSaveState);
}

}

void AClimbingInCPPGameMode::NewSaveGameData(FString SlotName, FString MapName, FString DataSlot)
{
// Create the directory
CurrentSaveGame = Cast(UGameplayStatics::CreateSaveGameObject(UCustomSaveGame::StaticClass()));
if (DataSlot.IsEmpty())
{
CurrentSlotName = SlotName + “/” + “Current”;
CurrentSaveGame->LevelName = nullptr;
}
else
{
CurrentSlotName = SlotName;
CurrentSaveGame->LevelName = MapName;
}
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, CurrentSlotName, 0);
Debug::Print(“No save game found : Created new save game data”);
}

void AClimbingInCPPGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
if (ShouldRun)
{
FString SelectedSaveSlot = UGameplayStatics::ParseOption(Options, “SaveGame”);
CurrentSlotName = SelectedSaveSlot;
LoadSaveGame();
UpdateCurrentSaveGameData();
Debug::Print(“Init game data”, 2, 1);
}
}

void AClimbingInCPPGameMode::PostInitializeComponents()
{
Super::PostInitializeComponents();
}

void AClimbingInCPPGameMode::BeginPlay()
{
Super::BeginPlay();
}

void AClimbingInCPPGameMode::UpdateCurrentSaveGameData()
{
auto GameInstance = Cast(UGameplayStatics::GetGameInstance(GetWorld()));
auto tempSlot = GameInstance->CurrentlyLoadedSaveState;
CurrentSlotName = SavePath + “Current”;
//Debug::Print(CurrentSlotName);
FString LevelName = GetWorld()->GetMapName();
if (UGameplayStatics::DoesSaveGameExist(CurrentSlotName, 0))
{
CurrentSaveGame->LevelName = LevelName;
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SavePath+LevelName, 0);
UGameplayStatics::SaveGameToSlot(CurrentSaveGame, CurrentSlotName, 0);
Debug::Print(“Game saved”);
//Debug::Print(SavePath+LevelName, 0, 5);
}
}

What exactly is the issue ?

What engine version is this ?
Are you saving dynamically spawned actors, or only level placed actors ?
Since 4.27, dynamic objects in shipping builds are named with a postfix number that is much less consistent. Even common singletons like GameInstance, GameMode, etc. will have a name that looks like MyGameInstance_C_21748423, and is never the same across two runs.
Even if you disable this “feature” to use the old naming convention, relying on names for cross-session saving is risky. Only the common singleton classes may be somewhat consistent.

If you are only saving Level-placed actors, ignore this as those should be consistent.


Looking at your code I can see that you are at least saving the Character, which is also a dynamic one. You added a check to avoid spawning a second character which is good, but you haven’t added code to load the saved data into the existing character.
Since the existing one will not have the same name as the saved one, you’ll have to retrieve it manually and load saved data into it.
You’ll have to identify other actors which are in the same situation and do the same thing.

Hiya Chatouille,

I am using engine version 5.3.

Sorry if I was unclear before, below is the Load Save Game function. I am saving Dynamically spawned actors to be loaded again.

From further diagnosing, the following lines seems to set the actors with the correct transform data, but when I attempt to execute their interface functionality, it does not even execute.

I think I avoid the issue of the Post prefix number as I write all the game data and spawn entirely new objects. I do not believe that this is the issue. EDIT : This may be the issue, I am going back and diagnosing the problem again, and adding a few more checks and will update the thread as I go.

I have attached a video of the exact problem and a comparison between the packaged game and the game running in the IDE.

for (FActorSaveData& ActorData : CurrentSaveGame->SavedActors)
		{
			if (!allActors.Contains(ActorData.ActorName))
			{
				if (!ActorData.ActorName.ToString().Contains("ClimbCharacter"))
				{
					AActor* newlySpawned = GetWorld()->SpawnActor(ActorData.ActorClass);
					newlySpawned->SetActorTransform(ActorData.Transform);
					allActors.Add(ActorData.ActorName);
					Debug::Print( "Name of Actor" +newlySpawned->GetName() + "Location" + newlySpawned->GetActorLocation().ToString(),FMath::RandRange(0, 100000000),10);
				}
			}
		}

After I spawn all these objects, I then access all objects in the world and attempt to load all their relevant data. This works in the IDE but not packaged game. I hope this clears up the problem.

// Iterate the entire world of actors
		for (FActorIterator It(GetWorld()); It; ++It)
		{
			AActor* Actor = *It;
			// Only interested in our 'gameplay actors'
			if (Actor->Implements<USaveInterface>())
			{
				for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
				{
					if (ActorData.ActorName == Actor->GetFName())
					{
						Actor->SetActorTransform(ActorData.Transform);
						
						FMemoryReader MemReader(ActorData.ByteData);
						FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
						Ar.ArIsSaveGame = true;
						// Convert binary array back into actors variables
						Actor->Serialize(Ar);
						ISaveInterface::Execute_OnActorLoaded(Actor);
					}
				}
			}
		}

The entire load class is here if you would like to read.

AClimbingInCPPGameMode::AClimbingInCPPGameMode()
{
}

void AClimbingInCPPGameMode::StartPlay()
{
	Super::StartPlay();
}

void AClimbingInCPPGameMode::SetSlotName(FString NewSlotName)
{
	// Ignore empty name
	if (NewSlotName.Len() == 0)
	{
		return;
	}

	CurrentSlotName = NewSlotName;
}

void AClimbingInCPPGameMode::WriteSaveGame()
{
	Debug::Print("Running write save game");
	Debug::Print(CurrentSlotName);
	auto GameInstance = Cast<UCustomGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	if (GameInstance == nullptr)
	{
		// Warn about failure to save?
		Debug::Print("FAILURE TO SAVE : GAME STATE == NULL");
		return;
	}
	if (UGameplayStatics::DoesSaveGameExist(CurrentSlotName, 0))
	{
		if (CurrentSaveGame)
		{
			CurrentSaveGame->SavedActors.Empty();
		}
	}
	// Iterate the entire world of actors
	for (FActorIterator It(GetWorld()); It; ++It)
	{
		AActor* Actor = *It;
		if (Actor->Implements<USaveInterface>())
		{
			FActorSaveData ActorData;
			ActorData.ActorName = Actor->GetFName();
			ActorData.Transform = Actor->GetActorTransform();
			ActorData.ActorClass = Actor->GetClass();
			FString LevelName;
			LevelName = GetWorld()->GetMapName();
			// Pass the array to fill with data from Actor
			FMemoryWriter MemWriter(ActorData.ByteData);

			FObjectAndNameAsStringProxyArchive Ar(MemWriter, true);
			// Find only variables with UPROPERTY(SaveGame)
			Ar.ArIsSaveGame = true;
			// Converts Actors SaveGame UPROPERTIES into binary array
			Actor->Serialize(Ar);
			//Debug::Print("Object added to save data: " + ActorData.ActorName.ToString());
			CurrentSaveGame->SavedActors.Add(ActorData);
			CurrentSaveGame->LevelName = LevelName;
		}
	}
	if (UGameplayStatics::DoesSaveGameExist(CurrentSlotName, 0))
	{
		UGameplayStatics::SaveGameToSlot(CurrentSaveGame, CurrentSlotName, 0);
		UpdateCurrentSaveGameData();
		Debug::Print("Game saved");
	}
}

void AClimbingInCPPGameMode::LoadSaveGame()
{
	FString LevelName;
	LevelName = GetWorld()->GetMapName();
	auto GameInstance = Cast<UCustomGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	// Update slot name first if specified, otherwise keeps default name
	FString ProjectPath = FPlatformMisc::ProjectDir();
	SavePath = ProjectPath + "Saved/SaveGames/" +GameInstance->CurrentlyLoadedSaveState;
	CurrentSlotName = SavePath + LevelName;
	Debug::Print(SavePath + LevelName + ": This is the current save slot", 10,10);
	if (UGameplayStatics::DoesSaveGameExist(CurrentSlotName, 0))
	{
		CurrentSlotName.RemoveFromStart("Current", ESearchCase::CaseSensitive);
		auto LoadedSave = UGameplayStatics::LoadGameFromSlot(CurrentSlotName, 0);
		CurrentSaveGame = Cast<UCustomSaveGame>(LoadedSave);
		if (!CurrentSaveGame)
		{
			Debug::Print("Failed to load SaveGame Data.");
			return;
		}
		TArray<FName> allActors;
		TArray<AActor*> allWorldActors;
		for (FActorIterator It(GetWorld()); It; ++It)
		{
			AActor* Actor = *It;
			FName ActorName = Actor->GetFName();
			allActors.Add(ActorName);
			allWorldActors.Add(Actor);
		}

		for (FActorSaveData& ActorData : CurrentSaveGame->SavedActors)
		{
			if (!allActors.Contains(ActorData.ActorName))
			{
				if (!ActorData.ActorName.ToString().Contains("ClimbCharacter"))
				{
					AActor* newlySpawned = GetWorld()->SpawnActor(ActorData.ActorClass);
					newlySpawned->SetActorTransform(ActorData.Transform);
					allActors.Add(ActorData.ActorName);
					allWorldActors.Add(newlySpawned);
					ISaveInterface::Execute_OnActorLoaded(newlySpawned);
					Debug::Print( "Name of Actor" +newlySpawned->GetName() + "Location" + newlySpawned->GetActorLocation().ToString(),FMath::RandRange(0, 100000000),10);
				}
			}
		}
		// Iterate the entire world of actors
		for (AActor* Actor : allWorldActors)
		{
			// Only interested in our 'gameplay actors'
			if (Actor->Implements<USaveInterface>())
			{
				for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
				{
					if (ActorData.ActorName == Actor->GetFName())
					{
						Actor->SetActorTransform(ActorData.Transform);
						FMemoryReader MemReader(ActorData.ByteData);
						FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
						Ar.ArIsSaveGame = true;
						// Convert binary array back into actors variables
						Actor->Serialize(Ar);
						if (ActorData.RuntimeActor)
						{
							Debug::Print( "Name of Actor: " +Actor->GetName(),FMath::RandRange(0, 100000000),10);	
						}
						ISaveInterface::Execute_OnActorLoaded(Actor);
					}
				}
			}
		}

		//OnSaveGameLoaded.Broadcast(CurrentSaveGame);
	}
	else
	{
		NewSaveGameData(CurrentSlotName, GetWorld()->GetMapName(), GameInstance->CurrentlyLoadedSaveState);
	}
}

void AClimbingInCPPGameMode::NewSaveGameData(FString SlotName, FString MapName, FString DataSlot)
{
	// Create the directory
	CurrentSaveGame = Cast<UCustomSaveGame>(UGameplayStatics::CreateSaveGameObject(UCustomSaveGame::StaticClass()));
	if (DataSlot.IsEmpty())
	{
		CurrentSlotName = SlotName + "/" + "Current";
		CurrentSaveGame->LevelName = nullptr;
	}
	else
	{
		CurrentSlotName = SlotName;
		CurrentSaveGame->LevelName = MapName;
	}
	UGameplayStatics::SaveGameToSlot(CurrentSaveGame, CurrentSlotName, 0);
	Debug::Print("No save game found : Created new save game data");
}

void AClimbingInCPPGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
	Super::InitGame(MapName, Options, ErrorMessage);
	FString SelectedSaveSlot = UGameplayStatics::ParseOption(Options, "SaveGame");
	CurrentSlotName = SelectedSaveSlot;
}

void AClimbingInCPPGameMode::PostInitializeComponents()
{
	Super::PostInitializeComponents();
}

void AClimbingInCPPGameMode::BeginPlay()
{
	Super::BeginPlay();
	if (ShouldRun)
	{
		LoadSaveGame();
		UpdateCurrentSaveGameData();
		Debug::Print("Init game data", 2, 1);
	}
}

void AClimbingInCPPGameMode::UpdateCurrentSaveGameData()
{
	auto GameInstance = Cast<UCustomGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	auto tempSlot = GameInstance->CurrentlyLoadedSaveState;
	CurrentSlotName = SavePath + "Current";
	//Debug::Print(CurrentSlotName);
	FString LevelName = GetWorld()->GetMapName();
	if (UGameplayStatics::DoesSaveGameExist(CurrentSlotName, 0))
	{
		CurrentSaveGame->LevelName = LevelName;
		UGameplayStatics::SaveGameToSlot(CurrentSaveGame, SavePath+LevelName, 0);
		UGameplayStatics::SaveGameToSlot(CurrentSaveGame, CurrentSlotName, 0);
		Debug::Print("Game saved");
		//Debug::Print(SavePath+LevelName, 0, 5);
	}
}

So, I’ve done some further testing, and made sure that the game is loading the correct objects.

However, I have now made it so any runtime objects that are spawned change the name in the save file so that the relevant data can be loaded, but a game crash occurs when I use, the following code. This does not occur in PIE and only occurs in the Packaged game crash. I have linked the entire crash dump. In the code for debugging purposes I made it so if it was a runtime actor, it would not serilize the variables thus not setting up the object in its relevant interface function.

Actor->Serialize(Ar);	

ClimbingInCPP.log (105.2 KB)
CrashContext.runtime-xml (7.9 KB)
CrashReportClient.ini (160 Bytes)
UEMinidump.dmp (1.2 MB)

At the bottom of this post I have also attached the OnActorLoaded_Implementation for the interface.

void AClimbingInCPPGameMode::LoadSaveGame()
{
	FString LevelName;
	LevelName = GetWorld()->GetMapName();
	auto GameInstance = Cast<UCustomGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	// Update slot name first if specified, otherwise keeps default name
	FString ProjectPath = FPlatformMisc::ProjectDir();
	SavePath = ProjectPath + "Saved/SaveGames/" +GameInstance->CurrentlyLoadedSaveState;
	CurrentSlotName = SavePath + LevelName;
	Debug::Print(SavePath + LevelName + ": This is the current save slot", 10,10);
	if (UGameplayStatics::DoesSaveGameExist(CurrentSlotName, 0))
	{
		CurrentSlotName.RemoveFromStart("Current", ESearchCase::CaseSensitive);
		auto LoadedSave = UGameplayStatics::LoadGameFromSlot(CurrentSlotName, 0);
		CurrentSaveGame = Cast<UCustomSaveGame>(LoadedSave);
		if (!CurrentSaveGame)
		{
			Debug::Print("Failed to load SaveGame Data.");
			return;
		}
		TArray<FName> allActors;
		TArray<AActor*> allWorldActors;
		for (FActorIterator It(GetWorld()); It; ++It)
		{
			AActor* Actor = *It;
			FName ActorName = Actor->GetFName();
			allActors.Add(ActorName);
			allWorldActors.Add(Actor);
		}

		for (FActorSaveData& ActorData : CurrentSaveGame->SavedActors)
		{
			if (!allActors.Contains(ActorData.ActorName))
			{
				if (!ActorData.ActorName.ToString().Contains("ClimbCharacter"))
				{
					AActor* newlySpawned = GetWorld()->SpawnActor(ActorData.ActorClass);
					ActorData.ActorName = FName(*newlySpawned->GetName());
					allActors.Add(FName(*newlySpawned->GetName()));
					allWorldActors.Add(newlySpawned);
					ActorData.RuntimeActor = true;
					Debug::Print( "Name of Actor" +newlySpawned->GetName() + "Location" + newlySpawned->GetActorLocation().ToString(),FMath::RandRange(0, 100000000),10);
				}
			}
		}
		// Iterate the entire world of actors
		for (AActor* Actor : allWorldActors)
		{
			// Only interested in our 'gameplay actors'
			if (Actor->Implements<USaveInterface>())
			{
				for (FActorSaveData ActorData : CurrentSaveGame->SavedActors)
				{
					if (ActorData.RuntimeActor == true)
					{
						Debug::Print("Found runtime actor data : " + ActorData.ActorName.ToString(),FMath::RandRange(0, 100000000),10);	
					}
					if (ActorData.ActorName == Actor->GetFName())
					{
						Actor->SetActorTransform(ActorData.Transform);
						FMemoryReader MemReader(ActorData.ByteData);
						FObjectAndNameAsStringProxyArchive Ar(MemReader, true);
						Ar.ArIsSaveGame = true;
						// Convert binary array back into actors variables
						if (!ActorData.RuntimeActor)
						{
							Actor->Serialize(Ar);	
						}
						if (ActorData.RuntimeActor == true)
						{
							Debug::Print( "Attempting to execute Actor Loaded :  "   +Actor->GetName(),FMath::RandRange(0, 100000000),10);	
						}
						ISaveInterface::Execute_OnActorLoaded(Actor);

					}
				}
			}
		}

		//OnSaveGameLoaded.Broadcast(CurrentSaveGame);
	}
	else
	{
		NewSaveGameData(CurrentSlotName, GetWorld()->GetMapName(), GameInstance->CurrentlyLoadedSaveState);
	}
}
void APitonSystemComponent::OnActorLoaded_Implementation()
{
	Super::OnActorLoaded_Implementation();
	Debug::Print("Actor loaded");
	Debug::Print( "Actor loaded and exection successful. :  " +GetName(),FMath::RandRange(0, 100000000),10);
	
	if (Placed)
	{
		if (IsAttachedToRouteEnd)
		{
			RouteEnded();
			Cable->CableLength = CableLength;
			Cable->SetMaterial(0, RopeMaterial);
			InteractionSphere->SetRelativeLocation(Cable->EndLocation);
		}
		else
		{
			Debug::Print("True");
			Debug::Print("Reading from its own data");
			Cable->EndLocation = EndTransform.GetLocation();
			Cable->CableLength = CableLength;
			Cable->SetMaterial(0, RopeMaterial);
			InteractionSphere->SetRelativeLocation(Cable->EndLocation);
		}
		Cable->CableLength = CableLength;
		Cable->SetMaterial(0, RopeMaterial); 
	}
	
	
}

Okay!

Problem solved. I do not have the time right now, but I will do a big post on how I went about solving this so if anyone else has any issues like this in the future please use this as reference! This was a pain to figure out!

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.