Files
ChildHood/docs/technical/交互系统-拾取时序图.md
T

9.8 KiB
Raw Blame History

交互系统 · 拾取流程时序图(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++ 对象)
  • 关卡加载完成 → GameInstance Spawn ASystemDriver
  • Driver::BeginPlay 显式创建 FInteractionManager + FEffectManager,控制顺序
  • Driver::BeginPlay 中用 TFunction 连线 EffectManager.OnEffectComplete → Manager.OnInteractionFinished
  • Driver::Tick 显式调用 EffectManager.Update(Delta),控制 Tick 顺序
  • 关卡卸载 → World 销毁 → Driver 自动销毁 → Manager 随 Driver 析构
  • 新关卡加载 → 重新 Spawn + 创建

系统协调关系

  • FInteractionManager 和 FEffectManager 是普通 C++ 类,由 ASystemDriver 持有
  • AChildHoodCharacter 输入回调通过 ASystemDriver::Get(World) 访问两个 Manager
  • Driver::BeginPlay 中通过 TFunction<void()> 连接 EffectManager 完成回调到 Manager
  • FInteractionManager 纯事件驱动,无 Tick/Update
  • UChildHoodGameInstance 不持有任何交互系统对象,仅 UPersistenceSubsystem 跨 World 存活

TurnToTarget 跨帧插值

TurnToTarget Action 不是瞬时完成的。EffectManager 在 Update 中维护 FTurnState(起始旋转、目标旋转、已用时间),每帧调用 FMath::RInterpTo 直到转向完成。多个 Action 可以并行执行。