Wiki
978 words
5 minutes
Next
Updated 2025-11-27
使用前置声明的意义
为了不编入头文件,加快编译速度
UPROPERTY
存档相关字段如果需要被系统识别,通常会显式配上 SaveGame 说明符。
例如:
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame)int32 PlayerLevel = 1;这能让数据边界更清晰,也方便后面排查“为什么这个字段没有被保存”。
USaveGame
想要进行存档的话。需要创建一个新的类继承于该类。
Unity存档方式
存档方式是使用PlayerPrefs,用KV值来进行存档
Unreal存档方式
整个继承自 USaveGame 的类进行存档。
UGamePlayStatics
调用SaveGame并选择槽位进行保存
常用入口主要是:
CreateSaveGameObjectSaveGameToSlotLoadGameFromSlotDoesSaveGameExistDeleteGameInSlot
切换关卡
UGameplayStatics::OpenLevel(this, FName("MapName"))
UUserWidget
存档 UI 一般会交给 UUserWidget 处理,例如:
- 存档列表
- 覆盖存档确认框
- 保存中提示
- 读取失败提示
Next
存档系统要解决什么问题
真正的存档系统,不只是“把一个对象写进磁盘”,而是要明确:
- 什么数据该存
- 什么数据不该存
- 什么时候存
- 切关时数据怎么流转
如果这四件事不提前设计,后面就会变成哪里需要就往 SaveGame 里硬塞。
数据边界
我更倾向于把数据分成三层:
- 全局持久数据:玩家档案、设置、已解锁内容
- 当前流程数据:当前关卡进度、任务状态、临时资源
- 纯运行时数据:缓存、UI 状态、临时引用,不应该进存档
只有前两类才值得考虑写入 USaveGame。
USaveGame 的职责
USaveGame 适合充当“磁盘落地格式”,而不是整个存档系统的入口。
它负责:
- 承载可序列化的数据
- 作为
UGameplayStatics::SaveGameToSlot的对象 - 在加载时恢复成内存中的基础数据
建议把真正的“存档流程控制”交给更高一层的管理器,例如 GameInstanceSubsystem 或 GameInstance。
槽位设计
槽位至少要想清楚三件事:
- 存档名怎么命名
- 一个用户是否允许多个档位
- 是否区分自动存档和手动存档
常见做法:
Profile_01Profile_01_AutoProfile_01_Checkpoint
如果后期要做 UI 列表,还要同步保存:
- 关卡名
- 存档时间
- 玩家等级或核心摘要信息
保存流程
一个比较稳的保存流程通常是:
- 从各系统收集可持久化数据
- 写入自定义
USaveGame - 调用
UGameplayStatics::SaveGameToSlot - 成功后更新 UI 或存档元数据
如果项目里数据较多,建议用异步保存,避免在切关或战斗中卡顿。
异步保存
异步保存的价值主要在于减少主线程卡顿,尤其是:
- 存档体积变大时
- 要保存大量状态时
- 平台 IO 较慢时
设计时要注意:
- 保存期间 UI 给出反馈
- 不要让同一槽位被并发覆盖
- 失败时要有重试或提示逻辑
跨关卡数据流
切关时最容易混淆的,是“当前关卡状态”和“全局持久状态”的边界。
一个常见链路是:
- 关卡内系统把状态汇总到管理器
- 管理器写入
USaveGame - 执行
OpenLevel - 新关卡加载后,从存档或
GameInstance恢复状态
所以 USaveGame 是磁盘层,GameInstance / Subsystem 更像运行时桥接层。
Blueprint 和 C++ 的分工
比较合理的分工通常是:
- C++:定义数据结构、存档读写入口、版本兼容处理
- Blueprint:存档按钮、UI 反馈、简单展示逻辑
如果把整个存档系统都堆在蓝图里,后面做版本升级和字段迁移会很痛苦。
经验
- 不要直接把 Actor 引用原样存进去,通常要转成 ID、名称或结构化快照
- 字段要明确哪些带
SaveGame - 存档对象是“数据镜像”,不是运行时世界本体
- 越早定义边界,后面越不容易炸