Files
ChildHood/Source/ChildHood/Interaction/EffectManager.cpp
T
starmen1265 4bf678b937 feat(interaction): 交互系统v1.1 Effect/Action架构实现
新增:
- 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>
2026-06-11 23:21:38 +08:00

448 lines
13 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 - 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];
}