diff --git a/.claude/agent-memory/game-dev/MEMORY.md b/.claude/agent-memory/game-dev/MEMORY.md
index a75b164..8a25ee9 100644
--- a/.claude/agent-memory/game-dev/MEMORY.md
+++ b/.claude/agent-memory/game-dev/MEMORY.md
@@ -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独立)
diff --git a/.claude/agent-memory/game-dev/interaction-effect-architecture.md b/.claude/agent-memory/game-dev/interaction-effect-architecture.md
new file mode 100644
index 0000000..658ac86
--- /dev/null
+++ b/.claude/agent-memory/game-dev/interaction-effect-architecture.md
@@ -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,只读缓存
diff --git a/docs/technical/交互系统-技术方案.md b/docs/technical/交互系统-技术方案.md
index 844d97f..7f1971f 100644
--- a/docs/technical/交互系统-技术方案.md
+++ b/docs/technical/交互系统-技术方案.md
@@ -1,67 +1,142 @@
-# 交互系统 技术设计文档(v0.1)
+# 交互系统 技术设计文档(v1.0 重构版)
> 对应执行案: `docs/design/交互系统-执行案.md` (v0.1.2)
> 关联系统: 角色控制系统、物品系统、收集系统、GameInstance 持久化层
-> 日期: 2026-06-07(2026-06-11 更新:加入多场景持久化架构)
+> 日期: 2026-06-11(v1.0 全面重构:Effect/Action 数据驱动架构)
+
+---
+
+## 迭代记录
+
+| 版本 | 日期 | 变更内容 |
+|------|------|----------|
+| v0.1 | 2026-06-07 | 初版技术方案,基于类型分发的状态机架构 |
+| v1.0 | 2026-06-11 | 全面重构:Effect/Action 数据驱动架构;普通 C++ Manager 挂 AInfo;门派生类;持久化 Subsystem;逻辑/表现解耦 |
---
## 概述
-交互系统让玩家通过接近并按 E 键的方式与环境中的物体和 NPC 产生互动。v0.1 原型实现四种交互类型(观察/拾取/操作/对话),覆盖完整的层级状态机和类型分发逻辑。
+交互系统让玩家通过接近并按 E 键的方式与环境中的物体和 NPC 产生互动。v1.0 采用 **Effect/Action 数据驱动架构**替代 v0.1 的类型分发状态机:
+
+- **Effect**:由多个 Action 按时间线编排而成的交互效果配方
+- **Action**:原子操作单元(播放动画、推镜、消耗、物品进背包等)
+- **EffectManager**:执行 Effect 的 Timeline 调度器
+- **交互物不关心"交互后发生什么"**,只负责触发配置好的 Effect
**实现复杂度**:中-高
-**预估工期**:5-7 天
-**新增/修改代码量**:约 600-800 行 C++ + 3 个 UMG 蓝图
+**预估工期**:6-8 天
+**新增/修改代码量**:约 800-1000 行 C++ + 3 个 UMG 蓝图
-### 跨场景持久化架构
+### 架构核心变更(v0.1 → v1.0)
-> 本项目后续将支持多场景切换(如城郊 → 乡下)。UE5 的 `OpenLevel()` 会销毁整个旧 World,包括 Character 及其所有 UActorComponent。因此需要将**持久数据**与**运行时行为**分离:
+| 维度 | v0.1 | v1.0 |
+|------|------|------|
+| 交互类型 | `EInteractType` 枚举 + Manager 内 `switch` | ❌ 不需要,Effect 定义一切 |
+| 状态机 | 5 阶段(Free/Enter/AnimLock/Active/Exit) | 2 阶段(Free/Executing) |
+| 新增交互类型 | 改代码加 case | 配数据写 Effect |
+| Manager 位置 | Character 上的 UActorComponent | ASystemDriver (AInfo) 持有的普通 C++ 类 |
+| Tick 驱动 | TickComponent | ASystemDriver Tick + PromptComponent 自有 Tick |
+| 持久化 | GameInstance 直接持有域数据 | UPersistenceSubsystem |
+| 门逻辑 | 基类硬编码 | ADoorActor + UDoorInteractableComponent 派生 |
+| 表现耦合 | Manager 持有 Camera/Widget | Prompt 独立,Effect 内 Action 各自处理 |
+| 跨场景 | 直接调 GameInstance | 通过 UPersistenceSubsystem |
-- **持久数据层**:`UChildHoodGameInstance`(跨关卡存活),存储已收集物品 ID、门状态等
-- **行为层**:`UInteractionManager` / `UInteractableComponent`(随 Character/场景销毁),仅负责运行时交互逻辑
-- **恢复机制**:`AInteractableActor::BeginPlay()` 时查询 GameInstance,若物品已收集则隐藏并禁用交互
+---
-v0.1 为单场景原型,此架构以接口预留形式存在,不影响当前功能。详见 [第 12 节:跨场景持久化设计](#12-跨场景持久化设计)。
+## 架构分层
+
+```
+┌──────────────────────────────────────────────────────────────────┐
+│ GameInstance 层(跨 World 持久,只有持久数据) │
+│ │
+│ UPersistenceSubsystem │
+│ └─ CollectedItemIDs / DoorStates / ResetAll │
+│ │
+├──────────────────────────────────────────────────────────────────┤
+│ World 层(per-world,随 World 生灭) │
+│ │
+│ ASystemDriver (AInfo, 全局唯一, 持有所有 World 级 Manager) │
+│ ├─ FInteractionManager(普通 C++,纯事件驱动,无 Tick) │
+│ │ ├─ 检测列表 / 选中管理 / 顶层状态 Free/Executing │
+│ │ ├─ 缓存 Presentation 数据 (PromptText, WorldPos) │
+│ │ └─ 所有 UObject 引用用 TWeakObjectPtr,不持有强引用 │
+│ │ │
+│ ├─ FEffectManager(普通 C++,需 Tick,由 Driver 驱动) │
+│ │ ├─ Effect Timeline 调度 / Action 执行分发 / Turn 插值 │
+│ │ └─ 所有 UObject 引用用 TWeakObjectPtr,不持有强引用 │
+│ │ │
+│ ├─ BeginPlay: 显式创建和初始化,控制顺序 │
+│ └─ Tick: 显式调用 EffectManager->Update(Delta),控制顺序 │
+│ └→ (未来其他 Manager 也在此 Tick,显式排序) │
+│ │
+│ 协调:Driver::BeginPlay 中 std::function 连线 │
+│ 协调:Character 输入回调中通过 Driver 访问两个 Manager │
+│ │
+│ AInteractableActor + UInteractableComponent (通用基类) │
+│ └─ ADoorActor + UDoorInteractableComponent (门派生) │
+│ └─ FInteractableData + FInteractEffect (配置 + Effect 定义) │
+│ └─ 碰撞注册 → Driver->InteractionManager │
+│ └─ 对 Presentation 完全不可见 │
+│ │
+├──────────────────────────────────────────────────────────────────┤
+│ Presentation 层(Character 上) │
+│ │
+│ UInteractionPromptComponent │
+│ └─ 自有 TickComponent,只读 Manager 缓存 → Prompt 位置/文本 │
+│ └─ 不直接访问任何 Component,通过 Driver 获取 Manager │
+│ │
+└──────────────────────────────────────────────────────────────────┘
+```
+
+### 数据流规则
+
+```
+┌──────────────┐ 直接读 ┌──────────────┐ 选择变化时 ┌──────────────┐
+│ Presentation │ ────────→ │ Logic │ ←─────────── │ World │
+│ (Prompt) │ │ (Manager │ 读一次并缓存 │ (Components │
+│ │ │ 缓存数据) │ │ 不可见) │
+└──────────────┘ └──────┬───────┘ └──────────────┘
+ │
+ │ 逻辑事件 (A 影响 B)
+ ↓
+ ┌──────────────┐
+ │ Persistence │
+ │ Subsystem │
+ └──────────────┘
+```
+
+- **表现层只读 Manager 缓存** — 不碰 Component,不碰任何 World 层对象
+- **Manager 在选择变化时从 Component 读一次并缓存** — Component 对 Presentation 不可见
+- **逻辑事件仅在 A 影响 B 时** — 如 Manager → Persistence 写入持久层
+- **两个表现实体之间不直接访问** — 各自从 Manager 读
---
## 1. 需求功能点罗列
-将执行文档中所有行为规则逐一列出,按实现方式分类。
-
### UE 内置功能
-以下功能点直接使用 UE5 引擎内置能力,无需或仅需极少量自定义代码:
-
-| 功能点 | 使用的 UE 内置能力 | 对应行为规则 | 备注 |
-|--------|-------------------|-------------|------|
-| 交互触发输入(E/ESC/Tab/滚轮) | Enhanced Input (IA_Interact / IA_Cancel / IA_SwitchInteract) | 通用触发/关闭/切换 | 通过 IMC 映射 |
-| 摄像机推镜与回位 | SetViewTargetWithBlend (VTBlend_EaseOut/EaseIn) | E1-E2/E5/S1-S2/K1-K2/K5 | 推镜用 EaseOut,回位用 EaseIn |
-| 移动输入屏蔽 | CMC + bIsInteracting 标志 | 通用触发规则 | DoMove() 中检查标志 |
-| 碰撞检测(进入/离开范围) | SphereComponent Overlap 事件 | 触发条件 | 每个交互物独立半径 |
-| UMG Widget 生命周期 | UUserWidget AddToViewport / RemoveFromParent | 提示 UI / 文本 UI | 淡入淡出用 UMG 内建动画 |
-| 动画播放 | AnimMontage + AnimInstance::Montage_Play | P2/D2/S3 | 有蒙太奇时使用,无则计时器替代 |
+| 功能点 | 使用的 UE 内置能力 | 对应行为规则 |
+|--------|-------------------|-------------|
+| 交互触发输入 | Enhanced Input (IA_Interact/IA_Cancel/IA_SwitchInteract) | 通用触发/关闭/切换 |
+| 摄像机推镜与回位 | SetViewTargetWithBlend | CameraPush/CameraReturn Action |
+| 移动输入屏蔽 | CMC + bIsInteracting 标志 | 通用触发规则 |
+| 碰撞检测 | SphereComponent Overlap 事件 | 触发条件 |
+| UMG Widget 生命周期 | UUserWidget AddToViewport / RemoveFromParent | 提示 UI |
+| 动画播放 | AnimMontage + AnimInstance::Montage_Play | PlayMontage Action |
### 自定义实现
-以下功能点需要编写自定义 C++ / 蓝图代码:
-
-| 功能点 | 实现方式概述 | 对应行为规则 | 预估复杂度 |
-|--------|-------------|-------------|-----------|
-| 层级状态机(Free/Interacting + 子阶段) | UActorComponent 内状态枚举 + TickComponent 驱动转换 | 状态定义/转换规则 | 高 |
-| 可交互物检测与选择 | 碰撞注册列表 + 水平距离排序 + 选中索引管理 | 提示 UI 显示/切换规则 | 中 |
-| 交互类型分发(4 类 + 操作子类型) | EInteractType + EOperateSubType 枚举 + switch | 各类型触发后行为 | 中 |
-| 角色自动转向 | TickComponent 中 FMath::RInterpTo 插值旋转 | 各类型转向阶段 | 低 |
-| 推镜目标位置计算 | 交互物位置 + 方向偏移 → 定位临时 CameraActor | E1-E2/S1-S2/K1-K2 | 低 |
-| 提示 UI 屏幕空间投影 | ProjectWorldLocationToScreen + Widget 锚点偏移 | 提示 UI 位置/显示 | 低 |
-| 文本底框显示控制 | UMG Widget 显隐 + 淡入淡出动画 | 通用文本显示 UI | 低 |
-| 门开关状态管理 | 布尔状态 + Mesh 旋转 + 提示文字切换 | D3/D6 | 低 |
-| oneShot 消耗管理 | 布尔标记 + 检测区域注销 | 触发条件/边界情况 | 低 |
-| 交互物配置数据 | FInteractableData USTRUCT + 编辑器属性暴露 | 全部 | 低 |
-| 交互系统→物品系统通知 | 多播委托 OnItemPickedUp / OnItemFound | P4/S5 | 低 |
-
-> **分类原则**:UE 内置能力覆盖 80% 以上需求且行为一致 → UE 内置;需要自定义状态管理、跨系统协调或特殊逻辑 → 自定义实现。
+| 功能点 | 实现方式概述 | 预估复杂度 |
+|--------|-------------|-----------|
+| 检测与选择 | 碰撞注册列表 + 水平距离排序 + 选中索引管理 | 中 |
+| Effect Timeline 调度 | EffectManager 按 TriggerTime 触发 Action | 高 |
+| Action 执行分发 | switch(ActionType) 分发到各 Action 处理逻辑 | 中 |
+| Effect 数据定义 | FInteractEffect + FInteractAction USTRUCT | 低 |
+| Prompt 屏幕投影 | ProjectWorldToScreen + Widget 位置更新 | 低 |
+| 门开关状态管理 | UDoorInteractableComponent 派生类 + Mesh 旋转 | 低 |
+| 持久化 | UPersistenceSubsystem (GameInstanceSubsystem) | 低 |
---
@@ -69,322 +144,311 @@ v0.1 为单场景原型,此架构以接口预留形式存在,不影响当前
```mermaid
flowchart TD
- subgraph 玩家侧
- Input[输入处理模块]
- CharState[角色状态模块]
+ subgraph GameInstance层
+ PS[UPersistenceSubsystem
跨场景持久数据
唯一跨World存活对象]
end
- subgraph 交互核心
- Detection[检测与选择模块]
- StateMachine[交互状态机模块]
- TypeHandler[类型处理器]
- TurnCtrl[角色转向模块]
- CameraCtrl[摄像机控制模块]
+ subgraph World层
+ Driver[ASystemDriver
AInfo 全局唯一
持有所有World级Manager
显式控制创建/Tick顺序]
+ Mgr["FInteractionManager
(普通C++,纯事件驱动,无Tick)"]
+ EFM["FEffectManager
(普通C++,Driver提供Tick)"]
end
- subgraph 可交互物侧
- InteractableData[交互物数据模块]
- Trigger[触发区域模块]
+ subgraph Interactable层
+ IC[UInteractableComponent
配置 + 检测 + 运行时状态]
+ DC[UDoorInteractableComponent
门开关状态]
+ IA[AInteractableActor]
+ DA[ADoorActor]
end
- subgraph 表现层
- PromptUI[提示 UI 模块]
- TextUI[文本底框 UI 模块]
+ subgraph Character协调层
+ Char[AChildHoodCharacter
输入回调中通过Driver访问两个Manager]
+ end
+
+ subgraph Presentation层
+ Prompt[UInteractionPromptComponent
自有Tick,只读Manager缓存
不碰Component]
end
subgraph 下游系统
- ItemSys[物品系统]
+ Bag[BackpackSystem]
end
- Input -->|E/ESC/Tab/滚轮| StateMachine
- Trigger -->|注册/注销| Detection
- InteractableData -->|FInteractableData| Detection
- Detection -->|排序列表 + 选中| StateMachine
- StateMachine -->|锁定/解锁移动| CharState
- StateMachine -->|转向目标| TurnCtrl
- StateMachine -->|推镜/回位| CameraCtrl
- StateMachine -->|类型分发| TypeHandler
- StateMachine -->|显隐/更新内容| PromptUI
- StateMachine -->|显示/淡出文本| TextUI
- TypeHandler -.->|拾取/翻找获得| ItemSys
+ Driver -->|持有| Mgr
+ Driver -->|持有| EFM
+ Input[Enhanced Input] --> Char
+ IC -->|碰撞注册| Driver
+ Char -->|Driver->InteractionMgr| Mgr
+ Char -->|Driver->EffectMgr| EFM
+ Driver -->|Tick| EFM
+ EFM -->|std::function 回调| Mgr
+ EFM -->|Action 执行| IA
+ EFM -->|Action 执行| PS
+ EFM -.->|AddToBackpack Action| Bag
+ Prompt -->|只读缓存| Mgr
+ DA -->|继承| IA
+ DC -->|继承| IC
```
-### 模块职责说明
-
-| 模块 | 职责 | 关键输入 | 关键输出 |
-|------|------|---------|---------|
-| 输入处理模块 | 接收 Enhanced Input 回调,转发给状态机 | IA_Interact / IA_Cancel / IA_SwitchInteract | BeginInteract / CancelInteract / SwitchTarget |
-| 检测与选择模块 | 维护附近可交互物列表,按距离排序,管理选中索引 | SphereComponent 碰撞事件 | NearbyInteractables 列表 + SelectedIndex |
-| 交互状态机模块 | 管理层级状态转换,Tick 驱动各阶段流转 | 输入 + 检测结果 + 计时器 | 驱动其他模块的指令 |
-| 类型处理器 | 按 EInteractType 分发到具体行为逻辑 | 当前交互物类型 + 当前阶段 | 类型特定动作 |
-| 角色转向模块 | Interacting/Enter 阶段插值旋转角色面朝交互物 | 目标旋转 + 已用时间 | 角色旋转更新 |
-| 摄像机控制模块 | 推镜(SetViewTargetWithBlend→临时 CameraActor)/ 回位 | 目标位置 + 持续时间 | ViewTarget 切换 |
-| 交互物数据模块 | 存储单个可交互物的全部配置数据和运行时状态 | 编辑器配置 / 运行时更新 | FInteractableData |
-| 触发区域模块 | 检测玩家进入/离开交互范围 | SphereComponent 碰撞 | 注册/注销到检测列表 |
-| 提示 UI 模块 | 屏幕空间悬浮提示(`[E] 查看旧冰箱`) | 选中状态变化 + 交互物位置 | 视觉提示 |
-| 文本底框 UI 模块 | 半透明底框 + 一句话叙事/对话文本 | 状态机指令 | 文本展示 + 淡出 |
-| 物品系统(下游) | 接收拾取/获得通知,处理背包和收集 | 委托回调 | 拾取通知 UI / 收集通知 |
-
---
## 3. 类设计图
```mermaid
classDiagram
- class AChildHoodCharacter {
- -UInteractionManager* InteractionManager
- -bool bIsInteracting
- +SetInteracting(bool bValue) void
- +IsInteracting() bool
- +DoMove(float Right, float Forward)
- +GetInteractionManager() UInteractionManager*
+ class ASystemDriver {
+ -FInteractionManager InteractionManager
+ -FEffectManager EffectManager
+ +GetInteractionManager() FInteractionManager&
+ +GetEffectManager() FEffectManager&
+ +BeginPlay() void
+ +Tick(float DeltaTime) void
+ +static Get(UWorld*) ASystemDriver*
+ Note: AInfo全局唯一,持有所有World级Manager
+ Note: 显式控制创建顺序和Tick顺序
+ Note: Manager是普通C++类,不是UObject
}
- class UChildHoodGameInstance {
- -TSet~FName~ CollectedItemIDs
- -TMap~FName_bool~ DoorStates
- +IsItemCollected(FName ItemId) bool
- +MarkItemCollected(FName ItemId) void
- +IsDoorOpen(FName DoorId) bool
- +SetDoorOpen(FName DoorId, bool bIsOpen) void
- +ResetAll() void
- }
-
- class AChildHoodPlayerController {
- +SetupInputComponent() void
- +OnInteractPressed() void
- +OnCancelPressed() void
- +OnSwitchInteract(float Direction) void
- }
-
- class UInteractionManager {
+ class FInteractionManager {
-EInteractionState State
- -EInteractPhase Phase
- -TArray~UInteractableComponent*~ NearbyInteractables
+ -TArray~TWeakObjectPtr~UInteractableComponent~~ NearbyInteractables
-int32 SelectedIndex
- -UInteractableComponent* CurrentInteractable
- -AActor* PushCameraActor
- -UCameraComponent* PushCameraComp
- -float TurnElapsed
- -float WaitElapsed
- -FTimerHandle AnimEventTimerHandle
- -FTimerHandle AnimEndTimerHandle
- -bool bAnimEventFired
- -FString LastNarrativeText
- +BeginInteract() void
+ -TWeakObjectPtr~UInteractableComponent~ CurrentInteractable
+ -TWeakObjectPtr~ACharacter~ PlayerCharacter
+ -FSelectedInfo CachedSelected
+ -TFunction~void~ OnInteractionFinished
+ +Initialize(ACharacter*) void
+ +BeginInteract() UInteractableComponent*
+CancelInteract() void
+SwitchTarget(int32 Direction) void
+RegisterInteractable(UInteractableComponent*) void
+UnregisterInteractable(UInteractableComponent*) void
- #TickComponent(float, ELevelTick) void
- -UpdateSelection() void
- -UpdatePromptUI() void
- -TickEnterPhase(float DeltaTime) void
- -TickAnimLockPhase(float DeltaTime) void
- -TickExitPhase(float DeltaTime) void
- -TransitionFromEnter() void
- -OnAnimLockComplete() void
- -ExecuteAnimEvent() void
- -ShowNarrativeText(FString Text) void
- -HideNarrativeText() void
- -PushCamera(FVector Target, float Duration) void
- -ReturnCamera(float Duration) void
- -CalculatePushPosition(UInteractableComponent*) FVector
- +OnItemPickedUp FMLocalItemEvent
- +OnItemFound FMLocalItemEvent
+ +GetState() EInteractionState
+ +HasSelection() bool
+ +GetSelectedPromptText() FString
+ +GetSelectedWorldPosition() FVector
+ -RefreshCachedSelection() void
+ Note: 普通C++类,非UObject
+ Note: 所有UObject引用用TWeakObjectPtr
+ Note: 纯事件驱动,无Tick
+ }
+
+ class FEffectManager {
+ -FInteractEffect CurrentEffect
+ -FEffectContext CurrentContext
+ -float ElapsedTime
+ -int32 NextActionIndex
+ -bool bIsPlaying
+ -FTurnState TurnState
+ -TFunction~void~ OnEffectComplete
+ +PlayEffect(FInteractEffect, FEffectContext) void
+ +CancelEffect() void
+ +IsPlaying() bool
+ +Update(float DeltaTime) void
+ -ExecuteAction(FInteractAction, FEffectContext) void
+ -FinishEffect() void
+ Note: 普通C++类,非UObject
+ Note: 所有UObject引用用TWeakObjectPtr
+ Note: Tick由Driver显式调用
+ }
+
+ class UPersistenceSubsystem {
+ -TSet~FName~ CollectedItemIDs
+ -TMap~FName_bool~ DoorStates
+ +IsItemCollected(FName) bool
+ +MarkItemCollected(FName) void
+ +IsDoorOpen(FName) bool
+ +SetDoorOpen(FName, bool) void
+ +ResetAll() void
}
class UInteractableComponent {
- -FInteractableData Data
+ #FInteractableData Data
-USphereComponent* DetectionSphere
-bool bIsConsumed
- -bool bDoorIsOpen
+GetInteractableData() FInteractableData
- +GetPromptText() FString
+ +GetPromptText() virtual FString
+IsConsumed() bool
- +Consume() void
+ +Consume() virtual void
+ +GetWorldLocation() FVector
+ +RestorePersistentState(UPersistenceSubsystem*) virtual void
+ #OnDetectionOverlapBegin(...) void
+ #OnDetectionOverlapEnd(...) void
+ }
+
+ class UDoorInteractableComponent {
+ -bool bDoorIsOpen
+ToggleDoor() void
+IsDoorOpen() bool
- +GetWorldLocation() FVector
- +RestorePersistentState(UChildHoodGameInstance*) void
- #OnDetectionOverlapBegin(UPrimitiveComponent*, AActor*) void
- #OnDetectionOverlapEnd(UPrimitiveComponent*, AActor*) void
+ +GetPromptText() FString
+ +RestorePersistentState(UPersistenceSubsystem*) void
+ }
+
+ class AInteractableActor {
+ #UStaticMeshComponent* MeshComp
+ #UInteractableComponent* InteractableComp
+ +OnInteractStarted(UInteractableComponent*) virtual void
+ +OnInteractEnded(UInteractableComponent*) virtual void
+ }
+
+ class ADoorActor {
+ +OnInteractStarted(UInteractableComponent*) void
+ }
+
+ class UInteractionPromptComponent {
+ -TWeakObjectPtr~ASystemDriver~ Driver
+ -UInteractionPromptWidget* PromptWidget
+ -float PromptHeightOffset
+ +Initialize(ASystemDriver*) void
+ #TickComponent(float) void
+ Note: 只读 Manager 缓存,不直接访问任何 Component
}
class FInteractableData {
+FName ItemId
+FString DisplayName
- +EInteractType Type
- +EOperateSubType OperateSubType
+float TriggerRadius
+bool bOneShot
+FString InteractVerb
+FString InteractVerbAlt
+ +FInteractEffect InteractEffect
+ }
+
+ class FInteractEffect {
+ +FString EffectName
+ +float TotalDuration
+ +bool bCancellable
+ +TArray~FInteractAction~ Actions
+ }
+
+ class FInteractAction {
+ +EInteractActionType Type
+ +float TriggerTime
+ +UAnimMontage* Montage
+ +float PushDistance
+ +float PushDuration
+ +float ReturnDuration
+ +float TurnDuration
+TArray~FString~ NarrativeTexts
- +bool bUseCameraPush
- +float CameraPushDistance
- +float CameraPushDuration
- +float PushWaitDelay
- +UAnimMontage* InteractMontage
- +float AnimDuration
- +float AnimEventTimepoint
- +EPostAction PostAction
- +float CameraReturnDuration
}
- class AInteractableActor {
- -UStaticMeshComponent* MeshComp
- -UInteractableComponent* InteractableComp
- +OnInteractStarted(UInteractableComponent*) void
- +OnInteractEnded(UInteractableComponent*) void
- }
-
- class APushCameraActor {
- +UCameraComponent* CameraComp
- }
-
- AChildHoodCharacter *-- UInteractionManager : 组合
- AChildHoodCharacter --> AChildHoodPlayerController : 关联
- UInteractionManager --> UInteractableComponent : 检测并驱动
- UInteractionManager o-- APushCameraActor : 聚合(引用)
+ ASystemDriver *-- FInteractionManager : 持有(值或unique_ptr)
+ ASystemDriver *-- FEffectManager : 持有(值或unique_ptr)
+ FInteractionManager ..> UPersistenceSubsystem : 依赖
+ FEffectManager ..> UPersistenceSubsystem : 依赖
AInteractableActor *-- UInteractableComponent : 组合
- AInteractableActor *-- UStaticMeshComponent : 组合
- UInteractableComponent *-- FInteractableData : 组合(内含)
- UInteractableComponent o-- USphereComponent : 聚合(引用子对象)
- UInteractableComponent ..> UChildHoodGameInstance : 恢复持久状态
- UInteractionManager ..> UChildHoodGameInstance : 写入消耗/门状态
+ ADoorActor --|> AInteractableActor : 继承
+ UDoorInteractableComponent --|> UInteractableComponent : 继承
+ ADoorActor *-- UDoorInteractableComponent : 组合
+ UInteractableComponent *-- FInteractableData : 内含
+ FInteractableData *-- FInteractEffect : 内含
+ FInteractEffect *-- FInteractAction : 内含数组
+ AChildHoodCharacter *-- UInteractionPromptComponent : 组合
+ UInteractionPromptComponent ..> ASystemDriver : 只读Manager缓存
```
-### 关系类型说明
-
-| 关系类型 | 含义 | 本项目中的体现 |
-|---------|------|--------------|
-| 组合 `*--` | 生命周期绑定 | Character 持有 InteractionManager;InteractableActor 持有 InteractableComponent |
-| 聚合 `o--` | 引用但非拥有 | InteractionManager 引用 PushCameraActor;InteractableComponent 引用 DetectionSphere(子对象) |
-| 关联 `-->` | 持久引用 | Character 关联 PlayerController;InteractionManager 引用 InteractableComponent 列表 |
-| 依赖 `..>` | 临时使用 | InteractableComponent 从 GameInstance 恢复状态;InteractionManager 向 GameInstance 写入消耗/门状态 |
-
### 新增类
| 类名 | 类型 | 职责 | 位置 |
|------|------|------|------|
-| `UInteractionManager` | UActorComponent | 交互全流程状态机、检测选择、类型分发、摄像机/转向/UI 控制 | `Source/ChildHood/Interaction/` |
-| `UInteractableComponent` | UActorComponent | 单个交互物配置数据、触发区域、运行时状态、跨场景持久状态恢复 | `Source/ChildHood/Interaction/` |
-| `AInteractableActor` | AActor | 通用交互物 Actor 基类,持有 Mesh + InteractableComponent | `Source/ChildHood/Interaction/` |
-| `APushCameraActor` | AActor | 推镜用临时摄像机,仅含 UCameraComponent | `Source/ChildHood/Interaction/` |
-| `UChildHoodGameInstance` | UGameInstance | 跨关卡持久数据持有者:已收集物品 ID、门状态等 | `Source/ChildHood/` |
+| `ASystemDriver` | AInfo | 全局唯一,持有所有 World 级 Manager,显式控制创建/Tick 顺序 | `Source/ChildHood/` |
+| `FInteractionManager` | 普通 C++ | 检测列表、选中管理、顶层状态,纯事件驱动无 Tick | `Source/ChildHood/Interaction/` |
+| `FEffectManager` | 普通 C++ | Effect Timeline 调度、Action 执行分发,由 Driver Tick 驱动 | `Source/ChildHood/Interaction/` |
+| `UPersistenceSubsystem` | UGameInstanceSubsystem | 跨场景持久数据 | `Source/ChildHood/Interaction/` |
+| `UInteractableComponent` | UActorComponent | 通用交互物配置+检测+运行时状态 | `Source/ChildHood/Interaction/` |
+| `AInteractableActor` | AActor | 通用交互物基类 | `Source/ChildHood/Interaction/` |
+| `UDoorInteractableComponent` | UActorComponent | 门:开关状态+动词切换+状态恢复 | `Source/ChildHood/Interaction/` |
+| `ADoorActor` | AActor | 门:Mesh 旋转视觉反馈 | `Source/ChildHood/Interaction/` |
+| `UInteractionPromptComponent` | UActorComponent | Prompt UI 显示/位置追踪 | `Source/ChildHood/Interaction/` |
### 修改类
| 类名 | 修改内容 |
|------|---------|
-| `AChildHoodCharacter` | 新增 `UInteractionManager*` 成员、`bIsInteracting` 标志、`SetInteracting()` / `GetInteractionManager()` 接口;`DoMove()` 添加交互中判断 |
-| `AChildHoodPlayerController` | 新增 `IA_Interact` / `IA_Cancel` / `IA_SwitchInteract` InputAction 属性;`SetupInputComponent()` 添加交互输入绑定 |
+| `AChildHoodCharacter` | 持有 `UInteractionPromptComponent`;输入回调通过 `ASystemDriver::Get()` 访问 Manager |
+| `UChildHoodGameInstance` | 删除所有交互相关对象持有;仅保留 GameInstance 级基础设施 |
+| `AChildHoodPlayerController` | 保留交互输入绑定 |
-### 新增蓝图
+### 删除类
-| 蓝图名 | 类型 | 用途 |
-|--------|------|------|
-| `WBP_InteractionPrompt` | UMG Widget | 交互提示 UI(`[E] 查看旧冰箱`),屏幕空间悬浮 |
-| `WBP_NarrativeText` | UMG Widget | 文本底框 UI(半透明背景 + 一句话文字) |
-| `BP_Interactable` | Actor Blueprint | 基于 AInteractableActor 的可配置交互物蓝图,每个场景交互物为其实例 |
+| 类名 | 原因 |
+|------|------|
+| 旧 `UInteractionManager` (UActorComponent) | 移入 GameInstance,重写为 UObject |
---
## 4. 时序图
-> ⚠️ 交互系统涉及 3+ 系统协调(交互→物品→收集),存在条件分支和类型分发,满足时序图绘制条件。
+### 4.1 拾取交互(地面物品)
-### 4.1 完整交互流程(以观察类为例)
+> 详见 `docs/technical/交互系统-拾取时序图.md`(基于 v1.0 架构更新版)
```mermaid
sequenceDiagram
autonumber
- participant Player as 玩家
- participant PC as PlayerController
- participant IM as InteractionManager
- participant IC as InteractableComponent
- participant Char as Character
- participant PCA as PushCameraActor
- participant UI as PromptUI
+ actor Player as 玩家
+ participant Char as AChildHoodCharacter
+ participant Mgr as InteractionManager
(GameInstance)
+ participant Comp as UInteractableComponent
+ participant EFM as EffectManager
+ participant Persist as PersistenceSubsystem
+ participant Bag as BackpackSystem
+ participant Pres as PromptComponent
+ participant Driver as AInfo Driver
- Note over IM: State: Free, 检测标记: true
+ Note over Player,Driver: === Phase 0: 进入检测范围 ===
+ Player->>Char: 移动进入 TriggerRadius
+ Comp->>Mgr: OnOverlap → GetGI()->GetMgr()->RegisterInteractable(this)
+ Mgr->>Mgr: Sort by Dist2D, SelectedIndex = 0
- Player->>PC: 按 E 键 (IA_Interact)
- activate PC
- PC->>IM: BeginInteract()
- deactivate PC
+ Note over Player,Driver: === Phase 1: Prompt 显示 (Pres 每帧 Polling) ===
+ loop 每帧 Tick
+ Pres->>Mgr: GetState() → Free
+ Pres->>Mgr: GetSelectedInteractable() → Comp
+ Pres->>Comp: GetPromptText() → "[E] 拾取 旧橡皮"
+ Pres->>Comp: GetWorldLocation() → FVector
+ Pres->>Pres: UpdatePromptWidget()
+ end
- activate IM
- IM->>IM: State=Interacting, Phase=Enter
- IM->>Char: SetInteracting(true)
- IM->>UI: HidePrompt()
+ Note over Player,Driver: === Phase 2: 按 E 触发 Effect ===
+ Player->>Char: 按 E
+ Char->>Mgr: GetGI()->GetMgr()->BeginInteract()
+ Mgr->>Mgr: State = Executing
+ Mgr->>Char: SetInteracting(true)
+ Mgr->>EFM: PlayEffect(Comp.Data.InteractEffect, Context)
+ Note right of EFM: Effect: "拾取地面物品"
T=0.0: TurnToTarget
T=0.0: PlayMontage
T=0.6: Consume+Hide+Backpack
T=1.0: PickupNotification
Total: 1.5s
- Note over IM: Phase: Enter — 转向 + 推镜
+ Note over Player,Driver: === Phase 3: Presentation 检测状态变化 ===
+ Pres->>Mgr: Tick: GetState() → Executing (变化!)
+ Pres->>Pres: HidePrompt()
- IM->>IM: Tick: RInterpTo 转向目标
- IM->>PCA: SetActorLocation(TargetPushPos)
- IM->>PC: SetViewTargetWithBlend(PCA, 0.5s, EaseOut)
+ Note over Player,Driver: === Phase 4: Effect 执行 (Driver Tick 驱动) ===
+ loop 每帧 Driver→EFM.Update(DeltaTime)
+ EFM->>EFM: ElapsedTime += DeltaTime
+ EFM->>EFM: 检查 TriggerTime 触发 Action
+ EFM->>Char: TurnToTarget: RInterpTo 转向
+ end
- Note over IM: 推镜完成后等待 0.2s
+ Note over EFM: T=0.0: TurnToTarget + PlayMontage
+ EFM->>Char: Montage_Play(PickupMontage)
- IM->>IM: Phase=Active, 显示叙事文本
- IM->>IM: ShowNarrativeText(随机选取)
+ Note over EFM: T=0.6: Consume + HideActor + AddToBackpack
+ EFM->>Comp: Consume()
+ Comp-->>Comp: bIsConsumed=true, DisableSphere
+ EFM->>Persist: MarkItemCollected("rubber_01")
+ EFM->>Char: SetActorHiddenInGame(true)
+ EFM->>Bag: AddItem("rubber_01", "旧橡皮")
+ Pres->>Comp: Tick: IsConsumed() → true
+ Pres->>Char: SetActorHiddenInGame(true)
- Note over IM: Phase: Active — 等待玩家关闭
+ Note over EFM: T=1.0: ShowPickupNotification
+ EFM->>Pres: 显示 "捡到了 [旧橡皮]!"
- Player->>PC: 按 E/ESC (IA_Cancel)
- PC->>IM: CancelInteract()
- IM->>IM: HideNarrativeText() (淡出 0.2s)
- IM->>PC: SetViewTargetWithBlend(Char, 0.3s, EaseIn)
-
- Note over IM: Phase: Exit — 相机回位
-
- IM->>Char: SetInteracting(false)
- IM->>IM: State=Free
- deactivate IM
-
- Note over IM: 回到 Free,若仍在范围内则重新显示提示
-```
-
-### 4.2 拾取流程(跨系统通知)
-
-```mermaid
-sequenceDiagram
- autonumber
- participant Player as 玩家
- participant IM as InteractionManager
- participant IC as InteractableComponent
- participant Char as Character
- participant ItemSys as 物品系统(委托)
-
- Player->>IM: BeginInteract() [Type=Pickup]
- activate IM
-
- IM->>IM: State=Interacting, Phase=Enter
- IM->>Char: SetInteracting(true)
- IM->>IM: Tick: 转向目标
-
- Note over IM: Phase: AnimLock — 播放拾取动画
-
- IM->>Char: PlayMontage(弯腰拾取)
- IM->>IM: 设置计时器: AnimEvent@60%
-
- Note over IM: 动画播放至 60%
-
- IM->>IC: Consume() (标记已消耗)
- IM->>IC: 通知 Actor 隐藏 Mesh
- IM->>ItemSys: OnItemPickedUp.Broadcast(ItemId, "杂物")
- activate ItemSys
- ItemSys-->>IM: (物品系统自行处理)
- deactivate ItemSys
-
- Note over IM: 动画自然播完
-
- IM->>Char: SetInteracting(false)
- IM->>IC: 注销检测
- IM->>IM: State=Free
- deactivate IM
+ Note over Player,Driver: === Phase 5: Effect 完成 ===
+ EFM->>EFM: ElapsedTime >= TotalDuration
+ EFM->>Mgr: OnEffectComplete 回调
+ Mgr->>Mgr: State = Free
+ Mgr->>Char: SetInteracting(false)
+ Pres->>Mgr: Tick: GetState() → Free (变化!)
```
---
@@ -394,526 +458,344 @@ sequenceDiagram
### 枚举
```cpp
-// 交互类型
-UENUM(BlueprintType)
-enum class EInteractType : uint8
-{
- Examine, // 观察
- Pickup, // 拾取
- Operate, // 操作
- Talk // 对话
-};
-
-// 操作子类型(仅 Operate 类型有效)
-UENUM(BlueprintType)
-enum class EOperateSubType : uint8
-{
- Door, // 门(开关切换)
- Search // 翻找
-};
-
-// 交互系统顶层状态
+// 交互系统顶层状态(简化为 2 阶段)
UENUM(BlueprintType)
enum class EInteractionState : uint8
{
- Free, // 自由状态
- Interacting, // 交互中
- SubSystem, // v0.1 预留
- Cutscene // v0.1 预留
+ Free, // 自由状态,检测/选择/Prompt
+ Executing, // 执行 Effect 中
};
-// Interacting 子阶段
+// Action 类型
UENUM(BlueprintType)
-enum class EInteractPhase : uint8
+enum class EInteractActionType : uint8
{
- None,
- Enter, // 进入:转向 + 推镜
- AnimLock, // 动画锁定:等待动画/计时完成
- Active, // 活跃:等待玩家输入(E/ESC)
- Exit // 退出:回位 + 淡出
-};
-
-// 交互结束后动作
-UENUM(BlueprintType)
-enum class EPostAction : uint8
-{
- Return, // 回到 Free
- Minigame, // v0.1 预留
- Dialogue, // v0.1 预留
- SceneChange // v0.1 预留
+ Wait UMETA(DisplayName = "等待"),
+ TurnToTarget UMETA(DisplayName = "转向交互物"),
+ PlayMontage UMETA(DisplayName = "播放人物动画"),
+ PlayMontageOnTarget UMETA(DisplayName = "播放交互物动画"),
+ CameraPush UMETA(DisplayName = "相机推镜"),
+ CameraReturn UMETA(DisplayName = "相机回位"),
+ Consume UMETA(DisplayName = "消耗交互物"),
+ HideActor UMETA(DisplayName = "隐藏交互物"),
+ ShowActor UMETA(DisplayName = "显示交互物"),
+ AddToBackpack UMETA(DisplayName = "物品进背包"),
+ ShowNarrative UMETA(DisplayName = "显示叙事文本"),
+ ShowPickupNotification UMETA(DisplayName = "显示拾取通知"),
+ ToggleDoor UMETA(DisplayName = "切换门状态"),
+ PlayDialogue UMETA(DisplayName = "播放对话"),
+ PlayStory UMETA(DisplayName = "播放小剧情"),
+ BroadcastEvent UMETA(DisplayName = "广播逻辑事件"),
};
```
-### 配置数据结构
+### FInteractAction — 原子操作定义
+
+```cpp
+USTRUCT(BlueprintType)
+struct FInteractAction
+{
+ GENERATED_BODY()
+
+ // === 编排 ===
+
+ UPROPERTY(EditAnywhere, Category = "Action")
+ EInteractActionType Type = EInteractActionType::Wait;
+
+ /** 相对于 Effect 开始的触发时间(秒) */
+ UPROPERTY(EditAnywhere, Category = "Action", meta = (ClampMin = "0"))
+ float TriggerTime = 0.f;
+
+ // === 各类型参数(按 EditCondition 折叠显示)===
+
+ // --- Wait ---
+ UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::Wait", EditConditionHides))
+ float WaitDuration = 0.5f;
+
+ // --- TurnToTarget ---
+ UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::TurnToTarget", EditConditionHides))
+ float TurnDuration = 0.2f;
+
+ // --- PlayMontage ---
+ UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::PlayMontage", EditConditionHides))
+ TObjectPtr Montage;
+
+ // --- PlayMontageOnTarget ---
+ UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::PlayMontageOnTarget", EditConditionHides))
+ TObjectPtr TargetMontage;
+
+ // --- CameraPush ---
+ UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::CameraPush", EditConditionHides))
+ float PushDistance = 80.f;
+ UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::CameraPush", EditConditionHides))
+ float PushDuration = 0.5f;
+
+ // --- CameraReturn ---
+ UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::CameraReturn", EditConditionHides))
+ float ReturnDuration = 0.3f;
+
+ // --- ShowNarrative ---
+ UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::ShowNarrative", EditConditionHides))
+ TArray NarrativeTexts;
+};
+```
+
+### FInteractEffect — Effect 定义
+
+```cpp
+USTRUCT(BlueprintType)
+struct FInteractEffect
+{
+ GENERATED_BODY()
+
+ /** Effect 名称(调试用) */
+ UPROPERTY(EditAnywhere, Category = "Effect")
+ FString EffectName;
+
+ /** Effect 总时长(秒),到时自动结束 */
+ UPROPERTY(EditAnywhere, Category = "Effect", meta = (ClampMin = "0.1", ClampMax = "10.0"))
+ float TotalDuration = 2.0f;
+
+ /** 是否可取消(ESC 中断) */
+ UPROPERTY(EditAnywhere, Category = "Effect")
+ bool bCancellable = true;
+
+ /** Action 列表 — 按 TriggerTime 编排,同时间并行 */
+ UPROPERTY(EditAnywhere, Category = "Effect")
+ TArray Actions;
+};
+```
+
+### FInteractableData — 简化后的配置
```cpp
-// 可交互物配置数据
USTRUCT(BlueprintType)
struct FInteractableData
{
GENERATED_BODY()
// === 基本信息 ===
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
- FName ItemId; // 唯一ID(如 "fridge_home")
+ UPROPERTY(EditAnywhere, Category = "Basic")
+ FName ItemId;
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
- FString DisplayName; // 显示名称(如 "旧冰箱")
-
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
- EInteractType Type = EInteractType::Examine;
-
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic",
- meta = (EditCondition = "Type == EInteractType::Operate", EditConditionHides))
- EOperateSubType OperateSubType = EOperateSubType::Door;
+ UPROPERTY(EditAnywhere, Category = "Basic")
+ FString DisplayName;
// === 触发 ===
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger")
- float TriggerRadius = 150.0f; // 触发半径(cm),默认 1.5m
+ UPROPERTY(EditAnywhere, Category = "Trigger", meta = (ClampMin = "50.0", ClampMax = "500.0"))
+ float TriggerRadius = 150.f;
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger")
- bool bOneShot = false; // 一次性消耗
+ UPROPERTY(EditAnywhere, Category = "Trigger")
+ bool bOneShot = false;
// === 提示文本 ===
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt")
- FString InteractVerb; // 动作词(如 "查看"、"拾取"、"打开")
+ UPROPERTY(EditAnywhere, Category = "Prompt")
+ FString InteractVerb;
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt",
- meta = (EditCondition = "Type == EInteractType::Operate && OperateSubType == EOperateSubType::Door"))
- FString InteractVerbAlt; // 备选动词(如 "关上",门打开时使用)
+ UPROPERTY(EditAnywhere, Category = "Prompt")
+ FString InteractVerbAlt; // 门关闭/打开时使用
- // === 叙事/对话文本 ===
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Text")
- TArray NarrativeTexts; // 文本列表,随机选取,不重复上次
+ // === 交互 Effect ===
+ UPROPERTY(EditAnywhere, Category = "Effect")
+ FInteractEffect InteractEffect;
- // === 摄像机推镜 ===
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera")
- bool bUseCameraPush = false;
-
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
- meta = (EditCondition = "bUseCameraPush"))
- float CameraPushDistance = 80.0f; // 推镜距离(cm)
-
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
- meta = (EditCondition = "bUseCameraPush"))
- float CameraPushDuration = 0.5f;
-
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
- meta = (EditCondition = "bUseCameraPush"))
- float CameraReturnDuration = 0.3f;
-
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
- meta = (EditCondition = "bUseCameraPush"))
- float PushWaitDelay = 0.2f; // 推镜完成后到文本显示的等待(秒)
-
- // === 动画 ===
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim")
- UAnimMontage* InteractMontage = nullptr; // 交互动画蒙太奇(可选)
-
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim")
- float AnimDuration = 1.0f; // 无蒙太奇时的动画持续时间(秒)
-
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim")
- float AnimEventTimepoint = 0.6f; // 动画事件触发点(0-1 比例)
-
- // === 结束动作 ===
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Post")
- EPostAction PostAction = EPostAction::Return;
+ // ❌ 已删除(由 Effect 内 Action 替代):
+ // EInteractType Type, EOperateSubType OperateSubType
+ // bUseCameraPush, CameraPushDistance/Duration, PushWaitDelay, CameraReturnDuration
+ // InteractMontage, AnimDuration, AnimEventTimepoint
+ // NarrativeTexts, EPostAction PostAction
};
```
-### 委托声明
+### FEffectContext — 运行时上下文
```cpp
-// 物品拾取/获得事件
+USTRUCT()
+struct FEffectContext
+{
+ TWeakObjectPtr Instigator;
+ TWeakObjectPtr TargetComp;
+ TWeakObjectPtr TargetActor;
+};
+```
+
+### 委托
+
+```cpp
+// 物品消耗事件(逻辑事件,Persistence/Backpack 订阅)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
FMLocalItemEvent,
FName, ItemId,
const FString&, DisplayName,
const FName&, Category
);
+
+// Effect 完成回调
+DECLARE_DELEGATE(FOnEffectComplete);
```
---
-## 6. 行为规则 → 实现方案映射
+## 6. 行为规则 → Effect 配置映射
-对执行文档中每条行为规则,逐一说明实现方案。
+执行文档中每条行为规则,现在通过 **预定义 Effect 配置** 实现。交互物实例在编辑器中配置 Effect,运行时由 EffectManager 执行。
-### 一、通用规则 — 触发条件
+### Effect 配置模板
-#### 规则 1: 距离 ≤ 触发半径 + Free 状态 + 未消耗 → 可触发
+#### 模板 A:观察类 Effect
-**实现**:`UInteractableComponent` 的 `USphereComponent` 碰撞处理注册/注销。`BeginInteract()` 检查三个前置条件。
+对应执行案规则 E1-E6。
-```cpp
-void UInteractionManager::BeginInteract()
-{
- if (State != EInteractionState::Free) return;
- if (!bIsInDetectionRange) return;
- if (!CurrentInteractable || CurrentInteractable->IsConsumed()) return;
- // ... 进入交互流程
-}
+```
+Effect: "观察物品"
+TotalDuration: 3.0s, Cancellable: true
+
+TriggerTime | Action | 参数
+----------- | ------------- | ----
+0.0s | TurnToTarget | TurnDuration=0.2
+0.0s | CameraPush | PushDistance=90, PushDuration=0.5
+0.7s | ShowNarrative | Texts=["冰箱嗡嗡响。里面有冰棒!"]
+0.7s | (等待用户输入,ShowNarrative 持续显示)
```
-**可调参数**:TriggerRadius(每个交互物独立配置,默认 150cm,范围 100-300cm)
+> ShowNarrative Action 执行后进入"等待用户输入"状态,用户按 E/ESC 后 Effect 继续。
+> 此行为通过在 ShowNarrative Action 中标记 `bWaitForInput=true` 实现(后续版本扩展)。
+> v0.1 简化:ShowNarrative 不等待,用 Effect TotalDuration 控制总时长。
----
+#### 模板 B:拾取类 Effect
-#### 规则 2-5: 提示 UI 显示/隐藏
+对应执行案规则 P1-P8。
-**实现**:
+```
+Effect: "拾取地面物品"
+TotalDuration: 1.5s, Cancellable: false
-- **进入范围**:`UInteractableComponent::OnDetectionOverlapBegin` → 调用 `Character->GetInteractionManager()->RegisterInteractable(this)` → InteractionManager 添加到 NearbyInteractables 并排序 → 更新选中 → 显示提示 UI(淡入 0.2s)
-- **提示格式**:`GetPromptText()` 返回 `"[E] " + InteractVerb + " " + DisplayName`;门类型根据 bDoorIsOpen 选择 InteractVerb 或 InteractVerbAlt
-- **提示位置**:每帧 `ProjectWorldLocationToScreen(InteractableLocation + Z偏移)` 更新 Widget 位置
-- **离开范围**:`OnDetectionOverlapEnd` → `UnregisterInteractable` → 若为当前选中则切换到下一个;若列表空则淡出提示
-
-```cpp
-FString UInteractableComponent::GetPromptText() const
-{
- FString Verb = Data.InteractVerb;
- if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
- {
- Verb = bDoorIsOpen ? Data.InteractVerbAlt : Data.InteractVerb;
- }
- return FString::Printf(TEXT("[E] %s %s"), *Verb, *Data.DisplayName);
-}
+TriggerTime | Action | 参数
+----------- | --------------------- | ----
+0.0s | TurnToTarget | TurnDuration=0.2
+0.0s | PlayMontage | Montage=AM_Pickup
+0.6s | Consume |
+0.6s | HideActor |
+0.6s | AddToBackpack |
+1.0s | ShowPickupNotification|
```
-**可调参数**:PromptFadeInDuration (0.2s, 0.1-0.5s), PromptFadeOutDuration (0.2s, 0.1-0.5s), PromptHeightOffset (交互物上方偏移)
+#### 模板 C:门操作 Effect
----
+对应执行案规则 D1-D6。
+
+```
+Effect: "操作门"
+TotalDuration: 1.0s, Cancellable: false
+
+TriggerTime | Action | 参数
+----------- | ------------- | ----
+0.0s | TurnToTarget | TurnDuration=0.2
+0.0s | PlayMontage | Montage=AM_PushDoor
+0.3s | ToggleDoor |
+```
+
+#### 模板 D:翻找类 Effect
+
+对应执行案规则 S1-S7。
+
+```
+Effect: "翻找废纸箱"
+TotalDuration: 3.5s, Cancellable: false
+
+TriggerTime | Action | 参数
+----------- | --------------------- | ----
+0.0s | TurnToTarget | TurnDuration=0.2
+0.0s | CameraPush | PushDistance=60, PushDuration=0.5
+0.1s | PlayMontage | Montage=AM_Search
+1.8s | Consume |
+1.8s | AddToBackpack |
+1.9s | CameraReturn | ReturnDuration=0.3
+2.2s | ShowNarrative | Texts=["在废纸箱里翻到了..."]
+3.2s | ShowPickupNotification|
+```
+
+#### 模板 E:对话类 Effect
+
+对应执行案规则 K1-K6。
+
+```
+Effect: "NPC对话"
+TotalDuration: 4.0s, Cancellable: true
+
+TriggerTime | Action | 参数
+----------- | ------------- | ----
+0.0s | TurnToTarget | TurnDuration=0.2
+0.0s | CameraPush | PushDistance=90, PushDuration=0.5
+0.7s | ShowNarrative | Texts=["zzz"]
+```
+
+### 通用规则实现
+
+#### 规则 1: 触发条件
+
+**实现**:`UInteractableComponent::OnDetectionOverlapBegin` → `GetGI()->GetMgr()->RegisterInteractable(this)`。Manager 的 `BeginInteract()` 检查 State==Free、SelectedIndex 有效、!IsConsumed()。
+
+#### 规则 2-5: Prompt UI 显示/隐藏
+
+**实现**:`UInteractionPromptComponent` 每帧 Tick 中:
+- 读 `Manager->GetState()` == Free → 显示
+- 读 `Manager->HasSelection()` → 有选中?
+- 读 `Manager->GetSelectedPromptText()` + `GetSelectedWorldPosition()` → 更新 Widget
+- State 变为 Executing → 隐藏
+- **不直接访问任何 Component**,所有数据来自 Manager 缓存
+
+Manager 在 `RegisterInteractable` / `UnregisterInteractable` / `SwitchTarget` 中调用 `RefreshCachedSelection()` 从 Component 读取并缓存。
#### 规则 6-10: 多交互物选择与切换
-**实现**:
+**实现**:同 v0.1,但 Manager 在 GameInstance 中。`RegisterInteractable` / `UnregisterInteractable` / `SwitchTarget` / `UpdateSelection` 逻辑不变。
-- **默认选中最近**:`UpdateSelection()` 按 `FVector::DistSquared2D(CharacterFoot, InteractableLocation)` 排序,`SelectedIndex = 0`
-- **距离判定点**:角色脚底中心(`GetActorLocation()`,已在地面)到交互物注册点的水平 2D 距离
-- **Tab/滚轮切换**:`SwitchTarget(Direction)` 调整 SelectedIndex(循环),`Direction = +1` (下一个) / `-1` (上一个)
-- **切换时 UI 立即更新**:SwitchTarget 内直接调用 `UpdatePromptUI()` 设置新内容
+#### 规则 11-14: 交互触发与锁定
+
+**实现**:Character 输入回调中协调两个系统:
```cpp
-void UInteractionManager::SwitchTarget(int32 Direction)
+void AChildHoodCharacter::OnInteract()
{
- if (NearbyInteractables.Num() == 0) return;
- if (State != EInteractionState::Free) return; // Interacting 时忽略
- SelectedIndex = (SelectedIndex + Direction + NearbyInteractables.Num()) % NearbyInteractables.Num();
- UpdatePromptUI();
+ auto* Driver = ASystemDriver::Get(GetWorld());
+ auto& Mgr = Driver->GetInteractionManager();
+ auto& EFM = Driver->GetEffectManager();
+
+ // 1. Manager:能交互吗?选中的是谁?
+ auto* Selected = Mgr.BeginInteract();
+ if (!Selected) return;
+
+ SetInteracting(true);
+
+ // 2. EffectManager:执行 Effect
+ FEffectContext Ctx;
+ Ctx.Instigator = this;
+ Ctx.TargetComp = Selected;
+ Ctx.TargetActor = Selected->GetOwner();
+ EFM.PlayEffect(Selected->GetInteractableData().InteractEffect, Ctx);
}
```
----
+`BeginInteract()` 返回选中的 Component(或 nullptr 拒绝),设置 `State = Executing`。Effect 执行期间所有输入被忽略(CancelInteract 检查 bCancellable)。
-#### 规则 11-14: 交互触发与锁定规则
+Effect 完成通过 Driver 中 `std::function` 连线:`EffectManager.OnEffectComplete → InteractionManager.OnInteractionFinished()`。
-**实现**:
+#### 规则 15-16: 交互结束
-- **E 键触发**:PlayerController 的 IA_Interact 回调 → `IM->BeginInteract()`
-- **Interacting 状态下**:`Char->SetInteracting(true)` → `DoMove()` 中 `if (bIsInteracting) return;` 屏蔽移动
-- **Tab/滚轮忽略**:`SwitchTarget()` 开头检查 `if (State != EInteractionState::Free) return;`
-- **E/ESC 在 AnimLock 时忽略**:`CancelInteract()` 检查 `if (Phase == EInteractPhase::AnimLock) return;`;AnimLock 中 `BeginInteract()` 因 State != Free 也被拒绝
+**实现**:`EffectManager::FinishEffect()` → `Manager->OnEffectComplete()` → `State = Free` + `Char->SetInteracting(false)`。Prompt 组件检测到 Free 后恢复显示。
----
+### 边界情况实现
-#### 规则 15-16: 交互结束规则
+#### B1: AnimLock 期间所有按键忽略
-**实现**:
+**实现**:Effect 的 `bCancellable = false` 时,`CancelInteract()` 被拒绝。拾取和门操作 Effect 均为不可取消。
-- **结束回到 Free**:Exit 阶段完成后 `State = Free`
-- **检测标记恢复**:回到 Free 后检查 `NearbyInteractables.Num() > 0`,若有则立即更新提示 UI
-- **oneShot 消耗**:`Consume()` 设置 `bIsConsumed = true`,注销检测区域(`DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision)`),从 NearbyInteractables 中移除
-- **跨场景持久化**:`Consume()` 同时调用 `GameInstance->MarkItemCollected(ItemId)` 写入持久层,确保场景切换后仍可恢复已消耗状态
+#### B2: oneShot 消耗后范围失效
----
+**实现**:`Consume()` 设置 `bIsConsumed = true`,禁用 DetectionSphere。UnregisterInteractable 触发时,Manager 检查 `State == Executing` 且移除的是 `CurrentInteractable` → 仅从 NearbyInteractables 移除,延迟清理 CurrentInteractable。
-#### 规则 17: 通用文本显示 UI
-
-**实现**:`WBP_NarrativeText` UMG Widget。
-
-- **位置**:Anchors 设为屏幕中心偏下(Vertical: 0.6, Horizontal: 0.5),通过 Padding 微调
-- **底框**:Border 组件,半透明黑色背景(默认 60% 透明度),圆角,尺寸随文字内容自适应
-- **文字**:TextBlock,白色,居中
-- **显示方式**:整句设置 Text,触发 FadeIn 动画(0.2s)
-- **淡出**:触发 FadeOut 动画(0.2s),完成后隐藏
-
-**可调参数**:TextBgOpacity (60%, 40-80%), TextFadeIn (0.2s, 0.1-0.3s), TextFadeOut (0.2s, 0.1-0.3s), TextVerticalOffset (屏幕高度 10%, 5-20%)
-
----
-
-### 二、观察类(Examine)
-
-#### 规则 E1-E3: 转向 + 推镜(同时开始)+ 等待
-
-**实现**:Enter 阶段 Tick 中并行处理:
-
-```cpp
-void UInteractionManager::TickEnterPhase(float DeltaTime)
-{
- // 1. 转向插值
- TurnElapsed += DeltaTime;
- FRotator TargetRot = CalculateTurnRotation(CurrentInteractable);
- float TurnAlpha = FMath::Clamp(TurnElapsed / TurnDuration, 0.f, 1.f);
- ACharacter* Char = Cast(GetOwner());
- Char->SetActorRotation(FMath::RInterpTo(Char->GetActorRotation(), TargetRot, DeltaTime, 1.f/TurnDuration));
-
- // 2. 推镜(一次性发起,SetViewTargetWithBlend 自带缓动)
- if (CurrentInteractable->GetInteractableData().bUseCameraPush && !bPushStarted)
- {
- FVector PushPos = CalculatePushPosition(CurrentInteractable);
- PushCamera(PushPos, CurrentInteractable->GetInteractableData().CameraPushDuration);
- bPushStarted = true;
- }
-
- // 3. 判断 Enter 阶段完成(转向完成 + 推镜已发起)
- bool bTurnComplete = TurnElapsed >= TurnDuration;
- bool bPushComplete = !CurrentInteractable->GetInteractableData().bUseCameraPush || bPushStarted;
- if (bTurnComplete && bPushComplete)
- {
- // 推镜后需要额外等待
- if (CurrentInteractable->GetInteractableData().bUseCameraPush && CurrentInteractable->GetInteractableData().PushWaitDelay > 0.f)
- {
- WaitElapsed += DeltaTime;
- if (WaitElapsed >= CurrentInteractable->GetInteractableData().PushWaitDelay)
- TransitionFromEnter();
- }
- else
- {
- TransitionFromEnter();
- }
- }
-}
-```
-
-**推镜目标计算**:
-
-```cpp
-FVector UInteractionManager::CalculatePushPosition(UInteractableComponent* Interactable)
-{
- const FInteractableData& Data = Interactable->GetInteractableData();
- FVector InteractPos = Interactable->GetWorldLocation();
- FVector CharPos = GetOwner()->GetActorLocation();
- FVector Dir = (InteractPos - CharPos).GetSafeNormal2D();
- // 交互物前方(面向角色的一侧),推镜距离
- FVector TargetPos = InteractPos - Dir * Data.CameraPushDistance;
- // 高度略低于交互物中心(微仰视)
- TargetPos.Z = InteractPos.Z * 0.8f;
- return TargetPos;
-}
-```
-
-**推镜执行**:
-
-```cpp
-void UInteractionManager::PushCamera(FVector Target, float Duration)
-{
- if (!PushCameraActor) return;
- FVector LookTarget = CurrentInteractable->GetWorldLocation();
- FRotator LookAtRot = (LookTarget - Target).Rotation();
- PushCameraActor->SetActorLocationAndRotation(Target, LookAtRot);
- APlayerController* PC = GetWorld()->GetFirstPlayerController();
- if (PC)
- {
- PC->SetViewTargetWithBlend(PushCameraActor, Duration, VTBlend_EaseOut);
- }
-}
-```
-
-**可调参数**:TurnDuration (0.2s, 0.1-0.4s), CameraPushDistance (80-100cm, 50-200cm), CameraPushDuration (0.5s, 0.3-1.0s), PushWaitDelay (0.2s, 0.1-0.5s)
-
----
-
-#### 规则 E4-E6: 文本显示 → 等待关闭 → 回位 → 可重复交互
-
-**实现**:
-
-- `TransitionFromEnter()` 对 Examine 类型:`Phase = Active`,调用 `ShowNarrativeText(随机文本)`
-- CancelInteract() 在 Active 阶段触发:`HideNarrativeText()` → `Phase = Exit` → `ReturnCamera(ReturnDuration, EaseIn)`
-- 可重复:Examine 默认 bOneShot = false,不调用 Consume()
-- 文本不重复上次:`PickRandomNarrative()` 排除 LastNarrativeText
-
-```cpp
-FString UInteractionManager::PickRandomNarrative(const TArray& Texts)
-{
- if (Texts.Num() == 0) return TEXT("");
- if (Texts.Num() == 1) return Texts[0];
- FString Candidate;
- do {
- Candidate = Texts[FMath::RandRange(0, Texts.Num() - 1)];
- } while (Candidate == LastNarrativeText && Texts.Num() > 1);
- LastNarrativeText = Candidate;
- return Candidate;
-}
-```
-
-**可调参数**:CameraReturnDuration (0.3s, 0.2-0.5s)
-
----
-
-### 三、拾取类(Pickup)
-
-#### 规则 P1-P8: 转向 → 动画(AnimLock) → 物品消失 + 通知 → 自动结束 → 一次性消耗
-
-**实现**:
-
-- **Enter**:仅转向,无推镜(`bUseCameraPush = false`)。转向完成 → `Phase = AnimLock`
-- **AnimLock**:播放蒙太奇(如有)或启动计时器。设置两个计时器:
-
-```cpp
-void UInteractionManager::EnterAnimLockPhase()
-{
- const FInteractableData& Data = CurrentInteractable->GetInteractableData();
- float TotalAnimTime = Data.InteractMontage ? Data.InteractMontage->GetPlayLength() : Data.AnimDuration;
-
- // 播放蒙太奇(如有)
- if (Data.InteractMontage)
- {
- ACharacter* Char = Cast(GetOwner());
- if (Char && Char->GetMesh())
- Char->GetMesh()->GetAnimInstance()->Montage_Play(Data.InteractMontage);
- }
-
- // 动画事件计时器(如 60% 时物品消失)
- float EventTime = TotalAnimTime * Data.AnimEventTimepoint;
- GetWorld()->GetTimerManager().SetTimer(AnimEventTimerHandle, this, &UInteractionManager::ExecuteAnimEvent, EventTime, false);
-
- // 动画结束计时器
- GetWorld()->GetTimerManager().SetTimer(AnimEndTimerHandle, this, &UInteractionManager::OnAnimLockComplete, TotalAnimTime, false);
-
- bAnimEventFired = false;
-}
-```
-
-- **ExecuteAnimEvent(60%)**:
- ```cpp
- void UInteractionManager::ExecuteAnimEvent()
- {
- bAnimEventFired = true;
- const FInteractableData& Data = CurrentInteractable->GetInteractableData();
- // Pickup: 物品消失 + 通知物品系统
- CurrentInteractable->Consume();
- if (AInteractableActor* Owner = Cast(CurrentInteractable->GetOwner()))
- Owner->SetActorHiddenInGame(true);
- OnItemPickedUp.Broadcast(Data.ItemId, Data.DisplayName, FName("杂物"));
- }
- ```
-- **OnAnimLockComplete**:Pickup 直接 `State = Free`(无 Active / Exit,因无摄像机/UI 需清理)
-- **不支持手动关闭**:AnimLock 阶段 CancelInteract() 被拒绝
-- **相机不动**:`bUseCameraPush = false`
-- **oneShot**:`Consume()` 已处理
-
-**可调参数**:AnimDuration (1.0s, 0.8-1.5s), AnimEventTimepoint (0.6, 0.5-0.7)
-
----
-
-### 四、操作类 — 门(Operate-Door)
-
-#### 规则 D1-D6: 转向 → 动画(AnimLock) → 门状态切换 → 自动结束 → 可重复交互
-
-**实现**:
-
-- **Enter**:仅转向,无推镜。转向完成 → `Phase = AnimLock`
-- **AnimLock**:播放推门蒙太奇或计时器。`AnimEventTimepoint = 0.4` 时门状态切换:
- ```cpp
- // ExecuteAnimEvent 中的 Door 分支
- if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
- {
- CurrentInteractable->ToggleDoor(); // bDoorIsOpen = !bDoorIsOpen
- // 持久化门状态到 GameInstance
- if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance())
- {
- GI->SetDoorOpen(Data.ItemId, CurrentInteractable->IsDoorOpen());
- }
- // 通知 Actor 处理门 Mesh 旋转
- if (AInteractableActor* Owner = Cast(CurrentInteractable->GetOwner()))
- Owner->OnInteractStarted(CurrentInteractable);
- }
- ```
-- **OnAnimLockComplete**:Door 直接 `State = Free`
-- **可重复**:Door 默认 bOneShot = false,不 Consume()
-- **提示文字变化**:`GetPromptText()` 根据 `bDoorIsOpen` 返回不同动词
-
-**可调参数**:AnimDuration (0.8s, 0.5-1.2s), AnimEventTimepoint (0.4, 0.3-0.5)
-
----
-
-### 五、操作类 — 翻找(Operate-Search)
-
-#### 规则 S1-S7: 转向 + 推镜 → 动画(AnimLock) → 结果判定 → 等待关闭 → 回位 → oneShot 不消失
-
-**实现**:
-
-- **Enter**:转向 + 推镜(`bUseCameraPush = true`,`CameraPushDistance = 60cm`)。转向+推镜完成 → `Phase = AnimLock`
-- **AnimLock**:播放翻找蒙太奇或计时器(`AnimDuration = 1.5s`)
-- **OnAnimLockComplete**:结果判定
- ```cpp
- // v0.1 废纸箱必定有物品
- CurrentInteractable->Consume(); // oneShot,但 Actor 不隐藏(视觉仍在)
- OnItemFound.Broadcast(Data.ItemId, Data.DisplayName, FName("杂物"));
- Phase = EInteractPhase::Active; // 等待玩家按 E/ESC
- // 有物品时不显示文本底框
- ```
-- **Active**:玩家按 E/ESC → `Phase = Exit` → 相机回位 → `State = Free`
-- **无物品分支(架构预留)**:`ShowNarrativeText("什么都没找到。")` → Active → Exit
-- **翻找后不消失**:`Consume()` 仅设置 bIsConsumed + 禁用检测,不调用 `SetActorHiddenInGame`
-
-**可调参数**:CameraPushDistance (60cm, 40-100cm), AnimDuration (1.5s, 1.0-2.0s)
-
----
-
-### 六、对话类(Talk)
-
-#### 规则 K1-K6: 转向 + 推镜 → 等待 → 文本 → 等待关闭 → 回位
-
-**实现**:与观察类流程完全一致,区别仅在文本来源(NPC 对话内容而非小孩想法)。
-
-- **Enter**:转向 + 推镜(`CameraPushDistance = 80-100cm`)+ 等待 0.2s
-- **Active**:`ShowNarrativeText(随机对话文本)` → 等待 E/ESC
-- **Exit**:淡出文本 → 相机回位 → Free
-- **可重复**:Talk 默认 bOneShot = false
-
----
-
-### 七、边界情况
-
-#### 规则 B1: AnimLock 期间所有按键忽略
-
-**实现**:`BeginInteract()` 检查 `State != Free` 直接返回;`CancelInteract()` 检查 `Phase == AnimLock` 直接返回;`SwitchTarget()` 检查 `State != Free` 直接返回
-
-#### 规则 B2: oneShot 消耗后范围失效
-
-**实现**:`Consume()` 内调用 `DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision)` + 从 NearbyInteractables 移除 + 若为当前选中则切换
-
-#### 规则 B3: 推镜期间角色不移动
-
-**实现**:`SetInteracting(true)` 已屏蔽 DoMove,覆盖此场景
-
-#### 规则 B4: 反复进出范围无累积副作用
-
-**实现**:`RegisterInteractable` 做去重检查(`if (NearbyInteractables.Contains(Interactable)) return;`),且提示 UI 始终只显示当前选中物体的一条
-
-#### 规则 B5: 切换+按 E 以切换后为准
-
-**实现**:`SwitchTarget()` 是同步操作,E 键回调在同帧或之后帧触发时,`CurrentInteractable` 已是切换后的新值
-
-#### 规则 B6: 恰好在边界 → 在范围内
-
-**实现**:SphereComponent Overlap 使用 `≤` 语义(UE5 默认行为),满足要求
-
----
-
-### 八、类型→阶段流转汇总
-
-> 汇总四种交互类型的完整阶段流转,供实现参考。
-
-| 类型 | Enter | AnimLock | Active | Exit | 备注 |
-|------|-------|----------|--------|------|------|
-| Examine | 转向 + 推镜 + 等待 | — | 显示文本, 等 E/ESC | 淡出 + 回位 | 可重复, 随机文本 |
-| Pickup | 转向 | 播放动画, 60%消失+通知 | — | — | 直接回 Free, oneShot |
-| Operate-Door | 转向 | 播放动画, 40%切换门 | — | — | 直接回 Free, 可重复 |
-| Operate-Search | 转向 + 推镜 | 播放动画 | 通知物品系统, 等 E/ESC | 回位 | oneShot 不消失 |
-| Talk | 转向 + 推镜 + 等待 | — | 显示文本, 等 E/ESC | 淡出 + 回位 | 可重复, 随机文本 |
+#### B3-B6: 同 v0.1 逻辑,不受架构变更影响。
---
@@ -921,15 +803,15 @@ void UInteractionManager::EnterAnimLockPhase()
| 资源类型 | 名称 | 数量 | 规格 | 触发时机/用途 |
|----------|------|------|------|---------------|
-| 动画蒙太奇 | AM_Pickup | 1 | 弯腰拾取,约 1.0s | Pickup 交互 AnimLock 阶段 |
-| 动画蒙太奇 | AM_PushDoor | 1 | 伸手推/拉门,约 0.8s | Door 交互 AnimLock 阶段 |
-| 动画蒙太奇 | AM_Search | 1 | 弯腰翻找,约 1.5s | Search 交互 AnimLock 阶段 |
-| UI Widget | WBP_InteractionPrompt | 1 | 轻量悬浮提示,含 TextBlock | 进入交互范围时显示 |
-| UI Widget | WBP_NarrativeText | 1 | 半透明底框 + TextBlock,带淡入淡出动画 | 观察/对话/翻找无结果时显示 |
+| 动画蒙太奇 | AM_Pickup | 1 | 弯腰拾取,约 1.0s | 拾取 Effect PlayMontage Action |
+| 动画蒙太奇 | AM_PushDoor | 1 | 伸手推/拉门,约 0.8s | 门操作 Effect PlayMontage Action |
+| 动画蒙太奇 | AM_Search | 1 | 弯腰翻找,约 1.5s | 翻找 Effect PlayMontage Action |
+| UI Widget | WBP_InteractionPrompt | 1 | 悬浮提示 + TextBlock | Prompt 组件 |
+| UI Widget | WBP_NarrativeText | 1 | 半透明底框 + TextBlock | ShowNarrative Action |
+| UI Widget | WBP_PickupNotification | 1 | 拾取通知弹窗 | ShowPickupNotification Action |
| 输入资产 | IA_Interact / IA_Cancel / IA_SwitchInteract | 3 | InputAction 资产 | 交互输入映射 |
-| 输入映射 | IMC_Default(追加绑定) | 1 | 在现有 IMC 中添加交互按键映射 | E/ESC/Tab/滚轮 |
-> v0.1 原型阶段:动画蒙太奇可用占位(简单 Character 动画或直接用计时器替代,系统已支持 Timer 模式)。UI Widget 使用简单 UMG 实现。交互物占位模型见场景执行案。
+> v0.1 原型:动画蒙太奇可用占位或 Timer 替代。UI Widget 使用简单 UMG。
---
@@ -937,79 +819,87 @@ void UInteractionManager::EnterAnimLockPhase()
| 风险 | 等级 | 说明 | 缓解措施 |
|------|------|------|---------|
-| SetViewTargetWithBlend 回位时机 | 🟡中 | Blend 开始后需要在准确时间点切换回 Free 状态,过早会导致相机跳切 | Exit 阶段设置与 CameraReturnDuration 等长的定时器,定时器到后再切 State=Free |
-| AnimMontage 缺失时兼容性 | 🟡中 | v0.1 可能没有完整动画资产,系统需在无蒙太奇时仍可运行 | 支持 Timer 模拟动画:AnimDuration 配置时长,计时器替代蒙太奇回调 |
-| 多交互物重叠时排序抖动 | 🟢低 | 距离接近时,角色微小移动可能导致选中物来回切换 | 使用 DistSquared2D 比较(避免 sqrt),v0.1 数量少暂不加滞后阈值 |
-| SphereComponent Overlap 性能 | 🟢低 | 10-20 个交互物同时检测,Tick 中排序 | v0.1 数量极少无性能压力;排序仅在列表变化时触发(Register/Unregister 时重排) |
-| 角色转向期间 Tick 精度 | 🟢低 | 低帧率时转向插值可能不精确 | 使用 RInterpTo 基于 DeltaTime,帧率无关 |
+| Consume 触发同步 EndOverlap | 🟡中 | Consume() 禁用碰撞同步触发 EndOverlap → UnregisterInteractable,可能清空 CurrentInteractable | Manager 的 UnregisterInteractable 在 Executing 状态下延迟清理 |
+| TurnToTarget 跨帧驱动 | 🟡中 | TurnToTarget Action 需要每帧 RInterpTo,不能一帧完成 | EffectManager.Update 中维护 TurnState,每帧驱动插值 |
+| EditCondition 折叠大量 Action 参数 | 🟢低 | FInteractAction 一个 struct 含所有 Action 类型参数,编辑器中按类型折叠 | 使用 EditConditionHides 自动隐藏不相关字段 |
+| ShowNarrative 等待用户输入 | 🟡中 | 观察/对话类需要等用户按 E/ESC,不能靠 TotalDuration 硬控 | v0.1 用 TotalDuration 硬控;后续版本给 ShowNarrative 加 bWaitForInput 标记,Effect 暂停 Timeline 等待 |
+| AInfo 驱动器生命周期 | 🟢低 | 关卡加载/卸载时需正确 Spawn/销毁 | OnWorldReady 中 Spawn,关卡卸载时 World 销毁自动清理 |
### 注意事项
-1. **Tick 开关优化**:State == Free 且 NearbyInteractables 为空时,关闭 TickComponent(`SetComponentTickEnabled(false)`)。注册首个交互物时开启。
-2. **推镜摄像机生命周期**:APushCameraActor 在 InteractionManager::BeginPlay 中 Spawn,随 Character 生命周期,不反复创建/销毁。
-3. **委托通知时机**:OnItemPickedUp / OnItemFound 在 ExecuteAnimEvent() 中广播,此时动画尚未结束。物品系统和收集系统需处理"动画未结束但已通知"的情况(v0.1 物品系统尚未实现,此通知仅作为架构预留)。
-4. **门状态持久化**:门状态通过 `UChildHoodGameInstance` 持久化。每次开关门时写入 `GI->SetDoorOpen()`,`InteractableComponent::RestorePersistentState()` 在 BeginPlay 时读取恢复。v0.1 单场景下等效于首次加载时所有门为关闭状态。
-5. **NPC 对话简化**:v0.1 Talk 类型在架构上与 Examine 共用 Active 阶段逻辑,仅文本来源不同。后续版本加入对话树时需扩展 Active 阶段。
+1. **Manager 不持有 World 引用**:通过 `GetGameInstance()->GetWorld()` 获取,注意 World 可能为空(加载中)
+2. **Driver 是 Manager 容器**:`ASystemDriver` 持有 `FInteractionManager` 和 `FEffectManager`(普通 C++),显式控制创建顺序和 Tick 顺序,不依赖 UE Subsystem 生命周期
+3. **Manager 是普通 C++**:`FInteractionManager` / `FEffectManager` 不是 UObject,不参与 GC。所有 UObject 引用使用 `TWeakObjectPtr`,回调使用 `TFunction`
+4. **InteractionManager 无 Tick**:所有操作由碰撞事件、输入回调、EffectManager 完成回调驱动,无每帧逻辑
+5. **GameInstance 不持有交互系统**:只有 `UPersistenceSubsystem` 跨 World 存活,所有交互逻辑随 World 生灭
+4. **Prompt 组件独立**:不参与 Effect 执行,只负责 Free 状态下的 UI 追踪
---
## 9. 数据流
-### 配置参数流
+### Effect 配置流
```
-AInteractableActor (编辑器实例)
- → UInteractableComponent::Data (FInteractableData)
- → InteractionManager 运行时读取 Data 中的类型/推镜/动画配置
+编辑器中配置 FInteractableData (per-instance)
+ → FInteractEffect: Action 列表 + 编排参数
+ → 关卡运行时,Manager 读 Data.InteractEffect
+ → EffectManager.PlayEffect(Effect, Context) 执行
```
-### 运行时状态流
+### 运行时数据流
```
输入 (Enhanced Input)
- → PlayerController 回调
- → InteractionManager::BeginInteract / CancelInteract / SwitchTarget
- → 状态机驱动 (State / Phase)
- → 角色转向 (Tick 插值)
- → 摄像机推镜/回位 (SetViewTargetWithBlend)
- → 动画播放 (Montage_Play 或 Timer)
- → UI 显隐 (Widget 动画)
- → 物品系统通知 (委托广播)
+ → Character 输入回调
+ → ASystemDriver::Get(World)->InteractionManager.BeginInteract() // 返回选中 Component
+ → ASystemDriver::Get(World)->EffectManager.PlayEffect(Effect, Context) // 执行 Effect
+ → Timeline 驱动 Action 按 TriggerTime 触发
+ → 各 Action 直接操作 UE API / 调用系统接口
+
+Effect 完成(Driver 中 std::function 连线)
+ → EffectManager.OnEffectComplete()
+ → InteractionManager.OnInteractionFinished() → State = Free
+ → Prompt 组件检测到 Free → 恢复 Prompt
```
-### 检测注册流
+### Prompt 数据流(独立于 Effect,只读 Manager 缓存)
```
-角色移动 → 进入 SphereComponent 范围
- → InteractableComponent::OnDetectionOverlapBegin
- → InteractionManager::RegisterInteractable
- → NearbyInteractables 排序
- → 选中更新
- → 提示 UI 显示/切换
+PromptComponent::TickComponent
+ → Manager->GetState() → Free?
+ → Manager->HasSelection() → 有选中?
+ → Manager->GetSelectedPromptText() → 缓存的文本
+ → Manager->GetSelectedWorldPosition() → 缓存的世界坐标
+ → ProjectWorldToScreen → 屏幕坐标
+ → Widget->SetPromptText + SetScreenPosition
+```
+
+```
+Manager 缓存刷新(选择变化时触发):
+ RegisterInteractable / UnregisterInteractable / SwitchTarget
+ → RefreshCachedSelection()
+ → 从 Component 读取 PromptText + WorldPosition
+ → 写入 CachedSelected
```
---
## 10. 可调参数汇总
-| 参数 | C++ 属性 | 默认值 | 位置 |
-|------|---------|--------|------|
-| 触发半径 | `FInteractableData::TriggerRadius` | 150.0 cm | InteractableComponent 编辑器 |
-| 提示 UI 淡入时长 | `PromptFadeInDuration` | 0.2s | InteractionManager 构造函数 |
-| 提示 UI 淡出时长 | `PromptFadeOutDuration` | 0.2s | InteractionManager 构造函数 |
-| 角色转向时长 | `TurnDuration` | 0.2s | InteractionManager 构造函数 |
-| 推镜距离 | `FInteractableData::CameraPushDistance` | 80-100 cm | InteractableComponent 编辑器 |
-| 推镜时长 | `FInteractableData::CameraPushDuration` | 0.5s | InteractableComponent 编辑器 |
-| 推镜后等待 | `FInteractableData::PushWaitDelay` | 0.2s | InteractableComponent 编辑器 |
-| 相机回位时长 | `FInteractableData::CameraReturnDuration` | 0.3s | InteractableComponent 编辑器 |
-| 文本底框背景透明度 | `TextBgOpacity` | 0.6 | WBP_NarrativeText 蓝图 |
-| 文本底框淡入时长 | `TextFadeInDuration` | 0.2s | WBP_NarrativeText 蓝图 |
-| 文本底框淡出时长 | `TextFadeOutDuration` | 0.2s | WBP_NarrativeText 蓝图 |
-| 拾取动画时长 | `FInteractableData::AnimDuration` | 1.0s | InteractableComponent 编辑器 |
-| 物品消失时间点 | `FInteractableData::AnimEventTimepoint` | 0.6 | InteractableComponent 编辑器 |
-| 门操作动画时长 | `FInteractableData::AnimDuration` | 0.8s | InteractableComponent 编辑器 |
-| 门状态切换时间点 | `FInteractableData::AnimEventTimepoint` | 0.4 | InteractableComponent 编辑器 |
-| 翻找动画时长 | `FInteractableData::AnimDuration` | 1.5s | InteractableComponent 编辑器 |
+| 参数 | 位置 | 默认值 |
+|------|------|--------|
+| 触发半径 | `FInteractableData::TriggerRadius` | 150 cm |
+| Effect TotalDuration | `FInteractEffect::TotalDuration` | 2.0s |
+| Effect bCancellable | `FInteractEffect::bCancellable` | true |
+| Action TriggerTime | `FInteractAction::TriggerTime` | 0s |
+| 转向时长 | `FInteractAction::TurnDuration` | 0.2s |
+| 推镜距离 | `FInteractAction::PushDistance` | 80 cm |
+| 推镜时长 | `FInteractAction::PushDuration` | 0.5s |
+| 回位时长 | `FInteractAction::ReturnDuration` | 0.3s |
+| Prompt 高度偏移 | `UInteractionPromptComponent::PromptHeightOffset` | 80 cm |
+
+> 所有时间/距离参数均在 per-instance 的 Effect 配置中可调,不再硬编码。
---
@@ -1018,148 +908,119 @@ AInteractableActor (编辑器实例)
```
Source/ChildHood/
├── Interaction/
-│ ├── InteractionTypes.h # 枚举、结构体、委托定义
-│ ├── InteractionManager.h/.cpp # 交互状态机核心组件(含 GameInstance 写入调用)
-│ ├── InteractableComponent.h/.cpp # 可交互物组件(数据+检测+持久状态恢复)
-│ ├── InteractableActor.h/.cpp # 通用交互物 Actor 基类(BeginPlay 调用恢复)
-│ └── PushCameraActor.h/.cpp # 推镜用临时摄像机
-├── ChildHoodGameInstance.h/.cpp # 新增:跨关卡持久数据持有者
-├── ChildHoodCharacter.h/.cpp # 修改:添加交互支持
-└── ChildHoodPlayerController.h/.cpp # 修改:添加交互输入绑定
+│ ├── InteractionTypes.h # 枚举、结构体(FInteractAction/FInteractEffect 等)
+│ ├── InteractionManager.h/.cpp # FInteractionManager(普通C++,检测/选择/状态)
+│ ├── EffectManager.h/.cpp # FEffectManager(普通C++,Effect调度/Action执行)
+│ ├── PersistenceSubsystem.h/.cpp # UPersistenceSubsystem(跨场景持久化)
+│ ├── InteractableActor.h/.cpp # 通用交互物基类(干净,无门逻辑)
+│ ├── InteractableComponent.h/.cpp # 通用组件(数据+检测,无门逻辑)
+│ ├── DoorActor.h/.cpp # 门 Actor(Mesh 旋转)
+│ ├── DoorInteractableComponent.h/.cpp # 门组件(开关状态+动词切换)
+│ ├── InteractionPromptComponent.h/.cpp # Prompt UI 组件
+│ ├── InteractionPromptWidget.h/.cpp # Prompt Widget 基类
+│ ├── NarrativeTextWidget.h/.cpp # 文本 Widget 基类
+│ └── PushCameraActor.h/.cpp # 推镜相机 Actor
+├── SystemDriver.h/.cpp # ASystemDriver(AInfo,持有所有World级Manager)
+├── ChildHoodGameInstance.h/.cpp # 干净壳,不持有交互对象
+├── ChildHoodCharacter.h/.cpp # 持有 PromptComponent,通过 Driver 访问 Manager
+└── ChildHoodPlayerController.h/.cpp # 输入绑定
Content/
├── Blueprints/
-│ └── BP_Interactable.uasset # 可配置交互物蓝图(基于 AInteractableActor)
+│ └── BP_DoorFrame.uasset # 门蓝图(基于 ADoorActor)
├── UI/
-│ ├── WBP_InteractionPrompt.uasset # 交互提示 UI
-│ └── WBP_NarrativeText.uasset # 文本底框 UI
+│ ├── WBP_InteractionPrompt.uasset # 交互提示
+│ ├── WBP_NarrativeText.uasset # 文本底框
+│ └── WBP_PickupNotification.uasset # 拾取通知
├── Input/
-│ ├── IA_Interact.uasset # E 键交互
-│ ├── IA_Cancel.uasset # ESC 取消
-│ └── IA_SwitchInteract.uasset # Tab/滚轮切换
+│ ├── IA_Interact.uasset
+│ ├── IA_Cancel.uasset
+│ └── IA_SwitchInteract.uasset
└── Animations/
- ├── AM_Pickup.uasset # 弯腰拾取蒙太奇(v0.1 可选)
- ├── AM_PushDoor.uasset # 推门蒙太奇(v0.1 可选)
- └── AM_Search.uasset # 翻找蒙太奇(v0.1 可选)
+ ├── AM_Pickup.uasset
+ ├── AM_PushDoor.uasset
+ └── AM_Search.uasset
```
---
## 12. 跨场景持久化设计
-> UE5 的 `OpenLevel()`(非无缝跳转)会销毁整个旧 World,包括 Character 及其所有 UActorComponent。本节定义交互系统如何通过 `UChildHoodGameInstance` 实现跨场景数据持久化。
-
-### 架构原则
+### 架构
```
┌──────────────────────────────────────────┐
-│ UChildHoodGameInstance │ ← 跨关卡存活
-│ ├── CollectedItemIDs: TSet │ 交互物消耗状态
-│ └── DoorStates: TMap │ 门开关状态
+│ UPersistenceSubsystem │ ← GameInstanceSubsystem,跨关卡存活
+│ ├── CollectedItemIDs: TSet │
+│ └── DoorStates: TMap │
└──────────────────────────────────────────┘
↕ 读取/写入
┌──────────────────────────────────────────┐
-│ 场景中的 AInteractableActor │ ← 随场景加载/销毁
-│ └── BeginPlay → RestorePersistentState │ 查询并恢复状态
+│ UInteractableComponent::BeginPlay │ ← 随场景加载/销毁
+│ └── RestorePersistentState(Subsystem) │ 查询并恢复状态
└──────────────────────────────────────────┘
```
-### UChildHoodGameInstance 接口
+### UPersistenceSubsystem 接口
```cpp
UCLASS()
-class UChildHoodGameInstance : public UGameInstance
+class UPersistenceSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
-
public:
- // === 物品消耗状态 ===
bool IsItemCollected(FName ItemId) const;
void MarkItemCollected(FName ItemId);
-
- // === 门状态 ===
bool IsDoorOpen(FName DoorId) const;
void SetDoorOpen(FName DoorId, bool bIsOpen);
-
- // === 重置(新游戏时调用)===
void ResetAll();
-
private:
- TSet CollectedItemIDs;
- TMap DoorStates;
+ UPROPERTY() TSet CollectedItemIDs;
+ UPROPERTY() TMap DoorStates;
};
```
-### 恢复流程
-
-`AInteractableActor::BeginPlay()` 或 `UInteractableComponent::BeginPlay()` 中:
-
-```cpp
-void UInteractableComponent::RestorePersistentState(UChildHoodGameInstance* GI)
-{
- if (!GI) return;
-
- // oneShot 物品:已消耗 → 隐藏 + 禁用检测
- if (Data.bOneShot && GI->IsItemCollected(Data.ItemId))
- {
- bIsConsumed = true;
- if (USphereComponent* Sphere = DetectionSphere)
- Sphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
- if (AInteractableActor* Owner = Cast(GetOwner()))
- Owner->SetActorHiddenInGame(true);
- }
-
- // 门:恢复开关状态
- if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
- {
- bDoorIsOpen = GI->IsDoorOpen(Data.ItemId);
- // 通知 Actor 应用视觉状态
- if (AInteractableActor* Owner = Cast(GetOwner()))
- Owner->OnInteractStarted(this); // 复用已有的 Mesh 旋转逻辑
- }
-}
-```
-
### 写入时机
-| 事件 | 调用位置 | 写入方法 |
-|------|---------|---------|
-| oneShot 物品被消耗 | `UInteractableComponent::Consume()` | `GI->MarkItemCollected(ItemId)` |
-| 门开关状态变化 | `InteractionManager::ExecuteAnimEvent()` Door 分支 | `GI->SetDoorOpen(ItemId, bIsOpen)` |
+| 事件 | 触发 Action | 写入方法 |
+|------|------------|---------|
+| 物品消耗 | `Consume` Action | `Subsystem->MarkItemCollected(ItemId)` |
+| 门开关切换 | `ToggleDoor` Action | `Subsystem->SetDoorOpen(DoorId, bIsOpen)` |
-### v0.1 行为
+### 恢复流程
-- v0.1 为单场景原型,不存在跨场景切换
-- `UChildHoodGameInstance` 在 v0.1 中以空数据初始化,所有 `IsItemCollected()` 返回 false
-- 后续多场景版本只需在 Project Settings 中将 GameInstance Class 设为 `UChildHoodGameInstance`,无需修改交互系统代码
+`UInteractableComponent::RestorePersistentState`(基类):恢复物品消耗状态。
+`UDoorInteractableComponent::RestorePersistentState`(派生类):额外恢复门开关状态 + 通知 DoorActor 视觉旋转。
+
+恢复调用链:`Component::BeginPlay → GetGameInstance()->GetSubsystem() → RestorePersistentState()`
---
-## 工期预估与实现拆分
+## 13. 工期预估与实现拆分
### 工作量估算
| 维度 | 估算 |
|------|------|
-| 总工期 | 5-7 天 |
-| 新增 C++ 代码 | 600-800 行 |
-| 新增蓝图 | 3 个 UMG + 1 个 Actor BP |
-| 修改现有代码 | 约 50 行 |
-| 实现复杂度 | 中-高 |
+| 总工期 | 6-8 天 |
+| 新增 C++ 代码 | 800-1000 行 |
+| 新增蓝图 | 3 个 UMG + 门 BP |
+| 修改现有代码 | 约 100 行 |
### 实现拆分建议
-供步骤⑥(程序实现)参考的推荐拆分方式,每个子任务可独立验证:
+| 子任务 | 内容 | 预估 | 依赖 | 验证方式 |
+|--------|------|------|------|---------|
+| 13a | InteractionTypes 重写 + FInteractAction/FInteractEffect 结构体 | 0.5d | 无 | 编译通过 |
+| 13b | UPersistenceSubsystem + UChildHoodGameInstance 清理 | 0.5d | 13a | PIE 测试 Subsystem 读写 |
+| 13c | InteractableComponent 清理(无门逻辑)+ InteractableActor 清理 | 0.5d | 13a | 编译通过 |
+| 13d | DoorActor + DoorInteractableComponent | 0.5d | 13c | PIE 门旋转 + 状态持久化 |
+| 13e | ASystemDriver + FInteractionManager | 1d | 13b | PIE Driver 创建 Manager,检测日志验证 |
+| 13f | FEffectManager + Driver Tick | 0.5d | 13e | PIE EffectManager 通过 Driver Tick 验证 |
+| 13g | UEffectManager + Action 执行分发 | 1.5d | 13a,13e | PIE 拾取 Effect 完整执行 |
+| 13h | UInteractionPromptComponent + Character 集成 | 1d | 13e | PIE Prompt 显示/隐藏 |
+| 13i | 全部 Effect 配置 + 10 交互物场景验证 | 1d | 13g,13h | 跑完 v0.1 探索路线 |
+| 13j | 蓝图迁移 + 集成测试 | 0.5d | 13i | 全流程回归 |
-| 子任务 | 内容 | 预估时间 | 依赖 | 验证方式 |
-|--------|------|---------|------|---------|
-| 6a. 数据类型定义 | InteractionTypes.h 枚举/结构体/委托 + InteractableComponent 基础结构 | 0.5 天 | 无 | 编译通过 |
-| 6b. InteractableComponent | USphereComponent 检测 + FInteractableData + Register/Unregister 逻辑 + AInteractableActor + BP_Interactable | 1 天 | 6a | PIE 中放置交互物,角色走近时控制台输出检测日志 |
-| 6c. InteractionManager 核心 | 状态机 + 检测列表排序 + 选中管理 + Character 集成(bIsInteracting) + InputAction 绑定 | 1.5 天 | 6a, 6b | PIE 中走近交互物,日志显示选中切换 |
-| 6d. 提示 UI | WBP_InteractionPrompt + 屏幕投影定位 + 淡入淡出 | 0.5 天 | 6c | PIE 中走近交互物看到提示,走近另一个时切换 |
-| 6e. Enter 阶段(转向 + 推镜) | TickEnterPhase + APushCameraActor + SetViewTargetWithBlend | 1 天 | 6c | PIE 按 E 后角色转向 + 相机推近 |
-| 6f. 类型处理器 + AnimLock | 四种类型的完整流程 + Timer/Montage 动画 + 动画事件 | 1.5 天 | 6e | PIE 验证四种交互类型各自完整流程 |
-| 6g. 文本 UI + Active/Exit | WBP_NarrativeText + ShowText/HideText + 相机回位 | 0.5 天 | 6f | PIE 观察类完整流程:推镜→文本→按E→回位 |
-| 6h. 跨系统委托 + 集成测试 | OnItemPickedUp/OnItemFound 委托 + 10 个交互物场景验证 | 0.5 天 | 6g | 按探索路线跑完全部交互物 |
-
-> **关键路径**:6a → 6b → 6c → 6e → 6f → 6g → 6h。6d(提示 UI)可在 6c 完成后并行。总关键路径约 5.5 天。
+> **关键路径**:13a → 13b → 13e → 13f → 13g → 13i → 13j。总关键路径约 5.5 天。
+> 13c/13d(门派生类)和 13h(Prompt)可在 13e 完成后并行。
diff --git a/docs/technical/交互系统-拾取时序图.md b/docs/technical/交互系统-拾取时序图.md
new file mode 100644
index 0000000..8b64ccd
--- /dev/null
+++ b/docs/technical/交互系统-拾取时序图.md
@@ -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
(普通C++ · ASystemDriver持有)
+ participant Comp as UInteractableComponent
(Data · 关卡实例)
+ participant Actor as AInteractableActor
+ participant EFM as FEffectManager
(普通C++ · ASystemDriver持有)
+ participant Persist as UPersistenceSubsystem
(Data · GameInstance)
+ participant Bag as BackpackSystem
+ participant Pres as UInteractionPromptComponent
(Presentation · Character)
+ participant Driver as ASystemDriver
(AInfo · 持有所有Manager)
+
+ rect rgb(230, 245, 255)
+ Note over Player,Driver: ═══ Phase 0 · 进入检测范围 ═══
+ Player->>Char: WASD 移动进入 TriggerRadius
+ Comp->>Mgr: DetectionSphere OnOverlapBegin
ASystemDriver::Get(World)->GetInteractionManager().RegisterInteractable(this)
+ Mgr->>Mgr: NearbyInteractables.Add(Comp)
Sort by Dist2D → SelectedIndex = 0
+ Mgr->>Mgr: RefreshCachedSelection()
缓存 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
只读 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)
Context = {Char, Comp, Actor}
+ Note right of EFM: Effect "拾取地面物品"
T=0.0 TurnToTarget + PlayMontage
T=0.6 Consume+Hide+Backpack
T=1.0 PickupNotification
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
DetectionSphere → NoCollision
+ Note right of Comp: 禁用碰撞同步触发 EndOverlap
→ UnregisterInteractable
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` 连接 EffectManager 完成回调到 Manager
+- `FInteractionManager` 纯事件驱动,无 Tick/Update
+- `UChildHoodGameInstance` 不持有任何交互系统对象,仅 `UPersistenceSubsystem` 跨 World 存活
+
+### TurnToTarget 跨帧插值
+
+TurnToTarget Action 不是瞬时完成的。EffectManager 在 Update 中维护 `FTurnState`(起始旋转、目标旋转、已用时间),每帧调用 `FMath::RInterpTo` 直到转向完成。多个 Action 可以并行执行。