向量数学

前言

本教程是一个面向游戏开发的简短线性代数介绍。线性代数是研究向量及其用途的学科。向量在 2D 和 3D 开发中都有许多应用,Godot 对它们的应用也非常广泛。要成为一名优秀的游戏开发者,对向量数学的理解是必不可少的。

备注

本教程不是线性代数的正式教科书。我们将只关注它如何应用于游戏开发。要更全面地了解数学,请参见 https://www.khanacademy.org/math/linear-algebra

坐标系(2D)

在 2D 空间中,使用水平轴(x)和垂直轴(y)定义坐标。2D 空间中的特定位置会被写成一对值,如 (4, 3)

../../_images/vector_axis1.png

备注

如果你是计算机图形学的新手,可能会觉得很奇怪,y 轴的正方向朝下而不是向上(你在数学课上学到的就像那样)。然而,这在大多数计算机图形应用程序中是常见的。

2D 平面上的任何位置都可以用一对数字来表示。然而,我们也可以将位置 (4, 3) 看作是从 (0, 0) 点或原点出发的偏移。画一个箭头从原点指向点:

../../_images/vector_xy1.png

这便是一个向量。一个向量的背后蕴藏着大量信息:除了告诉我们点在 (4, 3) 之外,还可以形象化地用向量方向(或向量角度) θ 和向量大小(线段长度)m表示它。在上述例子中,箭头便是一个位置向量——它表示空间中相对于原点的位置。

关于向量有一点非常重要:向量仅表示相对方向和大小,没有向量所在位置的概念。下图中的两个向量是相同的:

../../_images/vector_xy2.png

这两个向量表示的都是从某起点开始向右 4 个单位、向下 3 个单位处的一个点。画在平面中的哪个位置是无所谓的,向量表示的始终都是相对方向和大小。

向量运算

表示向量的方法有两种(XY 坐标,以及角度和大小),不过方便起见,程序员通常使用的是坐标表示法。举个例子,Godot 中以屏幕左上角为原点,因此要将一个名为 Node2D 的 2D 节点放在向右 400 像素、向下 300 像素的位置,可以使用如下代码:

GDScriptC#

  1. $Node2D.position = Vector2(400, 300)
  1. var node2D = GetNode<Node2D>("Node2D");
  2. node2D.Position = new Vector2(400, 300);

Godot supports both Vector2 and Vector3 for 2D and 3D usage, respectively. The same mathematical rules discussed in this article apply to both types, and wherever we link to Vector2 methods in the class reference, you can also check out their Vector3 counterparts.

成员访问

向量的各个分量可以直接通过名称访问。

GDScriptC#

  1. # Create a vector with coordinates (2, 5).
  2. var a = Vector2(2, 5)
  3. # Create a vector and assign x and y manually.
  4. var b = Vector2()
  5. b.x = 3
  6. b.y = 1
  1. // Create a vector with coordinates (2, 5).
  2. var a = new Vector2(2, 5);
  3. // Create a vector and assign x and y manually.
  4. var b = new Vector2();
  5. b.X = 3;
  6. b.Y = 1;

向量加法

两个向量相加减时,会将对应分量进行加减:

GDScriptC#

  1. var c = a + b # (2, 5) + (3, 1) = (5, 6)
  1. var c = a + b; // (2, 5) + (3, 1) = (5, 6)

我们也可以通过在第一个向量的末尾加上第二个向量来直观地了解这一点:

../../_images/vector_add1.png

注意,做 a + b 的加法和 b + a 得到的结果是一样的。

标量乘法

备注

向量可以同时表示方向和幅度。而仅表示幅度的值被称作标量(Scalar) 。标量在 Godot 中使用 float 类。

向量可以乘以标量

GDScriptC#

  1. var c = a * 2 # (2, 5) * 2 = (4, 10)
  2. var d = b / 3 # (3, 6) / 3 = (1, 2)
  3. var e = d * -2 # (1, 2) * -2 = (-2, -4)
  1. var c = a * 2; // (2, 5) * 2 = (4, 10)
  2. var d = b / 3; // (3, 6) / 3 = (1, 2)
  3. var e = d * -2; // (1, 2) * -2 = (-2, -4)

../../_images/vector_mult1.png

备注

向量与正标量相乘不会改变它的方向,只会改变它的幅值。与负标量相乘得到的是方向相反的向量。这就是向量的缩放

实际应用

让我们看看向量加减的两种常见应用。

移动

向量可以表示具有大小和方向的任何量。典型的例子有:位置、速度、加速度、力。在这幅图像中,飞船第 1 步的位置向量为 (1,3),速度向量为 (2,1)。速度向量表示飞船每一步移动的距离。通过将速度加到当前位置,我们可以求出第 2 步的位置。

../../_images/vector_movement1.png

小技巧

速度测量的是单位时间内位置的变化。新的位置是通过在前一个位置上增加速度与所经过时间的积(这里假设为一个单位,即 1 秒)得到的。

常见的 2D 游戏中,速度的单位通常是像素每秒,你需要将其乘以 _process()_physics_process() 回调的 delta 参数(从上一帧开始经过的时间)。

指向目标

在这个场景中,你有一辆坦克,坦克希望让炮塔指向机器人。把机器人的位置减去坦克的位置就得到了从坦克指向机器人的向量。

../../_images/vector_subtract2.webp

小技巧

要找到从 A 指向 B 的向量,请使用 B - A

单位向量

大小1 的向量称为单位向量,有时也被称为方向向量法线。当你需要记录方向时就可以使用单位向量。

归一化

对向量进行归一化就是将其长度缩减到 1,但是保持方向不变。其方法是将每个分量除以其幅度。因为这是很常见的运算,所以 Godot 提供了专门的 normalized() 方法:

GDScriptC#

  1. a = a.normalized()
  1. a = a.Normalized();

警告

因为归一化的过程中需要除以向量的长度,所以无法对长度为 0 的向量进行归一化。尝试进行这样的操作一般会出错。不过在 GDScript 中对长度为 0 的向量调用 normalized() 不会对向量的值进行更改,不会报错。

反射

单位向量的一种常见用法是表示法线。法向量是垂直于表面的单位向量,定义了表面的方向。它们通常用于照明、碰撞和涉及表面的其他操作。

举个例子,假设我们有一个移动的球,我们想让它从墙上或其他物体上弹回来:

../../_images/vector_reflect1.png

对于水平表面而言,其法线向量通常指向上方,即 (0, -1) 。当球与表面碰撞时,我们可以根据小球碰撞时的运动方向(碰撞时的剩余运动量)和表面法线的关系计算其反射向量。在Godot中, Vector2 类的 bounce() 方法可以快速处理这一系列过程,将该方法与 :ref:`KinematicBody2D <class_KinematicBody2D>`相结合的示例代码如下:

GDScriptC#

  1. var collision: KinematicCollision2D = move_and_collide(velocity * delta)
  2. if collision:
  3. var reflect = collision.get_remainder().bounce(collision.get_normal())
  4. velocity = velocity.bounce(collision.get_normal())
  5. move_and_collide(reflect)
  1. KinematicCollision2D collision = MoveAndCollide(_velocity * (float)delta);
  2. if (collision != null)
  3. {
  4. var reflect = collision.GetRemainder().Bounce(collision.GetNormal());
  5. _velocity = _velocity.Bounce(collision.GetNormal());
  6. MoveAndCollide(reflect);
  7. }

点积

点积是向量数学中最重要的概念之一,但经常被误解。点积是对两个向量的操作,返回一个标量。与同时包含大小和方向的向量不同,标量值只有大小。

点积公式有两种常见形式:

../../_images/vector_dot1.png

以及

../../_images/vector_dot2.png

数学符号 ||A|| 代表向量 A 的幅值,而 _A_x 表示向量 Ax 分量。

不过在大多数情况下用内置的 dot() 方法更加方便。请注意,两个向量的顺序并不重要:

GDScriptC#

  1. var c = a.dot(b)
  2. var d = b.dot(a) # These are equivalent.
  1. float c = a.Dot(b);
  2. float d = b.Dot(a); // These are equivalent.

与单位向量一起使用时,点积是最有用的,这样第一个公式就可以简化到只有 cosθ。这意味着我们可以使用点积得到两个向量之间夹角的一些信息:

../../_images/vector_dot3.png

使用单位向量时,结果总是会在 -1(180°)和 1(0°)之间。

朝向

我们可以利用这个事实来检测一个物体是否朝向另一个物体。在下图中,玩家 P 正试图避开僵尸 AB 。假设僵尸的视野是 180° ,它们能看到玩家吗?

../../_images/vector_facing2.png

绿色箭头 fAfB 是僵尸的单位向量,代表僵尸的朝向;蓝色半圆代表其视野。对于僵尸 A,我们用 P - A 找到指向玩家的方向向量 AP 并进行归一化处理。不过,Godot 有一个辅助方法可以快速完成以上流程,叫做 direction_to。如果这个向量和面对的向量之间的角度小于 90°,那么僵尸就可以看到玩家。

示例代码如下:

GDScriptC#

  1. var AP = A.direction_to(P)
  2. if AP.dot(fA) > 0:
  3. print("A sees P!")
  1. var AP = A.DirectionTo(P);
  2. if (AP.Dot(fA) > 0)
  3. {
  4. GD.Print("A sees P!");
  5. }

叉积

和点积一样,叉积也是对两个向量的运算。但是,叉乘积的结果是一个方向与两个向量垂直的向量。它的大小取决于相对角度,如果两个向量是平行的,那么叉积的结果将是一个空向量。

../../_images/vector_cross1.png ../../_images/vector_cross2.png

叉积是这样计算的:

GDScriptC#

  1. var c = Vector3()
  2. c.x = (a.y * b.z) - (a.z * b.y)
  3. c.y = (a.z * b.x) - (a.x * b.z)
  4. c.z = (a.x * b.y) - (a.y * b.x)
  1. var c = new Vector3();
  2. c.X = (a.Y * b.Z) - (a.Z * b.Y);
  3. c.Y = (a.Z * b.X) - (a.X * b.Z);
  4. c.Z = (a.X * b.Y) - (a.Y * b.X);

在Godot中,你可以使用内置方法 :ref:`Vector3.cross() <class_Vector3_method_cross>`完成以上计算:

GDScriptC#

  1. var c = a.cross(b)
  1. var c = a.Cross(b);

注意:叉积在 2D 平面中没有数学定义。Vector2.cross() 是在 2D 向量计算中模拟 3D 叉积的常用方法。

备注

在叉积中,顺序很重要。a.cross(b)b.cross(a) 的结果不一样,会得到指向相反的向量。

法线计算

One common use of cross products is to find the surface normal of a plane or surface in 3D space. If we have the triangle ABC we can use vector subtraction to find two edges AB and AC. Using the cross product, AB × AC produces a vector perpendicular to both: the surface normal.

下面是一个计算三角形法线的函数:

GDScriptC#

  1. func get_triangle_normal(a, b, c):
  2. # Find the surface normal given 3 vertices.
  3. var side1 = b - a
  4. var side2 = c - a
  5. var normal = side1.cross(side2)
  6. return normal
  1. Vector3 GetTriangleNormal(Vector3 a, Vector3 b, Vector3 c)
  2. {
  3. // Find the surface normal given 3 vertices.
  4. var side1 = b - a;
  5. var side2 = c - a;
  6. var normal = side1.Cross(side2);
  7. return normal;
  8. }

指向目标

在上面的点积部分,我们看到如何用它来查找两个向量之间的角度。然而在 3D 中,这些信息还不够。我们还需要知道在围绕什么轴旋转。我们可以通过计算当前面对的方向和目标方向的叉积来查找。由此得到的垂直向量就是旋转轴。

更多信息

有关在 Godot 中使用向量数学的更多信息,请参阅以下文章: