ROBOPOETS

Sep 25, 2024

Savegame Assets for Development and Debugging

Unreal Engine comes with a savegame system out of the box, where your savegames inherit from the USaveGame class. If you want to make the most out of this system and provide an easy way to test gameplay and debug issues, creating savegame assets is a great way to go about it.

Why Use Savegame Assets?

Savegame assets can help overcome two types of challenges:

When games become complex, it can get really difficult to test features that are only available past a certain point. Maybe they’re unlocked after a quest is completed or players need to have acquired certain skills or items. In short: the game needs to be in a certain state in order to test a certain feature meaningfully. It would be great if we could just prepare a savegame that represents that state and load it up whenever we need to.

Another thing that every game develper will eventually have to deal with are errors reported by players. Being able to reproduce an error is necessary to fix it. Ideally, players would be able to send a savegame that triggers the bug along with their report so we can inspect it and load it up to reproduce the error on our side.

This is what we’re going to build.

What’s an Asset?

Assets are the building blocks of every Unreal Engine game. They’re imported or created in the engine and are stored in the content directory. Think textures, materials, animations, audio files etc. They contain data and are reused between all objects that use them.

Assets

What’s a Data Asset?

As a game developer, you will probably want to create your own asset types at some point. In Unreal Engine you do that by inheriting from the UDataAsset class, or in our case the UPrimaryDataAsset class. Here is a minimal code example of what that would look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "MyDataAsset.generated.h"

UCLASS()
class MYGAME_API UMyDataAsset : public UPrimaryDataAsset
{
  GENERATED_BODY()

public:
  UPROPERTY(EditDefaultsOnly, Category = SomeCategory)
  float SomeNumber = 42.f;
};

You would then create an asset of that type in the editor’s content browser by opening the import menu, navigating to Miscellaneous → Data Asset, and selecting the type from the list. Open the new asset and this is what you’ll see:

MyDataAsset

Creating Savegame Assets

Let’s build a minimal savegame class and a matching data asset.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "MySaveGame.generated.h"

UCLASS()
class MYGAME_API UMySaveGame : public USaveGame
{
  GENERATED_BODY()

public:
  UPROPERTY(BlueprintReadWrite)
  FString SlotName;

  UPROPERTY(BlueprintReadWrite)
  int64 Timestamp;

  UPROPERTY(BlueprintReadWrite)
  FText PlayerName;

  UPROPERTY(BlueprintReadWrite)
  TArray<int32> Levels;
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "MySaveGameDataAsset.generated.h"

UCLASS()
class MYGAME_API UMySaveGameDataAsset : public UPrimaryDataAsset
{
  GENERATED_BODY()

public:
  UPROPERTY(EditDefaultsOnly, Category = Metadata)
  FString SlotName;

  UPROPERTY(EditDefaultsOnly, Category = Metadata)
  int64 Timestamp;

  UPROPERTY(EditDefaultsOnly, Category = Game)
  FText PlayerName;

  UPROPERTY(EditDefaultsOnly, Category = Game)
  TArray<int32> Levels;
};

The savegame keeps track of the player’s name and a list of unlocked levels, which we store as array of numerical ids for simplicity’s sake. We also store the savegame’s name and a timestamp for when it was saved.

Pay attention to the UPROPERTY declarations in the asset class. They need to include at least EditDefaultsOnly in order to be able to edit them directly from the content browser. It’s somewhat inconvenient that we have to duplicate these properties and keep them in sync between the two classes, but you’ll get used to it and it’s not something that needs to change terribly often during development.

The last step is to create a savegame asset and store some data in it:

MySaveGameDataAsset

Converting Asset to Savegame

Now, the asset itself is not the savegame, so we need a way to transfer the data when the game starts. The best place to do that depends on your particular setup, so let’s just use the UGameInstance class for now.

The idea is this: the game instance has a property that stores a reference to a savegame asset. When the game starts, either as a shipped build or PIE, that asset is used to create the default savegame: the one that’s available right from the beginning, the state of the game when starting a new playthrough. Here’s a simple implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"

class UMySaveGame;
class UMySaveGameDataAsset;

UCLASS()
class MYGAME_API UMyGameInstance : public UGameInstance
{
  GENERATED_BODY()

public:
  UPROPERTY()
  UMySaveGame* CurrentSaveGame = nullptr;

private:
  UPROPERTY(EditDefaultsOnly, Category = SaveGame)
  UMySaveGameDataAsset* DefaultSaveGameAsset = nullptr;

public:
  virtual void Init() override;
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include "MyGameInstance.h"

#include "Kismet/GameplayStatics.h"

#include "MySaveGame.h"
#include "MySaveGameDataAsset.h"

void UMyGameInstance::Init()
{
  Super::Init();

  CurrentSaveGame = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
  CurrentSaveGame->SlotName = DefaultSaveGameAsset->SlotName;
  CurrentSaveGame->Timestamp = DefaultSaveGameAsset->Timestamp;
  CurrentSaveGame->PlayerName = DefaultSaveGameAsset->PlayerName;
  CurrentSaveGame->Levels = DefaultSaveGameAsset->Levels;
}

Remember, this is only necessary for the initial savegame. It will be overwritten whenever you load a savegame from a previous checkpoint. It represents the state of the game when players start a new playthrough. It should also go without saying that the examples above are intentionally kept simple to illustrate a point. You will have to put this logic elsewhere depending on your specific use case. We do that too.

What we have so far is the ability to load a savegame that we have constructed to represent a particular state the game can be in. We can test the game in that state, find bugs and inspect edge cases. And we can start doing that the moment we hit the Play button in the editor.

Let’s say we want to use this with a savegame that a playtester sent us, along with a description of how to reproduce a specific bug. We can load their savegame without a problem, but what if we want to look at the actual data?

Converting Savegame to Asset

In order to turn a savegame file into a savegame asset, we’re going to write an editor module that does the job.

First, some boilerplate. We need to set up the editor module folder and register it in the build system. If you’re not familiar with the process, you can read up on it in the official docs. Once the module is set up, we’re going to build a UMG widget that allows us to select from a list of savegames and convert the selected savegame to an asset, stored somewhere in the content browser. Let’s call the class USaveGameInspector. This is a minimal implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma once

#include "CoreMinimal.h"
#include "EditorUtilityWidget.h"
#include "SaveGameInspector.generated.h"

class UMySaveGame;

UCLASS()
class MYGAMEEDITOR_API USaveGameInspector : public UEditorUtilityWidget
{
  GENERATED_BODY()

protected:
  UPROPERTY(BlueprintReadOnly)
  TArray<UMySaveGame*> SaveGames;

  UFUNCTION(BlueprintCallable)
  void LoadSaveGames();

  UFUNCTION(BlueprintCallable)
  void SaveAsAsset(UMySaveGame* SaveGame);
};

These are the important parts. First, there’s a function to find all savegames. For now, we’re just looking in the editor’s default savegame location. You can easily change that, but it’s usually good enough for most cases. The rest is pretty straightforward: read the file name and modification date and put it all in a list.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void USaveGameInspector::LoadSaveGames()
{
  FString SavePath(FPaths::ProjectSavedDir() + TEXT("SaveGames"));
  IFileManager &Mgr = IFileManager::Get();
  if (!Mgr.DirectoryExists(*SavePath))
  {
    return;
  }

  TArray<FString> Files;
  Mgr.FindFiles(Files, *SavePath, TEXT("sav"));

  SaveGames.Reset();
  for (const FString &F : Files)
  {
    if (UMySaveGame* SG = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(FPaths::GetBaseFilename(F), 0)))
    {
      SaveGames.Add(SG);
    }
  }
}

And here is the implementation for the function that converts the savegame to an asset:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void USaveGameInspector::SaveAsAsset(UMySaveGame * SaveGame)
{
  if (SaveGame)
  {
    FString TrimmedSlotName = SaveGame->SlotName.Replace(*FString(" "), *FString(""));
    FString PackageName = TEXT("/Game/Tmp/") + TrimmedSlotName;
    UPackage* Package = CreatePackage(*PackageName);
    Package->FullyLoad();

    UMySaveGameDataAsset* Asset = NewObject<UMySaveGameDataAsset>(Package, UMySaveGameDataAsset::StaticClass(), *TrimmedSlotName, RF_Public | RF_Standalone);
    Asset->SlotName = SaveGame->SlotName;
    Asset->Timestamp = SaveGame->Timestamp;
    Asset->PlayerName = SaveGame->PlayerName;
    Asset->Levels = SaveGame->Levels;

    Package->MarkPackageDirty();
    FAssetRegistryModule::AssetCreated(Asset);

    FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
    UPackage::SavePackage(Package, Asset, RF_Public | RF_Standalone, *PackageFileName, GError, nullptr, true, true, SAVE_NoError);
  }
}

We create the asset object and write the savegame property values to the asset’s. The only tricky part is saving the asset in a way that turns it into a file you can select in the content browser. That’s what the whole UPackage stuff does.

You might notice some similarities betwen this code and the function in the game instance class that writes the asset’s values to the savegame. That means we now have a complete roundtrip of the data.

Building the Editor Utility Widget

The last step is creating the editor widget that we will use to call all this functionality.

In the editor, create a new utility widget and select our widget class as the parent. Next, populate the widget tree by creating three widgets inside it:

Widget Tree

There are some other widgets in the screenshot that are used for grouping and layout. We’re not going into these here, but you’re free to go wild with whatever layout you prefer, of course. Hook up the functions to the buttons and you’re done.

Connections

The Final Result

Let’s see all of this in action with Days of Defiance, an upcoming game that’s being developed at Robo Poets. In the video below, we set a character’s starting location in the savegame asset. We move around a little, save the game, and load the saved game into a savegame asset where we can inspect the character’s saved position.