iT邦幫忙

2022 iThome 鐵人賽

DAY 12
3
Modern Web

30個遊戲程設的錦囊妙計系列 第 12

Trick 11: 站在彈道上的女孩-圓與線的碰撞問題

  • 分享至 

  • xImage
  •  

射擊遊戲的程式需要一個很重要的功能-檢測彈道與角色是否發生碰撞的能力。
碰撞圓
有同學說,我把子彈用一個圓形圍起來,遊戲中的角色也用圓形圍起來,那麼只要檢查兩個圓形有沒有發生碰撞就好啦!兩個圓形的碰撞檢測很簡單,只要兩個圓的中心點距離小於他們的半徑和,就代表發生了碰撞。

但是如果子彈的速度夠快,那麼是不是有可能在遊戲每幀的更新中,漏接了他們相撞的事件,進而引發玩家摔鍵盤的慘案。
碰撞圓漏接
所以比較好的方式,是檢測上圖中角色的白色圓和虛線的彈道是否產生碰撞。

要完成這個檢測法,就需要介紹其中一個向量的運算-內積(dot product)。

內積的幾何意義

內積是兩個向量之間的運算式之一,計算出來的結果是一個數值,代表兩個向量投影在其中一個向量上的位置的乘積。
https://chart.googleapis.com/chart?cht=tx&chl=%5Cvec%7Ba%7D%20%5Ccdot%20%5Cvec%7Bb%7D%20%3D%20%5Cvec%7Ba%7D%20.x%20%5Ccdot%20%5Cvec%7Bb%7D%20.x%20%2B%20%5Cvec%7Ba%7D%20.y%20%5Ccdot%20%5Cvec%7Bb%7D%20.y

內積的計算方法,就是將兩個向量的x乘積再加上兩個向量的y乘積。

/** 計算兩個向量的內積 */
function vectorDot(a: Point, b: Point): number {
    return a.x * b.x + a.y * b.y;
}

那剛剛說內積的值等於兩個向量投影的位置乘積又是什麼意思呢?
dot product meaning
如上圖的兩個向量,向量a的長度為2.5,向量b的長度為4。如果兩個向量都投影到向量b,那就好比把向量b當成一條數線,向量a投影到這條數線上的位置刻度是2,而向量b投影在自己身上的刻度是4,兩者相乘就是8,這個8會相等於兩個向量內積的結果。

同學們是不是想說「原來內積是這個意思啊!可是這個跟圓與線的碰撞有什麼關係?」

圓與線段的碰撞

碰撞條件
上圖是一個圓與線的碰撞圖,線段AB是子彈經過的路徑,G是代表角色的圓心,半徑是r,以上是我們可以從遊戲中取得的資料。只要以下兩個條件都成立,那就代表碰撞發生了。

  1. G在AB上的投影(D)介於AB之間
  2. GD的長度小於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;
        }
    ...
}

CG示範專案


上一篇
Trick 10: 向量的旋轉原來要歪看正著
下一篇
Trick 12: 直男與硬漢的交點-兩條線段的碰撞問題
系列文
30個遊戲程設的錦囊妙計32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言