From c14c63f30d8887453b68a9663d9b4199be9e065c Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Thu, 11 Jun 2026 23:21:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor(interaction):=20=E9=80=82=E9=85=8Dv1.1?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E7=9A=84=E8=A7=92=E8=89=B2/=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=99=A8/=E4=BA=A4=E4=BA=92=E7=89=A9=E6=94=B9?= =?UTF-8?q?=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Character: 替换UInteractionManager为UInteractionPromptComponent - GameInstance: 空壳化,持久数据迁移至PersistenceSubsystem - InteractionManager: 重写为普通C++ FInteractionManager - InteractableComponent: 碰撞回调接入ASystemDriver注册 Co-Authored-By: Claude Opus 4.8 --- Source/ChildHood/ChildHoodCharacter.cpp | 91 +- Source/ChildHood/ChildHoodCharacter.h | 29 +- Source/ChildHood/ChildHoodGameInstance.cpp | 31 +- Source/ChildHood/ChildHoodGameInstance.h | 44 +- .../Interaction/InteractableComponent.cpp | 26 +- .../Interaction/InteractionManager.cpp | 989 ++++-------------- .../Interaction/InteractionManager.h | 266 ++--- 7 files changed, 391 insertions(+), 1085 deletions(-) diff --git a/Source/ChildHood/ChildHoodCharacter.cpp b/Source/ChildHood/ChildHoodCharacter.cpp index 71eb351..2a044d6 100644 --- a/Source/ChildHood/ChildHoodCharacter.cpp +++ b/Source/ChildHood/ChildHoodCharacter.cpp @@ -1,3 +1,8 @@ +// ChildHood Project - 玩家角色 实现(v1.1 Effect/Action 架构适配) +// 变更:通过 ASystemDriver 访问 FInteractionManager + FEffectManager +// 新增 BeginPlay 初始化 PromptComponent +// 删除对旧 UInteractionManager 的依赖 + // Copyright Epic Games, Inc. All Rights Reserved. #include "ChildHoodCharacter.h" @@ -11,7 +16,12 @@ #include "EnhancedInputSubsystems.h" #include "InputActionValue.h" #include "ChildHood.h" +#include "Interaction/InteractionPromptComponent.h" +#include "SystemDriver.h" #include "Interaction/InteractionManager.h" +#include "Interaction/EffectManager.h" +#include "Interaction/InteractableComponent.h" +#include "Interaction/InteractionTypes.h" AChildHoodCharacter::AChildHoodCharacter() { @@ -55,8 +65,22 @@ AChildHoodCharacter::AChildHoodCharacter() FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); FollowCamera->bUsePawnControlRotation = false; - // 创建交互管理器组件 - InteractionManager = CreateDefaultSubobject(TEXT("InteractionManager")); + // 创建交互提示组件(替代旧 InteractionManager) + PromptComponent = CreateDefaultSubobject(TEXT("PromptComponent")); +} + +void AChildHoodCharacter::BeginPlay() +{ + Super::BeginPlay(); + + // 初始化 PromptComponent — 将其与 ASystemDriver 关联 + if (PromptComponent) + { + if (ASystemDriver* Driver = ASystemDriver::Get(this)) + { + PromptComponent->Initialize(Driver); + } + } } void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) @@ -71,7 +95,7 @@ void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInput EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look); EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look); - // Interaction — delegates to InteractionManager + // Interaction — delegates to ASystemDriver if (InteractAction) { EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnInteract); @@ -111,30 +135,59 @@ void AChildHoodCharacter::Look(const FInputActionValue& Value) void AChildHoodCharacter::OnInteract(const FInputActionValue& Value) { - if (UInteractionManager* IM = GetInteractionManager()) + ASystemDriver* Driver = ASystemDriver::Get(this); + if (!Driver) return; + + auto& Mgr = Driver->GetInteractionManager(); + auto& EFM = Driver->GetEffectManager(); + + // E 键三重功能: + // 1. Free 状态:发起交互 + // 2. WaitForInput 暂停态:恢复 Effect(观察/对话类按 E 继续) + // 3. 可取消 Effect 执行中:取消 Effect + + if (Mgr.GetState() == EInteractionState::Free) { - // E 键双重功能:Free 发起交互,Active 结束交互 - IM->BeginInteract(); - IM->CancelInteract(); + // 发起交互 + auto* Selected = Mgr.BeginInteract(); + if (!Selected) return; + + SetInteracting(true); + + FEffectContext Ctx; + Ctx.Instigator = this; + Ctx.TargetComp = Selected; + Ctx.TargetActor = Selected->GetOwner(); + EFM.PlayEffect(Selected->GetInteractableData().InteractEffect, Ctx); + } + else + { + // Executing 状态 → 尝试取消/恢复 + EFM.CancelEffect(); } } void AChildHoodCharacter::OnCancel(const FInputActionValue& Value) { - if (UInteractionManager* IM = GetInteractionManager()) - { - IM->CancelInteract(); - } + ASystemDriver* Driver = ASystemDriver::Get(this); + if (!Driver) return; + + auto& EFM = Driver->GetEffectManager(); + EFM.CancelEffect(); } void AChildHoodCharacter::OnSwitchInteract(const FInputActionValue& Value) { - if (UInteractionManager* IM = GetInteractionManager()) - { - float DirectionValue = Value.Get(); - int32 Direction = (DirectionValue >= 0.f) ? 1 : -1; - IM->SwitchTarget(Direction); - } + ASystemDriver* Driver = ASystemDriver::Get(this); + if (!Driver) return; + + auto& Mgr = Driver->GetInteractionManager(); + // 交互中不可切换 + if (Mgr.GetState() != EInteractionState::Free) return; + + float DirectionValue = Value.Get(); + int32 Direction = (DirectionValue >= 0.f) ? 1 : -1; + Mgr.SwitchTarget(Direction); } void AChildHoodCharacter::DoMove(float Right, float Forward) @@ -177,9 +230,9 @@ void AChildHoodCharacter::DoLook(float Yaw, float Pitch) // 交互系统方法 // ============================================================================ -UInteractionManager* AChildHoodCharacter::GetInteractionManager() const +UInteractionPromptComponent* AChildHoodCharacter::GetPromptComponent() const { - return InteractionManager; + return PromptComponent; } void AChildHoodCharacter::SetInteracting(bool bValue) diff --git a/Source/ChildHood/ChildHoodCharacter.h b/Source/ChildHood/ChildHoodCharacter.h index 290d0e9..27e0e24 100644 --- a/Source/ChildHood/ChildHoodCharacter.h +++ b/Source/ChildHood/ChildHoodCharacter.h @@ -1,3 +1,7 @@ +// ChildHood Project - 玩家角色(v1.1 Effect/Action 架构适配) +// 变更:移除 UInteractionManager,改用 UInteractionPromptComponent +// 交互操作通过 ASystemDriver 访问 FInteractionManager + FEffectManager + // Copyright Epic Games, Inc. All Rights Reserved. #pragma once @@ -10,7 +14,8 @@ class USpringArmComponent; class UCameraComponent; class UInputAction; -class UInteractionManager; +class UInteractionPromptComponent; +class ASystemDriver; struct FInputActionValue; DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All); @@ -18,6 +23,10 @@ DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All); /** * A simple player-controllable third person character * Implements a controllable orbiting camera + * + * v1.1 架构变更: + * - 交互操作通过 ASystemDriver 全局单例访问 FInteractionManager / FEffectManager + * - 身上挂载 UInteractionPromptComponent 负责 Free 状态下的 Prompt UI 追踪 */ UCLASS(abstract) class AChildHoodCharacter : public ACharacter @@ -58,21 +67,23 @@ protected: UPROPERTY(EditAnywhere, Category="Input") UInputAction* SwitchInteractAction; - /** 交互管理器组件 */ + /** 交互提示 UI 组件 — Free 状态下追踪选中交互物并显示 Prompt */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction", meta = (AllowPrivateAccess = "true")) - UInteractionManager* InteractionManager; + UInteractionPromptComponent* PromptComponent; /** Whether the character is currently in an interaction */ bool bIsInteracting = false; + virtual void BeginPlay() override; + public: /** Constructor */ AChildHoodCharacter(); - /** 获取交互管理器组件 */ + /** 获取交互提示组件 */ UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interaction") - UInteractionManager* GetInteractionManager() const; + UInteractionPromptComponent* GetPromptComponent() const; /** Set interaction state — called by interaction system */ UFUNCTION(BlueprintCallable, Category = "Interaction") @@ -91,21 +102,19 @@ protected: /** Initialize input action bindings */ virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; -protected: - /** Called for movement input */ void Move(const FInputActionValue& Value); /** Called for looking input */ void Look(const FInputActionValue& Value); - /** Called for interact input (E) — delegates to InteractionManager */ + /** Called for interact input (E) — delegates to ASystemDriver */ void OnInteract(const FInputActionValue& Value); - /** Called for cancel input (ESC) — delegates to InteractionManager */ + /** Called for cancel input (ESC) — delegates to ASystemDriver */ void OnCancel(const FInputActionValue& Value); - /** Called for switch interact target input (Tab/Wheel) — delegates to InteractionManager */ + /** Called for switch interact target input (Tab/Wheel) — delegates to ASystemDriver */ void OnSwitchInteract(const FInputActionValue& Value); public: diff --git a/Source/ChildHood/ChildHoodGameInstance.cpp b/Source/ChildHood/ChildHoodGameInstance.cpp index 2ca1fcd..4eedecb 100644 --- a/Source/ChildHood/ChildHoodGameInstance.cpp +++ b/Source/ChildHood/ChildHoodGameInstance.cpp @@ -1,30 +1,5 @@ -// ChildHood Project - 跨关卡持久数据持有者 实现 +// ChildHood Project - GameInstance(v1.1 清理版) +// 持久数据已迁移至 UPersistenceSubsystem(UGameInstanceSubsystem) #include "ChildHoodGameInstance.h" - -bool UChildHoodGameInstance::IsItemCollected(FName ItemId) const -{ - return CollectedItemIDs.Contains(ItemId); -} - -void UChildHoodGameInstance::MarkItemCollected(FName ItemId) -{ - CollectedItemIDs.Add(ItemId); -} - -bool UChildHoodGameInstance::IsDoorOpen(FName DoorId) const -{ - const bool* Found = DoorStates.Find(DoorId); - return Found ? *Found : false; -} - -void UChildHoodGameInstance::SetDoorOpen(FName DoorId, bool bIsOpen) -{ - DoorStates.Add(DoorId, bIsOpen); -} - -void UChildHoodGameInstance::ResetAll() -{ - CollectedItemIDs.Empty(); - DoorStates.Empty(); -} +// v0.1: 空壳实现,持久数据由 UPersistenceSubsystem 管理 diff --git a/Source/ChildHood/ChildHoodGameInstance.h b/Source/ChildHood/ChildHoodGameInstance.h index 3eae61e..128cb42 100644 --- a/Source/ChildHood/ChildHoodGameInstance.h +++ b/Source/ChildHood/ChildHoodGameInstance.h @@ -1,5 +1,5 @@ -// ChildHood Project - 跨关卡持久数据持有者 -// 对应技术方案: docs/technical/交互系统-技术方案.md 第12节(跨场景持久化设计) +// ChildHood Project - GameInstance(v1.1 清理版) +// 持久数据已迁移至 UPersistenceSubsystem(UGameInstanceSubsystem) #pragma once @@ -10,11 +10,8 @@ /** * UChildHoodGameInstance * - * 跨关卡持久数据持有者。UE5 的 OpenLevel() 会销毁整个旧 World(包括 - * Character 及其所有 UActorComponent),因此需要将持久数据与运行时行为分离。 - * - * v0.1 为单场景原型,此 GameInstance 以空数据初始化。 - * 后续多场景版本只需在 Project Settings 中将 GameInstance Class 设为此类即可。 + * v0.1: 空壳,持久化数据由 UPersistenceSubsystem 管理。 + * 后续可添加:全局配置、存档管理、音频设置等 GameInstance 级功能。 */ UCLASS() class UChildHoodGameInstance : public UGameInstance @@ -22,35 +19,6 @@ class UChildHoodGameInstance : public UGameInstance GENERATED_BODY() public: - - // === 物品消耗状态 === - - /** 查询指定物品是否已被收集(跨场景持久) */ - bool IsItemCollected(FName ItemId) const; - - /** 标记指定物品为已收集(写入持久层) */ - void MarkItemCollected(FName ItemId); - - // === 门状态 === - - /** 查询指定门的开关状态(跨场景持久),未记录的门默认为关闭 */ - bool IsDoorOpen(FName DoorId) const; - - /** 设置指定门的开关状态(写入持久层) */ - void SetDoorOpen(FName DoorId, bool bIsOpen); - - // === 重置 === - - /** 重置所有持久数据(新游戏时调用) */ - void ResetAll(); - -private: - - /** 已收集物品ID集合 */ - UPROPERTY() - TSet CollectedItemIDs; - - /** 门开关状态映射(true=打开,false=关闭) */ - UPROPERTY() - TMap DoorStates; + // v0.1: 空壳,持久化数据由 UPersistenceSubsystem 管理 + // 后续可添加:全局配置、存档管理、音频设置等 GameInstance 级功能 }; diff --git a/Source/ChildHood/Interaction/InteractableComponent.cpp b/Source/ChildHood/Interaction/InteractableComponent.cpp index 7c7dc1b..7984f73 100644 --- a/Source/ChildHood/Interaction/InteractableComponent.cpp +++ b/Source/ChildHood/Interaction/InteractableComponent.cpp @@ -3,6 +3,8 @@ #include "Interaction/InteractableComponent.h" #include "Interaction/InteractableActor.h" #include "Interaction/PersistenceSubsystem.h" +#include "SystemDriver.h" +#include "Interaction/InteractionManager.h" #include "ChildHood.h" #include "Components/SphereComponent.h" @@ -10,10 +12,6 @@ #include "GameFramework/GameInstance.h" #include "GameFramework/Character.h" -// 前向声明对应的头文件在.cpp中include -// ASystemDriver 在 Phase 2 创建,此处前向声明即可 -class ASystemDriver; - // ============================================================================ // 构造函数 // ============================================================================ @@ -164,9 +162,15 @@ void UInteractableComponent::OnDetectionOverlapBegin(UPrimitiveComponent* Overla UE_LOG(LogChildHood, Verbose, TEXT("检测到角色进入范围: %s ← %s"), *Data.DisplayName, *OtherActor->GetName()); - // Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().RegisterInteractable(this) - // 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建 - // TODO(Phase 2): 替换为 ASystemDriver 路径 + // 通过 ASystemDriver 注册到 InteractionManager + if (ASystemDriver* Driver = ASystemDriver::Get(this)) + { + Driver->GetInteractionManager().RegisterInteractable(this); + } + else + { + UE_LOG(LogChildHood, Warning, TEXT("InteractableComponent: ASystemDriver 不存在,无法注册 %s"), *Data.DisplayName); + } } void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* OverlappedComponent, @@ -182,7 +186,9 @@ void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* Overlapp UE_LOG(LogChildHood, Verbose, TEXT("角色离开范围: %s ← %s"), *Data.DisplayName, *OtherActor->GetName()); - // Phase 2 将改为通过 ASystemDriver::Get(GetWorld())->GetInteractionManager().UnregisterInteractable(this) - // 当前 Phase 1 仅记录日志,因为 FInteractionManager 尚未创建 - // TODO(Phase 2): 替换为 ASystemDriver 路径 + // 通过 ASystemDriver 从 InteractionManager 注销 + if (ASystemDriver* Driver = ASystemDriver::Get(this)) + { + Driver->GetInteractionManager().UnregisterInteractable(this); + } } diff --git a/Source/ChildHood/Interaction/InteractionManager.cpp b/Source/ChildHood/Interaction/InteractionManager.cpp index 324919a..08cd0e5 100644 --- a/Source/ChildHood/Interaction/InteractionManager.cpp +++ b/Source/ChildHood/Interaction/InteractionManager.cpp @@ -1,258 +1,38 @@ -// ChildHood Project - 交互管理器核心组件 实现 +// ChildHood Project - 交互管理器 实现(v1.1 Effect/Action 架构重写) // 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(行为规则实现) +// 架构:普通 C++ 类,由 ASystemDriver 值持有,纯事件驱动无 Tick #include "Interaction/InteractionManager.h" #include "Interaction/InteractableComponent.h" -#include "Interaction/InteractableActor.h" -#include "Interaction/PushCameraActor.h" -#include "Interaction/InteractionPromptWidget.h" -#include "Interaction/NarrativeTextWidget.h" -#include "ChildHoodCharacter.h" -#include "ChildHoodGameInstance.h" #include "ChildHood.h" #include "GameFramework/Character.h" -#include "GameFramework/PlayerController.h" -#include "GameFramework/SpringArmComponent.h" -#include "Camera/CameraComponent.h" -#include "Components/SphereComponent.h" #include "Engine/World.h" -#include "TimerManager.h" -#include "Kismet/GameplayStatics.h" -#include "Blueprint/WidgetBlueprintLibrary.h" - -// 定义交互系统日志类别 -DECLARE_LOG_CATEGORY_EXTERN(LogInteraction, Log, All); -DEFINE_LOG_CATEGORY(LogInteraction); // ============================================================================ -// 构造函数 +// 日志类别 // ============================================================================ -UInteractionManager::UInteractionManager() +// InteractionManager 使用项目全局日志 LogChildHood +// 如需独立日志类别可在 .h 中 DECLARE_LOG_CATEGORY_EXTERN,此处 DEFINE + +// ============================================================================ +// 初始化 +// ============================================================================ + +void FInteractionManager::Initialize(ACharacter* InPlayerCharacter) { - // 默认关闭 Tick,首个交互物注册时开启 - PrimaryComponentTick.bCanEverTick = true; - PrimaryComponentTick.bStartWithTickEnabled = false; + PlayerCharacter = InPlayerCharacter; + + UE_LOG(LogChildHood, Log, TEXT("InteractionManager: 初始化完成, Player=%s"), + InPlayerCharacter ? *InPlayerCharacter->GetName() : TEXT("nullptr")); } // ============================================================================ -// 生命周期 +// 交互物注册 // ============================================================================ -void UInteractionManager::BeginPlay() -{ - Super::BeginPlay(); - - // Spawn 推镜用临时摄像机 Actor(使用专用的 APushCameraActor) - // 随 Character 生命周期,不反复创建/销毁 - FActorSpawnParameters SpawnParams; - SpawnParams.Owner = GetOwner(); - SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; - - APushCameraActor* SpawnedCamera = GetWorld()->SpawnActor(APushCameraActor::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams); - PushCameraActor = SpawnedCamera; - if (PushCameraActor) - { - PushCameraActor->SetActorHiddenInGame(true); - } - - UE_LOG(LogInteraction, Log, TEXT("InteractionManager: BeginPlay 完成, PushCameraActor=%s"), - PushCameraActor ? *PushCameraActor->GetName() : TEXT("nullptr")); - - // --- 创建 UI Widget --- - APlayerController* PC = GetWorld()->GetFirstPlayerController(); - if (PC) - { - // 创建提示 UI - if (PromptWidgetClass) - { - PromptWidget = CreateWidget(PC, PromptWidgetClass); - if (PromptWidget) - { - PromptWidget->AddToViewport(10); - } - } - else - { - UE_LOG(LogInteraction, Warning, TEXT("InteractionManager: PromptWidgetClass 未设置,提示 UI 不可用")); - } - - // 创建文本底框 UI - if (NarrativeWidgetClass) - { - NarrativeWidget = CreateWidget(PC, NarrativeWidgetClass); - if (NarrativeWidget) - { - NarrativeWidget->AddToViewport(20); - } - } - else - { - UE_LOG(LogInteraction, Warning, TEXT("InteractionManager: NarrativeWidgetClass 未设置,文本 UI 不可用")); - } - } -} - -// ============================================================================ -// Tick 驱动 -// ============================================================================ - -void UInteractionManager::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) -{ - Super::TickComponent(DeltaTime, TickType, ThisTickFunction); - - if (State == EInteractionState::Free) - { - // Free 状态下更新提示 UI 的屏幕位置 - if (PromptWidget && PromptWidget->IsVisible() && - NearbyInteractables.Num() > 0 && NearbyInteractables.IsValidIndex(SelectedIndex)) - { - UInteractableComponent* Selected = NearbyInteractables[SelectedIndex]; - if (Selected) - { - UpdatePromptScreenPosition(Selected); - } - } - return; - } - - switch (Phase) - { - case EInteractPhase::Enter: - TickEnterPhase(DeltaTime); - break; - - case EInteractPhase::AnimLock: - TickAnimLockPhase(DeltaTime); - break; - - case EInteractPhase::Active: - // 等待玩家输入,无需 Tick 处理 - break; - - case EInteractPhase::Exit: - TickExitPhase(DeltaTime); - break; - - case EInteractPhase::None: - default: - break; - } -} - -// ============================================================================ -// 公开方法 -// ============================================================================ - -void UInteractionManager::BeginInteract() -{ - // 前置检查:必须 Free 状态 - if (State != EInteractionState::Free) - { - UE_LOG(LogInteraction, Verbose, TEXT("BeginInteract: 拒绝,当前状态不是 Free (State=%d)"), - static_cast(State)); - return; - } - - // 前置检查:必须有选中的交互物 - if (NearbyInteractables.Num() == 0 || !NearbyInteractables.IsValidIndex(SelectedIndex)) - { - UE_LOG(LogInteraction, Verbose, TEXT("BeginInteract: 拒绝,没有选中的交互物")); - return; - } - - UInteractableComponent* Target = NearbyInteractables[SelectedIndex]; - - // 前置检查:交互物不能已被消耗 - if (!Target || Target->IsConsumed()) - { - UE_LOG(LogInteraction, Verbose, TEXT("BeginInteract: 拒绝,交互物已被消耗")); - return; - } - - // 进入交互 - State = EInteractionState::Interacting; - Phase = EInteractPhase::Enter; - CurrentInteractable = Target; - - // 通知 Character 进入交互状态(屏蔽移动) - AChildHoodCharacter* Char = Cast(GetOwner()); - if (Char) - { - Char->SetInteracting(true); - } - - // 隐藏提示 UI - HidePrompt(); - - // 重置 Enter 阶段计时器 - TurnElapsed = 0.0f; - WaitElapsed = 0.0f; - bPushStarted = false; - - const FInteractableData& Data = Target->GetInteractableData(); - UE_LOG(LogInteraction, Log, TEXT("BeginInteract: 开始交互 '%s' (Type=%d)"), - *Data.DisplayName, static_cast(Data.Type)); -} - -void UInteractionManager::CancelInteract() -{ - // 前置检查:必须 Interacting 状态 - if (State != EInteractionState::Interacting) - { - return; - } - - // AnimLock 期间不可取消 - if (Phase == EInteractPhase::AnimLock) - { - UE_LOG(LogInteraction, Verbose, TEXT("CancelInteract: 拒绝,AnimLock 阶段不可取消")); - return; - } - - UE_LOG(LogInteraction, Log, TEXT("CancelInteract: 取消/结束交互")); - - // 隐藏叙事文本 - HideNarrativeText(); - - // 进入 Exit 阶段 - Phase = EInteractPhase::Exit; - - // 相机回位 - const FInteractableData* Data = CurrentInteractable ? &CurrentInteractable->GetInteractableData() : nullptr; - float ReturnDuration = (Data && Data->bUseCameraPush) ? Data->CameraReturnDuration : 0.3f; - ReturnCamera(ReturnDuration); - - // 清除可能残留的计时器 - GetWorld()->GetTimerManager().ClearTimer(AnimEventTimerHandle); - GetWorld()->GetTimerManager().ClearTimer(AnimEndTimerHandle); - - // 设置退出定时器,回位动画播完后回到 Free - GetWorld()->GetTimerManager().SetTimer(ExitTimerHandle, this, &UInteractionManager::OnExitTimerComplete, ReturnDuration, false); -} - -void UInteractionManager::SwitchTarget(int32 Direction) -{ - int32 Num = NearbyInteractables.Num(); - if (Num == 0) - { - return; - } - - // Interacting 状态下忽略切换 - if (State != EInteractionState::Free) - { - return; - } - - SelectedIndex = (SelectedIndex + Direction + Num) % Num; - UpdatePromptUI(); - - UE_LOG(LogInteraction, Log, TEXT("SwitchTarget: 切换到 %d/%d"), SelectedIndex, Num); -} - -void UInteractionManager::RegisterInteractable(UInteractableComponent* Interactable) +void FInteractionManager::RegisterInteractable(UInteractableComponent* Interactable) { if (!Interactable) { @@ -265,633 +45,238 @@ void UInteractionManager::RegisterInteractable(UInteractableComponent* Interacta return; } + // 已消耗的交互物不注册 + if (Interactable->IsConsumed()) + { + return; + } + NearbyInteractables.Add(Interactable); - // 按水平距离排序(DistSquared2D,避免 sqrt) - AActor* Owner = GetOwner(); - if (Owner) - { - FVector OwnerLoc = Owner->GetActorLocation(); - NearbyInteractables.Sort([OwnerLoc](const UInteractableComponent& A, const UInteractableComponent& B) - { - float DistA = FVector::DistSquared2D(OwnerLoc, A.GetWorldLocation()); - float DistB = FVector::DistSquared2D(OwnerLoc, B.GetWorldLocation()); - return DistA < DistB; - }); - } - + // 排序 + 重置选中索引 + 刷新缓存 UpdateSelection(); - UpdatePromptUI(); + RefreshCachedSelection(); - // 首个交互物注册时开启 Tick - if (NearbyInteractables.Num() == 1) - { - SetComponentTickEnabled(true); - } - - UE_LOG(LogInteraction, Log, TEXT("RegisterInteractable: 注册 '%s', 列表大小=%d"), + UE_LOG(LogChildHood, Log, TEXT("InteractionManager: 注册 '%s', 列表大小=%d"), *Interactable->GetInteractableData().DisplayName, NearbyInteractables.Num()); } -void UInteractionManager::UnregisterInteractable(UInteractableComponent* Interactable) +void FInteractionManager::UnregisterInteractable(UInteractableComponent* Interactable) { if (!Interactable) { return; } - bool bRemoved = NearbyInteractables.Remove(Interactable) > 0; - if (!bRemoved) + // 从列表中移除 + int32 RemovedCount = NearbyInteractables.Remove(Interactable); + if (RemovedCount == 0) { return; } - // 如果移除的是当前正在交互的交互物,清空引用 - if (CurrentInteractable == Interactable) - { - CurrentInteractable = nullptr; - } + // Executing 状态下:如果移除的是 CurrentInteractable, + // 仅从检测列表移除,不清理 CurrentInteractable(Effect 可能仍在执行) + // CurrentInteractable 将在 OnInteractionFinished 中统一清理 + // 这里不做额外的 CurrentInteractable 清理 - if (NearbyInteractables.Num() > 0) - { - UpdateSelection(); - UpdatePromptUI(); - } - else - { - // 列表为空,隐藏提示 - HidePrompt(); - // 最后一个注销时关闭 Tick - SetComponentTickEnabled(false); - } + // 重新排序 + 刷新缓存 + UpdateSelection(); + RefreshCachedSelection(); - UE_LOG(LogInteraction, Log, TEXT("UnregisterInteractable: 注销 '%s', 列表大小=%d"), + UE_LOG(LogChildHood, Log, TEXT("InteractionManager: 注销 '%s', 列表大小=%d"), *Interactable->GetInteractableData().DisplayName, NearbyInteractables.Num()); } // ============================================================================ -// Enter 阶段 Tick +// 交互触发 // ============================================================================ -void UInteractionManager::TickEnterPhase(float DeltaTime) +UInteractableComponent* FInteractionManager::BeginInteract() { - if (!CurrentInteractable) + // 前置检查:必须 Free 状态 + if (State != EInteractionState::Free) { - UE_LOG(LogInteraction, Warning, TEXT("TickEnterPhase: CurrentInteractable 为空,强制取消")); - CancelInteract(); - return; + UE_LOG(LogChildHood, Verbose, TEXT("InteractionManager::BeginInteract: 拒绝,当前状态不是 Free")); + return nullptr; } - ACharacter* Char = Cast(GetOwner()); - if (!Char) + // 前置检查:必须有选中的交互物 + if (!NearbyInteractables.IsValidIndex(SelectedIndex)) + { + UE_LOG(LogChildHood, Verbose, TEXT("InteractionManager::BeginInteract: 拒绝,没有选中的交互物")); + return nullptr; + } + + UInteractableComponent* Target = NearbyInteractables[SelectedIndex].Get(); + + // 前置检查:弱引用有效且未被消耗 + if (!Target || Target->IsConsumed()) + { + UE_LOG(LogChildHood, Verbose, TEXT("InteractionManager::BeginInteract: 拒绝,交互物无效或已消耗")); + return nullptr; + } + + // 进入 Executing 状态 + State = EInteractionState::Executing; + CurrentInteractable = Target; + + // 通知 Character 进入交互状态(屏蔽移动) + // FInteractionManager 只持有 ACharacter 基类指针,不直接依赖 AChildHoodCharacter。 + // 通过 UE 反射调用 SetInteracting(UFUNCTION(BlueprintCallable))。 + if (ACharacter* Char = PlayerCharacter.Get()) + { + UFunction* Fn = Char->FindFunction(FName(TEXT("SetInteracting"))); + if (Fn) + { + struct FSetInteractingParams { bool bValue; }; + FSetInteractingParams Params; + Params.bValue = true; + Char->ProcessEvent(Fn, &Params); + } + } + + UE_LOG(LogChildHood, Log, TEXT("InteractionManager::BeginInteract: 开始交互 '%s'"), + *Target->GetInteractableData().DisplayName); + + return Target; +} + +void FInteractionManager::CancelInteract() +{ + // 前置检查:必须 Executing 状态 + if (State != EInteractionState::Executing) { return; } - const FInteractableData& Data = CurrentInteractable->GetInteractableData(); + UE_LOG(LogChildHood, Log, TEXT("InteractionManager::CancelInteract: 请求取消交互")); - // --- 1. 转向插值 --- - TurnElapsed += DeltaTime; - - FVector CharLoc = Char->GetActorLocation(); - FVector TargetLoc = CurrentInteractable->GetWorldLocation(); - FVector DirToTarget = (TargetLoc - CharLoc); - DirToTarget.Z = 0.0f; - FRotator TargetRot = DirToTarget.Rotation(); - - // 使用 RInterpTo 进行平滑插值,InterpSpeed = 1/TurnDuration - float InterpSpeed = (TurnDuration > 0.f) ? (1.f / TurnDuration) : 10.f; - FRotator NewRot = FMath::RInterpTo(Char->GetActorRotation(), TargetRot, DeltaTime, InterpSpeed); - Char->SetActorRotation(NewRot); - - // --- 2. 推镜(一次性发起) --- - if (Data.bUseCameraPush && !bPushStarted) - { - FVector PushPos = CalculatePushPosition(CurrentInteractable); - PushCamera(PushPos, Data.CameraPushDuration); - bPushStarted = true; - } - - // --- 3. 判断完成 --- - bool bTurnComplete = TurnElapsed >= TurnDuration; - bool bPushComplete = !Data.bUseCameraPush || bPushStarted; - - if (bTurnComplete && bPushComplete) - { - // 推镜后需要额外等待 PushWaitDelay - if (Data.bUseCameraPush && Data.PushWaitDelay > 0.f) - { - WaitElapsed += DeltaTime; - if (WaitElapsed >= Data.PushWaitDelay) - { - TransitionFromEnter(); - } - } - else - { - TransitionFromEnter(); - } - } + // v0.1 简化:仅记录日志,实际取消由 ASystemDriver 协调 EffectManager + // ASystemDriver::CancelInteract 会先调用 EffectManager.Cancel(), + // EffectManager 完成后会回调 OnInteractionFinished + // 这里不做状态变更,保持 Executing 直到 OnInteractionFinished 被调用 } -// ============================================================================ -// AnimLock 阶段 Tick(v0.1 由计时器驱动,此处为空) -// ============================================================================ - -void UInteractionManager::TickAnimLockPhase(float DeltaTime) +void FInteractionManager::SwitchTarget(int32 Direction) { - // v0.1: AnimLock 由 AnimEventTimer 和 AnimEndTimer 驱动,无需 Tick 处理 -} - -// ============================================================================ -// Exit 阶段 Tick(v0.1 由定时器驱动回 Free) -// ============================================================================ - -void UInteractionManager::TickExitPhase(float DeltaTime) -{ - // v0.1: Exit 阶段由 ExitTimerHandle 定时器驱动回到 Free - // 此处可做相机回位进度检查等,简单方案留空 -} - -// ============================================================================ -// Enter 完成后类型分发 -// ============================================================================ - -void UInteractionManager::TransitionFromEnter() -{ - if (!CurrentInteractable) + int32 Num = NearbyInteractables.Num(); + if (Num == 0) { return; } - const FInteractableData& Data = CurrentInteractable->GetInteractableData(); - - UE_LOG(LogInteraction, Log, TEXT("TransitionFromEnter: 类型=%d 分发"), static_cast(Data.Type)); - - switch (Data.Type) - { - case EInteractType::Examine: - Phase = EInteractPhase::Active; - ShowNarrativeText(PickRandomNarrative(Data.NarrativeTexts)); - break; - - case EInteractType::Pickup: - Phase = EInteractPhase::AnimLock; - EnterAnimLockPhase(); - break; - - case EInteractType::Operate: - if (Data.OperateSubType == EOperateSubType::Door) - { - Phase = EInteractPhase::AnimLock; - EnterAnimLockPhase(); - } - else if (Data.OperateSubType == EOperateSubType::Search) - { - Phase = EInteractPhase::AnimLock; - EnterAnimLockPhase(); - } - break; - - case EInteractType::Talk: - Phase = EInteractPhase::Active; - ShowNarrativeText(PickRandomNarrative(Data.NarrativeTexts)); - break; - - default: - UE_LOG(LogInteraction, Warning, TEXT("TransitionFromEnter: 未知交互类型 %d"), static_cast(Data.Type)); - // 未知类型直接回 Free - CancelInteract(); - break; - } -} - -// ============================================================================ -// AnimLock 阶段 -// ============================================================================ - -void UInteractionManager::EnterAnimLockPhase() -{ - if (!CurrentInteractable) + // Executing 状态下忽略切换 + if (State != EInteractionState::Free) { return; } - const FInteractableData& Data = CurrentInteractable->GetInteractableData(); + SelectedIndex = (SelectedIndex + Direction + Num) % Num; + RefreshCachedSelection(); - // 计算总动画时长:有蒙太奇用蒙太奇长度,否则用配置的 AnimDuration - float TotalAnimTime = Data.AnimDuration; - if (Data.InteractMontage) - { - TotalAnimTime = Data.InteractMontage->GetPlayLength(); - if (TotalAnimTime <= 0.f) - { - TotalAnimTime = Data.AnimDuration; - } - - // 播放蒙太奇 - ACharacter* Char = Cast(GetOwner()); - if (Char && Char->GetMesh()) - { - Char->GetMesh()->GetAnimInstance()->Montage_Play(Data.InteractMontage); - UE_LOG(LogInteraction, Log, TEXT("EnterAnimLockPhase: 播放蒙太奇, 时长=%.2f"), TotalAnimTime); - } - } - else - { - UE_LOG(LogInteraction, Log, TEXT("EnterAnimLockPhase: 无蒙太奇,使用计时器, 时长=%.2f"), TotalAnimTime); - } - - // 动画事件触发计时器(如 60% 时物品消失) - float EventTime = TotalAnimTime * Data.AnimEventTimepoint; - GetWorld()->GetTimerManager().SetTimer(AnimEventTimerHandle, this, &UInteractionManager::ExecuteAnimEvent, EventTime, false); - - // 动画结束计时器 - GetWorld()->GetTimerManager().SetTimer(AnimEndTimerHandle, this, &UInteractionManager::OnAnimLockComplete, TotalAnimTime, false); - - bAnimEventFired = false; + UE_LOG(LogChildHood, Log, TEXT("InteractionManager::SwitchTarget: 切换到 %d/%d"), + SelectedIndex, Num); } // ============================================================================ -// 动画事件触发回调 +// Effect 完成回调 // ============================================================================ -void UInteractionManager::ExecuteAnimEvent() +void FInteractionManager::OnInteractionFinished() { - if (!CurrentInteractable) - { - return; - } - - bAnimEventFired = true; - const FInteractableData& Data = CurrentInteractable->GetInteractableData(); - - UE_LOG(LogInteraction, Log, TEXT("ExecuteAnimEvent: 类型=%d, 名称='%s'"), - static_cast(Data.Type), *Data.DisplayName); - - switch (Data.Type) - { - case EInteractType::Pickup: - { - // 标记消耗 → 隐藏 Actor → 通知物品系统 - CurrentInteractable->Consume(); - - // 隐藏交互物 Actor - if (AInteractableActor* OwnerActor = Cast(CurrentInteractable->GetOwner())) - { - OwnerActor->SetActorHiddenInGame(true); - } - - // 广播拾取事件 - OnItemPickedUp.Broadcast(Data.ItemId, Data.DisplayName, FName(TEXT("杂物"))); - UE_LOG(LogInteraction, Log, TEXT("ExecuteAnimEvent: Pickup 拾取完成 '%s'"), *Data.DisplayName); - } - break; - - case EInteractType::Operate: - { - if (Data.OperateSubType == EOperateSubType::Door) - { - // 切换门状态 - CurrentInteractable->ToggleDoor(); - - // 持久化门状态到 GameInstance - if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance()) - { - GI->SetDoorOpen(Data.ItemId, CurrentInteractable->IsDoorOpen()); - } - - // 通知 Actor 处理 Mesh 旋转等视觉反馈 - if (AInteractableActor* OwnerActor = Cast(CurrentInteractable->GetOwner())) - { - OwnerActor->OnInteractStarted(CurrentInteractable); - } - - UE_LOG(LogInteraction, Log, TEXT("ExecuteAnimEvent: Door 门状态切换, 开=%s"), - CurrentInteractable->IsDoorOpen() ? TEXT("true") : TEXT("false")); - } - // Search 类型的结果判定在 OnAnimLockComplete 中处理 - } - break; - - default: - break; - } -} - -// ============================================================================ -// 动画结束回调 -// ============================================================================ - -void UInteractionManager::OnAnimLockComplete() -{ - if (!CurrentInteractable) - { - return; - } - - const FInteractableData& Data = CurrentInteractable->GetInteractableData(); - - UE_LOG(LogInteraction, Log, TEXT("OnAnimLockComplete: 类型=%d, 名称='%s'"), - static_cast(Data.Type), *Data.DisplayName); - - switch (Data.Type) - { - case EInteractType::Pickup: - { - // 直接回到 Free(无 Active/Exit,无相机/UI 需清理) - AChildHoodCharacter* Char = Cast(GetOwner()); - if (Char) - { - Char->SetInteracting(false); - } - State = EInteractionState::Free; - Phase = EInteractPhase::None; - CurrentInteractable = nullptr; - - // 从检测列表移除已消耗的交互物 - // Consume 已在 ExecuteAnimEvent 中调用,但 UnregisterInteractable 应由 DetectionSphere 禁用触发 - // 这里主动从列表移除,确保提示 UI 正确更新 - if (NearbyInteractables.Num() > 0) - { - UpdateSelection(); - UpdatePromptUI(); - } - - UE_LOG(LogInteraction, Log, TEXT("OnAnimLockComplete: Pickup 完成,回到 Free")); - } - break; - - case EInteractType::Operate: - { - if (Data.OperateSubType == EOperateSubType::Door) - { - // 直接回到 Free - AChildHoodCharacter* Char = Cast(GetOwner()); - if (Char) - { - Char->SetInteracting(false); - } - State = EInteractionState::Free; - Phase = EInteractPhase::None; - CurrentInteractable = nullptr; - - // 更新提示(门动词可能变化) - UpdateSelection(); - UpdatePromptUI(); - - UE_LOG(LogInteraction, Log, TEXT("OnAnimLockComplete: Door 完成,回到 Free")); - } - else if (Data.OperateSubType == EOperateSubType::Search) - { - // 翻找结果判定:v0.1 必定有物品 - CurrentInteractable->Consume(); - OnItemFound.Broadcast(Data.ItemId, Data.DisplayName, FName(TEXT("杂物"))); - UE_LOG(LogInteraction, Log, TEXT("OnAnimLockComplete: Search 翻找完成,获得物品 '%s'"), *Data.DisplayName); - - // 进入 Active 阶段等待玩家按 E/ESC 关闭 - Phase = EInteractPhase::Active; - // 有物品时不显示文本底框(架构预留) - } - } - break; - - default: - break; - } -} - -// ============================================================================ -// UI 方法(v0.1 用日志替代) -// ============================================================================ - -void UInteractionManager::UpdateSelection() -{ - // 排序后选中索引重置为 0(最近的) - if (AActor* Owner = GetOwner()) - { - FVector OwnerLoc = Owner->GetActorLocation(); - NearbyInteractables.Sort([OwnerLoc](const UInteractableComponent& A, const UInteractableComponent& B) - { - float DistA = FVector::DistSquared2D(OwnerLoc, A.GetWorldLocation()); - float DistB = FVector::DistSquared2D(OwnerLoc, B.GetWorldLocation()); - return DistA < DistB; - }); - } - SelectedIndex = 0; -} - -void UInteractionManager::UpdatePromptUI() -{ - if (NearbyInteractables.Num() == 0 || !NearbyInteractables.IsValidIndex(SelectedIndex)) - { - HidePrompt(); - return; - } - - UInteractableComponent* Selected = NearbyInteractables[SelectedIndex]; - if (!Selected) - { - HidePrompt(); - return; - } - - FString PromptText = Selected->GetPromptText(); - - if (PromptWidget) - { - PromptWidget->SetPromptText(PromptText); - PromptWidget->Show(); - UpdatePromptScreenPosition(Selected); - } - - UE_LOG(LogInteraction, Verbose, TEXT("UpdatePromptUI: %s"), *PromptText); -} - -void UInteractionManager::HidePrompt() -{ - if (PromptWidget) - { - PromptWidget->Hide(); - } -} - -void UInteractionManager::ShowNarrativeText(const FString& Text) -{ - if (Text.IsEmpty()) - { - UE_LOG(LogInteraction, Warning, TEXT("ShowNarrativeText: text is empty")); - return; - } - - if (NarrativeWidget) - { - NarrativeWidget->SetNarrativeText(Text); - NarrativeWidget->Show(); - } - - UE_LOG(LogInteraction, Verbose, TEXT("ShowNarrativeText: %s"), *Text); -} - -void UInteractionManager::HideNarrativeText() -{ - if (NarrativeWidget) - { - NarrativeWidget->Hide(); - } -} - -// ============================================================================ -// 摄像机控制 -// ============================================================================ - -void UInteractionManager::PushCamera(FVector Target, float Duration) -{ - if (!PushCameraActor) - { - UE_LOG(LogInteraction, Warning, TEXT("PushCamera: PushCameraActor 为空")); - return; - } - - // 计算朝向交互物的旋转 - FVector LookTarget = CurrentInteractable ? CurrentInteractable->GetWorldLocation() : Target; - FRotator LookAtRot = (LookTarget - Target).Rotation(); - - PushCameraActor->SetActorHiddenInGame(false); - PushCameraActor->SetActorLocationAndRotation(Target, LookAtRot); - - // 切换 ViewTarget - APlayerController* PC = GetWorld()->GetFirstPlayerController(); - if (PC) - { - PC->SetViewTargetWithBlend(PushCameraActor, Duration, VTBlend_EaseOut); - UE_LOG(LogInteraction, Log, TEXT("PushCamera: 推镜到 %s, 时长=%.2f"), *Target.ToString(), Duration); - } -} - -void UInteractionManager::ReturnCamera(float Duration) -{ - AActor* Owner = GetOwner(); - if (!Owner) - { - return; - } - - APlayerController* PC = GetWorld()->GetFirstPlayerController(); - if (PC) - { - // 回位到 Character - PC->SetViewTargetWithBlend(Owner, Duration, VTBlend_EaseIn); - UE_LOG(LogInteraction, Log, TEXT("ReturnCamera: 相机回位, 时长=%.2f"), Duration); - } - - // 隐藏推镜 Actor - if (PushCameraActor) - { - PushCameraActor->SetActorHiddenInGame(true); - } -} - -// ============================================================================ -// 推镜位置计算 -// ============================================================================ - -FVector UInteractionManager::CalculatePushPosition(UInteractableComponent* Interactable) -{ - if (!Interactable) - { - return FVector::ZeroVector; - } - - const FInteractableData& Data = Interactable->GetInteractableData(); - FVector InteractPos = Interactable->GetWorldLocation(); - FVector CharPos = GetOwner() ? GetOwner()->GetActorLocation() : FVector::ZeroVector; - - FVector Dir = (InteractPos - CharPos); - Dir.Z = 0.0f; - Dir.Normalize(); - - // 交互物前方(面向角色的一侧),推镜距离偏移 - FVector TargetPos = InteractPos - Dir * Data.CameraPushDistance; - // 高度略低于交互物中心(微仰视) - TargetPos.Z = InteractPos.Z * 0.8f; - - return TargetPos; -} - -// ============================================================================ -// 叙事文本随机选取 -// ============================================================================ - -FString UInteractionManager::PickRandomNarrative(const TArray& Texts) -{ - if (Texts.Num() == 0) - { - return TEXT(""); - } - - if (Texts.Num() == 1) - { - LastNarrativeText = Texts[0]; - return Texts[0]; - } - - FString Candidate; - int32 Attempts = 0; - do - { - Candidate = Texts[FMath::RandRange(0, Texts.Num() - 1)]; - Attempts++; - } while (Candidate == LastNarrativeText && Attempts < 10); - - LastNarrativeText = Candidate; - return Candidate; -} - -// ============================================================================ -// Exit 阶段定时器回调 -// ============================================================================ - -void UInteractionManager::OnExitTimerComplete() -{ - UE_LOG(LogInteraction, Log, TEXT("OnExitTimerComplete: Exit 阶段结束,回到 Free")); - - // 通知 Character 解除交互状态 - AChildHoodCharacter* Char = Cast(GetOwner()); - if (Char) - { - Char->SetInteracting(false); - } + UE_LOG(LogChildHood, Log, TEXT("InteractionManager::OnInteractionFinished: Effect 执行完毕")); // 回到 Free 状态 State = EInteractionState::Free; - Phase = EInteractPhase::None; - CurrentInteractable = nullptr; - // 如果仍在检测范围内,重新显示提示 - if (NearbyInteractables.Num() > 0) + // 清理当前交互物引用 + CurrentInteractable.Reset(); + + // 通知 Character 解除交互状态 + // 通过 UE 反射调用 SetInteracting(false) + if (ACharacter* Char = PlayerCharacter.Get()) { - UpdateSelection(); - UpdatePromptUI(); + UFunction* Fn = Char->FindFunction(FName(TEXT("SetInteracting"))); + if (Fn) + { + struct FSetInteractingParams { bool bValue; }; + FSetInteractingParams Params; + Params.bValue = false; + Char->ProcessEvent(Fn, &Params); + } } + + // 重新排序和刷新缓存(消耗可能改变了列表) + UpdateSelection(); + RefreshCachedSelection(); } // ============================================================================ -// 提示 UI 屏幕位置更新 +// 状态查询 // ============================================================================ -void UInteractionManager::UpdatePromptScreenPosition(UInteractableComponent* Interactable) +UInteractableComponent* FInteractionManager::GetCurrentInteractable() const { - if (!Interactable || !PromptWidget) return; - - APlayerController* PC = GetWorld()->GetFirstPlayerController(); - if (!PC) return; - - // 交互物世界位置 + 高度偏移 → 屏幕坐标 - FVector WorldLoc = Interactable->GetWorldLocation() + FVector(0.f, 0.f, PromptHeightOffset); - FVector2D ScreenPos; - if (UGameplayStatics::ProjectWorldToScreen(PC, WorldLoc, ScreenPos, true)) - { - PromptWidget->SetScreenPosition(ScreenPos); - } + return CurrentInteractable.Get(); +} + +// ============================================================================ +// 内部方法 +// ============================================================================ + +void FInteractionManager::UpdateSelection() +{ + // 清理已失效的弱引用 + NearbyInteractables.RemoveAll([](const TWeakObjectPtr& WeakPtr) + { + return !WeakPtr.IsValid(); + }); + + // 清理已消耗的交互物 + NearbyInteractables.RemoveAll([](const TWeakObjectPtr& WeakPtr) + { + UInteractableComponent* Comp = WeakPtr.Get(); + return Comp && Comp->IsConsumed(); + }); + + // 按与玩家的水平距离排序(DistSquared2D,避免 sqrt) + if (ACharacter* Char = PlayerCharacter.Get()) + { + FVector PlayerLoc = Char->GetActorLocation(); + NearbyInteractables.Sort([PlayerLoc](const TWeakObjectPtr& A, const TWeakObjectPtr& B) + { + if (!A.IsValid()) return false; + if (!B.IsValid()) return true; + float DistA = FVector::DistSquared2D(PlayerLoc, A->GetWorldLocation()); + float DistB = FVector::DistSquared2D(PlayerLoc, B->GetWorldLocation()); + return DistA < DistB; + }); + } + + // 重置选中索引为 0(最近的) + SelectedIndex = 0; +} + +void FInteractionManager::RefreshCachedSelection() +{ + // 清理无效项后的安全检查 + if (NearbyInteractables.Num() == 0 || !NearbyInteractables.IsValidIndex(SelectedIndex)) + { + CachedSelected.Reset(); + return; + } + + UInteractableComponent* Selected = NearbyInteractables[SelectedIndex].Get(); + if (!Selected) + { + CachedSelected.Reset(); + return; + } + + // 从 Component 读取信息并缓存 + CachedSelected.PromptText = Selected->GetPromptText(); + CachedSelected.WorldPosition = Selected->GetWorldLocation(); + CachedSelected.bIsConsumed = Selected->IsConsumed(); + CachedSelected.bHasSelection = true; } diff --git a/Source/ChildHood/Interaction/InteractionManager.h b/Source/ChildHood/Interaction/InteractionManager.h index 433d21c..d006921 100644 --- a/Source/ChildHood/Interaction/InteractionManager.h +++ b/Source/ChildHood/Interaction/InteractionManager.h @@ -1,225 +1,135 @@ -// ChildHood Project - 交互管理器核心组件 +// ChildHood Project - 交互管理器(v1.1 Effect/Action 架构重写) // 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第6节(行为规则实现) -// 职责:交互全流程状态机、检测选择、类型分发、摄像机/转向/UI 控制 +// 职责:检测列表维护、选中管理、顶层状态(Free/Executing) +// 架构:普通 C++ 类(非 UObject、非 UActorComponent),由 ASystemDriver 值持有 +// 特点:纯事件驱动无 Tick,所有变化由外部调用触发 #pragma once #include "CoreMinimal.h" -#include "Components/ActorComponent.h" #include "InteractionTypes.h" -#include "InteractionManager.generated.h" class UInteractableComponent; -class APushCameraActor; -class AChildHoodCharacter; -class UInteractionPromptWidget; -class UNarrativeTextWidget; +class ACharacter; /** - * UInteractionManager + * FInteractionManager * - * 挂载在 AChildHoodCharacter 上的组件,负责交互全流程管理: - * - 层级状态机驱动(Free / Interacting + 子阶段) - * - 附近交互物检测列表排序与选中管理 - * - 四种交互类型分发(观察/拾取/操作/对话) - * - 角色自动转向插值 - * - 摄像机推镜/回位控制 - * - UI 提示与文本显示控制(v0.1 先用日志) + * 普通C++类,负责交互检测列表维护和顶层状态管理。 + * 由 ASystemDriver 值持有,不参与 UE 反射系统。 + * + * 数据流: + * InteractableComponent 碰撞回调 → ASystemDriver → RegisterInteractable/UnregisterInteractable + * Character 输入回调 → ASystemDriver → BeginInteract/CancelInteract/SwitchTarget + * EffectManager 完成回调 → ASystemDriver → OnInteractionFinished */ -UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent)) -class UInteractionManager : public UActorComponent +class FInteractionManager { - GENERATED_BODY() - public: - UInteractionManager(); + /** 初始化管理器,绑定玩家角色引用 */ + void Initialize(ACharacter* PlayerCharacter); - // === 公开方法 === + // === 交互物注册(由 InteractableComponent 碰撞回调通过 ASystemDriver 调用)=== - /** 发起交互(E键触发),前置检查通过后进入 Enter 阶段 */ - UFUNCTION(BlueprintCallable, Category = "Interaction") - void BeginInteract(); - - /** 取消/结束交互(ESC/E键在Active阶段触发),AnimLock期间不可取消 */ - UFUNCTION(BlueprintCallable, Category = "Interaction") - void CancelInteract(); - - /** 切换选中交互物(Tab/滚轮),Direction: +1下一个 / -1上一个 */ - UFUNCTION(BlueprintCallable, Category = "Interaction") - void SwitchTarget(int32 Direction); - - /** 注册交互物到检测列表(由 InteractableComponent 的碰撞回调触发) */ - UFUNCTION(BlueprintCallable, Category = "Interaction") + /** + * 注册交互物到检测列表。 + * 添加后按水平距离排序,重置选中索引为0,刷新缓存。 + */ void RegisterInteractable(UInteractableComponent* Interactable); - /** 从检测列表注销交互物(离开范围或消耗时触发) */ - UFUNCTION(BlueprintCallable, Category = "Interaction") + /** + * 从检测列表注销交互物。 + * Executing 状态下如果移除的是 CurrentInteractable,仅从列表移除, + * CurrentInteractable 延迟清理(因为 Effect 可能仍在执行)。 + */ void UnregisterInteractable(UInteractableComponent* Interactable); - // === 委托 === + // === 交互触发(Character 输入回调中调用)=== - /** 物品拾取事件 — 通知物品系统(Pickup 类型 ExecuteAnimEvent 中广播) */ - UPROPERTY(BlueprintAssignable, Category = "Interaction|Events") - FMLocalItemEvent OnItemPickedUp; + /** + * 发起交互(E键触发)。 + * 前置检查:State==Free、SelectedIndex有效、!IsConsumed()。 + * 成功后 State=Executing,返回选中的 Component。 + * 拒绝则返回 nullptr。 + */ + UInteractableComponent* BeginInteract(); - /** 物品获得事件 — 通知收集系统(Search 类型 OnAnimLockComplete 中广播) */ - UPROPERTY(BlueprintAssignable, Category = "Interaction|Events") - FMLocalItemEvent OnItemFound; + /** + * 取消交互(ESC/E键在Executing阶段触发)。 + * v0.1 简化:仅设置状态标记,实际取消由 ASystemDriver 协调 EffectManager。 + */ + void CancelInteract(); - // === 可调参数 === + /** + * 切换选中目标(Tab/滚轮)。 + * Direction: +1下一个 / -1上一个,循环。 + * 仅在 Free 状态下响应。 + */ + void SwitchTarget(int32 Direction); - /** 角色转向时长(秒) */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning") - float TurnDuration = 0.2f; + // === 状态查询 === - /** 提示 UI 淡入时长(秒) */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning") - float PromptFadeInDuration = 0.2f; + /** 获取当前顶层状态 */ + EInteractionState GetState() const { return State; } - /** 提示 UI 淡出时长(秒) */ - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning") - float PromptFadeOutDuration = 0.2f; + /** 当前是否有有效的选中交互物 */ + bool HasSelection() const { return CachedSelected.bHasSelection; } -protected: + /** 获取选中交互物的提示文本(从缓存读取,不访问 Component) */ + FString GetSelectedPromptText() const { return CachedSelected.PromptText; } - virtual void BeginPlay() override; - virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; + /** 获取选中交互物的世界坐标(从缓存读取) */ + FVector GetSelectedWorldPosition() const { return CachedSelected.WorldPosition; } + + /** 获取完整的选中信息缓存 */ + const FSelectedInfo& GetCachedSelectedInfo() const { return CachedSelected; } + + // === Effect 完成回调(由 EffectManager 通过 ASystemDriver 调用)=== + + /** + * Effect 执行完毕回调。 + * State 回到 Free,清理 CurrentInteractable,通知 Character 解除交互, + * 重新排序和刷新缓存(因为消耗可能改变了列表)。 + */ + void OnInteractionFinished(); + + // === 当前交互物访问(EffectManager 需要访问)=== + + /** 获取当前正在交互的交互物(Executing 状态下有效) */ + UInteractableComponent* GetCurrentInteractable() const; private: - // === 状态机 === - /** 交互系统顶层状态 */ EInteractionState State = EInteractionState::Free; - /** Interacting 子阶段 */ - EInteractPhase Phase = EInteractPhase::None; - - // === 检测与选择 === - /** 当前附近的交互物列表,按水平距离排序 */ - UPROPERTY() - TArray NearbyInteractables; + TArray> NearbyInteractables; - /** 当前选中的交互物索引 */ + /** 当前选中的交互物索引(NearbyInteractables 中的下标) */ int32 SelectedIndex = 0; - /** 当前正在交互的交互物(交互过程中锁定) */ - UPROPERTY() - UInteractableComponent* CurrentInteractable = nullptr; + /** 当前正在交互的交互物(Executing 状态下锁定) */ + TWeakObjectPtr CurrentInteractable; - // === 摄像机控制 === + /** 玩家角色弱引用(用于排序距离计算和 SetInteracting 调用) */ + TWeakObjectPtr PlayerCharacter; - /** 推镜用临时摄像机 Actor(BeginPlay 中 Spawn) */ - UPROPERTY() - TObjectPtr PushCameraActor; - - /** 推镜是否已发起(Enter阶段一次性标志) */ - bool bPushStarted = false; - - // === 计时器 === - - /** 转向已用时间 */ - float TurnElapsed = 0.0f; - - /** 推镜后等待已用时间 */ - float WaitElapsed = 0.0f; - - /** 动画事件计时器(如60%时物品消失) */ - FTimerHandle AnimEventTimerHandle; - - /** 动画结束计时器 */ - FTimerHandle AnimEndTimerHandle; - - /** 退出阶段回位计时器 */ - FTimerHandle ExitTimerHandle; - - /** 动画事件是否已触发 */ - bool bAnimEventFired = false; - - // === UI 配置 === - - /** 提示 UI 蓝图类(需在编辑器中指定为 WBP_InteractionPrompt) */ - UPROPERTY(EditAnywhere, Category = "Interaction|UI") - TSubclassOf PromptWidgetClass; - - /** 文本底框 UI 蓝图类(需在编辑器中指定为 WBP_NarrativeText) */ - UPROPERTY(EditAnywhere, Category = "Interaction|UI") - TSubclassOf NarrativeWidgetClass; - - /** 提示 UI 高度偏移(交互物上方的世界空间 Z 偏移,cm) */ - UPROPERTY(EditAnywhere, Category = "Interaction|UI") - float PromptHeightOffset = 80.0f; - - // === UI 实例 === - - /** 交互提示 Widget 实例 */ - UPROPERTY() - TObjectPtr PromptWidget; - - /** 文本底框 Widget 实例 */ - UPROPERTY() - TObjectPtr NarrativeWidget; - - // === 文本选择 === - - /** 上次显示的叙事文本(用于不重复选取) */ - FString LastNarrativeText; + /** Presentation 层缓存数据 — 选中交互物的摘要信息 */ + FSelectedInfo CachedSelected; // === 内部方法 === - /** 更新选中交互物(排序后 SelectedIndex = 0) */ + /** + * 更新选中交互物:按与玩家的水平距离排序,重置 SelectedIndex=0。 + * 同时清理已失效的弱引用。 + */ void UpdateSelection(); - /** 更新提示 UI 内容和位置 */ - void UpdatePromptUI(); - - /** 隐藏提示 UI */ - void HidePrompt(); - - /** 更新提示 UI 的屏幕空间位置(跟随交互物世界位置投影) */ - void UpdatePromptScreenPosition(UInteractableComponent* Interactable); - - /** Enter 阶段 Tick:转向插值 + 推镜 + 等待 */ - void TickEnterPhase(float DeltaTime); - - /** AnimLock 阶段 Tick(v0.1 由计时器驱动,此处可为空) */ - void TickAnimLockPhase(float DeltaTime); - - /** Exit 阶段 Tick(v0.1 由定时器驱动回 Free) */ - void TickExitPhase(float DeltaTime); - - /** Enter 阶段完成后,根据交互类型分发到下一阶段 */ - void TransitionFromEnter(); - - /** 进入 AnimLock 阶段:播放蒙太奇或启动计时器 */ - void EnterAnimLockPhase(); - - /** 动画事件触发点回调(如拾取物消失、门状态切换) */ - void ExecuteAnimEvent(); - - /** 动画结束回调 */ - void OnAnimLockComplete(); - - /** 显示叙事文本(v0.1 用日志输出) */ - void ShowNarrativeText(const FString& Text); - - /** 隐藏叙事文本(v0.1 用日志输出) */ - void HideNarrativeText(); - - /** 推镜:移动 PushCameraActor 并切换 ViewTarget */ - void PushCamera(FVector Target, float Duration); - - /** 相机回位:切换回 Character 的 FollowCamera */ - void ReturnCamera(float Duration); - - /** 计算推镜目标位置 */ - FVector CalculatePushPosition(UInteractableComponent* Interactable); - - /** 随机选取叙事文本,排除上次 */ - FString PickRandomNarrative(const TArray& Texts); - - /** Exit 阶段定时器回调:回到 Free 状态 */ - void OnExitTimerComplete(); + /** + * 从 Component 读取选中项信息并写入 CachedSelected。 + * Presentation 层每帧只读 CachedSelected,不直接访问 Component。 + */ + void RefreshCachedSelection(); };