总览
前五篇已经把资源、数据、运行时组件和 UI 预览串起来。本篇处理上线时一定会遇到的另一个问题:外观如何保存,角色如何初始化,旧存档如何迁移,资源缺失时如何回退。
生产项目里不要保存 Mutable 生成结果,也不要保存 CustomizableObjectInstance。存档和服务器资料应该保存稳定的外观描述:角色基准、Slot 里的 ItemID、颜色覆盖、版本号和必要的自定义参数。进入游戏时,再用同一套 ApplyOutfitSnapshot 恢复表现。
本篇目标
- 定义可保存的外观快照结构,不依赖 Mutable 内部对象。
- 建立默认外观体系,保证每个角色都能有安全回退。
- 设计初始化流程:新角色、读档、服务器资料、预览取消都复用同一入口。
- 设计存档版本迁移,处理装备改名、下架、Slot 变更和参数迁移。
- 明确坏档、缺资源、加载失败、Mutable 失败时的回退策略。
非目标
- 本篇不实现背包和玩家拥有权。
- 本篇不讨论云存档、账号合并和跨平台同步策略。
- 本篇不保存生成后的 Mesh、材质实例或贴图。
- 本篇不讲多人复制,网络权威在第八篇展开。
保存原则
外观保存应该满足三个条件:
- **稳定**:Mutable 图重构后,旧存档仍能迁移。
- **轻量**:只保存 ID 和少量参数,不保存资源对象。
- **可回退**:任意一件装备缺失,系统都能恢复到默认外观。
不要保存:
CustomizableObjectInstance
Generated SkeletalMesh
MaterialInstanceDynamic
Texture Render Target
Mutable internal option index
编辑器资产路径作为唯一身份
应该保存:
CharacterArchetypeId
BodyTypeId
Slot -> ItemID
Color overrides
Projector / decal overrides
SchemaVersion
ContentVersion
存档数据结构
USTRUCT(BlueprintType)
struct FOutfitSlotSaveData
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName SlotId;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName ItemId;
};
USTRUCT(BlueprintType)
struct FOutfitColorSaveData
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName ParamName;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FLinearColor Value;
};
USTRUCT(BlueprintType)
struct FOutfitSnapshotSaveData
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 SchemaVersion = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 ContentVersion = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName CharacterArchetypeId;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName BodyTypeId;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FOutfitSlotSaveData> Slots;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FOutfitColorSaveData> ColorOverrides;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Revision = 0;
};
为什么保存数组而不是直接保存 TMap,取决于项目序列化习惯。蓝图和跨平台工具链里,数组更容易做调试、导出和迁移;运行时可以再转换成 TMap。
默认外观配置
默认外观不是占位,而是系统安全网。建议建立 PDA_DefaultOutfit:
| 字段 | 说明 |
|---|---|
CharacterArchetypeId |
角色基准 ID |
BodyTypeId |
默认体型 |
DefaultItemsBySlot |
每个 Slot 的默认 ItemID |
DefaultColors |
默认颜色 |
FallbackSnapshot |
全局回退快照 |
MinimumRequiredSlots |
必须存在的 Slot,例如身体、上衣、下装 |
DefaultContentVersion |
默认内容版本 |
每个 Slot 都应该有默认装备:
| Slot | 默认 ItemID |
|---|---|
Upper |
eq_upper_basic_shirt |
Lower |
eq_lower_basic_pants |
Shoes |
eq_shoes_basic |
Head |
eq_head_none |
Hair |
eq_hair_default |
Face |
eq_face_none |
Back |
eq_back_none |
eq_head_none 这种“没有外观”的装备也要是数据资产。它负责把 Slot 写回 None,并恢复相关裁剪和显示参数。
初始化流程
角色出生时不要从 UI 或 Mutable 参数反推状态。初始化应从保存快照或默认配置开始:
Character BeginPlay
-> Get Appearance Profile from SaveGame or Server
-> If profile exists:
Migrate SaveData to current SchemaVersion
Normalize SaveData
Convert SaveData to FOutfitSnapshot
Else:
Build Default Snapshot from PDA_DefaultOutfit
-> BP_OutfitComponent.InitializeOutfitComponent(Snapshot)
-> ApplyOutfitSnapshot(Snapshot, bPreview=false)
-> On Success: mark appearance ready
-> On Fail: ApplyFallbackOutfit
初始化也要走 ApplyOutfitSnapshot,不要写一套“出生时专用 Mutable 参数设置”。同一入口才能保证默认值、裁剪、颜色和失败回退一致。
读档标准化
读出的存档不能直接应用,需要先标准化:
- 检查
SchemaVersion。 - 按迁移表更新旧
ItemID。 - 移除未知 Slot。
- 为缺失 Slot 补默认装备。
- 检查每个装备是否存在且启用。
- 检查装备是否适配当前角色标签。
- 检查颜色参数范围。
- 生成新的
Revision。
标准化函数建议:
| 函数 | 说明 |
|---|---|
MigrateOutfitSaveData |
按 SchemaVersion 逐步迁移 |
NormalizeOutfitSaveData |
补默认 Slot、排序、移除非法值 |
ConvertSaveDataToSnapshot |
转成运行时 FOutfitSnapshot |
ConvertSnapshotToSaveData |
正式外观成功后转成存档 |
ValidateSaveDataAgainstCatalog |
检查 ItemID、Slot、兼容性 |
BuildDefaultSnapshot |
从默认配置生成完整快照 |
版本迁移
旧存档一定会出现。常见迁移包括:
| 变化 | 迁移方式 |
|---|---|
| 装备改名 | eq_upper_jacket_old -> eq_upper_jacket_a |
| 装备下架 | 替换为 FallbackItemId 或 Slot 默认装备 |
| Slot 拆分 | Body -> Upper + Lower,按默认规则补齐 |
| Slot 合并 | 多个旧 Slot 合并为新 Slot,保留优先级最高的装备 |
| 颜色参数改名 | Color_Main -> Color_Primary |
| 体型 ID 改名 | 迁移到新 BodyTypeId |
| 内容包拆分 | 缺 DLC 时替换为基础包默认装备 |
迁移表可以是 DataTable 或 DataAsset:
USTRUCT(BlueprintType)
struct FOutfitMigrationRule
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly)
int32 FromSchemaVersion;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
int32 ToSchemaVersion;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FName OldItemId;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FName NewItemId;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FName OldParamName;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FName NewParamName;
};
迁移应该逐版本执行,而不是只写“当前版本兼容旧版本”的大函数。逐版本迁移更容易测试,也更容易回滚。
保存时机
外观保存要跟正式状态绑定:
| 行为 | 是否保存 | 原因 |
|---|---|---|
| 打开衣柜 | 否 | 只是进入会话 |
| 点击预览 | 否 | 临时外观 |
| 预览成功 | 否 | 还未确认 |
| 确认点击 | 不能立即保存 | Mutable 可能失败 |
| 正式应用成功 | 是 | CurrentOutfit 已更新 |
| 正式应用失败 | 否 | 保留旧存档 |
| 取消预览 | 否 | 回到旧状态 |
| 服务器拒绝 | 否 | 以服务器状态为准 |
保存触发点应该是 OnOutfitApplied(RequestId, Snapshot, bPreview=false)。这能避免 UI 点击确认后,实际外观失败但存档已经写入。
存档蓝图接口
建议提供一个 BPL_OutfitSave 或 UOutfitSaveSubsystem:
| 函数 | 说明 |
|---|---|
LoadOutfitSaveData(PlayerId) |
读取外观存档 |
SaveOutfitSnapshot(PlayerId, Snapshot) |
保存正式外观 |
BuildDefaultSaveData(CharacterArchetypeId) |
生成默认存档 |
MigrateOutfitSaveData(SaveData) |
迁移旧版本 |
NormalizeOutfitSaveData(SaveData) |
补默认值并排序 |
ValidateOutfitSaveData(SaveData) |
校验存档合法性 |
GetOutfitSaveDebugString(SaveData) |
输出调试字符串 |
角色组件不必知道存档文件路径。它只负责产出和应用 Snapshot。
坏档和缺资源处理
| 问题 | 处理 |
|---|---|
| 没有存档 | 使用默认外观 |
| 存档 JSON / Slot 数据损坏 | 丢弃坏字段,补默认;严重时整套默认 |
SchemaVersion 过旧 |
按迁移链执行 |
ItemID 不存在 |
查迁移表,失败用 Slot 默认装备 |
| 默认装备不存在 | 使用全局默认快照并记录严重错误 |
| 装备资源加载失败 | 保留旧外观或替换 fallback |
| Mutable 更新失败 | 不写新存档,回滚上一次稳定外观 |
| 保存写入失败 | UI 提示未保存,允许重试 |
| DLC 未安装 | 替换为基础包外观 |
坏档处理要“可继续游戏”。角色外观不应该因为一件旧装备缺失导致角色无法进入关卡。
初始化验收流程
建议做一套自动化或半自动化 Smoke Test:
Case 1: 无存档 -> 默认外观 -> 成功
Case 2: 正常存档 -> 恢复外观 -> 成功
Case 3: 存档缺 Upper -> 补默认上衣 -> 成功
Case 4: ItemID 改名 -> 迁移 -> 成功
Case 5: 装备下架 -> fallback -> 成功
Case 6: 资源缺失 -> 回退并记录错误 -> 成功进入游戏
Case 7: Mutable 失败 -> 不覆盖存档 -> 回滚
Case 8: 保存失败 -> UI 提示,旧存档保留
验收标准
- 存档中不包含生成 Mesh、材质实例、Mutable Instance 或临时 UI 状态。
- 新角色无存档时能生成完整默认外观。
- 每个 Slot 都有默认装备,包括“无帽子”这类空显示装备。
- 读档恢复、UI 确认、取消恢复、网络同步都能复用
ApplyOutfitSnapshot。 - 旧
ItemID可以通过迁移表转换,转换失败能 fallback。 - 缺失 Slot 会被默认装备补齐。
- 正式外观成功应用后才保存。
- 预览状态不会写入存档。
- Mutable 更新失败不会覆盖旧存档。
- 坏档不会阻止玩家进入游戏,系统能回退默认外观并记录错误。
- 日志能输出
PlayerId / SchemaVersion / ItemId / Slot / ErrorCode / Fallback。
本篇结论
保存系统保存的不是 Mutable 结果,而是一份稳定、可迁移、可回退的外观描述。只要默认外观、版本迁移和初始化入口设计清楚,后续 UI、服务器和热更新都可以围绕同一份 Snapshot 工作。下一篇进入性能和打包:如何异步加载、合并请求、控制 Mutable 生成预算,并保证打包后资源不丢。