refactor(interaction): 适配v1.1架构的角色/管理器/交互物改动

- Character: 替换UInteractionManager为UInteractionPromptComponent
- GameInstance: 空壳化,持久数据迁移至PersistenceSubsystem
- InteractionManager: 重写为普通C++ FInteractionManager
- InteractableComponent: 碰撞回调接入ASystemDriver注册

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 23:21:46 +08:00
parent 4bf678b937
commit c14c63f30d
7 changed files with 391 additions and 1085 deletions
+72 -19
View File
@@ -1,3 +1,8 @@
// ChildHood Project - 玩家角色 实现(v1.1 Effect/Action 架构适配)
// 变更:通过 ASystemDriver 访问 FInteractionManager + FEffectManager
// 新增 BeginPlay 初始化 PromptComponent
// 删除对旧 UInteractionManager 的依赖
// Copyright Epic Games, Inc. All Rights Reserved.
#include "ChildHoodCharacter.h"
@@ -11,7 +16,12 @@
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "ChildHood.h"
#include "Interaction/InteractionPromptComponent.h"
#include "SystemDriver.h"
#include "Interaction/InteractionManager.h"
#include "Interaction/EffectManager.h"
#include "Interaction/InteractableComponent.h"
#include "Interaction/InteractionTypes.h"
AChildHoodCharacter::AChildHoodCharacter()
{
@@ -55,8 +65,22 @@ AChildHoodCharacter::AChildHoodCharacter()
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
// 创建交互管理器组件
InteractionManager = CreateDefaultSubobject<UInteractionManager>(TEXT("InteractionManager"));
// 创建交互提示组件(替代旧 InteractionManager)
PromptComponent = CreateDefaultSubobject<UInteractionPromptComponent>(TEXT("PromptComponent"));
}
void AChildHoodCharacter::BeginPlay()
{
Super::BeginPlay();
// 初始化 PromptComponent — 将其与 ASystemDriver 关联
if (PromptComponent)
{
if (ASystemDriver* Driver = ASystemDriver::Get(this))
{
PromptComponent->Initialize(Driver);
}
}
}
void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
@@ -71,7 +95,7 @@ void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInput
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
// Interaction — delegates to InteractionManager
// Interaction — delegates to ASystemDriver
if (InteractAction)
{
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnInteract);
@@ -111,30 +135,59 @@ void AChildHoodCharacter::Look(const FInputActionValue& Value)
void AChildHoodCharacter::OnInteract(const FInputActionValue& Value)
{
if (UInteractionManager* IM = GetInteractionManager())
ASystemDriver* Driver = ASystemDriver::Get(this);
if (!Driver) return;
auto& Mgr = Driver->GetInteractionManager();
auto& EFM = Driver->GetEffectManager();
// E 键三重功能:
// 1. Free 状态:发起交互
// 2. WaitForInput 暂停态:恢复 Effect(观察/对话类按 E 继续)
// 3. 可取消 Effect 执行中:取消 Effect
if (Mgr.GetState() == EInteractionState::Free)
{
// E 键双重功能:Free 发起交互,Active 结束交互
IM->BeginInteract();
IM->CancelInteract();
// 发起交互
auto* Selected = Mgr.BeginInteract();
if (!Selected) return;
SetInteracting(true);
FEffectContext Ctx;
Ctx.Instigator = this;
Ctx.TargetComp = Selected;
Ctx.TargetActor = Selected->GetOwner();
EFM.PlayEffect(Selected->GetInteractableData().InteractEffect, Ctx);
}
else
{
// Executing 状态 → 尝试取消/恢复
EFM.CancelEffect();
}
}
void AChildHoodCharacter::OnCancel(const FInputActionValue& Value)
{
if (UInteractionManager* IM = GetInteractionManager())
{
IM->CancelInteract();
}
ASystemDriver* Driver = ASystemDriver::Get(this);
if (!Driver) return;
auto& EFM = Driver->GetEffectManager();
EFM.CancelEffect();
}
void AChildHoodCharacter::OnSwitchInteract(const FInputActionValue& Value)
{
if (UInteractionManager* IM = GetInteractionManager())
{
float DirectionValue = Value.Get<float>();
int32 Direction = (DirectionValue >= 0.f) ? 1 : -1;
IM->SwitchTarget(Direction);
}
ASystemDriver* Driver = ASystemDriver::Get(this);
if (!Driver) return;
auto& Mgr = Driver->GetInteractionManager();
// 交互中不可切换
if (Mgr.GetState() != EInteractionState::Free) return;
float DirectionValue = Value.Get<float>();
int32 Direction = (DirectionValue >= 0.f) ? 1 : -1;
Mgr.SwitchTarget(Direction);
}
void AChildHoodCharacter::DoMove(float Right, float Forward)
@@ -177,9 +230,9 @@ void AChildHoodCharacter::DoLook(float Yaw, float Pitch)
// 交互系统方法
// ============================================================================
UInteractionManager* AChildHoodCharacter::GetInteractionManager() const
UInteractionPromptComponent* AChildHoodCharacter::GetPromptComponent() const
{
return InteractionManager;
return PromptComponent;
}
void AChildHoodCharacter::SetInteracting(bool bValue)
+19 -10
View File
@@ -1,3 +1,7 @@
// ChildHood Project - 玩家角色(v1.1 Effect/Action 架构适配)
// 变更:移除 UInteractionManager,改用 UInteractionPromptComponent
// 交互操作通过 ASystemDriver 访问 FInteractionManager + FEffectManager
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
@@ -10,7 +14,8 @@
class USpringArmComponent;
class UCameraComponent;
class UInputAction;
class UInteractionManager;
class UInteractionPromptComponent;
class ASystemDriver;
struct FInputActionValue;
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
@@ -18,6 +23,10 @@ DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
/**
* A simple player-controllable third person character
* Implements a controllable orbiting camera
*
* v1.1 架构变更:
* - 交互操作通过 ASystemDriver 全局单例访问 FInteractionManager / FEffectManager
* - 身上挂载 UInteractionPromptComponent 负责 Free 状态下的 Prompt UI 追踪
*/
UCLASS(abstract)
class AChildHoodCharacter : public ACharacter
@@ -58,21 +67,23 @@ protected:
UPROPERTY(EditAnywhere, Category="Input")
UInputAction* SwitchInteractAction;
/** 交互管理器组件 */
/** 交互提示 UI 组件 — Free 状态下追踪选中交互物并显示 Prompt */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction", meta = (AllowPrivateAccess = "true"))
UInteractionManager* InteractionManager;
UInteractionPromptComponent* PromptComponent;
/** Whether the character is currently in an interaction */
bool bIsInteracting = false;
virtual void BeginPlay() override;
public:
/** Constructor */
AChildHoodCharacter();
/** 获取交互管理器组件 */
/** 获取交互提示组件 */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interaction")
UInteractionManager* GetInteractionManager() const;
UInteractionPromptComponent* GetPromptComponent() const;
/** Set interaction state — called by interaction system */
UFUNCTION(BlueprintCallable, Category = "Interaction")
@@ -91,21 +102,19 @@ protected:
/** Initialize input action bindings */
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
protected:
/** Called for movement input */
void Move(const FInputActionValue& Value);
/** Called for looking input */
void Look(const FInputActionValue& Value);
/** Called for interact input (E) — delegates to InteractionManager */
/** Called for interact input (E) — delegates to ASystemDriver */
void OnInteract(const FInputActionValue& Value);
/** Called for cancel input (ESC) — delegates to InteractionManager */
/** Called for cancel input (ESC) — delegates to ASystemDriver */
void OnCancel(const FInputActionValue& Value);
/** Called for switch interact target input (Tab/Wheel) — delegates to InteractionManager */
/** Called for switch interact target input (Tab/Wheel) — delegates to ASystemDriver */
void OnSwitchInteract(const FInputActionValue& Value);
public:
+3 -28
View File
@@ -1,30 +1,5 @@
// ChildHood Project - 跨关卡持久数据持有者 实现
// ChildHood Project - GameInstance(v1.1 清理版)
// 持久数据已迁移至 UPersistenceSubsystem(UGameInstanceSubsystem)
#include "ChildHoodGameInstance.h"
bool UChildHoodGameInstance::IsItemCollected(FName ItemId) const
{
return CollectedItemIDs.Contains(ItemId);
}
void UChildHoodGameInstance::MarkItemCollected(FName ItemId)
{
CollectedItemIDs.Add(ItemId);
}
bool UChildHoodGameInstance::IsDoorOpen(FName DoorId) const
{
const bool* Found = DoorStates.Find(DoorId);
return Found ? *Found : false;
}
void UChildHoodGameInstance::SetDoorOpen(FName DoorId, bool bIsOpen)
{
DoorStates.Add(DoorId, bIsOpen);
}
void UChildHoodGameInstance::ResetAll()
{
CollectedItemIDs.Empty();
DoorStates.Empty();
}
// v0.1: 空壳实现,持久数据由 UPersistenceSubsystem 管理
+6 -38
View File
@@ -1,5 +1,5 @@
// ChildHood Project - 跨关卡持久数据持有者
// 对应技术方案: docs/technical/交互系统-技术方案.md 第12节(跨场景持久化设计)
// ChildHood Project - GameInstance(v1.1 清理版)
// 持久数据已迁移至 UPersistenceSubsystem(UGameInstanceSubsystem)
#pragma once
@@ -10,11 +10,8 @@
/**
* UChildHoodGameInstance
*
* 跨关卡持久数据持有者。UE5 的 OpenLevel() 会销毁整个旧 World(包括
* Character 及其所有 UActorComponent),因此需要将持久数据与运行时行为分离。
*
* v0.1 为单场景原型,此 GameInstance 以空数据初始化。
* 后续多场景版本只需在 Project Settings 中将 GameInstance Class 设为此类即可。
* v0.1: 空壳,持久化数据由 UPersistenceSubsystem 管理。
* 后续可添加:全局配置、存档管理、音频设置等 GameInstance 级功能。
*/
UCLASS()
class UChildHoodGameInstance : public UGameInstance
@@ -22,35 +19,6 @@ class UChildHoodGameInstance : public UGameInstance
GENERATED_BODY()
public:
// === 物品消耗状态 ===
/** 查询指定物品是否已被收集(跨场景持久) */
bool IsItemCollected(FName ItemId) const;
/** 标记指定物品为已收集(写入持久层) */
void MarkItemCollected(FName ItemId);
// === 门状态 ===
/** 查询指定门的开关状态(跨场景持久),未记录的门默认为关闭 */
bool IsDoorOpen(FName DoorId) const;
/** 设置指定门的开关状态(写入持久层) */
void SetDoorOpen(FName DoorId, bool bIsOpen);
// === 重置 ===
/** 重置所有持久数据(新游戏时调用) */
void ResetAll();
private:
/** 已收集物品ID集合 */
UPROPERTY()
TSet<FName> CollectedItemIDs;
/** 门开关状态映射(true=打开,false=关闭) */
UPROPERTY()
TMap<FName, bool> DoorStates;
// v0.1: 空壳,持久化数据由 UPersistenceSubsystem 管理
// 后续可添加:全局配置、存档管理、音频设置等 GameInstance 级功能
};
@@ -3,6 +3,8 @@
#include "Interaction/InteractableComponent.h"
#include "Interaction/InteractableActor.h"
#include "Interaction/PersistenceSubsystem.h"
#include "SystemDriver.h"
#include "Interaction/InteractionManager.h"
#include "ChildHood.h"
#include "Components/SphereComponent.h"
@@ -10,10 +12,6 @@
#include "GameFramework/GameInstance.h"
#include "GameFramework/Character.h"
// 前向声明对应的头文件在.cpp中include
// ASystemDriver 在 Phase 2 创建,此处前向声明即可
class ASystemDriver;
// ============================================================================
// 构造函数
// ============================================================================
@@ -164,9 +162,15 @@ void UInteractableComponent::OnDetectionOverlapBegin(UPrimitiveComponent* Overla
UE_LOG(LogChildHood, Verbose, TEXT("检测到角色进入范围: %s ← %s"),
*Data.DisplayName, *OtherActor->GetName());
// Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().RegisterInteractable(this)
// 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建
// TODO(Phase 2): 替换为 ASystemDriver 路径
// 通过 ASystemDriver 注册到 InteractionManager
if (ASystemDriver* Driver = ASystemDriver::Get(this))
{
Driver->GetInteractionManager().RegisterInteractable(this);
}
else
{
UE_LOG(LogChildHood, Warning, TEXT("InteractableComponent: ASystemDriver 不存在,无法注册 %s"), *Data.DisplayName);
}
}
void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* OverlappedComponent,
@@ -182,7 +186,9 @@ void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* Overlapp
UE_LOG(LogChildHood, Verbose, TEXT("角色离开范围: %s ← %s"),
*Data.DisplayName, *OtherActor->GetName());
// Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().UnregisterInteractable(this)
// 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建
// TODO(Phase 2): 替换为 ASystemDriver 路径
// 通过 ASystemDriver 从 InteractionManager 注销
if (ASystemDriver* Driver = ASystemDriver::Get(this))
{
Driver->GetInteractionManager().UnregisterInteractable(this);
}
}
File diff suppressed because it is too large Load Diff
+88 -178
View File
@@ -1,225 +1,135 @@
// ChildHood Project - 交互管理器核心组件
// ChildHood Project - 交互管理器(v1.1 Effect/Action 架构重写)
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第6节(行为规则实现)
// 职责:交互全流程状态机、检测选择、类型分发、摄像机/转向/UI 控制
// 职责:检测列表维护、选中管理、顶层状态(Free/Executing)
// 架构:普通 C++ 类(非 UObject、非 UActorComponent),由 ASystemDriver 值持有
// 特点:纯事件驱动无 Tick,所有变化由外部调用触发
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InteractionTypes.h"
#include "InteractionManager.generated.h"
class UInteractableComponent;
class APushCameraActor;
class AChildHoodCharacter;
class UInteractionPromptWidget;
class UNarrativeTextWidget;
class ACharacter;
/**
* UInteractionManager
* FInteractionManager
*
* 挂载在 AChildHoodCharacter 上的组件,负责交互全流程管理:
* - 层级状态机驱动(Free / Interacting + 子阶段)
* - 附近交互物检测列表排序与选中管理
* - 四种交互类型分发(观察/拾取/操作/对话)
* - 角色自动转向插值
* - 摄像机推镜/回位控制
* - UI 提示与文本显示控制(v0.1 先用日志)
* 普通C++类,负责交互检测列表维护和顶层状态管理。
* 由 ASystemDriver 值持有,不参与 UE 反射系统。
*
* 数据流:
* InteractableComponent 碰撞回调 → ASystemDriver → RegisterInteractable/UnregisterInteractable
* Character 输入回调 → ASystemDriver → BeginInteract/CancelInteract/SwitchTarget
* EffectManager 完成回调 → ASystemDriver → OnInteractionFinished
*/
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
class UInteractionManager : public UActorComponent
class FInteractionManager
{
GENERATED_BODY()
public:
UInteractionManager();
/** 初始化管理器,绑定玩家角色引用 */
void Initialize(ACharacter* PlayerCharacter);
// === 公开方法 ===
// === 交互物注册(由 InteractableComponent 碰撞回调通过 ASystemDriver 调用)===
/** 发起交互(E键触发),前置检查通过后进入 Enter 阶段 */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void BeginInteract();
/** 取消/结束交互(ESC/E键在Active阶段触发),AnimLock期间不可取消 */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void CancelInteract();
/** 切换选中交互物(Tab/滚轮),Direction: +1下一个 / -1上一个 */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void SwitchTarget(int32 Direction);
/** 注册交互物到检测列表(由 InteractableComponent 的碰撞回调触发) */
UFUNCTION(BlueprintCallable, Category = "Interaction")
/**
* 注册交互物到检测列表。
* 添加后按水平距离排序,重置选中索引为0,刷新缓存。
*/
void RegisterInteractable(UInteractableComponent* Interactable);
/** 从检测列表注销交互物(离开范围或消耗时触发) */
UFUNCTION(BlueprintCallable, Category = "Interaction")
/**
* 从检测列表注销交互物。
* Executing 状态下如果移除的是 CurrentInteractable,仅从列表移除,
* CurrentInteractable 延迟清理(因为 Effect 可能仍在执行)。
*/
void UnregisterInteractable(UInteractableComponent* Interactable);
// === 委托 ===
// === 交互触发(Character 输入回调中调用)===
/** 物品拾取事件 — 通知物品系统(Pickup 类型 ExecuteAnimEvent 中广播) */
UPROPERTY(BlueprintAssignable, Category = "Interaction|Events")
FMLocalItemEvent OnItemPickedUp;
/**
* 发起交互(E键触发)。
* 前置检查:State==Free、SelectedIndex有效、!IsConsumed()。
* 成功后 State=Executing,返回选中的 Component。
* 拒绝则返回 nullptr。
*/
UInteractableComponent* BeginInteract();
/** 物品获得事件 — 通知收集系统(Search 类型 OnAnimLockComplete 中广播) */
UPROPERTY(BlueprintAssignable, Category = "Interaction|Events")
FMLocalItemEvent OnItemFound;
/**
* 取消交互(ESC/E键在Executing阶段触发)。
* v0.1 简化:仅设置状态标记,实际取消由 ASystemDriver 协调 EffectManager。
*/
void CancelInteract();
// === 可调参数 ===
/**
* 切换选中目标(Tab/滚轮)。
* Direction: +1下一个 / -1上一个,循环。
* 仅在 Free 状态下响应。
*/
void SwitchTarget(int32 Direction);
/** 角色转向时长(秒) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
float TurnDuration = 0.2f;
// === 状态查询 ===
/** 提示 UI 淡入时长(秒) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
float PromptFadeInDuration = 0.2f;
/** 获取当前顶层状态 */
EInteractionState GetState() const { return State; }
/** 提示 UI 淡出时长(秒) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
float PromptFadeOutDuration = 0.2f;
/** 当前是否有有效的选中交互物 */
bool HasSelection() const { return CachedSelected.bHasSelection; }
protected:
/** 获取选中交互物的提示文本(从缓存读取,不访问 Component) */
FString GetSelectedPromptText() const { return CachedSelected.PromptText; }
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/** 获取选中交互物的世界坐标(从缓存读取) */
FVector GetSelectedWorldPosition() const { return CachedSelected.WorldPosition; }
/** 获取完整的选中信息缓存 */
const FSelectedInfo& GetCachedSelectedInfo() const { return CachedSelected; }
// === Effect 完成回调(由 EffectManager 通过 ASystemDriver 调用)===
/**
* Effect 执行完毕回调。
* State 回到 Free,清理 CurrentInteractable,通知 Character 解除交互,
* 重新排序和刷新缓存(因为消耗可能改变了列表)。
*/
void OnInteractionFinished();
// === 当前交互物访问(EffectManager 需要访问)===
/** 获取当前正在交互的交互物(Executing 状态下有效) */
UInteractableComponent* GetCurrentInteractable() const;
private:
// === 状态机 ===
/** 交互系统顶层状态 */
EInteractionState State = EInteractionState::Free;
/** Interacting 子阶段 */
EInteractPhase Phase = EInteractPhase::None;
// === 检测与选择 ===
/** 当前附近的交互物列表,按水平距离排序 */
UPROPERTY()
TArray<UInteractableComponent*> NearbyInteractables;
TArray<TWeakObjectPtr<UInteractableComponent>> NearbyInteractables;
/** 当前选中的交互物索引 */
/** 当前选中的交互物索引(NearbyInteractables 中的下标) */
int32 SelectedIndex = 0;
/** 当前正在交互的交互物(交互过程中锁定) */
UPROPERTY()
UInteractableComponent* CurrentInteractable = nullptr;
/** 当前正在交互的交互物(Executing 状态下锁定) */
TWeakObjectPtr<UInteractableComponent> CurrentInteractable;
// === 摄像机控制 ===
/** 玩家角色弱引用(用于排序距离计算和 SetInteracting 调用) */
TWeakObjectPtr<ACharacter> PlayerCharacter;
/** 推镜用临时摄像机 Actor(BeginPlay 中 Spawn) */
UPROPERTY()
TObjectPtr<APushCameraActor> PushCameraActor;
/** 推镜是否已发起(Enter阶段一次性标志) */
bool bPushStarted = false;
// === 计时器 ===
/** 转向已用时间 */
float TurnElapsed = 0.0f;
/** 推镜后等待已用时间 */
float WaitElapsed = 0.0f;
/** 动画事件计时器(如60%时物品消失) */
FTimerHandle AnimEventTimerHandle;
/** 动画结束计时器 */
FTimerHandle AnimEndTimerHandle;
/** 退出阶段回位计时器 */
FTimerHandle ExitTimerHandle;
/** 动画事件是否已触发 */
bool bAnimEventFired = false;
// === UI 配置 ===
/** 提示 UI 蓝图类(需在编辑器中指定为 WBP_InteractionPrompt) */
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
TSubclassOf<UInteractionPromptWidget> PromptWidgetClass;
/** 文本底框 UI 蓝图类(需在编辑器中指定为 WBP_NarrativeText) */
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
TSubclassOf<UNarrativeTextWidget> NarrativeWidgetClass;
/** 提示 UI 高度偏移(交互物上方的世界空间 Z 偏移,cm) */
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
float PromptHeightOffset = 80.0f;
// === UI 实例 ===
/** 交互提示 Widget 实例 */
UPROPERTY()
TObjectPtr<UInteractionPromptWidget> PromptWidget;
/** 文本底框 Widget 实例 */
UPROPERTY()
TObjectPtr<UNarrativeTextWidget> NarrativeWidget;
// === 文本选择 ===
/** 上次显示的叙事文本(用于不重复选取) */
FString LastNarrativeText;
/** Presentation 层缓存数据 — 选中交互物的摘要信息 */
FSelectedInfo CachedSelected;
// === 内部方法 ===
/** 更新选中交互物(排序后 SelectedIndex = 0) */
/**
* 更新选中交互物:按与玩家的水平距离排序,重置 SelectedIndex=0。
* 同时清理已失效的弱引用。
*/
void UpdateSelection();
/** 更新提示 UI 内容和位置 */
void UpdatePromptUI();
/** 隐藏提示 UI */
void HidePrompt();
/** 更新提示 UI 的屏幕空间位置(跟随交互物世界位置投影) */
void UpdatePromptScreenPosition(UInteractableComponent* Interactable);
/** Enter 阶段 Tick:转向插值 + 推镜 + 等待 */
void TickEnterPhase(float DeltaTime);
/** AnimLock 阶段 Tick(v0.1 由计时器驱动,此处可为空) */
void TickAnimLockPhase(float DeltaTime);
/** Exit 阶段 Tick(v0.1 由定时器驱动回 Free) */
void TickExitPhase(float DeltaTime);
/** Enter 阶段完成后,根据交互类型分发到下一阶段 */
void TransitionFromEnter();
/** 进入 AnimLock 阶段:播放蒙太奇或启动计时器 */
void EnterAnimLockPhase();
/** 动画事件触发点回调(如拾取物消失、门状态切换) */
void ExecuteAnimEvent();
/** 动画结束回调 */
void OnAnimLockComplete();
/** 显示叙事文本(v0.1 用日志输出) */
void ShowNarrativeText(const FString& Text);
/** 隐藏叙事文本(v0.1 用日志输出) */
void HideNarrativeText();
/** 推镜:移动 PushCameraActor 并切换 ViewTarget */
void PushCamera(FVector Target, float Duration);
/** 相机回位:切换回 Character 的 FollowCamera */
void ReturnCamera(float Duration);
/** 计算推镜目标位置 */
FVector CalculatePushPosition(UInteractableComponent* Interactable);
/** 随机选取叙事文本,排除上次 */
FString PickRandomNarrative(const TArray<FString>& Texts);
/** Exit 阶段定时器回调:回到 Free 状态 */
void OnExitTimerComplete();
/**
* 从 Component 读取选中项信息并写入 CachedSelected。
* Presentation 层每帧只读 CachedSelected,不直接访问 Component。
*/
void RefreshCachedSelection();
};