一、问题场景:为什么你的存档系统总在迭代中崩溃?
项目中期加了一个新武器皮肤,玩家读档后背包全空;版本更新后旧存档直接报错崩溃;联机模式下主机存档覆盖了所有人的进度……这些血泪场景,几乎每个UE5团队都遇过。根因都指向同一个技术核心:SaveGame对象的序列化机制。UE5默认的 USaveGame + UGameplayStatics::SaveGameToSlot 看似开箱即用,但它只解决了“能存”,远没解决“存得好、读得稳、跨版本兼容”。本次评测,我拿最主流的原生USaveGame序列化作为基准方案,对比自定义二进制序列化与JSON结构化存档两种替代方案,从原理、实现到性能、扩展性做一次彻底横评。

二、方案A:原生USaveGame序列化——官方快车道
这是UE5最标准的做法:定义一个继承自 USaveGame 的UObject,用 UPROPERTY(SaveGame) 标记要持久化的字段,然后交给引擎自动完成序列化。
UCLASS()
class UMySaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY(SaveGame)
FString PlayerName;
UPROPERTY(SaveGame)
int32 Level = 1;
UPROPERTY(SaveGame)
TArray<FItemData> Inventory;
};
// 存档
UGameplayStatics::SaveGameToSlot(SaveObj, TEXT("Slot1"), 0);
// 读档
USaveGame* Loaded = UGameplayStatics::LoadGameFromSlot(TEXT("Slot1"), 0);
底层原理:引擎通过反射系统(Reflection System)扫描被 SaveGame 标记的 UPROPERTY,调用 FMemoryWriter 按属性类型写入二进制流,最终由 SaveGameToSlot 压缩后落盘为 .sav 文件。关键点在于:它依赖 FArchive 的 Serialize 接口逐属性写入,属于二进制非自描述序列化——文件中只有值,没有字段名和类型信息。
优点:零手写序列化代码,自动处理UObject引用、TArray、TMap等复杂类型;性能极高,读写速度接近裸内存拷贝。致命缺陷:一旦字段增删改类型,旧存档的二进制布局就与新代码不匹配,轻则数据错位,重则崩溃。官方没有内建版本迁移机制,FArchive 的版本号(CustomVersion)需要自己维护。
技巧:务必在SaveGame类中重写
Serialize函数并配合FCustomVersionRegistry做版本分支,否则每次改结构都是一场灾难。这是原生方案从“能用”到“能维护”的分水岭。
三、方案B:自定义二进制序列化——完全可控的硬核路线
抛弃 USaveGame 的自动反射,自己定义 FArchive 的读写逻辑,实现一个纯C++的存档结构体。典型做法是继承 FArchive 或者直接使用 FMemoryWriter/FMemoryReader 配合自定义的 Serialize 函数。
struct FMySaveData
{
int32 Version = 2;
FString PlayerName;
TArray<FItemData> Inventory;
void Serialize(FArchive& Ar)
{
Ar << Version;
Ar << PlayerName;
int32 Count = Inventory.Num();
Ar << Count;
if (Ar.IsLoading()) Inventory.SetNum(Count);
for (auto& Item : Inventory) Item.Serialize(Ar);
}
};
// 写入
FBufferArchive Buffer;
Data.Serialize(Buffer);
FFileHelper::SaveArrayToFile(Buffer, *Path);
底层原理:operator<< 最终调用 FArchive::Serialize 的字节流写入,由你显式控制每个字段的顺序和类型。因为是自己写序列化逻辑,可以在 Serialize 开头读取 Version,根据版本号走不同的分支读取逻辑,实现向前兼容的版本迁移。这是它相对原生方案最大的杀手锏。
优点:完全掌控数据布局,版本迁移灵活,性能与原生方案同级(都是二进制流),适合对存档体积和速度极度敏感的项目(如移动端、主机)。缺点:所有类型都要手写序列化,UObject引用需要手动转成路径字符串再还原,开发成本高,易出错。团队越大,维护成本越高。

四、方案C:JSON结构化存档——可读性与跨平台的代价
使用 FJsonObjectConverter 将UStruct直接转成JSON字符串,配合 FFileHelper::SaveStringToFile 落盘。UE5内置了 Json 和 JsonUtilities 模块,无需第三方库。
FString JsonStr;
FJsonObjectConverter::UStructToJsonObjectString(
FMySaveData::StaticStruct(), &Data, JsonStr);
FFileHelper::SaveStringToFile(JsonStr, *Path);
// 读档
FMySaveData Data;
FJsonObjectConverter::JsonObjectStringToUStruct(
JsonStr, &Data, 0, 0);
底层原理:FJsonObjectConverter 遍历UStruct的反射属性,将每个字段按JSON键值对输出,本质是自描述文本序列化。字段名和值一起存储,所以增删字段时,旧文件里多余的键会被忽略,缺失的键用默认值填充——天然具备一定的容错能力。
优点:存档文件人类可读,调试和热更极为方便;跨版本、跨语言解析容易;策划可以直接改数值测试。缺点:文本体积比二进制大3-5倍,解析速度慢一个数量级;JSON不支持二进制大对象和精确浮点,复杂嵌套结构转换有坑;UObject引用需要手动处理成软引用路径。
注意:JSON方案下浮点数精度可能丢失,对于需要精确复现的战斗回放类存档,慎用。
五、横向对比表格
| 对比维度 | 原生USaveGame | 自定义二进制 | JSON结构化 |
|---|---|---|---|
| 实现成本 | 极低 | 高 | 低 |
| 读写性能 | 极高 | 极高 | 中低 |
| 存档体积 | 小 | 最小 | 大(3-5倍) |
| 版本迁移能力 | 弱(需手动) | 强(可控) | 中(字段容错) |
| 可读性/调试 | 差 | 差 | 优 |
| 复杂类型支持 | 优(反射) | 需手写 | 中(有坑) |
| 适用场景 | 中小项目快速落地 | 移动/主机/竞技 | 工具链/热更/调试 |
六、选型建议:别盲目追“高级”,看阶段和团队
我的判断很明确:原型期和中小项目直接用原生USaveGame,但必须从第一天就加版本号。它省下的开发时间远超后期迁移成本,前提是你用 CustomVersion 做好了版本分支。
如果项目是移动端、主机或竞技类,存档体积和读写速度是硬指标,且团队有资深C++工程师,自定义二进制是唯一正解。它把控制权完全交给你,代价是每加一个字段都要动序列化代码。
JSON方案我推荐给需要频繁热更、跨平台同步或策划直接调参的项目,或者作为开发期的调试格式——正式发布时再转成二进制。它的自描述特性在版本兼容上的优势,是前两者不具备的。
最后说个实战组合拳:用JSON做开发期存档格式,发布时切换为自定义二进制,通过 ISaveSerializer 接口抽象,两套实现随时切换。这是我在多个上线项目中验证过的最优解。

关于火星人教育:创办24年来,火星人教育始终专注数字艺术与AIGC实战职业技能培训,开设AI漫剧创作、AIGC视觉设计、UE5游戏特效、室内全屋定制、MAYA三维动画、C4D动态设计、工业设计、NUKE影视后期、摄影摄像实训等课程,北京、深圳双校区开设线下面授班,面向美术生、在校大学生及意向转行的职场人群,以项目化实训教学,夯实专业技能,助力就业与职业提升。如果你也想深入UE5底层机制与存档架构,欢迎致电010-65918670咨询课程详情。








评论(0)