Unreal Engine Mutable 生产级换装系统

UE Mutable 换装系统(五):衣柜 UI、预览确认与交互状态

设计衣柜 UI、预览会话、Preview Actor、确认取消流程、快速点击防抖和 UI 异常反馈。

总览

第四篇把运行时核心收束到 BP_OutfitComponent。本篇接 UI。生产级衣柜 UI 不是“列表按钮调用 EquipItem”这么简单:它要显示当前穿搭、支持临时预览、处理加载中状态、允许取消恢复、避免连续点击打爆 Mutable 队列,还要把失败原因反馈给玩家。

UI 的目标不是接管换装逻辑,而是把用户意图整理成稳定请求,并准确反映组件状态。所有正式状态仍然由 BP_OutfitComponent 控制。

UE Mutable 换装系统(五):衣柜 UI、预览确认与交互状态 配图
衣柜交互蓝图:UI 管理预览会话,组件负责应用、回滚和通知。

本篇目标

  1. 设计 WBP_OutfitRoom 的页面结构、状态变量和事件绑定。
  2. 定义预览会话 FOutfitPreviewSession,让确认和取消都有明确依据。
  3. 区分 UI 选中、预览状态、正式状态和组件 Applying 状态。
  4. 支持快速点击防抖、预览请求合并和按钮 loading。
  5. 处理装备不可用、资源加载失败、Mutable 更新失败、关闭界面等异常。
  6. 为第六篇保存系统提供明确的“何时保存”边界。

非目标

  1. 本篇不实现背包、商城、购买、掉落和物品所有权。
  2. 本篇不直接保存存档,确认成功后只广播可保存事件。
  3. 本篇不做多人同步,服务器确认流程在第八篇。
  4. 本篇不让 UI 直接操作 Mutable 参数。

UI 架构

推荐页面结构:

WBP_OutfitRoom
  Header
    Category Tabs
    Search / Filter
  Item Grid
    WBP_OutfitItemCard
  Preview Panel
    3D Preview Actor 或角色本体
    Slot Summary
  Footer Actions
    Confirm
    Cancel
    Reset Slot

页面职责:

UI 元素 职责
Category Tabs 按 Slot 或标签过滤装备
Item Grid 展示可选装备、锁定、已拥有、已穿戴、预览中
Item Card 点击发起预览请求,不直接写正式状态
Preview Panel 显示正在预览的角色外观和加载状态
Confirm Button 只有预览成功且有改动时可点
Cancel Button 恢复进入衣柜前的正式外观
Reset Slot 把当前 Slot 切回默认装备,仍走预览

UI 状态模型

UI 需要独立保存“会话状态”,但不能复制一套换装系统。

USTRUCT(BlueprintType)
struct FOutfitPreviewSession
{
    GENERATED_BODY()

    UPROPERTY(BlueprintReadWrite)
    bool bIsActive = false;

    UPROPERTY(BlueprintReadWrite)
    FOutfitSnapshot EntrySnapshot;

    UPROPERTY(BlueprintReadWrite)
    FOutfitSnapshot PreviewSnapshot;

    UPROPERTY(BlueprintReadWrite)
    FName FocusSlotId;

    UPROPERTY(BlueprintReadWrite)
    FName LastClickedItemId;

    UPROPERTY(BlueprintReadWrite)
    int32 LastPreviewRequestId = 0;

    UPROPERTY(BlueprintReadWrite)
    bool bPreviewDirty = false;

    UPROPERTY(BlueprintReadWrite)
    bool bWaitingForPreview = false;
};

EntrySnapshot 是打开衣柜瞬间的正式状态。取消时回到它,确认成功后它才被新的正式状态替换。不要用“当前 UI 选中项”作为取消依据。

装备列表可以使用轻量 ViewData:

USTRUCT(BlueprintType)
struct FOutfitItemViewData
{
    GENERATED_BODY()

    UPROPERTY(BlueprintReadWrite)
    FName ItemId;

    UPROPERTY(BlueprintReadWrite)
    FText DisplayName;

    UPROPERTY(BlueprintReadWrite)
    TSoftObjectPtr<UTexture2D> Icon;

    UPROPERTY(BlueprintReadWrite)
    FName PrimarySlot;

    UPROPERTY(BlueprintReadWrite)
    bool bOwned = false;

    UPROPERTY(BlueprintReadWrite)
    bool bEquipped = false;

    UPROPERTY(BlueprintReadWrite)
    bool bPreviewed = false;

    UPROPERTY(BlueprintReadWrite)
    bool bCanEquip = true;

    UPROPERTY(BlueprintReadWrite)
    FText DisabledReason;
};

UI 显示用 ViewData,不要直接把 PDA_OutfitItem 暴露给每个卡片蓝图。这样后续接商城、权限、试穿、未拥有状态时不会污染数据资产。

WBP_OutfitRoom 变量清单

变量 类型 说明
TargetOutfitComponent BP_OutfitComponent 当前角色换装组件
PreviewOutfitComponent BP_OutfitComponent 可选,独立 Preview Actor 上的组件
PreviewSession FOutfitPreviewSession 当前衣柜会话
CurrentCategorySlot FName 当前 UI 分类
VisibleItems TArray<FOutfitItemViewData> 当前列表
bUsePreviewActor bool 是否用独立预览角色
PreviewDebounceSeconds float 快速点击防抖时间
PendingClickedItemId FName 防抖期内最后点击装备
bConfirmEnabled bool 确认按钮是否可用
bCancelEnabled bool 取消按钮是否可用
LastUiError FText 最近一次 UI 可展示错误

UI 函数清单

函数 输入 说明
OpenOutfitRoom(TargetCharacter) 角色 绑定组件,创建会话,刷新列表
CloseOutfitRoom(bCommitIfDirty) bool 关闭页面,必要时取消预览
BuildItemViewData(SlotId) Slot 从 Catalog 和组件状态生成列表数据
RefreshItemGrid() 刷新卡片状态
HandleItemClicked(ItemId) ItemID 记录点击并进入防抖
RequestPreviewItem(ItemId) ItemID 调用组件预览接口
HandlePreviewApplied(RequestId, Snapshot) 回调 更新 UI 预览状态
HandlePreviewFailed(RequestId, Error) 回调 显示错误并恢复按钮
ConfirmPreview() 提交预览
CancelPreview() 恢复 EntrySnapshot
ResetFocusedSlot() 当前 Slot 切默认装备
SetUiBusy(bBusy) bool 控制按钮和列表交互

打开衣柜流程

OpenOutfitRoom(TargetCharacter)
 -> Get BP_OutfitComponent
 -> EntrySnapshot = Component.GetCurrentOutfit()
 -> PreviewSnapshot = EntrySnapshot
 -> Bind OnOutfitApplied / OnOutfitApplyFailed / OnOutfitBusyChanged
 -> Spawn PreviewActor if needed
 -> Apply EntrySnapshot to PreviewActor
 -> BuildItemViewData(CurrentCategorySlot)
 -> Show UI

如果使用角色本体预览,取消时必须重新应用 EntrySnapshot。如果使用独立 Preview Actor,主角正式外观不会被预览影响,确认时再把 Preview Snapshot 提交给主角组件。

两种方案取舍:

方案 优点 风险
角色本体预览 实现简单,所见即所得 取消和失败回滚必须严谨,联网时要避免误同步
独立 Preview Actor 不污染主角状态,适合商城/角色创建 多一套组件和资源加载成本

生产项目更推荐衣柜和商城使用 Preview Actor,战斗内快速换装或轻量外观调整可以用本体预览。

点击预览流程

Item Card Click
 -> HandleItemClicked(ItemId)
 -> If item cannot equip: show DisabledReason and stop
 -> Set PendingClickedItemId = ItemId
 -> Start / Reset debounce timer

Debounce Timer Fires
 -> RequestPreviewItem(PendingClickedItemId)
 -> bWaitingForPreview = true
 -> SetUiBusy(true)
 -> PreviewComponent.EquipItem(ItemId, bPreview=true)

为什么需要防抖:玩家扫列表时会快速点击;如果每次点击都触发 Mutable 更新,UI 会感觉迟钝,队列也会堆积。100-200ms 防抖通常能保住体验。

确认流程

确认不能只保存 UI 当前选中项,必须确认预览已经成功应用。

ConfirmPreview
 -> If !PreviewSession.bPreviewDirty: close
 -> If bWaitingForPreview: disable confirm and wait
 -> Snapshot = PreviewSession.PreviewSnapshot
 -> TargetOutfitComponent.ApplyOutfitSnapshot(Snapshot, bPreview=false)
 -> On success:
      EntrySnapshot = Snapshot
      bPreviewDirty = false
      Broadcast OutfitCommitted(Snapshot)
      Close UI or stay open
 -> On fail:
      Show error
      Keep UI open
      Do not save

正式状态只在目标组件应用成功后才进入保存流程。第六篇会把 OutfitCommitted 接到 SaveGame 或服务器 Profile。

取消流程

CancelPreview
 -> Cancel pending preview request if possible
 -> If using PreviewActor:
      Destroy PreviewActor or reset to EntrySnapshot
 -> Else:
      TargetOutfitComponent.ApplyOutfitSnapshot(EntrySnapshot, bPreview=false)
 -> Clear PreviewSession
 -> Close UI

取消不能只关闭 UI。若预览曾经应用到角色本体,必须恢复进入衣柜前的外观。

按钮和列表状态

组件状态 列表 确认 取消 提示
Idle + 无改动 可点 禁用 可点
Idle + 有成功预览 可点 可点 可点 显示未保存标记
ResolvingData 禁止重复点击或只记最后点击 禁用 可点 加载数据
ApplyingMutable 禁止确认 禁用 可点但要取消 pending 预览中
Failed 可点 禁用 可点 显示失败原因

UI 不要在按钮点击后立刻把卡片标成“已装备”。应该等 OnOutfitApplied 成功后再标成预览中或已穿戴。

失败路径和回退

失败 UI 处理
装备未拥有 卡片显示锁定,点击提示获取方式
装备条件不满足 卡片禁用,展示职业/体型原因
Catalog 未加载 衣柜显示加载失败,提供重试
资源异步加载失败 显示“预览失败”,保留上一次成功预览
Slot 冲突 预览前展示将替换哪些部件
Mutable 更新失败 回滚预览状态,禁用确认
旧请求回调 UI 根据 RequestId 忽略
关闭界面时仍在更新 取消 pending,恢复 EntrySnapshot
确认失败 不保存,保留 UI,允许重试或取消

错误消息要分用户消息和开发日志。用户不需要看到 Slot_Upper option missing,但日志必须记录 ItemId / Slot / RequestId / ErrorCode

预览 Actor 方案

如果使用独立 Preview Actor,建议结构如下:

BP_OutfitPreviewActor
  SkeletalMeshComponent
  BP_OutfitComponent
  Camera Target
  Lighting Rig
  Turntable Root

UI 打开时生成 Preview Actor,并把主角的 CurrentOutfit 应用上去。之后所有点击都作用于 Preview Actor。确认时,把 Preview Actor 的 PreviewOutfitCurrentOutfit 提交给真实角色。

注意:

  1. Preview Actor 不参与网络复制。
  2. Preview Actor 不写存档。
  3. Preview Actor 可以使用更高优先级资源加载,但应在关闭 UI 时释放。
  4. Preview Actor 的动画可以是展示 Pose,不必使用完整战斗 AnimBP。

验收标准

  1. 打开衣柜时,UI 能显示当前正式穿搭。
  2. 点击装备只触发预览,不直接保存正式状态。
  3. 预览成功前,确认按钮不可用。
  4. 预览失败不会改变正式状态。
  5. 取消能恢复进入衣柜前的外观。
  6. 快速点击多件装备时,最终只预览最后一次有效点击。
  7. 装备不可用时,UI 显示明确原因,并且不会调用 Mutable。
  8. Slot 冲突能在 UI 上提示将替换的部件。
  9. 使用 Preview Actor 时,关闭 UI 会释放预览角色和临时资源。
  10. 确认成功后才广播保存事件。
  11. UI 不直接访问 Mutable Instance。
  12. 所有失败都有用户提示和开发日志。

本篇结论

衣柜 UI 的核心是预览会话,而不是按钮节点。UI 只负责整理意图和展示状态;BP_OutfitComponent 负责状态和 Mutable 应用。只要预览、确认、取消、失败回退这些边界清楚,后面的存档和多人同步才能复用同一份 Snapshot,而不是从 UI 里抠状态。

参考资料