main() blog

プログラムやゲーム、旅、愛する家族について綴っていきます。

【UE5】SaveGameを使ってみよう!(C++編)

概要

SaveGameは、ゲームの状態を保存し、読み込むためのシステムです。
SaveGameを使用することで、プレイヤーの進行状況、設定、ゲームデータなどを保存できます。

※本記事はC++での開発を前提としています。

環境

UnrealEngine 5.4.2

実装

クラス定義

保存するセーブデータのクラスを定義します。
USaveGameを継承してクラスを作成します。

ツール > 新規C++クラスを選択します。

全てのクラスを選択してSaveGameを選択します。

MySaveGameとしてクラスを作成します。

以下のヘッダーファイルとソースファイルが追加されます。

#pragma once

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

/**
 * 
 */
UCLASS()
class TEST002_API UMySaveGame : public USaveGame
{
    GENERATED_BODY()
    
};
#include "MySaveGame.h"

一旦、セーブデータとして以下のパラメータを追加してみます。
保存する変数にはUPROPERTY()を付ける必要があります。

#pragma once

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

/**
 * 
 */
UCLASS()
class TEST002_API UMySaveGame : public USaveGame
{
    GENERATED_BODY()

public:
    UPROPERTY()
    FString Name;

    UPROPERTY()
    int32 HP = 0;

    UPROPERTY()
    int32 Exp = 0;
};

生成

CreateSaveGameを使用して生成します。
今回は試しにGameInstanceSubsystemに実装してみます。
これは自分のプロジェクトに応じて適宜実装箇所を検討してください。

用意したUMySaveGameのインスタンスを保持するためにヘッダに変数を追加します。
SaveGameのロード処理を確認してみると生成したインスタンスを指定して読み込みを行うようにはなっていないようですので、インスタンスを保持する必要は無いかもしれません。
他のクラス等でゲームデータを管理するものを作り、セーブ、ロードを行うタイミングのみでSaveGameを介してデータのやり取りを行う方法でも対応はできそうです。
今回は普通にゲームデータとしてSaveGameクラスのインスタンスを生成して、それを操作する方法で実装してみます。

UCLASS()
class HOGE_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    virtual void Initialize(FSubsystemCollectionBase& Collection);
    virtual void Deinitialize();

private:
    const FString SlotNameGameData = FString("GameData");

private:
    UPROPERTY()
    UMySaveGame* saveData = nullptr;
};
#include "MyGameInstanceSubsystem.h"
#include "Kismet/GameplayStatics.h"
#include "MySaveGame.h"

void UMyGameInstanceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    UE_LOG(LogTemp, Log, TEXT("UMyGameInstanceSubsystem::Initialize()"));

    Super::Initialize(Collection);

    if(!saveData)
    {
        // UMySaveGameのインスタンスを生成.
        saveData = Cast<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
        ensureMsgf(saveData, TEXT("saveData is null."));
    }
}

データをセーブする(非同期)

データのサイズが大きくなった場合、セーブロードに時間がかかるケースが出てくると思われます。
その場合は非同期でセーブロードを行う必要が出てくるので先にこちらを試してみたいと思います。
SaveAsync()とデリゲートで呼ばれる関数SaveCompleted()を追加します。

UCLASS()
class HOGE_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    virtual void Initialize(FSubsystemCollectionBase& Collection);
    virtual void Deinitialize();

    bool AsyncSave();

private:
    const FString SlotNameGameData = FString("GameData");

private:
    UPROPERTY()
    UMySaveGame* saveData = nullptr;

    // デリゲートに登録する関数はUFUNCTIONが必要.
    UFUNCTION()
    void SaveCompleted(const FString& SlotName, const int32 UserIndex, bool bSuccess);
};

デリゲートでセーブ処理終了時に呼ばれる関数を登録します。

bool UMyGameInstanceSubsystem::AsyncSave()
{
    if (!saveData)
    {
        return false;
    }

    FAsyncSaveGameToSlotDelegate delegate = FAsyncSaveGameToSlotDelegate::CreateUObject(this, &UMyGameInstanceSubsystem::SaveCompleted);

    UGameplayStatics::AsyncSaveGameToSlot(saveData, SlotNameGameData, 0, delegate);

    return true;
}

void UMyGameInstanceSubsystem::SaveCompleted(const FString& SlotName, const int32 UserIndex, bool bSuccess)
{
    UE_LOG(LogTemp, Log, TEXT("UMyGameInstanceSubsystem::SaveCompleted() : [%s][%d][%d]"), *SlotName, UserIndex, bSuccess);

    if (bSuccess)
    {
        UE_LOG(LogTemp, Log, TEXT("success : AsyncSaveGameToSlot()"));
    }
    else
    {
        UE_LOG(LogTemp, Log, TEXT("failed : AsyncSaveGameToSlot()"));
    }
}

セーブデータのパラメータに適当な値を設定してセーブ処理を呼び出します。

hoge()
{
    if (saveData)
    {
        saveData->Name = FString("HogeHoge");
        saveData->HP = 200;
        saveData->Exp = 2000;

        AsyncSave();
    }
}

セーブが成功すると以下にセーブデータ(*.sav)が保存されます。
今回はスロット名を”GameData"としているので以下のファイルが作成されています。

プロジェクト\Saved\SaveGames\GameData.sav

データをロードする(非同期)

LoadAsync()とデリゲートで呼ばれる関数LoadCompleted()を追加します。

UCLASS()
class HOGE_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    virtual void Initialize(FSubsystemCollectionBase& Collection);
    virtual void Deinitialize();

    bool AsyncSave();
    bool AsyncLoad();

private:
    const FString SlotNameGameData = FString("GameData");

private:
    UPROPERTY()
    UMySaveGame* saveData = nullptr;

    // デリゲートに登録する関数はUFUNCTIONが必要.
    UFUNCTION()
    void SaveCompleted(const FString& SlotName, const int32 UserIndex, bool bSuccess);

    UFUNCTION()
    void LoadCompleted(const FString& SlotName, const int32 UserIndex, USaveGame* SaveGame);
};

ロード処理終了時に呼ばれる関数を登録します。

bool UMyGameInstanceSubsystem::AsyncLoad()
{
    if (!saveData)
    {
        return false;
    }

    if (!UGameplayStatics::DoesSaveGameExist(SlotNameGameData, 0))
    {
        UE_LOG(LogTemp, Log, TEXT("not found GameData : [%s]"), *SlotNameGameData);
        return false;
    }

    FAsyncLoadGameFromSlotDelegate delegate = FAsyncLoadGameFromSlotDelegate::CreateUObject(this, &UMyGameInstanceSubsystem::LoadCompleted);

    UGameplayStatics::AsyncLoadGameFromSlot(SlotNameGameData, 0, delegate);

    return true;
}

void UMyGameInstanceSubsystem::LoadCompleted(const FString& SlotName, const int32 UserIndex, USaveGame* SaveGame)
{
    UE_LOG(LogTemp, Log, TEXT("UMyGameInstanceSubsystem::LoadCompleted() : [%s][%d]"), *SlotName, UserIndex);

    if (SaveGame)
    {
        UMySaveGame* data = Cast<UMySaveGame>(SaveGame);
        UE_LOG(LogTemp, Log, TEXT("success : AsyncLoadGameToSlot() : [%s][%d][%d]"), *data->Name, data->HP, data->Exp);

        saveData->Name = data->Name;
        saveData->HP = data->HP;
        saveData->Exp = data->Exp;

        UE_LOG(LogTemp, Log, TEXT("apply save data : [%s][%d][%d]"), *saveData->Name, saveData->HP, saveData->Exp);
    }
    else
    {
        UE_LOG(LogTemp, Log, TEXT("failed : AsyncLoadGameToSlot()"));
    }
}

ロード処理を呼んでデータが読み込まれていることを確認します。

LogTemp: UMyGameInstanceSubsystem::LoadCompleted() : [GameData][0]
LogTemp: success : AsyncLoadGameToSlot() : [HogeHoge][200][2000]
LogTemp: apply save data : [HogeHoge][200][2000]

データをセーブする(同期)

同期処理でのセーブロードの方法も書いておきます。

UCLASS()
class HOGE_API UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    virtual void Initialize(FSubsystemCollectionBase& Collection);
    virtual void Deinitialize();

    bool Save();

private:
    const FString SlotNameGameData = FString("GameData");

private:
    UPROPERTY()
    UMySaveGame* saveData = nullptr;

    // デリゲートに登録する関数はUFUNCTIONが必要.
    UFUNCTION()
    void SaveCompleted(const FString& SlotName, const int32 UserIndex, bool bSuccess);

    UFUNCTION()
    void LoadCompleted(const FString& SlotName, const int32 UserIndex, USaveGame* SaveGame);
};
bool UMyGameInstanceSubsystem::Save()
{
    if (!saveData)
    {
        return false;
    }

    if (!UGameplayStatics::SaveGameToSlot(saveData, SlotNameGameData, 0))
    {
        UE_LOG(LogTemp, Log, TEXT("failed : SaveGameToSlot()"));
        return false;
    }

    UE_LOG(LogTemp, Log, TEXT("success : SaveGameToSlot()"));

    return true;
}

適当な値を設定してセーブします。

hoge()
{
    if (saveData)
    {
        saveData->Name = FString("HogeHoge");
        saveData->HP = 100;
        saveData->Exp = 1000;

        Save();
    }
}

データをロードする(同期)

bool UMyGameInstanceSubsystem::Load()
{
    if (!saveData)
    {
        return false;
    }

    if (!UGameplayStatics::DoesSaveGameExist(SlotNameGameData, 0))
    {
        UE_LOG(LogTemp, Log, TEXT("not found GameData : [%s]"), *SlotNameGameData);
        return false;
    }

    UMySaveGame* data = Cast<UMySaveGame>(UGameplayStatics::LoadGameFromSlot(SlotNameGameData, 0));

    if (!data)
    {
        UE_LOG(LogTemp, Log, TEXT("failed : LoadGameToSlot()"));
        return false;
    }

    UE_LOG(LogTemp, Log, TEXT("success : LoadGameToSlot() : [%s][%d][%d]"), *data->Name, data->HP, data->Exp);

    saveData->Name = data->Name;
    saveData->HP = data->HP;
    saveData->Exp = data->Exp;

    UE_LOG(LogTemp, Log, TEXT("apply save data : [%s][%d][%d]"), *saveData->Name, saveData->HP, saveData->Exp);

    return true;
}

ログを確認して読み込まれていることを確認します。

LogTemp: success : LoadGameToSlot() : [HogeHoge][200][2000]
LogTemp: apply save data : [HogeHoge][200][2000]