feat(interaction): 新增交互系统核心C++框架(6a-6f)

This commit is contained in:
2026-06-11 11:25:15 +08:00
parent a4fd3d3092
commit 00ca7044c7
12 changed files with 1776 additions and 0 deletions
+1
View File
@@ -25,6 +25,7 @@ public class ChildHood : ModuleRules
PublicIncludePaths.AddRange(new string[] {
"ChildHood",
"ChildHood/Interaction",
"ChildHood/Variant_Platforming",
"ChildHood/Variant_Platforming/Animation",
"ChildHood/Variant_Combat",
@@ -0,0 +1,30 @@
// ChildHood Project - 跨关卡持久数据持有者 实现
#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();
}
+56
View File
@@ -0,0 +1,56 @@
// ChildHood Project - 跨关卡持久数据持有者
// 对应技术方案: docs/technical/交互系统-技术方案.md 第12节(跨场景持久化设计)
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "ChildHoodGameInstance.generated.h"
/**
* UChildHoodGameInstance
*
* 跨关卡持久数据持有者。UE5 的 OpenLevel() 会销毁整个旧 World(包括
* Character 及其所有 UActorComponent),因此需要将持久数据与运行时行为分离。
*
* v0.1 为单场景原型,此 GameInstance 以空数据初始化。
* 后续多场景版本只需在 Project Settings 中将 GameInstance Class 设为此类即可。
*/
UCLASS()
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<FName> CollectedItemIDs;
/** 门开关状态映射(true=打开,false=关闭) */
UPROPERTY()
TMap<FName, bool> DoorStates;
};
@@ -0,0 +1,47 @@
// ChildHood Project - 通用交互物 Actor 基类 实现
#include "Interaction/InteractableActor.h"
#include "Interaction/InteractableComponent.h"
#include "Components/StaticMeshComponent.h"
// ============================================================================
// 构造函数
// ============================================================================
AInteractableActor::AInteractableActor()
{
PrimaryActorTick.bCanEverTick = false;
// 创建 Mesh 为根组件
MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComp"));
RootComponent = MeshComp;
MeshComp->SetCollisionProfileName(TEXT("BlockAll"));
// 创建交互组件(UActorComponent 不需要 SetupAttachment,数据驱动型组件)
InteractableComp = CreateDefaultSubobject<UInteractableComponent>(TEXT("InteractableComp"));
}
// ============================================================================
// 交互回调
// ============================================================================
void AInteractableActor::OnInteractStarted_Implementation(UInteractableComponent* Comp)
{
if (!Comp || !MeshComp) return;
const FInteractableData& Data = Comp->GetInteractableData();
// 门类型:根据开关状态旋转 Mesh
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
{
FRotator CurrentRot = MeshComp->GetRelativeRotation();
float TargetYaw = Comp->IsDoorOpen() ? 90.0f : 0.0f;
MeshComp->SetRelativeRotation(FRotator(CurrentRot.Pitch, TargetYaw, CurrentRot.Roll));
}
}
void AInteractableActor::OnInteractEnded_Implementation(UInteractableComponent* Comp)
{
// 默认空实现,子类可覆盖
}
@@ -0,0 +1,69 @@
// ChildHood Project - 通用交互物 Actor 基类
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
// 职责:持有 Mesh + InteractableComponent,提供可被子类覆盖的交互回调
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "InteractableActor.generated.h"
class UInteractableComponent;
/**
* AInteractableActor
*
* 通用交互物 Actor 基类。场景中的每个交互物(冰箱、门、NPC 等)
* 均为该类的实例或其子类的实例。
*
* 组成:
* - UStaticMeshComponent* MeshComp(根组件,承载可视模型)
* - UInteractableComponent* InteractableComp(交互逻辑与数据)
*
* 子类可覆盖 OnInteractStarted / OnInteractEnded 以实现类型特定行为,
* 如门开关时的 Mesh 旋转、拾取物的消失效果等。
*/
UCLASS()
class AInteractableActor : public AActor
{
GENERATED_BODY()
public:
AInteractableActor();
// === 组件访问 ===
/** 获取静态网格组件 */
UFUNCTION(BlueprintCallable, Category = "Interact")
UStaticMeshComponent* GetMeshComp() const { return MeshComp; }
/** 获取交互组件 */
UFUNCTION(BlueprintCallable, Category = "Interact")
UInteractableComponent* GetInteractableComp() const { return InteractableComp; }
// === 可被子类覆盖的交互回调 ===
/**
* 交互开始时调用(门开关切换、状态恢复时也调用)。
* 默认实现:门类型根据开关状态对 Mesh 做 Yaw 旋转(开门+90度/关门0度)。
*/
UFUNCTION(BlueprintNativeEvent, Category = "Interact")
void OnInteractStarted(UInteractableComponent* Comp);
virtual void OnInteractStarted_Implementation(UInteractableComponent* Comp);
/** 交互结束时调用。默认为空实现,子类可覆盖。 */
UFUNCTION(BlueprintNativeEvent, Category = "Interact")
void OnInteractEnded(UInteractableComponent* Comp);
virtual void OnInteractEnded_Implementation(UInteractableComponent* Comp);
protected:
/** 交互物的可视模型 */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* MeshComp;
/** 交互物的交互组件(数据 + 检测 + 状态) */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
UInteractableComponent* InteractableComp;
};
@@ -0,0 +1,221 @@
// ChildHood Project - 可交互物组件 实现
#include "Interaction/InteractableComponent.h"
#include "Interaction/InteractableActor.h"
#include "Interaction/InteractionManager.h"
#include "ChildHoodGameInstance.h"
#include "ChildHood.h"
#include "Components/SphereComponent.h"
#include "Engine/World.h"
#include "GameFramework/Character.h"
#include "Kismet/GameplayStatics.h"
// ============================================================================
// 构造函数
// ============================================================================
UInteractableComponent::UInteractableComponent()
{
// 不需要 Tick,检测由碰撞事件驱动
PrimaryComponentTick.bCanEverTick = false;
}
// ============================================================================
// 生命周期
// ============================================================================
void UInteractableComponent::BeginPlay()
{
Super::BeginPlay();
AActor* Owner = GetOwner();
if (!Owner)
{
UE_LOG(LogChildHood, Warning, TEXT("InteractableComponent: 无 Owner,跳过初始化"));
return;
}
// --- 创建检测球 ---
DetectionSphere = NewObject<USphereComponent>(this, USphereComponent::StaticClass(), TEXT("DetectionSphere"));
if (DetectionSphere)
{
DetectionSphere->SetupAttachment(Owner->GetRootComponent());
DetectionSphere->RegisterComponent();
DetectionSphere->SetSphereRadius(Data.TriggerRadius);
DetectionSphere->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
// 关闭物理模拟,仅用于 Overlap 检测
DetectionSphere->SetGenerateOverlapEvents(true);
DetectionSphere->SetCanEverAffectNavigation(false);
// 绑定碰撞事件
DetectionSphere->OnComponentBeginOverlap.AddDynamic(this, &UInteractableComponent::OnDetectionOverlapBegin);
DetectionSphere->OnComponentEndOverlap.AddDynamic(this, &UInteractableComponent::OnDetectionOverlapEnd);
}
else
{
UE_LOG(LogChildHood, Error, TEXT("InteractableComponent: DetectionSphere 创建失败"));
}
// --- 恢复跨场景持久状态 ---
if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance<UChildHoodGameInstance>())
{
RestorePersistentState(GI);
}
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent Init: %s (Type=%s, Radius=%f)"),
Data.DisplayName.IsEmpty() ? TEXT("(unnamed)") : *Data.DisplayName,
*UEnum::GetValueAsString(Data.Type),
Data.TriggerRadius);
}
// ============================================================================
// 提示文本
// ============================================================================
FString UInteractableComponent::GetPromptText() const
{
FString Verb = Data.InteractVerb;
// 门类型:根据当前开关状态选择动词
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
{
Verb = bDoorIsOpen ? Data.InteractVerbAlt : Data.InteractVerb;
}
return FString::Printf(TEXT("[E] %s %s"), *Verb, *Data.DisplayName);
}
// ============================================================================
// 消耗管理
// ============================================================================
void UInteractableComponent::Consume()
{
bIsConsumed = true;
// 禁用检测球,不再触发 Overlap
if (DetectionSphere)
{
DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
// 写入持久层
if (UWorld* World = GetWorld())
{
if (UChildHoodGameInstance* GI = World->GetGameInstance<UChildHoodGameInstance>())
{
GI->MarkItemCollected(Data.ItemId);
}
}
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent 已消耗: %s"), *Data.ItemId.ToString());
}
// ============================================================================
// 门状态管理
// ============================================================================
void UInteractableComponent::ToggleDoor()
{
bDoorIsOpen = !bDoorIsOpen;
UE_LOG(LogChildHood, Log, TEXT("InteractableComponent 门状态切换: %s → %s"),
*Data.ItemId.ToString(),
bDoorIsOpen ? TEXT("开") : TEXT("关"));
}
// ============================================================================
// 位置查询
// ============================================================================
FVector UInteractableComponent::GetWorldLocation() const
{
AActor* Owner = GetOwner();
return Owner ? Owner->GetActorLocation() : FVector::ZeroVector;
}
// ============================================================================
// 跨场景持久状态恢复
// ============================================================================
void UInteractableComponent::RestorePersistentState(UChildHoodGameInstance* GI)
{
if (!GI) return;
// oneShot 物品:已消耗 → 隐藏 + 禁用检测
if (Data.bOneShot && GI->IsItemCollected(Data.ItemId))
{
bIsConsumed = true;
if (DetectionSphere)
{
DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
if (AInteractableActor* Owner = Cast<AInteractableActor>(GetOwner()))
{
Owner->SetActorHiddenInGame(true);
}
UE_LOG(LogChildHood, Log, TEXT("RestorePersistentState: 已消耗物品隐藏: %s"), *Data.ItemId.ToString());
}
// 门:恢复开关状态
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
{
bDoorIsOpen = GI->IsDoorOpen(Data.ItemId);
// 通知 Actor 应用视觉状态(Mesh 旋转)
if (AInteractableActor* Owner = Cast<AInteractableActor>(GetOwner()))
{
Owner->OnInteractStarted(this);
}
UE_LOG(LogChildHood, Log, TEXT("RestorePersistentState: 门状态恢复: %s → %s"),
*Data.ItemId.ToString(),
bDoorIsOpen ? TEXT("开") : TEXT("关"));
}
}
// ============================================================================
// 碰撞回调
// ============================================================================
void UInteractableComponent::OnDetectionOverlapBegin(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult)
{
if (!OtherActor) return;
// 只响应 Character(玩家)的进入
ACharacter* Character = Cast<ACharacter>(OtherActor);
if (!Character) return;
// 已消耗的交互物不再注册
if (bIsConsumed) return;
UE_LOG(LogChildHood, Verbose, TEXT("检测到角色进入范围: %s ← %s"),
*Data.DisplayName, *OtherActor->GetName());
// 获取角色的 InteractionManager 并注册自己
if (UInteractionManager* IM = Character->FindComponentByClass<UInteractionManager>())
{
IM->RegisterInteractable(this);
}
}
void UInteractableComponent::OnDetectionOverlapEnd(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex)
{
if (!OtherActor) return;
ACharacter* Character = Cast<ACharacter>(OtherActor);
if (!Character) return;
UE_LOG(LogChildHood, Verbose, TEXT("角色离开范围: %s ← %s"),
*Data.DisplayName, *OtherActor->GetName());
// 获取角色的 InteractionManager 并注销自己
if (UInteractionManager* IM = Character->FindComponentByClass<UInteractionManager>())
{
IM->UnregisterInteractable(this);
}
}
@@ -0,0 +1,101 @@
// ChildHood Project - 可交互物组件
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第12节(跨场景持久化)
// 职责:单个交互物配置数据、触发区域、运行时状态、跨场景持久状态恢复
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InteractionTypes.h"
#include "InteractableComponent.generated.h"
class UChildHoodGameInstance;
class USphereComponent;
/**
* UInteractableComponent
*
* 挂载在 AInteractableActor(或其子类)上的组件。
* 持有该交互物的全部配置数据(FInteractableData)、运行时触发检测球、
* 消耗/门状态管理,以及跨场景持久状态恢复。
*/
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
class UInteractableComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInteractableComponent();
// === 配置数据 ===
/** 该交互物的全部配置数据,编辑器中按实例独立配置 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact")
FInteractableData Data;
// === 查询接口 ===
/** 获取配置数据引用(只读) */
const FInteractableData& GetInteractableData() const { return Data; }
/**
* 获取提示文本,格式:"[E] 动作词 名称"
* 门类型根据当前开关状态选择动词
*/
FString GetPromptText() const;
/** 该交互物是否已被消耗(oneShot 模式) */
bool IsConsumed() const { return bIsConsumed; }
/** 标记为已消耗,禁用检测球,写入持久层 */
void Consume();
// === 门状态管理 ===
/** 切换门开关状态 */
void ToggleDoor();
/** 门是否处于打开状态 */
bool IsDoorOpen() const { return bDoorIsOpen; }
/** 获取交互物在世界空间的位置 */
FVector GetWorldLocation() const;
/** 从 GameInstance 恢复跨场景持久状态(BeginPlay 中调用) */
void RestorePersistentState(UChildHoodGameInstance* GI);
protected:
virtual void BeginPlay() override;
private:
/** 运行时创建的检测球,用于触发玩家进入/离开交互范围 */
UPROPERTY()
USphereComponent* DetectionSphere;
/** oneShot 物品是否已被消耗 */
bool bIsConsumed = false;
/** 门是否处于打开状态(仅 Operate+Door 类型使用) */
bool bDoorIsOpen = false;
// === 碰撞回调 ===
/** 玩家进入检测范围 */
UFUNCTION()
void OnDetectionOverlapBegin(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep,
const FHitResult& SweepResult);
/** 玩家离开检测范围 */
UFUNCTION()
void OnDetectionOverlapEnd(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor,
UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex);
};
@@ -0,0 +1,815 @@
// ChildHood Project - 交互管理器核心组件 实现
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(行为规则实现)
#include "Interaction/InteractionManager.h"
#include "Interaction/InteractableComponent.h"
#include "Interaction/InteractableActor.h"
#include "Interaction/PushCameraActor.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"
// 定义交互系统日志类别
DECLARE_LOG_CATEGORY_EXTERN(LogInteraction, Log, All);
DEFINE_LOG_CATEGORY(LogInteraction);
// ============================================================================
// 构造函数
// ============================================================================
UInteractionManager::UInteractionManager()
{
// 默认关闭 Tick,首个交互物注册时开启
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = false;
}
// ============================================================================
// 生命周期
// ============================================================================
void UInteractionManager::BeginPlay()
{
Super::BeginPlay();
// Spawn 推镜用临时摄像机 Actor(使用专用的 APushCameraActor)
// 随 Character 生命周期,不反复创建/销毁
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = GetOwner();
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
APushCameraActor* SpawnedCamera = GetWorld()->SpawnActor<APushCameraActor>(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"));
}
// ============================================================================
// Tick 驱动
// ============================================================================
void UInteractionManager::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// Free 状态不需要 Tick
if (State == EInteractionState::Free)
{
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<int32>(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<AChildHoodCharacter>(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<int32>(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)
{
if (!Interactable)
{
return;
}
// 去重检查
if (NearbyInteractables.Contains(Interactable))
{
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();
// 首个交互物注册时开启 Tick
if (NearbyInteractables.Num() == 1)
{
SetComponentTickEnabled(true);
}
UE_LOG(LogInteraction, Log, TEXT("RegisterInteractable: 注册 '%s', 列表大小=%d"),
*Interactable->GetInteractableData().DisplayName, NearbyInteractables.Num());
}
void UInteractionManager::UnregisterInteractable(UInteractableComponent* Interactable)
{
if (!Interactable)
{
return;
}
bool bRemoved = NearbyInteractables.Remove(Interactable) > 0;
if (!bRemoved)
{
return;
}
// 如果移除的是当前正在交互的交互物,清空引用
if (CurrentInteractable == Interactable)
{
CurrentInteractable = nullptr;
}
if (NearbyInteractables.Num() > 0)
{
UpdateSelection();
UpdatePromptUI();
}
else
{
// 列表为空,隐藏提示
HidePrompt();
// 最后一个注销时关闭 Tick
SetComponentTickEnabled(false);
}
UE_LOG(LogInteraction, Log, TEXT("UnregisterInteractable: 注销 '%s', 列表大小=%d"),
*Interactable->GetInteractableData().DisplayName, NearbyInteractables.Num());
}
// ============================================================================
// Enter 阶段 Tick
// ============================================================================
void UInteractionManager::TickEnterPhase(float DeltaTime)
{
if (!CurrentInteractable)
{
UE_LOG(LogInteraction, Warning, TEXT("TickEnterPhase: CurrentInteractable 为空,强制取消"));
CancelInteract();
return;
}
ACharacter* Char = Cast<ACharacter>(GetOwner());
if (!Char)
{
return;
}
const FInteractableData& Data = CurrentInteractable->GetInteractableData();
// --- 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();
}
}
}
// ============================================================================
// AnimLock 阶段 Tick(v0.1 由计时器驱动,此处为空)
// ============================================================================
void UInteractionManager::TickAnimLockPhase(float DeltaTime)
{
// 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)
{
return;
}
const FInteractableData& Data = CurrentInteractable->GetInteractableData();
UE_LOG(LogInteraction, Log, TEXT("TransitionFromEnter: 类型=%d 分发"), static_cast<int32>(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<int32>(Data.Type));
// 未知类型直接回 Free
CancelInteract();
break;
}
}
// ============================================================================
// AnimLock 阶段
// ============================================================================
void UInteractionManager::EnterAnimLockPhase()
{
if (!CurrentInteractable)
{
return;
}
const FInteractableData& Data = CurrentInteractable->GetInteractableData();
// 计算总动画时长:有蒙太奇用蒙太奇长度,否则用配置的 AnimDuration
float TotalAnimTime = Data.AnimDuration;
if (Data.InteractMontage)
{
TotalAnimTime = Data.InteractMontage->GetPlayLength();
if (TotalAnimTime <= 0.f)
{
TotalAnimTime = Data.AnimDuration;
}
// 播放蒙太奇
ACharacter* Char = Cast<ACharacter>(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;
}
// ============================================================================
// 动画事件触发回调
// ============================================================================
void UInteractionManager::ExecuteAnimEvent()
{
if (!CurrentInteractable)
{
return;
}
bAnimEventFired = true;
const FInteractableData& Data = CurrentInteractable->GetInteractableData();
UE_LOG(LogInteraction, Log, TEXT("ExecuteAnimEvent: 类型=%d, 名称='%s'"),
static_cast<int32>(Data.Type), *Data.DisplayName);
switch (Data.Type)
{
case EInteractType::Pickup:
{
// 标记消耗 → 隐藏 Actor → 通知物品系统
CurrentInteractable->Consume();
// 隐藏交互物 Actor
if (AInteractableActor* OwnerActor = Cast<AInteractableActor>(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<UChildHoodGameInstance>())
{
GI->SetDoorOpen(Data.ItemId, CurrentInteractable->IsDoorOpen());
}
// 通知 Actor 处理 Mesh 旋转等视觉反馈
if (AInteractableActor* OwnerActor = Cast<AInteractableActor>(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<int32>(Data.Type), *Data.DisplayName);
switch (Data.Type)
{
case EInteractType::Pickup:
{
// 直接回到 Free(无 Active/Exit,无相机/UI 需清理)
AChildHoodCharacter* Char = Cast<AChildHoodCharacter>(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<AChildHoodCharacter>(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;
}
// v0.1: 用日志输出替代 UI
FString PromptText = Selected->GetPromptText();
UE_LOG(LogInteraction, Log, TEXT("UpdatePromptUI: [提示] %s"), *PromptText);
}
void UInteractionManager::HidePrompt()
{
// v0.1: 用日志输出替代 UI
UE_LOG(LogInteraction, Verbose, TEXT("HidePrompt: 隐藏交互提示"));
}
void UInteractionManager::ShowNarrativeText(const FString& Text)
{
// v0.1: 用日志输出替代 UI
if (Text.IsEmpty())
{
UE_LOG(LogInteraction, Warning, TEXT("ShowNarrativeText: 文本为空"));
return;
}
UE_LOG(LogInteraction, Log, TEXT("ShowNarrativeText: [文本] %s"), *Text);
}
void UInteractionManager::HideNarrativeText()
{
// v0.1: 用日志输出替代 UI
UE_LOG(LogInteraction, Verbose, TEXT("HideNarrativeText: 隐藏叙事文本"));
}
// ============================================================================
// 摄像机控制
// ============================================================================
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<FString>& 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<AChildHoodCharacter>(GetOwner());
if (Char)
{
Char->SetInteracting(false);
}
// 回到 Free 状态
State = EInteractionState::Free;
Phase = EInteractPhase::None;
CurrentInteractable = nullptr;
// 如果仍在检测范围内,重新显示提示
if (NearbyInteractables.Num() > 0)
{
UpdateSelection();
UpdatePromptUI();
}
}
@@ -0,0 +1,206 @@
// ChildHood Project - 交互管理器核心组件
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)+ 第6节(行为规则实现)
// 职责:交互全流程状态机、检测选择、类型分发、摄像机/转向/UI 控制
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InteractionTypes.h"
#include "InteractionManager.generated.h"
class UInteractableComponent;
class APushCameraActor;
class AChildHoodCharacter;
/**
* UInteractionManager
*
* 挂载在 AChildHoodCharacter 上的组件,负责交互全流程管理:
* - 层级状态机驱动(Free / Interacting + 子阶段)
* - 附近交互物检测列表排序与选中管理
* - 四种交互类型分发(观察/拾取/操作/对话)
* - 角色自动转向插值
* - 摄像机推镜/回位控制
* - UI 提示与文本显示控制(v0.1 先用日志)
*/
UCLASS(ClassGroup=(Interaction), meta=(BlueprintSpawnableComponent))
class UInteractionManager : public UActorComponent
{
GENERATED_BODY()
public:
UInteractionManager();
// === 公开方法 ===
/** 发起交互(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")
void RegisterInteractable(UInteractableComponent* Interactable);
/** 从检测列表注销交互物(离开范围或消耗时触发) */
UFUNCTION(BlueprintCallable, Category = "Interaction")
void UnregisterInteractable(UInteractableComponent* Interactable);
// === 委托 ===
/** 物品拾取事件 — 通知物品系统(Pickup 类型 ExecuteAnimEvent 中广播) */
UPROPERTY(BlueprintAssignable, Category = "Interaction|Events")
FMLocalItemEvent OnItemPickedUp;
/** 物品获得事件 — 通知收集系统(Search 类型 OnAnimLockComplete 中广播) */
UPROPERTY(BlueprintAssignable, Category = "Interaction|Events")
FMLocalItemEvent OnItemFound;
// === 可调参数 ===
/** 角色转向时长(秒) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
float TurnDuration = 0.2f;
/** 提示 UI 淡入时长(秒) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
float PromptFadeInDuration = 0.2f;
/** 提示 UI 淡出时长(秒) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction|Tuning")
float PromptFadeOutDuration = 0.2f;
protected:
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
private:
// === 状态机 ===
/** 交互系统顶层状态 */
EInteractionState State = EInteractionState::Free;
/** Interacting 子阶段 */
EInteractPhase Phase = EInteractPhase::None;
// === 检测与选择 ===
/** 当前附近的交互物列表,按水平距离排序 */
UPROPERTY()
TArray<UInteractableComponent*> NearbyInteractables;
/** 当前选中的交互物索引 */
int32 SelectedIndex = 0;
/** 当前正在交互的交互物(交互过程中锁定) */
UPROPERTY()
UInteractableComponent* CurrentInteractable = nullptr;
// === 摄像机控制 ===
/** 推镜用临时摄像机 Actor(BeginPlay 中 Spawn) */
UPROPERTY()
TObjectPtr<APushCameraActor> PushCameraActor;
/** 推镜是否已发起(Enter阶段一次性标志) */
bool bPushStarted = false;
// === 计时器 ===
/** 转向已用时间 */
float TurnElapsed = 0.0f;
/** 推镜后等待已用时间 */
float WaitElapsed = 0.0f;
/** 动画事件计时器(如60%时物品消失) */
FTimerHandle AnimEventTimerHandle;
/** 动画结束计时器 */
FTimerHandle AnimEndTimerHandle;
/** 退出阶段回位计时器 */
FTimerHandle ExitTimerHandle;
/** 动画事件是否已触发 */
bool bAnimEventFired = false;
// === UI 引用(v0.1 后续任务创建) ===
/** 交互提示 Widget(v0.1 用日志替代) */
UPROPERTY()
TObjectPtr<UUserWidget> PromptWidget;
/** 文本底框 Widget(v0.1 用日志替代) */
UPROPERTY()
TObjectPtr<UUserWidget> NarrativeWidget;
// === 文本选择 ===
/** 上次显示的叙事文本(用于不重复选取) */
FString LastNarrativeText;
// === 内部方法 ===
/** 更新选中交互物(排序后 SelectedIndex = 0) */
void UpdateSelection();
/** 更新提示 UI(v0.1 用日志输出) */
void UpdatePromptUI();
/** 隐藏提示 UI(v0.1 用日志输出) */
void HidePrompt();
/** 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<FString>& Texts);
/** Exit 阶段定时器回调:回到 Free 状态 */
void OnExitTimerComplete();
};
@@ -0,0 +1,178 @@
// ChildHood Project - 交互系统数据类型定义
// 对应技术方案: docs/technical/交互系统-技术方案.md 第5节(核心数据结构)+ 第12节(跨场景持久化)
#pragma once
#include "CoreMinimal.h"
#include "InteractionTypes.generated.h"
// ============================================================================
// 枚举
// ============================================================================
/** 交互类型 */
UENUM(BlueprintType)
enum class EInteractType : uint8
{
Examine UMETA(DisplayName = "观察"),
Pickup UMETA(DisplayName = "拾取"),
Operate UMETA(DisplayName = "操作"),
Talk UMETA(DisplayName = "对话")
};
/** 操作子类型(仅 Operate 类型有效) */
UENUM(BlueprintType)
enum class EOperateSubType : uint8
{
Door UMETA(DisplayName = "门(开关切换)"),
Search UMETA(DisplayName = "翻找")
};
/** 交互系统顶层状态 */
UENUM(BlueprintType)
enum class EInteractionState : uint8
{
Free UMETA(DisplayName = "自由状态"),
Interacting UMETA(DisplayName = "交互中"),
SubSystem UMETA(DisplayName = "子系统(v0.1预留)"),
Cutscene UMETA(DisplayName = "过场(v0.1预留)")
};
/** Interacting 子阶段 */
UENUM(BlueprintType)
enum class EInteractPhase : uint8
{
None UMETA(DisplayName = "无"),
Enter UMETA(DisplayName = "进入(转向+推镜)"),
AnimLock UMETA(DisplayName = "动画锁定"),
Active UMETA(DisplayName = "活跃(等待输入)"),
Exit UMETA(DisplayName = "退出(回位)")
};
/** 交互结束后动作 */
UENUM(BlueprintType)
enum class EPostAction : uint8
{
Return UMETA(DisplayName = "回到Free"),
Minigame UMETA(DisplayName = "小游戏(v0.1预留)"),
Dialogue UMETA(DisplayName = "对话(v0.1预留)"),
SceneChange UMETA(DisplayName = "场景切换(v0.1预留)")
};
// ============================================================================
// 配置数据结构
// ============================================================================
/** 可交互物配置数据 — 每个交互物实例独立配置 */
USTRUCT(BlueprintType)
struct FInteractableData
{
GENERATED_BODY()
// === 基本信息 ===
/** 唯一ID(如 "fridge_home"),用于持久化存储和物品系统对接 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
FName ItemId;
/** 显示名称(如 "旧冰箱"),用于提示UI */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
FString DisplayName;
/** 交互类型 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic")
EInteractType Type = EInteractType::Examine;
/** 操作子类型,仅 Type==Operate 时可见可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Basic",
meta = (EditCondition = "Type == EInteractType::Operate", EditConditionHides))
EOperateSubType OperateSubType = EOperateSubType::Door;
// === 触发 ===
/** 触发半径(cm),默认1.5m */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger",
meta = (ClampMin = "50.0", ClampMax = "500.0"))
float TriggerRadius = 150.0f;
/** 是否一次性消耗(消耗后不再可交互) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Trigger")
bool bOneShot = false;
// === 提示文本 ===
/** 动作词(如 "查看"、"拾取"、"打开") */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt")
FString InteractVerb;
/** 备选动词(如 "关上"),门打开时使用,仅 Operate+Door 时可见 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Prompt",
meta = (EditCondition = "Type == EInteractType::Operate && OperateSubType == EOperateSubType::Door"))
FString InteractVerbAlt;
// === 叙事/对话文本 ===
/** 文本列表,随机选取,不重复上次 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Text")
TArray<FString> NarrativeTexts;
// === 摄像机推镜 ===
/** 是否使用摄像机推镜 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera")
bool bUseCameraPush = false;
/** 推镜距离(cm),仅 bUseCameraPush 时可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
meta = (EditCondition = "bUseCameraPush", ClampMin = "10.0", ClampMax = "300.0"))
float CameraPushDistance = 80.0f;
/** 推镜过渡时长(秒),仅 bUseCameraPush 时可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
meta = (EditCondition = "bUseCameraPush", ClampMin = "0.1", ClampMax = "2.0"))
float CameraPushDuration = 0.5f;
/** 相机回位时长(秒),仅 bUseCameraPush 时可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
meta = (EditCondition = "bUseCameraPush", ClampMin = "0.1", ClampMax = "2.0"))
float CameraReturnDuration = 0.3f;
/** 推镜完成后到文本显示的等待时间(秒),仅 bUseCameraPush 时可编辑 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Camera",
meta = (EditCondition = "bUseCameraPush", ClampMin = "0.0", ClampMax = "1.0"))
float PushWaitDelay = 0.2f;
// === 动画 ===
/** 交互动画蒙太奇(可选,为空时用计时器模拟) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim")
TObjectPtr<UAnimMontage> InteractMontage;
/** 无蒙太奇时的动画持续时间(秒) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim",
meta = (ClampMin = "0.1", ClampMax = "5.0"))
float AnimDuration = 1.0f;
/** 动画事件触发点(0-1比例),如0.6表示动画播放至60%时触发物品消失 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Anim",
meta = (ClampMin = "0.0", ClampMax = "1.0"))
float AnimEventTimepoint = 0.6f;
// === 结束动作 ===
/** 交互结束后的动作 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interact|Post")
EPostAction PostAction = EPostAction::Return;
};
// ============================================================================
// 委托
// ============================================================================
/** 物品拾取/获得事件 — 通知物品系统和收集系统 */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
FMLocalItemEvent,
FName, ItemId,
const FString&, DisplayName,
const FName&, Category
);
@@ -0,0 +1,19 @@
// ChildHood Project - 推镜用临时摄像机 实现
#include "Interaction/PushCameraActor.h"
#include "Camera/CameraComponent.h"
// ============================================================================
// 构造函数
// ============================================================================
APushCameraActor::APushCameraActor()
{
PrimaryActorTick.bCanEverTick = false;
// 创建摄像机组件
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComp"));
// 不需要根组件的碰撞
SetRootComponent(CameraComp);
}
@@ -0,0 +1,33 @@
// ChildHood Project - 推镜用临时摄像机
// 对应技术方案: docs/technical/交互系统-技术方案.md 第3节(类设计)
// 职责:极简 Actor,仅持有一个 UCameraComponent,供 SetViewTargetWithBlend 使用
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PushCameraActor.generated.h"
class UCameraComponent;
/**
* APushCameraActor
*
* 交互系统推镜时使用的临时摄像机 Actor。
* 由 InteractionManager 在 BeginPlay 中 Spawn,随 Character 生命周期。
* InteractionManager 在推镜时设置其 Location/Rotation,
* 然后通过 PlayerController::SetViewTargetWithBlend 切换 ViewTarget。
*/
UCLASS()
class APushCameraActor : public AActor
{
GENERATED_BODY()
public:
APushCameraActor();
/** 推镜摄像机组件 */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UCameraComponent* CameraComp;
};