mcpField
U
ue-serialization-savegames

ue-serialization-savegames

github.com/quodsoler開発4191.1k 回インストール4 週間前に更新
リポジトリ

ue-serialization-savegames について

ゲーム状態や設定、プラットフォームデータを永続化する UE 5.8 開発者向けスキルです。スロット API、非同期 I/O、バイナリシリアライズ、バージョン移行、アクターのスナップショットを扱います。

主な機能

  1. 01USaveGame スロット操作
  2. 02非同期保存と読み込み
  3. 03FArchive バイナリシリアライズ
  4. 04アクターのスナップショットとバージョン移行
  5. 05設定、ユーザー設定、プラットフォームストレージ

活用シーン

  1. 01プレイヤーの進行状況の保存と復元
  2. 02ローカルプレイヤーごとのセーブ管理
  3. 03バージョン間でのセーブデータ移行
  4. 04アクター状態や生バイト列のシリアライズ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/quodsoler/unreal-engine-skills --skill ue-serialization-savegames

インストール後、Claude Code で/ue-serialization-savegamesを入力して読み込まれたか確認します。

同梱ファイルreferences/save-system-architecture.md

詳細

UE のシリアライズとセーブゲーム

対象エンジン:UE 5.8。以下の API は 5.8 のヘッダーで検証済みです。古い形式は「非推奨 — 使用しないでください」に記載しています。

このスキルでは、ゲームの状態の永続化を扱います。具体的には、UGameplayStatics を介した USaveGame オブジェクト、プラットフォームの ISaveGameSystem、FArchive によるバイナリシリアライズ、UPROPERTY(SaveGame) を使ったアクターのスナップショット、セーブデータのバージョン管理、設定の保存です。Build.cs のモジュール:Core、CoreUObject、Engine。UDeveloperSettings を使用する場合は DeveloperSettings を PublicDependencyModuleNames に追加し、FJsonObjectConverter を使用する場合は Json と JsonUtilities を PrivateDependencyModuleNames に追加してください。

コンテキスト

.agents/ue-project-context.md が存在する場合は読み込んでください(モジュール名、規約、有効なプラグイン、GAS/ネットワークの設定)。存在しなくても作業を中断しないでください。

依頼内容とコードベースから該当する領域を特定してください。異なるコードが必要になる解釈が 2 つ考えられる場合に限り、質問してください。

依頼の内容…参照先
セーブオブジェクト、スロット名、セーブ/ロード/削除USaveGame とスロット API
ゲームスレッドをブロックしない処理、コールバック非同期セーブとロード
プレイヤーごとのセーブ、設定プロファイル、画面分割ULocalPlayerSaveGame
生バイト、FArchive、メモリバッファーFArchive とバイナリシリアライズ
アクター、ワールド状態、トランスフォームのスナップショットUPROPERTY(SaveGame) によるアクターの状態
独自の Serialize が必要な USTRUCT、パッチ後に古いセーブデータで問題が発生する場合USTRUCT のカスタムシリアライズ、バージョン管理
コンソール/クラウドストレージ、スロットの列挙、セーブ UIISaveGameSystem とプラットフォームストレージ
プロジェクト設定、ユーザーオプション、.ini、JSON、圧縮設定、Settings、ファイル形式
スロットマネージャーサブシステム、メタデータ、サムネイル、チェックサム、暗号化セーブシステムのアーキテクチャ

USaveGame とスロット API

USaveGame は GameFramework/SaveGame.h:23 で宣言されている抽象 UObject です。これをサブクラス化して UPROPERTY フィールドを追加し、すべての処理を UGameplayStatics 経由で行ってください。

UGameplayStatics::SaveGameToSlot は、すべての非 transient な UPROPERTY を書き込みます。SaveGame フラグで対象を絞り込むことはありません(Kismet/GameplayStatics.h:1159)。除外するフィールドには Transient を指定してください。SaveGame 指定子が有効なのは、ArIsSaveGame が設定されたアーカイブの場合のみです。アクターの状態を参照してください。

// MySaveGame.h
#pragma once

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

USTRUCT(BlueprintType)
struct FMyInventoryItem
{
    GENERATED_BODY()

    UPROPERTY(SaveGame) FName ItemId;
    UPROPERTY(SaveGame) int32 Quantity = 0;
};

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

public:
    UPROPERTY(SaveGame) int32 SaveVersion = 0;   // bump alongside MySaveStage::Latest
    UPROPERTY(SaveGame) float PlayerHealth = 100.f;
    UPROPERTY(SaveGame) FVector LastCheckpoint = FVector::ZeroVector;
    UPROPERTY(SaveGame) TArray<FMyInventoryItem> InventoryItems;
    UPROPERTY(SaveGame) TMap<FName, int32> AbilityLevels;

    /** Store asset references as paths; a hard pointer cannot round-trip through a file. */
    UPROPERTY(SaveGame) FSoftObjectPath LastEquippedWeapon;

    /** Excluded from SaveGameToSlot because it is Transient. */
    UPROPERTY(Transient) float RuntimeOnlyScratch = 0.f;
};

スロット関数はすべて UGameplayStatics の静的関数です(Kismet/GameplayStatics.h)。

呼び出しシグネチャ
CreateSaveGameObjectUSaveGame* (TSubclassOf<USaveGame> SaveGameClass)(:1124)
SaveGameToSlotbool (USaveGame*, const FString& SlotName, const int32 UserIndex)(:1167)
LoadGameFromSlotUSaveGame* (const FString& SlotName, const int32 UserIndex)(:1211)
DoesSaveGameExistbool (const FString& SlotName, const int32 UserIndex)(:1175)
DeleteGameInSlotbool (const FString& SlotName, const int32 UserIndex)(:1231)
SaveGameToMemorybool (USaveGame*, TArray<uint8>& OutSaveData)(:1134)
LoadGameFromMemoryUSaveGame* (const TArray<uint8>& InSaveData)(:1182)
SaveDataToSlotbool (const TArray<uint8>&, const FString& SlotName, const int32 UserIndex)(:1143)
LoadDataFromSlotbool (TArray<uint8>&, const FString& SlotName, const int32 UserIndex)(:1191)

非同期保存と読み込み

非同期のエントリーポイントは通常の(動的ではない)デリゲートを受け取るため、バインドする関数は UFUNCTION にしてはいけません。

  • DECLARE_DELEGATE_ThreeParams(FAsyncSaveGameToSlotDelegate, const FString&, const int32, bool) (GameplayStatics.h:44)
  • DECLARE_DELEGATE_ThreeParams(FAsyncLoadGameFromSlotDelegate, const FString&, const int32, USaveGame*) (GameplayStatics.h:47)
// MySaveSubsystem.h
#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "MySaveSubsystem.generated.h"

class UMySaveGame; class USaveGame;

UCLASS()
class MYGAME_API UMySaveSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    void RequestSave(float PlayerHealth);
    void RequestLoad();

private:
    void HandleSaveComplete(const FString& SlotName, const int32 UserIndex, bool bSuccess);
    void HandleLoadComplete(const FString& SlotName, const int32 UserIndex, USaveGame* LoadedSave);
    void RunMigrations(UMySaveGame* Save);

    UPROPERTY()
    TObjectPtr<UMySaveGame> CurrentSave = nullptr;
    bool bSaveInProgress = false;
};
// MySaveSubsystem.cpp
#include "MySaveSubsystem.h"
#include "MySaveGame.h"
#include "Kismet/GameplayStatics.h"

static const FString GMySaveSlot = TEXT("MainSave");
static constexpr int32 GMyUserIndex = 0; // see ULocalPlayerSaveGame for per-user consoles

void UMySaveSubsystem::RequestSave(float PlayerHealth)
{
    if (bSaveInProgress) { return; } // overlapping writes to one slot can truncate the file
    if (!CurrentSave)
    {
        CurrentSave = Cast<UMySaveGame>(
            UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
    }
    if (!CurrentSave) { return; }
    CurrentSave->PlayerHealth = PlayerHealth;
    CurrentSave->SaveVersion = MySaveStage::Latest; // in-memory data is current; see Versioning

    // Synchronous form blocks the game thread until the platform write finishes:
    // UGameplayStatics::SaveGameToSlot(CurrentSave, GMySaveSlot, GMyUserIndex);
    bSaveInProgress = true;
    UGameplayStatics::AsyncSaveGameToSlot(CurrentSave, GMySaveSlot, GMyUserIndex,
        FAsyncSaveGameToSlotDelegate::CreateUObject(this, &UMySaveSubsystem::HandleSaveComplete));
}

void UMySaveSubsystem::HandleSaveComplete(const FString& SlotName, const int32 UserIndex, bool bSuccess)
{
    bSaveInProgress = false;
    UE_LOG(LogMyGame, Log, TEXT("Save '%s' (user %d) ok=%d"), *SlotName, UserIndex, bSuccess ? 1 : 0);
}

void UMySaveSubsystem::RequestLoad()
{
    UGameplayStatics::AsyncLoadGameFromSlot(GMySaveSlot, GMyUserIndex,
        FAsyncLoadGameFromSlotDelegate::CreateUObject(this, &UMySaveSubsystem::HandleLoadComplete));
}

void UMySaveSubsystem::HandleLoadComplete(const FString& SlotName, const int32 UserIndex, USaveGame* LoadedSave)
{
    CurrentSave = Cast<UMySaveGame>(LoadedSave); // null when the slot is missing or unreadable
    if (!CurrentSave)
    {
        CurrentSave = Cast<UMySaveGame>(
            UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
        return;
    }
    RunMigrations(CurrentSave);
}

存続させる所有 UObject がない場合は CreateLambda を使用できます。呼び出しが書き込みの完了前に戻るため、値でキャプチャしてください。デリゲートはゲームスレッドで実行され(check(IsInGameThread())、GameplayStatics.cpp:2417)、スロット名が空の場合、またはシリアライズに失敗した場合は、呼び出し中に同期的に実行されます(:2424)。SaveGameToMemory 自体は常にゲームスレッドで実行され(:2410)、非同期なのはプラットフォームへの書き込みだけです。

ULocalPlayerSaveGame

ULocalPlayerSaveGame は GameFramework/SaveGame.h:47 で宣言されています(LocalPlayerSaveGame.h はありません)。セーブを1人のローカルプレイヤーに紐付け、プラットフォームユーザーインデックスを自動的に解決し、サブクラスでオーバーライドできるバージョン管理用フックを提供します。GetLatestDataVersion() は現在のスキーマ番号を返します。HandlePostLoad() はこれを、最後に保存したときの値である GetSavedDataVersion() と比較し、古いデータを移行します。HandlePreSave() は書き込み前にフィールドをサニタイズし、HandlePostSave(bool bSuccess) で保存結果を受け取ります。それぞれで Super:: を呼び出してください。2段階の移行を含む完全なサブクラスは、references/save-system-architecture.md にあります。

ネイティブのデリゲートオーバーロードは ULocalPlayer* を受け取ります。APlayerController* のオーバーロードが受け取るのは、代わりに動的な FOnLocalPlayerSaveGameLoaded です。

// Synchronous: returns null only for invalid parameters, otherwise creates a fresh instance.
UMyLocalPlayerSave* Save = Cast<UMyLocalPlayerSave>(
    ULocalPlayerSaveGame::LoadOrCreateSaveGameForLocalPlayer(
        UMyLocalPlayerSave::StaticClass(), PlayerController, TEXT("PlayerSlot0")));

// Asynchronous, native delegate: pass the ULocalPlayer, not the controller.
const ULocalPlayer* LocalPlayer = PlayerController ? PlayerController->GetLocalPlayer() : nullptr;
const bool bScheduled = ULocalPlayerSaveGame::AsyncLoadOrCreateSaveGameForLocalPlayer(
    UMyLocalPlayerSave::StaticClass(), LocalPlayer, TEXT("PlayerSlot0"),
    FOnLocalPlayerSaveGameLoadedNative::CreateUObject(this, &AMyPlayerController::HandleSaveLoaded));

// Writing back; results arrive through HandlePostSave, not the return value.
Save->AsyncSaveGameToSlotForLocalPlayer(); // bool: the save was requested
Save->SaveGameToSlotForLocalPlayer();      // synchronous

ほかに確認済みのメンバー(GameFramework/SaveGame.h:50-224):CreateNewSaveGameForLocalPlayer、GetLocalPlayerController、GetLocalPlayer、SetLocalPlayer、GetPlatformUserId、GetPlatformUserIndex、GetSaveSlotName、SetSaveSlotName、GetSavedDataVersion、GetInvalidDataVersion、WasLoaded、IsSaveInProgress、WasLastSaveSuccessful、WasSaveRequested、InitializeSaveGame、ResetToDefault。

FArchiveとバイナリシリアライズ

FArchive(Serialization/Archive.h)は双方向です。読み込みと書き込みの両方を、1つの operator<< の本体で処理します。

Ar.IsLoading()                 // Archive.h:272
Ar.IsSaving()                  // Archive.h:284
Ar.IsError()                   // Archive.h:398 — check after every block
Ar.Tell()                      // Archive.h:185 — int64 position
Ar.IsSaveGame()                // Archive.h:659 — reads the ArIsSaveGame field
Ar.ArIsSaveGame = true;        // Archive.h:942 — public bitfield, no setter exists
Ar.UsingCustomVersion(Guid);   // Archive.h:2006
Ar.CustomVer(Guid);            // Archive.h:269 — int32

FMemoryWriter / FMemoryReader は、TArray<uint8> との間でバイト列をやり取りします。どちらも bIsPersistent を受け取るため(MemoryWriter.h:26、MemoryReader.h:52)、アーカイブが一時的なメモリ内書き込みではなく、ディスク上への書き込みと同様に動作するよう、true を渡してください。

#include "Serialization/MemoryWriter.h"
#include "Serialization/MemoryReader.h"

bool UMyObject::WriteBlob(TArray<uint8>& OutBytes)
{
    FMemoryWriter Writer(OutBytes, /*bIsPersistent=*/true);
    int32 Magic = 0x4D595347;   // 'MYSG'
    int32 Version = 2;
    Writer << Magic << Version << BinaryBlob;
    return !Writer.IsError();
}

bool UMyObject::ReadBlob(const TArray<uint8>& InBytes)
{
    FMemoryReader Reader(InBytes, /*bIsPersistent=*/true);
    int32 Magic = 0;
    int32 Version = 0;
    Reader << Magic << Version;
    if (Reader.IsError() || Magic != 0x4D595347 || Version < 1) { return false; }
    Reader << BinaryBlob;
    return !Reader.IsError();
}

virtual void UMyObject::Serialize(FArchive& Ar) をオーバーライドすると、UObject のバイト単位の制御が可能になります。タグ付きプロパティブロックが書き込まれるよう、最初に Super::Serialize(Ar) を呼び出してください。自由関数の FArchive& operator<<(FArchive& Ar, FMyCustomData& Data) を定義すると、通常の構造体をアーカイブでシリアライズできるようになります。FBufferArchive(Serialization/BufferArchive.h:47)はメモリライターと TArray<uint8> の両方を継承しているため、アーカイブ自体がバッファです。

UPROPERTY(SaveGame) によるアクター状態の保存

UPROPERTY(SaveGame) は CPF_SaveGame を設定します(UObject/ObjectMacros.h:458、指定子は :1194)。これは ArIsSaveGame が設定されたアーカイブでのみ有効になります。これを使って実行中のアクターをスナップショットとして保存できます。メモリアーカイブを FObjectAndNameAsStringProxyArchive(Serialization/ObjectAndNameAsStringProxyArchive.h:21)でラップすると、UObject と FName の参照が、読み込み順に依存するインデックスではなく文字列として保持されます。

// MyActorRecord.cpp — include MemoryWriter.h, MemoryReader.h,
// ObjectAndNameAsStringProxyArchive.h and GameFramework/Actor.h.
void FMyActorRecord::SaveActor(AActor* Actor)
{
    if (!Actor) { return; }
    ActorClass = Actor->GetClass();
    ActorTransform = Actor->GetActorTransform();
    ByteData.Reset();

    FMemoryWriter MemWriter(ByteData, /*bIsPersistent=*/true);
    FObjectAndNameAsStringProxyArchive Ar(MemWriter, /*bInLoadIfFindFails=*/true);
    Ar.ArIsSaveGame = true; // only UPROPERTY(SaveGame) fields are written
    Actor->Serialize(Ar);
}

void FMyActorRecord::RestoreActor(AActor* Actor) const
{
    if (!Actor || ByteData.Num() == 0) { return; }
    Actor->SetActorTransform(ActorTransform);

    FMemoryReader MemReader(ByteData, /*bIsPersistent=*/true);
    FObjectAndNameAsStringProxyArchive Ar(MemReader, /*bInLoadIfFindFails=*/true);
    Ar.ArIsSaveGame = true;
    Actor->Serialize(Ar);
}

FObjectAndNameAsStringProxyArchive は bResolveRedirectors と bResolveCoreRedirects も公開しています(どちらもデフォルトは false)。ビルド間でアセット名が変更されている可能性がある場合は、これらを設定してください。FNameAsStringProxyArchive(Serialization/NameAsStringProxyArchive.h:11)が処理するのは FName のみです。ワールド全体のキャプチャパイプライン、アクターごとの FGuid 識別子、アクターのセーブインターフェース、リスポーンの順序については、セーブシステムのアーキテクチャ を参照してください。

USTRUCT のカスタムシリアライズ

USTRUCT のフックは、bool Serialize(FArchive& Ar) と WithSerializer = true を指定した TStructOpsTypeTraits の特殊化です。トレイトを特殊化しないと、この関数は呼び出されません。エンジンは TStructOpsTypeTraits<CppStruct>::WithSerializer に基づいて処理を制御します(UObject/Class.h:1307。トレイトは UObject/StructOpsTypeTraits.h:24 で宣言されています)。戻り値の true は「完全に処理済みのため、デフォルトのタグ付きプロパティ処理をスキップする」という意味です。

// MyStatBlock.h — include "CoreMinimal.h" then "MyStatBlock.generated.h".
USTRUCT(BlueprintType)
struct MYGAME_API FMyStatBlock
{
    GENERATED_BODY()

    UPROPERTY(SaveGame) float HP = 100.f;
    UPROPERTY(SaveGame) float Stamina = 100.f;

    bool Serialize(FArchive& Ar);
};

template<>
struct TStructOpsTypeTraits<FMyStatBlock> : public TStructOpsTypeTraitsBase2<FMyStatBlock>
{
    enum
    {
        WithSerializer = true
    };
};
// MyStatBlock.cpp — include "MyStatBlock.h" and "MySaveVersion.h".
bool FMyStatBlock::Serialize(FArchive& Ar)
{
    Ar.UsingCustomVersion(FMySaveVersion::GUID);
    if (Ar.CustomVer(FMySaveVersion::GUID) < FMySaveVersion::SplitStamina)
    {
        float LegacyHealth = 0.f; // the old layout stored a single float
        Ar << LegacyHealth;
        HP = LegacyHealth;
        Stamina = 100.f;
    }
    else
    {
        Ar << HP << Stamina;
    }
    return true;
}

同じベースにある関連トレイト(StructOpsTypeTraits.h):WithPostSerialize、WithStructuredSerializer、WithSerializeFromMismatchedTag、WithNetSerializer、WithIdentical。

バージョン管理

明示的なバージョンフィールド — UGameplayStatics 経由で書き込む USaveGame では、フィールドがほかの UPROPERTY と同じように扱われるため、最も簡単な方法です。

namespace MySaveStage
{
    constexpr int32 Initial = 0, AddedInventory = 1, SoftRefForWeapon = 2, Latest = SoftRefForWeapon;
}

void UMySaveSubsystem::RunMigrations(UMySaveGame* Save)
{
    if (!Save || Save->SaveVersion == MySaveStage::Latest) { return; }
    if (Save->SaveVersion < MySaveStage::AddedInventory) { Save->InventoryItems.Reset(); }
    if (Save->SaveVersion < MySaveStage::SoftRefForWeapon) { Save->LastEquippedWeapon.Reset(); }
    Save->SaveVersion = MySaveStage::Latest; // stamp only after every step ran
}

FCustomVersion — アーカイブごとのバージョン管理です(Serialization/CustomVersion.h:FCustomVersion は :39、FCustomVersionRegistration は :211)。SaveGameToSlot/SaveGameToMemory は、登録済みのすべてのカスタムバージョンをファイルヘッダーに書き込み、ロード時に復元するため(GameplayStatics.cpp:233,207)、USaveGame 内で CustomVer を使用できます。単独の FMemoryWriter/FMemoryReader の組み合わせにはバージョン情報が含まれません。Ar.GetCustomVersions() を自分でシリアライズするか(Archive.h:555,562)、ロード時に CustomVer が -1 を返すことになります。

// MySaveVersion.h — include "CoreMinimal.h" and "Misc/Guid.h".
struct FMySaveVersion
{
    enum Type { Initial = 0, AddedQuestData = 1, SplitStamina = 2, VersionPlusOne, Latest = VersionPlusOne - 1 };

    /** Generate once with FGuid::NewGuid(), then hardcode forever. */
    static const FGuid GUID;
};

// MySaveVersion.cpp — include "MySaveVersion.h" and "Serialization/CustomVersion.h".
const FGuid FMySaveVersion::GUID(0xA1B2C3D4, 0xE5F60718, 0x293A4B5C, 0x6D7E8F90);

// Registers the version with every archive for the module's lifetime.
FCustomVersionRegistration GRegisterMySaveVersion(
    FMySaveVersion::GUID, FMySaveVersion::Latest, TEXT("MySaveVersion"));

// Inside any Serialize(); UsingCustomVersion is required when saving (CustomVer asserts
// otherwise, Archive.cpp:646) and a harmless no-op when loading (Archive.cpp:631).
// CoreData and QuestData are UPROPERTY members of UMyObject.
void UMyObject::SerializeVersioned(FArchive& Ar)
{
    Ar.UsingCustomVersion(FMySaveVersion::GUID);
    const int32 Version = Ar.CustomVer(FMySaveVersion::GUID);
    Ar << CoreData;
    if (Version >= FMySaveVersion::AddedQuestData) { Ar << QuestData; }
    else if (Ar.IsLoading())                       { QuestData.Reset(); }
}

ISaveGameSystem とプラットフォームストレージ

UGameplayStatics は、プラットフォームの ISaveGameSystem(Engine/Public/SaveGameSystem.h:19)を経由します。これは IPlatformFeaturesModule::Get().GetSaveGameSystem() から取得します(Engine/Public/PlatformFeatures.h:41)。UGameplayStatics では利用できない機能、たとえばスロットの列挙、ネイティブの保存 UI、複数ユーザーの確認が必要な場合に限り、直接使用してください。

#include "SaveGameSystem.h"
#include "PlatformFeatures.h"

ISaveGameSystem* SaveSystem = IPlatformFeaturesModule::Get().GetSaveGameSystem();
if (!SaveSystem) { return; }
const bool bPerUser = SaveSystem->DoesSaveSystemSupportMultipleUsers();
TArray<FString> FoundSlots;
SaveSystem->GetSaveGameNames(FoundSlots, /*UserIndex=*/0);
const ISaveGameSystem::ESaveExistsResult R = SaveSystem->DoesSaveGameExistWithResult(TEXT("MainSave"), 0);

インターフェースのメンバー(SaveGameSystem.h:34-58):PlatformHasNativeUI、DoesSaveSystemSupportMultipleUsers、DoesSaveGameExist、DoesSaveGameExistWithResult、GetSaveGameNames、SaveGame、LoadGame、DeleteGame。ISaveGameSystem には、FPlatformUserId をキーとする非同期メソッド群 DoesSaveGameExistAsync、SaveGameAsync、LoadGameAsync、LoadGameIfExistsAsync、DeleteGameAsync、GetSaveGameNamesAsync と InitAsync も宣言されています(:78-97)。FBaseAsyncSaveGameSystem(:178)は、UE::Tasks 上でこれらを実装するヘルパーベースクラスです。セーブパスを自分で組み立てないでください。FGenericSaveGameSystem::GetSaveGamePath(:169)はデスクトップ用のフォールバックにすぎず、コンソールやクラウドのバックエンドはファイルシステムをまったく使用しません。

設定、セッティング、ファイル形式

設定はセーブスロットとは別のチャネルです。オプションや調整には .ini、進行状況には USaveGame を使用します。

必要なものAPIヘッダー
プロジェクト設定ページ、Config=Game、DefaultConfigUDeveloperSettings + GetDefault<T>()。GetContainerName / GetCategoryName / GetSectionName をオーバーライドEngine/DeveloperSettings.h:23,31-35(モジュール DeveloperSettings)
プレイヤーオプション(解像度、オーディオ、キー割り当て)UGameUserSettings::ApplySettings(bool bCheckForCommandLineOverrides)、SaveSettings()、LoadSettings(bool bForceReload)GameFramework/GameUserSettings.h:49,311,307
オブジェクトの UPROPERTY(Config) フィールドの書き込み/読み込みUObject::SaveConfig()、LoadConfig()、OverrideConfigSection(FString&)UObject/Object.h:1283,1389,1370
生の .ini キーGConfig->GetString/SetString(Section, Key, Value, GGameIni)、GConfig->Flush(bRemoveFromCache, GGameIni)Misc/ConfigCacheIni.h:1402,1408,1395。GGameIni は CoreGlobals.h:439
USTRUCT と JSON テキスト間の変換。バイト配列とファイル間の変換(ツール用のみ)FJsonObjectConverter::UStructToJsonObjectString / JsonObjectStringToUStruct。FPaths::ProjectSavedDir() 配下で FFileHelper::SaveArrayToFile / LoadFileToArrayJsonObjectConverter.h:156,313。Misc/FileHelper.h:185,79。Misc/Paths.h:290

セクション [/Script/ModuleName.ClassName] はクラスの CDO に対応します。DefaultEngine.ini の [/Script/Engine.Engine] 配下で GameUserSettingsClassName=/Script/MyGame.MyGameUserSettings を指定して、UGameUserSettings のサブクラスを登録してください。圧縮には FName 形式(NAME_Zlib、NAME_Oodle、NAME_Gzip、NAME_LZ4(UObject/UnrealNames.inl:210-214))を指定して FCompression::CompressMemoryBound / CompressMemory / UncompressMemory(Misc/Compression.h:73,108,143)を使用します。非圧縮サイズもブロブとともに保存する必要があります。FArchiveSaveCompressedProxy(TArray<uint8>&, FName, ECompressionFlags)(Serialization/ArchiveSaveCompressedProxy.h:28、Flush() は :36)と FArchiveLoadCompressedProxy(ArchiveLoadCompressedProxy.h:22)を使うと、同じ処理をアーカイブとして扱えます。UDeveloperSettings クラス、GConfig の呼び出し、JSON の往復変換、圧縮、チェックサム(FCrc::MemCrc32、Misc/Crc.h:29)、FAES 暗号化の実装例については、セーブシステムのアーキテクチャを参照してください。

非推奨 — 使用しないでください

出力しないもの5.8 で使用するもの出典
#include "GameFramework/LocalPlayerSaveGame.h"#include "GameFramework/SaveGame.h"ULocalPlayerSaveGame は GameFramework/SaveGame.h:47 で宣言されています。このヘッダーは存在しません
Ar.SetIsSaveGame(true)Ar.ArIsSaveGame = true;Serialization/Archive.h:942 のパブリックビットフィールド。ヘッダーにセッターはありません
void FMyStruct::Serialize(FArchive&)bool Serialize(FArchive&) + WithSerializer = true を指定した TStructOpsTypeTraits判定箇所は UObject/Class.h:1307、トレイトは UObject/StructOpsTypeTraits.h:24
COMPRESS_ZLIBNAME_ZlibMisc/CompressionFlags.h:18 の COMPRESS_ZLIB_DEPRECATED
FCompression::GetCompressionFormatFromDeprecatedFlags(Flags)NAME_Zlib / NAME_Oodle を直接渡すMisc/Compression.h:173 で UE_DEPRECATED(5.5)
UGameUserSettings::WindowPosX / WindowPosYWindowPositions 配列GameFramework/GameUserSettings.h:480,485 で UE_DEPRECATED(5.6)
AsyncSaveGameToSlot のコールバックに UFUNCTION() を指定通常のメンバー関数FAsyncSaveGameToSlotDelegate は DECLARE_DELEGATE_ThreeParams(GameplayStatics.h:44)で宣言されており、ダイナミックではありません
ISteamRemoteStorage、SteamRemoteStorage()、IOnlineTitleFileInterfaceIPlatformFeaturesModule::Get().GetSaveGameSystem() 経由の ISaveGameSystemこれらはどれも 5.8 のエンジンヘッダーにはありません

よくある間違い

UPROPERTY(SaveGame) が SaveGameToSlot の保存対象を絞り込むと思い込むこと: 絞り込みません。SaveGameToSlot は一時的でないすべてのプロパティを書き込みます(GameplayStatics.h:1159)。フィールドを除外するには Transient を使用してください。SaveGame が関係するのは、ArIsSaveGame = true のアーカイブだけです。

プロキシアーカイブを使わずにアクターをシリアライズすること: 通常の FMemoryWriter は、UObject と FName の参照を、再起動後には保持されないインデックスとして保存します。必ず FObjectAndNameAsStringProxyArchive でラップしてください。

ネイティブのローカルプレイヤーデリゲートに APlayerController* を渡すこと: FOnLocalPlayerSaveGameLoadedNative のオーバーロードが受け取るのは const ULocalPlayer* です。最初に PlayerController->GetLocalPlayer() を呼び出してください。

バージョン管理を逆にすること: すべての移行手順が実行された後にのみ SaveVersion = Latest を設定してください(また、毎回の保存時にも設定し、新規保存データが次回のロード時に移行されないようにします)。保存用アーカイブでは Ar.UsingCustomVersion() が必須です。そうでない場合、CustomVer の check() が失敗します。一方、ロード用アーカイブでは何も行わず、CustomVer はファイルのバージョンを返します。アーカイブにバージョンがない場合は -1 を返します(Archive.cpp:631,646-648)。

Saved/SaveGames/*.sav のパスをハードコーディングすること: コンソールやクラウドバックエンドはこのディレクトリにアクセスしません。UGameplayStatics または ISaveGameSystem を使用してください。

関連スキル

  • ue-cpp-foundations — UPROPERTY/USTRUCT の指定子、UObject のライフタイム、サブシステムの種類
  • ue-data-assets-tables — FSoftObjectPath、FPrimaryAssetId、保存された参照先の非同期ロード
  • ue-gameplay-framework — セーブマネージャーのホストとしての UGameInstance、GameMode によって制御される自動保存ポイント
  • ue-world-level-streaming — ストリーミングレベルと World Partition アクターの状態の永続化
  • ue-async-threading — シリアライズや圧縮をゲームスレッド以外で実行すること
  • ue-networking-replication — サーバー権限でのセーブとクライアントローカルのプロファイル

コメント