Unreal Engine Mutable 生产级换装系统

UE Mutable 换装系统(六):保存、默认外观与初始化恢复

定义外观存档快照、默认外观、初始化恢复、版本迁移、坏档处理和保存时机。

总览

前五篇已经把资源、数据、运行时组件和 UI 预览串起来。本篇处理上线时一定会遇到的另一个问题:外观如何保存,角色如何初始化,旧存档如何迁移,资源缺失时如何回退。

生产项目里不要保存 Mutable 生成结果,也不要保存 CustomizableObjectInstance。存档和服务器资料应该保存稳定的外观描述:角色基准、Slot 里的 ItemID、颜色覆盖、版本号和必要的自定义参数。进入游戏时,再用同一套 ApplyOutfitSnapshot 恢复表现。

UE Mutable 换装系统(六):保存、默认外观与初始化恢复 配图
保存恢复蓝图:存档只保存稳定快照,初始化时迁移、标准化并应用。

本篇目标

  1. 定义可保存的外观快照结构,不依赖 Mutable 内部对象。
  2. 建立默认外观体系,保证每个角色都能有安全回退。
  3. 设计初始化流程:新角色、读档、服务器资料、预览取消都复用同一入口。
  4. 设计存档版本迁移,处理装备改名、下架、Slot 变更和参数迁移。
  5. 明确坏档、缺资源、加载失败、Mutable 失败时的回退策略。

非目标

  1. 本篇不实现背包和玩家拥有权。
  2. 本篇不讨论云存档、账号合并和跨平台同步策略。
  3. 本篇不保存生成后的 Mesh、材质实例或贴图。
  4. 本篇不讲多人复制,网络权威在第八篇展开。

保存原则

外观保存应该满足三个条件:

  1. **稳定**:Mutable 图重构后,旧存档仍能迁移。
  2. **轻量**:只保存 ID 和少量参数,不保存资源对象。
  3. **可回退**:任意一件装备缺失,系统都能恢复到默认外观。

不要保存:

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 参数设置”。同一入口才能保证默认值、裁剪、颜色和失败回退一致。

读档标准化

读出的存档不能直接应用,需要先标准化:

  1. 检查 SchemaVersion
  2. 按迁移表更新旧 ItemID
  3. 移除未知 Slot。
  4. 为缺失 Slot 补默认装备。
  5. 检查每个装备是否存在且启用。
  6. 检查装备是否适配当前角色标签。
  7. 检查颜色参数范围。
  8. 生成新的 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_OutfitSaveUOutfitSaveSubsystem

函数 说明
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 提示,旧存档保留

验收标准

  1. 存档中不包含生成 Mesh、材质实例、Mutable Instance 或临时 UI 状态。
  2. 新角色无存档时能生成完整默认外观。
  3. 每个 Slot 都有默认装备,包括“无帽子”这类空显示装备。
  4. 读档恢复、UI 确认、取消恢复、网络同步都能复用 ApplyOutfitSnapshot
  5. ItemID 可以通过迁移表转换,转换失败能 fallback。
  6. 缺失 Slot 会被默认装备补齐。
  7. 正式外观成功应用后才保存。
  8. 预览状态不会写入存档。
  9. Mutable 更新失败不会覆盖旧存档。
  10. 坏档不会阻止玩家进入游戏,系统能回退默认外观并记录错误。
  11. 日志能输出 PlayerId / SchemaVersion / ItemId / Slot / ErrorCode / Fallback

本篇结论

保存系统保存的不是 Mutable 结果,而是一份稳定、可迁移、可回退的外观描述。只要默认外观、版本迁移和初始化入口设计清楚,后续 UI、服务器和热更新都可以围绕同一份 Snapshot 工作。下一篇进入性能和打包:如何异步加载、合并请求、控制 Mutable 生成预算,并保证打包后资源不丢。

参考资料