898 lines
24 KiB
C++
898 lines
24 KiB
C++
// 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);
|
||
}
|
||
}
|