mcpField
U
ue-serialization-savegames

ue-serialization-savegames

github.com/quodsoler开发4191.1k 次安装4 周前更新
代码仓库

关于 ue-serialization-savegames

面向 UE 5.8 开发者的序列化与存档技能,介绍保存游戏状态、设置及平台存储的 API 和模式。涵盖存档槽、异步读写、二进制序列化、版本迁移与 Actor 状态快照。

主要功能

  1. 01USaveGame 槽位读写
  2. 02异步保存与加载
  3. 03FArchive 二进制序列化
  4. 04Actor 状态快照与版本迁移
  5. 05配置、设置与平台存储

适用场景

  1. 01保存和恢复玩家游戏进度
  2. 02为本地玩家管理独立存档
  3. 03跨版本迁移存档数据
  4. 04序列化 Actor 状态或原始字节

安装

复制下面的命令,在终端中运行。

$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) Actor 快照、存档版本管理,以及配置/设置存储。Build.cs 模块:Core、CoreUObject、Engine;使用 UDeveloperSettings 时,将 DeveloperSettings 添加到 PublicDependencyModuleNames;使用 FJsonObjectConverter 时,将 Json 和 JsonUtilities 添加到 PrivateDependencyModuleNames。

上下文

如果 .agents/ue-project-context.md 存在,请阅读(模块名称、约定、已启用的插件、GAS/网络设置)。如果该文件缺失,请继续操作。

根据请求和代码库识别相关领域。只有当两种合理理解会导致不同代码时才提问。

请求涉及……转到
存档对象、存档槽名称、保存/加载/删除USaveGame 和存档槽 API
不阻塞游戏线程、回调异步保存与加载
每位玩家的存档、设置配置文件、分屏ULocalPlayerSaveGame
原始字节、FArchive、内存缓冲区FArchive 和二进制序列化
Actor 快照、世界状态、变换使用 UPROPERTY(SaveGame) 保存 Actor 状态
需要自定义 Serialize 的 USTRUCT;补丁后旧存档失效USTRUCT 自定义序列化、版本管理
控制台/云存储、存档槽枚举、存档 UIISaveGameSystem 和平台存储
项目设置、用户选项、.ini、JSON、压缩配置、设置与文件格式
存档系统子系统、元数据、缩略图、校验和、加密存档系统架构

USaveGame 和存档槽 API

USaveGame 是在 GameFramework/SaveGame.h:23 中声明的抽象 UObject。创建其子类,添加 UPROPERTY 字段,并通过 UGameplayStatics 处理所有操作。

UGameplayStatics::SaveGameToSlot 会写入所有非临时 UPROPERTY——不会根据 SaveGame 标志进行筛选(Kismet/GameplayStatics.h:1159)。将字段标记为 Transient 可将其排除。SaveGame 说明符仅对设置了 ArIsSaveGame 的存档有效——参见Actor 状态。

// 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)。它会将存档绑定到一名本地玩家,自动解析平台用户索引,并提供供子类重写的版本控制钩子。GetLatestDataVersion() 返回当前架构编号。HandlePostLoad() 会将其与 GetSavedDataVersion()(即上次保存时存储的值)进行比较,并迁移旧数据。HandlePreSave() 会在写入前清理字段,保存结果则会传递给 HandlePostSave(bool bSuccess)。每个函数中都要调用 Super::。有关包含两步迁移的完整子类,请参阅引用资料/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)是双向的:一个 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) 保存 Actor 状态

UPROPERTY(SaveGame) 会设置 CPF_SaveGame(UObject/ObjectMacros.h:458,说明符位于 :1194),但只有设置了 ArIsSaveGame 的归档才会识别它。要快照当前 Actor,可将内存归档封装在 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。完整的世界捕获流程、逐 Actor 的 FGuid 标识、Actor 保存接口和重生顺序,请参阅:存档系统架构。

USTRUCT 自定义序列化

USTRUCT 钩子是 bool Serialize(FArchive& Ar),并且需要专门化 TStructOpsTypeTraits,并将 WithSerializer = true。如果没有该 traits 专门化,就永远不会调用此函数——引擎会检查 TStructOpsTypeTraits<CppStruct>::WithSerializer(UObject/Class.h:1307;trait 在 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;
}

同一基础类型上的相关 traits(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),因此 CustomVer 可用于 USaveGame。单独使用一对 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 还声明了一组异步接口:DoesSaveGameExistAsync、SaveGameAsync、LoadGameAsync、LoadGameIfExistsAsync、DeleteGameAsync、GetSaveGameNamesAsync 和 InitAsync,均以 FPlatformUserId 为键(:78-97);FBaseAsyncSaveGameSystem(:178)是一个辅助基类,可在 UE::Tasks 上实现这些接口。切勿手动拼接存档路径:FGenericSaveGameSystem::GetSaveGamePath(:169)仅是桌面平台的回退方案,主机和云端后端会完全忽略文件系统。

配置、设置和文件格式

设置与存档槽是不同的渠道:.ini 用于选项和调整,USaveGame 用于进度。

需求API头文件
项目设置页面、Config=Game、DefaultConfigUDeveloperSettings + GetDefault<T>();重写 GetContainerName / GetCategoryName / GetSectionNameEngine/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 子类。压缩使用 FCompression::CompressMemoryBound / CompressMemory / UncompressMemory(Misc/Compression.h:73,108,143),并指定 FName 格式——NAME_Zlib、NAME_Oodle、NAME_Gzip、NAME_LZ4(UObject/UnrealNames.inl:210-214)——且必须将未压缩大小与数据块一起存储;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;头文件中没有 setter
void FMyStruct::Serialize(FArchive&)bool Serialize(FArchive&) + TStructOpsTypeTraits,其中 WithSerializer = true守卫位于 UObject/Class.h:1307,特征位于 UObject/StructOpsTypeTraits.h:24
COMPRESS_ZLIBNAME_ZlibMisc/CompressionFlags.h:18 中标记为 COMPRESS_ZLIB_DEPRECATED
FCompression::GetCompressionFormatFromDeprecatedFlags(Flags)直接传入 NAME_Zlib / NAME_OodleMisc/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()、IOnlineTitleFileInterface通过 IPlatformFeaturesModule::Get().GetSaveGameSystem() 使用 ISaveGameSystem这些名称均未出现在任何 5.8 引擎头文件中

常见错误

以为 UPROPERTY(SaveGame) 会筛选 SaveGameToSlot: 并非如此——SaveGameToSlot 会写入所有非临时属性(GameplayStatics.h:1159)。使用 Transient 排除字段;只有对于 ArIsSaveGame = true 的归档,SaveGame 才有作用。

在没有代理归档的情况下序列化 Actor: 裸用 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 Actor 状态
  • ue-async-threading — 在游戏线程之外执行序列化或压缩
  • ue-networking-replication — 服务器权威存档与客户端本地配置文件

评论

支持 Markdown