9.8 KiB
9.8 KiB
交互系统 · 拾取流程时序图(v1.0 Effect 驱动版)
场景
玩家进入地上某个交互物的交互范围,按 E 交互后,人物做动作,交互物消失,背包加一个新物品,UI 显示捡到一个物品。
架构层级
| 层级 | 实体 | 所在位置 |
|---|---|---|
| Logic | FInteractionManager |
ASystemDriver 持有 (普通C++, 纯事件驱动, 缓存 Presentation 数据) |
| Logic | FEffectManager |
ASystemDriver 持有 (普通C++, Driver 提供Tick) |
| World | ASystemDriver |
AInfo 全局唯一 (持有所有Manager, 显式控制创建/Tick顺序) |
| Data | UInteractableComponent |
关卡实例 |
| Data | UPersistenceSubsystem |
GameInstanceSubsystem |
| Data | BackpackSystem |
GameInstance |
| Presentation | UInteractionPromptComponent |
Character |
数据流规则
- 表现层直接读数据层(Manager state / Component data / Backpack data)
- 逻辑事件仅在 A 影响 B 时使用(如 Consume Action → Persistence 写入)
- 两个表现实体之间不直接访问
Effect 配置
Effect: "拾取地面物品"
TotalDuration: 1.5s, Cancellable: false
TriggerTime | Action | 说明
----------- | --------------------- | ----
0.0s | TurnToTarget | TurnDuration=0.2,转向交互物
0.0s | PlayMontage | Montage=AM_Pickup,弯腰拾取动画
0.6s | Consume | 标记消耗,禁用检测球
0.6s | HideActor | 隐藏交互物 Actor
0.6s | AddToBackpack | 物品进背包 + 持久化
1.0s | ShowPickupNotification| 显示 "捡到了 [旧橡皮]!"
时序图
sequenceDiagram
autonumber
actor Player as 玩家
participant Char as AChildHoodCharacter
participant Mgr as FInteractionManager<br/>(普通C++ · ASystemDriver持有)
participant Comp as UInteractableComponent<br/>(Data · 关卡实例)
participant Actor as AInteractableActor
participant EFM as FEffectManager<br/>(普通C++ · ASystemDriver持有)
participant Persist as UPersistenceSubsystem<br/>(Data · GameInstance)
participant Bag as BackpackSystem
participant Pres as UInteractionPromptComponent<br/>(Presentation · Character)
participant Driver as ASystemDriver<br/>(AInfo · 持有所有Manager)
rect rgb(230, 245, 255)
Note over Player,Driver: ═══ Phase 0 · 进入检测范围 ═══
Player->>Char: WASD 移动进入 TriggerRadius
Comp->>Mgr: DetectionSphere OnOverlapBegin<br/>ASystemDriver::Get(World)->GetInteractionManager().RegisterInteractable(this)
Mgr->>Mgr: NearbyInteractables.Add(Comp)<br/>Sort by Dist2D → SelectedIndex = 0
Mgr->>Mgr: RefreshCachedSelection()<br/>缓存 PromptText + WorldPosition
end
rect rgb(255, 250, 230)
Note over Player,Driver: ═══ Phase 1 · Prompt 显示(Pres 每帧读 Manager 缓存)═══
loop 每帧 Pres TickComponent
Pres->>Mgr: GetState() → Free
Pres->>Mgr: HasSelection() → true
Pres->>Mgr: GetSelectedPromptText() → "[E] 拾取 旧橡皮"
Pres->>Mgr: GetSelectedWorldPosition() → FVector
Pres->>Pres: PromptWidget.SetText + SetScreenPosition
end
Note right of Pres: Pres 不碰 Component<br/>只读 Manager 缓存
end
rect rgb(230, 255, 230)
Note over Player,Driver: ═══ Phase 2 · 按 E,Character 通过 Driver 协调两个 Manager ═══
Player->>Char: 按 E (IA_Interact)
Char->>Mgr: Driver->GetInteractionManager().BeginInteract() → 返回 Comp
Mgr->>Mgr: State = Executing, CurrentInteractable = Comp
Char->>Char: SetInteracting(true) → 屏蔽移动
Char->>EFM: Driver->GetEffectManager().PlayEffect(Comp.Data.InteractEffect, Context)<br/>Context = {Char, Comp, Actor}
Note right of EFM: Effect "拾取地面物品"<br/>T=0.0 TurnToTarget + PlayMontage<br/>T=0.6 Consume+Hide+Backpack<br/>T=1.0 PickupNotification<br/>Total=1.5s
end
rect rgb(255, 240, 245)
Note over Player,Driver: ═══ Phase 3 · Presentation 检测状态变化 ═══
Pres->>Mgr: Tick: GetState() → Executing (变化!)
Pres->>Pres: PromptWidget.Hide()
end
rect rgb(240, 240, 255)
Note over Player,Driver: ═══ Phase 4 · Effect 执行(ASystemDriver Tick → EffectManager)═══
Note over Driver: T=0.0: TurnToTarget + PlayMontage
Driver->>EFM: Driver::Tick → EffectManager.Update(DeltaTime)
EFM->>EFM: ElapsedTime=0.0 → 触发 TurnToTarget Action
EFM->>Char: RInterpTo 转向 Comp(持续帧)
EFM->>EFM: 触发 PlayMontage Action
EFM->>Char: GetMesh()->GetAnimInstance()->Montage_Play(AM_Pickup)
Note right of Char: 播放弯腰拾取动画
Note over Driver: T=0.0→0.6: 持续转向 + 动画播放
loop 每帧 Driver Tick
Driver->>EFM: Tick → Update(DeltaTime)
EFM->>EFM: ElapsedTime += DeltaTime
EFM->>Char: TurnToTarget: RInterpTo 插值
end
Note over Driver: T=0.6: Consume + HideActor + AddToBackpack
Driver->>EFM: Tick → Update → ElapsedTime=0.6 触发
EFM->>Comp: Consume() — 数据变更
Comp-->>Comp: bIsConsumed=true<br/>DetectionSphere → NoCollision
Note right of Comp: 禁用碰撞同步触发 EndOverlap<br/>→ UnregisterInteractable<br/>Manager 标记延迟清理
EFM->>Persist: MarkItemCollected("rubber_01")
Persist-->>Persist: CollectedItemIDs.Add("rubber_01")
EFM->>Actor: SetActorHiddenInGame(true)
EFM->>Bag: AddItem("rubber_01", "旧橡皮")
Bag-->>Bag: Items.Add({Id:"rubber_01", Name:"旧橡皮"})
Pres->>Comp: Tick: IsConsumed() → true (变化!)
Pres->>Actor: SetActorHiddenInGame(true)
Note over Driver: T=1.0: ShowPickupNotification
Driver->>EFM: Tick → Update → ElapsedTime=1.0 触发
EFM->>Pres: 显示拾取通知 "捡到了 [旧橡皮]!"
end
rect rgb(230, 255, 255)
Note over Player,Driver: ═══ Phase 5 · Effect 完成,回到 Free ═══
Driver->>EFM: Tick → Update → ElapsedTime >= TotalDuration(1.5s)
EFM->>EFM: FinishEffect()
EFM->>Mgr: OnEffectComplete 回调
Mgr->>Mgr: State = Free, CurrentInteractable = nullptr
Mgr->>Char: SetInteracting(false) → 恢复移动
Pres->>Mgr: Tick: GetState() → Free (变化!)
Pres->>Mgr: GetSelectedInteractable() → null (已注销)
Note over Pres: 无其他交互物在范围,不显示 Prompt
Player->>Char: WASD 继续移动
end
关键数据流说明
表现层读数据(只读 Manager 缓存,不碰 Component)
| 读取时机 | PromptComponent 读谁 | 读什么 |
|---|---|---|
| 每帧 Tick | Manager | GetState() |
| Free 状态 | Manager | HasSelection() |
| Free 状态 | Manager | GetSelectedPromptText() |
| Free 状态 | Manager | GetSelectedWorldPosition() |
Manager 缓存更新(选择变化时从 Component 读一次)
| 触发时机 | Manager 方法 | 缓存内容 |
|---|---|---|
| RegisterInteractable | RefreshCachedSelection() | PromptText + WorldPosition |
| UnregisterInteractable | RefreshCachedSelection() | PromptText + WorldPosition |
| SwitchTarget | RefreshCachedSelection() | PromptText + WorldPosition |
Effect 内 Action 执行(直接操作)
| Action | 操作对象 | 说明 |
|---|---|---|
| TurnToTarget | Char->SetActorRotation | RInterpTo 跨帧插值 |
| PlayMontage | Char->GetMesh()->GetAnimInstance() | 播放蒙太奇 |
| Consume | Component->Consume() | 标记消耗+禁用检测 |
| HideActor | Actor->SetActorHiddenInGame(true) | 隐藏 |
| AddToBackpack | PersistenceSubsystem + BackpackSystem | 写入持久层+背包 |
| ShowPickupNotification | PromptComponent / Widget | 显示通知 |
逻辑事件(仅 A 影响 B)
| 事件 | 触发者 | 订阅者 |
|---|---|---|
| OnItemConsumed | Manager(由 Consume Action 触发) | PersistenceSubsystem |
| OnDoorToggled | Manager(由 ToggleDoor Action 触发) | PersistenceSubsystem + DoorComponent |
注意事项
EndOverlap 时序问题
Phase 4 中 Consume() 禁用 DetectionSphere 会同步触发 EndOverlap,进而调用 Manager->UnregisterInteractable()。此时 Manager 处于 Executing 状态且 Effect 仍在使用 CurrentInteractable。
解决方案:Manager 的 UnregisterInteractable 中,当 State == Executing 且移除的正是 CurrentInteractable 时,仅从 NearbyInteractables 列表移除,不清除 CurrentInteractable 引用,等 Effect 完成回调中统一清理。
ASystemDriver 生命周期
ASystemDriver是 AInfo 全局唯一,持有所有 World 级 Manager(普通 C++ 对象)- 关卡加载完成 →
GameInstanceSpawnASystemDriver Driver::BeginPlay显式创建FInteractionManager+FEffectManager,控制顺序Driver::BeginPlay中用TFunction连线EffectManager.OnEffectComplete → Manager.OnInteractionFinishedDriver::Tick显式调用EffectManager.Update(Delta),控制 Tick 顺序- 关卡卸载 → World 销毁 → Driver 自动销毁 → Manager 随 Driver 析构
- 新关卡加载 → 重新 Spawn + 创建
系统协调关系
FInteractionManager和FEffectManager是普通 C++ 类,由ASystemDriver持有AChildHoodCharacter输入回调通过ASystemDriver::Get(World)访问两个 ManagerDriver::BeginPlay中通过TFunction<void()>连接 EffectManager 完成回调到 ManagerFInteractionManager纯事件驱动,无 Tick/UpdateUChildHoodGameInstance不持有任何交互系统对象,仅UPersistenceSubsystem跨 World 存活
TurnToTarget 跨帧插值
TurnToTarget Action 不是瞬时完成的。EffectManager 在 Update 中维护 FTurnState(起始旋转、目标旋转、已用时间),每帧调用 FMath::RInterpTo 直到转向完成。多个 Action 可以并行执行。