From 4bf678b937a261b8adf2e532154d77934609b26b Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Thu, 11 Jun 2026 23:21:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(interaction):=20=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E7=B3=BB=E7=BB=9Fv1.1=20Effect/Action=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增: - 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 --- Source/ChildHood/Interaction/DoorActor.cpp | 49 ++ Source/ChildHood/Interaction/DoorActor.h | 68 +++ .../Interaction/DoorInteractableComponent.cpp | 89 ++++ .../ChildHood/Interaction/EffectManager.cpp | 447 ++++++++++++++++++ Source/ChildHood/Interaction/EffectManager.h | 123 +++++ .../InteractionPromptComponent.cpp | 160 +++++++ .../Interaction/InteractionPromptComponent.h | 70 +++ Source/ChildHood/SystemDriver.cpp | 148 ++++++ Source/ChildHood/SystemDriver.h | 81 ++++ 9 files changed, 1235 insertions(+) create mode 100644 Source/ChildHood/Interaction/DoorActor.cpp create mode 100644 Source/ChildHood/Interaction/DoorActor.h create mode 100644 Source/ChildHood/Interaction/DoorInteractableComponent.cpp create mode 100644 Source/ChildHood/Interaction/EffectManager.cpp create mode 100644 Source/ChildHood/Interaction/EffectManager.h create mode 100644 Source/ChildHood/Interaction/InteractionPromptComponent.cpp create mode 100644 Source/ChildHood/Interaction/InteractionPromptComponent.h create mode 100644 Source/ChildHood/SystemDriver.cpp create mode 100644 Source/ChildHood/SystemDriver.h diff --git a/Source/ChildHood/Interaction/DoorActor.cpp b/Source/ChildHood/Interaction/DoorActor.cpp new file mode 100644 index 0000000..4c7be9f --- /dev/null +++ b/Source/ChildHood/Interaction/DoorActor.cpp @@ -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(TEXT("DoorInteractableComp")); +} + +// ============================================================================ +// 交互回调 +// ============================================================================ + +void ADoorActor::OnInteractStarted_Implementation(UInteractableComponent* Comp) +{ + // 只响应门组件的交互 + if (UDoorInteractableComponent* DoorComp = Cast(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")); +} diff --git a/Source/ChildHood/Interaction/DoorActor.h b/Source/ChildHood/Interaction/DoorActor.h new file mode 100644 index 0000000..084eb1f --- /dev/null +++ b/Source/ChildHood/Interaction/DoorActor.h @@ -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 DoorInteractableComp; + + /** 门打开时的 Yaw 偏移(度),可在编辑器中调整 */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door", meta = (ClampMin = "0", ClampMax = "180")) + float OpenYawOffset = 90.f; +}; diff --git a/Source/ChildHood/Interaction/DoorInteractableComponent.cpp b/Source/ChildHood/Interaction/DoorInteractableComponent.cpp new file mode 100644 index 0000000..5b1ac99 --- /dev/null +++ b/Source/ChildHood/Interaction/DoorInteractableComponent.cpp @@ -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(Owner)) + { + DoorActor->UpdateDoorVisual(bDoorIsOpen); + } + } + + // 持久化门状态 + if (UWorld* World = GetWorld()) + { + if (UPersistenceSubsystem* Persistence = World->GetGameInstance()->GetSubsystem()) + { + 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(Owner)) + { + DoorActor->UpdateDoorVisual(bDoorIsOpen); + } + } + + UE_LOG(LogChildHood, Log, TEXT("Door RestorePersistentState: %s → %s"), + *Data.ItemId.ToString(), + bDoorIsOpen ? TEXT("Open") : TEXT("Closed")); +} diff --git a/Source/ChildHood/Interaction/EffectManager.cpp b/Source/ChildHood/Interaction/EffectManager.cpp new file mode 100644 index 0000000..5a29d38 --- /dev/null +++ b/Source/ChildHood/Interaction/EffectManager.cpp @@ -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(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( + 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( + 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( + 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( + 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& 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]; +} diff --git a/Source/ChildHood/Interaction/EffectManager.h b/Source/ChildHood/Interaction/EffectManager.h new file mode 100644 index 0000000..9cdf872 --- /dev/null +++ b/Source/ChildHood/Interaction/EffectManager.h @@ -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 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& 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); +}; diff --git a/Source/ChildHood/Interaction/InteractionPromptComponent.cpp b/Source/ChildHood/Interaction/InteractionPromptComponent.cpp new file mode 100644 index 0000000..8669cde --- /dev/null +++ b/Source/ChildHood/Interaction/InteractionPromptComponent.cpp @@ -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(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; + } +} diff --git a/Source/ChildHood/Interaction/InteractionPromptComponent.h b/Source/ChildHood/Interaction/InteractionPromptComponent.h new file mode 100644 index 0000000..3467e40 --- /dev/null +++ b/Source/ChildHood/Interaction/InteractionPromptComponent.h @@ -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 Driver; + + /** Prompt Widget 实例 */ + UPROPERTY() + TObjectPtr PromptWidget; + + /** Prompt Widget 蓝图类(编辑器中指定 WBP_InteractionPrompt) */ + UPROPERTY(EditAnywhere, Category = "Interaction|UI") + TSubclassOf PromptWidgetClass; + + /** 提示 UI 高度偏移(cm) */ + UPROPERTY(EditAnywhere, Category = "Interaction|UI") + float PromptHeightOffset = 80.0f; + + /** 上一次记录的状态(用于检测状态变化) */ + bool bWasShowingPrompt = false; + + /** 更新 Prompt 显示 */ + void UpdatePrompt(); + + /** 隐藏 Prompt */ + void HidePrompt(); +}; diff --git a/Source/ChildHood/SystemDriver.cpp b/Source/ChildHood/SystemDriver.cpp new file mode 100644 index 0000000..68470f9 --- /dev/null +++ b/Source/ChildHood/SystemDriver.cpp @@ -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(); +#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 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(); +} diff --git a/Source/ChildHood/SystemDriver.h b/Source/ChildHood/SystemDriver.h new file mode 100644 index 0000000..8748cfa --- /dev/null +++ b/Source/ChildHood/SystemDriver.h @@ -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 EffectManagerPtr; + + // === Effect 完成回调连线 === + + /** EffectManager 的 OnEffectComplete 回调目标 */ + void OnEffectComplete(); +};