4bf678b937
新增: - EffectManager: Effect Timeline调度 + Action执行分发 - SystemDriver: AInfo全局驱动器, 持有FInteractionManager/FEffectManager - InteractionPromptComponent: 独立Prompt UI组件(只读Manager缓存) - DoorActor/DoorInteractableComponent: 门派生类 修改: - InteractionManager: 从UActorComponent重写为普通C++ FInteractionManager - Character: 替换旧InteractionManager为PromptComponent, 通过Driver访问Manager - GameInstance: 空壳化, 持久数据迁移至PersistenceSubsystem - InteractableComponent: 碰撞回调接入ASystemDriver注册路径 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
448 lines
13 KiB
C++
448 lines
13 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 "Components/SphereComponent.h"
|
||
#include "Kismet/GameplayStatics.h"
|
||
#include "Animation/AnimInstance.h"
|
||
|
||
// ============================================================================
|
||
// 公开接口
|
||
// ============================================================================
|
||
|
||
void FEffectManager::SetOnEffectComplete(FOnEffectComplete&& Callback)
|
||
{
|
||
OnEffectComplete = MoveTemp(Callback);
|
||
}
|
||
|
||
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;
|
||
// v0.1 用日志占位的 Action
|
||
case EInteractActionType::AddToBackpack:
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: AddToBackpack - Item: %s"),
|
||
CurrentContext.TargetComp.IsValid()
|
||
? *CurrentContext.TargetComp->GetInteractableData().DisplayName
|
||
: TEXT("Unknown"));
|
||
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.Broadcast();
|
||
}
|
||
}
|
||
|
||
void FEffectManager::ResumeFromWait()
|
||
{
|
||
bIsWaitingForInput = false;
|
||
// Timeline 继续推进,后续 Action 按 TriggerTime 正常触发
|
||
}
|
||
|
||
void FEffectManager::PerformCleanup()
|
||
{
|
||
// 1. 文本底框淡出移除(v0.1 日志占位,后续替换为 Widget 操作)
|
||
// TODO: 如果 NarrativeWidget 显示中,触发淡出动画
|
||
|
||
// 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);
|
||
|
||
// 延迟销毁 PushCamera(在 PerformCleanup 中统一清理)
|
||
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];
|
||
}
|
||
|
||
// v0.1: 日志输出(后续替换为 Widget 显示)
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowNarrative - %s"), *SelectedText);
|
||
|
||
if (Action.bWaitForInput)
|
||
{
|
||
bIsWaitingForInput = true;
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: WaitForInput paused"));
|
||
}
|
||
}
|
||
|
||
void FEffectManager::ExecuteHideNarrative()
|
||
{
|
||
// v0.1: 日志占位(后续替换为 Widget 操作)
|
||
UE_LOG(LogTemp, Verbose, TEXT("EffectManager: HideNarrative"));
|
||
}
|
||
|
||
void FEffectManager::ExecuteShowPickupNotification()
|
||
{
|
||
// v0.1: 日志占位(后续替换为 Widget 显示)
|
||
if (CurrentContext.TargetComp.IsValid())
|
||
{
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ShowPickupNotification - %s"),
|
||
*CurrentContext.TargetComp->GetInteractableData().DisplayName);
|
||
}
|
||
}
|
||
|
||
void FEffectManager::ExecuteToggleDoor()
|
||
{
|
||
// v0.1: 日志占位(门逻辑由 UDoorInteractableComponent 派生类实现,Phase 2)
|
||
UE_LOG(LogTemp, Log, TEXT("EffectManager: ToggleDoor (placeholder)"));
|
||
}
|
||
|
||
// ============================================================================
|
||
// 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];
|
||
}
|