Files
ChildHood/docs/technical/interaction-system-tech.md
T
starmen1265 150f94c674 docs(technical): v0.1 程序设计——角色控制+交互+收集三份技术方案
P-003 步骤④程序设计产出:

- player-controller-tech.md: 角色控制系统(参数调整+输入绑定,2-3天)
- interaction-system-tech.md: 交互系统核心框架(状态机+推镜+物品中转,5-7天)
- collection-system-tech.md: 收集系统进度追踪+通知UI(1-2天)

覆盖三份执行案全部行为规则,含美术需求清单和技术风险分析。
等待 PM 质量门审查。
2026-06-06 00:30:50 +08:00

945 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 交互系统技术设计文档(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<FString> 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<TWeakObjectPtr<UInteractableComponent>> 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<USphereComponent>("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<UInteractableComponent>& A,
const TWeakObjectPtr<UInteractableComponent>& 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>(ACameraActor::StaticClass(),
PushLocation, PushRotation, Params);
}
else
{
PushCamera->SetActorLocationAndRotation(PushLocation, PushRotation);
}
// 混合到推镜摄像机
APlayerController* PC = Cast<APlayerController>(Cast<APawn>(GetOwner())->GetController());
PC->SetViewTargetWithBlend(PushCamera, Duration,
EViewTargetBlendFunction::VTBlend_EaseInOut, 1.0f);
bIsCameraPushed = true;
}
void UInteractionManager::StartCameraReturn(float Duration)
{
if (!bIsCameraPushed) return;
APlayerController* PC = Cast<APlayerController>(Cast<APawn>(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<ACharacter>(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<FName, int32> 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<FName, int32>` 存储,支持计数查询。
---
## 数据流总览
```
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 / 鼠标滚轮
```