merge: 合并交互系统技术方案 v1.1 修订 (P-014)

This commit is contained in:
2026-06-11 22:55:08 +08:00
+236 -17
View File
@@ -2,7 +2,7 @@
> 对应执行案: `docs/design/交互系统-执行案.md` (v0.1.2)
> 关联系统: 角色控制系统、物品系统、收集系统、GameInstance 持久化层
> 日期: 2026-06-11(v1.0 全面重构:Effect/Action 数据驱动架构)
> 日期: 2026-06-11(2026-06-11 更新:审查修订 v1.1)
---
@@ -12,6 +12,7 @@
|------|------|----------|
| v0.1 | 2026-06-07 | 初版技术方案,基于类型分发的状态机架构 |
| v1.0 | 2026-06-11 | 全面重构:Effect/Action 数据驱动架构;普通 C++ Manager 挂 AInfo;门派生类;持久化 Subsystem;逻辑/表现解耦 |
| v1.1 | 2026-06-11 | 审查修订:补充 ShowNarrative 的 bWaitForInput/bRandomSelect;补充 FSelectedInfo/FTurnState 定义;补充 CancelEffect 行为;修正模板 A/E 关闭流程;修正时序图标注 |
---
@@ -238,6 +239,8 @@ classDiagram
-float ElapsedTime
-int32 NextActionIndex
-bool bIsPlaying
-bool bIsWaitingForInput
-int32 LastNarrativeIndex
-FTurnState TurnState
-TFunction~void~ OnEffectComplete
+PlayEffect(FInteractEffect, FEffectContext) void
@@ -246,6 +249,9 @@ classDiagram
+Update(float DeltaTime) void
-ExecuteAction(FInteractAction, FEffectContext) void
-FinishEffect() void
-ResumeFromWait() void
-PerformCleanup() void
-PickRandomNarrative(TArray~FString~) FString
Note: 普通C++类,非UObject
Note: 所有UObject引用用TWeakObjectPtr
Note: Tick由Driver显式调用
@@ -387,7 +393,7 @@ sequenceDiagram
autonumber
actor Player as 玩家
participant Char as AChildHoodCharacter
participant Mgr as InteractionManager<br/>(GameInstance)
participant Mgr as FInteractionManager<br/>(ASystemDriver持有)
participant Comp as UInteractableComponent
participant EFM as EffectManager
participant Persist as PersistenceSubsystem
@@ -397,7 +403,7 @@ sequenceDiagram
Note over Player,Driver: === Phase 0: 进入检测范围 ===
Player->>Char: 移动进入 TriggerRadius
Comp->>Mgr: OnOverlap → GetGI()->GetMgr()->RegisterInteractable(this)
Comp->>Mgr: OnOverlap → Driver->GetInteractionManager().RegisterInteractable(this)
Mgr->>Mgr: Sort by Dist2D, SelectedIndex = 0
Note over Player,Driver: === Phase 1: Prompt 显示 (Pres 每帧 Polling) ===
@@ -411,7 +417,7 @@ sequenceDiagram
Note over Player,Driver: === Phase 2: 按 E 触发 Effect ===
Player->>Char: 按 E
Char->>Mgr: GetGI()->GetMgr()->BeginInteract()
Char->>Mgr: Driver->GetInteractionManager().BeginInteract()
Mgr->>Mgr: State = Executing
Mgr->>Char: SetInteracting(true)
Mgr->>EFM: PlayEffect(Comp.Data.InteractEffect, Context)
@@ -481,6 +487,7 @@ enum class EInteractActionType : uint8
ShowActor UMETA(DisplayName = "显示交互物"),
AddToBackpack UMETA(DisplayName = "物品进背包"),
ShowNarrative UMETA(DisplayName = "显示叙事文本"),
HideNarrative UMETA(DisplayName = "隐藏叙事文本"),
ShowPickupNotification UMETA(DisplayName = "显示拾取通知"),
ToggleDoor UMETA(DisplayName = "切换门状态"),
PlayDialogue UMETA(DisplayName = "播放对话"),
@@ -537,6 +544,14 @@ struct FInteractAction
// --- ShowNarrative ---
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::ShowNarrative", EditConditionHides))
TArray<FString> NarrativeTexts;
/** 是否等待用户按 E/ESC 后再继续后续 Action */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::ShowNarrative", EditConditionHides))
bool bWaitForInput = false;
/** 是否从 NarrativeTexts 中随机选取(不重复上一次) */
UPROPERTY(EditAnywhere, meta = (EditCondition = "Type==EInteractActionType::ShowNarrative", EditConditionHides))
bool bRandomSelect = true;
};
```
@@ -619,6 +634,80 @@ struct FEffectContext
};
```
### FSelectedInfo — Presentation 层缓存数据
> Manager 在选择变化时从 Component 读一次并写入此结构体,Prompt 组件每帧只读此缓存,不直接访问 Component。
```cpp
struct FSelectedInfo
{
/** 是否有有效选中 */
bool bHasSelection = false;
/** 选中交互物的提示文本(如 "[E] 查看旧冰箱") */
FString PromptText;
/** 选中交互物的世界坐标(用于屏幕空间投影) */
FVector WorldPosition = FVector::ZeroVector;
/** 选中交互物是否已消耗(消耗后不再显示提示) */
bool bIsConsumed = false;
void Reset()
{
bHasSelection = false;
PromptText.Empty();
WorldPosition = FVector::ZeroVector;
bIsConsumed = false;
}
};
```
**写入时机**:`RefreshCachedSelection()` — 在 `RegisterInteractable` / `UnregisterInteractable` / `SwitchTarget` 中调用。
**读取方**:`UInteractionPromptComponent::TickComponent` 每帧读取 `CachedSelected.PromptText` 和 `CachedSelected.WorldPosition`,不碰任何 Component。
### FTurnState — TurnToTarget 跨帧插值状态
> TurnToTarget Action 需要每帧 RInterpTo 驱动旋转,不能一帧完成。EffectManager 在 Update 中维护此状态。
```cpp
struct FTurnState
{
/** 是否正在转向 */
bool bIsActive = false;
/** 转向起始旋转 */
FRotator StartRotation = FRotator::ZeroRotator;
/** 转向目标旋转(面朝交互物方向) */
FRotator TargetRotation = FRotator::ZeroRotator;
/** 已用时间(秒) */
float ElapsedTime = 0.f;
/** 转向总时长(秒) */
float Duration = 0.2f;
/** 是否已完成 */
bool IsComplete() const { return ElapsedTime >= Duration; }
/** 获取当前插值 Alpha [0,1] */
float GetAlpha() const { return FMath::Clamp(ElapsedTime / Duration, 0.f, 1.f); }
void Reset()
{
bIsActive = false;
StartRotation = FRotator::ZeroRotator;
TargetRotation = FRotator::ZeroRotator;
ElapsedTime = 0.f;
Duration = 0.2f;
}
};
```
**生命周期**:`TurnToTarget` Action 触发时初始化 → `EffectManager::Update` 每帧驱动插值 → `IsComplete()` 后停止。
### 委托
```cpp
@@ -648,19 +737,23 @@ DECLARE_DELEGATE(FOnEffectComplete);
```
Effect: "观察物品"
TotalDuration: 3.0s, Cancellable: true
TotalDuration: 10.0s (安全上限,实际由 bWaitForInput 控制结束), Cancellable: true
TriggerTime | Action | 参数
----------- | ------------- | ----
0.0s | TurnToTarget | TurnDuration=0.2
0.0s | CameraPush | PushDistance=90, PushDuration=0.5
0.7s | ShowNarrative | Texts=["冰箱嗡嗡响。里面有冰棒!"]
0.7s | (等待用户输入,ShowNarrative 持续显示)
0.7s | ShowNarrative | Texts=["冰箱嗡嗡响。里面有冰棒!"], bWaitForInput=true, bRandomSelect=true
(用户按E/ESC后继续)
~0.7s+ | HideNarrative | (淡出 0.2s)
~0.7s+ | CameraReturn | ReturnDuration=0.3
```
> ShowNarrative Action 执行后进入"等待用户输入"状态,用户按 E/ESC 后 Effect 继续。
> 此行为通过在 ShowNarrative Action 中标记 `bWaitForInput=true` 实现(后续版本扩展)。
> v0.1 简化:ShowNarrative 不等待,用 Effect TotalDuration 控制总时长。
> **关闭流程**(对应执行案 E6):ShowNarrative 标记 `bWaitForInput=true`,EffectManager 暂停 Timeline 等待用户按 E/ESC。用户输入后 Effect 恢复,执行 HideNarrative(底框淡出 0.2s)+ CameraReturn(相机回位 0.3s ease-in),Effect 完成。
>
> **随机文本选取**(对应执行案 E5):ShowNarrative 标记 `bRandomSelect=true`,执行时从 NarrativeTexts 列表中随机选取一条,不重复上一次的内容。若列表只有一条则每次显示同一条。
>
> **可重复交互**(对应执行案 E5):观察类 Effect 的 bOneShot=false,不调用 Consume。每次交互重新执行 Effect,ShowNarrative 每次随机选取新文本。
#### 模板 B:拾取类 Effect
@@ -721,20 +814,139 @@ TriggerTime | Action | 参数
```
Effect: "NPC对话"
TotalDuration: 4.0s, Cancellable: true
TotalDuration: 10.0s (安全上限,实际由 bWaitForInput 控制结束), Cancellable: true
TriggerTime | Action | 参数
----------- | ------------- | ----
0.0s | TurnToTarget | TurnDuration=0.2
0.0s | CameraPush | PushDistance=90, PushDuration=0.5
0.7s | ShowNarrative | Texts=["zzz"]
0.7s | ShowNarrative | Texts=["zzz"], bWaitForInput=true, bRandomSelect=true
(用户按E/ESC后继续)
~0.7s+ | HideNarrative | (淡出 0.2s)
~0.7s+ | CameraReturn | ReturnDuration=0.3
```
> **关闭流程**(对应执行案 K6):与观察类完全一致。ShowNarrative 标记 `bWaitForInput=true`,用户按 E/ESC 后执行 HideNarrative + CameraReturn。
>
> **随机对话选取**(对应执行案 K6):ShowNarrative 标记 `bRandomSelect=true`,从对话文本列表中随机选取,不重复上一次。若列表只有一条则每次相同。
### CancelEffect 行为定义
> 对应审查问题:执行案要求观察/对话类可按 E/ESC 中断,需明确定义 Effect 取消时的完整行为。
`EffectManager::CancelEffect()` 在用户按 E/ESC 且当前 Effect 的 `bCancellable=true` 时触发。完整行为如下:
```cpp
void FEffectManager::CancelEffect()
{
if (!bIsPlaying) return;
if (!CurrentEffect.bCancellable) return;
// 1. 检查是否处于 WaitForInput 暂停态
if (bIsWaitingForInput)
{
// 用户输入正是期望的恢复信号 → 走正常恢复流程
// 执行 WaitForInput 之后的所有剩余 Action(HideNarrative, CameraReturn 等)
ResumeFromWait();
return;
}
// 2. 非暂停态的强制取消(如安全超时)
// 停止 Timeline
bIsPlaying = false;
// 3. 执行收尾清理
PerformCleanup();
}
```
#### 收尾清理行为(PerformCleanup)
无论 CancelEffect 走哪条路径,Effect 结束时必须确保以下清理:
| 清理项 | 条件 | 行为 |
|--------|------|------|
| 文本底框 | ShowNarrative 曾显示但未 HideNarrative | 强制淡出(0.2s)并移除 Widget |
| 相机回位 | CameraPush 曾执行但未 CameraReturn | 强制回位(0.3s ease-in),`SetViewTargetWithBlend(Character)` |
| 蒙太奇停止 | PlayMontage 正在播放 | `Montage_Stop(0.2f)` 淡出停止 |
| 转向状态 | TurnToTarget 未完成 | 立即设为目标旋转(瞬移到最终位置) |
| Timeline 状态 | 任何 | 重置 ElapsedTime、NextActionIndex、TurnState |
#### WaitForInput 暂停机制
当 ShowNarrative Action 标记 `bWaitForInput=true` 时:
1. EffectManager 执行 ShowNarrative(显示文本底框)
2. 设置 `bIsWaitingForInput = true`,暂停 Timeline(不再推进 ElapsedTime)
3. 用户按 E/ESC → Character 输入回调 → `EffectManager.CancelEffect()`
4. CancelEffect 检测到 `bIsWaitingForInput == true` → 调用 `ResumeFromWait()`
5. ResumeFromWait 设置 `bIsWaitingForInput = false`,恢复 Timeline
6. 后续 Action(HideNarrative、CameraReturn)按 TriggerTime 正常执行
7. Timeline 到达 TotalDuration → `FinishEffect()` → 回到 Free 状态
```cpp
void FEffectManager::ResumeFromWait()
{
bIsWaitingForInput = false;
// 继续 Update 中正常触发后续 Action
// HideNarrative/CameraReturn 的 TriggerTime 应设在 ShowNarrative 之后
}
```
> **设计要点**:WaitForInput 不是取消,而是"暂停等待"。用户按 E/ESC 是期望的恢复信号,不是强制中断。CancelEffect 检测到暂停态时走恢复路径,而非强制清理路径。
### ShowNarrative Action 执行逻辑
> 对应执行案:观察类「文本从列表中随机选取,不重复上一次」、对话类「每次触发随机选取,不重复上一次」。
```cpp
void FEffectManager::ExecuteShowNarrative(const FInteractAction& Action, const FEffectContext& Context)
{
// 1. 选取文本
FString SelectedText;
if (Action.bRandomSelect && Action.NarrativeTexts.Num() > 1)
{
SelectedText = PickRandomNarrative(Action.NarrativeTexts);
}
else if (Action.NarrativeTexts.Num() > 0)
{
SelectedText = Action.NarrativeTexts[0];
}
// 2. 显示文本底框
// 通过 Context 中的 Instigator 获取 PC,显示 NarrativeWidget
ShowNarrativeWidget(SelectedText);
// 3. 如果需要等待用户输入
if (Action.bWaitForInput)
{
bIsWaitingForInput = true;
// Update() 中检测到 bIsWaitingForInput 后不再推进 ElapsedTime
}
}
FString FEffectManager::PickRandomNarrative(const TArray<FString>& Texts)
{
if (Texts.Num() == 0) return TEXT("");
if (Texts.Num() == 1) return Texts[0];
int32 CandidateIndex;
do {
CandidateIndex = FMath::RandRange(0, Texts.Num() - 1);
} while (CandidateIndex == LastNarrativeIndex && Texts.Num() > 1);
LastNarrativeIndex = CandidateIndex;
return Texts[CandidateIndex];
}
```
> **LastNarrativeIndex** 是 FEffectManager 的成员变量,跨 Effect 调用保持(同一交互物的多次交互不重复)。当交互物切换时由 InteractionManager 重置。
### 通用规则实现
#### 规则 1: 触发条件
**实现**:`UInteractableComponent::OnDetectionOverlapBegin` → `GetGI()->GetMgr()->RegisterInteractable(this)`。Manager 的 `BeginInteract()` 检查 State==Free、SelectedIndex 有效、!IsConsumed()。
**实现**:`UInteractableComponent::OnDetectionOverlapBegin` → `ASystemDriver::Get(World)->GetInteractionManager().RegisterInteractable(this)`。Manager 的 `BeginInteract()` 检查 State==Free、SelectedIndex 有效、!IsConsumed()。
#### 规则 2-5: Prompt UI 显示/隐藏
@@ -749,7 +961,7 @@ Manager 在 `RegisterInteractable` / `UnregisterInteractable` / `SwitchTarget`
#### 规则 6-10: 多交互物选择与切换
**实现**:同 v0.1,但 Manager 在 GameInstance 中。`RegisterInteractable` / `UnregisterInteractable` / `SwitchTarget` / `UpdateSelection` 逻辑不变。
**实现**:同 v0.1,但 Manager 在 ASystemDriver 中(非 GameInstance)。`RegisterInteractable` / `UnregisterInteractable` / `SwitchTarget` / `UpdateSelection` 逻辑不变。
#### 规则 11-14: 交互触发与锁定
@@ -777,7 +989,13 @@ void AChildHoodCharacter::OnInteract()
}
```
`BeginInteract()` 返回选中的 Component(或 nullptr 拒绝),设置 `State = Executing`。Effect 执行期间所有输入被忽略(CancelInteract 检查 bCancellable)。
`BeginInteract()` 返回选中的 Component(或 nullptr 拒绝),设置 `State = Executing`。
**CancelInteract(E/ESC)行为**:
- 若 EffectManager 处于 `bIsWaitingForInput` 暂停态 → `ResumeFromWait()`(恢复 Timeline,执行关闭流程 Action)
- 若 Effect 的 `bCancellable=true` 但非暂停态 → `CancelEffect()` 强制取消 + `PerformCleanup()` 清理
- 若 Effect 的 `bCancellable=false` → 忽略输入(拾取、门操作、翻找期间)
- Interacting 状态下 Tab/滚轮/新 E 键触发交互 → 全部忽略
Effect 完成通过 Driver 中 `std::function` 连线:`EffectManager.OnEffectComplete → InteractionManager.OnInteractionFinished()`。
@@ -822,7 +1040,7 @@ Effect 完成通过 Driver 中 `std::function` 连线:`EffectManager.OnEffectC
| Consume 触发同步 EndOverlap | 🟡中 | Consume() 禁用碰撞同步触发 EndOverlap → UnregisterInteractable,可能清空 CurrentInteractable | Manager 的 UnregisterInteractable 在 Executing 状态下延迟清理 |
| TurnToTarget 跨帧驱动 | 🟡中 | TurnToTarget Action 需要每帧 RInterpTo,不能一帧完成 | EffectManager.Update 中维护 TurnState,每帧驱动插值 |
| EditCondition 折叠大量 Action 参数 | 🟢低 | FInteractAction 一个 struct 含所有 Action 类型参数,编辑器中按类型折叠 | 使用 EditConditionHides 自动隐藏不相关字段 |
| ShowNarrative 等待用户输入 | 🟡中 | 观察/对话类需要等用户按 E/ESC,不能靠 TotalDuration 硬控 | v0.1 用 TotalDuration 硬控;后续版本给 ShowNarrative 加 bWaitForInput 标记,Effect 暂停 Timeline 等待 |
| ShowNarrative 等待用户输入 | 🟡中 | 观察/对话类需要等用户按 E/ESC,不能靠 TotalDuration 硬控 | ShowNarrative Action 支持 `bWaitForInput=true`,EffectManager 暂停 Timeline 等待用户输入后恢复,TotalDuration 仅作安全上限 |
| AInfo 驱动器生命周期 | 🟢低 | 关卡加载/卸载时需正确 Spawn/销毁 | OnWorldReady 中 Spawn,关卡卸载时 World 销毁自动清理 |
### 注意事项
@@ -832,7 +1050,8 @@ Effect 完成通过 Driver 中 `std::function` 连线:`EffectManager.OnEffectC
3. **Manager 是普通 C++**:`FInteractionManager` / `FEffectManager` 不是 UObject,不参与 GC。所有 UObject 引用使用 `TWeakObjectPtr`,回调使用 `TFunction<void()>`
4. **InteractionManager 无 Tick**:所有操作由碰撞事件、输入回调、EffectManager 完成回调驱动,无每帧逻辑
5. **GameInstance 不持有交互系统**:只有 `UPersistenceSubsystem` 跨 World 存活,所有交互逻辑随 World 生灭
4. **Prompt 组件独立**:不参与 Effect 执行,只负责 Free 状态下的 UI 追踪
6. **Prompt 组件独立**:不参与 Effect 执行,只负责 Free 状态下的 UI 追踪
7. **WaitForInput 暂停机制**:ShowNarrative Action 支持 `bWaitForInput=true`,EffectManager 暂停 Timeline 等待用户输入。用户按 E/ESC 走 ResumeFromWait(恢复后续 Action),而非强制取消
---