Merge pull request 'fix/asset-catalog-pipeline' (#37) from fix/asset-catalog-pipeline into main
Reviewed-on: http://micecola.top:8418/starmen1265/ChildHood/pulls/37
This commit was merged in pull request #37.
This commit is contained in:
@@ -25,6 +25,7 @@ public class ChildHood : ModuleRules
|
|||||||
|
|
||||||
PublicIncludePaths.AddRange(new string[] {
|
PublicIncludePaths.AddRange(new string[] {
|
||||||
"ChildHood",
|
"ChildHood",
|
||||||
|
"ChildHood/Interaction",
|
||||||
"ChildHood/Variant_Platforming",
|
"ChildHood/Variant_Platforming",
|
||||||
"ChildHood/Variant_Platforming/Animation",
|
"ChildHood/Variant_Platforming/Animation",
|
||||||
"ChildHood/Variant_Combat",
|
"ChildHood/Variant_Combat",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "EnhancedInputSubsystems.h"
|
#include "EnhancedInputSubsystems.h"
|
||||||
#include "InputActionValue.h"
|
#include "InputActionValue.h"
|
||||||
#include "ChildHood.h"
|
#include "ChildHood.h"
|
||||||
|
#include "Interaction/InteractionManager.h"
|
||||||
|
|
||||||
AChildHoodCharacter::AChildHoodCharacter()
|
AChildHoodCharacter::AChildHoodCharacter()
|
||||||
{
|
{
|
||||||
@@ -31,8 +32,6 @@ AChildHoodCharacter::AChildHoodCharacter()
|
|||||||
CMC->BrakingDecelerationWalking = 1500.0f; // 停止减速 0.3s (450/0.3)
|
CMC->BrakingDecelerationWalking = 1500.0f; // 停止减速 0.3s (450/0.3)
|
||||||
CMC->BrakingDecelerationFalling = 1500.0f;
|
CMC->BrakingDecelerationFalling = 1500.0f;
|
||||||
CMC->MinAnalogWalkSpeed = 20.f;
|
CMC->MinAnalogWalkSpeed = 20.f;
|
||||||
// WalkableFloorAngle 默认 45°,无需修改
|
|
||||||
// v0.1 不跳跃,不设置 JumpZVelocity / AirControl
|
|
||||||
|
|
||||||
// Create a camera boom (pulls in towards the player if there is a collision)
|
// Create a camera boom (pulls in towards the player if there is a collision)
|
||||||
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
|
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
|
||||||
@@ -42,19 +41,22 @@ AChildHoodCharacter::AChildHoodCharacter()
|
|||||||
CameraBoom->bUsePawnControlRotation = true;
|
CameraBoom->bUsePawnControlRotation = true;
|
||||||
|
|
||||||
// 摄像机软跟随
|
// 摄像机软跟随
|
||||||
CameraBoom->bEnableCameraLag = true; // 启用位置延迟跟随
|
CameraBoom->bEnableCameraLag = true;
|
||||||
CameraBoom->CameraLagSpeed = 6.0f; // 跟随速度(可调 2-8)
|
CameraBoom->CameraLagSpeed = 6.0f;
|
||||||
CameraBoom->bEnableCameraRotationLag = false; // v0.1 不启用旋转延迟
|
CameraBoom->bEnableCameraRotationLag = false;
|
||||||
|
|
||||||
// 摄像机碰撞处理
|
// 摄像机碰撞处理
|
||||||
CameraBoom->bDoCollisionTest = true; // 启用碰撞检测
|
CameraBoom->bDoCollisionTest = true;
|
||||||
CameraBoom->ProbeSize = 12.0f; // 碰撞探针半径
|
CameraBoom->ProbeSize = 12.0f;
|
||||||
CameraBoom->ProbeChannel = ECC_Camera; // 使用 Camera 碰撞通道
|
CameraBoom->ProbeChannel = ECC_Camera;
|
||||||
|
|
||||||
// Create a follow camera
|
// Create a follow camera
|
||||||
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
|
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
|
||||||
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
|
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
|
||||||
FollowCamera->bUsePawnControlRotation = false;
|
FollowCamera->bUsePawnControlRotation = false;
|
||||||
|
|
||||||
|
// 创建交互管理器组件
|
||||||
|
InteractionManager = CreateDefaultSubobject<UInteractionManager>(TEXT("InteractionManager"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||||
@@ -69,7 +71,7 @@ void AChildHoodCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInput
|
|||||||
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
||||||
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AChildHoodCharacter::Look);
|
||||||
|
|
||||||
// Interaction IAs — bind to placeholder callbacks; interaction system will hook into these
|
// Interaction — delegates to InteractionManager
|
||||||
if (InteractAction)
|
if (InteractAction)
|
||||||
{
|
{
|
||||||
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnInteract);
|
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AChildHoodCharacter::OnInteract);
|
||||||
@@ -109,23 +111,35 @@ void AChildHoodCharacter::Look(const FInputActionValue& Value)
|
|||||||
|
|
||||||
void AChildHoodCharacter::OnInteract(const FInputActionValue& Value)
|
void AChildHoodCharacter::OnInteract(const FInputActionValue& Value)
|
||||||
{
|
{
|
||||||
// v0.1 placeholder — interaction system will provide implementation
|
if (UInteractionManager* IM = GetInteractionManager())
|
||||||
// Interaction system hooks in via blueprint or future C++ delegate
|
{
|
||||||
|
// E 键双重功能:Free 发起交互,Active 结束交互
|
||||||
|
IM->BeginInteract();
|
||||||
|
IM->CancelInteract();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AChildHoodCharacter::OnCancel(const FInputActionValue& Value)
|
void AChildHoodCharacter::OnCancel(const FInputActionValue& Value)
|
||||||
{
|
{
|
||||||
// v0.1 placeholder — interaction system will provide implementation
|
if (UInteractionManager* IM = GetInteractionManager())
|
||||||
|
{
|
||||||
|
IM->CancelInteract();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AChildHoodCharacter::OnSwitchInteract(const FInputActionValue& Value)
|
void AChildHoodCharacter::OnSwitchInteract(const FInputActionValue& Value)
|
||||||
{
|
{
|
||||||
// v0.1 placeholder — interaction system will provide implementation
|
if (UInteractionManager* IM = GetInteractionManager())
|
||||||
|
{
|
||||||
|
float DirectionValue = Value.Get<float>();
|
||||||
|
int32 Direction = (DirectionValue >= 0.f) ? 1 : -1;
|
||||||
|
IM->SwitchTarget(Direction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AChildHoodCharacter::DoMove(float Right, float Forward)
|
void AChildHoodCharacter::DoMove(float Right, float Forward)
|
||||||
{
|
{
|
||||||
// P1: 交互中忽略移动输入
|
// 交互中屏蔽移动
|
||||||
if (bIsInteracting)
|
if (bIsInteracting)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -159,6 +173,15 @@ void AChildHoodCharacter::DoLook(float Yaw, float Pitch)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 交互系统方法
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
UInteractionManager* AChildHoodCharacter::GetInteractionManager() const
|
||||||
|
{
|
||||||
|
return InteractionManager;
|
||||||
|
}
|
||||||
|
|
||||||
void AChildHoodCharacter::SetInteracting(bool bValue)
|
void AChildHoodCharacter::SetInteracting(bool bValue)
|
||||||
{
|
{
|
||||||
bIsInteracting = bValue;
|
bIsInteracting = bValue;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
class USpringArmComponent;
|
class USpringArmComponent;
|
||||||
class UCameraComponent;
|
class UCameraComponent;
|
||||||
class UInputAction;
|
class UInputAction;
|
||||||
|
class UInteractionManager;
|
||||||
struct FInputActionValue;
|
struct FInputActionValue;
|
||||||
|
|
||||||
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
|
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
|
||||||
@@ -57,6 +58,10 @@ protected:
|
|||||||
UPROPERTY(EditAnywhere, Category="Input")
|
UPROPERTY(EditAnywhere, Category="Input")
|
||||||
UInputAction* SwitchInteractAction;
|
UInputAction* SwitchInteractAction;
|
||||||
|
|
||||||
|
/** 交互管理器组件 */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction", meta = (AllowPrivateAccess = "true"))
|
||||||
|
UInteractionManager* InteractionManager;
|
||||||
|
|
||||||
/** Whether the character is currently in an interaction */
|
/** Whether the character is currently in an interaction */
|
||||||
bool bIsInteracting = false;
|
bool bIsInteracting = false;
|
||||||
|
|
||||||
@@ -65,6 +70,22 @@ public:
|
|||||||
/** Constructor */
|
/** Constructor */
|
||||||
AChildHoodCharacter();
|
AChildHoodCharacter();
|
||||||
|
|
||||||
|
/** 获取交互管理器组件 */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interaction")
|
||||||
|
UInteractionManager* GetInteractionManager() const;
|
||||||
|
|
||||||
|
/** Set interaction state — called by interaction system */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction")
|
||||||
|
void SetInteracting(bool bValue);
|
||||||
|
|
||||||
|
/** Query: is the character currently interacting? */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category="Interaction")
|
||||||
|
bool IsInteracting() const;
|
||||||
|
|
||||||
|
/** Query: is the character currently running (moving on ground)? */
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category="Interaction")
|
||||||
|
bool IsRunning() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
/** Initialize input action bindings */
|
/** Initialize input action bindings */
|
||||||
@@ -78,13 +99,13 @@ protected:
|
|||||||
/** Called for looking input */
|
/** Called for looking input */
|
||||||
void Look(const FInputActionValue& Value);
|
void Look(const FInputActionValue& Value);
|
||||||
|
|
||||||
/** Called for interact input (E) — placeholder, delegates to interaction system */
|
/** Called for interact input (E) — delegates to InteractionManager */
|
||||||
void OnInteract(const FInputActionValue& Value);
|
void OnInteract(const FInputActionValue& Value);
|
||||||
|
|
||||||
/** Called for cancel input (ESC) — placeholder, delegates to interaction system */
|
/** Called for cancel input (ESC) — delegates to InteractionManager */
|
||||||
void OnCancel(const FInputActionValue& Value);
|
void OnCancel(const FInputActionValue& Value);
|
||||||
|
|
||||||
/** Called for switch interact target input (Tab/Wheel) — placeholder, delegates to interaction system */
|
/** Called for switch interact target input (Tab/Wheel) — delegates to InteractionManager */
|
||||||
void OnSwitchInteract(const FInputActionValue& Value);
|
void OnSwitchInteract(const FInputActionValue& Value);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -97,18 +118,6 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category="Input")
|
UFUNCTION(BlueprintCallable, Category="Input")
|
||||||
virtual void DoLook(float Yaw, float Pitch);
|
virtual void DoLook(float Yaw, float Pitch);
|
||||||
|
|
||||||
/** Set interaction state — called by interaction system */
|
|
||||||
UFUNCTION(BlueprintCallable, Category="Interaction")
|
|
||||||
void SetInteracting(bool bValue);
|
|
||||||
|
|
||||||
/** Query: is the character currently interacting? */
|
|
||||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="Interaction")
|
|
||||||
bool IsInteracting() const;
|
|
||||||
|
|
||||||
/** Query: is the character currently running (moving on ground)? */
|
|
||||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category="Interaction")
|
|
||||||
bool IsRunning() const;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
/** Returns CameraBoom subobject **/
|
/** Returns CameraBoom subobject **/
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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,897 @@
|
|||||||
|
// ChildHood Project - 交互管理器核心组件 实现
|
||||||
|
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(行为规则实现)
|
||||||
|
|
||||||
|
#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()
|
||||||
|
{
|
||||||
|
// 默认关闭 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"));
|
||||||
|
|
||||||
|
// --- 创建 UI Widget ---
|
||||||
|
APlayerController* PC = GetWorld()->GetFirstPlayerController();
|
||||||
|
if (PC)
|
||||||
|
{
|
||||||
|
// 创建提示 UI
|
||||||
|
if (PromptWidgetClass)
|
||||||
|
{
|
||||||
|
PromptWidget = CreateWidget<UInteractionPromptWidget>(PC, PromptWidgetClass);
|
||||||
|
if (PromptWidget)
|
||||||
|
{
|
||||||
|
PromptWidget->AddToViewport(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogInteraction, Warning, TEXT("InteractionManager: PromptWidgetClass 未设置,提示 UI 不可用"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文本底框 UI
|
||||||
|
if (NarrativeWidgetClass)
|
||||||
|
{
|
||||||
|
NarrativeWidget = CreateWidget<UNarrativeTextWidget>(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<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 提示 UI 屏幕位置更新
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void UInteractionManager::UpdatePromptScreenPosition(UInteractableComponent* Interactable)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
// 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;
|
||||||
|
class UInteractionPromptWidget;
|
||||||
|
class UNarrativeTextWidget;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 配置 ===
|
||||||
|
|
||||||
|
/** 提示 UI 蓝图类(需在编辑器中指定为 WBP_InteractionPrompt) */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||||
|
TSubclassOf<UInteractionPromptWidget> PromptWidgetClass;
|
||||||
|
|
||||||
|
/** 文本底框 UI 蓝图类(需在编辑器中指定为 WBP_NarrativeText) */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||||
|
TSubclassOf<UNarrativeTextWidget> NarrativeWidgetClass;
|
||||||
|
|
||||||
|
/** 提示 UI 高度偏移(交互物上方的世界空间 Z 偏移,cm) */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Interaction|UI")
|
||||||
|
float PromptHeightOffset = 80.0f;
|
||||||
|
|
||||||
|
// === UI 实例 ===
|
||||||
|
|
||||||
|
/** 交互提示 Widget 实例 */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInteractionPromptWidget> PromptWidget;
|
||||||
|
|
||||||
|
/** 文本底框 Widget 实例 */
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UNarrativeTextWidget> NarrativeWidget;
|
||||||
|
|
||||||
|
// === 文本选择 ===
|
||||||
|
|
||||||
|
/** 上次显示的叙事文本(用于不重复选取) */
|
||||||
|
FString LastNarrativeText;
|
||||||
|
|
||||||
|
// === 内部方法 ===
|
||||||
|
|
||||||
|
/** 更新选中交互物(排序后 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<FString>& Texts);
|
||||||
|
|
||||||
|
/** Exit 阶段定时器回调:回到 Free 状态 */
|
||||||
|
void OnExitTimerComplete();
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// ChildHood Project - 交互提示 UI 基类 实现
|
||||||
|
|
||||||
|
#include "Interaction/InteractionPromptWidget.h"
|
||||||
|
#include "Components/TextBlock.h"
|
||||||
|
|
||||||
|
void UInteractionPromptWidget::NativeOnInitialized()
|
||||||
|
{
|
||||||
|
Super::NativeOnInitialized();
|
||||||
|
|
||||||
|
// 初始隐藏
|
||||||
|
SetVisibility(ESlateVisibility::Hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionPromptWidget::SetPromptText(const FString& Text)
|
||||||
|
{
|
||||||
|
if (PromptTextBlock)
|
||||||
|
{
|
||||||
|
PromptTextBlock->SetText(FText::FromString(Text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionPromptWidget::SetScreenPosition(const FVector2D& Position)
|
||||||
|
{
|
||||||
|
// 使用 RenderTranslation 偏移 Widget 位置
|
||||||
|
// 蓝图中 CanvasPanel 的 Anchors 应设置为屏幕左上角
|
||||||
|
// 此处直接设置 RenderTranslation 来移动位置
|
||||||
|
SetRenderTranslation(Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionPromptWidget::Show()
|
||||||
|
{
|
||||||
|
if (bIsVisible) return;
|
||||||
|
bIsVisible = true;
|
||||||
|
|
||||||
|
SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||||
|
|
||||||
|
if (FadeInAnim)
|
||||||
|
{
|
||||||
|
PlayAnimation(FadeInAnim, 0.0f, 1, EUMGSequencePlayMode::Forward, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UInteractionPromptWidget::Hide()
|
||||||
|
{
|
||||||
|
if (!bIsVisible) return;
|
||||||
|
|
||||||
|
if (FadeOutAnim)
|
||||||
|
{
|
||||||
|
PlayAnimation(FadeOutAnim, 0.0f, 1, EUMGSequencePlayMode::Forward, 1.0f);
|
||||||
|
// 动画播完后隐藏(简单方案:直接设置定时器隐藏)
|
||||||
|
// 动画时长由蓝图中的 FadeOutAnim 决定,这里用固定 0.2s
|
||||||
|
FTimerHandle TempHandle;
|
||||||
|
if (UWorld* World = GetWorld())
|
||||||
|
{
|
||||||
|
World->GetTimerManager().SetTimer(TempHandle, [this]()
|
||||||
|
{
|
||||||
|
bIsVisible = false;
|
||||||
|
SetVisibility(ESlateVisibility::Hidden);
|
||||||
|
}, 0.2f, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bIsVisible = false;
|
||||||
|
SetVisibility(ESlateVisibility::Hidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// ChildHood Project - 交互提示 UI 基类
|
||||||
|
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(提示UI显示/隐藏)
|
||||||
|
// 职责:屏幕空间悬浮提示 "[E] 动作词 名称",跟随交互物位置
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Blueprint/UserWidget.h"
|
||||||
|
#include "InteractionPromptWidget.generated.h"
|
||||||
|
|
||||||
|
class UTextBlock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UInteractionPromptWidget
|
||||||
|
*
|
||||||
|
* 交互提示 UI 的 C++ 基类。蓝图子类(WBP_InteractionPrompt)继承此类,
|
||||||
|
* 在 UMG 编辑器中布局可视组件,并确保组件名与 BindWidget 匹配。
|
||||||
|
*
|
||||||
|
* 绑定要求:
|
||||||
|
* - TextBlock 名为 "PromptTextBlock" — 显示提示文字
|
||||||
|
* - WidgetAnimation 名为 "FadeInAnim" — 淡入动画(0.2s)
|
||||||
|
* - WidgetAnimation 名为 "FadeOutAnim" — 淡出动画(0.2s)
|
||||||
|
*/
|
||||||
|
UCLASS(Abstract, Blueprintable)
|
||||||
|
class UInteractionPromptWidget : public UUserWidget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/** 设置提示文字内容 */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction|Prompt")
|
||||||
|
void SetPromptText(const FString& Text);
|
||||||
|
|
||||||
|
/** 设置屏幕空间位置(由 InteractionManager 每帧调用) */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction|Prompt")
|
||||||
|
void SetScreenPosition(const FVector2D& Position);
|
||||||
|
|
||||||
|
/** 显示提示(播放淡入动画) */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction|Prompt")
|
||||||
|
void Show();
|
||||||
|
|
||||||
|
/** 隐藏提示(播放淡出动画) */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction|Prompt")
|
||||||
|
void Hide();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
/** 提示文字 TextBlock,蓝图子类必须有一个同名 TextBlock */
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
UTextBlock* PromptTextBlock;
|
||||||
|
|
||||||
|
/** 淡入动画(0.2s Opacity 0→1),蓝图子类必须有同名 WidgetAnimation */
|
||||||
|
UPROPERTY(meta = (BindWidgetAnim), Transient)
|
||||||
|
UWidgetAnimation* FadeInAnim;
|
||||||
|
|
||||||
|
/** 淡出动画(0.2s Opacity 1→0),蓝图子类必须有同名 WidgetAnimation */
|
||||||
|
UPROPERTY(meta = (BindWidgetAnim), Transient)
|
||||||
|
UWidgetAnimation* FadeOutAnim;
|
||||||
|
|
||||||
|
/** 是否正在显示中 */
|
||||||
|
bool bIsVisible = false;
|
||||||
|
|
||||||
|
virtual void NativeOnInitialized() override;
|
||||||
|
};
|
||||||
@@ -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,57 @@
|
|||||||
|
// ChildHood Project - 文本底框 UI 基类 实现
|
||||||
|
|
||||||
|
#include "Interaction/NarrativeTextWidget.h"
|
||||||
|
#include "Components/TextBlock.h"
|
||||||
|
|
||||||
|
void UNarrativeTextWidget::NativeOnInitialized()
|
||||||
|
{
|
||||||
|
Super::NativeOnInitialized();
|
||||||
|
|
||||||
|
// 初始隐藏
|
||||||
|
SetVisibility(ESlateVisibility::Hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UNarrativeTextWidget::SetNarrativeText(const FString& Text)
|
||||||
|
{
|
||||||
|
if (NarrativeTextBlock)
|
||||||
|
{
|
||||||
|
NarrativeTextBlock->SetText(FText::FromString(Text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UNarrativeTextWidget::Show()
|
||||||
|
{
|
||||||
|
if (bIsVisible) return;
|
||||||
|
bIsVisible = true;
|
||||||
|
|
||||||
|
SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||||
|
|
||||||
|
if (FadeInAnim)
|
||||||
|
{
|
||||||
|
PlayAnimation(FadeInAnim, 0.0f, 1, EUMGSequencePlayMode::Forward, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UNarrativeTextWidget::Hide()
|
||||||
|
{
|
||||||
|
if (!bIsVisible) return;
|
||||||
|
|
||||||
|
if (FadeOutAnim)
|
||||||
|
{
|
||||||
|
PlayAnimation(FadeOutAnim, 0.0f, 1, EUMGSequencePlayMode::Forward, 1.0f);
|
||||||
|
FTimerHandle TempHandle;
|
||||||
|
if (UWorld* World = GetWorld())
|
||||||
|
{
|
||||||
|
World->GetTimerManager().SetTimer(TempHandle, [this]()
|
||||||
|
{
|
||||||
|
bIsVisible = false;
|
||||||
|
SetVisibility(ESlateVisibility::Hidden);
|
||||||
|
}, 0.2f, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bIsVisible = false;
|
||||||
|
SetVisibility(ESlateVisibility::Hidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// ChildHood Project - 文本底框 UI 基类
|
||||||
|
// 对应技术方案: docs/technical/交互系统-技术方案.md 第6节(通用文本显示UI)
|
||||||
|
// 职责:半透明底框 + 一句话叙事/对话文本,带淡入淡出
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Blueprint/UserWidget.h"
|
||||||
|
#include "NarrativeTextWidget.generated.h"
|
||||||
|
|
||||||
|
class UTextBlock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UNarrativeTextWidget
|
||||||
|
*
|
||||||
|
* 文本底框 UI 的 C++ 基类。蓝图子类(WBP_NarrativeText)继承此类,
|
||||||
|
* 在 UMG 编辑器中布局可视组件,并确保组件名与 BindWidget 匹配。
|
||||||
|
*
|
||||||
|
* 绑定要求:
|
||||||
|
* - TextBlock 名为 "NarrativeTextBlock" — 显示叙事/对话文字
|
||||||
|
* - WidgetAnimation 名为 "FadeInAnim" — 淡入动画(0.2s)
|
||||||
|
* - WidgetAnimation 名为 "FadeOutAnim" — 淡出动画(0.2s)
|
||||||
|
*
|
||||||
|
* 布局要求:
|
||||||
|
* - Anchors: Horizontal Center, Vertical ~0.6(屏幕中心偏下)
|
||||||
|
* - Border: 半透明黑色背景(~60%),圆角,SizeToContent
|
||||||
|
* - TextBlock: 白色文字,居中,自动换行
|
||||||
|
*/
|
||||||
|
UCLASS(Abstract, Blueprintable)
|
||||||
|
class UNarrativeTextWidget : public UUserWidget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/** 设置叙事文本内容 */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction|Narrative")
|
||||||
|
void SetNarrativeText(const FString& Text);
|
||||||
|
|
||||||
|
/** 显示文本(播放淡入动画) */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction|Narrative")
|
||||||
|
void Show();
|
||||||
|
|
||||||
|
/** 隐藏文本(播放淡出动画) */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Interaction|Narrative")
|
||||||
|
void Hide();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
/** 叙事文字 TextBlock,蓝图子类必须有一个同名 TextBlock */
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
UTextBlock* NarrativeTextBlock;
|
||||||
|
|
||||||
|
/** 淡入动画(0.2s),蓝图子类必须有同名 WidgetAnimation */
|
||||||
|
UPROPERTY(meta = (BindWidgetAnim), Transient)
|
||||||
|
UWidgetAnimation* FadeInAnim;
|
||||||
|
|
||||||
|
/** 淡出动画(0.2s),蓝图子类必须有同名 WidgetAnimation */
|
||||||
|
UPROPERTY(meta = (BindWidgetAnim), Transient)
|
||||||
|
UWidgetAnimation* FadeOutAnim;
|
||||||
|
|
||||||
|
/** 是否正在显示中 */
|
||||||
|
bool bIsVisible = false;
|
||||||
|
|
||||||
|
virtual void NativeOnInitialized() override;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+129
-7
@@ -1,8 +1,8 @@
|
|||||||
# 交互系统 技术设计文档(v0.1)
|
# 交互系统 技术设计文档(v0.1)
|
||||||
|
|
||||||
> 对应执行案: `docs/design/交互系统-执行案.md` (v0.1.2)
|
> 对应执行案: `docs/design/交互系统-执行案.md` (v0.1.2)
|
||||||
> 关联系统: 角色控制系统、物品系统、收集系统
|
> 关联系统: 角色控制系统、物品系统、收集系统、GameInstance 持久化层
|
||||||
> 日期: 2026-06-07
|
> 日期: 2026-06-07(2026-06-11 更新:加入多场景持久化架构)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,16 @@
|
|||||||
**预估工期**:5-7 天
|
**预估工期**:5-7 天
|
||||||
**新增/修改代码量**:约 600-800 行 C++ + 3 个 UMG 蓝图
|
**新增/修改代码量**:约 600-800 行 C++ + 3 个 UMG 蓝图
|
||||||
|
|
||||||
|
### 跨场景持久化架构
|
||||||
|
|
||||||
|
> 本项目后续将支持多场景切换(如城郊 → 乡下)。UE5 的 `OpenLevel()` 会销毁整个旧 World,包括 Character 及其所有 UActorComponent。因此需要将**持久数据**与**运行时行为**分离:
|
||||||
|
|
||||||
|
- **持久数据层**:`UChildHoodGameInstance`(跨关卡存活),存储已收集物品 ID、门状态等
|
||||||
|
- **行为层**:`UInteractionManager` / `UInteractableComponent`(随 Character/场景销毁),仅负责运行时交互逻辑
|
||||||
|
- **恢复机制**:`AInteractableActor::BeginPlay()` 时查询 GameInstance,若物品已收集则隐藏并禁用交互
|
||||||
|
|
||||||
|
v0.1 为单场景原型,此架构以接口预留形式存在,不影响当前功能。详见 [第 12 节:跨场景持久化设计](#12-跨场景持久化设计)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 需求功能点罗列
|
## 1. 需求功能点罗列
|
||||||
@@ -130,6 +140,16 @@ classDiagram
|
|||||||
+GetInteractionManager() UInteractionManager*
|
+GetInteractionManager() UInteractionManager*
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UChildHoodGameInstance {
|
||||||
|
-TSet~FName~ CollectedItemIDs
|
||||||
|
-TMap~FName_bool~ DoorStates
|
||||||
|
+IsItemCollected(FName ItemId) bool
|
||||||
|
+MarkItemCollected(FName ItemId) void
|
||||||
|
+IsDoorOpen(FName DoorId) bool
|
||||||
|
+SetDoorOpen(FName DoorId, bool bIsOpen) void
|
||||||
|
+ResetAll() void
|
||||||
|
}
|
||||||
|
|
||||||
class AChildHoodPlayerController {
|
class AChildHoodPlayerController {
|
||||||
+SetupInputComponent() void
|
+SetupInputComponent() void
|
||||||
+OnInteractPressed() void
|
+OnInteractPressed() void
|
||||||
@@ -186,6 +206,7 @@ classDiagram
|
|||||||
+ToggleDoor() void
|
+ToggleDoor() void
|
||||||
+IsDoorOpen() bool
|
+IsDoorOpen() bool
|
||||||
+GetWorldLocation() FVector
|
+GetWorldLocation() FVector
|
||||||
|
+RestorePersistentState(UChildHoodGameInstance*) void
|
||||||
#OnDetectionOverlapBegin(UPrimitiveComponent*, AActor*) void
|
#OnDetectionOverlapBegin(UPrimitiveComponent*, AActor*) void
|
||||||
#OnDetectionOverlapEnd(UPrimitiveComponent*, AActor*) void
|
#OnDetectionOverlapEnd(UPrimitiveComponent*, AActor*) void
|
||||||
}
|
}
|
||||||
@@ -230,6 +251,8 @@ classDiagram
|
|||||||
AInteractableActor *-- UStaticMeshComponent : 组合
|
AInteractableActor *-- UStaticMeshComponent : 组合
|
||||||
UInteractableComponent *-- FInteractableData : 组合(内含)
|
UInteractableComponent *-- FInteractableData : 组合(内含)
|
||||||
UInteractableComponent o-- USphereComponent : 聚合(引用子对象)
|
UInteractableComponent o-- USphereComponent : 聚合(引用子对象)
|
||||||
|
UInteractableComponent ..> UChildHoodGameInstance : 恢复持久状态
|
||||||
|
UInteractionManager ..> UChildHoodGameInstance : 写入消耗/门状态
|
||||||
```
|
```
|
||||||
|
|
||||||
### 关系类型说明
|
### 关系类型说明
|
||||||
@@ -239,15 +262,17 @@ classDiagram
|
|||||||
| 组合 `*--` | 生命周期绑定 | Character 持有 InteractionManager;InteractableActor 持有 InteractableComponent |
|
| 组合 `*--` | 生命周期绑定 | Character 持有 InteractionManager;InteractableActor 持有 InteractableComponent |
|
||||||
| 聚合 `o--` | 引用但非拥有 | InteractionManager 引用 PushCameraActor;InteractableComponent 引用 DetectionSphere(子对象) |
|
| 聚合 `o--` | 引用但非拥有 | InteractionManager 引用 PushCameraActor;InteractableComponent 引用 DetectionSphere(子对象) |
|
||||||
| 关联 `-->` | 持久引用 | Character 关联 PlayerController;InteractionManager 引用 InteractableComponent 列表 |
|
| 关联 `-->` | 持久引用 | Character 关联 PlayerController;InteractionManager 引用 InteractableComponent 列表 |
|
||||||
|
| 依赖 `..>` | 临时使用 | InteractableComponent 从 GameInstance 恢复状态;InteractionManager 向 GameInstance 写入消耗/门状态 |
|
||||||
|
|
||||||
### 新增类
|
### 新增类
|
||||||
|
|
||||||
| 类名 | 类型 | 职责 | 位置 |
|
| 类名 | 类型 | 职责 | 位置 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| `UInteractionManager` | UActorComponent | 交互全流程状态机、检测选择、类型分发、摄像机/转向/UI 控制 | `Source/ChildHood/Interaction/` |
|
| `UInteractionManager` | UActorComponent | 交互全流程状态机、检测选择、类型分发、摄像机/转向/UI 控制 | `Source/ChildHood/Interaction/` |
|
||||||
| `UInteractableComponent` | UActorComponent | 单个交互物配置数据、触发区域、运行时状态 | `Source/ChildHood/Interaction/` |
|
| `UInteractableComponent` | UActorComponent | 单个交互物配置数据、触发区域、运行时状态、跨场景持久状态恢复 | `Source/ChildHood/Interaction/` |
|
||||||
| `AInteractableActor` | AActor | 通用交互物 Actor 基类,持有 Mesh + InteractableComponent | `Source/ChildHood/Interaction/` |
|
| `AInteractableActor` | AActor | 通用交互物 Actor 基类,持有 Mesh + InteractableComponent | `Source/ChildHood/Interaction/` |
|
||||||
| `APushCameraActor` | AActor | 推镜用临时摄像机,仅含 UCameraComponent | `Source/ChildHood/Interaction/` |
|
| `APushCameraActor` | AActor | 推镜用临时摄像机,仅含 UCameraComponent | `Source/ChildHood/Interaction/` |
|
||||||
|
| `UChildHoodGameInstance` | UGameInstance | 跨关卡持久数据持有者:已收集物品 ID、门状态等 | `Source/ChildHood/` |
|
||||||
|
|
||||||
### 修改类
|
### 修改类
|
||||||
|
|
||||||
@@ -599,6 +624,7 @@ void UInteractionManager::SwitchTarget(int32 Direction)
|
|||||||
- **结束回到 Free**:Exit 阶段完成后 `State = Free`
|
- **结束回到 Free**:Exit 阶段完成后 `State = Free`
|
||||||
- **检测标记恢复**:回到 Free 后检查 `NearbyInteractables.Num() > 0`,若有则立即更新提示 UI
|
- **检测标记恢复**:回到 Free 后检查 `NearbyInteractables.Num() > 0`,若有则立即更新提示 UI
|
||||||
- **oneShot 消耗**:`Consume()` 设置 `bIsConsumed = true`,注销检测区域(`DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision)`),从 NearbyInteractables 中移除
|
- **oneShot 消耗**:`Consume()` 设置 `bIsConsumed = true`,注销检测区域(`DetectionSphere->SetCollisionEnabled(ECollisionEnabled::NoCollision)`),从 NearbyInteractables 中移除
|
||||||
|
- **跨场景持久化**:`Consume()` 同时调用 `GameInstance->MarkItemCollected(ItemId)` 写入持久层,确保场景切换后仍可恢复已消耗状态
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -794,6 +820,11 @@ void UInteractionManager::EnterAnimLockPhase()
|
|||||||
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
|
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
|
||||||
{
|
{
|
||||||
CurrentInteractable->ToggleDoor(); // bDoorIsOpen = !bDoorIsOpen
|
CurrentInteractable->ToggleDoor(); // bDoorIsOpen = !bDoorIsOpen
|
||||||
|
// 持久化门状态到 GameInstance
|
||||||
|
if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance<UChildHoodGameInstance>())
|
||||||
|
{
|
||||||
|
GI->SetDoorOpen(Data.ItemId, CurrentInteractable->IsDoorOpen());
|
||||||
|
}
|
||||||
// 通知 Actor 处理门 Mesh 旋转
|
// 通知 Actor 处理门 Mesh 旋转
|
||||||
if (AInteractableActor* Owner = Cast<AInteractableActor>(CurrentInteractable->GetOwner()))
|
if (AInteractableActor* Owner = Cast<AInteractableActor>(CurrentInteractable->GetOwner()))
|
||||||
Owner->OnInteractStarted(CurrentInteractable);
|
Owner->OnInteractStarted(CurrentInteractable);
|
||||||
@@ -917,7 +948,7 @@ void UInteractionManager::EnterAnimLockPhase()
|
|||||||
1. **Tick 开关优化**:State == Free 且 NearbyInteractables 为空时,关闭 TickComponent(`SetComponentTickEnabled(false)`)。注册首个交互物时开启。
|
1. **Tick 开关优化**:State == Free 且 NearbyInteractables 为空时,关闭 TickComponent(`SetComponentTickEnabled(false)`)。注册首个交互物时开启。
|
||||||
2. **推镜摄像机生命周期**:APushCameraActor 在 InteractionManager::BeginPlay 中 Spawn,随 Character 生命周期,不反复创建/销毁。
|
2. **推镜摄像机生命周期**:APushCameraActor 在 InteractionManager::BeginPlay 中 Spawn,随 Character 生命周期,不反复创建/销毁。
|
||||||
3. **委托通知时机**:OnItemPickedUp / OnItemFound 在 ExecuteAnimEvent() 中广播,此时动画尚未结束。物品系统和收集系统需处理"动画未结束但已通知"的情况(v0.1 物品系统尚未实现,此通知仅作为架构预留)。
|
3. **委托通知时机**:OnItemPickedUp / OnItemFound 在 ExecuteAnimEvent() 中广播,此时动画尚未结束。物品系统和收集系统需处理"动画未结束但已通知"的情况(v0.1 物品系统尚未实现,此通知仅作为架构预留)。
|
||||||
4. **门状态持久化**:v0.1 门状态仅保存在内存中(bDoorIsOpen),关卡重新加载后重置。不需要持久化到存档。
|
4. **门状态持久化**:门状态通过 `UChildHoodGameInstance` 持久化。每次开关门时写入 `GI->SetDoorOpen()`,`InteractableComponent::RestorePersistentState()` 在 BeginPlay 时读取恢复。v0.1 单场景下等效于首次加载时所有门为关闭状态。
|
||||||
5. **NPC 对话简化**:v0.1 Talk 类型在架构上与 Examine 共用 Active 阶段逻辑,仅文本来源不同。后续版本加入对话树时需扩展 Active 阶段。
|
5. **NPC 对话简化**:v0.1 Talk 类型在架构上与 Examine 共用 Active 阶段逻辑,仅文本来源不同。后续版本加入对话树时需扩展 Active 阶段。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -988,10 +1019,11 @@ AInteractableActor (编辑器实例)
|
|||||||
Source/ChildHood/
|
Source/ChildHood/
|
||||||
├── Interaction/
|
├── Interaction/
|
||||||
│ ├── InteractionTypes.h # 枚举、结构体、委托定义
|
│ ├── InteractionTypes.h # 枚举、结构体、委托定义
|
||||||
│ ├── InteractionManager.h/.cpp # 交互状态机核心组件
|
│ ├── InteractionManager.h/.cpp # 交互状态机核心组件(含 GameInstance 写入调用)
|
||||||
│ ├── InteractableComponent.h/.cpp # 可交互物组件(数据+检测)
|
│ ├── InteractableComponent.h/.cpp # 可交互物组件(数据+检测+持久状态恢复)
|
||||||
│ ├── InteractableActor.h/.cpp # 通用交互物 Actor 基类
|
│ ├── InteractableActor.h/.cpp # 通用交互物 Actor 基类(BeginPlay 调用恢复)
|
||||||
│ └── PushCameraActor.h/.cpp # 推镜用临时摄像机
|
│ └── PushCameraActor.h/.cpp # 推镜用临时摄像机
|
||||||
|
├── ChildHoodGameInstance.h/.cpp # 新增:跨关卡持久数据持有者
|
||||||
├── ChildHoodCharacter.h/.cpp # 修改:添加交互支持
|
├── ChildHoodCharacter.h/.cpp # 修改:添加交互支持
|
||||||
└── ChildHoodPlayerController.h/.cpp # 修改:添加交互输入绑定
|
└── ChildHoodPlayerController.h/.cpp # 修改:添加交互输入绑定
|
||||||
|
|
||||||
@@ -1013,6 +1045,96 @@ Content/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 12. 跨场景持久化设计
|
||||||
|
|
||||||
|
> UE5 的 `OpenLevel()`(非无缝跳转)会销毁整个旧 World,包括 Character 及其所有 UActorComponent。本节定义交互系统如何通过 `UChildHoodGameInstance` 实现跨场景数据持久化。
|
||||||
|
|
||||||
|
### 架构原则
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ UChildHoodGameInstance │ ← 跨关卡存活
|
||||||
|
│ ├── CollectedItemIDs: TSet<FName> │ 交互物消耗状态
|
||||||
|
│ └── DoorStates: TMap<FName, bool> │ 门开关状态
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
↕ 读取/写入
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ 场景中的 AInteractableActor │ ← 随场景加载/销毁
|
||||||
|
│ └── BeginPlay → RestorePersistentState │ 查询并恢复状态
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### UChildHoodGameInstance 接口
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
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:
|
||||||
|
TSet<FName> CollectedItemIDs;
|
||||||
|
TMap<FName, bool> DoorStates;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 恢复流程
|
||||||
|
|
||||||
|
`AInteractableActor::BeginPlay()` 或 `UInteractableComponent::BeginPlay()` 中:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void UInteractableComponent::RestorePersistentState(UChildHoodGameInstance* GI)
|
||||||
|
{
|
||||||
|
if (!GI) return;
|
||||||
|
|
||||||
|
// oneShot 物品:已消耗 → 隐藏 + 禁用检测
|
||||||
|
if (Data.bOneShot && GI->IsItemCollected(Data.ItemId))
|
||||||
|
{
|
||||||
|
bIsConsumed = true;
|
||||||
|
if (USphereComponent* Sphere = DetectionSphere)
|
||||||
|
Sphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
if (AInteractableActor* Owner = Cast<AInteractableActor>(GetOwner()))
|
||||||
|
Owner->SetActorHiddenInGame(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 门:恢复开关状态
|
||||||
|
if (Data.Type == EInteractType::Operate && Data.OperateSubType == EOperateSubType::Door)
|
||||||
|
{
|
||||||
|
bDoorIsOpen = GI->IsDoorOpen(Data.ItemId);
|
||||||
|
// 通知 Actor 应用视觉状态
|
||||||
|
if (AInteractableActor* Owner = Cast<AInteractableActor>(GetOwner()))
|
||||||
|
Owner->OnInteractStarted(this); // 复用已有的 Mesh 旋转逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 写入时机
|
||||||
|
|
||||||
|
| 事件 | 调用位置 | 写入方法 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| oneShot 物品被消耗 | `UInteractableComponent::Consume()` | `GI->MarkItemCollected(ItemId)` |
|
||||||
|
| 门开关状态变化 | `InteractionManager::ExecuteAnimEvent()` Door 分支 | `GI->SetDoorOpen(ItemId, bIsOpen)` |
|
||||||
|
|
||||||
|
### v0.1 行为
|
||||||
|
|
||||||
|
- v0.1 为单场景原型,不存在跨场景切换
|
||||||
|
- `UChildHoodGameInstance` 在 v0.1 中以空数据初始化,所有 `IsItemCollected()` 返回 false
|
||||||
|
- 后续多场景版本只需在 Project Settings 中将 GameInstance Class 设为 `UChildHoodGameInstance`,无需修改交互系统代码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 工期预估与实现拆分
|
## 工期预估与实现拆分
|
||||||
|
|
||||||
### 工作量估算
|
### 工作量估算
|
||||||
|
|||||||
+162
-17
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
> 任务ID: P-009
|
> 任务ID: P-009
|
||||||
> 对应执行案: `docs/design/收集系统-执行案.md` (v0.1.1)
|
> 对应执行案: `docs/design/收集系统-执行案.md` (v0.1.1)
|
||||||
> 关联系统: 物品系统(上游单向接收)、交互系统(间接,通过物品系统委托)
|
> 关联系统: 物品系统(上游单向接收)、交互系统(间接,通过物品系统委托)、GameInstance 持久化层
|
||||||
> 日期: 2026-06-07
|
> 日期: 2026-06-07(2026-06-11 更新:加入多场景持久化架构)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -15,6 +15,16 @@
|
|||||||
**预估工期**:2-3 天
|
**预估工期**:2-3 天
|
||||||
**新增/修改代码量**:约 250-350 行 C++ + 3 个 UMG 蓝图
|
**新增/修改代码量**:约 250-350 行 C++ + 3 个 UMG 蓝图
|
||||||
|
|
||||||
|
### 跨场景持久化架构
|
||||||
|
|
||||||
|
> 收集进度是玩家核心持久数据。当前设计中 `UCollectionManager` 将「行为控制」(UI、通知、动画)和「持久数据」(CollectedIDs、Progress)混在同一组件,随 Character 销毁而丢失。需要通过数据层分离解决:
|
||||||
|
|
||||||
|
- **持久数据层**:收集状态(CollectedIDs、Progress、bIsComplete)存储在 `UChildHoodGameInstance` 中(跨关卡存活)
|
||||||
|
- **行为层**:`UCollectionManager` 仅负责运行时 UI 控制和委托监听,BeginPlay 时从 GameInstance 恢复状态
|
||||||
|
- **交互物恢复**:场景中的已收集物品由交互系统通过 `InteractableComponent::RestorePersistentState()` 处理(见交互系统技术方案第 12 节)
|
||||||
|
|
||||||
|
v0.1 为单场景原型,此架构以接口预留形式存在,不影响当前功能。详见 [第 12 节:跨场景持久化设计](#12-跨场景持久化设计)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 需求功能点罗列
|
## 1. 需求功能点罗列
|
||||||
@@ -123,6 +133,17 @@ classDiagram
|
|||||||
-CheckCompletion() void
|
-CheckCompletion() void
|
||||||
-ShowCompletionPrompt() void
|
-ShowCompletionPrompt() void
|
||||||
-UpdateProgressUI() void
|
-UpdateProgressUI() void
|
||||||
|
-LoadStateFromGameInstance() void
|
||||||
|
-SaveStateToGameInstance() void
|
||||||
|
}
|
||||||
|
|
||||||
|
class UChildHoodGameInstance {
|
||||||
|
-TSet~FName~ CollectedItemIDs
|
||||||
|
-int32 CollectionProgress
|
||||||
|
-bool bCollectionComplete
|
||||||
|
+GetCollectionState(TSet_FName_&, int32&, bool&) void
|
||||||
|
+SetCollectionState(TSet_FName_&, int32, bool) void
|
||||||
|
+ResetAll() void
|
||||||
}
|
}
|
||||||
|
|
||||||
class UCollectionNotifWidget {
|
class UCollectionNotifWidget {
|
||||||
@@ -175,6 +196,7 @@ classDiagram
|
|||||||
|
|
||||||
AChildHoodCharacter *-- UCollectionManager : 组合
|
AChildHoodCharacter *-- UCollectionManager : 组合
|
||||||
UCollectionManager --> UInteractionManager : 关联(绑定委托)
|
UCollectionManager --> UInteractionManager : 关联(绑定委托)
|
||||||
|
UCollectionManager ..> UChildHoodGameInstance : 读取/写入持久状态
|
||||||
UCollectionManager o-- UCollectionNotifWidget : 聚合(管理生命周期)
|
UCollectionManager o-- UCollectionNotifWidget : 聚合(管理生命周期)
|
||||||
UCollectionManager o-- UCollectionProgressWidget : 聚合(管理生命周期)
|
UCollectionManager o-- UCollectionProgressWidget : 聚合(管理生命周期)
|
||||||
UCollectionManager ..> UCollectionCompleteWidget : 按需创建
|
UCollectionManager ..> UCollectionCompleteWidget : 按需创建
|
||||||
@@ -188,7 +210,7 @@ classDiagram
|
|||||||
| 组合 `*--` | 生命周期绑定 | Character 持有 CollectionManager;CollectionManager 内含 Config |
|
| 组合 `*--` | 生命周期绑定 | Character 持有 CollectionManager;CollectionManager 内含 Config |
|
||||||
| 聚合 `o--` | 引用但非拥有 | CollectionManager 管理通知/进度 Widget 的创建和销毁 |
|
| 聚合 `o--` | 引用但非拥有 | CollectionManager 管理通知/进度 Widget 的创建和销毁 |
|
||||||
| 关联 `-->` | 持久引用 | CollectionManager 绑定 InteractionManager 的委托 |
|
| 关联 `-->` | 持久引用 | CollectionManager 绑定 InteractionManager 的委托 |
|
||||||
| 依赖 `..>` | 临时使用 | CollectionManager 按需创建完成提示 Widget |
|
| 依赖 `..>` | 临时使用 | CollectionManager 从 GameInstance 读取/写入持久状态;按需创建完成提示 Widget |
|
||||||
|
|
||||||
### 新增类
|
### 新增类
|
||||||
|
|
||||||
@@ -489,6 +511,8 @@ void UCollectionManager::ProcessNewCollection(FName ItemId)
|
|||||||
// 步骤 3: 记录
|
// 步骤 3: 记录
|
||||||
CollectedIDs.Add(ItemId);
|
CollectedIDs.Add(ItemId);
|
||||||
Progress = CollectedIDs.Num(); // 用 Set 大小作为权威进度值,避免状态不一致
|
Progress = CollectedIDs.Num(); // 用 Set 大小作为权威进度值,避免状态不一致
|
||||||
|
// 持久化到 GameInstance
|
||||||
|
SaveStateToGameInstance();
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -519,6 +543,7 @@ void UCollectionManager::ProcessNewCollection(FName ItemId)
|
|||||||
if (Progress >= Config.TotalCount && !bIsComplete)
|
if (Progress >= Config.TotalCount && !bIsComplete)
|
||||||
{
|
{
|
||||||
bIsComplete = true;
|
bIsComplete = true;
|
||||||
|
SaveStateToGameInstance(); // 持久化完成状态
|
||||||
CheckCompletion(); // 触发完成表现
|
CheckCompletion(); // 触发完成表现
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -637,10 +662,8 @@ void UCollectionManager::BeginPlay()
|
|||||||
{
|
{
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
|
|
||||||
// 初始化状态
|
// 从 GameInstance 恢复持久状态(v0.1 单场景等效于空数据初始化)
|
||||||
Progress = 0;
|
LoadStateFromGameInstance();
|
||||||
CollectedIDs.Empty();
|
|
||||||
bIsComplete = false;
|
|
||||||
|
|
||||||
// 创建进度 UI
|
// 创建进度 UI
|
||||||
if (ProgressWidgetClass)
|
if (ProgressWidgetClass)
|
||||||
@@ -652,7 +675,12 @@ void UCollectionManager::BeginPlay()
|
|||||||
if (ProgressWidget)
|
if (ProgressWidget)
|
||||||
{
|
{
|
||||||
ProgressWidget->AddToViewport(5);
|
ProgressWidget->AddToViewport(5);
|
||||||
ProgressWidget->UpdateProgress(0, Config.TotalCount);
|
ProgressWidget->UpdateProgress(Progress, Config.TotalCount);
|
||||||
|
// 已完成时恢复完成样式
|
||||||
|
if (bIsComplete)
|
||||||
|
{
|
||||||
|
ProgressWidget->SetCompleteStyle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -772,20 +800,34 @@ void UCollectionManager::ShowCompletionPrompt()
|
|||||||
|
|
||||||
#### 重启状态处理
|
#### 重启状态处理
|
||||||
|
|
||||||
**实现**:v0.1 无持久化。CollectionManager::BeginPlay() 中完全重置:
|
**实现**:收集状态通过 `UChildHoodGameInstance` 跨场景持久化。CollectionManager::BeginPlay() 中从 GameInstance 恢复:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void UCollectionManager::BeginPlay()
|
void UCollectionManager::LoadStateFromGameInstance()
|
||||||
{
|
{
|
||||||
Super::BeginPlay();
|
if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance<UChildHoodGameInstance>())
|
||||||
Progress = 0;
|
{
|
||||||
CollectedIDs.Empty();
|
GI->GetCollectionState(CollectedIDs, Progress, bIsComplete);
|
||||||
bIsComplete = false;
|
}
|
||||||
// ... 创建 UI、绑定委托
|
else
|
||||||
|
{
|
||||||
|
// 回退:无 GameInstance 时初始化为空(编辑器预览等场景)
|
||||||
|
Progress = 0;
|
||||||
|
CollectedIDs.Empty();
|
||||||
|
bIsComplete = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UCollectionManager::SaveStateToGameInstance()
|
||||||
|
{
|
||||||
|
if (UChildHoodGameInstance* GI = GetWorld()->GetGameInstance<UChildHoodGameInstance>())
|
||||||
|
{
|
||||||
|
GI->SetCollectionState(CollectedIDs, Progress, bIsComplete);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
重启后进度 UI 恢复显示 `📦 杂物 0/10`。
|
v0.1 单场景下:GameInstance 以空数据初始化,等效于 `📦 杂物 0/10`。场景内正常收集后状态写入 GameInstance,若通过 Seamless Travel 切换关卡则自动恢复。非无缝跳转(OpenLevel)时 GameInstance 仍存活,新场景的 CollectionManager 可读取之前的数据。
|
||||||
|
|
||||||
#### 收集通知与拾取通知时序
|
#### 收集通知与拾取通知时序
|
||||||
|
|
||||||
@@ -819,6 +861,7 @@ void UCollectionManager::BeginPlay()
|
|||||||
### 注意事项
|
### 注意事项
|
||||||
|
|
||||||
1. **委托绑定时机**:CollectionManager 在 BeginPlay() 中通过 `GetOwner()->GetComponentByClass<UInteractionManager>()` 获取引用并绑定。需确保 InteractionManager 已初始化。若为 nullptr 则在 Tick 中延迟绑定(最多重试 N 帧后报错)。
|
1. **委托绑定时机**:CollectionManager 在 BeginPlay() 中通过 `GetOwner()->GetComponentByClass<UInteractionManager>()` 获取引用并绑定。需确保 InteractionManager 已初始化。若为 nullptr 则在 Tick 中延迟绑定(最多重试 N 帧后报错)。
|
||||||
|
2. **持久化读写时机**:`SaveStateToGameInstance()` 在每次 ProcessNewCollection 记录后和完成判定后调用,保证 GameInstance 中的数据始终最新。`LoadStateFromGameInstance()` 仅在 BeginPlay 中调用一次。
|
||||||
2. **v0.1 无拾取通知 UI**:执行文档中拾取通知由物品系统负责,但 v0.1 物品系统仅为委托层(InteractionManager 上的委托)。收集通知的 0.3s 延迟仍按设计实现(为拾取通知预留时间窗口),即使拾取通知 UI 暂未实现。
|
2. **v0.1 无拾取通知 UI**:执行文档中拾取通知由物品系统负责,但 v0.1 物品系统仅为委托层(InteractionManager 上的委托)。收集通知的 0.3s 延迟仍按设计实现(为拾取通知预留时间窗口),即使拾取通知 UI 暂未实现。
|
||||||
3. **通知移除回调**:UCollectionNotifWidget 淡出完成后需通知 CollectionManager 从 ActiveNotifications 中移除自身引用。使用委托或弱引用回调实现,避免循环引用。
|
3. **通知移除回调**:UCollectionNotifWidget 淡出完成后需通知 CollectionManager 从 ActiveNotifications 中移除自身引用。使用委托或弱引用回调实现,避免循环引用。
|
||||||
4. **FName 作为 ID**:收集系统使用 FName(ItemId)作为去重键,与 InteractionManager 委托广播的 ItemId 类型一致。
|
4. **FName 作为 ID**:收集系统使用 FName(ItemId)作为去重键,与 InteractionManager 委托广播的 ItemId 类型一致。
|
||||||
@@ -847,14 +890,33 @@ InteractionManager (ExecuteAnimEvent)
|
|||||||
├─ 查品类 (Category == TrackedCategory?) → 不匹配则忽略
|
├─ 查品类 (Category == TrackedCategory?) → 不匹配则忽略
|
||||||
├─ 查重复 (CollectedIDs.Contains?) → 已存在则忽略
|
├─ 查重复 (CollectedIDs.Contains?) → 已存在则忽略
|
||||||
├─ 记录 (CollectedIDs.Add + Progress = Num())
|
├─ 记录 (CollectedIDs.Add + Progress = Num())
|
||||||
|
├─ SaveStateToGameInstance() → UChildHoodGameInstance (持久化)
|
||||||
├─ UpdateProgressUI() → WBP_CollectionProgress (文本 + 弹性动画)
|
├─ UpdateProgressUI() → WBP_CollectionProgress (文本 + 弹性动画)
|
||||||
├─ SetTimer(NotifDelay) → ShowCollectionNotification() → WBP_CollectionNotification
|
├─ SetTimer(NotifDelay) → ShowCollectionNotification() → WBP_CollectionNotification
|
||||||
└─ 完成判定 (Progress >= TotalCount)
|
└─ 完成判定 (Progress >= TotalCount)
|
||||||
→ bIsComplete = true
|
→ bIsComplete = true
|
||||||
|
→ SaveStateToGameInstance() (持久化完成状态)
|
||||||
→ SetCompleteStyle() → WBP_CollectionProgress (金色)
|
→ SetCompleteStyle() → WBP_CollectionProgress (金色)
|
||||||
→ SetTimer(TotalDelay) → ShowCompletionPrompt() → WBP_CollectionComplete
|
→ SetTimer(TotalDelay) → ShowCompletionPrompt() → WBP_CollectionComplete
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 跨场景恢复流
|
||||||
|
|
||||||
|
```
|
||||||
|
场景切换 (OpenLevel / Seamless Travel)
|
||||||
|
→ 新场景加载
|
||||||
|
→ AChildHoodCharacter::BeginPlay()
|
||||||
|
→ UCollectionManager::BeginPlay()
|
||||||
|
→ LoadStateFromGameInstance()
|
||||||
|
→ UChildHoodGameInstance::GetCollectionState(OutIDs, OutProgress, OutComplete)
|
||||||
|
→ 恢复 CollectedIDs / Progress / bIsComplete
|
||||||
|
→ 创建进度 UI(显示已恢复的进度值)
|
||||||
|
→ 若 bIsComplete → 恢复完成样式
|
||||||
|
→ 场景中 AInteractableActor::BeginPlay()
|
||||||
|
→ UInteractableComponent::RestorePersistentState(GI)
|
||||||
|
→ GI->IsItemCollected(ItemId) → 已收集则隐藏 + 禁用
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 可调参数汇总
|
## 10. 可调参数汇总
|
||||||
@@ -886,10 +948,11 @@ InteractionManager (ExecuteAnimEvent)
|
|||||||
Source/ChildHood/
|
Source/ChildHood/
|
||||||
├── Collection/
|
├── Collection/
|
||||||
│ ├── CollectionTypes.h # FCollectionConfig 结构体定义
|
│ ├── CollectionTypes.h # FCollectionConfig 结构体定义
|
||||||
│ ├── CollectionManager.h/.cpp # 收集系统核心组件
|
│ ├── CollectionManager.h/.cpp # 收集系统核心组件(含 GameInstance 读写)
|
||||||
│ ├── CollectionNotifWidget.h/.cpp # 收集通知 UI 基类
|
│ ├── CollectionNotifWidget.h/.cpp # 收集通知 UI 基类
|
||||||
│ ├── CollectionProgressWidget.h/.cpp # 收集进度 UI 基类
|
│ ├── CollectionProgressWidget.h/.cpp # 收集进度 UI 基类
|
||||||
│ └── CollectionCompleteWidget.h/.cpp # 完成提示 UI 基类
|
│ └── CollectionCompleteWidget.h/.cpp # 完成提示 UI 基类
|
||||||
|
├── ChildHoodGameInstance.h/.cpp # 新增:跨关卡持久数据持有者(收集状态在此持久化)
|
||||||
├── ChildHoodCharacter.h/.cpp # 修改:添加 CollectionManager 成员和访问器
|
├── ChildHoodCharacter.h/.cpp # 修改:添加 CollectionManager 成员和访问器
|
||||||
|
|
||||||
Content/
|
Content/
|
||||||
@@ -928,3 +991,85 @@ Content/
|
|||||||
|
|
||||||
> **关键路径**:6b-1 → 6b-2 → 6b-4 → 6b-5 → 6b-6。6b-3(进度 UI)可在 6b-1 完成后并行。总关键路径约 3 天。
|
> **关键路径**:6b-1 → 6b-2 → 6b-4 → 6b-5 → 6b-6。6b-3(进度 UI)可在 6b-1 完成后并行。总关键路径约 3 天。
|
||||||
> **前置依赖**:交互系统的 InteractionManager 组件需已完成(特别是 OnItemPickedUp/OnItemFound 委托),方可进行 6b-2 的委托绑定测试。
|
> **前置依赖**:交互系统的 InteractionManager 组件需已完成(特别是 OnItemPickedUp/OnItemFound 委托),方可进行 6b-2 的委托绑定测试。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. 跨场景持久化设计
|
||||||
|
|
||||||
|
> 收集进度是玩家的核心持久数据,必须在场景切换时保留。本节定义收集系统如何通过 `UChildHoodGameInstance` 实现跨场景数据持久化。
|
||||||
|
|
||||||
|
### 架构原则
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ UChildHoodGameInstance │ ← 跨关卡存活
|
||||||
|
│ ├── CollectedItemIDs: TSet<FName> │ 所有场景共享
|
||||||
|
│ ├── CollectionProgress: int32 │
|
||||||
|
│ └── bCollectionComplete: bool │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
↕ BeginPlay 加载 / 收集时回写
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ UCollectionManager (每个场景重建) │ ← 行为层
|
||||||
|
│ ├── 从 GameInstance 读取恢复进度 │ 不持有持久数据所有权
|
||||||
|
│ ├── 收集时写入 GameInstance │
|
||||||
|
│ └── 控制 UI / 通知 / 动画 │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
↕ 场景恢复
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ 场景中的 AInteractableActor │ ← 由交互系统负责恢复
|
||||||
|
│ └── RestorePersistentState() │ (见交互系统技术方案第 12 节)
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### UChildHoodGameInstance 收集相关接口
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
UCLASS()
|
||||||
|
class UChildHoodGameInstance : public UGameInstance
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
// === 收集状态 ===
|
||||||
|
void GetCollectionState(TSet<FName>& OutIDs, int32& OutProgress, bool& OutComplete) const;
|
||||||
|
void SetCollectionState(const TSet<FName>& IDs, int32 Progress, bool bComplete);
|
||||||
|
|
||||||
|
// === 物品消耗状态(交互系统也使用)===
|
||||||
|
bool IsItemCollected(FName ItemId) const;
|
||||||
|
void MarkItemCollected(FName ItemId);
|
||||||
|
|
||||||
|
// === 门状态(交互系统也使用)===
|
||||||
|
bool IsDoorOpen(FName DoorId) const;
|
||||||
|
void SetDoorOpen(FName DoorId, bool bIsOpen);
|
||||||
|
|
||||||
|
// === 重置 ===
|
||||||
|
void ResetAll();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// 收集系统
|
||||||
|
TSet<FName> CollectedItemIDs;
|
||||||
|
int32 CollectionProgress = 0;
|
||||||
|
bool bCollectionComplete = false;
|
||||||
|
// 交互系统
|
||||||
|
TMap<FName, bool> DoorStates;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:`CollectedItemIDs` 同时被 `IsItemCollected()`(交互物恢复)和 `GetCollectionState()`(收集进度恢复)使用,确保两系统数据一致性。
|
||||||
|
|
||||||
|
### 读写时机
|
||||||
|
|
||||||
|
| 时机 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| CollectionManager::BeginPlay | `LoadStateFromGameInstance()` | 场景加载时恢复进度 |
|
||||||
|
| ProcessNewCollection 记录后 | `SaveStateToGameInstance()` | 每次新收集时同步写入 |
|
||||||
|
| 完成判定后 | `SaveStateToGameInstance()` | 完成状态立即持久化 |
|
||||||
|
| InteractableComponent::Consume | `GI->MarkItemCollected()` | 交互物消耗状态写入(由交互系统调用) |
|
||||||
|
|
||||||
|
### v0.1 行为
|
||||||
|
|
||||||
|
- v0.1 为单场景原型,不存在跨场景切换
|
||||||
|
- `UChildHoodGameInstance` 在 v0.1 中以空数据初始化,所有查询返回默认值
|
||||||
|
- `LoadStateFromGameInstance()` 等效于初始化为 0
|
||||||
|
- 后续多场景版本只需在 Project Settings 中将 GameInstance Class 设为 `UChildHoodGameInstance`,收集系统代码无需修改
|
||||||
|
- 后续版本可通过 USaveGame 将 GameInstance 数据序列化到磁盘,实现跨会话持久化
|
||||||
|
|||||||
Reference in New Issue
Block a user