总览
第四篇把运行时核心收束到 BP_OutfitComponent。本篇接 UI。生产级衣柜 UI 不是“列表按钮调用 EquipItem”这么简单:它要显示当前穿搭、支持临时预览、处理加载中状态、允许取消恢复、避免连续点击打爆 Mutable 队列,还要把失败原因反馈给玩家。
UI 的目标不是接管换装逻辑,而是把用户意图整理成稳定请求,并准确反映组件状态。所有正式状态仍然由 BP_OutfitComponent 控制。
本篇目标
- 设计
WBP_OutfitRoom的页面结构、状态变量和事件绑定。 - 定义预览会话
FOutfitPreviewSession,让确认和取消都有明确依据。 - 区分 UI 选中、预览状态、正式状态和组件 Applying 状态。
- 支持快速点击防抖、预览请求合并和按钮 loading。
- 处理装备不可用、资源加载失败、Mutable 更新失败、关闭界面等异常。
- 为第六篇保存系统提供明确的“何时保存”边界。
非目标
- 本篇不实现背包、商城、购买、掉落和物品所有权。
- 本篇不直接保存存档,确认成功后只广播可保存事件。
- 本篇不做多人同步,服务器确认流程在第八篇。
- 本篇不让 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。若预览曾经应用到角色本体,必须恢复进入衣柜前的外观。
失败路径和回退
| 失败 | 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 的 PreviewOutfit 或 CurrentOutfit 提交给真实角色。
注意:
- Preview Actor 不参与网络复制。
- Preview Actor 不写存档。
- Preview Actor 可以使用更高优先级资源加载,但应在关闭 UI 时释放。
- Preview Actor 的动画可以是展示 Pose,不必使用完整战斗 AnimBP。
验收标准
- 打开衣柜时,UI 能显示当前正式穿搭。
- 点击装备只触发预览,不直接保存正式状态。
- 预览成功前,确认按钮不可用。
- 预览失败不会改变正式状态。
- 取消能恢复进入衣柜前的外观。
- 快速点击多件装备时,最终只预览最后一次有效点击。
- 装备不可用时,UI 显示明确原因,并且不会调用 Mutable。
- Slot 冲突能在 UI 上提示将替换的部件。
- 使用 Preview Actor 时,关闭 UI 会释放预览角色和临时资源。
- 确认成功后才广播保存事件。
- UI 不直接访问 Mutable Instance。
- 所有失败都有用户提示和开发日志。
本篇结论
衣柜 UI 的核心是预览会话,而不是按钮节点。UI 只负责整理意图和展示状态;BP_OutfitComponent 负责状态和 Mutable 应用。只要预览、确认、取消、失败回退这些边界清楚,后面的存档和多人同步才能复用同一份 Snapshot,而不是从 UI 里抠状态。