Files
ChildHood/docs/technical/交互系统-技术方案.md
T

48 KiB
Raw Blame History

交互系统 技术设计文档(v0.1)

对应执行案: docs/design/交互系统-执行案.md (v0.1.2) 关联系统: 角色控制系统、物品系统、收集系统、GameInstance 持久化层 日期: 2026-06-07(2026-06-11 更新:加入多场景持久化架构)


概述

交互系统让玩家通过接近并按 E 键的方式与环境中的物体和 NPC 产生互动。v0.1 原型实现四种交互类型(观察/拾取/操作/对话),覆盖完整的层级状态机和类型分发逻辑。

实现复杂度:中-高 预估工期:5-7 天 新增/修改代码量:约 600-800 行 C++ + 3 个 UMG 蓝图

跨场景持久化架构

本项目后续将支持多场景切换(如城郊 → 乡下)。UE5 的 OpenLevel() 会销毁整个旧 World,包括 Character 及其所有 UActorComponent。因此需要将持久数据与运行时行为分离:

  • 持久数据层:UChildHoodGameInstance(跨关卡存活),存储已收集物品 ID、门状态等
  • 行为层:UInteractionManager / UInteractableComponent(随 Character/场景销毁),仅负责运行时交互逻辑
  • 恢复机制:AInteractableActor::BeginPlay() 时查询 GameInstance,若物品已收集则隐藏并禁用交互

v0.1 为单场景原型,此架构以接口预留形式存在,不影响当前功能。详见 第 12 节:跨场景持久化设计。


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. 功能领域图

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. 类设计图

classDiagram
    class AChildHoodCharacter {
        -UInteractionManager* InteractionManager
        -bool bIsInteracting
        +SetInteracting(bool bValue) void
        +IsInteracting() bool
        +DoMove(float Right, float Forward)
        +GetInteractionManager() UInteractionManager*
    }

    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 {
        -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
        +RestorePersistentState(UChildHoodGameInstance*) void
        #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 : 聚合(引用子对象)
    UInteractableComponent ..> UChildHoodGameInstance : 恢复持久状态
    UInteractionManager ..> UChildHoodGameInstance : 写入消耗/门状态

关系类型说明

关系类型 含义 本项目中的体现
组合 *-- 生命周期绑定 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/

修改类

类名 修改内容
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 完整交互流程(以观察类为例)

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 拾取流程(跨系统通知)

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. 核心数据结构

枚举

// 交互类型
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 预留
};

配置数据结构

// 可交互物配置数据
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;
};

委托声明

// 物品拾取/获得事件
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
    FMLocalItemEvent,
    FName, ItemId,
    const FString&, DisplayName,
    const FName&, Category
);

6. 行为规则 → 实现方案映射

对执行文档中每条行为规则,逐一说明实现方案。

一、通用规则 — 触发条件

规则 1: 距离 ≤ 触发半径 + Free 状态 + 未消耗 → 可触发

实现:UInteractableComponent 的 USphereComponent 碰撞处理注册/注销。BeginInteract() 检查三个前置条件。

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 → 若为当前选中则切换到下一个;若列表空则淡出提示
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() 设置新内容
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 中移除
  • 跨场景持久化:Consume() 同时调用 GameInstance->MarkItemCollected(ItemId) 写入持久层,确保场景切换后仍可恢复已消耗状态

规则 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 中并行处理:

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();
        }
    }
}

推镜目标计算:

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;
}

推镜执行:

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
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:播放蒙太奇(如有)或启动计时器。设置两个计时器:
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%):
    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 时门状态切换:
    // ExecuteAnimEvent 中的 Door 分支
    if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
    {
        CurrentInteractable->ToggleDoor();  // bDoorIsOpen = !bDoorIsOpen
        // 持久化门状态到 GameInstance
        if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance<UChildHoodGameInstance>())
        {
            GI->SetDoorOpen(Data.ItemId, CurrentInteractable->IsDoorOpen());
        }
        // 通知 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)


规则 S1-S7: 转向 + 推镜 → 动画(AnimLock) → 结果判定 → 等待关闭 → 回位 → oneShot 不消失

实现:

  • Enter:转向 + 推镜(bUseCameraPush = true,CameraPushDistance = 60cm)。转向+推镜完成 → Phase = AnimLock
  • AnimLock:播放翻找蒙太奇或计时器(AnimDuration = 1.5s)
  • OnAnimLockComplete:结果判定
    // 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. 门状态持久化:门状态通过 UChildHoodGameInstance 持久化。每次开关门时写入 GI->SetDoorOpen(),InteractableComponent::RestorePersistentState() 在 BeginPlay 时读取恢复。v0.1 单场景下等效于首次加载时所有门为关闭状态。
  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          # 交互状态机核心组件(含 GameInstance 写入调用)
│   ├── InteractableComponent.h/.cpp       # 可交互物组件(数据+检测+持久状态恢复)
│   ├── InteractableActor.h/.cpp           # 通用交互物 Actor 基类(BeginPlay 调用恢复)
│   └── PushCameraActor.h/.cpp             # 推镜用临时摄像机
├── ChildHoodGameInstance.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 可选)

12. 跨场景持久化设计

UE5 的 OpenLevel()(非无缝跳转)会销毁整个旧 World,包括 Character 及其所有 UActorComponent。本节定义交互系统如何通过 UChildHoodGameInstance 实现跨场景数据持久化。

架构原则

┌──────────────────────────────────────────┐
│ UChildHoodGameInstance                   │  ← 跨关卡存活
│   ├── CollectedItemIDs: TSet<FName>      │     交互物消耗状态
│   └── DoorStates: TMap<FName, bool>      │     门开关状态
└──────────────────────────────────────────┘
          ↕ 读取/写入
┌──────────────────────────────────────────┐
│ 场景中的 AInteractableActor              │  ← 随场景加载/销毁
│   └── BeginPlay → RestorePersistentState │     查询并恢复状态
└──────────────────────────────────────────┘

UChildHoodGameInstance 接口

UCLASS()
class UChildHoodGameInstance : public UGameInstance
{
    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<FName> CollectedItemIDs;
    TMap<FName, bool> DoorStates;
};

恢复流程

AInteractableActor::BeginPlay() 或 UInteractableComponent::BeginPlay() 中:

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<AInteractableActor>(GetOwner()))
            Owner->SetActorHiddenInGame(true);
    }

    // 门:恢复开关状态
    if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
    {
        bDoorIsOpen = GI->IsDoorOpen(Data.ItemId);
        // 通知 Actor 应用视觉状态
        if (AInteractableActor* Owner = Cast<AInteractableActor>(GetOwner()))
            Owner->OnInteractStarted(this);  // 复用已有的 Mesh 旋转逻辑
    }
}

写入时机

事件 调用位置 写入方法
oneShot 物品被消耗 UInteractableComponent::Consume() GI->MarkItemCollected(ItemId)
门开关状态变化 InteractionManager::ExecuteAnimEvent() Door 分支 GI->SetDoorOpen(ItemId, bIsOpen)

v0.1 行为

  • v0.1 为单场景原型,不存在跨场景切换
  • UChildHoodGameInstance 在 v0.1 中以空数据初始化,所有 IsItemCollected() 返回 false
  • 后续多场景版本只需在 Project Settings 中将 GameInstance Class 设为 UChildHoodGameInstance,无需修改交互系统代码

工期预估与实现拆分

工作量估算

维度 估算
���工期 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 天。