向量數學

前言

本教學簡要且實用地介紹了線性代數在遊戲開發中的應用。線性代數是研究向量及其用途的學科。向量在 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) 外,也可以將它視為角度 θ``(theta)與長度(或稱幅度)``m。在這個例子中,這個箭頭就是一個**位置向量**—代表相對於原點的空間位置。

關於向量,非常重要的一點是:它們僅代表**相對**方向和大小,並沒有“向量本身的位置”這個概念。下列兩個向量是完全相同的:

../../_images/vector_xy2.png

這兩個向量都代表從某一個起點往右 4 單位、往下 3 單位的位置。你在平面上哪邊畫這個向量都一樣,它始終代表相對的方向和大小。

向量運算

你可以用 x、y 座標或角度和長度來表示一個向量,但為了方便,程式設計師大多使用座標表示法。例如,在 Godot 中,螢幕左上角是原點。若要將一個名為 Node2D 的 2D 節點放在向右 400 像素、向下 300 像素的位置,請用以下程式碼:

$Node2D.position = Vector2(400, 300)

Godot 分別提供 Vector2Vector3 來處理 2D 與 3D 向量。本文討論的數學規則對這兩種型別都適用。只要在類別參考中連結到 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

向量相加

當兩個向量相加或相減時,各分量會一一相加或相減:

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

我們也可以用圖像方式理解:把第二個向量接在第一個向量的尾端。

../../_images/vector_add1.png

注意,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)
../../_images/vector_mult1.png

備註

向量乘以正的標量時,方向不變,只會改變大小。若乘以負的標量,則方向會相反。這就是**縮放**向量的方法。

實務應用

來看看向量加減法兩個常見的應用場景。

移動

向量可以表示**任何**有大小和方向的量,常見例子有:位置、速度、加速度、力等。在這個例子中,第一步驟時太空船的位置向量為 (1, 3),速度向量為 (2, 1)。速度向量表示每一步太空船移動的距離。只要將速度加到當前位置,就能算出下一步的位置。

../../_images/vector_movement1.png

小訣竅

速度代表單位時間內位置的**變化量**。新位置可以用「前一位置 + 速度 × 經過時間」(這裡假設經過 1 單位時間,例如 1 秒)來計算。

在典型的 2D 遊戲場景中,你通常會有以每秒像素為單位的速度,然後乘上 _process()_physics_process() 回呼中傳入的 delta 參數(也就是自上一影格以來經過的時間)。

指向目標

在這個情境下,你有一輛坦克要把炮塔對準一個機器人。用機器人的位置減去坦克的位置,就能得到從坦克指向機器人的向量。

../../_images/vector_subtract2.webp

小訣竅

要找到從 A 指向 B 的向量,只要用 B - A 即可。

單位向量

大小為 1 的向量稱作**單位向量**,有時也稱**方向向量**或**法線**。當你只需要記錄方向時,單位向量非常有用。

正規化

正規化 一個向量,是指將其長度縮減到 1 ,但方向不變。做法就是將每個分量都除以該向量的大小。由於這操作很常見,Godot 提供了 normalized() 這個專用方法:

a = a.normalized()

警告

因為正規化必須除以向量的長度,所以無法對長度為 0 的向量進行正規化。通常這麼做會造成錯誤。不過在 GDScript 中,對長度為 0 的向量呼叫 normalized() 方法時,會直接返回原值,不會丟出錯誤。

反射

單位向量的一個常見用途是表示**法線**。法線向量是與表面垂直的單位向量,定義了表面的方向。它們常被用在光照、碰撞判斷,以及其他涉及表面的運算。

舉例來說,假如我們有一個移動的球要讓它從牆壁或其他物件反彈:

../../_images/vector_reflect1.png

這是一個水平表面,因此法線向量為 (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)

點積

點積**是向量數學中非常重要、但經常被誤解的概念之一。點積是針對兩個向量運算,結果會得到一個**標量值。和同時有大小與方向的向量不同,標量只有大小。

點積有兩種常見寫法:

../../_images/vector_dot1.png

../../_images/vector_dot2.png

數學符號 ||A|| 表示向量 A 的大小,Ax 表示向量 A 的 x 分量。

不過在大多數情況下,直接用內建的 dot() 方法最方便。注意,兩個向量的順序不影響結果:

var c = a.dot(b)
var 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 算出指向玩家的方向向量,再進行正規化(Godot 有 direction_to() 輔助方法)。若這個向量和朝向向量的夾角小於 90°,殭屍就能看到玩家。

程式實作範例如下:

var AP = A.direction_to(P)
if AP.dot(fA) > 0:
    print("A sees P!")

外積

和點積類似, 外積 (cross product)也是兩個向量的運算。但外積的結果是一個同時垂直於兩個原始向量的向量,其大小取決於兩者的夾角。若兩個向量平行,則外積的結果會是零向量。

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

外積的計算公式如下:

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)

在 Godot 裡,可以使用內建的 Vector3.cross() 方法:

var c = a.cross(b)

外積在數學上沒有針對 2D 定義。Godot 的 Vector2.cross() 方法則是一種常見的 2D 外積類比運算。

備註

外積運算順序會影響結果。a.cross(b) 的結果與 b.cross(a) 會相反,兩者指向**相反方向**。

計算法線

外積一個常見用途是在 3D 空間裡求平面或曲面的法線向量。假設有三角形 ABC,可以用向量減法算出兩條邊 ABAC,接著用外積 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

指向目標

在上面的點積說明中,我們已經看到可以藉由點積計算兩個向量的夾角。然而在 3D 空間中,這還不足夠。你還需要知道應該繞哪個軸旋轉。這時可以用目前朝向與目標方向的外積來計算,所得垂直向量就是旋轉軸。

更多資訊

想進一步瞭解 Godot 中的向量數學應用,請參考下列文章: