Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

向量數學

前言

本教學簡要且實用地介紹了線性代數在遊戲開發中的應用。線性代數是研究向量及其用途的學科。向量在 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 中的向量數學應用,請參考下列文章: