序列化与存档

自动关联目录:序列化与存档

序列化在 UE 里是同一套机制服务三个场景:存到磁盘(.uasset)、运行时存档(SaveGame)、跨网络复制。三者都依赖 反射 提供的属性描述。

一句话定位:序列化是按"与 CDO 的差异"来存的——所以你改了默认值,旧的存档和资产会跟着变,这是很多"诡异 bug"的根源。

三个场景的区别

场景载体序列化什么关键类
资产.uasset / .umapUObject 的属性差异FPackageSaveContext、FLinkerSave
存档.savUSaveGame 子树FObjectAndNameAsStringProxyArchive
网络复制网络包标了 Replicated 的属性FNetBitWriter(见 Replication)

本页只讲前两个;网络复制在 GamePlay / Replication。

FArchive:统一读写接口

FArchive 是读写的抽象:同一个 Serialize 函数,传入写归档就是存,传入读归档就是读。

void UMySave::Serialize(FArchive& Ar) override
{
    Super::Serialize(Ar);
    Ar << CustomInt;
    Ar << CustomStr;
    // 自定义结构要自己展开
}
Archive用途
FMemoryWriter / FMemoryReader内存 ↔ 字节流
FFileHelper::SaveArrayToFile落盘
FObjectAndNameAsStringProxyArchiveSaveGame 标准用法
FMemoryArchive基类,自定义归档从这里派生

版本兼容:CustomVersion

存档一定会遇到"旧版本数据,新版本代码"。UE 的做法是给每段结构注册一个 GUID 版本号。

// 1. 定义版本 GUID(每个项目/系统一个,不要复用)
static const FGuid FMySaveVersionGuid(0x12345678, 0x1234, 0x1234, 0x12, 0x34, 0x56, 0x78, 0x9A);
FCustomVersionRegistration GRegisterMySaveVersion(FMySaveVersionGuid, 3, TEXT("MySaveVer"));

// 2. 写:存当前版本
Ar.SetCustomVersion(FMySaveVersionGuid, 3, TEXT("MySaveVer"));

// 3. 读:按版本分支处理
const int32 Ver = Ar.CustomVer(FMySaveVersionGuid);
if (Ver < 2) { /* 老数据迁移 */ }
if (Ver >= 3) { Ar << NewField; }
规则说明
GUID 一旦发布不能改改了等于换了套版本体系
版本号只增不减减了旧存档无法判断
新增字段要有默认值老存档读不到,靠 CDO/默认值兜底
删除字段不要立刻删代码留一版迁移逻辑再清理

最常见的存档事故:直接改了 UPROPERTY 的类型(比如 int32 → FString),旧存档读进来直接崩。正确做法是加新字段 + 迁移,而不是改类型。

SaveGame 实操

UCLASS()
class UMySaveGame : public USaveGame
{
    GENERATED_BODY()
public:
    UPROPERTY(SaveGame) int32 Level;
    UPROPERTY(SaveGame) FString PlayerName;
    UPROPERTY(SaveGame) TArray<int32> Inventory;
};

// 存
UMySaveGame* Save = CastChecked<UMySaveGame>(UGameplayStatics::CreateSaveGameObject(UMySaveGame::StaticClass()));
Save->Level = 10;
UGameplayStatics::SaveGameToSlot(Save, TEXT("Slot1"), 0);

// 读
if (USaveGame* Loaded = UGameplayStatics::LoadGameFromSlot(TEXT("Slot1"), 0))
{
    UMySaveGame* Data = Cast<UMySaveGame>(Loaded);
}

只有标了 SaveGame 的 UPROPERTY 才会被写入。漏标的表现是"存档成功但读回来是默认值",非常难查。

什么能序列化,什么不能

能不能
UPROPERTY 标记的标量、FString、FName裸指针(不标 UPROPERTY)
UPROPERTY 的 TArray/TMap/TSet非 USTRUCT 的自定义类(除非自己实现 Serialize)
USTRUCT 且成员都是可序列化类型委托(Delegate)、TFunction
TSubclassOf、FSoftObjectPath运行时状态(线程、Socket、句柄)

需要存"对象的引用"时,存路径而不是指针:

UPROPERTY(SaveGame)
FSoftObjectPath ItemAssetPath;    // 存路径,读取时再异步加载

直接序列化 UObject* 指针,在不同机器上地址全无意义。

与资产序列化的关系

.uasset 的序列化用的是同一套属性遍历,区别在:

  • 资产存的是"与 CDO 的差异",所以改默认值会影响所有已存资产
  • 资产支持拆包(Package)、引用其它包(ImportMap/ExportMap)
  • 资产的加载与打包流程见 Asset&Pak&Patch

改 UPROPERTY 默认值后一定要重新保存相关资产,否则编辑器里看到的是新的、磁盘里是旧的,行为不一致。

性能

手段说明
减少 UPROPERTY 字段序列化是逐属性遍历,字段越多越慢
大数组用自定义 Serialize跳过逐个属性的元数据开销
异步写盘SaveGame 走后台线程,别卡主线程
控制存档频率每次序列化都是一次全量遍历
Transient 标记明确不需要序列化的字段省掉开销
// 大块数据自定义序列化,比逐属性快很多
Ar.Serialize(RawData.GetData(), RawData.Num() * sizeof(int32));

常见坑

坑说明
漏标 SaveGame存档成功但读回默认值
直接改字段类型旧存档读取崩溃
存了裸指针 / 委托读回来是野指针
没用 CustomVersion无法迁移,只能清档
在 Serialize 里调用会触发 GC 的 API归档持有对象期间 GC 是危险的
把运行时状态写进存档读回来是一堆失效句柄
依赖 UPROPERTY 的声明顺序序列化按名字走,但迁移时按名字取字段,改名会断
存档写盘不校验崩溃在写盘中途会产生截断的坏档,要做原子替换(先写临时文件再 rename)