ユーニックス総合研究所

  • home
  • archives
  • game-dev-escape-from-forward

衝突した対象と逆方向にベクトルを反転させる【ゲーム開発】

連続動作環境のゲーム開発で衝突したオブジェクトと反対方向に自分を移動させたい場合、衝突対象のオブジェクトと自分の座標の差分を取ることで反対方向へのベクトルを得られます。
この記事ではそのコードを解説します。

実行風景

以下がコードを実行したところです。

今回のサンプルではベクトル演算を使って連続動作環境で衝突判定を行い、衝突応答としてベクトルを反転させています。

グローバル変数

グローバル変数は以下になります。

const CW = 600  // キャンバスの横幅  
const CH = 500  // キャンバスの高さ  
var ct  // 2Dコンテキスト  
var circles = []  // 円の配列  
var collisionResponses = []  // 衝突応答の関数を登録する配列  
var circle1, circle2  // 円1、円2  

Vector3クラス

ベクトルを表すクラスは以下になります。

// ベクトルクラス  
class Vector3 {    
    constructor (x=0, y=0, z=0) {    
        this.x = x    
        this.y = y    
        this.z = z    
    }    

    // ベクトルをクローン(複製)する  
    clone () {  
        return new Vector3(this.x, this.y, this.z)  
    }  

    // ベクトルにベクトルを加算する  
    add (other) {    
        this.x += other.x    
        this.y += other.y    
        this.z += other.z    
        return this  
    }    

    // ベクトルにベクトルを減算する  
    sub (other) {    
        this.x -= other.x    
        this.y -= other.y    
        this.z -= other.z    
        return this  
    }    

    // ベクトルを正規化する  
    normalize () {    
        let m = Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z)    
        let tol = 0.001    
        if (m <= tol) m = 1    

        this.x /= m    
        this.y /= m    
        this.z /= m    

        if (Math.abs(this.x) < tol) this.x = 0    
        if (Math.abs(this.y) < tol) this.y = 0    
        if (Math.abs(this.z) < tol) this.z = 0    
        return this    
    }    
}    

このVector3はX座標、Y座標、Z座標を持つベクトルです。
このベクトルは円形の移動処理に使われます。
円形は座標を持っていますが、この座標にベクトルを加算することで座標を更新します。

    // ベクトルを正規化する  
    normalize () {    
        let m = Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z)    
        let tol = 0.001    
        if (m <= tol) m = 1    

        this.x /= m    
        this.y /= m    
        this.z /= m    

        if (Math.abs(this.x) < tol) this.x = 0    
        if (Math.abs(this.y) < tol) this.y = 0    
        if (Math.abs(this.z) < tol) this.z = 0    
        return this    
    }    

上記のnormalizeメソッドはベクトルを正規化します。
このメソッドはベクトルを次の方程式を満たす単位ベクトルにします。

|v| = √(x^2 + y^2 + z^2) = 1  

正規化されたベクトルの長さは1になります。

変数mはベクトルのマグニチュードでベクトルの大きさを表しています。
変数mの計算式は

|v| = √(x^2 + y^2 + z^2)  

になります。
mtol以下の場合はm1にします。
あとはベクトルの各座標をmで割るだけです。

Circleクラス

円形のクラスは以下になります。

class Circle {    
    constructor () {    
        this.pos = new Vector3()  // 座標  
        this.velocity = new Vector3()  // ベクトル  
        this.radius = 40  // 半径  
        this.color = '#00ff44'  // 色  
        this.speed = 1  // 速さ  
    }    

    update () {    
        // 画面外への衝突判定  
        if (this.pos.x < 0) {    
            this.velocity.x = this.speed    
        } else if (this.pos.x >= CW) {    
            this.velocity.x = -this.speed  
        }    
        if (this.pos.y < 0) {    
            this.velocity.y = this.speed  
        } else if (this.pos.y >= CH) {    
            this.velocity.y = -this.speed  
        }    

        // 他の円との衝突判定  
        for (const c of circles) {  
            if (c === this) {  
                continue  
            }  
            // 衝突検出  
            const isCollided = Math.abs(this.pos.x - c.pos.x) < this.radius &&  
                               Math.abs(this.pos.y - c.pos.y) < this.radius  
            if (isCollided) {  
                // 衝突応答を登録  
                collisionResponses.push(() => {  
                    // 加算したベクトル分を戻す  
                    this.pos.sub(this.velocity)  

                    // 衝突したcと反対方向のベクトルを取得  
                    const p = this.pos.clone().sub(c.pos)  

                    // 正規化  
                    p.normalize()  

                    // ベクトルを更新  
                    this.velocity = p                      
                })  
            }  
        }  

        // 座標にベクトルを加算  
        this.pos.add(this.velocity)    
    }    

    draw () {    
        // 円を描画  
        ct.beginPath()    
        ct.fillStyle = this.color  
        ct.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI*2, false)    
        ct.fill()    
    }    
}    

円形のクラスの更新処理はupdateメソッドで行います。
このメソッドではまず画面外にはみ出さないように衝突判定をします。

        // 画面外への衝突判定  
        if (this.pos.x < 0) {    
            this.velocity.x = this.speed    
        } else if (this.pos.x >= CW) {    
            this.velocity.x = -this.speed  
        }    
        if (this.pos.y < 0) {    
            this.velocity.y = this.speed  
        } else if (this.pos.y >= CH) {    
            this.velocity.y = -this.speed  
        }    

座標posが画面外に出ていたら対応するベクトルを反転させます。

        // 他の円との衝突判定  
        for (const c of circles) {  
            if (c === this) {  
                continue  
            }  
            // 衝突検出  
            const isCollided = Math.abs(this.pos.x - c.pos.x) < this.radius &&   
                               Math.abs(this.pos.y - c.pos.y) < this.radius  
            if (isCollided) {  
                // 衝突応答を登録  
                collisionResponses.push(() => {  
                    // 加算したベクトル分を戻す  
                    this.pos.sub(this.velocity)  

                    // 衝突したcと反対方向のベクトルを取得  
                    const p = this.pos.clone().sub(c.pos)  

                    // 正規化  
                    p.normalize()  

                    // ベクトルを更新  
                    this.velocity = p                      
                })  
            }  
        }  

上記は他の円と衝突判定をしているところです。
circlesには自分も含めた他の円が格納されています。
これと衝突判定をします。

    const isCollided = Math.abs(this.pos.x - c.pos.x) < this.radius &&   
                       Math.abs(this.pos.y - c.pos.y) < this.radius  

座標の差分の絶対値がthis.radius(円の半径)より小さければ衝突していると見なします。

    if (isCollided) {  
        // 衝突応答を登録  
        collisionResponses.push(() => {  
            // 加算したベクトル分を戻す  
            this.pos.sub(this.velocity)  

            // 衝突したcと反対方向のベクトルを取得  
            const p = this.pos.clone().sub(c.pos)  

            // 正規化  
            p.normalize()  

            // ベクトルを更新  
            this.velocity = p                      
        })  
    }  

他の円と衝突していたら衝突応答をcollisionResponseに登録します。
この配列は衝突応答の関数を格納する配列です。
衝突応答ではまず座標からベクトル分を減算します。

    // 加算したベクトル分を戻す  
    this.pos.sub(this.velocity)  

こうすることで前回のupdateで加算されたベクトル分を戻しています。
つまり、衝突対象とめり込まないようにしています。

    // 衝突したcと反対方向のベクトルを取得  
    const p = this.pos.clone().sub(c.pos)  

上記では自分の座標と他の円の座標との差分を取ったベクトルpを取得します。
こうすることで衝突対象との反対方向のベクトルを得ることができます。

    // 正規化  
    p.normalize()  

上記ではpを正規化します。
こうすることでベクトルpの長さを1にフィックスして、加算しやすい値にしています。

    // ベクトルを更新  
    this.velocity = p  

上記ではベクトルを更新します。

main関数

main関数は以下になります。

function main() {    
    // キャンバスの初期化  
    const canvas = document.querySelector('#game-canvas')     
    canvas.width = CW  
    canvas.height = CH  

    // 2Dコンテキストの取得  
    ct = canvas.getContext('2d')    

    // 円1  
    circle1 = new Circle()    
    circle1.pos.x = CW/2 - 100    
    circle1.pos.y = CH/2    
    circle1.velocity.x = 1    
    circle1.color = '#00ff44'  

    // 円2  
    circle2 = new Circle()    
    circle2.pos.x = CW/2 + 100    
    circle2.pos.y = CH/2    
    circle2.color = '#ff4400'  

    // 円1と円2を登録  
    circles.push(circle1)  
    circles.push(circle2)  

    setInterval(() => {    
        // 衝突応答をクリア  
        collisionResponses = []  

        // 更新  
        circle1.update()    
        circle2.update()    

        // 衝突応答を実行  
        for (const func of collisionResponses) {  
            func()  
        }  

        // 描画  
        ct.clearRect(0, 0, CW, CH)    
        circle1.draw()    
        circle2.draw()    
    }, 1000/60)    
}    

main()    

今回のJavaScriptのサンプルコードでは2Dコンテキストを使って絵を出しています。
circle1circle2に衝突してベクトルを反転させます。
ゲームループはsetInterval()で行っています。
この関数は第1引数の関数を第2引数のミリ秒間隔で連続して呼び出すものです。

おわりに

なにか参考になれば幸いです。