8011cb6472
- P-008 交互系统技术设计审查通过,审查记录 docs/plans/reviews/P-008-review.md - D-001 关卡执行案审查通过,审查记录 docs/plans/reviews/D-001-review.md - 新增 AI 美术调研参考图(我的暑假/放学后少年/真实场景/美术风格) - 新增关卡执行案、白模搭建指南和搭建报告 - 完善工作流技能:质量门审查、Agent记忆、需求分析 - 更新交互系统和角色控制系统技术设计文档 - 清理过时参考文档
1045 lines
43 KiB
Markdown
1045 lines
43 KiB
Markdown
# 交互系统 技术设计文档(v0.1 原型)
|
||
|
||
> 对应执行案: `docs/design/prototype/interaction-system-execution.md` (v0.1.2)
|
||
> 关联系统: 角色控制系统、物品系统、收集系统
|
||
> 阶段: 原型
|
||
> 日期: 2026-06-07
|
||
|
||
---
|
||
|
||
## 概述
|
||
|
||
交互系统让玩家通过接近并按 E 键的方式与环境中的物体和 NPC 产生互动。v0.1 原型实现四种交互类型(观察/拾取/操作/对话),覆盖完整的层级状态机和类型分发逻辑。
|
||
|
||
**实现复杂度**:中-高
|
||
**预估工期**:5-7 天
|
||
**新增/修改代码量**:约 600-800 行 C++ + 3 个 UMG 蓝图
|
||
|
||
---
|
||
|
||
## 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 | 有蒙太奇时使用,无则计时器替代 |
|
||
|
||
### 自定义实现
|
||
|
||
以下功能点需要编写自定义 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 内置;需要自定义状态管理、跨系统协调或特殊逻辑 → 自定义实现。
|
||
|
||
---
|
||
|
||
## 2. 功能领域图
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
subgraph 玩家侧
|
||
Input[输入处理模块]
|
||
CharState[角色状态模块]
|
||
end
|
||
|
||
subgraph 交互核心
|
||
Detection[检测与选择模块]
|
||
StateMachine[交互状态机模块]
|
||
TypeHandler[类型处理器]
|
||
TurnCtrl[角色转向模块]
|
||
CameraCtrl[摄像机控制模块]
|
||
end
|
||
|
||
subgraph 可交互物侧
|
||
InteractableData[交互物数据模块]
|
||
Trigger[触发区域模块]
|
||
end
|
||
|
||
subgraph 表现层
|
||
PromptUI[提示 UI 模块]
|
||
TextUI[文本底框 UI 模块]
|
||
end
|
||
|
||
subgraph 下游系统
|
||
ItemSys[物品系统]
|
||
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
|
||
```
|
||
|
||
### 模块职责说明
|
||
|
||
| 模块 | 职责 | 关键输入 | 关键输出 |
|
||
|------|------|---------|---------|
|
||
| 输入处理模块 | 接收 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 AChildHoodPlayerController {
|
||
+SetupInputComponent() void
|
||
+OnInteractPressed() void
|
||
+OnCancelPressed() void
|
||
+OnSwitchInteract(float Direction) void
|
||
}
|
||
|
||
class UInteractionManager {
|
||
-EInteractionState State
|
||
-EInteractPhase Phase
|
||
-TArray~UInteractableComponent*~ NearbyInteractables
|
||
-int32 SelectedIndex
|
||
-UInteractableComponent* CurrentInteractable
|
||
-AActor* PushCameraActor
|
||
-UCameraComponent* PushCameraComp
|
||
-float TurnElapsed
|
||
-float WaitElapsed
|
||
-FTimerHandle AnimEventTimerHandle
|
||
-FTimerHandle AnimEndTimerHandle
|
||
-bool bAnimEventFired
|
||
-FString LastNarrativeText
|
||
+BeginInteract() void
|
||
+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
|
||
}
|
||
|
||
class UInteractableComponent {
|
||
-FInteractableData Data
|
||
-USphereComponent* DetectionSphere
|
||
-bool bIsConsumed
|
||
-bool bDoorIsOpen
|
||
+GetInteractableData() FInteractableData
|
||
+GetPromptText() FString
|
||
+IsConsumed() bool
|
||
+Consume() void
|
||
+ToggleDoor() void
|
||
+IsDoorOpen() bool
|
||
+GetWorldLocation() FVector
|
||
#OnDetectionOverlapBegin(UPrimitiveComponent*, AActor*) void
|
||
#OnDetectionOverlapEnd(UPrimitiveComponent*, AActor*) void
|
||
}
|
||
|
||
class FInteractableData {
|
||
+FName ItemId
|
||
+FString DisplayName
|
||
+EInteractType Type
|
||
+EOperateSubType OperateSubType
|
||
+float TriggerRadius
|
||
+bool bOneShot
|
||
+FString InteractVerb
|
||
+FString InteractVerbAlt
|
||
+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 : 聚合(引用)
|
||
AInteractableActor *-- UInteractableComponent : 组合
|
||
AInteractableActor *-- UStaticMeshComponent : 组合
|
||
UInteractableComponent *-- FInteractableData : 组合(内含)
|
||
UInteractableComponent o-- USphereComponent : 聚合(引用子对象)
|
||
```
|
||
|
||
### 关系类型说明
|
||
|
||
| 关系类型 | 含义 | 本项目中的体现 |
|
||
|---------|------|--------------|
|
||
| 组合 `*--` | 生命周期绑定 | Character 持有 InteractionManager;InteractableActor 持有 InteractableComponent |
|
||
| 聚合 `o--` | 引用但非拥有 | InteractionManager 引用 PushCameraActor;InteractableComponent 引用 DetectionSphere(子对象) |
|
||
| 关联 `-->` | 持久引用 | Character 关联 PlayerController;InteractionManager 引用 InteractableComponent 列表 |
|
||
|
||
### 新增类
|
||
|
||
| 类名 | 类型 | 职责 | 位置 |
|
||
|------|------|------|------|
|
||
| `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/` |
|
||
|
||
### 修改类
|
||
|
||
| 类名 | 修改内容 |
|
||
|------|---------|
|
||
| `AChildHoodCharacter` | 新增 `UInteractionManager*` 成员、`bIsInteracting` 标志、`SetInteracting()` / `GetInteractionManager()` 接口;`DoMove()` 添加交互中判断 |
|
||
| `AChildHoodPlayerController` | 新增 `IA_Interact` / `IA_Cancel` / `IA_SwitchInteract` InputAction 属性;`SetupInputComponent()` 添加交互输入绑定 |
|
||
|
||
### 新增蓝图
|
||
|
||
| 蓝图名 | 类型 | 用途 |
|
||
|--------|------|------|
|
||
| `WBP_InteractionPrompt` | UMG Widget | 交互提示 UI(`[E] 查看旧冰箱`),屏幕空间悬浮 |
|
||
| `WBP_NarrativeText` | UMG Widget | 文本底框 UI(半透明背景 + 一句话文字) |
|
||
| `BP_Interactable` | Actor Blueprint | 基于 AInteractableActor 的可配置交互物蓝图,每个场景交互物为其实例 |
|
||
|
||
---
|
||
|
||
## 4. 时序图
|
||
|
||
> ⚠️ 交互系统涉及 3+ 系统协调(交互→物品→收集),存在条件分支和类型分发,满足时序图绘制条件。
|
||
|
||
### 4.1 完整交互流程(以观察类为例)
|
||
|
||
```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
|
||
|
||
Note over IM: State: Free, 检测标记: true
|
||
|
||
Player->>PC: 按 E 键 (IA_Interact)
|
||
activate PC
|
||
PC->>IM: BeginInteract()
|
||
deactivate PC
|
||
|
||
activate IM
|
||
IM->>IM: State=Interacting, Phase=Enter
|
||
IM->>Char: SetInteracting(true)
|
||
IM->>UI: HidePrompt()
|
||
|
||
Note over IM: Phase: Enter — 转向 + 推镜
|
||
|
||
IM->>IM: Tick: RInterpTo 转向目标
|
||
IM->>PCA: SetActorLocation(TargetPushPos)
|
||
IM->>PC: SetViewTargetWithBlend(PCA, 0.5s, EaseOut)
|
||
|
||
Note over IM: 推镜完成后等待 0.2s
|
||
|
||
IM->>IM: Phase=Active, 显示叙事文本
|
||
IM->>IM: ShowNarrativeText(随机选取)
|
||
|
||
Note over IM: Phase: Active — 等待玩家关闭
|
||
|
||
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
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 核心数据结构
|
||
|
||
### 枚举
|
||
|
||
```cpp
|
||
// 交互类型
|
||
UENUM(BlueprintType)
|
||
enum class EInteractType : uint8
|
||
{
|
||
Examine, // 观察
|
||
Pickup, // 拾取
|
||
Operate, // 操作
|
||
Talk // 对话
|
||
};
|
||
|
||
// 操作子类型(仅 Operate 类型有效)
|
||
UENUM(BlueprintType)
|
||
enum class EOperateSubType : uint8
|
||
{
|
||
Door, // 门(开关切换)
|
||
Search // 翻找
|
||
};
|
||
|
||
// 交互系统顶层状态
|
||
UENUM(BlueprintType)
|
||
enum class EInteractionState : uint8
|
||
{
|
||
Free, // 自由状态
|
||
Interacting, // 交互中
|
||
SubSystem, // v0.1 预留
|
||
Cutscene // v0.1 预留
|
||
};
|
||
|
||
// Interacting 子阶段
|
||
UENUM(BlueprintType)
|
||
enum class EInteractPhase : 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 预留
|
||
};
|
||
```
|
||
|
||
### 配置数据结构
|
||
|
||
```cpp
|
||
// 可交互物配置数据
|
||
USTRUCT(BlueprintType)
|
||
struct FInteractableData
|
||
{
|
||
GENERATED_BODY()
|
||
|
||
// === 基本信息 ===
|
||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
|
||
FName ItemId; // 唯一ID(如 "fridge_home")
|
||
|
||
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, BlueprintReadWrite, Category = "Interact|Trigger")
|
||
float TriggerRadius = 150.0f; // 触发半径(cm),默认 1.5m
|
||
|
||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger")
|
||
bool bOneShot = false; // 一次性消耗
|
||
|
||
// === 提示文本 ===
|
||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt")
|
||
FString InteractVerb; // 动作词(如 "查看"、"拾取"、"打开")
|
||
|
||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt",
|
||
meta = (EditCondition = "Type == EInteractType::Operate && OperateSubType == EOperateSubType::Door"))
|
||
FString InteractVerbAlt; // 备选动词(如 "关上",门打开时使用)
|
||
|
||
// === 叙事/对话文本 ===
|
||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Text")
|
||
TArray<FString> NarrativeTexts; // 文本列表,随机选取,不重复上次
|
||
|
||
// === 摄像机推镜 ===
|
||
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;
|
||
};
|
||
```
|
||
|
||
### 委托声明
|
||
|
||
```cpp
|
||
// 物品拾取/获得事件
|
||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
|
||
FMLocalItemEvent,
|
||
FName, ItemId,
|
||
const FString&, DisplayName,
|
||
const FName&, Category
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 行为规则 → 实现方案映射
|
||
|
||
对执行文档中每条行为规则,逐一说明实现方案。
|
||
|
||
### 一、通用规则 — 触发条件
|
||
|
||
#### 规则 1: 距离 ≤ 触发半径 + Free 状态 + 未消耗 → 可触发
|
||
|
||
**实现**:`UInteractableComponent` 的 `USphereComponent` 碰撞处理注册/注销。`BeginInteract()` 检查三个前置条件。
|
||
|
||
```cpp
|
||
void UInteractionManager::BeginInteract()
|
||
{
|
||
if (State != EInteractionState::Free) return;
|
||
if (!bIsInDetectionRange) return;
|
||
if (!CurrentInteractable || CurrentInteractable->IsConsumed()) return;
|
||
// ... 进入交互流程
|
||
}
|
||
```
|
||
|
||
**可调参数**:TriggerRadius(每个交互物独立配置,默认 150cm,范围 100-300cm)
|
||
|
||
---
|
||
|
||
#### 规则 2-5: 提示 UI 显示/隐藏
|
||
|
||
**实现**:
|
||
|
||
- **进入范围**:`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);
|
||
}
|
||
```
|
||
|
||
**可调参数**:PromptFadeInDuration (0.2s, 0.1-0.5s), PromptFadeOutDuration (0.2s, 0.1-0.5s), PromptHeightOffset (交互物上方偏移)
|
||
|
||
---
|
||
|
||
#### 规则 6-10: 多交互物选择与切换
|
||
|
||
**实现**:
|
||
|
||
- **默认选中最近**:`UpdateSelection()` 按 `FVector::DistSquared2D(CharacterFoot, InteractableLocation)` 排序,`SelectedIndex = 0`
|
||
- **距离判定点**:角色脚底中心(`GetActorLocation()`,已在地面)到交互物注册点的水平 2D 距离
|
||
- **Tab/滚轮切换**:`SwitchTarget(Direction)` 调整 SelectedIndex(循环),`Direction = +1` (下一个) / `-1` (上一个)
|
||
- **切换时 UI 立即更新**:SwitchTarget 内直接调用 `UpdatePromptUI()` 设置新内容
|
||
|
||
```cpp
|
||
void UInteractionManager::SwitchTarget(int32 Direction)
|
||
{
|
||
if (NearbyInteractables.Num() == 0) return;
|
||
if (State != EInteractionState::Free) return; // Interacting 时忽略
|
||
SelectedIndex = (SelectedIndex + Direction + NearbyInteractables.Num()) % NearbyInteractables.Num();
|
||
UpdatePromptUI();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### 规则 11-14: 交互触发与锁定规则
|
||
|
||
**实现**:
|
||
|
||
- **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 也被拒绝
|
||
|
||
---
|
||
|
||
#### 规则 15-16: 交互结束规则
|
||
|
||
**实现**:
|
||
|
||
- **结束回到 Free**:Exit 阶段完成后 `State = Free`
|
||
- **检测标记恢复**:回到 Free 后检查 `NearbyInteractables.Num() > 0`,若有则立即更新提示 UI
|
||
- **oneShot 消耗**:`Consume()` 设置 `bIsConsumed = true`,注销检测区域(`DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision)`),从 NearbyInteractables 中移除
|
||
|
||
---
|
||
|
||
#### 规则 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<ACharacter>(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<FString>& 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<ACharacter>(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<AInteractableActor>(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
|
||
// 通知 Actor 处理门 Mesh 旋转
|
||
if (AInteractableActor* Owner = Cast<AInteractableActor>(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 | 淡出 + 回位 | 可重复, 随机文本 |
|
||
|
||
---
|
||
|
||
## 7. 美术资源需求
|
||
|
||
| 资源类型 | 名称 | 数量 | 规格 | 触发时机/用途 |
|
||
|----------|------|------|------|---------------|
|
||
| 动画蒙太奇 | 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,带淡入淡出动画 | 观察/对话/翻找无结果时显示 |
|
||
| 输入资产 | IA_Interact / IA_Cancel / IA_SwitchInteract | 3 | InputAction 资产 | 交互输入映射 |
|
||
| 输入映射 | IMC_Default(追加绑定) | 1 | 在现有 IMC 中添加交互按键映射 | E/ESC/Tab/滚轮 |
|
||
|
||
> v0.1 原型阶段:动画蒙太奇可用占位(简单 Character 动画或直接用计时器替代,系统已支持 Timer 模式)。UI Widget 使用简单 UMG 实现。交互物占位模型见场景执行案。
|
||
|
||
---
|
||
|
||
## 8. 技术风险与注意事项
|
||
|
||
| 风险 | 等级 | 说明 | 缓解措施 |
|
||
|------|------|------|---------|
|
||
| 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,帧率无关 |
|
||
|
||
### 注意事项
|
||
|
||
1. **Tick 开关优化**:State == Free 且 NearbyInteractables 为空时,关闭 TickComponent(`SetComponentTickEnabled(false)`)。注册首个交互物时开启。
|
||
2. **推镜摄像机生命周期**:APushCameraActor 在 InteractionManager::BeginPlay 中 Spawn,随 Character 生命周期,不反复创建/销毁。
|
||
3. **委托通知时机**:OnItemPickedUp / OnItemFound 在 ExecuteAnimEvent() 中广播,此时动画尚未结束。物品系统和收集系统需处理"动画未结束但已通知"的情况(v0.1 物品系统尚未实现,此通知仅作为架构预留)。
|
||
4. **门状态持久化**:v0.1 门状态仅保存在内存中(bDoorIsOpen),关卡重新加载后重置。不需要持久化到存档。
|
||
5. **NPC 对话简化**:v0.1 Talk 类型在架构上与 Examine 共用 Active 阶段逻辑,仅文本来源不同。后续版本加入对话树时需扩展 Active 阶段。
|
||
|
||
---
|
||
|
||
## 9. 数据流
|
||
|
||
### 配置参数流
|
||
|
||
```
|
||
AInteractableActor (编辑器实例)
|
||
→ UInteractableComponent::Data (FInteractableData)
|
||
→ InteractionManager 运行时读取 Data 中的类型/推镜/动画配置
|
||
```
|
||
|
||
### 运行时状态流
|
||
|
||
```
|
||
输入 (Enhanced Input)
|
||
→ PlayerController 回调
|
||
→ InteractionManager::BeginInteract / CancelInteract / SwitchTarget
|
||
→ 状态机驱动 (State / Phase)
|
||
→ 角色转向 (Tick 插值)
|
||
→ 摄像机推镜/回位 (SetViewTargetWithBlend)
|
||
→ 动画播放 (Montage_Play 或 Timer)
|
||
→ UI 显隐 (Widget 动画)
|
||
→ 物品系统通知 (委托广播)
|
||
```
|
||
|
||
### 检测注册流
|
||
|
||
```
|
||
角色移动 → 进入 SphereComponent 范围
|
||
→ InteractableComponent::OnDetectionOverlapBegin
|
||
→ InteractionManager::RegisterInteractable
|
||
→ NearbyInteractables 排序
|
||
→ 选中更新
|
||
→ 提示 UI 显示/切换
|
||
```
|
||
|
||
---
|
||
|
||
## 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 编辑器 |
|
||
|
||
---
|
||
|
||
## 11. 文件结构
|
||
|
||
```
|
||
Source/ChildHood/
|
||
├── Interaction/
|
||
│ ├── InteractionTypes.h # 枚举、结构体、委托定义
|
||
│ ├── InteractionManager.h/.cpp # 交互状态机核心组件
|
||
│ ├── InteractableComponent.h/.cpp # 可交互物组件(数据+检测)
|
||
│ ├── InteractableActor.h/.cpp # 通用交互物 Actor 基类
|
||
│ └── PushCameraActor.h/.cpp # 推镜用临时摄像机
|
||
├── ChildHoodCharacter.h/.cpp # 修改:添加交互支持
|
||
└── ChildHoodPlayerController.h/.cpp # 修改:添加交互输入绑定
|
||
|
||
Content/
|
||
├── Blueprints/
|
||
│ └── BP_Interactable.uasset # 可配置交互物蓝图(基于 AInteractableActor)
|
||
├── UI/
|
||
│ ├── WBP_InteractionPrompt.uasset # 交互提示 UI
|
||
│ └── WBP_NarrativeText.uasset # 文本底框 UI
|
||
├── Input/
|
||
│ ├── IA_Interact.uasset # E 键交互
|
||
│ ├── IA_Cancel.uasset # ESC 取消
|
||
│ └── IA_SwitchInteract.uasset # Tab/滚轮切换
|
||
└── Animations/
|
||
├── AM_Pickup.uasset # 弯腰拾取蒙太奇(v0.1 可选)
|
||
├── AM_PushDoor.uasset # 推门蒙太奇(v0.1 可选)
|
||
└── AM_Search.uasset # 翻找蒙太奇(v0.1 可选)
|
||
```
|
||
|
||
---
|
||
|
||
## 工期预估与实现拆分
|
||
|
||
### 工作量估算
|
||
|
||
| 维度 | 估算 |
|
||
|------|------|
|
||
| 总工期 | 5-7 天 |
|
||
| 新增 C++ 代码 | 600-800 行 |
|
||
| 新增蓝图 | 3 个 UMG + 1 个 Actor BP |
|
||
| 修改现有代码 | 约 50 行 |
|
||
| 实现复杂度 | 中-高 |
|
||
|
||
### 实现拆分建议
|
||
|
||
供步骤⑥(程序实现)参考的推荐拆分方式,每个子任务可独立验证:
|
||
|
||
| 子任务 | 内容 | 预估时间 | 依赖 | 验证方式 |
|
||
|--------|------|---------|------|---------|
|
||
| 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 天。
|