UE5 C++实战:使用Timeline组件实现高效自动门系统
2026/7/26 6:46:14 网站建设 项目流程

1. 项目概述:为什么选择Timeline做自动门?

在UE5里实现一个自动门,听起来是个入门级的练习,但很多刚接触C++的开发者,包括我自己刚开始的时候,都会下意识地直接去写Tick函数,用线性插值(Lerp)去更新门的位置或旋转。这么做当然能跑起来,但代码很快就会变得难以维护——门的开关速度、缓动效果、延迟触发逻辑全都糅在Tick里,改起来心惊胆战。

这就是Timeline组件出场的时候了。你可以把它理解为一个基于时间轴的曲线播放器。我们把门的移动轨迹(比如从关到开90度)定义成一条曲线,横轴是时间(0到1秒),纵轴是门的旋转角度(0到90度)。Timeline负责按时间播放这条曲线,我们只需要在它播放的每个“帧”里,拿到当前曲线输出的值,赋给门就行。所有关于“如何运动”的复杂逻辑——加速、减速、匀速——都交给了这条可视化的曲线来定义,C++代码只负责“播放”和“应用结果”,职责清晰,后期调整动画效果只需在编辑器中拉几下曲线,无需重新编译代码。

这次,我们就彻底抛弃蓝图,用纯C++在UE5中,结合Timeline组件,打造一个可复用的、带感应区域的自动门Actor。你会学到如何声明和绑定Timeline事件,如何将曲线资产与C++类协作,以及如何构建一个清晰的交互框架。文末会提供完整的、可直接粘贴使用的项目代码。

2. 核心设计思路与类结构规划

在动手写代码前,我们先花几分钟把设计思路理清楚。一个健壮的自动门系统不应该只是一个会动的模型,它应该包含清晰的逻辑模块。

2.1 功能分解与组件选型

我们的自动门需要实现以下核心功能:

  1. 感知:检测玩家或物体是否进入触发区域。
  2. 决策:根据感知结果,决定门应该打开还是关闭。
  3. 执行:驱动门模型执行平滑的打开或关闭动画。

对应到UE的组件系统,我的方案是这样的:

  • 感知:使用一个BoxComponentSphereComponent作为触发器(Trigger Volume)。这是最轻量、高效的检测方式,比射线检测更适用于区域持续感应。
  • 决策:逻辑放在自动门的主类(如AAutoDoor)中。它需要监听触发器的OnComponentBeginOverlapOnComponentEndOverlap事件。
  • 执行:核心就是UTimelineComponent。门的运动状态(旋转角度或位置)将由一条UCurveFloat曲线驱动。我们还需要一个UStaticMeshComponentUSceneComponent来代表门的可视部分。

2.2 类与组件关系图

在脑海中或纸上画出这样一个结构,会让编码过程顺畅很多:

AAutoDoor (继承自AActor) ├── USceneComponent* Root (根组件,可选但推荐) ├── UStaticMeshComponent* DoorMesh (门的静态网格体) ├── UBoxComponent* TriggerVolume (触发盒子) └── UTimelineComponent* DoorTimeline (时间轴组件) └── 绑定到 UCurveFloat* DoorCurve (驱动曲线)

DoorTimeline是这个系统的“发动机”,DoorCurve是“油门踏板曲线”,TriggerVolume是“点火开关”,而DoorMesh是最终被驱动的“车轮”。

2.3 为什么用C++而不是蓝图?

你可能看过很多用蓝图连线的Timeline教程,确实快。但用C++实现有不可替代的优势:

  • 性能与复用性:C++类编译后运行效率更高,并且可以方便地创建子类或作为组件添加到其他蓝图中,实现一次编写,多处复用。
  • 维护性:所有关键属性(如曲线资产、开关速度)都可以暴露为UPROPERTY,在编辑器细节面板中调整,兼具了蓝图的易调整性和C++的清晰结构。
  • 深入学习:理解C++中如何动态绑定委托(Delegate),如何与资源管理器交互,是进阶UE5开发的必经之路。

3. 实战步骤一:创建C++类与基础组件

接下来,我们进入实战环节。请确保你已创建一个UE5的C++项目(如“ThirdPerson”或空白项目均可)。

3.1 创建自动门Actor类

  1. 在内容浏览器的“C++类”文件夹上右键,选择“新建C++类”。
  2. 选择父类为“Actor”,命名为AutoDoor(引擎会自动添加前缀A,生成AAutoDoor类)。
  3. 点击创建,UE5会生成头文件(.h)和源文件(.cpp),并打开你的开发环境(如Visual Studio 2022或Rider)。

3.2 声明组件与属性(头文件)

打开AutoDoor.h文件,我们先来定义这个类需要哪些“零件”和“属性”。

// AutoDoor.h #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Components/TimelineComponent.h" // 必须包含Timeline头文件 #include "AutoDoor.generated.h" // 必须放在最后 // 声明一个委托类型,用于Timeline更新时调用。这个委托接受一个float参数。 DECLARE_DELEGATE_OneParam(FOnTimelineFloat, float); UCLASS() class YOURPROJECT_API AAutoDoor : public AActor { GENERATED_BODY() public: AAutoDoor(); protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override; // 组件声明 protected: // 门的网格体组件 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Door Components") UStaticMeshComponent* DoorMesh; // 触发区域组件 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Door Components") class UBoxComponent* TriggerVolume; // 时间轴组件 UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Door Components") UTimelineComponent* DoorTimeline; // 资产与属性声明 protected: // 驱动时间轴的浮点曲线资产。在编辑器中指定。 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door Properties") UCurveFloat* DoorOpenCurve; // 记录时间轴是否在播放“开门”动画。用于防止重复触发。 UPROPERTY(BlueprintReadOnly, Category = "Door State") bool bIsOpening; // 记录门是否处于开启状态。 UPROPERTY(BlueprintReadOnly, Category = "Door State") bool bIsOpen; // 函数声明 protected: // 在BeginPlay中绑定的时间轴更新函数 UFUNCTION() void OnTimelineUpdate(float OutputValue); // 在BeginPlay中绑定的时间轴完成函数 UFUNCTION() void OnTimelineFinished(); // 触发器重叠开始事件 UFUNCTION() void OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); // 触发器重叠结束事件 UFUNCTION() void OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex); // 控制门开关的接口函数 void OpenDoor(); void CloseDoor(); };

关键点解析:

  • #include "Components/TimelineComponent.h":使用UTimelineComponent必须包含此头文件。
  • DECLARE_DELEGATE_OneParam:这是我们为Timeline的“更新”事件自定义的委托签名。Timeline在播放时,每一帧都会调用一个函数,并传入当前曲线值(float)。我们通过这个委托将Timeline和我们的更新函数OnTimelineUpdate连接起来。
  • UPROPERTY宏:这是UE反射系统的核心。
    • VisibleAnywhere:在编辑器的细节面板中可见,但不可编辑。
    • EditAnywhere:在细节面板中可见且可编辑。
    • BlueprintReadOnly/Write:决定蓝图是否能读取或修改这个变量。
    • Category:在细节面板中属性所属的分类,保持整洁。
  • UFUNCTION():同样用于反射,使得函数可以被蓝图调用或绑定为事件。
  • bIsOpeningbIsOpen:这两个状态变量至关重要。bIsOpening表示时间轴正在向“开”的方向播放(可能正在开也可能正在关),用于防止打断。bIsOpen表示门的最终状态,用于决定下一次触发是播放开门曲线还是关门曲线。

4. 实战步骤二:实现组件构建与事件绑定

现在打开AutoDoor.cpp文件,开始实现构造函数和BeginPlay函数。

4.1 构造函数中创建组件

// AutoDoor.cpp #include "AutoDoor.h" #include "Components/BoxComponent.h" #include "Components/StaticMeshComponent.h" AAutoDoor::AAutoDoor() { PrimaryActorTick.bCanEverTick = true; // 我们需要Tick来驱动Timeline // 创建根组件(可选,但建议有) USceneComponent* RootComp = CreateDefaultSubobject<USceneComponent>(TEXT("Root")); RootComponent = RootComp; // 创建门网格体组件,并附着到根组件 DoorMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("DoorMesh")); DoorMesh->SetupAttachment(RootComponent); // 可以在构造函数里设置一个默认网格体(如果项目里有) // static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Path/To/Your/Mesh")); // if (MeshAsset.Succeeded()) { DoorMesh->SetStaticMesh(MeshAsset.Object); } // 创建触发器盒子组件,并附着到根组件 TriggerVolume = CreateDefaultSubobject<UBoxComponent>(TEXT("TriggerVolume")); TriggerVolume->SetupAttachment(RootComponent); TriggerVolume->SetBoxExtent(FVector(100.0f, 100.0f, 100.0f)); // 设置默认大小 TriggerVolume->SetCollisionProfileName(TEXT("Trigger")); // 设置碰撞预设为Trigger,只检测重叠,不产生物理阻挡 // 创建时间轴组件 DoorTimeline = CreateDefaultSubobject<UTimelineComponent>(TEXT("DoorTimeline")); // 初始化状态变量 bIsOpening = false; bIsOpen = false; }

注意CreateDefaultSubobject只能在构造函数中调用。它创建的是组件模板,在编辑器放置或生成这个Actor时,实际使用的是这些模板的实例。

4.2 BeginPlay中绑定事件与设置Timeline

BeginPlay是游戏开始时执行的函数,我们在这里进行动态的事件绑定和Timeline设置。

void AAutoDoor::BeginPlay() { Super::BeginPlay(); // 检查关键资产是否已赋值 if (!DoorOpenCurve) { UE_LOG(LogTemp, Error, TEXT("DoorOpenCurve is not assigned on %s!"), *GetName()); return; } // 绑定触发器重叠事件 if (TriggerVolume) { TriggerVolume->OnComponentBeginOverlap.AddDynamic(this, &AAutoDoor::OnTriggerBeginOverlap); TriggerVolume->OnComponentEndOverlap.AddDynamic(this, &AAutoDoor::OnTriggerEndOverlap); } else { UE_LOG(LogTemp, Error, TEXT("TriggerVolume is missing on %s!"), *GetName()); } // 设置时间轴 if (DoorTimeline && DoorOpenCurve) { // 创建并绑定浮点曲线轨道 FOnTimelineFloat TimelineProgress; TimelineProgress.BindUFunction(this, FName("OnTimelineUpdate")); DoorTimeline->AddInterpFloat(DoorOpenCurve, TimelineProgress); // 绑定时间轴完成事件 FOnTimelineEvent TimelineFinished; TimelineFinished.BindUFunction(this, FName("OnTimelineFinished")); DoorTimeline->SetTimelineFinishedFunc(TimelineFinished); // 设置时间轴循环模式为无循环(播放一次) DoorTimeline->SetLooping(false); // 设置时间轴播放方向为正向 DoorTimeline->SetPlayRate(1.0f); // 播放速率,1.0为正常速度 } }

关键点解析:

  • AddDynamic:用于将C++函数绑定到动态多播委托(如重叠事件)。这是UE中连接事件与函数的标准方式。
  • BindUFunction:用于将UObject的成员函数绑定到委托。这里我们将OnTimelineUpdate函数绑定到Timeline的更新委托,将OnTimelineFinished绑定到完成委托。
  • AddInterpFloat:这是Timeline组件的核心函数。它告诉Timeline:“当你播放时,用这条DoorOpenCurve曲线来生成浮点值,并且每生成一个新值,就调用TimelineProgress这个委托(也就是我们的OnTimelineUpdate函数)”。

4.3 实现Tick函数驱动Timeline

虽然Timeline组件内部有自己的计时器,但它需要在Actor的Tick函数中被“推动”才能工作。

void AAutoDoor::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (DoorTimeline) { DoorTimeline->TickComponent(DeltaTime, ELevelTick::LEVELTICK_TimeOnly, nullptr); } }

很简单,对吧?这就是将Timeline的更新逻辑整合到游戏主循环中的方式。

5. 实战步骤三:核心逻辑实现

现在来实现最核心的几个函数:Timeline更新、完成回调以及触发器的响应函数。

5.1 Timeline更新函数:驱动门运动

这个函数是门动画的“心脏”。Timeline每帧(根据曲线精度)都会调用它,并传入当前曲线计算出的值。

void AAutoDoor::OnTimelineUpdate(float OutputValue) { // OutputValue 就是 DoorOpenCurve 在当前时间点输出的值 // 假设曲线范围是0(关)到1(开),或者0到90(角度) // 这里我们假设曲线输出0到90,对应门的旋转角度 if (DoorMesh) { // 获取门的初始旋转(在编辑器里设置好的关闭状态) FRotator DoorRotation = DoorMesh->GetRelativeRotation(); // 我们只改变Yaw(偏航角)来模拟门的开关 // 假设门沿Z轴朝前,开门是绕Z轴旋转 // 根据你的模型和开门方向调整,这里假设绕Z轴旋转(Roll) DoorRotation.Roll = OutputValue; // 使用曲线输出值直接设置旋转角度 DoorMesh->SetRelativeRotation(DoorRotation); } }

实操心得:这里的OutputValue如何映射到门的变换,完全取决于你的曲线设计和预期运动。如果你的门是平移滑动,就应该修改SetRelativeLocation。曲线可以定义任何运动轨迹,比如先快后慢(Ease Out)的开门效果。在编辑器中编辑曲线非常直观。

5.2 Timeline完成函数:更新状态

当一次开门或关门动画播放完毕时,我们需要更新门的状态。

void AAutoDoor::OnTimelineFinished() { // 动画播放完毕,根据播放方向更新最终状态 if (bIsOpening) { // 如果刚才是在播放开门动画,那么现在门是开的 bIsOpen = true; } else { // 如果刚才是在播放关门动画,那么现在门是关的 bIsOpen = false; } // 无论开门关门,动画播放完毕,bIsOpening都应重置为false bIsOpening = false; }

5.3 触发器事件与开关门控制

最后,我们需要响应玩家的进入和离开。

void AAutoDoor::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { // 简单的过滤:只对玩家角色做出反应。你可以扩展这里,比如检查特定的Tag。 APawn* PlayerPawn = Cast<APawn>(OtherActor); if (PlayerPawn && PlayerPawn->IsPlayerControlled()) { OpenDoor(); } } void AAutoDoor::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { // 同样过滤玩家角色 APawn* PlayerPawn = Cast<APawn>(OtherActor); if (PlayerPawn && PlayerPawn->IsPlayerControlled()) { // 可以添加一个延迟关门的逻辑,这里为了简单直接关闭 CloseDoor(); } } void AAutoDoor::OpenDoor() { // 如果门已经是开的,或者正在播放开门动画,则忽略 if (bIsOpen || bIsOpening || !DoorTimeline || !DoorOpenCurve) { return; } // 设置状态:正在播放开门动画 bIsOpening = true; // 设置时间轴从0开始正向播放到1.0(曲线时间长度) DoorTimeline->PlayFromStart(); } void AAutoDoor::CloseDoor() { // 如果门已经是关的,或者正在播放动画,则忽略 if (!bIsOpen || bIsOpening || !DoorTimeline || !DoorOpenCurve) { return; } // 设置状态:正在播放关门动画(反向播放) bIsOpening = false; // 注意:这里bIsOpening为false表示“正在关门” // 设置时间轴反向播放(从当前位置倒放回起点) DoorTimeline->Reverse(); }

关键逻辑解析:

  • 状态机bIsOpenbIsOpening构成了一个简单的状态机,防止函数被意外重复调用导致Timeline播放混乱。
  • PlayFromStart()Reverse():这是控制Timeline播放方向的核心函数。开门时正向播放,关门时反向播放。这意味着我们只需要一条从0(关)到1(或90,开)的曲线,就可以同时控制开门和关门两个动作,非常高效。
  • 重叠过滤:示例中只对玩家控制的Pawn做出反应。在实际项目中,你可能需要更复杂的过滤,比如检查Actor的Tag、Class等,确保只有特定物体能触发门。

6. 在编辑器中配置与测试

代码写完了,接下来是让它在编辑器中跑起来。

6.1 创建并编辑浮点曲线资产

  1. 在内容浏览器中右键,选择“杂项” -> “曲线” -> “曲线浮点型(CurveFloat)”。
  2. 命名它为DA_DoorCurve(前缀DA表示DataAsset)。
  3. 双击打开曲线编辑器。你会看到横轴(时间,0-1秒或更长)和纵轴(输出值)。
  4. 点击曲线添加点。例如:
    • 在时间0.0,值0.0(门关闭)。
    • 在时间1.0,值90.0(门打开90度)。
  5. 右键点击点,可以设置点的插值模式。选择“自动”或“立方体”可以让曲线平滑(缓入缓出)。一条典型的缓动曲线是起点和终点平缓,中间陡峭。

6.2 配置AutoDoor蓝图实例

  1. 编译你的C++代码。
  2. 在内容浏览器中,基于AAutoDoor类创建一个蓝图(右键点击AAutoDoor-> “创建蓝图类”),命名为BP_AutoDoor
  3. 打开BP_AutoDoor
  4. 在“组件”面板,选中DoorMesh,在细节面板为其指定一个静态网格体(如一个立方体)。
  5. 选中TriggerVolume,调整其“盒体范围”到合适的大小和位置,使其覆盖门前的区域。
  6. 在“我的蓝图”的“细节”面板,找到“Door Properties”分类,将我们创建的DA_DoorCurve拖拽或指定给Door Open Curve属性。
  7. 调整DoorMesh的位置和旋转,使其初始状态(旋转0度)对应“关闭”状态。

6.3 测试与调试

  1. BP_AutoDoor拖入关卡。
  2. 点击运行。控制角色走向触发区域,门应该平滑打开。离开区域,门应平滑关闭。
  3. 如果门不动,请按以下步骤排查:
    • 检查曲线资产:确保Door Open Curve已正确赋值。
    • 检查触发器:在编辑器视口中查看触发器的线框是否在预期位置。确保玩家的碰撞体能够与触发器产生重叠。
    • 检查碰撞预设:确保TriggerVolume的碰撞预设是“Trigger”,并且玩家的胶囊体或网格体碰撞与之有重叠响应。
    • 输出日志:在BeginPlay和关键函数中添加UE_LOG打印信息,查看程序执行流程。
    • 检查旋转轴:如果门旋转方向不对,修改OnTimelineUpdate函数中DoorRotation的成员(.Roll,.Pitch,.Yaw),直到符合你的模型朝向。

7. 进阶优化与功能扩展

一个基础的自动门已经完成了。但要让它在实际项目中更可用,我们还需要考虑更多。

7.1 添加可配置参数

将更多参数暴露给设计师在蓝图中调整,增加灵活性。

AutoDoor.hUPROPERTY区域添加:

// 门打开的角度(度)。与曲线最大值对应。 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door Properties", meta = (ClampMin = "0.0", ClampMax = "180.0")) float OpenAngle; // 开门/关门的动画时长(秒)。影响Timeline的播放速率。 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door Properties", meta = (ClampMin = "0.1")) float AnimationDuration; // 玩家离开后,门自动关闭的延迟时间(秒)。 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door Properties", meta = (ClampMin = "0.0")) float AutoCloseDelay;

然后在OnTimelineUpdate中,将OutputValue映射到[0, OpenAngle]范围:

void AAutoDoor::OnTimelineUpdate(float OutputValue) { if (DoorMesh) { FRotator DoorRotation = DoorMesh->GetRelativeRotation(); // 将曲线输出值(假设0-1)线性映射到0到OpenAngle度 float CurrentAngle = OutputValue * OpenAngle; DoorRotation.Yaw = CurrentAngle; // 假设绕Y轴旋转 DoorMesh->SetRelativeRotation(DoorRotation); } }

BeginPlay中,根据AnimationDuration设置Timeline的播放速率:

if (DoorTimeline && DoorOpenCurve) { // ... 其他绑定代码 float CurveTimeLength = DoorOpenCurve->FloatCurve.GetLastKey().Time; // 获取曲线时间长度 if (CurveTimeLength > 0 && AnimationDuration > 0) { float DesiredPlayRate = CurveTimeLength / AnimationDuration; DoorTimeline->SetPlayRate(DesiredPlayRate); } }

对于延迟关门,需要在OnTriggerEndOverlap中设置一个定时器:

// 在头文件声明定时器句柄 FTimerHandle AutoCloseTimerHandle; void AAutoDoor::OnTriggerEndOverlap(...) { APawn* PlayerPawn = Cast<APawn>(OtherActor); if (PlayerPawn && PlayerPawn->IsPlayerControlled()) { // 设置一个延迟调用CloseDoor的定时器 GetWorld()->GetTimerManager().SetTimer(AutoCloseTimerHandle, this, &AAutoDoor::CloseDoor, AutoCloseDelay, false); } } void AAutoDoor::OpenDoor() { // 在准备开门时,清除可能存在的延迟关门的定时器 GetWorld()->GetTimerManager().ClearTimer(AutoCloseTimerHandle); // ... 原有的开门逻辑 }

7.2 处理多人游戏与网络同步

如果你的游戏是多人游戏,门的开关状态必须同步到所有客户端。

  1. 将关键变量和函数标记为可复制
    // AutoDoor.h UPROPERTY(ReplicatedUsing = OnRep_DoorState) bool bReplicatedIsOpen; UFUNCTION() void OnRep_DoorState(); // 在类声明末尾加上 virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
  2. 实现复制
    // AutoDoor.cpp void AAutoDoor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AAutoDoor, bReplicatedIsOpen); } void AAutoDoor::OnRep_DoorState() { // 当bReplicatedIsOpen在客户端被更新时,根据其值在本地播放开门或关门动画 if (bReplicatedIsOpen) { // 在客户端模拟开门动画(可能需要RPC调用或直接驱动Timeline) } else { // 在客户端模拟关门动画 } }
  3. 使用RPC(远程过程调用)OpenDoorCloseDoor函数应该只在服务器端执行逻辑,然后通过RPC通知客户端。
    // 服务器端RPC UFUNCTION(Server, Reliable) void ServerOpenDoor(); void ServerOpenDoor_Implementation(); // 在客户端调用 void AAutoDoor::OpenDoor() { if (GetLocalRole() < ROLE_Authority) { // 我们是客户端,请求服务器开门 ServerOpenDoor(); return; } // ... 服务器端的实际开门逻辑 }

网络同步是一个深水区,需要根据你的游戏架构仔细设计。

7.3 添加声音与粒子效果

一个好的交互离不开视听反馈。

  1. 添加音频组件
    // AutoDoor.h UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Door Components") class UAudioComponent* DoorAudioComponent; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door Properties|Sound") USoundBase* DoorOpenSound; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Door Properties|Sound") USoundBase* DoorCloseSound;
  2. 在构造函数中创建,在OpenDoorCloseDoor中播放对应的声音。
  3. 同样,可以添加UParticleSystemComponent来在门边播放灰尘或气流粒子效果,在Timeline更新函数中控制其参数(如随着门开启动粒子)。

8. 完整代码整合与项目注意事项

以下是整合了基础功能和部分优化(如可配置角度、时长)的完整代码摘要。由于篇幅限制,网络同步和音效部分请参考上一节自行添加。

AutoDoor.h

#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Components/TimelineComponent.h" #include "AutoDoor.generated.h" DECLARE_DELEGATE_OneParam(FOnTimelineFloat, float); UCLASS() class YOURPROJECT_API AAutoDoor : public AActor { GENERATED_BODY() public: AAutoDoor(); protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override; UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UStaticMeshComponent* DoorMesh; UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UBoxComponent* TriggerVolume; UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UTimelineComponent* DoorTimeline; UPROPERTY(EditAnywhere, BlueprintReadWrite) UCurveFloat* DoorOpenCurve; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(ClampMin="0.0", ClampMax="180.0")) float OpenAngle = 90.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(ClampMin="0.1")) float AnimationDuration = 1.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(ClampMin="0.0")) float AutoCloseDelay = 1.0f; UPROPERTY(BlueprintReadOnly) bool bIsOpening; UPROPERTY(BlueprintReadOnly) bool bIsOpen; protected: UFUNCTION() void OnTimelineUpdate(float OutputValue); UFUNCTION() void OnTimelineFinished(); UFUNCTION() void OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); UFUNCTION() void OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex); void OpenDoor(); void CloseDoor(); FTimerHandle AutoCloseTimerHandle; };

AutoDoor.cpp

#include "AutoDoor.h" #include "Components/BoxComponent.h" #include "Components/StaticMeshComponent.h" #include "TimerManager.h" AAutoDoor::AAutoDoor() : OpenAngle(90.0f), AnimationDuration(1.0f), AutoCloseDelay(1.0f), bIsOpening(false), bIsOpen(false) { PrimaryActorTick.bCanEverTick = true; USceneComponent* RootComp = CreateDefaultSubobject<USceneComponent>(TEXT("Root")); RootComponent = RootComp; DoorMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("DoorMesh")); DoorMesh->SetupAttachment(RootComponent); TriggerVolume = CreateDefaultSubobject<UBoxComponent>(TEXT("TriggerVolume")); TriggerVolume->SetupAttachment(RootComponent); TriggerVolume->SetBoxExtent(FVector(100.0f, 100.0f, 100.0f)); TriggerVolume->SetCollisionProfileName(TEXT("Trigger")); DoorTimeline = CreateDefaultSubobject<UTimelineComponent>(TEXT("DoorTimeline")); } void AAutoDoor::BeginPlay() { Super::BeginPlay(); if (!DoorOpenCurve) { UE_LOG(LogTemp, Error, TEXT("DoorOpenCurve missing on %s!"), *GetName()); return; } if (!TriggerVolume) { UE_LOG(LogTemp, Error, TEXT("TriggerVolume missing on %s!"), *GetName()); return; } TriggerVolume->OnComponentBeginOverlap.AddDynamic(this, &AAutoDoor::OnTriggerBeginOverlap); TriggerVolume->OnComponentEndOverlap.AddDynamic(this, &AAutoDoor::OnTriggerEndOverlap); if (DoorTimeline && DoorOpenCurve) { FOnTimelineFloat TimelineProgress; TimelineProgress.BindUFunction(this, FName("OnTimelineUpdate")); DoorTimeline->AddInterpFloat(DoorOpenCurve, TimelineProgress); FOnTimelineEvent TimelineFinished; TimelineFinished.BindUFunction(this, FName("OnTimelineFinished")); DoorTimeline->SetTimelineFinishedFunc(TimelineFinished); DoorTimeline->SetLooping(false); float CurveLength = DoorOpenCurve->FloatCurve.GetLastKey().Time; if (CurveLength > 0 && AnimationDuration > 0) DoorTimeline->SetPlayRate(CurveLength / AnimationDuration); } } void AAutoDoor::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (DoorTimeline) DoorTimeline->TickComponent(DeltaTime, ELevelTick::LEVELTICK_TimeOnly, nullptr); } void AAutoDoor::OnTimelineUpdate(float OutputValue) { if (DoorMesh) { FRotator Rot = DoorMesh->GetRelativeRotation(); Rot.Yaw = OutputValue * OpenAngle; DoorMesh->SetRelativeRotation(Rot); } } void AAutoDoor::OnTimelineFinished() { bIsOpen = bIsOpening; bIsOpening = false; } void AAutoDoor::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { if (APawn* PlayerPawn = Cast<APawn>(OtherActor)) if (PlayerPawn->IsPlayerControlled()) { GetWorld()->GetTimerManager().ClearTimer(AutoCloseTimerHandle); OpenDoor(); } } void AAutoDoor::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { if (APawn* PlayerPawn = Cast<APawn>(OtherActor)) if (PlayerPawn->IsPlayerControlled()) GetWorld()->GetTimerManager().SetTimer(AutoCloseTimerHandle, this, &AAutoDoor::CloseDoor, AutoCloseDelay, false); } void AAutoDoor::OpenDoor() { if (bIsOpen || bIsOpening || !DoorTimeline || !DoorOpenCurve) return; bIsOpening = true; DoorTimeline->PlayFromStart(); } void AAutoDoor::CloseDoor() { if (!bIsOpen || bIsOpening || !DoorTimeline || !DoorOpenCurve) return; bIsOpening = false; DoorTimeline->Reverse(); }

项目注意事项与避坑指南

  1. 曲线资产管理:确保你的曲线资产在打包后能被正确引用。最好将其放在不会被意外移动或删除的文件夹中(如/Game/Data/Curves/)。
  2. 组件附着:所有组件(DoorMesh,TriggerVolume)都必须SetupAttachment到根组件或一个共同的父组件上,否则它们在关卡中的变换关系会错乱。
  3. 碰撞设置TriggerVolume的碰撞预设必须是“Trigger”,并且确保玩家的角色或Pawn拥有与之匹配的碰撞通道响应(通常是“Overlap”)。
  4. 时间轴方向:理解PlayFromStart()Reverse()是关键。确保你的曲线在时间0的值对应“关闭”状态,在时间结束的值对应“打开”状态。
  5. 状态管理bIsOpeningbIsOpen的逻辑务必清晰。这是防止动画播放冲突、确保门行为符合预期的核心。
  6. 性能考虑:如果一个关卡有上百扇门,每扇门都在Tick里驱动Timeline,可能会有性能开销。对于大量简单的门,可以考虑用更轻量的方式(如直接在Tick里做插值)。但对于需要复杂曲线和精细控制的交互,Timeline是更优解。
  7. 蓝图与C++的协作:将关键属性(OpenAngle,AnimationDuration)暴露为UPROPERTY(EditAnywhere, BlueprintReadWrite),可以让关卡设计师在不修改代码的情况下,在蓝图中批量调整所有门的参数,这是C++赋能工作流的最佳实践。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询