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 可以并行执行。