merge: 合并交互系统技术方案 v1.1 修订 (P-014)
This commit is contained in:
+236
-17
@@ -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),而非强制取消
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user