# 交互系统技术设计文档(v0.1 原型) > 任务ID: P-003(部分) > 对应执行案: `docs/design/prototype/interaction-system-execution.md` > 关联系统: 角色控制、物品系统、收集系统 > 阶段: 原型 > 日期: 2026-06-06 --- ## 概述 交互系统是 v0.1 的核心体验系统,让玩家通过"走近并按键"与环境物体、NPC 产生互动。包含四种交互类型(观察/拾取/操作/对话),涉及状态机管理、摄像机推镜、UI 显示、跨系统通知等多个子系统。 **实现复杂度**:中-高 **预估工期**:5-7 天 **新增代码量**:约 800-1200 行 C++ + 2 个 UMG 蓝图 --- ## 类与组件架构 ### 新增类 | 类名 | 类型 | 职责 | 位置 | |------|------|------|------| | `IChildHoodInteractable` | UInterface | 交互物统一接口 | `Interaction/` | | `UInteractableComponent` | UActorComponent | 可附着到任意 Actor,提供交互数据与触发检测 | `Interaction/` | | `UInteractionManager` | UActorComponent | 交互状态机 + 检测 + 选择 + 摄像机推镜 + UI 管理 | `Interaction/` | ### 修改类 | 类名 | 修改内容 | |------|---------| | `AChildHoodCharacter` | 持有 InteractionManager,提供交互状态查询接口 | | `AChildHoodPlayerController` | 注册交互相关 IA(E/ESC/Tab/滚轮) | ### 新增蓝图 | 蓝图名 | 类型 | 用途 | |--------|------|------| | `WBP_InteractionPrompt` | UMG Widget | 交互提示 UI(`[E] 查看旧冰箱`) | | `WBP_InteractionText` | UMG Widget | 文本底框(半透明背景 + 文字) | | `BP_InteractableActor` | Actor Blueprint | 可交互物基类蓝图(带 InteractableComponent) | | `BP_PickupItem` | Actor Blueprint | 拾取物蓝图(继承 BP_InteractableActor) | | `BP_InteractableDoor` | Actor Blueprint | 门蓝图(继承 BP_InteractableActor,含开关状态) | | `BP_InteractableNPC` | Actor Blueprint | NPC 蓝图(继承 BP_InteractableActor) | ### 架构关系 ``` AChildHoodPlayerController ├── Enhanced Input │ ├── IA_Interact (E) ──────────┐ │ ├── IA_Cancel (ESC) ──────────┤ │ └── IA_SwitchInteract (Tab) ──┤ │ ▼ AChildHoodCharacter UInteractionManager ├── CapsuleComponent ├── 状态机 (Free/Interacting) ├── SkeletalMesh ├── 检测列表 (NearbyInteractables) ├── SpringArm + Camera ├── 当前选中 (SelectedInteractable) ├── UInteractionManager ◄─────────┤── 摄像机推镜控制 └── bIsInteracting 标志 ├── UI 管理 (Prompt + Text) └── 交互流程驱动 场景中的可交互物: AActor (任意 Actor) └── UInteractableComponent (实现 IChildHoodInteractable) ├── FInteractableData (配置数据) ├── USphereComponent (触发检测范围) └── UStaticMeshComponent (占位模型) ``` --- ## 核心数据结构 ### 枚举 ```cpp // 交互类型 UENUM(BlueprintType) enum class EInteractType : uint8 { Examine, // 观察 Pickup, // 拾取 Operate, // 操作 Talk // 对话 }; // 操作子类型 UENUM(BlueprintType) enum class EOperateSubType : uint8 { None, 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, // 活跃:等待玩家输入 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) FName ItemId; // 物品唯一ID(如 "fridge_home") UPROPERTY(EditAnywhere, BlueprintReadWrite) FString DisplayName; // 显示名称(如 "旧冰箱") UPROPERTY(EditAnywhere, BlueprintReadWrite) EInteractType Type = EInteractType::Examine; UPROPERTY(EditAnywhere, BlueprintReadWrite) EOperateSubType SubType = EOperateSubType::None; // 触发 UPROPERTY(EditAnywhere, BlueprintReadWrite) float TriggerRadius = 150.0f; // 触发半径(cm) UPROPERTY(EditAnywhere, BlueprintReadWrite) bool bOneShot = false; // 一次性消耗 // 提示文本 UPROPERTY(EditAnywhere, BlueprintReadWrite) FString InteractVerb; // 动作词(如 "查看"、"拾取") // 叙事文本 UPROPERTY(EditAnywhere, BlueprintReadWrite) TArray NarrativeTexts; // 文本列表,随机选取 // 摄像机推镜 UPROPERTY(EditAnywhere, BlueprintReadWrite) bool bUseCameraPush = false; // 是否推镜 UPROPERTY(EditAnywhere, BlueprintReadWrite) float CameraPushDistance = 100.0f; // 推镜距离(cm) UPROPERTY(EditAnywhere, BlueprintReadWrite) float CameraPushHeightOffset = -20.0f; // 推镜高度偏移(略低于物体中心) UPROPERTY(EditAnywhere, BlueprintReadWrite) float CameraPushDuration = 0.5f; // 推镜时长(秒) UPROPERTY(EditAnywhere, BlueprintReadWrite) float CameraReturnDuration = 0.3f; // 回位时长(秒) // 动画 UPROPERTY(EditAnywhere, BlueprintReadWrite) UAnimMontage* InteractionMontage = nullptr; // 交互动画 UPROPERTY(EditAnywhere, BlueprintReadWrite) float AnimEventProgress = 0.6f; // 动画中触发事件的进度点(0-1) // 物品(拾取/翻找) UPROPERTY(EditAnywhere, BlueprintReadWrite) FName RewardItemId; // 获得的物品ID UPROPERTY(EditAnywhere, BlueprintReadWrite) FString RewardItemCategory = TEXT("杂物"); // 物品品类 // 结束动作 UPROPERTY(EditAnywhere, BlueprintReadWrite) EPostAction PostAction = EPostAction::Return; }; ``` --- ## 行为规则 → 实现方案映射 ### 一、状态机实现(UInteractionManager) #### 状态机设计 ```cpp UCLASS() class UInteractionManager : public UActorComponent { GENERATED_BODY() // 当前状态 EInteractionState State = EInteractionState::Free; EInteractPhase Phase = EInteractPhase::None; // 检测 bool bInDetectionRange = false; TArray> NearbyInteractables; int32 SelectedIndex = 0; // 当前交互 UPROPERTY() UInteractableComponent* CurrentInteractable = nullptr; // 摄像机推镜 UPROPERTY() ACameraActor* PushCamera = nullptr; // 计时器 float PhaseTimer = 0.f; float TurnProgress = 0.f; // UI UPROPERTY() UUserWidget* PromptWidget = nullptr; UPROPERTY() UUserWidget* TextWidget = nullptr; }; ``` #### 状态转换逻辑 ``` Tick 流程: Free 状态: ├── 更新 NearbyInteractables(基于 Overlap 事件维护) ├── 按距离排序 ├── 更新 bInDetectionRange ├── 更新 PromptWidget(选中物的提示) └── 等 E 键触发 E 键触发: Free → Interacting/Enter ├── 保存当前摄像机状态 ├── 设置 bIsInteracting = true(通知角色控制) ├── 启动角色转向(0.2s) └── 如需推镜,启动推镜 Enter 阶段完成: Enter → AnimLock 或 Active(取决于交互类型) ├── 有动画 → AnimLock(等动画播完) └── 无动画 → Active(等文本显示 + 玩家输入) AnimLock 结束(动画播完): ├── 拾取类 → 执行物品逻辑 → Exit ├── 门操作 → 执行状态切换 → Exit └── 翻找类 → 结果判定 → Active 或 Exit Active 阶段(等 E/ESC): E/ESC → Exit Exit 阶段: ├── 淡出 UI ├── 摄像机回位 └── 完成后 → Free(检查 postAction,v0.1 全为 Return) ``` --- ### 二、通用规则 #### G1. 触发条件 **实现**:UInteractableComponent 持有 USphereComponent 作为触发范围 ```cpp // UInteractableComponent 初始化 TriggerSphere = CreateDefaultSubobject("TriggerSphere"); TriggerSphere->SetSphereRadius(Data.TriggerRadius); TriggerSphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly); TriggerSphere->SetCollisionResponseToAllChannels(ECR_Ignore); TriggerSphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); TriggerSphere->SetupAttachment(GetOwner()->GetRootComponent()); // Overlap 事件 → 通知 InteractionManager TriggerSphere->OnComponentBeginOverlap.AddDynamic(this, &UInteractableComponent::OnOverlapBegin); TriggerSphere->OnComponentEndOverlap.AddDynamic(this, &UInteractableComponent::OnOverlapEnd); ``` InteractionManager 维护 NearbyInteractables 列表,按距离排序。 **消耗检查**: ```cpp bool IChildHoodInteractable::CanInteract() const { return !bConsumed; // oneShot 且已消耗则返回 false } ``` #### G2. 提示 UI 显示规则 **实现**:WBP_InteractionPrompt(UMG Widget) - **位置**:通过 `ProjectWorldLocationToScreen` 将交互物注册点投影到屏幕空间 - **格式**:`[E] {动词} {物体名}`(如 `[E] 查看旧冰箱`) - **淡入/淡出**:Widget Animation 0.2s - **更新时机**:每 Tick 更新位置和内容(仅当选中物变化时刷新内容) **多交互物切换**: ```cpp void UInteractionManager::SwitchToNext() { if (NearbyInteractables.Num() == 0) return; SelectedIndex = (SelectedIndex + 1) % NearbyInteractables.Num(); UpdatePromptWidget(); } void UInteractionManager::SwitchToPrevious() { if (NearbyInteractables.Num() == 0) return; SelectedIndex = (SelectedIndex - 1 + NearbyInteractables.Num()) % NearbyInteractables.Num(); UpdatePromptWidget(); } ``` **距离排序**: ```cpp NearbyInteractables.Sort([this](const TWeakObjectPtr& A, const TWeakObjectPtr& B) { float DistA = FVector::Dist2D(Character->GetActorLocation(), A->GetInteractLocation()); float DistB = FVector::Dist2D(Character->GetActorLocation(), B->GetInteractLocation()); return DistA < DistB; }); ``` #### G3. 交互触发(E 键) **实现**:InteractionManager::OnInteractPressed() ```cpp void UInteractionManager::OnInteractPressed() { if (State != EInteractionState::Free || !bInDetectionRange) return; auto Selected = GetSelectedInteractable(); if (!Selected || !Selected->CanInteract()) return; StartInteraction(Selected); } ``` #### G4. 通用文本显示 UI **实现**:WBP_InteractionText(UMG Widget) - **位置**:屏幕正中偏下(Anchors: Center, Offset Y = +10% 屏幕高度) - **底框**:半透明黑色背景(Background Color: Black, Opacity 0.6),圆角 - **文字**:白色,居中,内容自适应宽高 - **显示**:整句一次性显示(无打字机效果) - **淡入/淡出**:Widget Animation 0.2s --- ### 三、交互类型一:观察(Examine) #### 流程实现 ```cpp void UInteractionManager::StartExamine(UInteractableComponent* Interactable) { Phase = EInteractPhase::Enter; PhaseTimer = 0.f; // 1. 角色转向(0.2s) StartCharacterTurn(Interactable->GetInteractLocation()); TurnDuration = 0.2f; // 2. 推镜(与转向同时,0.5s ease-out) StartCameraPush(Interactable, 0.5f); // 3. 设置 Enter 阶段完成时间 = max(转向, 推镜) + 0.2s 等待 EnterCompleteDelay = FMath::Max(0.2f, 0.5f) + 0.2f; } ``` **Enter → Active 转换**: ```cpp // Tick 中 if (Phase == EInteractPhase::Enter) { PhaseTimer += DeltaTime; UpdateCharacterTurn(DeltaTime); UpdateCameraPush(DeltaTime); if (PhaseTimer >= EnterCompleteDelay) { // 4. 显示文本 FString Text = SelectRandomText(CurrentInteractable); ShowInteractionText(Text); Phase = EInteractPhase::Active; } } ``` **关闭(E/ESC)**: ```cpp void UInteractionManager::CloseExamine() { Phase = EInteractPhase::Exit; HideInteractionText(); // 淡出 0.2s StartCameraReturn(0.3f); // ease-in 回位 ExitDuration = 0.3f; } ``` **文本随机选取**: ```cpp FString UInteractionManager::SelectRandomText(UInteractableComponent* Interactable) { auto& Texts = Interactable->GetNarrativeTexts(); if (Texts.Num() == 0) return TEXT(""); if (Texts.Num() == 1) return Texts[0]; // 不重复上一次 int32 Index; do { Index = FMath::RandRange(0, Texts.Num() - 1); } while (Index == LastTextIndex && Texts.Num() > 1); LastTextIndex = Index; return Texts[Index]; } ``` --- ### 四、交互类型二:拾取(Pickup) #### 流程实现 ```cpp void UInteractionManager::StartPickup(UInteractableComponent* Interactable) { Phase = EInteractPhase::Enter; PhaseTimer = 0.f; // 1. 角色转向(0.2s) StartCharacterTurn(Interactable->GetInteractLocation()); // 拾取不推镜,相机保持第三人称位置 // 2. 转向完成后播放动画 → AnimLock bPendingAnimLock = true; PendingMontage = nullptr; // v0.1 可能无动画 PendingAnimDuration = 1.0f; // 拾取动画时长 // 3. 物品消失在动画 60% 处 AnimEventProgress = 0.6f; bAnimEventTriggered = false; } // Tick 中处理 AnimLock if (Phase == EInteractPhase::AnimLock) { PhaseTimer += DeltaTime; float Progress = PhaseTimer / PendingAnimDuration; // 物品消失时间点 if (!bAnimEventTriggered && Progress >= AnimEventProgress) { bAnimEventTriggered = true; CurrentInteractable->SetVisibility(false); // 视觉消失 // 通知物品系统 NotifyItemSystem(CurrentInteractable->GetRewardItemId(), CurrentInteractable->GetRewardItemCategory()); } // 动画播完 if (PhaseTimer >= PendingAnimDuration) { CurrentInteractable->MarkConsumed(); // 标记已消耗 Phase = EInteractPhase::Exit; // Exit 立即完成(无回位动画) FinishInteraction(); } } ``` **拾取物外观**:所有拾取物使用同一个占位模型(简单球形或方块,0.1m 直径)。通过材质颜色区分稀有度(普通=白色,稀有=金色)。 **不支持手动关闭**:拾取全程 AnimLock,忽略所有按键。 --- ### 五、交互类型三:操作(Operate) #### 3A. 门(开关切换) ```cpp void UInteractionManager::StartDoor(UInteractableComponent* Interactable) { Phase = EInteractPhase::Enter; PhaseTimer = 0.f; // 1. 角色转向(0.2s) StartCharacterTurn(Interactable->GetInteractLocation()); // 不推镜 // 2. 转向完成后播放动画 → AnimLock bPendingAnimLock = true; PendingAnimDuration = 0.8f; // 门操作动画时长 AnimEventProgress = 0.4f; // 40% 处切换状态 } // AnimLock 中的事件 if (!bAnimEventTriggered && Progress >= AnimEventProgress) { bAnimEventTriggered = true; // 通知门的蓝图切换开关状态 CurrentInteractable->OnInteractEvent(); } // 动画播完 → Exit if (PhaseTimer >= PendingAnimDuration) { FinishInteraction(); // 直接回到 Free } ``` **门状态切换**:在 BP_InteractableDoor 蓝图中实现。InteractableComponent 触发事件 → 蓝图响应旋转门 Mesh + 更新提示文本("打开门" ↔ "关上门")。 #### 3B. 废纸箱(翻找) ```cpp void UInteractionManager::StartSearch(UInteractableComponent* Interactable) { Phase = EInteractPhase::Enter; PhaseTimer = 0.f; // 1. 角色转向(0.2s) StartCharacterTurn(Interactable->GetInteractLocation()); // 2. 推镜(与转向同时,0.5s) StartCameraPush(Interactable, 0.5f); // 3. 转向完成后播放翻找动画 → AnimLock bPendingAnimLock = true; PendingAnimDuration = 1.5f; // 翻找动画时长 } // AnimLock 完成后 void UInteractionManager::OnSearchAnimComplete() { if (CurrentInteractable->HasRewardItem()) { // 有物品分支 NotifyItemSystem(CurrentInteractable->GetRewardItemId(), CurrentInteractable->GetRewardItemCategory()); CurrentInteractable->MarkConsumed(); // 物品系统弹出拾取通知,收集系统弹出收集通知 // 等待 E/ESC Phase = EInteractPhase::Active; } else { // 无物品分支(架构预留) ShowInteractionText(TEXT("什么都没找到。")); Phase = EInteractPhase::Active; } } ``` --- ### 六、交互类型四:对话(Talk)— 简化版 ```cpp void UInteractionManager::StartTalk(UInteractableComponent* Interactable) { // 与观察类流程几乎相同 Phase = EInteractPhase::Enter; PhaseTimer = 0.f; StartCharacterTurn(Interactable->GetInteractLocation()); StartCameraPush(Interactable, 0.5f); // Enter 完成后显示 NPC 对话文本 // Active 阶段等 E/ESC 关闭 } ``` 与观察类的区别: - 文本来源不同(对话内容 vs 小孩想法) - 无功能差异,v0.1 共用同一流程 --- ### 七、摄像机推镜系统 #### 实现方案:ACameraActor + SetViewTargetWithBlend **原理**:交互时创建临时 CameraActor,通过 UE5 内置的摄像机混合系统平滑切换视角。 ```cpp void UInteractionManager::StartCameraPush(UInteractableComponent* Interactable, float Duration) { if (!Interactable->GetData().bUseCameraPush) return; // 计算推镜目标位置 FVector InteractLoc = Interactable->GetInteractLocation(); FVector CharacterLoc = GetOwner()->GetActorLocation(); FVector DirToCharacter = (CharacterLoc - InteractLoc).GetSafeNormal2D(); // 推镜位置:物体前方指定距离,略低于物体中心 float PushDist = Interactable->GetData().CameraPushDistance; float HeightOffset = Interactable->GetData().CameraPushHeightOffset; FVector PushLocation = InteractLoc - DirToCharacter * PushDist; PushLocation.Z = InteractLoc.Z + HeightOffset + 80.f; // 80 = 角色站立高度补偿 FRotator PushRotation = (InteractLoc - PushLocation).Rotation(); // 创建或复用推镜摄像机 if (!PushCamera) { FActorSpawnParameters Params; PushCamera = GetWorld()->SpawnActor(ACameraActor::StaticClass(), PushLocation, PushRotation, Params); } else { PushCamera->SetActorLocationAndRotation(PushLocation, PushRotation); } // 混合到推镜摄像机 APlayerController* PC = Cast(Cast(GetOwner())->GetController()); PC->SetViewTargetWithBlend(PushCamera, Duration, EViewTargetBlendFunction::VTBlend_EaseInOut, 1.0f); bIsCameraPushed = true; } void UInteractionManager::StartCameraReturn(float Duration) { if (!bIsCameraPushed) return; APlayerController* PC = Cast(Cast(GetOwner())->GetController()); APawn* Pawn = PC->GetPawn(); // 混合回角色(SpringArm 恢复控制) PC->SetViewTargetWithBlend(Pawn, Duration, EViewTargetBlendFunction::VTBlend_EaseInOut, 1.0f); bIsCameraPushed = false; } ``` **生命周期**:PushCamera 在 InteractionManager 初始化时创建,整个游戏期间复用,不需要每次交互都 Spawn/Destroy。 --- ### 八、角色转向实现 ```cpp void UInteractionManager::StartCharacterTurn(const FVector& TargetLocation) { ACharacter* Character = Cast(GetOwner()); if (!Character) return; TurnStartRotation = Character->GetActorRotation(); FVector Direction = (TargetLocation - Character->GetActorLocation()); Direction.Z = 0.f; TurnTargetRotation = Direction.Rotation(); TurnProgress = 0.f; TurnDuration = 0.2f; bIsTurning = true; // 暂停 CMC 的自动转向,防止冲突 Character->GetCharacterMovement()->bOrientRotationToMovement = false; } void UInteractionManager::UpdateCharacterTurn(float DeltaTime) { if (!bIsTurning) return; TurnProgress += DeltaTime / TurnDuration; if (TurnProgress >= 1.f) { TurnProgress = 1.f; bIsTurning = false; // 不立即恢复 bOrientRotationToMovement,等交互结束 } // Ease-out 插值 float Alpha = 1.f - FMath::Pow(1.f - TurnProgress, 3.f); FRotator NewRotation = FMath::Lerp(TurnStartRotation, TurnTargetRotation, Alpha); GetOwner()->SetActorRotation(NewRotation); } ``` --- ### 九、物品系统中转(补充执行案缺失部分) > 执行案要求"物品系统(通知中转+拾取通知+背包记录)需在本步骤纳入技术方案"。 #### UItemManager — 轻量物品管理组件 ```cpp UCLASS() class UItemManager : public UActorComponent { GENERATED_BODY() // 简单背包:ItemID → 数量 UPROPERTY() TMap Inventory; public: // 接收交互系统的拾取通知 void OnItemPickedUp(FName ItemId, const FString& Category, const FString& ItemNumber); // 查询 int32 GetItemCount(FName ItemId) const; bool HasItem(FName ItemId) const; // 事件:通知 UI 显示拾取通知 UPROPERTY(BlueprintAssignable) FOnItemPickedUpSignature OnItemPickedUpEvent; // 事件:通知收集系统 UPROPERTY(BlueprintAssignable) FOnItemCollectedSignature OnItemCollectedEvent; }; ``` #### 数据流 ``` 交互系统 (InteractionManager) │ OnItemPickedUp(ItemId, Category, Number) ▼ 物品系统 (ItemManager) ├── Inventory[ItemId]++ (背包记录) ├── OnItemPickedUpEvent → WBP_PickupNotification("找到了 XXX") └── OnItemCollectedEvent → 收集系统("发现了新的杂物,进度 X/Y") ``` #### 拾取通知 UI(WBP_PickupNotification) - **位置**:屏幕右上角 - **内容**:物品名称 + 简短描述 - **效果**:弹出后 2 秒自动淡出 - **v0.1 简化**:只显示文字,不显示图标 #### 背包 v0.1 不做背包 UI,仅内部记录。Inventory 使用 `TMap` 存储,支持计数查询。 --- ## 数据流总览 ``` Enhanced Input │ ├── WASD → AChildHoodCharacter::DoMove() │ ├── 检查 bIsInteracting(由 InteractionManager 控制) │ └── CMC::AddMovementInput() │ ├── 鼠标 → AChildHoodCharacter::DoLook() │ └── Controller::AddYawInput/AddPitchInput │ ├── E → UInteractionManager::OnInteractPressed() │ ├── Free 状态 → StartInteraction() │ └── Active 状态 → CloseCurrentInteraction() │ ├── ESC → UInteractionManager::OnCancelPressed() │ └── Active 状态 → CloseCurrentInteraction() │ ├── Tab/滚轮 → UInteractionManager::SwitchToNext/Previous() │ └── Interacting 状态下 WASD/Tab/滚轮被忽略 UInteractableComponent (场景中的交互物) │ Overlap 事件 ▼ UInteractionManager (角色上的组件) ├── 维护 NearbyInteractables 列表 ├── 管理状态机 (Free ↔ Interacting) ├── 控制摄像机推镜 (SetViewTargetWithBlend) ├── 控制角色转向 (SetActorRotation) ├── 控制 UI 显示 (Prompt / Text) └── 通知 UItemManager (拾取/翻找获得物品) ├── Inventory 记录 ├── PickupNotification UI └── → 收集系统通知 ``` --- ## 边界情况处理 | 边界情况 | 实现方案 | |---------|---------| | 快速连续按 E | AnimLock 阶段忽略所有输入;Exit 完成才回到 Free | | 交互物被消耗后范围残留 | MarkConsumed() 使 CanInteract() 返回 false;从 NearbyInteractables 移除 | | 推镜期间角色位置 | 移动输入已被 bIsInteracting 阻断,角色不移动 | | 同一交互物反复进出范围 | Prompt 每次淡入淡出,无累积。NearbyInteractables 列表自动增减 | | 切换交互物同时按 E | Tab/Scroll 先执行切换,下一帧 E 按下时读取新 SelectedIndex | | 距离恰好在触发半径 | Sphere Overlap 使用 ≤ 语义,边界上视为在范围内 | | 窄空间推镜位置不理想 | v0.1 接受不完美;推镜目标位置计算后可做简单碰撞检测(射线检测到阻挡则缩短距离) | --- ## 美术资源需求 ### 占位模型(第一轮,5 个) | 资源类型 | 名称 | 数量 | 规格 | 触发时机/用途 | |----------|------|------|------|---------------| | 3D 占位模型 | 旧冰箱 | 1 | 白色方箱体,约 1.7m 高 × 0.7m 宽 × 0.7m 深 | 家区域观察交互 | | 3D 占位模型 | 家的门 | 1 | 扁平方片(约 2.0m × 0.9m),可绕一侧轴旋转 | 家区域操作交互(开关) | | 3D 占位模型 | 拾取物通用模型 | 1 | 简单球形,直径约 0.1m | 拾取交互(多个实例,颜色区分) | | 3D 占位模型 | 公用电话亭 | 1 | 窄高方柱,约 2.0m 高 × 0.8m 宽 | 街道区域观察交互 | | 3D 占位模型 | NPC 老板(简化) | 1 | 简单人形胶囊体,坐姿 | 小卖部对话交互 | ### 占位模型(第二轮,5 个) | 资源类型 | 名称 | 数量 | 规格 | 触发时机/用途 | |----------|------|------|------|---------------| | 3D 占位模型 | 老式电视 | 1 | 方箱 + 凸起背部,约 0.5m 高 × 0.6m 宽 | 家区域观察交互 | | 3D 占位模型 | 零食货架 | 1 | 多层架结构,约 1.8m 高 × 1.0m 宽 | 小卖部观察交互 | | 3D 占位模型 | 拾取物(水浒卡) | 1 | 小方片,约 0.06m × 0.09m | 拾取交互(复用通用拾取模型也可) | | 3D 占位模型 | 墙面涂鸦 | 1 | 墙面贴花/平面色块,约 0.5m × 0.5m | 街道观察交互 | | 3D 占位模型 | 废纸箱 | 1 | 开口方箱,约 0.5m 高 × 0.4m 宽 | 巷子翻找交互 | ### 材质 | 资源类型 | 名称 | 数量 | 规格 | 用途 | |----------|------|------|------|------| | 材质 | 拾取物_普通 | 1 | 白色/浅灰,Unlit 或简单 Lit | 通用拾取物材质 | | 材质 | 拾取物_稀有 | 1 | 金色,可加简单闪烁 | 稀有拾取物材质 | ### 动画 | 资源类型 | 名称 | 数量 | 规格 | 用途 | |----------|------|------|------|------| | 动画 Montage | 弯腰拾取 | 1 | 约 1.0 秒,从站立→弯腰→站起 | 拾取交互 | | 动画 Montage | 推门 | 1 | 约 0.8 秒,伸手推/拉动作 | 门操作交互 | | 动画 Montage | 翻找 | 1 | 约 1.5 秒,弯腰翻动物品 | 废纸箱翻找交互 | > v0.1 执行案明确允许"若无动画资源,角色在地面滑动也可接受"。动画可简化为角色原地不动(跳过 AnimLock,直接触发事件)。 ### UI | 资源类型 | 名称 | 数量 | 规格 | 用途 | |----------|------|------|------|------| | UMG Widget | 交互提示 | 1 | `[E] 动作词 物体名`,白色文字 + 轻描边 | 显示在交互物上方 | | UMG Widget | 文本底框 | 1 | 半透明黑底(60%),白色居中文字,自适应大小 | 观察/对话/翻找无结果文本 | | UMG Widget | 拾取通知 | 1 | 屏幕右上角,物品名称,2 秒淡出 | 拾取/翻找获得物品提示 | --- ## 技术风险与注意事项 | 风险 | 等级 | 说明 | 缓解措施 | |------|------|------|---------| | 推镜位置计算不准确 | 🟡 中 | 贴墙物体的推镜位置可能在墙内 | v0.1 接受不完美;加简单射线检测缩短距离 | | 摄像机混合与 SpringArm 冲突 | 🟡 中 | SetViewTargetWithBlend 切换时可能有跳变 | 使用 ACameraActor 作为中间目标,确保初始位置匹配当前摄像机位置 | | 多交互物切换延迟感 | 🟢 低 | Tab 切换后提示更新需要即时 | 切换时立即刷新 UI 内容 | | Overlap 事件遗漏 | 🟢 低 | 角色快速移动可能错过 Overlap 事件 | 用 Tick 补充检测(每隔 N 帧全量扫描)或增大触发范围 | | 动画缺失时的降级处理 | 🟢 低 | 无拾取/操作动画时需跳过 AnimLock | AnimLock 阶段无 Montage 时直接用定时器模拟时长 | | 蓝图与 C++ 的交互复杂度 | 🟢 低 | 交互物具体行为(门旋转、物品消失)在蓝图中实现 | C++ 提供虚函数/委托,蓝图 Override | ### 需要注意 1. **摄像机推镜的初始位置同步**:创建 PushCamera 时必须设置到当前摄像机位置,否则混合开始瞬间会有跳变。 2. **bOrientRotationToMovement 的恢复时机**:交互结束时必须恢复此标志,否则角色退出交互后不再自动转向。 3. **交互物 Overlap 性能**:每个交互物有独立的 SphereComponent,10 个交互物最多 10 个 Overlap 检测。v0.1 规模无性能问题,后续扩展需考虑优化。 4. **PushCamera 的生命周期**:不要每次交互都 Spawn 新的,复用同一个 CameraActor。EndPlay 时清理。 --- ## 文件结构 ``` Source/ChildHood/ ├── Interaction/ │ ├── InteractableInterface.h # IChildHoodInteractable 接口 │ ├── InteractableComponent.h/.cpp # 交互物组件(数据 + 触发范围) │ ├── InteractionManager.h/.cpp # 交互状态机 + 检测 + 推镜 + UI │ ├── InteractionTypes.h # 枚举、结构体定义 │ └── ItemManager.h/.cpp # 物品系统(背包 + 通知中转) ├── Player/ │ ├── ChildHoodCharacter.h/.cpp # 修改:添加 InteractionManager │ └── ChildHoodPlayerController.h/.cpp # 修改:添加交互 IA 注册 └── UI/ ├── InteractionPromptWidget.h/.cpp # (可选:C++ 基类,也可纯蓝图) └── InteractionTextWidget.h/.cpp # (可选:C++ 基类,也可纯蓝图) Content/ ├── Blueprints/ │ ├── Interaction/ │ │ ├── BP_InteractableActor # 可交互物基类蓝图 │ │ ├── BP_PickupItem # 拾取物蓝图 │ │ ├── BP_InteractableDoor # 门蓝图 │ │ ├── BP_InteractableSearch # 翻找物蓝图(废纸箱) │ │ └── BP_InteractableNPC # NPC 蓝图 │ └── Characters/ │ └── BP_ChildHoodCharacter # 玩家角色蓝图(修改) ├── UI/ │ ├── WBP_InteractionPrompt # 交互提示 Widget │ ├── WBP_InteractionText # 文本底框 Widget │ └── WBP_PickupNotification # 拾取通知 Widget └── Input/ ├── IMC_Default # 输入映射上下文(修改:添加交互 IA) ├── IA_Move # 已有 ├── IA_MouseLook # 已有 ├── IA_Interact # 新增:E 键 ├── IA_Cancel # 新增:ESC 键 └── IA_SwitchInteract # 新增:Tab / 鼠标滚轮 ```