射擊遊戲的程式需要一個很重要的功能-檢測彈道與角色是否發生碰撞的能力。
有同學說,我把子彈用一個圓形圍起來,遊戲中的角色也用圓形圍起來,那麼只要檢查兩個圓形有沒有發生碰撞就好啦!兩個圓形的碰撞檢測很簡單,只要兩個圓的中心點距離小於他們的半徑和,就代表發生了碰撞。
但是如果子彈的速度夠快,那麼是不是有可能在遊戲每幀的更新中,漏接了他們相撞的事件,進而引發玩家摔鍵盤的慘案。
所以比較好的方式,是檢測上圖中角色的白色圓和虛線的彈道是否產生碰撞。
要完成這個檢測法,就需要介紹其中一個向量的運算-內積(dot product)。
內積是兩個向量之間的運算式之一,計算出來的結果是一個數值,代表兩個向量投影在其中一個向量上的位置的乘積。
內積的計算方法,就是將兩個向量的x乘積再加上兩個向量的y乘積。
/** 計算兩個向量的內積 */
function vectorDot(a: Point, b: Point): number {
return a.x * b.x + a.y * b.y;
}
那剛剛說內積的值等於兩個向量投影的位置乘積又是什麼意思呢?
如上圖的兩個向量,向量a的長度為2.5,向量b的長度為4。如果兩個向量都投影到向量b,那就好比把向量b當成一條數線,向量a投影到這條數線上的位置刻度是2,而向量b投影在自己身上的刻度是4,兩者相乘就是8,這個8會相等於兩個向量內積的結果。
同學們是不是想說「原來內積是這個意思啊!可是這個跟圓與線的碰撞有什麼關係?」
上圖是一個圓與線的碰撞圖,線段AB是子彈經過的路徑,G是代表角色的圓心,半徑是r,以上是我們可以從遊戲中取得的資料。只要以下兩個條件都成立,那就代表碰撞發生了。
我們拉一條輔助線AG,然後把向量AB和向量AG用內積的方法投影到向量AB。依照內積的涵義,A就是數線AB的原點,刻度是0,B的刻度等於向量AB的長度,而內積的結果就是D的刻度乘以B的刻度,換句話說,D的刻度就等於內積除以向量AB的長度。
有了D的刻度就可以進行第一項條件檢測,只要D的刻度介於0和B的刻度,碰撞的第一個條件就成立了。
接下來由於AD的長度等於D的刻度,向量AG的長度也能算出來,再利用畢氏定理就能算出GD的長度,於是第二項條件也就能夠順利檢測了。
/** 撰寫一個線段與圓的碰撞檢測函式 */
function lineCircleHittest(
// 第一個參數「線段」: 包含一個起點與一個終點
line: {start: Point, end: Point},
// 第二個參數「圓」: 包含圓心點與半徑
circle: {center: Point, radius: number}
): boolean {
// 首先建立向量AB = 線的終點 - 線的起點
let AB = line.end.sub(line.start);
// 先計算一下AB長度
let ABLength = AB.length;
// 再建立向量AG = 圓心 - 線的起點
let AG = circle.center.sub(line.start);
// 計算B的刻度 = AB長度
let BonAB = ABLength;
// 計算D的刻度 = AB.AG / AB長度
let DonAB = vectorDot(AB, AG) / ABLength;
// 進行第一項條件檢測, 0 < D的刻度 < B的刻度
if (0 < DonAB && DonAB < BonAB) {
// 計算AG長度
let AGLength = AG.length;
// 計算GD長度 = √AG*AG-D*D
let GDLength = Math.sqrt(AGLength * AGLength - DonAB * DonAB);
// 進行第二項條件檢測
if (GDLength < circle.r) {
// 發生碰撞了
return true;
}
}
// 沒發生碰撞
return false;
}
謝謝幾何數學的內積,賜給我們遊戲的力量!
如果還要更進一步檢測圓與一個有體積的線段,也就是給線段一個粗細的參數,那麼很簡單,我們只要在進行第二項條件檢測時,將圓的半徑與線段粗細的一半相加,使用這個相加的距離條件和GD長度相比就行了。
/** 撰寫一個線段與圓的碰撞檢測函式 */
function lineCircleHittest(
// 第一個參數「線段」: 再加上粗細(thickness)的參數
line: {start: Point, end: Point, thickness: number},
...
// 進行第二項條件檢測的時候,要將圓的半徑與線條粗細的一半相加
if (GDLength < circle.r + line.thickness / 2) {
// 發生碰撞了
return true;
}
...
}