1. 项目概述为什么选择Timeline做自动门在UE5里实现一个自动门听起来是个入门级的练习但很多刚接触C的开发者包括我自己刚开始的时候都会下意识地直接去写Tick函数用线性插值Lerp去更新门的位置或旋转。这么做当然能跑起来但代码很快就会变得难以维护——门的开关速度、缓动效果、延迟触发逻辑全都糅在Tick里改起来心惊胆战。这就是Timeline组件出场的时候了。你可以把它理解为一个基于时间轴的曲线播放器。我们把门的移动轨迹比如从关到开90度定义成一条曲线横轴是时间0到1秒纵轴是门的旋转角度0到90度。Timeline负责按时间播放这条曲线我们只需要在它播放的每个“帧”里拿到当前曲线输出的值赋给门就行。所有关于“如何运动”的复杂逻辑——加速、减速、匀速——都交给了这条可视化的曲线来定义C代码只负责“播放”和“应用结果”职责清晰后期调整动画效果只需在编辑器中拉几下曲线无需重新编译代码。这次我们就彻底抛弃蓝图用纯C在UE5中结合Timeline组件打造一个可复用的、带感应区域的自动门Actor。你会学到如何声明和绑定Timeline事件如何将曲线资产与C类协作以及如何构建一个清晰的交互框架。文末会提供完整的、可直接粘贴使用的项目代码。2. 核心设计思路与类结构规划在动手写代码前我们先花几分钟把设计思路理清楚。一个健壮的自动门系统不应该只是一个会动的模型它应该包含清晰的逻辑模块。2.1 功能分解与组件选型我们的自动门需要实现以下核心功能感知检测玩家或物体是否进入触发区域。决策根据感知结果决定门应该打开还是关闭。执行驱动门模型执行平滑的打开或关闭动画。对应到UE的组件系统我的方案是这样的感知使用一个BoxComponent或SphereComponent作为触发器Trigger Volume。这是最轻量、高效的检测方式比射线检测更适用于区域持续感应。决策逻辑放在自动门的主类如AAutoDoor中。它需要监听触发器的OnComponentBeginOverlap和OnComponentEndOverlap事件。执行核心就是UTimelineComponent。门的运动状态旋转角度或位置将由一条UCurveFloat曲线驱动。我们还需要一个UStaticMeshComponent或USceneComponent来代表门的可视部分。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类在内容浏览器的“C类”文件夹上右键选择“新建C类”。选择父类为“Actor”命名为AutoDoor引擎会自动添加前缀A生成AAutoDoor类。点击创建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()同样用于反射使得函数可以被蓝图调用或绑定为事件。bIsOpening和bIsOpen这两个状态变量至关重要。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 CreateDefaultSubobjectUSceneComponent(TEXT(Root)); RootComponent RootComp; // 创建门网格体组件并附着到根组件 DoorMesh CreateDefaultSubobjectUStaticMeshComponent(TEXT(DoorMesh)); DoorMesh-SetupAttachment(RootComponent); // 可以在构造函数里设置一个默认网格体如果项目里有 // static ConstructorHelpers::FObjectFinderUStaticMesh MeshAsset(TEXT(/Game/Path/To/Your/Mesh)); // if (MeshAsset.Succeeded()) { DoorMesh-SetStaticMesh(MeshAsset.Object); } // 创建触发器盒子组件并附着到根组件 TriggerVolume CreateDefaultSubobjectUBoxComponent(TEXT(TriggerVolume)); TriggerVolume-SetupAttachment(RootComponent); TriggerVolume-SetBoxExtent(FVector(100.0f, 100.0f, 100.0f)); // 设置默认大小 TriggerVolume-SetCollisionProfileName(TEXT(Trigger)); // 设置碰撞预设为Trigger只检测重叠不产生物理阻挡 // 创建时间轴组件 DoorTimeline CreateDefaultSubobjectUTimelineComponent(TEXT(DoorTimeline)); // 初始化状态变量 bIsOpening false; bIsOpen false; }注意CreateDefaultSubobject只能在构造函数中调用。它创建的是组件模板在编辑器放置或生成这个Actor时实际使用的是这些模板的实例。4.2 BeginPlay中绑定事件与设置TimelineBeginPlay是游戏开始时执行的函数我们在这里进行动态的事件绑定和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 CastAPawn(OtherActor); if (PlayerPawn PlayerPawn-IsPlayerControlled()) { OpenDoor(); } } void AAutoDoor::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { // 同样过滤玩家角色 APawn* PlayerPawn CastAPawn(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(); }关键逻辑解析状态机bIsOpen和bIsOpening构成了一个简单的状态机防止函数被意外重复调用导致Timeline播放混乱。PlayFromStart()和Reverse()这是控制Timeline播放方向的核心函数。开门时正向播放关门时反向播放。这意味着我们只需要一条从0关到1或90开的曲线就可以同时控制开门和关门两个动作非常高效。重叠过滤示例中只对玩家控制的Pawn做出反应。在实际项目中你可能需要更复杂的过滤比如检查Actor的Tag、Class等确保只有特定物体能触发门。6. 在编辑器中配置与测试代码写完了接下来是让它在编辑器中跑起来。6.1 创建并编辑浮点曲线资产在内容浏览器中右键选择“杂项” - “曲线” - “曲线浮点型(CurveFloat)”。命名它为DA_DoorCurve前缀DA表示DataAsset。双击打开曲线编辑器。你会看到横轴时间0-1秒或更长和纵轴输出值。点击曲线添加点。例如在时间0.0值0.0门关闭。在时间1.0值90.0门打开90度。右键点击点可以设置点的插值模式。选择“自动”或“立方体”可以让曲线平滑缓入缓出。一条典型的缓动曲线是起点和终点平缓中间陡峭。6.2 配置AutoDoor蓝图实例编译你的C代码。在内容浏览器中基于AAutoDoor类创建一个蓝图右键点击AAutoDoor- “创建蓝图类”命名为BP_AutoDoor。打开BP_AutoDoor。在“组件”面板选中DoorMesh在细节面板为其指定一个静态网格体如一个立方体。选中TriggerVolume调整其“盒体范围”到合适的大小和位置使其覆盖门前的区域。在“我的蓝图”的“细节”面板找到“Door Properties”分类将我们创建的DA_DoorCurve拖拽或指定给Door Open Curve属性。调整DoorMesh的位置和旋转使其初始状态旋转0度对应“关闭”状态。6.3 测试与调试将BP_AutoDoor拖入关卡。点击运行。控制角色走向触发区域门应该平滑打开。离开区域门应平滑关闭。如果门不动请按以下步骤排查检查曲线资产确保Door Open Curve已正确赋值。检查触发器在编辑器视口中查看触发器的线框是否在预期位置。确保玩家的碰撞体能够与触发器产生重叠。检查碰撞预设确保TriggerVolume的碰撞预设是“Trigger”并且玩家的胶囊体或网格体碰撞与之有重叠响应。输出日志在BeginPlay和关键函数中添加UE_LOG打印信息查看程序执行流程。检查旋转轴如果门旋转方向不对修改OnTimelineUpdate函数中DoorRotation的成员.Roll,.Pitch,.Yaw直到符合你的模型朝向。7. 进阶优化与功能扩展一个基础的自动门已经完成了。但要让它在实际项目中更可用我们还需要考虑更多。7.1 添加可配置参数将更多参数暴露给设计师在蓝图中调整增加灵活性。在AutoDoor.h的UPROPERTY区域添加// 门打开的角度度。与曲线最大值对应。 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 CastAPawn(OtherActor); if (PlayerPawn PlayerPawn-IsPlayerControlled()) { // 设置一个延迟调用CloseDoor的定时器 GetWorld()-GetTimerManager().SetTimer(AutoCloseTimerHandle, this, AAutoDoor::CloseDoor, AutoCloseDelay, false); } } void AAutoDoor::OpenDoor() { // 在准备开门时清除可能存在的延迟关门的定时器 GetWorld()-GetTimerManager().ClearTimer(AutoCloseTimerHandle); // ... 原有的开门逻辑 }7.2 处理多人游戏与网络同步如果你的游戏是多人游戏门的开关状态必须同步到所有客户端。将关键变量和函数标记为可复制// AutoDoor.h UPROPERTY(ReplicatedUsing OnRep_DoorState) bool bReplicatedIsOpen; UFUNCTION() void OnRep_DoorState(); // 在类声明末尾加上 virtual void GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const override;实现复制// AutoDoor.cpp void AAutoDoor::GetLifetimeReplicatedProps(TArrayFLifetimeProperty OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AAutoDoor, bReplicatedIsOpen); } void AAutoDoor::OnRep_DoorState() { // 当bReplicatedIsOpen在客户端被更新时根据其值在本地播放开门或关门动画 if (bReplicatedIsOpen) { // 在客户端模拟开门动画可能需要RPC调用或直接驱动Timeline } else { // 在客户端模拟关门动画 } }使用RPC远程过程调用OpenDoor和CloseDoor函数应该只在服务器端执行逻辑然后通过RPC通知客户端。// 服务器端RPC UFUNCTION(Server, Reliable) void ServerOpenDoor(); void ServerOpenDoor_Implementation(); // 在客户端调用 void AAutoDoor::OpenDoor() { if (GetLocalRole() ROLE_Authority) { // 我们是客户端请求服务器开门 ServerOpenDoor(); return; } // ... 服务器端的实际开门逻辑 }网络同步是一个深水区需要根据你的游戏架构仔细设计。7.3 添加声音与粒子效果一个好的交互离不开视听反馈。添加音频组件// 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;在构造函数中创建在OpenDoor和CloseDoor中播放对应的声音。同样可以添加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(ClampMin0.0, ClampMax180.0)) float OpenAngle 90.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta(ClampMin0.1)) float AnimationDuration 1.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta(ClampMin0.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 CreateDefaultSubobjectUSceneComponent(TEXT(Root)); RootComponent RootComp; DoorMesh CreateDefaultSubobjectUStaticMeshComponent(TEXT(DoorMesh)); DoorMesh-SetupAttachment(RootComponent); TriggerVolume CreateDefaultSubobjectUBoxComponent(TEXT(TriggerVolume)); TriggerVolume-SetupAttachment(RootComponent); TriggerVolume-SetBoxExtent(FVector(100.0f, 100.0f, 100.0f)); TriggerVolume-SetCollisionProfileName(TEXT(Trigger)); DoorTimeline CreateDefaultSubobjectUTimelineComponent(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 CastAPawn(OtherActor)) if (PlayerPawn-IsPlayerControlled()) { GetWorld()-GetTimerManager().ClearTimer(AutoCloseTimerHandle); OpenDoor(); } } void AAutoDoor::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { if (APawn* PlayerPawn CastAPawn(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(); }项目注意事项与避坑指南曲线资产管理确保你的曲线资产在打包后能被正确引用。最好将其放在不会被意外移动或删除的文件夹中如/Game/Data/Curves/。组件附着所有组件DoorMesh,TriggerVolume都必须SetupAttachment到根组件或一个共同的父组件上否则它们在关卡中的变换关系会错乱。碰撞设置TriggerVolume的碰撞预设必须是“Trigger”并且确保玩家的角色或Pawn拥有与之匹配的碰撞通道响应通常是“Overlap”。时间轴方向理解PlayFromStart()和Reverse()是关键。确保你的曲线在时间0的值对应“关闭”状态在时间结束的值对应“打开”状态。状态管理bIsOpening和bIsOpen的逻辑务必清晰。这是防止动画播放冲突、确保门行为符合预期的核心。性能考虑如果一个关卡有上百扇门每扇门都在Tick里驱动Timeline可能会有性能开销。对于大量简单的门可以考虑用更轻量的方式如直接在Tick里做插值。但对于需要复杂曲线和精细控制的交互Timeline是更优解。蓝图与C的协作将关键属性OpenAngle,AnimationDuration暴露为UPROPERTY(EditAnywhere, BlueprintReadWrite)可以让关卡设计师在不修改代码的情况下在蓝图中批量调整所有门的参数这是C赋能工作流的最佳实践。