From 150f94c6748b29502d4c3a7856ddc988536ad01e Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Sat, 6 Jun 2026 00:30:50 +0800 Subject: [PATCH] =?UTF-8?q?docs(technical):=20v0.1=20=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E2=80=94=E2=80=94=E8=A7=92=E8=89=B2=E6=8E=A7?= =?UTF-8?q?=E5=88=B6+=E4=BA=A4=E4=BA=92+=E6=94=B6=E9=9B=86=E4=B8=89?= =?UTF-8?q?=E4=BB=BD=E6=8A=80=E6=9C=AF=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P-003 步骤④程序设计产出: - player-controller-tech.md: 角色控制系统(参数调整+输入绑定,2-3天) - interaction-system-tech.md: 交互系统核心框架(状态机+推镜+物品中转,5-7天) - collection-system-tech.md: 收集系统进度追踪+通知UI(1-2天) 覆盖三份执行案全部行为规则,含美术需求清单和技术风险分析。 等待 PM 质量门审查。 --- docs/technical/collection-system-tech.md | 501 ++++++++++++ docs/technical/interaction-system-tech.md | 944 ++++++++++++++++++++++ docs/technical/player-controller-tech.md | 382 +++++++++ 3 files changed, 1827 insertions(+) create mode 100644 docs/technical/collection-system-tech.md create mode 100644 docs/technical/interaction-system-tech.md create mode 100644 docs/technical/player-controller-tech.md diff --git a/docs/technical/collection-system-tech.md b/docs/technical/collection-system-tech.md new file mode 100644 index 0000000..140038c --- /dev/null +++ b/docs/technical/collection-system-tech.md @@ -0,0 +1,501 @@ +# 收集系统技术设计文档(v0.1 原型) + +> 任务ID: P-003(部分) +> 对应执行案: `docs/design/prototype/collection-system-execution.md` +> 关联系统: 物品系统(上游)、交互系统(间接) +> 阶段: 原型 +> 日期: 2026-06-06 + +--- + +## 概述 + +收集系统是 v0.1 的辅助体验系统,追踪玩家在场景中收集"杂物"品类物品的进度。系统从物品系统(ItemManager)接收获得通知,判定是否为新收集,更新进度,弹出收集通知 UI。 + +v0.1 范围极小:单一品类"杂物",共 10 个物品,无持久化、无分类、无图鉴。 + +**实现复杂度**:低 +**预估工期**:1-2 天 +**新增代码量**:约 200-300 行 C++ + 2 个 UMG 蓝图 + +--- + +## 类与组件架构 + +### 新增类 + +| 类名 | 类型 | 职责 | 位置 | +|------|------|------|------| +| `UCollectionManager` | UActorComponent | 收集进度追踪 + 收集通知 UI + 进度 UI | `Collection/` | + +### 修改类 + +| 类名 | 修改内容 | +|------|---------| +| `AChildHoodCharacter` | 持有 CollectionManager,提供收集进度查询接口 | + +### 新增蓝图 + +| 蓝图名 | 类型 | 用途 | +|--------|------|------| +| `WBP_CollectionNotification` | UMG Widget | 收集通知("发现了新的杂物,进度 X/Y") | +| `WBP_CollectionProgress` | UMG Widget | 收集进度常驻 UI("📦 杂物 3/10") | +| `WBP_CollectionComplete` | UMG Widget | 全部收集完成提示("杂物全部收集完成!") | + +### 架构关系 + +``` +AChildHoodCharacter +├── UInteractionManager (交互系统) +├── UItemManager (物品系统) +└── UCollectionManager (收集系统) + ├── TSet CollectedIDs // 已收集物品 ID 集合 + ├── int32 Progress // 当前收集数 + ├── bool bIsComplete // 是否全部完成 + ├── WBP_CollectionProgress // 常驻进度 UI + ├── WBP_CollectionNotification // 弹出式收集通知 + └── WBP_CollectionComplete // 完成提示 + +数据流: +UItemManager (物品系统) + │ OnItemCollectedEvent(ItemId, Category, Number) + ▼ +UCollectionManager (收集系统) + ├── 1. 查品类 → 仅处理"杂物" + ├── 2. 查重复 → CollectedIDs.Contains(ItemId) + ├── 3. 记录 → CollectedIDs.Add + Progress++ + ├── 4. 弹收集通知 → WBP_CollectionNotification + └── 5. 完成判定 → Progress == TotalCount +``` + +--- + +## 核心数据结构 + +### 配置数据 + +```cpp +UCLASS() +class UCollectionManager : public UActorComponent +{ + GENERATED_BODY() + + // === 配置参数(构造函数设置默认值) === + + // 收集品类配置 + UPROPERTY(EditAnywhere, Category="Collection") + FString TargetCategory = TEXT("杂物"); // 追踪的品类名 + + UPROPERTY(EditAnywhere, Category="Collection") + int32 TotalCount = 10; // 品类总数 + + // 收集通知参数 + UPROPERTY(EditAnywhere, Category="Collection|Notification") + float NotificationDelay = 0.3f; // 拾取通知后延迟(秒) + + UPROPERTY(EditAnywhere, Category="Collection|Notification") + float NotificationFadeInDuration = 0.2f; + + UPROPERTY(EditAnywhere, Category="Collection|Notification") + float NotificationDisplayDuration = 2.0f; + + UPROPERTY(EditAnywhere, Category="Collection|Notification") + float NotificationFadeOutDuration = 0.3f; + + UPROPERTY(EditAnywhere, Category="Collection|Notification") + int32 MaxSimultaneousNotifications = 2; + + UPROPERTY(EditAnywhere, Category="Collection|Notification") + FLinearColor NotificationColor = FLinearColor(0.94f, 0.82f, 0.38f); // #F0D060 浅金色 + + // 进度 UI 参数 + UPROPERTY(EditAnywhere, Category="Collection|Progress") + float ProgressScaleAmplitude = 1.2f; // 弹性缩放倍率 + + UPROPERTY(EditAnywhere, Category="Collection|Progress") + float ProgressScaleDuration = 0.2f; // 弹性动画时长 + + // 完成表现参数 + UPROPERTY(EditAnywhere, Category="Collection|Complete") + float CompleteScaleAmplitude = 1.4f; + + UPROPERTY(EditAnywhere, Category="Collection|Complete") + float CompleteScaleDuration = 0.5f; + + UPROPERTY(EditAnywhere, Category="Collection|Complete") + float CompleteMessageDelay = 0.5f; // 收集通知消失后延迟 + + UPROPERTY(EditAnywhere, Category="Collection|Complete") + float CompleteMessageDisplayDuration = 2.0f; + + // === 运行时状态 === + + UPROPERTY() + TSet CollectedIDs; + + int32 Progress = 0; + bool bIsComplete = false; + + // UI 实例 + UPROPERTY() + UUserWidget* ProgressWidget = nullptr; + + UPROPERTY() + TArray ActiveNotifications; + + // UI 类引用 + UPROPERTY(EditAnywhere, Category="Collection|UI") + TSubclassOf NotificationWidgetClass; + + UPROPERTY(EditAnywhere, Category="Collection|UI") + TSubclassOf ProgressWidgetClass; + + UPROPERTY(EditAnywhere, Category="Collection|UI") + TSubclassOf CompleteWidgetClass; + + // 延迟通知定时器 + FTimerHandle NotificationDelayTimer; + FName PendingNotificationItemId; +}; +``` + +--- + +## 行为规则 → 实现方案映射 + +### 一、收到获得物品通知后的流程 + +#### 入口:ItemManager 委托回调 + +```cpp +// UCollectionManager::BeginPlay 中绑定 ItemManager 委托 +void UCollectionManager::BeginPlay() +{ + Super::BeginPlay(); + + // 查找 ItemManager 并绑定委托 + if (auto* ItemManager = GetOwner()->FindComponentByClass()) + { + ItemManager->OnItemCollectedEvent.AddDynamic(this, &UCollectionManager::OnItemReceived); + } + + // 创建常驻进度 UI + CreateProgressWidget(); +} +``` + +#### 五步流程实现 + +```cpp +void UCollectionManager::OnItemReceived(FName ItemId, const FString& Category, const FString& ItemNumber) +{ + // 步骤 1:查品类 — 不是"杂物"则忽略 + if (Category != TargetCategory) return; + + // 步骤 2:查重复 — 已收集过则忽略 + if (CollectedIDs.Contains(ItemId)) return; + + // 步骤 3:记录 + CollectedIDs.Add(ItemId); + Progress++; + + // 步骤 4:弹收集通知(延迟 0.3 秒,等拾取通知先显示) + PendingNotificationItemId = ItemId; + GetWorld()->GetTimerManager().SetTimer( + NotificationDelayTimer, + this, &UCollectionManager::ShowCollectionNotification, + NotificationDelay, + false + ); + + // 更新进度 UI + UpdateProgressWidget(); + + // 步骤 5:完成判定 + if (Progress >= TotalCount && !bIsComplete) + { + bIsComplete = true; + // 完成表现延迟到收集通知消失后 + GetWorld()->GetTimerManager().SetTimer( + CompleteTimer, + this, &UCollectionManager::ShowCompletionCelebration, + NotificationDelay + NotificationDisplayDuration + NotificationFadeOutDuration + CompleteMessageDelay, + false + ); + } +} +``` + +--- + +### 二、收集通知 UI + +#### WBP_CollectionNotification + +**实现**:UMG Widget Blueprint + +- **位置**:屏幕右上角,拾取通知下方 +- **Anchors**:TopRight,Offset X = -20, Offset Y 依赖拾取通知位置 +- **内容**:`TextBlock` 显示 `发现了新的杂物,进度 {Progress}/{TotalCount}` +- **文字颜色**:浅金色 `#F0D060`(FLinearColor(0.94, 0.82, 0.38)) + +#### 显示动画(Widget Animation) + +``` +淡入 (0.2s) → 停留 (2.0s) → 淡出 (0.3s) → 自动移除 +``` + +```cpp +void UCollectionManager::ShowCollectionNotification() +{ + if (!NotificationWidgetClass) return; + + // 检查最大同屏数 + CleanExpiredNotifications(); + if (ActiveNotifications.Num() >= MaxSimultaneousNotifications) return; + + // 创建通知 Widget + UUserWidget* Notification = CreateWidget(GetWorld(), NotificationWidgetClass); + Notification->AddToViewport(); + + // 设置内容(通过 BlueprintImplementableEvent 或直接设置 TextBlock) + // 调用蓝图函数设置文字:FString::Format("发现了新的杂物,进度 {0}/{1}", ...) + SetNotificationText(Notification, Progress, TotalCount); + + ActiveNotifications.Add(Notification); + + // 设置自动销毁定时器 + float TotalLifetime = NotificationFadeInDuration + NotificationDisplayDuration + NotificationFadeOutDuration; + FTimerHandle AutoRemoveTimer; + GetWorld()->GetTimerManager().SetTimer(AutoRemoveTimer, [this, Notification]() + { + Notification->RemoveFromParent(); + ActiveNotifications.Remove(Notification); + }, TotalLifetime, false); +} +``` + +#### 与拾取通知的叠放 + +- 收集通知通过 Anchor 定位在拾取通知下方 +- 实现方式:通知 Widget 的 Anchor 设为 TopRight,Y 偏移量 = 拾取通知高度 + 间距 +- v0.1 简化:固定偏移位置,不动态检测拾取通知实际高度 + +--- + +### 三、收集进度 UI + +#### WBP_CollectionProgress + +**实现**:UMG Widget Blueprint + +- **位置**:屏幕左上角 +- **Anchors**:TopLeft,Offset X = 3% 屏幕宽度,Offset Y = 3% 屏幕高度 +- **格式**:`📦 杂物 {Progress}/{TotalCount}` +- **图标**:v0.1 用简单 emoji 或纯色方块(20×20 像素) +- **文字**:白色,正常字体 +- **常驻显示**:游戏开始后始终可见,直到角色销毁 + +#### 数值更新动画 + +```cpp +void UCollectionManager::UpdateProgressWidget() +{ + if (!ProgressWidget) return; + + // 更新文字 + UpdateProgressText(Progress, TotalCount); + + // 播放弹性动画(蓝图中的 Widget Animation) + PlayProgressPulseAnimation(ProgressScaleAmplitude, ProgressScaleDuration); +} +``` + +弹性动画在 UMG 蓝图中通过 Widget Animation 实现: +- 缩放序列:1.0 → 1.2 → 1.0,时长 0.2 秒 +- 使用 Ease: Out 重心曲线 + +--- + +### 四、全部收集完成表现 + +#### WBP_CollectionComplete + +**实现**:UMG Widget Blueprint + +- **位置**:屏幕正中 +- **内容**:`杂物全部收集完成!`,白色大字 +- **显示时长**:2.0 秒后淡出 + +```cpp +void UCollectionManager::ShowCompletionCelebration() +{ + // 1. 进度 UI 数字变金色 + 放大弹性效果 + if (ProgressWidget) + { + SetProgressTextColor(FLinearColor::Gold); + PlayProgressPulseAnimation(CompleteScaleAmplitude, CompleteScaleDuration); + } + + // 2. 屏幕中央弹出完成提示 + if (CompleteWidgetClass) + { + UUserWidget* CompleteWidget = CreateWidget(GetWorld(), CompleteWidgetClass); + CompleteWidget->AddToViewport(); + + // 自动淡出移除 + FTimerHandle AutoRemoveTimer; + GetWorld()->GetTimerManager().SetTimer(AutoRemoveTimer, [CompleteWidget]() + { + CompleteWidget->RemoveFromParent(); + }, CompleteMessageDisplayDuration, false); + } +} +``` + +--- + +### 五、重启状态处理 + +v0.1 无持久化,每次游戏启动自动重置。 + +```cpp +void UCollectionManager::BeginPlay() +{ + // ... 绑定委托 ... + + // 初始化状态(保证干净状态) + CollectedIDs.Empty(); + Progress = 0; + bIsComplete = false; + + // 创建进度 UI,显示 "📦 杂物 0/10" + CreateProgressWidget(); +} +``` + +无需额外重置逻辑——组件生命周期由 UE 管理,角色销毁时自动清理。 + +--- + +## 数据流总览 + +``` +UInteractionManager (交互系统) + │ 拾取/翻找完成 + ▼ +UItemManager (物品系统) + │ OnItemPickedUpEvent → WBP_PickupNotification(拾取通知) + │ OnItemCollectedEvent(ItemId, Category, Number) + ▼ +UCollectionManager (收集系统) + ├── 1. Category != "杂物" → 忽略 + ├── 2. CollectedIDs.Contains(ItemId) → 忽略 + ├── 3. CollectedIDs.Add + Progress++ + ├── 4. 延迟 0.3s → WBP_CollectionNotification(收集通知) + │ 内容:"发现了新的杂物,进度 X/Y" + │ 颜色:浅金色 #F0D060 + ├── 5. Progress == 10 → bIsComplete = true + │ → 进度 UI 变金色 + │ → 延迟 → WBP_CollectionComplete(完成提示) + └── WBP_CollectionProgress(常驻进度 UI) + 内容:"📦 杂物 X/10" + 位置:左上角 +``` + +--- + +## 边界情况处理 + +| 边界情况 | 实现方案 | +|---------|---------| +| 重复获得同一物品 | CollectedIDs.Contains() 判重,已收集则忽略。不会导致 Progress 超过 10 | +| 全部收集完成后继续收到通知 | bIsComplete 已为 true,CollectedIDs 查重拦截。无副作用 | +| 游戏重启 | 无持久化,组件重新初始化。CollectedIDs/Progress/bIsComplete 全部重置 | +| 收集通知与拾取通知时序冲突 | 0.3s 延迟确保拾取通知先显示。固定偏移避免视觉重叠 | +| 多条收集通知快速触发 | MaxSimultaneousNotifications = 2,超出则丢弃。v0.1 正常流程不会触发此限制 | + +--- + +## 美术资源需求 + +### UI + +| 资源类型 | 名称 | 数量 | 规格 | 用途 | +|----------|------|------|------|------| +| UMG Widget | 收集通知 | 1 | 浅金色文字 `#F0D060`,右上角弹出,2 秒淡出 | 新收集时弹出 | +| UMG Widget | 收集进度 | 1 | 白色文字 + 占位图标,左上角常驻 | 显示收集进度 | +| UMG Widget | 完成提示 | 1 | 白色大字居中,2 秒淡出 | 全部收集完成 | + +### 图标 + +| 资源类型 | 名称 | 数量 | 规格 | 用途 | +|----------|------|------|------|------| +| 2D 图标 | 杂物品类图标 | 1 | 20×20 像素,简单符号或纯色方块 | 进度 UI 中的品类标识 | + +> v0.1 不需要 3D 模型、动画或其他美术资产。所有交互物的外观由交互系统负责。 + +--- + +## 技术风险与注意事项 + +| 风险 | 等级 | 说明 | 缓解措施 | +|------|------|------|---------| +| 收集通知位置与拾取通知重叠 | 🟢 低 | 固定偏移可能不够精确 | v0.1 接受不完美;后续版本可动态检测拾取通知位置 | +| Widget 生命周期管理 | 🟢 低 | 通知 Widget 需要正确销毁 | 定时器自动移除;组件销毁时清理所有 Active Notifications | +| ItemManager 委托时序 | 🟢 低 | BeginPlay 中绑定委托时 ItemManager 可能未初始化 | 使用 FindComponentByClass 查找,若为空则在 Tick 中延迟绑定一次 | + +### 需要注意 + +1. **委托绑定时机**:UCollectionManager 必须在 ItemManager 的 OnItemCollectedEvent 委托注册之后才能收到通知。确保 ItemManager 的初始化先于 CollectionManager,或在 CollectionManager 中做延迟绑定。 +2. **UI 叠放层级**:收集通知的 ZOrder 需要高于进度 UI,低于可能的弹窗/菜单。建议:进度 UI = 0,收集通知 = 1,完成提示 = 2。 +3. **进度 UI 文字更新**:使用 `SetNum` 或 `SetText` 更新 TextBlock 时需确保在 GameThread 上执行(委托回调本身在 GT 上)。 + +--- + +## 可调参数汇总 + +所有参数均在构造函数中设置默认值,蓝图可覆盖: + +| 参数 | C++ 属性 | 默认值 | 对应执行案参数 | +|------|---------|--------|-------------| +| 追踪品类名 | `TargetCategory` | "杂物" | — | +| 品类总数 | `TotalCount` | 10 | 10 个杂物品 | +| 通知延迟 | `NotificationDelay` | 0.3 秒 | 0.1-0.5 秒 | +| 通知淡入 | `NotificationFadeInDuration` | 0.2 秒 | 0.1-0.3 秒 | +| 通知停留 | `NotificationDisplayDuration` | 2.0 秒 | 1.5-3.0 秒 | +| 通知淡出 | `NotificationFadeOutDuration` | 0.3 秒 | 0.2-0.5 秒 | +| 通知颜色 | `NotificationColor` | #F0D060 | — | +| 最大同屏数 | `MaxSimultaneousNotifications` | 2 | 1-3 条 | +| 进度弹性倍率 | `ProgressScaleAmplitude` | 1.2 | 1.1-1.5 | +| 进度弹性时长 | `ProgressScaleDuration` | 0.2 秒 | 0.1-0.4 秒 | +| 完成弹性倍率 | `CompleteScaleAmplitude` | 1.4 | 1.2-1.8 | +| 完成弹性时长 | `CompleteScaleDuration` | 0.5 秒 | 0.3-0.8 秒 | +| 完成提示延迟 | `CompleteMessageDelay` | 0.5 秒 | 0.3-1.0 秒 | +| 完成提示时长 | `CompleteMessageDisplayDuration` | 2.0 秒 | 1.5-3.0 秒 | + +--- + +## 文件结构 + +``` +Source/ChildHood/ +├── Collection/ +│ ├── CollectionManager.h/.cpp # 收集系统主组件 +│ └── CollectionTypes.h # (可选,v0.1 极简可能不需要独立类型文件) +├── Player/ +│ └── ChildHoodCharacter.h/.cpp # 修改:添加 CollectionManager +└── Interaction/ + └── ItemManager.h/.cpp # 已有:OnItemCollectedEvent 委托 + +Content/ +├── Blueprints/ +│ └── Characters/ +│ └── BP_ChildHoodCharacter # 玩家角色蓝图(修改:添加 CollectionManager 组件引用) +├── UI/ +│ ├── WBP_CollectionNotification # 收集通知 Widget +│ ├── WBP_CollectionProgress # 收集进度 Widget +│ └── WBP_CollectionComplete # 完成提示 Widget +└── Data/ + └── (v0.1 不需要 DataAsset,参数硬编码在 C++ 中) +``` diff --git a/docs/technical/interaction-system-tech.md b/docs/technical/interaction-system-tech.md new file mode 100644 index 0000000..3a2e3a2 --- /dev/null +++ b/docs/technical/interaction-system-tech.md @@ -0,0 +1,944 @@ +# 交互系统技术设计文档(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 / 鼠标滚轮 +``` diff --git a/docs/technical/player-controller-tech.md b/docs/technical/player-controller-tech.md new file mode 100644 index 0000000..cc8336e --- /dev/null +++ b/docs/technical/player-controller-tech.md @@ -0,0 +1,382 @@ +# 角色控制技术设计文档(v0.1 原型) + +> 任务ID: P-003(部分) +> 对应执行案: `docs/design/prototype/player-controller-execution.md` +> 阶段: 原型 +> 日期: 2026-06-06 + +--- + +## 概述 + +角色控制系统基于现有 `AChildHoodCharacter` + `AChildHoodPlayerController` 框架进行参数调整和少量代码修改。核心架构(SpringArm 摄像机 + Enhanced Input + CMC 移动)已就绪,v0.1 工作以**配置级调整**为主,不引入新的类或组件。 + +**实现复杂度**:低 +**预估工期**:2-3 天 +**新增/修改代码量**:约 80-100 行 C++ + +--- + +## 类与组件架构 + +### 修改现有类 + +| 类 | 修改内容 | 代码量 | +|----|---------|--------| +| `AChildHoodCharacter` | 移除跳跃、调整 CMC/SpringArm 参数、添加交互状态标志、新增交互相关 IA 绑定 | ~60 行 | +| `AChildHoodPlayerController` | 调整 Pitch 限幅(通过 CameraManager) | ~5 行 | + +### 不新增类 + +v0.1 角色控制系统不引入新类。所有功能通过修改现有类的参数和少量逻辑实现。 + +### 组件挂载关系(不变) + +``` +AChildHoodCharacter +├── UCapsuleComponent (Root) — 碰撞胶囊体 +├── USkeletalMeshComponent (Mesh) — 角色网格 +├── USpringArmComponent (CameraBoom) — 摄像机摇臂 +└── UCameraComponent (FollowCamera) — 跟随摄像机 + +AChildHoodPlayerController +├── APlayerCameraManager — 摄像机管理器(设置 Pitch 限幅) +└── UEnhancedInputComponent — 输入组件 + └── IMC 注册(通过 DefaultMappingContexts) +``` + +--- + +## 行为规则 → 实现方案映射 + +### 一、摄像机系统 + +#### C1. 默认位置(距离 3.5m / 高度 0.8m / 俯角 15°) + +**实现**:构造函数中配置 SpringArm 参数 + +```cpp +// AChildHoodCharacter 构造函数 +CameraBoom->TargetArmLength = 350.0f; // 3.5m 距离 +CameraBoom->SocketOffset = FVector(0.f, 0.f, 80.f); // 0.8m 高于角色 +// 初始俯角由 Controller Pitch 控制,在 PlayerController 中设置初始旋转 +``` + +**可调参数**:TargetArmLength, SocketOffset.Z + +--- + +#### C2. 软跟随(3-5x 角色移动速度) + +**实现**:启用 SpringArm 原生 Camera Lag + +```cpp +CameraBoom->bEnableCameraLag = true; +CameraBoom->CameraLagSpeed = 6.0f; // 可调,范围 2-8 +CameraBoom->bEnableCameraRotationLag = false; // v0.1 不启用旋转延迟 +``` + +**可调参数**:CameraLagSpeed + +--- + +#### C3. 视角旋转(水平 150°/s,垂直 +45°/-30°) + +**水平旋转实现**:通过 Enhanced Input IA 的 Value 值 × PlayerController::InputYawScale 控制。 + +```cpp +// InputYawScale 在 PlayerController 中设置 +// 灵敏度通过 IA 的 Scale 值控制,不需要自定义旋转速度限制 +// UE5 鼠标输入是相对移动,不受固定 °/s 限制 +// v0.1 使用默认鼠标输入灵敏度,后续版本可加灵敏度滑块 +``` + +**垂直限幅实现**:在 CameraManager 中设置 + +```cpp +// AChildHoodPlayerController::BeginPlay 或构造函数中 +PlayerCameraManager->ViewPitchMin = -30.0f; // 向下看最多 30° +PlayerCameraManager->ViewPitchMax = 45.0f; // 向上看最多 45° +``` + +> 注:执行案中 150°/s 是手柄摇杆的设计参考值。键鼠模式下鼠标移动是相对输入,由玩家手速决定实际旋转速度。v0.1 优先键鼠,此参数暂不在代码中硬性限制。 + +**可调参数**:ViewPitchMin, ViewPitchMax, IA Scale + +--- + +#### C4. 摄像机碰撞处理 + +**实现**:启用 SpringArm 原生碰撞检测 + +```cpp +CameraBoom->bDoCollisionTest = true; +CameraBoom->ProbeSize = 12.0f; // 碰撞探针半径 +CameraBoom->ProbeChannel = ECC_Camera; // 使用 Camera 碰撞通道 + +// 最小距离保证:SpringArm 自动缩短到碰撞面 +// 当目标位置被阻挡时,SpringArm 将摄像机移到最近的未阻挡位置 +// 默认行为已满足"紧贴碰撞面内侧"的要求 +``` + +**最小距离保证**(0.5m):SpringArm 的 `TargetArmLength` 最短会被碰撞面推到约 0(取决于探针半径和角色胶囊体大小)。如需严格保证 0.5m 最小距离,可在蓝图中加 Tick 检查后处理。**v0.1 先用原生行为,视体验再决定是否添加。** + +**恢复**:碰撞消除后 SpringArm 自动恢复到 TargetArmLength,配合 Camera Lag 实现平滑回位。 + +**v0.1 简化**:仅处理与静态几何体碰撞(默认行为),不处理动态物体。 + +--- + +#### C5. 跑动中角色始终背对摄像机 + +**实现**:已有配置满足 + +```cpp +bUseControllerRotationYaw = false; // 角色不跟随 Controller Yaw +GetCharacterMovement()->bOrientRotationToMovement = true; // 角色朝移动方向旋转 +GetCharacterMovement()->RotationRate = FRotator(0.f, 720.f, 0.f); // 转向速度 720°/s +``` + +角色转向速度 720°/s 远快于鼠标旋转速度,自然满足"摄像机追不到角色正面"的效果。无需额外逻辑。 + +--- + +### 二、移动系统 + +#### M1. 移动方向(相对摄像机,对角 45° 合成) + +**实现**:已有 `DoMove()` 逻辑满足 + +```cpp +void AChildHoodCharacter::DoMove(float Right, float Forward) +{ + const FRotator Rotation = GetController()->GetControlRotation(); + const FRotator YawRotation(0, Rotation.Yaw, 0); + const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X); + const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y); + AddMovementInput(ForwardDirection, Forward); + AddMovementInput(RightDirection, Right); +} +``` + +- 对角输入(W+A)由 Enhanced Input 的 2D Axis 自然处理 +- `AddMovementInput` 不叠加速度(CMC 内部归一化) +- 连续旋转中移动方向实时跟随摄像机朝向(每帧重新计算方向) + +**无需修改。** + +--- + +#### M2. 角色转向(720°/s,最短路径,边跑边转) + +**实现**:CMC 参数配置 + +```cpp +GetCharacterMovement()->bOrientRotationToMovement = true; +GetCharacterMovement()->RotationRate = FRotator(0.f, 720.f, 0.f); +``` + +CMC 默认走最短路径旋转,转向期间角色持续移动(不"停顿再转向")。**无需修改。** + +--- + +#### M3. 跑步(4.5 m/s,起步加速 0.2s) + +**速度**: +```cpp +GetCharacterMovement()->MaxWalkSpeed = 450.0f; // 4.5 m/s (UE5 1uu = 1cm) +``` + +**加速**:CMC 的 `MaxAcceleration` 控制加速过程 + +```cpp +// 从 0 到 450 cm/s 在 0.2s → 加速度 = 450/0.2 = 2250 cm/s² +GetCharacterMovement()->MaxAcceleration = 2250.0f; +``` + +> 注:CMC 加速不是严格线性,受 GroundFriction 等因素影响。2250 是起始值,实际手感需测试微调。 + +--- + +#### M4. 停止减速(0.3s) + +**实现**: + +```cpp +// 从 450 到 0 cm/s 在 0.3s → 减速度 = 450/0.3 = 1500 cm/s² +GetCharacterMovement()->BrakingDecelerationWalking = 1500.0f; +``` + +> 同样受 GroundFriction 影响,1500 是起始值。 + +--- + +#### M5. 坡道与台阶(坡度 ≤45°,台阶 ≤0.15m) + +**实现**: + +```cpp +GetCharacterMovement()->WalkableFloorAngle = 45.0f; // 最大可行走坡度 +// 台阶高度由胶囊体半径和 CMC 的 StepHeight 决定 +// 默认 StepOffset = 45.0f (4.5cm),覆盖 0.15m 需要调大 +// 但 v0.1 场景设计避免高台阶,先保持默认 +GetCharacterMovement()->SetWalkableFloorAngle(45.0f); +``` + +--- + +### 三、输入映射 + +#### I1. WASD/方向键 → 移动 + +**实现**:已有 IA_Move + IMC 配置。保持不变。 + +#### I2. 鼠标移动 → 视角旋转 + +**实现**:已有 IA_MouseLook + IMC 配置。保持不变。 + +#### I3. E/ESC/Tab/滚轮 → 委托交互系统 + +**实现**:在 Character 中添加交互相关 IA 指针和绑定 + +```cpp +// 新增成员(AChildHoodCharacter.h) +UPROPERTY(EditAnywhere, Category="Input") +UInputAction* InteractAction; // E 键 + +UPROPERTY(EditAnywhere, Category="Input") +UInputAction* CancelAction; // ESC 键 + +UPROPERTY(EditAnywhere, Category="Input") +UInputAction* SwitchInteractAction; // Tab 键 + +// SetupPlayerInputComponent 中绑定 +// 回调函数直接调用 InteractionManager 的对应接口 +``` + +> 具体回调实现见交互系统技术设计文档。 + +#### I4. 移除跳跃 + +**实现**:移除 JumpAction 及其绑定 + +```cpp +// 删除 JumpAction 成员 +// 删除 SetupPlayerInputComponent 中的 Jump 绑定 +// 删除 DoJumpStart/DoJumpEnd 函数 +// CMC: GetCharacterMovement()->JumpZVelocity 不需要设置(不跳跃) +``` + +--- + +### 四、输入优先级 & 跨系统交互 + +#### P1. 交互中忽略移动输入 + +**实现**:在 `DoMove()` 中添加状态检查 + +```cpp +// 新增成员 +bool bIsInteracting = false; + +// 修改 DoMove +void AChildHoodCharacter::DoMove(float Right, float Forward) +{ + if (bIsInteracting) return; // 交互中忽略移动 + // ... 原有逻辑 +} + +// 外部接口(供 InteractionManager 调用) +UFUNCTION(BlueprintCallable) +void SetInteracting(bool bInteracting); +``` + +#### P2. 交互中摄像机控制权交接 + +**实现**:InteractionManager 在进入交互时通过 `SetViewTargetWithBlend` 切换摄像机目标;退出时切换回 Character。角色控制系统本身不需要修改摄像机逻辑。 + +交互结束后摄像机自动回到 SpringArm 默认跟随(因为 ViewTarget 切回 Character,SpringArm 恢复控制)。 + +#### P3. 状态信号提供 + +**实现**:提供查询接口 + +```cpp +UFUNCTION(BlueprintCallable, BlueprintPure) +bool IsInteracting() const { return bIsInteracting; } + +UFUNCTION(BlueprintCallable, BlueprintPure) +bool IsRunning() const; +// IsRunning: GetVelocity().Size() > Threshold && GetCharacterMovement()->IsMovingOnGround() +``` + +--- + +## 数据流 + +### 配置参数流 + +``` +C++ 构造函数默认值 → 蓝图派生类可覆盖 → 运行时通过 Details 面板微调 +``` + +v0.1 原型阶段所有参数在 C++ 中设置默认值,蓝图可覆盖。不使用 DataAsset 或配置表。 +批量生产阶段迁移到参数 DataAsset。 + +### 运行时状态流 + +``` +Enhanced Input (WASD/鼠标) + → AChildHoodPlayerController (IMC 注册) + → AChildHoodCharacter (IA 回调) + ├─ DoMove() → CMC (AddMovementInput) + ├─ DoLook() → Controller (AddYawInput/AddPitchInput) + │ → SpringArm (Controller Rotation) + │ → Camera (位置/旋转) + ├─ InteractAction → InteractionManager (委托) + └─ bIsInteracting → DoMove() (输入屏蔽) +``` + +--- + +## 美术资源需求 + +v0.1 角色控制系统**不直接需要**美术资产。以下为间接需求: + +| 资源类型 | 名称 | 数量 | 规格 | 说明 | +|----------|------|------|------|------| +| 占位动画 | Idle(站立待机) | 1 | 简单 T-pose 或 A-pose 即可 | v0.1 可无动画 | +| 占位动画 | Run(跑步循环) | 1 | 简单跑步循环,角色可在地面滑动 | 执行案明确说"若无动画资源,角色在地面滑动也可接受" | + +> v0.1 角色控制不包含跳跃、蹲下、攀爬,因此不需要对应动画。 + +--- + +## 技术风险与注意事项 + +| 风险 | 等级 | 说明 | 缓解措施 | +|------|------|------|---------| +| 软跟随手感不佳 | 🟢 低 | CameraLagSpeed 需要实际测试调整 | 多个预设值快速切换测试 | +| 摄像机碰撞穿墙 | 🟢 低 | SpringArm 原生碰撞处理基本满足 | 角落/窄空间需额外测试 | +| CMC 加速/减速不符合预期 | 🟢 低 | CMC 物理模型不完全线性 | 参数范围已预留,微调即可 | +| 角色转身"飘"的感觉 | 🟢 低 | 720°/s 偏快可能导致动画不自然 | v0.1 无正式动画,暂时不是问题 | + +--- + +## 可调参数汇总 + +所有参数均在构造函数中设置默认值,蓝图可覆盖: + +| 参数 | C++ 属性 | 默认值 | 位置 | +|------|---------|--------|------| +| 摄像机距离 | `CameraBoom->TargetArmLength` | 350.0f | 构造函数 | +| 摄像机高度 | `CameraBoom->SocketOffset.Z` | 80.0f | 构造函数 | +| 跟随速度 | `CameraBoom->CameraLagSpeed` | 6.0f | 构造函数 | +| 俯角上限 | `CameraManager->ViewPitchMax` | 45.0f | BeginPlay | +| 俯角下限 | `CameraManager->ViewPitchMin` | -30.0f | BeginPlay | +| 碰撞检测 | `CameraBoom->bDoCollisionTest` | true | 构造函数 | +| 跑步速度 | `CMC->MaxWalkSpeed` | 450.0f | 构造函数 | +| 加速度 | `CMC->MaxAcceleration` | 2250.0f | 构造函数 | +| 减速度 | `CMC->BrakingDecelerationWalking` | 1500.0f | 构造函数 | +| 转向速度 | `CMC->RotationRate.Yaw` | 720.0f | 构造函数 | +| 可行走坡度 | `CMC->WalkableFloorAngle` | 45.0f | 构造函数 |