docs(technical): 交互系统技术方案 v1.0 重写:Effect/Action 数据驱动架构
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
- [UE5.7 Python API](ue57-python-api.md) — 实测 API 差异(set_actor_rotation、new_level、CWD、import 路径、Rotator 关键字参数)
|
||||
- [V01 Bugfix](level-builder-bugfix-v01.md) — 8个白模关卡问题诊断修复(搭建指南6+解析器双重解析+构建器静默fallback)
|
||||
- [BSP Pivot Types](bsp-pivot-types.md) — 38种 BSP 资产实测锚点+自动补偿机制(角落/底边中心/底面中心)
|
||||
- [交互系统架构](interaction-effect-architecture.md) — v1.0 Effect/Action 数据驱动架构(Manager在GI、AInfo Tick、门派生、Prompt独立)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: interaction-effect-architecture
|
||||
description: 交互系统 v1.0 Effect/Action 数据驱动架构决策记录
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
## 交互系统架构决策(v1.0 重构,2026-06-11)
|
||||
|
||||
### 核心架构:Effect/Action 数据驱动
|
||||
|
||||
交互物不关心"交互后发生什么",只配置 Effect(由 Action 组成)交给 EffectManager 执行。
|
||||
|
||||
**Why:** v0.1 用 switch(Type) 硬编码,新增交互类型需改代码。v1.0 配数据即可。
|
||||
|
||||
### Manager 归属
|
||||
- FInteractionManager / FEffectManager 是普通 C++ 类,不是 UObject
|
||||
- 由 ASystemDriver (AInfo) 持有,随 World 生灭
|
||||
- 不依赖 UE Subsystem 生命周期,显式控制创建顺序和 Tick 顺序
|
||||
- 所有 UObject 引用用 TWeakObjectPtr,回调用 TFunction
|
||||
- GameInstance 不持有任何交互系统对象,只有 UPersistenceSubsystem 跨 World
|
||||
|
||||
### 分层规则
|
||||
- 表现层只读 Manager 缓存(getter),不碰 Component
|
||||
- Manager 在选择变化时从 Component 读一次并缓存
|
||||
- 逻辑事件仅用于 "A 影响 B"(如 Consume → Persistence)
|
||||
|
||||
### 协调方式
|
||||
- Character 输入回调通过 ASystemDriver::Get(World) 访问两个 Manager
|
||||
- Driver::BeginPlay 中 TFunction 连线 EffectManager.OnEffectComplete → Manager.OnInteractionFinished
|
||||
- PromptComponent 通过 Driver 获取 Manager,只读缓存
|
||||
+729
-868
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,224 @@
|
||||
# 交互系统 · 拾取流程时序图(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| 显示 "捡到了 [旧橡皮]!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 时序图
|
||||
|
||||
```mermaid
|
||||
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 可以并行执行。
|
||||
Reference in New Issue
Block a user