1e5f40e633
P-016: 将EffectManager的ShowNarrative/HideNarrative/ShowPickupNotification 从日志占位改为真实CreateWidget+AddToViewport+淡入淡出生命周期。 - 新增UPickupNotificationWidget C++基类(对称NarrativeTextWidget) - EffectManager新增SetWidgetClasses注入接口与Widget弱引用缓存 - PerformCleanup补齐Narrative/Pickup Widget兜底淡出移除 - ASystemDriver新增两个TSubclassOf配置点,BeginPlay注入给EffectManager - Widget Class未注入时回退日志占位,保证Effect流程不卡 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
608 lines
18 KiB
C++
608 lines
18 KiB
C++
// ChildHood Project - Effect Timeline 调度器 实现
|
||
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节 + 第6节
|
||
|
||
#include "Interaction/EffectManager.h"
|
||
#include "Engine/World.h"
|
||
#include "GameFramework/PlayerController.h"
|
||
#include "GameFramework/Character.h"
|
||
#include "Interaction/InteractableComponent.h"
|
||
#include "Interaction/PersistenceSubsystem.h"
|
||
#include "Interaction/PushCameraActor.h"
|
||
#include "Interaction/NarrativeTextWidget.h"
|
||
#include "Interaction/PickupNotificationWidget.h"
|
||
#include "Blueprint/UserWidget.h"
|
||
#include "Components/SphereComponent.h"
|
||
#include "Interaction/DoorInteractableComponent.h"
|
||
#include "Interaction/DoorActor.h"
|
||
#include "Kismet/GameplayStatics.h"
|
||
#include "Animation/AnimInstance.h"
|
||
|
||
// ============================================================================
|
||
// 公开接口
|
||
// ============================================================================
|
||
|
||
void FEffectManager::SetOnEffectComplete(FOnEffectComplete&& Callback)
|
||
{
|
||
OnEffectComplete = MoveTemp(Callback);
|
||
}
|
||
|
||
void FEffectManager::SetOnItemAddedCallback(FOnItemAdded&& Callback)
|
||
{
|
||
OnItemAdded = MoveTemp(Callback);
|
||
}
|
||
|
||
void FEffectManager::SetWidgetClasses(TSubclassOf<UNarrativeTextWidget> NarrativeClass,
|
||
TSubclassOf<UPickupNotificationWidget> PickupClass)
|
||
{
|
||
NarrativeWidgetClass = NarrativeClass;
|
||
PickupWidgetClass = PickupClass;
|
||
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: WidgetClasses 注入 — Narrative=%s, Pickup=%s"),
|
||
NarrativeClass ? *NarrativeClass->GetName() : TEXT("(null,日志占位)"),
|
||
PickupClass ? *PickupClass->GetName() : TEXT("(null,日志占位)"));
|
||
}
|
||
|
||
void FEffectManager::PlayEffect(const FInteractEffect& Effect, const FEffectContext& Context)
|
||
{
|
||
// 如果有正在播放的 Effect,先强制清理
|
||
if (bIsPlaying)
|
||
{
|
||
PerformCleanup();
|
||
}
|
||
|
||
CurrentEffect = Effect;
|
||
CurrentContext = Context;
|
||
ElapsedTime = 0.f;
|
||
NextActionIndex = 0;
|
||
bIsPlaying = true;
|
||
bIsWaitingForInput = false;
|
||
TurnState.Reset();
|
||
LastNarrativeIndex = -1;
|
||
CachedPushCamera = nullptr;
|
||
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: PlayEffect '%s' (Duration=%.2f, Actions=%d)"),
|
||
*CurrentEffect.EffectName, CurrentEffect.TotalDuration, CurrentEffect.Actions.Num());
|
||
}
|
||
|
||
void FEffectManager::CancelEffect()
|
||
{
|
||
if (!bIsPlaying) return;
|
||
if (!CurrentEffect.bCancellable) return;
|
||
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: CancelEffect (WaitingForInput=%s)"),
|
||
bIsWaitingForInput ? TEXT("true") : TEXT("false"));
|
||
|
||
if (bIsWaitingForInput)
|
||
{
|
||
// 用户输入是期望的恢复信号 -> 走正常恢复
|
||
ResumeFromWait();
|
||
return;
|
||
}
|
||
|
||
// 非暂停态强制取消
|
||
bIsPlaying = false;
|
||
PerformCleanup();
|
||
}
|
||
|
||
// ============================================================================
|
||
// 每帧更新
|
||
// ============================================================================
|
||
|
||
void FEffectManager::Update(float DeltaTime)
|
||
{
|
||
if (!bIsPlaying) return;
|
||
if (bIsWaitingForInput) return; // 暂停等待用户输入
|
||
|
||
// 1. 推进时间
|
||
ElapsedTime += DeltaTime;
|
||
|
||
// 2. 检查是否到达 TotalDuration
|
||
if (ElapsedTime >= CurrentEffect.TotalDuration)
|
||
{
|
||
FinishEffect();
|
||
return;
|
||
}
|
||
|
||
// 3. 触发到时的 Action
|
||
while (NextActionIndex < CurrentEffect.Actions.Num())
|
||
{
|
||
const FInteractAction& Action = CurrentEffect.Actions[NextActionIndex];
|
||
if (ElapsedTime >= Action.TriggerTime)
|
||
{
|
||
ExecuteAction(Action);
|
||
NextActionIndex++;
|
||
}
|
||
else
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 4. 更新 TurnToTarget 插值
|
||
UpdateTurn(DeltaTime);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Action 分发
|
||
// ============================================================================
|
||
|
||
void FEffectManager::ExecuteAction(const FInteractAction& Action)
|
||
{
|
||
switch (Action.Type)
|
||
{
|
||
case EInteractActionType::Wait:
|
||
ExecuteWait(Action);
|
||
break;
|
||
case EInteractActionType::TurnToTarget:
|
||
ExecuteTurnToTarget(Action);
|
||
break;
|
||
case EInteractActionType::PlayMontage:
|
||
ExecutePlayMontage(Action);
|
||
break;
|
||
case EInteractActionType::CameraPush:
|
||
ExecuteCameraPush(Action);
|
||
break;
|
||
case EInteractActionType::CameraReturn:
|
||
ExecuteCameraReturn(Action);
|
||
break;
|
||
case EInteractActionType::Consume:
|
||
ExecuteConsume();
|
||
break;
|
||
case EInteractActionType::HideActor:
|
||
ExecuteHideActor();
|
||
break;
|
||
case EInteractActionType::ShowActor:
|
||
ExecuteShowActor();
|
||
break;
|
||
case EInteractActionType::ShowNarrative:
|
||
ExecuteShowNarrative(Action);
|
||
break;
|
||
case EInteractActionType::HideNarrative:
|
||
ExecuteHideNarrative();
|
||
break;
|
||
case EInteractActionType::ShowPickupNotification:
|
||
ExecuteShowPickupNotification();
|
||
break;
|
||
case EInteractActionType::ToggleDoor:
|
||
ExecuteToggleDoor();
|
||
break;
|
||
// AddToBackpack:通过 Driver 回调链通知收集系统(收集系统技术方案第13节)
|
||
case EInteractActionType::AddToBackpack:
|
||
if (CurrentContext.TargetComp.IsValid())
|
||
{
|
||
const FInteractableData& ItemData = CurrentContext.TargetComp->GetInteractableData();
|
||
if (OnItemAdded.IsBound())
|
||
{
|
||
// 回调连线:EffectManager → ASystemDriver → FCollectionManager::OnItemObtained
|
||
OnItemAdded.Execute(ItemData.ItemId, ItemData.DisplayName, ItemData.Category);
|
||
}
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: AddToBackpack - Item: %s (Category: %s)"),
|
||
*ItemData.DisplayName, *ItemData.Category.ToString());
|
||
}
|
||
break;
|
||
case EInteractActionType::PlayMontageOnTarget:
|
||
case EInteractActionType::PlayDialogue:
|
||
case EInteractActionType::PlayStory:
|
||
case EInteractActionType::BroadcastEvent:
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: Action %d not implemented in v0.1"),
|
||
static_cast<int32>(Action.Type));
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Effect 结束 / 清理 / 恢复
|
||
// ============================================================================
|
||
|
||
void FEffectManager::FinishEffect()
|
||
{
|
||
bIsPlaying = false;
|
||
PerformCleanup();
|
||
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: FinishEffect '%s'"), *CurrentEffect.EffectName);
|
||
|
||
// 广播完成回调
|
||
if (OnEffectComplete.IsBound())
|
||
{
|
||
OnEffectComplete.Execute();
|
||
}
|
||
}
|
||
|
||
void FEffectManager::ResumeFromWait()
|
||
{
|
||
bIsWaitingForInput = false;
|
||
// Timeline 继续推进,后续 Action 按 TriggerTime 正常触发
|
||
}
|
||
|
||
void FEffectManager::PerformCleanup()
|
||
{
|
||
// 1. 文本底框强制淡出移除(ShowNarrative 曾显示但未 HideNarrative 时兜底)
|
||
RemoveNarrativeWidget();
|
||
RemovePickupWidget();
|
||
|
||
// 2. 相机回位(如果推镜过)
|
||
if (CachedPushCamera.IsValid())
|
||
{
|
||
if (CurrentContext.Instigator.IsValid())
|
||
{
|
||
APlayerController* PC = Cast<APlayerController>(
|
||
CurrentContext.Instigator.Get()->GetController());
|
||
if (PC)
|
||
{
|
||
PC->SetViewTargetWithBlend(CurrentContext.Instigator.Get(), 0.2f);
|
||
}
|
||
}
|
||
|
||
// 销毁推镜摄像机
|
||
APushCameraActor* PushCam = CachedPushCamera.Get();
|
||
CachedPushCamera = nullptr;
|
||
if (PushCam && PushCam->GetWorld())
|
||
{
|
||
PushCam->Destroy();
|
||
}
|
||
}
|
||
|
||
// 3. 蒙太奇停止
|
||
if (CurrentContext.Instigator.IsValid())
|
||
{
|
||
ACharacter* Char = CurrentContext.Instigator.Get();
|
||
if (Char->GetMesh() && Char->GetMesh()->GetAnimInstance())
|
||
{
|
||
Char->GetMesh()->GetAnimInstance()->Montage_Stop(0.2f);
|
||
}
|
||
}
|
||
|
||
// 4. TurnToTarget 未完成 -> 瞬移到目标旋转
|
||
if (TurnState.bIsActive && CurrentContext.Instigator.IsValid())
|
||
{
|
||
CurrentContext.Instigator.Get()->SetActorRotation(TurnState.TargetRotation);
|
||
TurnState.Reset();
|
||
}
|
||
|
||
// 5. 重置所有状态变量
|
||
ElapsedTime = 0.f;
|
||
NextActionIndex = 0;
|
||
bIsWaitingForInput = false;
|
||
TurnState.Reset();
|
||
}
|
||
|
||
// ============================================================================
|
||
// 各 Action 实现
|
||
// ============================================================================
|
||
|
||
void FEffectManager::ExecuteWait(const FInteractAction& Action)
|
||
{
|
||
// Wait Action 由 Timeline 自然推进处理,此处仅做日志
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: Wait (%.2fs)"), Action.WaitDuration);
|
||
}
|
||
|
||
void FEffectManager::ExecuteTurnToTarget(const FInteractAction& Action)
|
||
{
|
||
if (!CurrentContext.Instigator.IsValid() || !CurrentContext.TargetActor.IsValid()) return;
|
||
|
||
ACharacter* Char = CurrentContext.Instigator.Get();
|
||
FVector DirToTarget = CurrentContext.TargetActor->GetActorLocation() - Char->GetActorLocation();
|
||
DirToTarget.Z = 0;
|
||
DirToTarget.Normalize();
|
||
|
||
TurnState.bIsActive = true;
|
||
TurnState.StartRotation = Char->GetActorRotation();
|
||
TurnState.TargetRotation = DirToTarget.Rotation();
|
||
TurnState.ElapsedTime = 0.f;
|
||
TurnState.Duration = Action.TurnDuration;
|
||
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: TurnToTarget (Duration=%.2fs)"), Action.TurnDuration);
|
||
}
|
||
|
||
void FEffectManager::ExecutePlayMontage(const FInteractAction& Action)
|
||
{
|
||
if (!CurrentContext.Instigator.IsValid()) return;
|
||
if (!Action.Montage) return;
|
||
|
||
ACharacter* Char = CurrentContext.Instigator.Get();
|
||
if (Char->GetMesh() && Char->GetMesh()->GetAnimInstance())
|
||
{
|
||
Char->GetMesh()->GetAnimInstance()->Montage_Play(Action.Montage);
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: PlayMontage '%s'"),
|
||
*Action.Montage->GetName());
|
||
}
|
||
}
|
||
|
||
void FEffectManager::ExecuteCameraPush(const FInteractAction& Action)
|
||
{
|
||
if (!CurrentContext.Instigator.IsValid()) return;
|
||
|
||
APlayerController* PC = Cast<APlayerController>(
|
||
CurrentContext.Instigator.Get()->GetController());
|
||
if (!PC) return;
|
||
|
||
// 计算推镜摄像机位置:沿角色到目标方向偏移
|
||
FVector CharLoc = CurrentContext.Instigator.Get()->GetActorLocation();
|
||
FVector DirToTarget(0.f, 0.f, 0.f);
|
||
if (CurrentContext.TargetActor.IsValid())
|
||
{
|
||
DirToTarget = (CurrentContext.TargetActor->GetActorLocation() - CharLoc).GetSafeNormal();
|
||
}
|
||
else
|
||
{
|
||
// 没有目标 Actor 时使用角色朝向
|
||
DirToTarget = CurrentContext.Instigator.Get()->GetActorForwardVector();
|
||
}
|
||
|
||
FVector PushLoc = CharLoc + DirToTarget * Action.PushDistance + FVector(0.f, 0.f, 80.f);
|
||
|
||
FActorSpawnParameters Params;
|
||
Params.Owner = CurrentContext.Instigator.Get();
|
||
APushCameraActor* PushCam = CurrentContext.Instigator.Get()->GetWorld()->SpawnActor<APushCameraActor>(
|
||
PushLoc, DirToTarget.Rotation(), Params);
|
||
|
||
if (PushCam)
|
||
{
|
||
PC->SetViewTargetWithBlend(PushCam, Action.PushDuration);
|
||
CachedPushCamera = PushCam;
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: CameraPush (Dist=%.1f, Dur=%.2fs)"),
|
||
Action.PushDistance, Action.PushDuration);
|
||
}
|
||
}
|
||
|
||
void FEffectManager::ExecuteCameraReturn(const FInteractAction& Action)
|
||
{
|
||
if (!CurrentContext.Instigator.IsValid()) return;
|
||
|
||
APlayerController* PC = Cast<APlayerController>(
|
||
CurrentContext.Instigator.Get()->GetController());
|
||
if (!PC) return;
|
||
|
||
PC->SetViewTargetWithBlend(CurrentContext.Instigator.Get(), Action.ReturnDuration);
|
||
|
||
// 相机已回到角色,立即销毁推镜摄像机并清空缓存,
|
||
// 避免 PerformCleanup 检测到有效缓存后冗余回位 + 重复销毁。
|
||
if (APushCameraActor* PushCam = CachedPushCamera.Get())
|
||
{
|
||
CachedPushCamera = nullptr;
|
||
if (PushCam->GetWorld())
|
||
{
|
||
PushCam->Destroy();
|
||
}
|
||
}
|
||
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: CameraReturn (Dur=%.2fs)"), Action.ReturnDuration);
|
||
}
|
||
|
||
void FEffectManager::ExecuteConsume()
|
||
{
|
||
if (!CurrentContext.TargetComp.IsValid()) return;
|
||
|
||
CurrentContext.TargetComp->Consume();
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: Consume - %s"),
|
||
*CurrentContext.TargetComp->GetInteractableData().DisplayName);
|
||
}
|
||
|
||
void FEffectManager::ExecuteHideActor()
|
||
{
|
||
if (!CurrentContext.TargetActor.IsValid()) return;
|
||
|
||
CurrentContext.TargetActor.Get()->SetActorHiddenInGame(true);
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideActor"));
|
||
}
|
||
|
||
void FEffectManager::ExecuteShowActor()
|
||
{
|
||
if (!CurrentContext.TargetActor.IsValid()) return;
|
||
|
||
CurrentContext.TargetActor.Get()->SetActorHiddenInGame(false);
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: ShowActor"));
|
||
}
|
||
|
||
void FEffectManager::ExecuteShowNarrative(const FInteractAction& Action)
|
||
{
|
||
FString SelectedText;
|
||
if (Action.bRandomSelect && Action.NarrativeTexts.Num() > 1)
|
||
{
|
||
SelectedText = PickRandomNarrative(Action.NarrativeTexts);
|
||
}
|
||
else if (Action.NarrativeTexts.Num() > 0)
|
||
{
|
||
SelectedText = Action.NarrativeTexts[0];
|
||
}
|
||
|
||
// P-016:接通真实 Widget(半透明底框 + 整句一次显示 + 淡入)
|
||
if (NarrativeWidgetClass && CurrentContext.Instigator.IsValid())
|
||
{
|
||
if (APlayerController* PC = Cast<APlayerController>(
|
||
CurrentContext.Instigator.Get()->GetController()))
|
||
{
|
||
// 若上一次的 Widget 仍在,先移除(避免叠加)
|
||
RemoveNarrativeWidget();
|
||
|
||
UNarrativeTextWidget* Widget = CreateWidget<UNarrativeTextWidget>(PC, NarrativeWidgetClass);
|
||
if (Widget)
|
||
{
|
||
Widget->AddToViewport();
|
||
Widget->SetNarrativeText(SelectedText);
|
||
Widget->Show();
|
||
CachedNarrativeWidget = Widget;
|
||
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowNarrative (Widget) - %s"), *SelectedText);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Widget Class 未注入(蓝图未创建/未配置)→ 回退日志占位,保证 Effect 流程不卡
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowNarrative (日志占位,WBP_NarrativeText 未配置) - %s"),
|
||
*SelectedText);
|
||
}
|
||
|
||
if (Action.bWaitForInput)
|
||
{
|
||
bIsWaitingForInput = true;
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: WaitForInput paused"));
|
||
}
|
||
}
|
||
|
||
void FEffectManager::ExecuteHideNarrative()
|
||
{
|
||
// P-016:淡出叙事文本 Widget(执行案要求 0.2s 淡出)
|
||
// Hide() 播放 FadeOutAnim,0.2s 后 SetVisibility(Hidden)。Widget 实例保留在视口中,
|
||
// 下次 ShowNarrative 时由 RemoveNarrativeWidget 先行清理避免叠加。
|
||
if (CachedNarrativeWidget.IsValid())
|
||
{
|
||
CachedNarrativeWidget->Hide();
|
||
CachedNarrativeWidget = nullptr;
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideNarrative (Widget 淡出)"));
|
||
}
|
||
else
|
||
{
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideNarrative (无活动 Widget)"));
|
||
}
|
||
}
|
||
|
||
void FEffectManager::ExecuteShowPickupNotification()
|
||
{
|
||
FString DisplayName;
|
||
if (CurrentContext.TargetComp.IsValid())
|
||
{
|
||
DisplayName = CurrentContext.TargetComp->GetInteractableData().DisplayName;
|
||
}
|
||
|
||
// 执行案 §跨系统交互:拾取通知("找到了 XXX")归物品系统控制,不在交互系统执行案范围。
|
||
// P-016 范围:仅提供表现能力,v0.1 由交互系统 EffectManager 先行接通(物品系统 AddToBackpack
|
||
// 接通属于 P-013)。若 Widget Class 已注入则显示,否则日志占位。
|
||
const FString NotificationText = DisplayName.IsEmpty()
|
||
? TEXT("找到了东西!")
|
||
: FString::Printf(TEXT("找到了 %s!"), *DisplayName);
|
||
|
||
if (PickupWidgetClass && CurrentContext.Instigator.IsValid())
|
||
{
|
||
if (APlayerController* PC = Cast<APlayerController>(
|
||
CurrentContext.Instigator.Get()->GetController()))
|
||
{
|
||
RemovePickupWidget();
|
||
|
||
UPickupNotificationWidget* Widget = CreateWidget<UPickupNotificationWidget>(PC, PickupWidgetClass);
|
||
if (Widget)
|
||
{
|
||
Widget->AddToViewport();
|
||
Widget->SetPickupText(NotificationText);
|
||
Widget->Show();
|
||
CachedPickupWidget = Widget;
|
||
|
||
// 自动淡出(执行案要求通知短暂停留后消失),2.0s 后 Hide
|
||
if (UWorld* World = Widget->GetWorld())
|
||
{
|
||
TWeakObjectPtr<UPickupNotificationWidget> WeakWidget = Widget;
|
||
FTimerHandle AutoHideHandle;
|
||
World->GetTimerManager().SetTimer(AutoHideHandle, [WeakWidget]()
|
||
{
|
||
if (WeakWidget.IsValid())
|
||
{
|
||
WeakWidget->Hide();
|
||
}
|
||
}, 2.0f, false);
|
||
}
|
||
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowPickupNotification (Widget) - %s"),
|
||
*NotificationText);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowPickupNotification (日志占位,WBP_PickupNotification 未配置) - %s"),
|
||
*NotificationText);
|
||
}
|
||
}
|
||
|
||
void FEffectManager::ExecuteToggleDoor()
|
||
{
|
||
if (!CurrentContext.TargetComp.IsValid()) return;
|
||
|
||
if (UDoorInteractableComponent* DoorComp = Cast<UDoorInteractableComponent>(CurrentContext.TargetComp.Get()))
|
||
{
|
||
DoorComp->ToggleDoor();
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ToggleDoor executed"));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 表现层 Widget 辅助(P-016)
|
||
// ============================================================================
|
||
|
||
void FEffectManager::RemoveNarrativeWidget()
|
||
{
|
||
if (CachedNarrativeWidget.IsValid())
|
||
{
|
||
UNarrativeTextWidget* Widget = CachedNarrativeWidget.Get();
|
||
CachedNarrativeWidget = nullptr;
|
||
|
||
// 强制移除并清理(PerformCleanup / 下次 ShowNarrative 调用)
|
||
// 若 Widget 正在播放淡出动画,RemoveFromParent 会中断它,这是兜底清理的可接受行为
|
||
if (Widget && Widget->IsInViewport())
|
||
{
|
||
Widget->RemoveFromParent();
|
||
}
|
||
}
|
||
}
|
||
|
||
void FEffectManager::RemovePickupWidget()
|
||
{
|
||
if (CachedPickupWidget.IsValid())
|
||
{
|
||
UPickupNotificationWidget* Widget = CachedPickupWidget.Get();
|
||
CachedPickupWidget = nullptr;
|
||
|
||
if (Widget && Widget->IsInViewport())
|
||
{
|
||
Widget->RemoveFromParent();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// TurnToTarget 插值
|
||
// ============================================================================
|
||
|
||
void FEffectManager::UpdateTurn(float DeltaTime)
|
||
{
|
||
if (!TurnState.bIsActive) return;
|
||
|
||
TurnState.ElapsedTime += DeltaTime;
|
||
|
||
if (TurnState.IsComplete())
|
||
{
|
||
if (CurrentContext.Instigator.IsValid())
|
||
{
|
||
CurrentContext.Instigator.Get()->SetActorRotation(TurnState.TargetRotation);
|
||
}
|
||
TurnState.bIsActive = false;
|
||
return;
|
||
}
|
||
|
||
float Alpha = TurnState.GetAlpha();
|
||
FRotator NewRot = FMath::Lerp(TurnState.StartRotation, TurnState.TargetRotation, Alpha);
|
||
if (CurrentContext.Instigator.IsValid())
|
||
{
|
||
CurrentContext.Instigator.Get()->SetActorRotation(NewRot);
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 辅助方法
|
||
// ============================================================================
|
||
|
||
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];
|
||
}
|