向量数学
前言
本教程是一个面向游戏开发的简短线性代数介绍。线性代数是研究向量及其用途的学科。向量在 2D 和 3D 开发中都有许多应用,Godot 对它们的应用也非常广泛。要成为一名优秀的游戏开发者,对向量数学的理解是必不可少的。
备注
本教程不是线性代数的正式教科书。我们将只关注它如何应用于游戏开发。要更全面地了解数学,请参见 https://www.khanacademy.org/math/linear-algebra
坐标系(2D)
在 2D 空间中,使用水平轴(x
)和垂直轴(y
)定义坐标。2D 空间中的特定位置会被写成一对值,如 (4, 3)
。

备注
如果你是计算机图形学的新手,可能会觉得很奇怪,y
轴的正方向朝下而不是向上(像你在数学课上学到的那样)。然而,这在大多数计算机图形应用程序中很常见。
2D 平面上的任何位置都可以通过一对数字来标识。但是,我们也可以将位置 (4, 3)
视为从 (0, 0)
点或原点的偏移。从原点画一个指向该点的箭头:

这是一个向量。向量代表了很多有用的信息。除了告诉我们该点位于 (4, 3)
之外,我们还可以将其视为角度 θ
(西塔)和长度(即幅度)m
。在这种情况下,箭头是一个位置向量——它表示空间中相对于原点的位置。
关于向量有一点非常重要:向量仅表示相对方向和大小,没有向量所在位置的概念。下图中的两个向量是相同的:

这两个向量表示的都是从某起点开始向右 4 个单位、向下 3 个单位处的一个点。画在平面中的哪个位置是无所谓的,向量表示的始终都是相对方向和大小。
向量运算
表示向量的方法有两种(XY 坐标,以及角度和大小),不过方便起见,程序员通常使用的是坐标表示法。举个例子,Godot 中以屏幕左上角为原点,因此要将一个名为 Node2D
的 2D 节点放在向右 400 像素、向下 300 像素的位置,可以使用如下代码:
$Node2D.position = Vector2(400, 300)
var node2D = GetNode<Node2D>("Node2D");
node2D.Position = new Vector2(400, 300);
Godot 支持分别用于 2D 和 3D 用途的 Vector2 和 Vector3。本文中讨论的相同数学规则适用于这两种类型,并且无论我们在类参考中链接到 Vector2
方法,你也可以查看它们的 Vector3
对应项。
成员访问
向量的各个分量可以直接通过名称访问。
# Create a vector with coordinates (2, 5).
var a = Vector2(2, 5)
# Create a vector and assign x and y manually.
var b = Vector2()
b.x = 3
b.y = 1
// Create a vector with coordinates (2, 5).
var a = new Vector2(2, 5);
// Create a vector and assign x and y manually.
var b = new Vector2();
b.X = 3;
b.Y = 1;
向量加法
两个向量相加减时,会将对应分量进行加减:
var c = a + b # (2, 5) + (3, 1) = (5, 6)
var c = a + b; // (2, 5) + (3, 1) = (5, 6)
我们也可以通过在第一个向量的末尾加上第二个向量来直观地了解这一点:

注意,做 a + b
的加法和 b + a
得到的结果是一样的。
标量乘法
备注
向量可以同时表示方向和幅度。而仅表示幅度的值被称作标量。标量在 Godot 中使用 float 类。
向量可以乘以标量:
var c = a * 2 # (2, 5) * 2 = (4, 10)
var d = b / 3 # (3, 6) / 3 = (1, 2)
var e = d * -2 # (1, 2) * -2 = (-2, -4)
var c = a * 2; // (2, 5) * 2 = (4, 10)
var d = b / 3; // (3, 6) / 3 = (1, 2)
var e = d * -2; // (1, 2) * -2 = (-2, -4)

备注
向量与正标量相乘不会改变它的方向,只会改变它的幅度值。与负标量相乘得到的是方向相反的向量。这就是向量的缩放。
实际应用
让我们看看向量加减的两种常见用途。
移动
向量可以表示具有大小和方向的任何量。典型的例子有:位置、速度、加速度、力。在这幅图像中,飞船第 1 步的位置向量为 (1,3)
,速度向量为 (2,1)
。速度向量表示飞船每一步移动的距离。通过将速度加到当前位置,我们可以求出第 2 步的位置。

小技巧
速度测量的是单位时间内位置的变化。新的位置是通过在前一个位置上增加速度与所经过时间的积(这里假设为一个单位,即 1 秒)得到的。
常见的 2D 游戏中,速度的单位通常是像素每秒,你需要将其乘以 _process() 或 _physics_process() 回调的 delta
参数(从上一帧开始经过的时间)。
指向目标
在这个场景中,你有一辆坦克,坦克希望让炮塔指向机器人。把机器人的位置减去坦克的位置就得到了从坦克指向机器人的向量。

小技巧
要找到从 A
指向 B
的向量,请使用 B - A
。
单位向量
大小为 1
的向量称为单位向量,有时也被称为方向向量或法线。当你需要记录方向时就可以使用单位向量。
归一化
对向量进行归一化就是将其长度缩减到 1
,但是保持方向不变。其方法是将每个分量除以其幅度。因为这是很常见的运算,所以 Godot 提供了专门的 normalized() 方法:
a = a.normalized()
a = a.Normalized();
警告
因为归一化的过程中需要除以向量的长度,所以无法对长度为 0
的向量进行归一化。尝试进行这样的操作一般会出错。不过在 GDScript 中对长度为 0 的向量调用 normalized()
不会对向量的值进行更改,不会报错。
反射
单位向量的一种常见用法是表示法线。法向量是垂直于表面的单位向量,定义了表面的方向。它们通常用于照明、碰撞和涉及表面的其他操作。
举个例子,假设我们有一个移动的球,我们想让它从墙上或其他物体上弹回来:

由于这是一个水平表面,因此表面法线的值为 (0, -1)
。当球发生碰撞时,我们会取其剩余运动(撞击表面时剩余的运动量)并使用法线反射。在 Godot 中,有一个 bounce() 方法来处理这个问题。以下是上图使用 CharacterBody2D 的代码示例:
var collision: KinematicCollision2D = move_and_collide(velocity * delta)
if collision:
var reflect = collision.get_remainder().bounce(collision.get_normal())
velocity = velocity.bounce(collision.get_normal())
move_and_collide(reflect)
KinematicCollision2D collision = MoveAndCollide(_velocity * (float)delta);
if (collision != null)
{
var reflect = collision.GetRemainder().Bounce(collision.GetNormal());
_velocity = _velocity.Bounce(collision.GetNormal());
MoveAndCollide(reflect);
}
点积
点积是向量数学中最重要的概念之一,但经常被误解。点积是对两个向量的操作,返回一个标量。与同时包含大小和方向的向量不同,标量值只有大小。
点积公式有两种常见形式:

以及

数学符号 ||A|| 代表向量 A
的幅值,而 Ax 表示向量 A
的 x
分量。
不过在大多数情况下用内置的 dot() 方法更加方便。请注意,两个向量的顺序并不重要:
var c = a.dot(b)
var d = b.dot(a) # These are equivalent.
float c = a.Dot(b);
float d = b.Dot(a); // These are equivalent.
与单位向量一起使用时,点积是最有用的,这样第一个公式就可以简化到只有 cos(θ)
。这意味着我们可以使用点积得到两个向量之间夹角的一些信息:

使用单位向量时,结果总是会在 -1
(180°)和 1
(0°)之间。
朝向
我们可以利用这个事实来检测一个物体是否朝向另一个物体。在下图中,玩家 P
正试图避开僵尸 A
和 B
。假设僵尸的视野是 180° ,它们能看到玩家吗?

绿色箭头 fA
和 fB
是僵尸的单位向量,代表僵尸的朝向;蓝色半圆代表其视野。对于僵尸 A
,我们用 P - A
找到指向玩家的方向向量 AP
并进行归一化处理。不过,Godot 有一个辅助方法可以快速完成以上流程,名为 direction_to()。如果该向量和面对的向量之间的角度小于 90°,那么僵尸就可以看到玩家。
示例代码如下:
var AP = A.direction_to(P)
if AP.dot(fA) > 0:
print("A sees P!")
var AP = A.DirectionTo(P);
if (AP.Dot(fA) > 0)
{
GD.Print("A sees P!");
}
叉积
和点积一样,叉积也是对两个向量的运算。但是,叉乘积的结果是一个方向与两个向量垂直的向量。它的大小取决于相对角度,如果两个向量是平行的,那么叉积的结果将是一个空向量。


叉积是这样计算的:
var c = Vector3()
c.x = (a.y * b.z) - (a.z * b.y)
c.y = (a.z * b.x) - (a.x * b.z)
c.z = (a.x * b.y) - (a.y * b.x)
var c = new Vector3();
c.X = (a.Y * b.Z) - (a.Z * b.Y);
c.Y = (a.Z * b.X) - (a.X * b.Z);
c.Z = (a.X * b.Y) - (a.Y * b.X);
在 Godot 中,你可以使用内置方法 Vector3.cross() 完成以上计算:
var c = a.cross(b)
var c = a.Cross(b);
注意:叉积在 2D 平面中没有数学定义。Vector2.cross() 是在 2D 向量计算中模拟 3D 叉积的常用方法。
备注
在叉积中,顺序很重要。a.cross(b)
和 b.cross(a)
的结果不一样,会得到指向相反的向量。
法线计算
叉积的一个常见用途是找到 3D 空间中平面或曲面的表面法线向量。如果有三角形 ABC
,我们可以使用向量减法来找到两条边 AB
和 AC
。使用叉积,AB × AC
会产生一个垂直于两者的向量:即表面法线。
下面是一个计算三角形法线的函数:
func get_triangle_normal(a, b, c):
# Find the surface normal given 3 vertices.
var side1 = b - a
var side2 = c - a
var normal = side1.cross(side2)
return normal
Vector3 GetTriangleNormal(Vector3 a, Vector3 b, Vector3 c)
{
// Find the surface normal given 3 vertices.
var side1 = b - a;
var side2 = c - a;
var normal = side1.Cross(side2);
return normal;
}
指向目标
在上面的点积部分,我们看到如何用它来查找两个向量之间的角度。然而在 3D 中,这些信息还不够。我们还需要知道在围绕什么轴旋转。我们可以通过计算当前面对的方向和目标方向的叉积来查找。由此得到的垂直向量就是旋转轴。
更多信息
有关在 Godot 中使用向量数学的更多信息,请参阅以下文章: