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>
This commit is contained in:
2026-06-11 23:21:38 +08:00
parent 690cfeb29d
commit 4bf678b937
9 changed files with 1235 additions and 0 deletions
@@ -0,0 +1,49 @@
// ChildHood Project - 门 Actor 实现
#include "Interaction/DoorActor.h"
#include "Interaction/DoorInteractableComponent.h"
#include "Interaction/InteractableComponent.h"
#include "ChildHood.h"
// ============================================================================
// 构造函数
// ============================================================================
ADoorActor::ADoorActor()
{
PrimaryActorTick.bCanEverTick = false;
// 基类构造函数已创建 MeshComp(根组件)和 InteractableComp
// 创建门专用交互组件,替代基类的通用 InteractableComp
DoorInteractableComp = CreateDefaultSubobject<UDoorInteractableComponent>(TEXT("DoorInteractableComp"));
}
// ============================================================================
// 交互回调
// ============================================================================
void ADoorActor::OnInteractStarted_Implementation(UInteractableComponent* Comp)
{
// 只响应门组件的交互
if (UDoorInteractableComponent* DoorComp = Cast<UDoorInteractableComponent>(Comp))
{
DoorComp->ToggleDoor();
}
}
// ============================================================================
// 视觉更新
// ============================================================================
void ADoorActor::UpdateDoorVisual(bool bIsOpen)
{
if (!MeshComp) return;
// 瞬时旋转:开门偏移 OpenYawOffset 度,关门回到 0 度
const float TargetYaw = bIsOpen ? OpenYawOffset : 0.f;
MeshComp->SetRelativeRotation(FRotator(0.f, TargetYaw, 0.f));
UE_LOG(LogChildHood, Verbose, TEXT("DoorActor Visual: Yaw=%.0f (%s)"),
TargetYaw, bIsOpen ? TEXT("Open") : TEXT("Closed"));
}
+68
View File
@@ -0,0 +1,68 @@
// ChildHood Project - 门 Actor
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
// 职责:门的视觉表现(Mesh 旋转)、交互回调分发
// 派生自 AInteractableActor,使用 UDoorInteractableComponent 替代基类组件
#pragma once
#include "CoreMinimal.h"
#include "Interaction/InteractableActor.h"
#include "DoorActor.generated.h"
class UDoorInteractableComponent;
/**
* ADoorActor
*
* 门类型的交互物 Actor。在基类基础上:
* - 使用 UDoorInteractableComponent 替代 UInteractableComponent
* - OnInteractStarted 中调用 ToggleDoor 并更新 Mesh 旋转
* - UpdateDoorVisual 执行实际的 Mesh Yaw 旋转(开门+90, 关门0)
*
* v0.1 阶段旋转为瞬时 SetRelativeRotation,不使用动画插值。
*/
UCLASS()
class ADoorActor : public AInteractableActor
{
GENERATED_BODY()
public:
ADoorActor();
// === 组件访问 ===
/** 获取门交互组件 */
UFUNCTION(BlueprintCallable, Category = "Door")
UDoorInteractableComponent* GetDoorInteractableComp() const { return DoorInteractableComp; }
// === 交互回调 ===
/**
* 交互开始时调用。
* Cast 到 UDoorInteractableComponent 后调用 ToggleDoor,
* 由 Component 负责状态切换和持久化,Actor 只负责视觉。
*/
virtual void OnInteractStarted_Implementation(UInteractableComponent* Comp) override;
// === 视觉更新 ===
/**
* 更新门 Mesh 的视觉旋转。
* bIsOpen=true → Yaw +90 度(打开)
* bIsOpen=false → Yaw 0 度(关闭)
* 由 UDoorInteractableComponent 在 ToggleDoor/RestorePersistentState 中调用。
*/
UFUNCTION(BlueprintCallable, Category = "Door")
void UpdateDoorVisual(bool bIsOpen);
protected:
/** 门交互组件(替代基类的 InteractableComp,提供门特有状态和行为) */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UDoorInteractableComponent> DoorInteractableComp;
/** 门打开时的 Yaw 偏移(度),可在编辑器中调整 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door", meta = (ClampMin = "0", ClampMax = "180"))
float OpenYawOffset = 90.f;
};
@@ -0,0 +1,89 @@
// ChildHood Project - 门交互组件 实现
#include "Interaction/DoorInteractableComponent.h"
#include "Interaction/DoorActor.h"
#include "Interaction/PersistenceSubsystem.h"
#include "ChildHood.h"
#include "Engine/World.h"
#include "GameFramework/GameInstance.h"
// ============================================================================
// 构造函数
// ============================================================================
UDoorInteractableComponent::UDoorInteractableComponent()
{
// 门不需要 Tick,状态变更由交互事件驱动
PrimaryComponentTick.bCanEverTick = false;
}
// ============================================================================
// 门操作
// ============================================================================
void UDoorInteractableComponent::ToggleDoor()
{
bDoorIsOpen = !bDoorIsOpen;
// 通知 ADoorActor 更新视觉旋转
if (AActor* Owner = GetOwner())
{
if (ADoorActor* DoorActor = Cast<ADoorActor>(Owner))
{
DoorActor->UpdateDoorVisual(bDoorIsOpen);
}
}
// 持久化门状态
if (UWorld* World = GetWorld())
{
if (UPersistenceSubsystem* Persistence = World->GetGameInstance()->GetSubsystem<UPersistenceSubsystem>())
{
Persistence->SetDoorOpen(Data.ItemId, bDoorIsOpen);
}
}
UE_LOG(LogChildHood, Log, TEXT("Door Toggle: %s → %s"),
*Data.ItemId.ToString(),
bDoorIsOpen ? TEXT("Open") : TEXT("Closed"));
}
// ============================================================================
// 提示文本
// ============================================================================
FString UDoorInteractableComponent::GetPromptText_Implementation() const
{
// 根据门状态选择动词:已打开用 Alt 动词(如"关上"),已关闭用主动词(如"打开")
const FString& Verb = bDoorIsOpen ? Data.InteractVerbAlt : Data.InteractVerb;
return FString::Printf(TEXT("[E] %s %s"), *Verb, *Data.DisplayName);
}
// ============================================================================
// 跨场景持久状态恢复
// ============================================================================
void UDoorInteractableComponent::RestorePersistentState(UPersistenceSubsystem* Persistence)
{
if (!Persistence) return;
// 1. 先调用基类恢复消耗状态
Super::RestorePersistentState(Persistence);
// 2. 恢复门开关状态
bDoorIsOpen = Persistence->IsDoorOpen(Data.ItemId);
// 3. 通知 DoorActor 更新视觉旋转
if (AActor* Owner = GetOwner())
{
if (ADoorActor* DoorActor = Cast<ADoorActor>(Owner))
{
DoorActor->UpdateDoorVisual(bDoorIsOpen);
}
}
UE_LOG(LogChildHood, Log, TEXT("Door RestorePersistentState: %s → %s"),
*Data.ItemId.ToString(),
bDoorIsOpen ? TEXT("Open") : TEXT("Closed"));
}
@@ -0,0 +1,447 @@
// 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];
}
@@ -0,0 +1,123 @@
// ChildHood Project - Effect Timeline 调度器(v1.1 Effect/Action 架构)
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第6节(复杂接口描述)
// 职责:Effect Timeline 调度、Action 执行分发、TurnToTarget 插值、WaitForInput 暂停机制
// 注意:普通 C++ 类,非 UObject,由 ASystemDriver 持有并 Tick 驱动
#pragma once
#include "CoreMinimal.h"
#include "InteractionTypes.h"
class APushCameraActor;
/**
* FEffectManager
*
* Effect Timeline 调度器,管理一次交互 Effect 的完整生命周期:
* - 按 TriggerTime 依次触发各 Action
* - 维护 TurnToTarget 跨帧插值
* - 支持 WaitForInput 暂停/恢复机制
* - 支持取消(CancelEffect)
* - Effect 结束时通过 OnEffectComplete 回调通知上层
*
* 生命周期:由 ASystemDriver 在 BeginPlay 中构造,每帧由 ASystemDriver::Tick 调用 Update。
*/
class FEffectManager
{
public:
// === 公开接口 ===
/** 设置完成回调(由 ASystemDriver 在 BeginPlay 中设置) */
void SetOnEffectComplete(FOnEffectComplete&& Callback);
/** 播放 Effect — 初始化状态,开始 Timeline 推进 */
void PlayEffect(const FInteractEffect& Effect, const FEffectContext& Context);
/** 取消 Effect(用户按 E/ESC) — 区分 WaitForInput 恢复 vs 强制取消 */
void CancelEffect();
/** 当前是否正在播放 Effect */
bool IsPlaying() const { return bIsPlaying; }
/** 当前是否暂停等待用户输入 */
bool IsWaitingForInput() const { return bIsWaitingForInput; }
/** 每帧更新(由 ASystemDriver::Tick 调用) */
void Update(float DeltaTime);
private:
// === Effect 状态 ===
/** 当前播放中的 Effect 数据 */
FInteractEffect CurrentEffect;
/** 当前 Effect 的运行时上下文 */
FEffectContext CurrentContext;
/** 从 Effect 开始累计的经过时间 */
float ElapsedTime = 0.f;
/** 下一个待触发的 Action 索引(Actions 按 TriggerTime 排序) */
int32 NextActionIndex = 0;
/** 是否正在播放 Effect */
bool bIsPlaying = false;
/** 是否暂停等待用户输入(ShowNarrative bWaitForInput=true 时设置) */
bool bIsWaitingForInput = false;
/** 上一次随机选取的叙事文本索引(用于避免连续重复) */
int32 LastNarrativeIndex = -1;
// === Turn 插值 ===
/** TurnToTarget 跨帧插值状态 */
FTurnState TurnState;
// === 推镜缓存 ===
/** 推镜期间 Spawn 的临时摄像机(弱引用,Effect 结束时销毁) */
TWeakObjectPtr<APushCameraActor> CachedPushCamera;
// === 回调 ===
/** Effect 完成回调(正常结束或取消后清理完毕时触发) */
FOnEffectComplete OnEffectComplete;
// === 内部方法 ===
/** 执行单个 Action(按 Type 分发到对应 ExecuteXxx) */
void ExecuteAction(const FInteractAction& Action);
/** Effect 正常结束 — 清理状态 + 广播回调 */
void FinishEffect();
/** 从 WaitForInput 暂停恢复 */
void ResumeFromWait();
/** 清理所有运行时状态(推镜相机、蒙太奇、转向等) */
void PerformCleanup();
/** 从 NarrativeTexts 中随机选取一条(避免与上次重复) */
FString PickRandomNarrative(const TArray<FString>& Texts);
// === 各 Action 处理 ===
void ExecuteWait(const FInteractAction& Action);
void ExecuteTurnToTarget(const FInteractAction& Action);
void ExecutePlayMontage(const FInteractAction& Action);
void ExecuteCameraPush(const FInteractAction& Action);
void ExecuteCameraReturn(const FInteractAction& Action);
void ExecuteConsume();
void ExecuteHideActor();
void ExecuteShowActor();
void ExecuteShowNarrative(const FInteractAction& Action);
void ExecuteHideNarrative();
void ExecuteShowPickupNotification();
void ExecuteToggleDoor();
/** 更新 TurnToTarget 跨帧插值(在 Update 末尾调用) */
void UpdateTurn(float DeltaTime);
};
@@ -0,0 +1,160 @@
// ChildHood Project - 交互提示追踪组件 实现
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(提示UI显示/隐藏)
// 职责:Free 状态下追踪 Manager 缓存,更新 Prompt Widget 位置和文本
#include "Interaction/InteractionPromptComponent.h"
#include "Interaction/InteractionPromptWidget.h"
#include "SystemDriver.h"
#include "Interaction/InteractionManager.h"
#include "ChildHood.h"
#include "Engine/World.h"
#include "Blueprint/WidgetLayoutLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/PlayerController.h"
// ============================================================================
// 构造函数
// ============================================================================
UInteractionPromptComponent::UInteractionPromptComponent()
{
// 启用 Tick,每帧更新 Prompt 位置
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = true;
}
// ============================================================================
// 初始化
// ============================================================================
void UInteractionPromptComponent::Initialize(ASystemDriver* InDriver)
{
Driver = InDriver;
UE_LOG(LogChildHood, Log, TEXT("InteractionPromptComponent: 初始化完成, Driver=%s"),
InDriver ? *InDriver->GetName() : TEXT("nullptr"));
}
// ============================================================================
// 生命周期
// ============================================================================
void UInteractionPromptComponent::BeginPlay()
{
Super::BeginPlay();
// 根据 PromptWidgetClass 创建 Prompt Widget
if (PromptWidgetClass)
{
if (APlayerController* PC = GetWorld()
? GetWorld()->GetFirstPlayerController()
: nullptr)
{
PromptWidget = CreateWidget<UInteractionPromptWidget>(PC, PromptWidgetClass);
if (PromptWidget)
{
PromptWidget->AddToViewport();
PromptWidget->SetVisibility(ESlateVisibility::Hidden);
UE_LOG(LogChildHood, Log, TEXT("InteractionPromptComponent: Prompt Widget 创建完成"));
}
}
else
{
UE_LOG(LogChildHood, Warning, TEXT("InteractionPromptComponent: 无法获取 PlayerController,Prompt Widget 未创建"));
}
}
else
{
UE_LOG(LogChildHood, Warning, TEXT("InteractionPromptComponent: PromptWidgetClass 未设置,Prompt Widget 未创建"));
}
}
void UInteractionPromptComponent::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// 没有 Widget 则跳过
if (!PromptWidget)
{
return;
}
// 获取 Driver(弱引用检查)
ASystemDriver* DriverPtr = Driver.Get();
if (!DriverPtr)
{
HidePrompt();
return;
}
// 获取 InteractionManager 引用
FInteractionManager& Manager = DriverPtr->GetInteractionManager();
// 检查 State == Free 且 HasSelection()
if (Manager.GetState() == EInteractionState::Free && Manager.HasSelection())
{
UpdatePrompt();
}
else
{
HidePrompt();
}
}
// ============================================================================
// Prompt 显示/隐藏
// ============================================================================
void UInteractionPromptComponent::UpdatePrompt()
{
ASystemDriver* DriverPtr = Driver.Get();
if (!DriverPtr || !PromptWidget)
{
return;
}
FInteractionManager& Manager = DriverPtr->GetInteractionManager();
// 从 Manager 缓存读取数据
FString PromptText = Manager.GetSelectedPromptText();
FVector WorldPos = Manager.GetSelectedWorldPosition();
// 应用高度偏移
FVector AdjustedPos = WorldPos + FVector(0.f, 0.f, PromptHeightOffset);
// ProjectWorldToScreen
APlayerController* PC = GetWorld()
? GetWorld()->GetFirstPlayerController()
: nullptr;
if (!PC)
{
return;
}
FVector2D ScreenPos;
bool bOnScreen = PC->ProjectWorldLocationToScreen(AdjustedPos, ScreenPos, true);
if (!bOnScreen)
{
HidePrompt();
return;
}
// 更新 Widget 位置和文本
PromptWidget->SetPromptText(PromptText);
PromptWidget->SetScreenPosition(ScreenPos);
PromptWidget->Show();
bWasShowingPrompt = true;
}
void UInteractionPromptComponent::HidePrompt()
{
if (PromptWidget && bWasShowingPrompt)
{
PromptWidget->Hide();
bWasShowingPrompt = false;
}
}
@@ -0,0 +1,70 @@
// ChildHood Project - 交互提示追踪组件
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(提示UI显示/隐藏)
// 职责:独立于 Effect 执行,只负责 Free 状态下的 Prompt UI 追踪
// 架构:UActorComponent,自有 Tick,只读 InteractionManager 缓存
// 挂载:Character 上,由 Character::BeginPlay 中调用 Initialize
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InteractionPromptComponent.generated.h"
class ASystemDriver;
class UInteractionPromptWidget;
/**
* UInteractionPromptComponent
*
* 交互提示追踪组件。每帧检查 InteractionManager 的缓存数据,
* 在 Free 状态且有选中交互物时显示 Prompt Widget,Executing 时隐藏。
*
* 数据流:
* InteractionManager 缓存 (CachedSelected) → 此组件 Tick 只读 → Prompt Widget 显示/隐藏
*
* 不直接访问任何 InteractableComponent,所有数据来自 Manager 缓存。
*/
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
class UInteractionPromptComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInteractionPromptComponent();
/** 初始化(Character::BeginPlay 中调用) */
void Initialize(ASystemDriver* InDriver);
protected:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
private:
/** 弱引用 Driver */
TWeakObjectPtr<ASystemDriver> Driver;
/** Prompt Widget 实例 */
UPROPERTY()
TObjectPtr<UInteractionPromptWidget> PromptWidget;
/** Prompt Widget 蓝图类(编辑器中指定 WBP_InteractionPrompt) */
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
TSubclassOf<UInteractionPromptWidget> PromptWidgetClass;
/** 提示 UI 高度偏移(cm) */
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
float PromptHeightOffset = 80.0f;
/** 上一次记录的状态(用于检测状态变化) */
bool bWasShowingPrompt = false;
/** 更新 Prompt 显示 */
void UpdatePrompt();
/** 隐藏 Prompt */
void HidePrompt();
};
+148
View File
@@ -0,0 +1,148 @@
// ChildHood Project - 系统驱动器 实现(v1.1 Effect/Action 架构)
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
// 职责:全局唯一的 World 级驱动器,持有所有 Manager,负责初始化、Tick、跨 Manager 协调
#include "SystemDriver.h"
#include "ChildHood.h"
// FEffectManager 尚未创建时,此 include 会失败。
// 临时处理:条件编译,EffectManager.h 存在时才包含。
// 当 EffectManager 子代理完成后,去掉 __has_include 判断直接包含。
#if __has_include("Interaction/EffectManager.h")
#include "Interaction/EffectManager.h"
#define HAS_EFFECT_MANAGER 1
#else
#define HAS_EFFECT_MANAGER 0
#endif
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/Character.h"
// ============================================================================
// 构造函数
// ============================================================================
ASystemDriver::ASystemDriver()
{
// AInfo 默认不需要 Tick,我们在构造函数中开启
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
// 如果 FEffectManager 可用,在构造时创建实例
#if HAS_EFFECT_MANAGER
EffectManagerPtr = MakeUnique<FEffectManager>();
#endif
}
// ============================================================================
// 生命周期
// ============================================================================
void ASystemDriver::BeginPlay()
{
Super::BeginPlay();
// --- 初始化 InteractionManager ---
if (UWorld* World = GetWorld())
{
if (APlayerController* PC = World->GetFirstPlayerController())
{
if (ACharacter* Char = PC->GetCharacter())
{
InteractionManager.Initialize(Char);
UE_LOG(LogChildHood, Log, TEXT("SystemDriver: InteractionManager 初始化完成"));
}
else
{
UE_LOG(LogChildHood, Warning, TEXT("SystemDriver: PlayerController 无 Character,InteractionManager 未初始化"));
}
}
else
{
UE_LOG(LogChildHood, Warning, TEXT("SystemDriver: World 中无 PlayerController"));
}
}
// --- 初始化 EffectManager ---
#if HAS_EFFECT_MANAGER
if (EffectManagerPtr.IsValid())
{
// 绑定 Effect 完成回调
FOnEffectComplete Callback;
Callback.BindLambda([this]() { this->OnEffectComplete(); });
EffectManagerPtr->SetOnEffectComplete(MoveTemp(Callback));
UE_LOG(LogChildHood, Log, TEXT("SystemDriver: EffectManager 初始化完成"));
}
#else
UE_LOG(LogChildHood, Log, TEXT("SystemDriver: EffectManager 尚未创建(EffectManager.h 不存在)"));
#endif
}
void ASystemDriver::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// --- Tick EffectManager ---
// InteractionManager 无 Tick(纯事件驱动)
#if HAS_EFFECT_MANAGER
if (EffectManagerPtr.IsValid())
{
EffectManagerPtr->Update(DeltaTime);
}
#endif
}
// ============================================================================
// 全局访问
// ============================================================================
ASystemDriver* ASystemDriver::Get(const UObject* WorldContextObject)
{
if (!WorldContextObject)
{
return nullptr;
}
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
if (!World)
{
return nullptr;
}
// 使用 TActorIterator 查找场景中的 ASystemDriver
// 全局唯一,取第一个即可
for (TActorIterator<ASystemDriver> It(World); It; ++It)
{
return *It;
}
// 场景中不存在时返回 nullptr
// 应在关卡中放置一个 ASystemDriver,或在 GameMode::BeginPlay 中 Spawn
UE_LOG(LogChildHood, Verbose, TEXT("SystemDriver::Get: 场景中未找到 ASystemDriver"));
return nullptr;
}
// ============================================================================
// Manager 访问
// ============================================================================
FEffectManager& ASystemDriver::GetEffectManager()
{
checkf(EffectManagerPtr.IsValid(),
TEXT("ASystemDriver::GetEffectManager: EffectManager 未初始化。")
TEXT("请确保 EffectManager.h 已创建且 HAS_EFFECT_MANAGER == 1"));
return *EffectManagerPtr;
}
// ============================================================================
// Effect 完成回调
// ============================================================================
void ASystemDriver::OnEffectComplete()
{
// Effect 完成后通知 InteractionManager 恢复 Free 状态
InteractionManager.OnInteractionFinished();
}
+81
View File
@@ -0,0 +1,81 @@
// ChildHood Project - 系统驱动器(v1.1 Effect/Action 架构)
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
// 职责:全局唯一的 World 级驱动器,持有所有 Manager(FInteractionManager, FEffectManager),
// 负责初始化、Tick 驱动、跨 Manager 协调。
// 架构:AInfo 子类(不渲染、不占场景空间),每关只有一个实例。
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Info.h"
#include "Interaction/InteractionManager.h"
#include "SystemDriver.generated.h"
// FEffectManager 由另一个子代理并行创建,此处只前向声明
class FEffectManager;
/**
* ASystemDriver
*
* 全局唯一的 World 级系统驱动器。持有 FInteractionManager 和 FEffectManager,
* 负责它们的初始化、Tick 驱动和跨 Manager 协调。
*
* 访问方式:ASystemDriver::Get(WorldContextObject) 静态方法。
*
* 数据流:
* InteractableComponent 碰撞回调 → Get() → GetInteractionManager().Register/Unregister
* Character 输入回调 → Get() → GetInteractionManager().BeginInteract/Cancel/Switch
* EffectManager 完成 → OnEffectComplete → GetInteractionManager().OnInteractionFinished
*/
UCLASS()
class ASystemDriver : public AInfo
{
GENERATED_BODY()
public:
ASystemDriver();
// === 生命周期 ===
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
// === 全局访问 ===
/**
* 获取当前 World 中的 ASystemDriver 实例。
* WorldContextObject 用于获取 World 上下文(通常传 this 或 GetWorld())。
*/
UFUNCTION(BlueprintPure, Category = "System", meta = (WorldContext = "WorldContextObject"))
static ASystemDriver* Get(const UObject* WorldContextObject);
// === Manager 访问 ===
/** 获取交互管理器(值引用) */
FInteractionManager& GetInteractionManager() { return InteractionManager; }
const FInteractionManager& GetInteractionManager() const { return InteractionManager; }
/**
* 获取效果管理器(值引用)。
* FEffectManager 尚未创建时调用会触发链接错误,仅在 EffectManager.h 存在时使用。
*/
FEffectManager& GetEffectManager();
private:
/** 交互管理器 — 普通 C++ 值持有 */
FInteractionManager InteractionManager;
/**
* 效果管理器 — 普通 C++ 值持有。
* FEffectManager 定义在 Interaction/EffectManager.h 中(由另一个子代理创建)。
* 使用 unique_ptr 延迟构造,避免头文件中需要完整定义。
*/
TUniquePtr<FEffectManager> EffectManagerPtr;
// === Effect 完成回调连线 ===
/** EffectManager 的 OnEffectComplete 回调目标 */
void OnEffectComplete();
};