【Unreal Engine】Save/Loadシステムの実装:SaveGameクラスによるセーブデータの保存と読み込み

作成: 2026-02-07

UEのSaveGameクラスを使ったセーブ/ロードシステムの実装方法を解説。同期・非同期セーブ、スロット管理、C++とBlueprintの両方のコード例を紹介します。

概要

動作確認環境: UE 5.4+

「プレイヤーの進行状況をセーブしたいが、どこから手をつけるべきかわからない…」――セーブ/ロードシステムはほぼすべてのゲームで必要ですが、データの永続化、スロット管理、非同期処理など考慮すべきポイントが多く、初めて実装すると戸惑いがちです。

UEの USaveGame クラスを使えば、保存したいデータを定義するカスタムコンテナを作成し、スロット管理・非同期セーブまでシンプルなAPIで実装できます。本記事ではC++での実装を中心に、基本概念からベストプラクティスまで解説します。

基本概念

Save/Loadシステムを構成する主要な要素を整理します。

要素説明
SaveGameクラスUSaveGame を継承したカスタムデータコンテナ。保存したい変数を定義する
Save Slot Nameセーブファイルの識別名。複数スロット対応に使う
User Indexユーザープロファイルの識別番号(分割画面マルチプレイヤー向け)
同期セーブSaveGameToSlot — 処理完了までブロックする。メニュー画面向き
非同期セーブAsyncSaveGameToSlot — バックグラウンドで実行(推奨
非同期ロードAsyncLoadGameFromSlot — バックグラウンドでロード
バイナリセーブSaveGameToMemory / LoadGameFromMemory — ディスクを介さずメモリ上で処理。クラウドセーブやネットワーク送信時に使う

セーブ処理の基本的な流れは、「SaveGameオブジェクトにゲーム状態を転送 → スロットに保存」です。ロード時はその逆で、「スロットからSaveGameオブジェクトを復元 → ゲーム内オブジェクトにデータを転送」となります。

推奨: ゲームプレイ中のセーブには AsyncSaveGameToSlot を使ってください。同期セーブはメインスレッドをブロックし、フレームレート低下の原因になります。

開発環境では、セーブファイルはプロジェクトの Saved/SaveGames/ フォルダに .sav 拡張子で保存されます。パッケージ化ビルドでは、プラットフォームに応じた場所(Windowsでは AppData/Local/[GameName]/Saved/SaveGames/)に保存されます。

SaveGameクラスの作成

Save/Loadシステムの最初のステップは、保存するデータを定義するカスタム SaveGame クラスの作成です。このクラスはゲーム状態とディスク間の中間コンテナとして機能します。

C++での定義

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

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

public:
    UPROPERTY(VisibleAnywhere, Category = "SaveData")
    FString PlayerName;

    UPROPERTY(VisibleAnywhere, Category = "SaveData")
    int32 PlayerScore = 0;

    UPROPERTY(VisibleAnywhere, Category = "SaveData")
    FVector PlayerLocation = FVector::ZeroVector;

    UPROPERTY(VisibleAnywhere, Category = "SaveData")
    TArray<FName> InventoryItemIDs;
};

保存する変数は必要最小限に留めましょう。ロード時に再計算・再取得できるデータ(HPの最大値、アイテムの表示名など)は保存しなくて構いません。

Blueprintでの作成

C++を使わない場合は、コンテンツブラウザで右クリック > Blueprint Class > 親クラスに SaveGame を選択して作成します。変数を追加すれば、Blueprintでもまったく同じ機能を実現できます。

ゲームの保存(C++)

UGameplayStatics クラスの関数を使って、SaveGameオブジェクトをディスクに保存します。以下は非同期セーブの実装例です。

#include "Kismet/GameplayStatics.h"
#include "MySaveGame.h"

void AMyPlayerController::SaveGameData()
{
    const FString SlotName = TEXT("MainSlot");
    const int32 UserIndex = 0;

    // 既存データがあればロード、なければ新規作成
    UMySaveGame* SaveInstance = nullptr;
    if (UGameplayStatics::DoesSaveGameExist(SlotName, UserIndex))
    {
        SaveInstance = Cast<UMySaveGame>(
            UGameplayStatics::LoadGameFromSlot(SlotName, UserIndex));
    }
    if (!SaveInstance)
    {
        SaveInstance = Cast<UMySaveGame>(
            UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
    }

    // 現在のゲーム状態をSaveGameオブジェクトに転送
    SaveInstance->PlayerName = TEXT("Hero");
    SaveInstance->PlayerScore = CurrentScore;
    SaveInstance->PlayerLocation = GetPawn()->GetActorLocation();

    // 非同期で保存(メインスレッドをブロックしない)
    FAsyncSaveGameToSlotDelegate OnSaved;
    OnSaved.BindUObject(this, &AMyPlayerController::OnGameSaved);
    UGameplayStatics::AsyncSaveGameToSlot(SaveInstance, SlotName, UserIndex, OnSaved);
}

void AMyPlayerController::OnGameSaved(const FString& SlotName, const int32 UserIndex, bool bSuccess)
{
    if (bSuccess)
    {
        UE_LOG(LogTemp, Log, TEXT("Save succeeded: %s"), *SlotName);
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Save failed: %s"), *SlotName);
    }
}

DoesSaveGameExist で既存セーブの有無を確認し、あれば上書き、なければ新規作成するのが一般的なパターンです。AsyncSaveGameToSlot は完了時にデリゲートを呼び出すため、成功・失敗に応じたフィードバック(UIへの通知など)を実装できます。

セーブタイミングの指針

  • チェックポイント到達時 / レベルクリア時 に自動セーブ
  • メニュー画面 からの手動セーブは同期処理でも可
  • 頻繁すぎる自動セーブはディスクI/O負荷に注意

ゲームの読み込み(C++)

ロード処理は保存処理の逆です。セーブデータからSaveGameオブジェクトを復元し、ゲーム内オブジェクトにデータを適用します。

同期ロード

ロードはメニュー画面等で行われることが多く、同期処理で問題ないケースがほとんどです。

void AMyPlayerController::LoadGameData()
{
    const FString SlotName = TEXT("MainSlot");
    const int32 UserIndex = 0;

    if (!UGameplayStatics::DoesSaveGameExist(SlotName, UserIndex))
    {
        UE_LOG(LogTemp, Warning, TEXT("No save data in slot: %s"), *SlotName);
        return;
    }

    UMySaveGame* Loaded = Cast<UMySaveGame>(
        UGameplayStatics::LoadGameFromSlot(SlotName, UserIndex));
    if (!Loaded) return;

    // SaveGameオブジェクトからゲーム状態を復元
    SetPlayerName(Loaded->PlayerName);
    if (APawn* MyPawn = GetPawn())
    {
        MyPawn->SetActorLocation(Loaded->PlayerLocation);
    }
}

非同期ロード

大規模なセーブデータを扱う場合は、AsyncLoadGameFromSlot でバックグラウンドロードも可能です。

void AMyPlayerController::AsyncLoadGameData()
{
    const FString SlotName = TEXT("MainSlot");
    const int32 UserIndex = 0;

    FAsyncLoadGameFromSlotDelegate OnLoaded;
    OnLoaded.BindUObject(this, &AMyPlayerController::OnGameLoaded);
    UGameplayStatics::AsyncLoadGameFromSlot(SlotName, UserIndex, OnLoaded);
}

void AMyPlayerController::OnGameLoaded(const FString& SlotName, const int32 UserIndex, USaveGame* LoadedData)
{
    if (UMySaveGame* Loaded = Cast<UMySaveGame>(LoadedData))
    {
        // ゲーム状態を復元
        SetPlayerName(Loaded->PlayerName);
    }
}

最適化とトラブルシューティング

最適化のポイント

セーブデータの設計次第で、保存・ロード時間やファイルサイズに大きな差が出ます。

  • 必要最小限のデータのみ保存: ロード時に再生成可能なデータ(派生値、デフォルト設定など)は省略する
  • 構造体でグループ化: 関連するデータを USTRUCT にまとめると管理しやすい
  • IDで参照する: UObject への直接参照は避け、FSoftObjectPath やRowNameを保存する。ロード時に LoadObjectFindObject で再取得する
  • レベル内アクターの保存: アクターの名前やタグをキーとして保存し、ロード時に GetAllActorsOfClass 等で検索して状態を復元する
  • データバージョニング: ゲームの更新で SaveGame クラスの構造が変わった場合に備え、バージョン番号をセーブデータに含めておくと互換性を維持しやすい。以下のように SaveVersion を持たせ、ロード時にマイグレーション処理を分岐させるのが一般的なパターン
UPROPERTY(VisibleAnywhere, Category = "SaveData")
int32 SaveVersion = 1; // バージョンが上がるたびにインクリメント
  • 暗号化・改ざん防止: 出荷ビルドではセーブデータの改ざん対策も考慮が必要。FArchive をカスタムしてAES暗号化を挟む方法や、チェックサムを付与する方法がある。プロジェクト設定の Crypto セクションでパッケージ暗号化キーも設定可能

よくある問題と対処

問題対処
セーブが保存されないSaveGameToSlot の戻り値とログを確認。C++では UPROPERTY() の指定漏れにも注意
ロード後の状態が不正データ転送処理をデバッグ。レベルロード順序とアクターのスポーンタイミングに注意
パッケージ化ビルドで失敗セーブ先フォルダへの書き込み権限を確認
ロード時にクラッシュNullチェック漏れ。存在しないアクターやオブジェクトへのアクセスがないか確認

まとめ

  • USaveGame を継承してカスタムデータコンテナを定義し、保存したい変数を宣言
  • 非同期セーブ (AsyncSaveGameToSlot) でフレームレートへの影響を回避
  • 非同期ロード (AsyncLoadGameFromSlot) で大規模データのバックグラウンド読み込みも可能
  • スロット名とユーザーインデックスで複数セーブファイルを管理
  • UObject の直接参照は避け、ID・パスで保存する

さらに学ぶために