☰
UE4 C++ FVector从入门到避坑:三维向量基础与实战指南
2026/10/14 13:26:17 网站建设 项目流程

1. 为什么FVector是UE4 C++里绕不开的第一道坎

刚接触UE4 C++的人,十个里有八个会在FVector上卡一下。不是因为它难,而是因为它太基础了,基础到很多教程直接跳过,默认你懂。结果就是:你知道它是个三维向量,但真到写代码的时候,Location、Direction、Normal、Rotation这些概念混在一起,编译能过,运行起来角色却往地里钻或者原地打转。

FVector在UE4的C++体系里,地位相当于盖房子用的砖。你做的任何跟位置、方向、距离、速度有关的事情,底层都是它在撑着。Actor的坐标是FVector,移动方向是FVector,两点之间的距离计算靠FVector,连射线检测的起点终点也是FVector。可以说,不把FVector吃透,后面学移动组件、物理碰撞、AI寻路,全是空中楼阁。

这篇笔记面向的是刚上手UE4 C++、已经能创建Actor但还没搞明白三维数学怎么在引擎里落地的人。我会从FVector的结构定义讲起,把构造方式、常用运算、坐标系约定、和FRotator的关系、以及实际写代码时最容易踩的几个坑,全部拆开说清楚。每个知识点都配可编译的代码片段,你照着敲一遍就能理解为什么这么设计。

提示:UE4的C++和纯C++写三维数学有一个本质区别——UE4的坐标系是左手系,Z轴朝上。这个约定会影响你所有向量的方向判断,后面会专门展开。

2. FVector的内部构造:三个float撑起整个三维世界

2.1 成员变量与内存布局

打开UE4的源码,FVector的定义在Engine/Source/Runtime/Core/Public/Math/Vector.h里。核心就三个成员:

struct FVector { float X; float Y; float Z; // ... 大量成员函数 };

没有虚函数表,没有继承,就是三个连续的float。这意味着它在内存里占12个字节,可以安全地按值传递、放进数组、做memcpy。UE4之所以这么设计,是因为三维向量在游戏运行时被调用的频率极高——每帧每个Actor的位置更新、每次碰撞检测、每个粒子的运动,都在操作FVector。如果它带虚函数或者堆分配,性能开销会大到无法接受。

你可以把FVector理解成一个"数据包裹",它本身不持有任何资源,拷贝它就像拷贝三个数字一样便宜。这也是为什么UE4的API里大量出现const FVector&和直接返回FVector的写法——前者避免无谓拷贝,后者因为拷贝成本低所以直接返回值反而更清晰。

2.2 构造FVector的几种方式

最直接的构造就是逐个传参:

FVector MyLocation(100.0f, 200.0f, 50.0f);

这里X=100,Y=200,Z=50。注意UE4的单位是厘米,所以这个位置在游戏世界里是离原点X方向1米、Y方向2米、Z方向0.5米的地方。

默认构造函数FVector()不会把成员清零,它走的是C++默认初始化,三个float是未定义值。这一点和很多人的直觉相反——很多人以为FVector()会得到一个零向量。实际上你要零向量得显式写:

FVector ZeroVector = FVector::ZeroVector;

FVector::ZeroVector是UE4提供的静态常量,等价于FVector(0, 0, 0)。类似的还有FVector::OneVector(1,1,1)、FVector::UpVector(0,0,1)、FVector::ForwardVector(1,0,0)、FVector::RightVector(0,1,0)。这些静态常量在写代码时非常常用,建议记住。

还有一种从其他类型转换的构造:

FVector FromInt(10, 20, 30); // 从int隐式转换 FVector FromDouble(10.0, 20.0, 30.0); // 从double转换,会有精度损失警告

UE4默认float是单精度,如果你传double进去,编译器可能会警告"conversion from 'double' to 'float' possible loss of data"。在UE4里写浮点数字面量,养成加f后缀的习惯,比如100.0f而不是100.0。

2.3 为什么不用构造函数清零

这里有一个设计上的取舍值得说。UE4选择让FVector()不做初始化,是为了性能。在大量临时向量的场景下(比如每帧计算几百个中间结果),多一次清零就多一次内存写入。UE4把这个责任交给开发者:你需要零向量就用ZeroVector,需要未初始化就用默认构造。这个思路在UE4的数学库里贯穿始终,FRotator、FTransform都是类似的处理。

注意:如果你写FVector MyVec;然后直接读MyVec.X,得到的是垃圾值。调试模式下可能看起来是0,但Release模式下可能是个极大的数。这个坑在刚学的时候特别容易踩,因为调试版和发布版行为不一致。

3. FVector的运算:不只是加减乘除

3.1 基础运算符重载

FVector重载了+、-、*、/、+=、-=等运算符,用起来和普通数字一样直观:

FVector A(1.0f, 2.0f, 3.0f); FVector B(4.0f, 5.0f, 6.0f); FVector Sum = A + B; // (5, 7, 9) FVector Diff = B - A; // (3, 3, 3) FVector Scaled = A * 2.0f; // (2, 4, 6) FVector Divided = B / 2.0f; // (2, 2.5, 3)

注意*和/只支持标量,不支持两个FVector相乘。两个向量相乘在数学上有两种:点积和叉积,UE4分别用DotProduct和CrossProduct函数实现,没有用运算符重载。这是有意为之——运算符重载太多会让代码可读性下降,A * B到底是点积还是叉积,看代码的人得去查文档。

3.2 点积:判断方向和夹角

点积的公式是A·B = |A||B|cosθ,结果是一个标量。在UE4里:

float Dot = FVector::DotProduct(A, B);

点积最常用的场景是判断两个向量的方向关系:

  • 点积大于0:两个向量夹角小于90度,大致同向
  • 点积等于0:两个向量垂直
  • 点积小于0:两个向量夹角大于90度,大致反向

比如你要判断敌人是否在角色前方,可以拿角色的前向向量和"角色指向敌人"的向量做点积,结果大于0就是在前方。这个技巧在AI感知、攻击判定里用得非常多。

如果A和B都是单位向量(长度为1),那么点积直接等于夹角的余弦值。这时候可以用FMath::Acos反推出夹角:

float AngleRad = FMath::Acos(Dot); // 弧度 float AngleDeg = FMath::RadiansToDegrees(AngleRad); // 角度

3.3 叉积:求垂直向量和判断左右

叉积的公式是A×B = |A||B|sinθ·n,结果是一个向量,方向垂直于A和B构成的平面,符合右手定则(UE4是左手系,但叉积的数学定义不变,只是视觉上的方向感需要适应)。

FVector Cross = FVector::CrossProduct(A, B);

叉积在实际项目里最常用的场景是:已知一个前向向量和一个上向向量,求右向向量。比如:

FVector Forward = GetActorForwardVector(); FVector Up = GetActorUpVector(); FVector Right = FVector::CrossProduct(Up, Forward); // 注意顺序

顺序很重要,Up × Forward和Forward × Up结果方向相反。在UE4的左手坐标系里,Up(0,0,1) × Forward(1,0,0)得到的是(0,1,0),也就是Y轴正方向,正好是"右"。

3.4 归一化与长度计算

向量的长度用Size():

float Len = A.Size(); // sqrt(X² + Y² + Z²)

如果只需要比较长度大小而不需要精确值,用SizeSquared()更高效,省去了开方:

float LenSq = A.SizeSquared(); if (LenSq > 10000.0f) { /* 距离超过100厘米 */ }

归一化用Normalize()或GetSafeNormal():

FVector Normalized = A.GetSafeNormal(); // 返回新向量,不修改A A.Normalize(); // 原地归一化,返回bool表示是否成功

GetSafeNormal()和Normalize()的区别在于对零向量的处理。Normalize()如果向量长度接近0,会返回false并且不修改原向量(或者修改成零向量,取决于版本)。GetSafeNormal()则会在长度过小时返回零向量,不会产生NaN。在实际项目里,我强烈建议用GetSafeNormal(),因为除以零产生的NaN会像瘟疫一样传播,一个NaN的位置会导致整个Actor消失或者物理系统崩溃,而且极难排查。

提示:GetSafeNormal()有一个可选参数Tolerance,默认是SMALL_NUMBER(约1e-8)。如果你的向量长度可能非常小但不为零,可以调大这个容差。

3.5 距离计算:Dist和DistSquared

两个点之间的距离:

float Dist = FVector::Dist(PointA, PointB); float DistSq = FVector::DistSquared(PointA, PointB);

同样,如果只是做距离比较(比如判断是否进入触发范围),用DistSquared和阈值的平方比较,避免开方运算。在每帧执行几百次的逻辑里,这个优化是有意义的。

4. 坐标系、旋转与FVector的配合

4.1 UE4的左手坐标系约定

UE4用的是左手坐标系:X轴向前,Y轴向右,Z轴向上。这和其他一些引擎(比如Unity的左手系但Y轴向上,或者OpenGL传统的右手系)不一样。这个约定直接影响你对向量方向的理解。

举个例子:FVector::ForwardVector是(1,0,0),FVector::RightVector是(0,1,0),FVector::UpVector是(0,0,1)。当你调用GetActorForwardVector()时,得到的就是Actor本地坐标系的X轴在世界空间中的方向。

很多从其他引擎转过来的人会习惯性地认为"前"是Z或者-Y,在UE4里会完全对不上。我的建议是:在纸上画一个坐标轴,标上X前、Y右、Z上,贴在显示器旁边,写代码的时候随时对照,一周就能形成肌肉记忆。

4.2 FRotator与FVector的转换

FRotator表示旋转,由Pitch(俯仰)、Yaw(偏航)、Roll(翻滚)三个角度组成。它和FVector之间可以互相转换:

FRotator Rotation = FRotator(0.0f, 90.0f, 0.0f); // Yaw旋转90度 FVector Direction = Rotation.Vector(); // 得到对应的前向单位向量

FRotator::Vector()返回的是旋转对应的前向单位向量。反过来,如果你有一个方向向量,想得到对应的旋转:

FVector Direction = FVector(1.0f, 1.0f, 0.0f).GetSafeNormal(); FRotator Rotation = Direction.Rotation();

FVector::Rotation()返回的FRotator的Roll始终为0,因为一个方向向量无法确定翻滚角。如果你需要完整的旋转(包括Roll),得用FMatrix或者FQuat。

这里有一个常见的坑:FRotator的构造函数参数顺序是(Pitch, Yaw, Roll),不是(X, Y, Z)。很多人会写成FRotator(90, 0, 0)以为是绕X轴转90度,实际上那是Pitch=90度,也就是抬头看天。这个顺序问题在调试旋转时经常让人困惑。

4.3 本地坐标与世界坐标的转换

Actor的GetActorLocation()返回的是世界坐标,GetActorForwardVector()返回的是世界空间中的前向方向。如果你有一个本地空间的偏移量,想转换成世界空间:

FVector LocalOffset(100.0f, 0.0f, 0.0f); // 本地前方100厘米 FVector WorldOffset = GetActorTransform().TransformVector(LocalOffset); FVector WorldLocation = GetActorLocation() + WorldOffset;

TransformVector会应用旋转和缩放,但不应用平移。如果你想要完整的变换(包括平移),用TransformPosition。

反过来,世界坐标转本地:

FVector WorldPoint = FVector(500.0f, 300.0f, 100.0f); FVector LocalPoint = GetActorTransform().InverseTransformPosition(WorldPoint);

这两个函数在实现"角色相对位置"的逻辑时非常关键,比如"在角色前方2米处生成一个特效",就得用本地偏移加变换。

5. 实战中FVector的典型用法与踩坑记录

5.1 移动逻辑:每帧更新位置

最简单的移动就是在Tick里累加位置:

void AMyActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); FVector DeltaMove(100.0f * DeltaTime, 0.0f, 0.0f); AddActorWorldOffset(DeltaMove); }

这里100.0f * DeltaTime保证每秒移动100厘米,和帧率无关。如果不乘DeltaTime,帧率越高移动越快,这是新手最常见的错误之一。

AddActorWorldOffset会触发碰撞检测和物理更新,比直接SetActorLocation更安全。如果你确定不需要碰撞,用SetActorLocation性能更好。

5.2 方向向量:从A指向B

求从A到B的方向:

FVector Direction = (TargetLocation - StartLocation).GetSafeNormal();

这个写法在AI追踪、子弹飞行、视线检测里到处都是。注意一定要GetSafeNormal(),如果A和B重合,不归一化会得到零向量,归一化会得到NaN。

5.3 插值:VInterpTo和Lerp

UE4提供了FMath::VInterpTo做平滑插值:

FVector Current = GetActorLocation(); FVector Target = FVector(1000.0f, 0.0f, 0.0f); FVector NewLocation = FMath::VInterpTo(Current, Target, DeltaTime, 5.0f); SetActorLocation(NewLocation);

最后一个参数是插值速度,越大越快到达目标。这个函数内部用的是指数衰减,不会过冲,适合做相机跟随、UI元素平滑移动。

FMath::Lerp则是线性插值:

FVector Result = FMath::Lerp(A, B, 0.5f); // A和B的中点

Lerp的第三个参数是Alpha,0返回A,1返回B。注意它不保证结果在A和B之间,如果Alpha超出0-1范围,会外推。

5.4 常见坑:浮点精度与NaN传播

前面提过NaN的问题,这里展开说。FVector的运算里,最容易产生NaN的操作是除以零和归一化零向量。一旦某个FVector的某个分量变成NaN,它参与的任何运算结果都是NaN。如果这个NaN被设置到Actor的位置上,Actor会直接从场景里消失,而且不会报错。

排查NaN的方法:在可疑的地方加检查:

if (MyVector.ContainsNaN()) { UE_LOG(LogTemp, Error, TEXT("Vector contains NaN!")); MyVector = FVector::ZeroVector; }

ContainsNaN()是FVector的成员函数,会检查三个分量是否有NaN。养成在关键位置加这个检查的习惯,能省下大量调试时间。

另一个坑是浮点比较。不要用==比较两个FVector:

// 错误做法 if (A == B) { ... } // 正确做法 if (A.Equals(B, 0.001f)) { ... }

Equals的第二个参数是容差。浮点运算的累积误差会让理论上相等的两个向量在最低位上有差异,直接用==可能永远为false。

5.5 性能细节:避免不必要的开方和拷贝

在Tick里做距离判断时,用DistSquared代替Dist。在循环里传FVector参数时,用const FVector&避免拷贝。这些优化单个看起来微不足道,但在每帧执行几千次的代码路径上,累积起来就是几毫秒的差距。

还有一个细节:FVector的operator*返回的是临时对象,如果连续做多次运算,比如A * 2.0f + B * 3.0f - C,会产生多个临时FVector。现代编译器一般能优化掉,但在性能敏感的代码里,可以手动展开成逐个分量的运算,减少临时对象。

6. 从FVector延伸出去:接下来该学什么

FVector搞明白之后,下一步自然是FRotator和FTransform。FRotator负责旋转,FTransform则是位置、旋转、缩放的组合,Actor的Transform就是FTransform类型。这三个结构构成了UE4空间数学的基础。

再往后是FMatrix和FQuat。FMatrix是4x4矩阵,用于复杂的空间变换组合;FQuat是四元数,用于避免万向节锁的旋转插值。不过在大多数游戏逻辑里,FVector加FRotator已经够用了,FMatrix和FQuat更多出现在渲染和动画系统里。

我个人在实际项目里的体会是:FVector的坑主要集中在"零向量归一化"和"坐标系方向搞反"这两件事上。前者靠GetSafeNormal和ContainsNaN检查解决,后者靠多画图、多打印实际向量值来建立直觉。刚开始写的时候,可以在Tick里用DrawDebugLine把关键向量画出来:

DrawDebugLine(GetWorld(), GetActorLocation(), GetActorLocation() + GetActorForwardVector() * 200.0f, FColor::Red, false, -1.0f, 0, 2.0f);

红色线画出前向,绿色画右向,蓝色画上向,一眼就能看出方向对不对。这个调试手段比看日志数字直观得多,建议每个刚学FVector的人都用起来。

最后分享一个小技巧:UE4的FVector在蓝图里也有对应类型,C++里算好的向量可以通过UPROPERTY(BlueprintReadWrite)暴露给蓝图,两边可以无缝配合。如果你不确定某个数学运算在C++里怎么写,可以先在蓝图里连一遍节点,然后看编译后的C++代码(UE4的蓝图可以nativize),这是一个很好的学习路径。

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

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

立即咨询