业务场景 :旨在实现叉车(或 AGV 等物流设备)根据给定的路径坐标进行精准移动。后续可通过算法动态传入路径点,并通过 WebSocket 协议实现 UE 客户端与后端的实时通讯。
技术栈 :主要基于 UE C++ 实现,结合蓝图进行参数配置与测试。
核心目标:
叉车能够严格按照指定的样条线(Spline) 轨迹移动。
引入时间轴(Timeline) 组件,实现对叉车移动速度的精准且平滑的控制。
支持动态生成样条线 ,以适配后端实时下发的路径规划数据。
通过WebSocket协议将UE与网站部分连接
叉车按照动态生成的样条线移动
我们首先让叉车沿着样条线动起来
前期准备工作
要使用UEC++控制叉车按照样条线,我们首先在UE中创建一个C++类,作为样条线的父类;
再创建一个C++类,作为叉车类的父类,在该C++类中创建叉车的移动等函数,实现叉车的移动
创建样条线Actor 创建样条线的C++类,并创建基于该C++类的蓝图类,在蓝图类中放入样条线组件(Spline Component)注意不是Spline Mesh Component,那是用来依附模型的
创建完毕后,将BP_Spline这个Actor拖入场景中,下面稍微讲解一下如何先手动拉出一条自己想要路径的样条线:
在场景中,鼠标左键点击 那条短线上面的最后一个点 (白色的方块点)。选中后,该点会变成高亮的颜色,并且出现常规的移动坐标轴(红绿蓝箭头)。
按住键盘上的 Alt 键不放 。
鼠标左键点击并拖拽坐标轴 (比如拖拽红色的X轴,或者在两个轴之间的平面上拖拽)。
松开鼠标 。你会发现,系统自动为你生成了一个新的样条线控制点,并且线条被拉长了!
重复这个过程 :保持选中最后一个点 -> 按住 Alt -> 往外拖拽。你就可以像画画一样,把样条线拉成任意你想要的轨迹。
创建叉车Actor 按照创建样条线相同的方法,我们创建叉车的C++类,并以这个C++类创建一个蓝图类
在蓝图类中摆放叉车的模型
叉车沿着样条线移动 要让叉车“认识”并“跑到”样条线上,我们需要完成以下几个步骤:
全局搜索 :在当前关卡中找到目标样条线 Actor。
获取组件 :从该 Actor 身上提取出包含路径数据的 USplineComponent。
计算距离 :获取样条线的总长度,并算出我们想要定位的目标距离。
空间转换 :将这个距离值转换为世界坐标空间中的 3D 坐标点(X, Y, Z)。
应用位移 :把叉车 Actor 的坐标设置过去。
下面给出具体代码:
Forklift.h部分1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #pragma once #include "CoreMinimal.h" #include "Spline.h" #include "Components/SplineComponent.h" #include "GameFramework/Actor.h" #include "Forklift.generated.h" UCLASS ()class WMSPRE_API AForklift : public AActor{ GENERATED_BODY () public : AForklift (); protected : virtual void BeginPlay () override ; public : virtual void Tick (float DeltaTime) override ; UFUNCTION (BlueprintCallable, Category = "Forklift" ) void MoveBySpline (float DeltaTime) ; private : USplineComponent* SplineComponent; ASpline* SplineRef = nullptr ; float CurrentDistanceAlongSpline = 0.0f ; float MovementSpeed = 200.0f ; };
Forklift.cpp部分1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include "Forklift.h" #include "EngineUtils.h" #include "Kismet/KismetMathLibrary.h" AForklift::AForklift () { PrimaryActorTick.bCanEverTick = true ; } void AForklift::BeginPlay () { Super::BeginPlay (); for (TActorIterator<ASpline> It (GetWorld ()); It; ++It) { SplineRef = *It; break ; } if (SplineRef != nullptr ) { SplineComponent = SplineRef->FindComponentByClass <USplineComponent>(); } } void AForklift::Tick (float DeltaTime) { Super::Tick (DeltaTime); MoveBySpline (DeltaTime); } void AForklift::MoveBySpline (float DeltaTime) { if (SplineComponent == nullptr ) { return ; } CurrentDistanceAlongSpline += MovementSpeed * DeltaTime; float SplineLength = SplineComponent->GetSplineLength (); if (CurrentDistanceAlongSpline > SplineLength) { CurrentDistanceAlongSpline = 0.0f ; } FVector NewLocation = SplineComponent->GetLocationAtDistanceAlongSpline ( CurrentDistanceAlongSpline, ESplineCoordinateSpace::World ); SetActorLocation (NewLocation); }
在实现上述基础坐标定位的过程中,我们调用了几个 Unreal Engine 5 中非常关键的底层机制和数学函数,我们先了解几个UE5中的基础函数:
全局 Actor 遍历 (TActorIterator): 在代码中,我们使用了 TActorIterator<ASpline>(GetWorld())。这是 UE C++ 中用于在当前关卡(World)中查找特定类(及其子类)所有实例的标准迭代器。 技术提示 :这个操作会扫描场景中的大量对象,性能开销较大,因此在实际开发中,通常只在 BeginPlay 中执行一次,或者直接通过暴露变量到编辑器中进行引用赋值,严禁将其放在每帧执行的 Tick 函数中。
**组件获取 (FindComponentByClass)**: Actor 本身只是一个容器,真正的路径数据存储在它的组件里。通过 FindComponentByClass<USplineComponent>(),我们能够安全地提取出控制路径走向的核心组件,进而读取它的数据。
**样条线核心 API (GetLocationAtDistanceAlongSpline)**: 这是样条线系统的“灵魂函数”。它接收一个一维的距离值(Distance),并将其投射到弯曲的 3D 样条线上,最终返回一个精确的 3D 世界坐标(FVector)。我们在传参时指定了 ESplineCoordinateSpace::World,确保返回的坐标是绝对的世界坐标,这样叉车才能正确到位。
**坐标应用 (SetActorLocation)**: 最后,我们将计算出的目标坐标直接赋予叉车。由于缺少时间的过渡,这个函数在当前表现为“瞬间传送”。
在上面的准备工作中,我们在 C++ 的 Tick 函数中,通过 DeltaTime 手动累加移动距离来实现叉车的位移。这种“生硬计算”虽然能让模型动起来,但面临三个致命缺陷:
无法变速 :难以实现真实的起步加速和到站刹车减速。
状态难控 :暂停、倒车或从指定中途点出发的逻辑编写极度复杂。
帧率依赖 :容易因设备卡顿导致物理表现不同步。
为此,我们通过 C++ 封装一个标准的 时间轴组件(Timeline Component) ,作为所有物流设备的“动力核心”。我们将通过 C++ 绑定一个 Timeline 组件,利用它输出的平滑曲线(Curve)来动态驱动插值的 Alpha 值,从而让叉车真正拥有起步、匀速、停车的物理表现
使用时间轴(Timeline)驱动叉车的移动 创建浮点曲线(Curve Float) 首先在 UE 引擎中,右键新建 -> 其他 -> 曲线 -> 选择 Curve Float。
关键帧设置 :添加两个关键帧,起点为 **(0, 0)**,终点为 **(1, 1)**。这条 1 秒长、从 0 渐变到 1 的直线段,将作为我们后续换算路程的基础比例尺。
编写自定义时间轴组件 (UMyTimeLineComponent) 为了方便以后复用,我们将Timeline组件写成一个标准的组件(继承自UActorComponent),后续我们将其绑定在穿梭车、AGV等复用。
创建一个继承自 UActorComponent 的 C++ 类,方便挂载到任何 Actor 上。
源文件 (MyTimeLineComponent.cpp) 避坑指南:
性能优化 :时间轴底层自带 Tick 计算,务必在构造函数中设置 PrimaryComponentTick.bCanEverTick = false; 关闭当前组件的额外 Tick。
事件绑定 :利用引擎反射机制,将蓝图中配置的浮点曲线(MovementCurve)绑定到 OnTimelineUpdate 函数上,并在函数内部通过多播委托OnTimelineUpdateDelegate.Broadcast(Value) 将 0~1 的进度值广播出去。
MyTimeLineComponent的具体代码:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "Components/TimelineComponent.h" #include "MyTimeLineComponent.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam (FOnTimelineUpdateSignature, float , Value);DECLARE_DYNAMIC_MULTICAST_DELEGATE (FOnTimelineFinishedSignature);UCLASS ( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )class WMSPRE_API UMyTimeLineComponent : public UActorComponent{ GENERATED_BODY () public : UMyTimeLineComponent (); protected : virtual void BeginPlay () override ; public : virtual void TickComponent (float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override ; UPROPERTY (EditAnywhere, BlueprintReadWrite, Category = "Timeline" ) UCurveFloat* MovementCurve; UPROPERTY (BlueprintAssignable, Category = "Timeline" ) FOnTimelineUpdateSignature OnTimelineUpdateDelegate; UPROPERTY (BlueprintAssignable, Category = "Timeline" ) FOnTimelineFinishedSignature OnTimelineFinishedDelegate; UFUNCTION (BlueprintCallable, Category = "Timeline" ) void PlayTimeline () ; UFUNCTION (BlueprintCallable, Category = "Timeline" ) void SetPlayRate (float NewRate) ; private : UPROPERTY () UTimelineComponent* InternalTimeline; UFUNCTION () void OnTimelineUpdate (float Value) ; UFUNCTION () void OnTimelineFinished () ; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #include "MyTimeLineComponent.h" #include "Components/TimelineComponent.h" UMyTimeLineComponent::UMyTimeLineComponent () { PrimaryComponentTick.bCanEverTick = false ; InternalTimeline = CreateDefaultSubobject <UTimelineComponent>(TEXT ("InternalTimeline" )); } void UMyTimeLineComponent::BeginPlay () { Super::BeginPlay (); if (MovementCurve) { FOnTimelineFloat UpdateFunction; UpdateFunction.BindUFunction (this , FName ("OnTimelineUpdate" )); InternalTimeline->AddInterpFloat (MovementCurve, UpdateFunction); FOnTimelineEvent FinishedFunction; FinishedFunction.BindUFunction (this , FName ("OnTimelineFinished" )); InternalTimeline->SetTimelineFinishedFunc (FinishedFunction); } else { UE_LOG (LogTemp, Error, TEXT ("[%s] MovementCurve 为空!请在蓝图详情面板中为其指定一条浮点曲线。" ), *GetName ()); } } void UMyTimeLineComponent::TickComponent (float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent (DeltaTime, TickType, ThisTickFunction); } void UMyTimeLineComponent::OnTimelineUpdate (float Value) { OnTimelineUpdateDelegate.Broadcast (Value); } void UMyTimeLineComponent::OnTimelineFinished () { OnTimelineFinishedDelegate.Broadcast (); } void UMyTimeLineComponent::PlayTimeline () { if (InternalTimeline) { InternalTimeline->PlayFromStart (); } } void UMyTimeLineComponent::SetPlayRate (float NewRate) { if (InternalTimeline) { InternalTimeline->SetPlayRate (NewRate); } }
注意不要忘记在时间轴的蓝图中绑定曲线MovementCurve
要使用新增的时间轴组件,我们要在叉车的文件中修改相关的代码。
在 Forklift.h 中: 1 2 3 4 5 6 7 8 9 #include "Component/MyTimelineComponent.h" UPROPERTY (VisibleAnywhere, BlueprintReadOnly, Category = "Components" )UMyTimelineComponent* TimelineComp; UFUNCTION ()void HandleTimelineUpdate (float Value) ;
在 Forklift.cpp 中: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include "Forklift.h" #include "EngineUtils.h" #include "Kismet/KismetMathLibrary.h" AForklift::AForklift () { PrimaryActorTick.bCanEverTick = false ; TimelineComp = CreateDefaultSubobject <UMyTimeLineComponent>(TEXT ("TimelineComp" )); } void AForklift::BeginPlay () { Super::BeginPlay (); for (TActorIterator<ASpline> It (GetWorld ()); It; ++It) { SplineRef = *It; break ; } if (SplineRef != nullptr ) { SplineComponent = SplineRef->FindComponentByClass <USplineComponent>(); } if (TimelineComp) { TimelineComp->OnTimelineUpdateDelegate.AddDynamic (this , &AForklift::HandleTimelineUpdate); TimelineComp->PlayTimeline (); } } void AForklift::HandleTimelineUpdate (float Value) { if (SplineComponent == nullptr ) { return ; } float SplineLength = SplineComponent->GetSplineLength (); float CurrentDistance = SplineLength * Value; FVector NewLocation = SplineComponent->GetLocationAtDistanceAlongSpline ( CurrentDistance, ESplineCoordinateSpace::World ); FRotator NewRotation = SplineComponent->GetRotationAtDistanceAlongSpline ( CurrentDistance, ESplineCoordinateSpace::World ); SetActorLocationAndRotation (NewLocation, NewRotation); }
这样我们就实现了通过时间轴来驱动叉车进行移动,不过此时叉车的移动速度过快且无法控制。时间轴本质控制的是“完成整个路程所需的时间”,而不是直接控制“速度”。因此,在使用时间轴时,我们控制叉车速度的终极武器是:动态修改时间轴的播放速率(Play Rate)。
通过时间轴修改叉车移动的速度 基于 1.0 秒的基准曲线,我们可以使用以下物理公式:
所需总时间 :
Time = SplineLength/TargetSpeed
(样条线总长度/目标速度)
时间轴播放速率 :
PlayRate = 1.0/Time
合并化简后得出最终公式 :
PlayRate = TargetSpeed/SplineLength
举个例子 :
假设样条线全长 1000 cm,您希望叉车的速度是 200 cm/s。
按照公式:PlayRate = 200 / 1000 = 0.2。
这意味着时间轴会以 0.2 倍速播放,原本 1 秒播完的曲线,现在需要 5 秒播完。正好符合 1000 /200 = 5 s。
我们现在修改对应的C++文件
修改 Forklift.h 添加一个用于控制速度的变量
1 2 3 4 5 6 7 8 9 public : UPROPERTY (EditAnywhere, BlueprintReadWrite, Category = "Forklift Setup" ) float MovementSpeed = 300.0f ; UFUNCTION (BlueprintCallable, Category = "Forklift Action" ) void StartMovingOnSpline () ;
修改 Forklift.cpp 实现计算播放速率并启动时间轴的逻辑写在一个专门启动移动的函数(StartMovingOnSpline)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void AForklift::StartMovingOnSpline () { if (SplineComponent == nullptr || TimelineComp == nullptr ) { UE_LOG (LogTemp, Error, TEXT ("无法移动:样条线或时间轴组件为空!" )); return ; } float SplineLength = SplineComponent->GetSplineLength (); if (SplineLength <= 0.0f ) return ; float CalculatedPlayRate = MovementSpeed / SplineLength; TimelineComp->SetPlayRate (CalculatedPlayRate); TimelineComp->PlayTimeline (); UE_LOG (LogTemp, Warning, TEXT ("叉车出发!速度: %f cm/s, 播放速率: %f" ), MovementSpeed, CalculatedPlayRate); }
但是此时叉车的移动依然是靠着提前在场景中放好的样条线来驱动的,而真实的场景中,叉车的路径不是固定的,单独一个样条线也无法驱动所有叉车运动,因此接下来我们选择动态生成样条线。
动态生成样条线 在真实 WMS 场景下,叉车不可能依赖场景中预设的线条。我们需要赋予叉车“自己画路自己走”的能力。
这个方法可以接受到后端发来的一组路径规划的坐标等,并根据这些坐标,动态的在自己身上生成一条包含这些点的临时样条线,然后驱动叉车沿着这条临时线开。当叉车抵达终点后,该临时样条线被销毁。
实现这个功能,我们需要做两个事情:
让每个叉车自带一个USplineComponent(注意:是记录路径的样条线组件,不是附着网格体的 SplineMeshComponent)。
强制将该样条线设置为“绝对世界坐标”,否则样条线会跟着叉车一起移动,无法起到导航的效果
修改 Forklift.h 我们需要在叉车身上添加属于它自己的样条线组件,并暴露一个接收点位数组的函数 MoveAlongPath。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "Components/SplineComponent.h" #include "Component/MyTimeLineComponent.h" #include "Forklift.generated.h" UCLASS ()class WMSPRE_API AForklift : public AActor{ GENERATED_BODY () public : AForklift (); protected : virtual void BeginPlay () override ; public : UPROPERTY (VisibleAnywhere, BlueprintReadOnly, Category = "Components" ) USceneComponent* RootComp; UPROPERTY (VisibleAnywhere, BlueprintReadOnly, Category = "Components" ) UMyTimeLineComponent* TimelineComp; UPROPERTY (VisibleAnywhere, BlueprintReadOnly, Category = "Components" ) USplineComponent* PathSpline; UPROPERTY (EditAnywhere, BlueprintReadWrite, Category = "Forklift Setup" ) float MovementSpeed = 300.0f ; UFUNCTION (BlueprintCallable, Category = "Forklift Action" ) void MoveAlongPath (const TArray<FVector>& PathPoints) ; private : UFUNCTION () void HandleTimelineUpdate (float Value) ; };
修改 Forklift.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include "Forklift.h" AForklift::AForklift () { PrimaryActorTick.bCanEverTick = false ; RootComp = CreateDefaultSubobject <USceneComponent>(TEXT ("RootComp" )); RootComponent = RootComp; TimelineComp = CreateDefaultSubobject <UMyTimeLineComponent>(TEXT ("TimelineComp" )); PathSpline = CreateDefaultSubobject <USplineComponent>(TEXT ("PathSpline" )); PathSpline->SetupAttachment (RootComponent); PathSpline->SetUsingAbsoluteLocation (true ); PathSpline->SetUsingAbsoluteRotation (true ); } void AForklift::BeginPlay () { Super::BeginPlay (); if (TimelineComp) { TimelineComp->OnTimelineUpdateDelegate.AddDynamic (this , &AForklift::HandleTimelineUpdate); } } void AForklift::MoveAlongPath (const TArray<FVector>& PathPoints) { if (PathPoints.Num () < 2 || PathSpline == nullptr || TimelineComp == nullptr ) { UE_LOG (LogTemp, Warning, TEXT ("点位不足或组件丢失,无法移动!" )); return ; } PathSpline->ClearSplinePoints (); for (int32 i = 0 ; i < PathPoints.Num (); ++i) { PathSpline->AddSplinePoint (PathPoints[i], ESplineCoordinateSpace::World, false ); PathSpline->SetSplinePointType (i, ESplinePointType::Linear, false ); } PathSpline->UpdateSpline (); float SplineLength = PathSpline->GetSplineLength (); if (SplineLength > 0.0f ) { float PlayRate = MovementSpeed / SplineLength; TimelineComp->SetPlayRate (PlayRate); TimelineComp->PlayTimeline (); UE_LOG (LogTemp, Warning, TEXT ("叉车收到新路线!共 %d 个点,路线长度: %f, 速度: %f" ), PathPoints.Num (), SplineLength, MovementSpeed); } } void AForklift::HandleTimelineUpdate (float Value) { if (PathSpline == nullptr ) return ; float SplineLength = PathSpline->GetSplineLength (); float CurrentDistance = SplineLength * Value; FVector NewLocation = PathSpline->GetLocationAtDistanceAlongSpline (CurrentDistance, ESplineCoordinateSpace::World); FRotator NewRotation = PathSpline->GetRotationAtDistanceAlongSpline (CurrentDistance, ESplineCoordinateSpace::World); SetActorLocationAndRotation (NewLocation, NewRotation); }
测试 上面代码完成后,我们可以通过在蓝图中绑定按键输入触发移动函数,并手动设置一些坐标来驱动叉车的移动
打开关卡蓝图
在蓝图的事件图表(Event Graph)中,找一个测试按键(比如键盘 1)。
使用 Make Array 节点构建一个包含 3-4 个 Vector 坐标的数组(例如:起点 (0,0,0) -> 途经点 (1000,0,0) -> 转弯点 (1000,1000,0))。
将这个数组连入我们刚刚写好的 Move Along Path 节点。
运行游戏,按下 1,叉车就会开始移动
至此我们已经实现了叉车按照动态生成的样条线进行移动,并暴露了接口 MoveAlongPath(const TArray<FVector>& PathPoints) 供后端数据调用。
那我们来总结一下我们到底具体干了什么 前置原型阶段(最初的静态测试) 在引入复杂机制前,最初的版本仅仅是为了让叉车“动起来”。
实现了什么 :在场景中手动拉出一条 BP_Spline 线,然后在叉车的 Tick 函数中,通过 DeltaTime * 速度 每帧手动累加行驶距离。
第一次迭代:引入时间轴 (Timeline) 为了解决无法变速、难于控制等物理表现生硬的问题,我们重构了驱动核心。
新增了什么 :创建了独立且可复用的 C++ 组件 UMyTimeLineComponent,利用一条 0.0 到 1.0 的 CurveFloat(浮点曲线)来输出移动进度。
舍弃了什么 :彻底关闭了叉车类的每帧更新(PrimaryActorTick.bCanEverTick = false),删除了在 Tick 函数中手动累加距离的代码。
为什么要这么做 :
摆脱帧率绑架 :纯代码在 Tick 中累加位移,会受到电脑配置和游戏掉帧的影响。
动作扩展性 :手动累加极难处理变速运动(如起步加速、到站减速),且很难实现随时暂停、倒车等状态控制。时间轴完美解决了这些物理平滑过渡的需求。
第二次迭代:速度换算 (PlayRate 控制) 有了时间轴后,我们需要解决“距离变长,速度变快”的谬误。
新增了什么 :在移动执行前,加入了一套数学换算逻辑:CalculatedPlayRate = MovementSpeed / SplineLength,以此动态修改时间轴的 PlayRate(播放速率)。
舍弃了什么 :舍弃了时间轴默认的“固定时间播完全程”的特性(即 1 秒钟跑完曲线)。
为什么要这么做 :时间轴本质上控制的是“耗时”而非“速度”。如果不加以干预,叉车走 10 米和走 1000 米都会在 1 秒内完成,这显然违背物理常识。通过路程和目标速度反推播放速率,我们保证了无论路线多长,叉车永远以设定的真实物理速度(如 300 cm/s)匀速行驶。
第三次迭代:动态生成专属样条线 为了适配真实的仓储业务,我们让叉车具备了“自行铺路”的能力。
新增了什么 :给每一辆叉车 Actor 安装了专属 的 USplineComponent,并强制设为绝对世界坐标。暴露出 MoveAlongPath 接口,接收 TArray<FVector> 数组,动态绘制点位类型为直线(Linear)的路径。
舍弃了什么 :
舍弃了在场景中手工预设画好的静态样条线。
彻底删除了 BeginPlay 中极其耗费性能的 TActorIterator<ASpline> 全局遍历查找代码。
为什么要这么做 :
性能痛点 :TActorIterator 会扫描场景中的大量对象,性能开销极大,不适合作为常规逻辑。
业务刚需 :真实的 WMS 场景中,成百上千台 AGV/叉车的路径是后端算法实时动态规划的。固定的路线无法应对拥堵绕行、随时变道的需求。将样条线内置到叉车身上,做到了真正的“数据驱动模型”。