其他
在 Unreal Engine…
适用于 UE 存档系统、玩家进度持久化与数据序列化的实现与迁移。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
在 Unreal Engine 中实现存通用
解决思路
- 先阅读 `.agents/ue-project-context.md` 获取引擎版本(UE 5.0+ 才有 `ULocalPlayerSaveGame`)、模块名、目标平台、是否多人;文件不存在则让用户先运行 `/ue-project-context`。
- 写代码前先确认需求:存档复杂度(简单键值 vs 复杂世界状态)、数据类型(基础类型/嵌套结构/软硬资源引用)、是否需要版本迁移、存档槽数量与是否按用户区分、是否必须异步。
- 继承 `USaveGame`(来自 `GameFramework/SaveGame.h`),用 `UPROPERTY(SaveGame)` 标记字段以启用自动序列化;所有被保存的 `USTRUCT` 必须加 `GENERATED_BODY()`,否则会静默序列化失败。
- 存档中始终包含 `int32 SaveVersion` 字段;资源引用用 `FSoftObjectPath`,绝不要保存裸 `UObject*`、`AActor*` 或硬 `TObjectPtr<>`。
- 保存流程:先用 `UGameplayStatics::CreateSaveGameObject` 创建对象并填充字段,再调用 `SaveGameToSlot`(同步,阻塞游戏线程)或优先使用 `AsyncSaveGameToSlot`(异步,带 `FAsyncSaveGameToSlotDelegate` 回调)。
- 读取流程:先用 `DoesSaveGameExist` 判断,再用 `LoadGameFromSlot` 或 `AsyncLoadGameFromSlot`(带 `FAsyncLoadGameFromSlotDelegate`);删除用 `DeleteGameInSlot`。
- PC 上 UserIndex 固定为 0,主机平台用 `GetPlatformUserIndex()`。
- UE 5.0+ 可用 `ULocalPlayerSaveGame`:重写 `GetLatestDataVersion()` 返回最新版本号,重写 `HandlePostLoad()` 做迁移(用 `GetSavedDataVersion()` 判断旧版本);用 `LoadOrCreateSaveGameForLocalPlayer` / `AsyncLoadOrCreateSaveGameForLocalPlayer` 加载或创建,用 `AsyncSaveGameToSlotForLocalPlayer`(优先)或 `SaveGameToSlotForLocalPlayer` 保存。
- 使用 `FArchive` 自定义序列化:用 `Ar.IsLoading()` / `Ar.IsSaving()` 判断方向,`Ar.IsError()` 检查读写失败(每次读写后都要检查,出错立即中止),`Ar.Tell()` 获取位置,`Ar.CustomVer(Key)` 获取版本号。
- 用 `FMemoryWriter` / `FMemoryReader` 在 `TArray<uint8>` 上序列化:构造时传 `bIsPersistent=true`,先写入版本头(如 `Writer << Version`),再写数据,最后用 `checkf(!Writer.IsError(), ...)` 校验;读取时先读版本号,若版本非法或 `Reader.IsError()` 则视为损坏数据。
- 用 `FBufferArchive`(来自 `Serialization/BufferArchive.h`)合并 writer 与缓冲区:它本身就是 `TArray<uint8>`,可写入 magic number(如 `0x53415645`)与结构体,最后用 `MoveTemp(Buffer)` 取出字节数组。
- 为自定义结构体定义 `FArchive& operator<<(FArchive& Ar, FMyCustomData& Data)`,使其可被任意 `FArchive`(含 `FBufferArchive`、`FMemoryWriter`)序列化。
- 大存档用 `FArchiveSaveCompressedProxy` / `FArchiveLoadCompressedProxy`(来自 `Serialization/ArchiveSaveCompressedProxy.h`)配合 `NAME_Zlib` 压缩:压缩后调用 `Comp.Flush()`;解压时先 `Raw.SetNum(KnownUncompressedSize)` 再 `Decomp.Serialize(...)`。
- 需要精确二进制布局时重写 `UObject::Serialize(FArchive& Ar)`:先调用 `Super::Serialize(Ar)`,再写字段,读取时检查 `Ar.IsLoading() && Ar.IsError()` 处理损坏。
- 整数版本迁移:定义 `ESaveVersion` 枚举(含 `VersionPlusOne` 与 `Latest = VersionPlusOne - 1`),在 `RunMigrations` 中按版本号逐级迁移,最后把 `SaveVersion` 更新为 `Latest`。
- 基于 FArchive 的版本迁移:定义版本枚举与 `FGuid`(用 `FGuid::NewGuid()` 生成一次后硬编码),用 `FCustomVersionRegistration` 全局注册;在 `Serialize()` 中调用 `Ar.UsingCustomVersion(GUID)` 并用 `Ar.CustomVer(GUID)` 判断版本,旧版本缺失的数据要 `Reset()` 初始化。
- 结构体字段重命名/类型变更时,在结构体的 `Serialize()` 中按 `Ar.CustomVer(...)` 分支读取旧字段并迁移到新字段。
- 用户偏好设置继承 `UGameUserSettings`,字段用 `UPROPERTY(Config, BlueprintReadWrite)`,在 `DefaultEngine.ini` 的 `[/Script/Engine.Engine]` 下设置 `GameUserSettingsClassName`;保存用 `ApplySettings(false)` + `SaveSettings()`。
- 项目设置继承 `UDeveloperSettings`(`UCLASS(Config=Game, DefaultConfig, meta=(DisplayName=...))`),用 `GetDefault<UMyProjectSettings>()` 访问;Build.cs 需添加 `DeveloperSettings` 模块依赖。
- 直接访问配置用 `GConfig->GetString/SetString`(来自 `Misc/ConfigCacheIni.h`),写入后调用 `GConfig->Flush(false, GGameIni)`;对象级用 `SaveConfig()` / `LoadConfig()`。INI 段名 `[/Script/ModuleName.ClassName]` 映射到 CDO,自定义段名需重写 `OverrideConfigSection(FString& SectionName)`。
- 云存档:通过 `IPlatformFeaturesModule::Get().GetSaveGameSystem()` 获取 `ISaveGameSystem`,用 `DoesSaveSystemSupportMultipleUsers()` 判断;跨平台模式是先序列化到 `TArray<uint8>` 再通过平台 SDK 上传;Steam 可用 `ISteamRemoteStorage` 的 `IsCloudEnabledForApp()`、`FileWrite`、`FileRead`。
- 存档加密用 `FAES`(来自 `Misc/AES.h`):构造 32 字节 `FAESKey` 时用 `FMemory::Memzero` 清零后 `FMemory::Memcpy` 拷贝 UTF-8 字节(不要用 `Key.Left(32)`,短字符串会静默截断导致加解密损坏);加密前用 `Align(Data.Num(), FAES::AESBlockSize)` 补零,再调用 `FAES::EncryptData` / `FAES::DecryptData`。
- 避免常见错误:不保存裸指针、必须有版本字段、不要每帧同步存档(用异步)、所有保存结构体加 `GENERATED_BODY()`、每次读写后检查 `Ar.IsError()`、用 `bSaveInProgress` 标志防止异步存档重叠、不要硬编码存档路径(用 `UGameplayStatics` API,`FPaths::ProjectSavedDir()` 仅用于调试)。
- 损坏恢复:读取失败或 magic/版本校验失败时丢弃损坏数据并回退到新存档;可选维护备份槽(覆盖 `Slot_Primary` 前先写 `Slot_Backup`),加载时依次尝试主槽、备份槽、新建。
- 大存档分块:按子系统拆分到多个槽(如 `Save_World_00`、`Save_Inventory`、`Save_Quests`),用 `AsyncLoadGameFromSlot` 并行加载,避免单文件瓶颈。
- 多人存档归属:共享世界状态(任务、经济、敌人状态)由服务器权威存档写入(服务器 `AGameMode` 写,客户端通过 RPC 发送变更,绝不直接写共享存档);玩家个人偏好(按键、UI 布局)用 `ULocalPlayerSaveGame` 保存在客户端本地。
- Build.cs 依赖:`PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" })`;使用 `UDeveloperSettings` 时再加 `PublicDependencyModuleNames.Add("DeveloperSettings")`。
适用边界 · 注意事项
- 项目未提供 `.agents/ue-project-context.md` 且无法确认引擎版本、模块名、目标平台与是否多人时,不应直接给出实现建议。
- 不需要跨版本兼容、无未来补丁计划的一次性原型,可省略复杂的版本迁移与自定义版本注册机制。
- 非竞争性或非经济敏感的单机游戏,可不做 AES 加密(加密也无法防住有决心的玩家,权威存档仍需服务端校验)。