Files
ChildHood/Source/ChildHood/Interaction/InteractionManager.cpp
T

898 lines
24 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ChildHood Project - 交互管理器核心组件 实现
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(行为规则实现)
#include "Interaction/InteractionManager.h"
#include "Interaction/InteractableComponent.h"
#include "Interaction/InteractableActor.h"
#include "Interaction/PushCameraActor.h"
#include "Interaction/InteractionPromptWidget.h"
#include "Interaction/NarrativeTextWidget.h"
#include "ChildHoodCharacter.h"
#include "ChildHoodGameInstance.h"
#include "ChildHood.h"
#include "GameFramework/Character.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/SphereComponent.h"
#include "Engine/World.h"
#include "TimerManager.h"
#include "Kismet/GameplayStatics.h"
#include "Blueprint/WidgetBlueprintLibrary.h"
// 定义交互系统日志类别
DECLARE_LOG_CATEGORY_EXTERN(LogInteraction, Log, All);
DEFINE_LOG_CATEGORY(LogInteraction);
// ============================================================================
// 构造函数
// ============================================================================
UInteractionManager::UInteractionManager()
{
// 默认关闭 Tick,首个交互物注册时开启
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = false;
}
// ============================================================================
// 生命周期
// ============================================================================
void UInteractionManager::BeginPlay()
{
Super::BeginPlay();
// Spawn 推镜用临时摄像机 Actor(使用专用的 APushCameraActor)
// 随 Character 生命周期,不反复创建/销毁
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = GetOwner();
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
APushCameraActor* SpawnedCamera = GetWorld()->SpawnActor<APushCameraActor>(APushCameraActor::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams);
PushCameraActor = SpawnedCamera;
if (PushCameraActor)
{
PushCameraActor->SetActorHiddenInGame(true);
}
UE_LOG(LogInteraction, Log, TEXT("InteractionManager: BeginPlay 完成, PushCameraActor=%s"),
PushCameraActor ? *PushCameraActor->GetName() : TEXT("nullptr"));
// --- 创建 UI Widget ---
APlayerController* PC = GetWorld()->GetFirstPlayerController();
if (PC)
{
// 创建提示 UI
if (PromptWidgetClass)
{
PromptWidget = CreateWidget<UInteractionPromptWidget>(PC, PromptWidgetClass);
if (PromptWidget)
{
PromptWidget->AddToViewport(10);
}
}
else
{
UE_LOG(LogInteraction, Warning, TEXT("InteractionManager: PromptWidgetClass 未设置,提示 UI 不可用"));
}
// 创建文本底框 UI
if (NarrativeWidgetClass)
{
NarrativeWidget = CreateWidget<UNarrativeTextWidget>(PC, NarrativeWidgetClass);
if (NarrativeWidget)
{
NarrativeWidget->AddToViewport(20);
}
}
else
{
UE_LOG(LogInteraction, Warning, TEXT("InteractionManager: NarrativeWidgetClass 未设置,文本 UI 不可用"));
}
}
}
// ============================================================================
// Tick 驱动
// ============================================================================
void UInteractionManager::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (State == EInteractionState::Free)
{
// Free 状态下更新提示 UI 的屏幕位置
if (PromptWidget && PromptWidget->IsVisible() &&
NearbyInteractables.Num() > 0 && NearbyInteractables.IsValidIndex(SelectedIndex))
{
UInteractableComponent* Selected = NearbyInteractables[SelectedIndex];
if (Selected)
{
UpdatePromptScreenPosition(Selected);
}
}
return;
}
switch (Phase)
{
case EInteractPhase::Enter:
TickEnterPhase(DeltaTime);
break;
case EInteractPhase::AnimLock:
TickAnimLockPhase(DeltaTime);
break;
case EInteractPhase::Active:
// 等待玩家输入,无需 Tick 处理
break;
case EInteractPhase::Exit:
TickExitPhase(DeltaTime);
break;
case EInteractPhase::None:
default:
break;
}
}
// ============================================================================
// 公开方法
// ============================================================================
void UInteractionManager::BeginInteract()
{
// 前置检查:必须 Free 状态
if (State != EInteractionState::Free)
{
UE_LOG(LogInteraction, Verbose, TEXT("BeginInteract: 拒绝,当前状态不是 Free (State=%d)"),
static_cast<int32>(State));
return;
}
// 前置检查:必须有选中的交互物
if (NearbyInteractables.Num() == 0 || !NearbyInteractables.IsValidIndex(SelectedIndex))
{
UE_LOG(LogInteraction, Verbose, TEXT("BeginInteract: 拒绝,没有选中的交互物"));
return;
}
UInteractableComponent* Target = NearbyInteractables[SelectedIndex];
// 前置检查:交互物不能已被消耗
if (!Target || Target->IsConsumed())
{
UE_LOG(LogInteraction, Verbose, TEXT("BeginInteract: 拒绝,交互物已被消耗"));
return;
}
// 进入交互
State = EInteractionState::Interacting;
Phase = EInteractPhase::Enter;
CurrentInteractable = Target;
// 通知 Character 进入交互状态(屏蔽移动)
AChildHoodCharacter* Char = Cast<AChildHoodCharacter>(GetOwner());
if (Char)
{
Char->SetInteracting(true);
}
// 隐藏提示 UI
HidePrompt();
// 重置 Enter 阶段计时器
TurnElapsed = 0.0f;
WaitElapsed = 0.0f;
bPushStarted = false;
const FInteractableData& Data = Target->GetInteractableData();
UE_LOG(LogInteraction, Log, TEXT("BeginInteract: 开始交互 '%s' (Type=%d)"),
*Data.DisplayName, static_cast<int32>(Data.Type));
}
void UInteractionManager::CancelInteract()
{
// 前置检查:必须 Interacting 状态
if (State != EInteractionState::Interacting)
{
return;
}
// AnimLock 期间不可取消
if (Phase == EInteractPhase::AnimLock)
{
UE_LOG(LogInteraction, Verbose, TEXT("CancelInteract: 拒绝,AnimLock 阶段不可取消"));
return;
}
UE_LOG(LogInteraction, Log, TEXT("CancelInteract: 取消/结束交互"));
// 隐藏叙事文本
HideNarrativeText();
// 进入 Exit 阶段
Phase = EInteractPhase::Exit;
// 相机回位
const FInteractableData* Data = CurrentInteractable ? &CurrentInteractable->GetInteractableData() : nullptr;
float ReturnDuration = (Data && Data->bUseCameraPush) ? Data->CameraReturnDuration : 0.3f;
ReturnCamera(ReturnDuration);
// 清除可能残留的计时器
GetWorld()->GetTimerManager().ClearTimer(AnimEventTimerHandle);
GetWorld()->GetTimerManager().ClearTimer(AnimEndTimerHandle);
// 设置退出定时器,回位动画播完后回到 Free
GetWorld()->GetTimerManager().SetTimer(ExitTimerHandle, this, &UInteractionManager::OnExitTimerComplete, ReturnDuration, false);
}
void UInteractionManager::SwitchTarget(int32 Direction)
{
int32 Num = NearbyInteractables.Num();
if (Num == 0)
{
return;
}
// Interacting 状态下忽略切换
if (State != EInteractionState::Free)
{
return;
}
SelectedIndex = (SelectedIndex + Direction + Num) % Num;
UpdatePromptUI();
UE_LOG(LogInteraction, Log, TEXT("SwitchTarget: 切换到 %d/%d"), SelectedIndex, Num);
}
void UInteractionManager::RegisterInteractable(UInteractableComponent* Interactable)
{
if (!Interactable)
{
return;
}
// 去重检查
if (NearbyInteractables.Contains(Interactable))
{
return;
}
NearbyInteractables.Add(Interactable);
// 按水平距离排序(DistSquared2D,避免 sqrt)
AActor* Owner = GetOwner();
if (Owner)
{
FVector OwnerLoc = Owner->GetActorLocation();
NearbyInteractables.Sort([OwnerLoc](const UInteractableComponent& A, const UInteractableComponent& B)
{
float DistA = FVector::DistSquared2D(OwnerLoc, A.GetWorldLocation());
float DistB = FVector::DistSquared2D(OwnerLoc, B.GetWorldLocation());
return DistA < DistB;
});
}
UpdateSelection();
UpdatePromptUI();
// 首个交互物注册时开启 Tick
if (NearbyInteractables.Num() == 1)
{
SetComponentTickEnabled(true);
}
UE_LOG(LogInteraction, Log, TEXT("RegisterInteractable: 注册 '%s', 列表大小=%d"),
*Interactable->GetInteractableData().DisplayName, NearbyInteractables.Num());
}
void UInteractionManager::UnregisterInteractable(UInteractableComponent* Interactable)
{
if (!Interactable)
{
return;
}
bool bRemoved = NearbyInteractables.Remove(Interactable) > 0;
if (!bRemoved)
{
return;
}
// 如果移除的是当前正在交互的交互物,清空引用
if (CurrentInteractable == Interactable)
{
CurrentInteractable = nullptr;
}
if (NearbyInteractables.Num() > 0)
{
UpdateSelection();
UpdatePromptUI();
}
else
{
// 列表为空,隐藏提示
HidePrompt();
// 最后一个注销时关闭 Tick
SetComponentTickEnabled(false);
}
UE_LOG(LogInteraction, Log, TEXT("UnregisterInteractable: 注销 '%s', 列表大小=%d"),
*Interactable->GetInteractableData().DisplayName, NearbyInteractables.Num());
}
// ============================================================================
// Enter 阶段 Tick
// ============================================================================
void UInteractionManager::TickEnterPhase(float DeltaTime)
{
if (!CurrentInteractable)
{
UE_LOG(LogInteraction, Warning, TEXT("TickEnterPhase: CurrentInteractable 为空,强制取消"));
CancelInteract();
return;
}
ACharacter* Char = Cast<ACharacter>(GetOwner());
if (!Char)
{
return;
}
const FInteractableData& Data = CurrentInteractable->GetInteractableData();
// --- 1. 转向插值 ---
TurnElapsed += DeltaTime;
FVector CharLoc = Char->GetActorLocation();
FVector TargetLoc = CurrentInteractable->GetWorldLocation();
FVector DirToTarget = (TargetLoc - CharLoc);
DirToTarget.Z = 0.0f;
FRotator TargetRot = DirToTarget.Rotation();
// 使用 RInterpTo 进行平滑插值,InterpSpeed = 1/TurnDuration
float InterpSpeed = (TurnDuration > 0.f) ? (1.f / TurnDuration) : 10.f;
FRotator NewRot = FMath::RInterpTo(Char->GetActorRotation(), TargetRot, DeltaTime, InterpSpeed);
Char->SetActorRotation(NewRot);
// --- 2. 推镜(一次性发起) ---
if (Data.bUseCameraPush && !bPushStarted)
{
FVector PushPos = CalculatePushPosition(CurrentInteractable);
PushCamera(PushPos, Data.CameraPushDuration);
bPushStarted = true;
}
// --- 3. 判断完成 ---
bool bTurnComplete = TurnElapsed >= TurnDuration;
bool bPushComplete = !Data.bUseCameraPush || bPushStarted;
if (bTurnComplete && bPushComplete)
{
// 推镜后需要额外等待 PushWaitDelay
if (Data.bUseCameraPush && Data.PushWaitDelay > 0.f)
{
WaitElapsed += DeltaTime;
if (WaitElapsed >= Data.PushWaitDelay)
{
TransitionFromEnter();
}
}
else
{
TransitionFromEnter();
}
}
}
// ============================================================================
// AnimLock 阶段 Tick(v0.1 由计时器驱动,此处为空)
// ============================================================================
void UInteractionManager::TickAnimLockPhase(float DeltaTime)
{
// v0.1: AnimLock 由 AnimEventTimer 和 AnimEndTimer 驱动,无需 Tick 处理
}
// ============================================================================
// Exit 阶段 Tick(v0.1 由定时器驱动回 Free)
// ============================================================================
void UInteractionManager::TickExitPhase(float DeltaTime)
{
// v0.1: Exit 阶段由 ExitTimerHandle 定时器驱动回到 Free
// 此处可做相机回位进度检查等,简单方案留空
}
// ============================================================================
// Enter 完成后类型分发
// ============================================================================
void UInteractionManager::TransitionFromEnter()
{
if (!CurrentInteractable)
{
return;
}
const FInteractableData& Data = CurrentInteractable->GetInteractableData();
UE_LOG(LogInteraction, Log, TEXT("TransitionFromEnter: 类型=%d 分发"), static_cast<int32>(Data.Type));
switch (Data.Type)
{
case EInteractType::Examine:
Phase = EInteractPhase::Active;
ShowNarrativeText(PickRandomNarrative(Data.NarrativeTexts));
break;
case EInteractType::Pickup:
Phase = EInteractPhase::AnimLock;
EnterAnimLockPhase();
break;
case EInteractType::Operate:
if (Data.OperateSubType == EOperateSubType::Door)
{
Phase = EInteractPhase::AnimLock;
EnterAnimLockPhase();
}
else if (Data.OperateSubType == EOperateSubType::Search)
{
Phase = EInteractPhase::AnimLock;
EnterAnimLockPhase();
}
break;
case EInteractType::Talk:
Phase = EInteractPhase::Active;
ShowNarrativeText(PickRandomNarrative(Data.NarrativeTexts));
break;
default:
UE_LOG(LogInteraction, Warning, TEXT("TransitionFromEnter: 未知交互类型 %d"), static_cast<int32>(Data.Type));
// 未知类型直接回 Free
CancelInteract();
break;
}
}
// ============================================================================
// AnimLock 阶段
// ============================================================================
void UInteractionManager::EnterAnimLockPhase()
{
if (!CurrentInteractable)
{
return;
}
const FInteractableData& Data = CurrentInteractable->GetInteractableData();
// 计算总动画时长:有蒙太奇用蒙太奇长度,否则用配置的 AnimDuration
float TotalAnimTime = Data.AnimDuration;
if (Data.InteractMontage)
{
TotalAnimTime = Data.InteractMontage->GetPlayLength();
if (TotalAnimTime <= 0.f)
{
TotalAnimTime = Data.AnimDuration;
}
// 播放蒙太奇
ACharacter* Char = Cast<ACharacter>(GetOwner());
if (Char && Char->GetMesh())
{
Char->GetMesh()->GetAnimInstance()->Montage_Play(Data.InteractMontage);
UE_LOG(LogInteraction, Log, TEXT("EnterAnimLockPhase: 播放蒙太奇, 时长=%.2f"), TotalAnimTime);
}
}
else
{
UE_LOG(LogInteraction, Log, TEXT("EnterAnimLockPhase: 无蒙太奇,使用计时器, 时长=%.2f"), TotalAnimTime);
}
// 动画事件触发计时器(如 60% 时物品消失)
float EventTime = TotalAnimTime * Data.AnimEventTimepoint;
GetWorld()->GetTimerManager().SetTimer(AnimEventTimerHandle, this, &UInteractionManager::ExecuteAnimEvent, EventTime, false);
// 动画结束计时器
GetWorld()->GetTimerManager().SetTimer(AnimEndTimerHandle, this, &UInteractionManager::OnAnimLockComplete, TotalAnimTime, false);
bAnimEventFired = false;
}
// ============================================================================
// 动画事件触发回调
// ============================================================================
void UInteractionManager::ExecuteAnimEvent()
{
if (!CurrentInteractable)
{
return;
}
bAnimEventFired = true;
const FInteractableData& Data = CurrentInteractable->GetInteractableData();
UE_LOG(LogInteraction, Log, TEXT("ExecuteAnimEvent: 类型=%d, 名称='%s'"),
static_cast<int32>(Data.Type), *Data.DisplayName);
switch (Data.Type)
{
case EInteractType::Pickup:
{
// 标记消耗 → 隐藏 Actor → 通知物品系统
CurrentInteractable->Consume();
// 隐藏交互物 Actor
if (AInteractableActor* OwnerActor = Cast<AInteractableActor>(CurrentInteractable->GetOwner()))
{
OwnerActor->SetActorHiddenInGame(true);
}
// 广播拾取事件
OnItemPickedUp.Broadcast(Data.ItemId, Data.DisplayName, FName(TEXT("杂物")));
UE_LOG(LogInteraction, Log, TEXT("ExecuteAnimEvent: Pickup 拾取完成 '%s'"), *Data.DisplayName);
}
break;
case EInteractType::Operate:
{
if (Data.OperateSubType == EOperateSubType::Door)
{
// 切换门状态
CurrentInteractable->ToggleDoor();
// 持久化门状态到 GameInstance
if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance<UChildHoodGameInstance>())
{
GI->SetDoorOpen(Data.ItemId, CurrentInteractable->IsDoorOpen());
}
// 通知 Actor 处理 Mesh 旋转等视觉反馈
if (AInteractableActor* OwnerActor = Cast<AInteractableActor>(CurrentInteractable->GetOwner()))
{
OwnerActor->OnInteractStarted(CurrentInteractable);
}
UE_LOG(LogInteraction, Log, TEXT("ExecuteAnimEvent: Door 门状态切换, 开=%s"),
CurrentInteractable->IsDoorOpen() ? TEXT("true") : TEXT("false"));
}
// Search 类型的结果判定在 OnAnimLockComplete 中处理
}
break;
default:
break;
}
}
// ============================================================================
// 动画结束回调
// ============================================================================
void UInteractionManager::OnAnimLockComplete()
{
if (!CurrentInteractable)
{
return;
}
const FInteractableData& Data = CurrentInteractable->GetInteractableData();
UE_LOG(LogInteraction, Log, TEXT("OnAnimLockComplete: 类型=%d, 名称='%s'"),
static_cast<int32>(Data.Type), *Data.DisplayName);
switch (Data.Type)
{
case EInteractType::Pickup:
{
// 直接回到 Free(无 Active/Exit,无相机/UI 需清理)
AChildHoodCharacter* Char = Cast<AChildHoodCharacter>(GetOwner());
if (Char)
{
Char->SetInteracting(false);
}
State = EInteractionState::Free;
Phase = EInteractPhase::None;
CurrentInteractable = nullptr;
// 从检测列表移除已消耗的交互物
// Consume 已在 ExecuteAnimEvent 中调用,但 UnregisterInteractable 应由 DetectionSphere 禁用触发
// 这里主动从列表移除,确保提示 UI 正确更新
if (NearbyInteractables.Num() > 0)
{
UpdateSelection();
UpdatePromptUI();
}
UE_LOG(LogInteraction, Log, TEXT("OnAnimLockComplete: Pickup 完成,回到 Free"));
}
break;
case EInteractType::Operate:
{
if (Data.OperateSubType == EOperateSubType::Door)
{
// 直接回到 Free
AChildHoodCharacter* Char = Cast<AChildHoodCharacter>(GetOwner());
if (Char)
{
Char->SetInteracting(false);
}
State = EInteractionState::Free;
Phase = EInteractPhase::None;
CurrentInteractable = nullptr;
// 更新提示(门动词可能变化)
UpdateSelection();
UpdatePromptUI();
UE_LOG(LogInteraction, Log, TEXT("OnAnimLockComplete: Door 完成,回到 Free"));
}
else if (Data.OperateSubType == EOperateSubType::Search)
{
// 翻找结果判定:v0.1 必定有物品
CurrentInteractable->Consume();
OnItemFound.Broadcast(Data.ItemId, Data.DisplayName, FName(TEXT("杂物")));
UE_LOG(LogInteraction, Log, TEXT("OnAnimLockComplete: Search 翻找完成,获得物品 '%s'"), *Data.DisplayName);
// 进入 Active 阶段等待玩家按 E/ESC 关闭
Phase = EInteractPhase::Active;
// 有物品时不显示文本底框(架构预留)
}
}
break;
default:
break;
}
}
// ============================================================================
// UI 方法(v0.1 用日志替代)
// ============================================================================
void UInteractionManager::UpdateSelection()
{
// 排序后选中索引重置为 0(最近的)
if (AActor* Owner = GetOwner())
{
FVector OwnerLoc = Owner->GetActorLocation();
NearbyInteractables.Sort([OwnerLoc](const UInteractableComponent& A, const UInteractableComponent& B)
{
float DistA = FVector::DistSquared2D(OwnerLoc, A.GetWorldLocation());
float DistB = FVector::DistSquared2D(OwnerLoc, B.GetWorldLocation());
return DistA < DistB;
});
}
SelectedIndex = 0;
}
void UInteractionManager::UpdatePromptUI()
{
if (NearbyInteractables.Num() == 0 || !NearbyInteractables.IsValidIndex(SelectedIndex))
{
HidePrompt();
return;
}
UInteractableComponent* Selected = NearbyInteractables[SelectedIndex];
if (!Selected)
{
HidePrompt();
return;
}
FString PromptText = Selected->GetPromptText();
if (PromptWidget)
{
PromptWidget->SetPromptText(PromptText);
PromptWidget->Show();
UpdatePromptScreenPosition(Selected);
}
UE_LOG(LogInteraction, Verbose, TEXT("UpdatePromptUI: %s"), *PromptText);
}
void UInteractionManager::HidePrompt()
{
if (PromptWidget)
{
PromptWidget->Hide();
}
}
void UInteractionManager::ShowNarrativeText(const FString& Text)
{
if (Text.IsEmpty())
{
UE_LOG(LogInteraction, Warning, TEXT("ShowNarrativeText: text is empty"));
return;
}
if (NarrativeWidget)
{
NarrativeWidget->SetNarrativeText(Text);
NarrativeWidget->Show();
}
UE_LOG(LogInteraction, Verbose, TEXT("ShowNarrativeText: %s"), *Text);
}
void UInteractionManager::HideNarrativeText()
{
if (NarrativeWidget)
{
NarrativeWidget->Hide();
}
}
// ============================================================================
// 摄像机控制
// ============================================================================
void UInteractionManager::PushCamera(FVector Target, float Duration)
{
if (!PushCameraActor)
{
UE_LOG(LogInteraction, Warning, TEXT("PushCamera: PushCameraActor 为空"));
return;
}
// 计算朝向交互物的旋转
FVector LookTarget = CurrentInteractable ? CurrentInteractable->GetWorldLocation() : Target;
FRotator LookAtRot = (LookTarget - Target).Rotation();
PushCameraActor->SetActorHiddenInGame(false);
PushCameraActor->SetActorLocationAndRotation(Target, LookAtRot);
// 切换 ViewTarget
APlayerController* PC = GetWorld()->GetFirstPlayerController();
if (PC)
{
PC->SetViewTargetWithBlend(PushCameraActor, Duration, VTBlend_EaseOut);
UE_LOG(LogInteraction, Log, TEXT("PushCamera: 推镜到 %s, 时长=%.2f"), *Target.ToString(), Duration);
}
}
void UInteractionManager::ReturnCamera(float Duration)
{
AActor* Owner = GetOwner();
if (!Owner)
{
return;
}
APlayerController* PC = GetWorld()->GetFirstPlayerController();
if (PC)
{
// 回位到 Character
PC->SetViewTargetWithBlend(Owner, Duration, VTBlend_EaseIn);
UE_LOG(LogInteraction, Log, TEXT("ReturnCamera: 相机回位, 时长=%.2f"), Duration);
}
// 隐藏推镜 Actor
if (PushCameraActor)
{
PushCameraActor->SetActorHiddenInGame(true);
}
}
// ============================================================================
// 推镜位置计算
// ============================================================================
FVector UInteractionManager::CalculatePushPosition(UInteractableComponent* Interactable)
{
if (!Interactable)
{
return FVector::ZeroVector;
}
const FInteractableData& Data = Interactable->GetInteractableData();
FVector InteractPos = Interactable->GetWorldLocation();
FVector CharPos = GetOwner() ? GetOwner()->GetActorLocation() : FVector::ZeroVector;
FVector Dir = (InteractPos - CharPos);
Dir.Z = 0.0f;
Dir.Normalize();
// 交互物前方(面向角色的一侧),推镜距离偏移
FVector TargetPos = InteractPos - Dir * Data.CameraPushDistance;
// 高度略低于交互物中心(微仰视)
TargetPos.Z = InteractPos.Z * 0.8f;
return TargetPos;
}
// ============================================================================
// 叙事文本随机选取
// ============================================================================
FString UInteractionManager::PickRandomNarrative(const TArray<FString>& Texts)
{
if (Texts.Num() == 0)
{
return TEXT("");
}
if (Texts.Num() == 1)
{
LastNarrativeText = Texts[0];
return Texts[0];
}
FString Candidate;
int32 Attempts = 0;
do
{
Candidate = Texts[FMath::RandRange(0, Texts.Num() - 1)];
Attempts++;
} while (Candidate == LastNarrativeText && Attempts < 10);
LastNarrativeText = Candidate;
return Candidate;
}
// ============================================================================
// Exit 阶段定时器回调
// ============================================================================
void UInteractionManager::OnExitTimerComplete()
{
UE_LOG(LogInteraction, Log, TEXT("OnExitTimerComplete: Exit 阶段结束,回到 Free"));
// 通知 Character 解除交互状态
AChildHoodCharacter* Char = Cast<AChildHoodCharacter>(GetOwner());
if (Char)
{
Char->SetInteracting(false);
}
// 回到 Free 状态
State = EInteractionState::Free;
Phase = EInteractPhase::None;
CurrentInteractable = nullptr;
// 如果仍在检测范围内,重新显示提示
if (NearbyInteractables.Num() > 0)
{
UpdateSelection();
UpdatePromptUI();
}
}
// ============================================================================
// 提示 UI 屏幕位置更新
// ============================================================================
void UInteractionManager::UpdatePromptScreenPosition(UInteractableComponent* Interactable)
{
if (!Interactable || !PromptWidget) return;
APlayerController* PC = GetWorld()->GetFirstPlayerController();
if (!PC) return;
// 交互物世界位置 + 高度偏移 → 屏幕坐标
FVector WorldLoc = Interactable->GetWorldLocation() + FVector(0.f, 0.f, PromptHeightOffset);
FVector2D ScreenPos;
if (UGameplayStatics::ProjectWorldToScreen(PC, WorldLoc, ScreenPos, true))
{
PromptWidget->SetScreenPosition(ScreenPos);
}
}