关于 ue-serialization-savegames
面向 UE 5.8 开发者的序列化与存档技能,介绍保存游戏状态、设置及平台存储的 API 和模式。涵盖存档槽、异步读写、二进制序列化、版本迁移与 Actor 状态快照。
主要功能
- 01USaveGame 槽位读写
- 02异步保存与加载
- 03FArchive 二进制序列化
- 04Actor 状态快照与版本迁移
- 05配置、设置与平台存储
适用场景
- 01保存和恢复玩家游戏进度
- 02为本地玩家管理独立存档
- 03跨版本迁移存档数据
- 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 自定义序列化、版本管理 |
| 控制台/云存储、存档槽枚举、存档 UI | ISaveGameSystem 和平台存储 |
项目设置、用户选项、.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):
| 调用 | 签名 |
|---|---|
CreateSaveGameObject | USaveGame* (TSubclassOf<USaveGame> SaveGameClass) (:1124) |
SaveGameToSlot | bool (USaveGame*, const FString& SlotName, const int32 UserIndex) (:1167) |
LoadGameFromSlot | USaveGame* (const FString& SlotName, const int32 UserIndex) (:1211) |
DoesSaveGameExist | bool (const FString& SlotName, const int32 UserIndex) (:1175) |
DeleteGameInSlot | bool (const FString& SlotName, const int32 UserIndex) (:1231) |
SaveGameToMemory | bool (USaveGame*, TArray<uint8>& OutSaveData) (:1134) |
LoadGameFromMemory | USaveGame* (const TArray<uint8>& InSaveData) (:1182) |
SaveDataToSlot | bool (const TArray<uint8>&, const FString& SlotName, const int32 UserIndex) (:1143) |
LoadDataFromSlot | bool (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、DefaultConfig | UDeveloperSettings + 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 / LoadFileToArray | JsonObjectConverter.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_ZLIB | NAME_Zlib | Misc/CompressionFlags.h:18 中标记为 COMPRESS_ZLIB_DEPRECATED |
FCompression::GetCompressionFormatFromDeprecatedFlags(Flags) | 直接传入 NAME_Zlib / NAME_Oodle | Misc/Compression.h:173 中标记为 UE_DEPRECATED(5.5) |
UGameUserSettings::WindowPosX / WindowPosY | WindowPositions 数组 | 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— 服务器权威存档与客户端本地配置文件