JavaScript

【Javascript】当たり判定を実装してみた

実際のオブジェクトはキャラクターのような複雑な形状だが、衝突器オブジェクトは四角形等の計算しやすい形状。衝突器が衝突していたら衝突とみなすことで複雑な形状の当たり判定を考える必要がなくなる。

オブジェクト数が多い当たり判定

先ほどは2オブジェクトでしたが、今度はオブジェクト数を100個に増やしてみました。

サンプルコード

<html>
<body>
<script type="text/javascript">

/** キャンバスの幅 */
const CANVAS_WIDTH = 500;
/** キャンバスの高さ */
const CANVAS_HEIGHT = 500;
/** オブジェクトが移動する速さ */
const OBJECT_SPEED = 5;
/** オブジェクトの幅 */
const OBJECT_WIDTH = 10;
/** オブジェクトの高さ */
const OBJECT_HEIGHT = 10;
/** オブジェクトの数の上限 */
const OBJECT_NUM_MAX = 100;

/** オブジェクトの色をランダムに取得する関数 */
function getRandomColor() {return Math.floor(Math.random() * 255);}


/** 四角形オブジェクト用の衝突器(Collider) */
class RectangleCollider {
    constructor(width, height, x, y, dx = 0, dy = 0) {
        this._width = width;
        this._height = height;

        /** オブジェクトと衝突器の相対座標(オブジェクトの当たり判定をずらしたい場合に使う) */
        this._dx = dx;
        this._dy = dy;

        this.update(x, y);
    }

    /** 上下左右の座標を取得するゲッター */
    get top() {return this._y;}
    get bottom() {return this._y + this._height;}
    get left() {return this._x;}
    get right() {return this._x + this._width;}

    /** 衝突器の座標を更新するメソッド */
    update(x, y) {
        this._x = x + this._dx;
        this._y = y + this._dy;
    }
}


/** 四角形オブジェクトのクラス */
class RectangleObject {
    constructor(x, y, width, height, context, colorFixed = false) {
        this._x = x;
        this._y = y;
        this._dx = OBJECT_SPEED;
        this._dy = OBJECT_SPEED;

        this._width = width;
        this._height = height;
        this._color = 'rgb(0, 0, 0)';
        this._context = context;

        this._colorFixed = colorFixed;

        /** 上で作った四角形オブジェクト用衝突器を設定 */
        this._collider = new RectangleCollider(this._width, this._height, x, y);
    }

    /** 衝突器のゲッター(座標を更新してから返す) */
    get collider() {
        this._collider.update(this._x, this._y);
        return this._collider;
    }

    get vector() {
        return Math.abs()
    }

    /**
     * フレーム毎に実行する更新処理
     * 今回は、壁との当たり判定とオブジェクトの位置の更新を行う
     * (この下で出てくる当たり判定器で壁の当たり判定も行うかどうかは複雑性・性能との相談)
     */
    update() {
        /** 上下左右の壁との当たり判定 */
        if (this._x < 0) {
            this._dx = Math.abs(this._dx);
        } else if (this._x > (CANVAS_WIDTH - this._width)) {
            this._dx = -Math.abs(this._dx);
        }
        if (this._y < 0) {
            this._dy = Math.abs(this._dy);
        } if (this._y > (CANVAS_HEIGHT - this._width)) {
            this._dy = -Math.abs(this._dy);
        }

        /** x,y座標の更新 */
        this._x += this._dx;
        this._y += this._dy;
    }

    /** フレーム毎に実行する描画処理 */
    render() {
        this._context.fillStyle = this._color;
        this._context.fillRect(this._x, this._y, this._width, this._height);
    }

    /**
     * 衝突検出時に実行する処理
     * 今回は衝突したら移動方向と色を変える
     */
    hit() {
        /** このまま進むと */
        this._dy = -this._dy;
        if (this._colorFixed == false) {
            this._color = 'rgb(' + getRandomColor() + ',' + getRandomColor() + ',' + getRandomColor() + ')';
        }
    }
}

/** 当たり判定器のクラス */
class CollisionDetector {
    /** 2オブジェクトの当たり判定処理 */
    detectCollision(obj1, obj2) {
        /** 衝突器を対応する当たり判定処理に渡す(今は四角形しかないけど) */
        return this._detectRectangleCollision(obj1.collider, obj2.collider);
    }

    /** 四角形オブジェクト同士の当たり判定処理 */
    _detectRectangleCollision(rect1, rect2) {
        const horizontal = (rect1.left < rect2.right) && (rect2.left < rect1.right);
        const vertical = (rect1.top < rect2.bottom) && (rect2.top < rect1.bottom); /** y軸の正方向が下向きな点に注意 */
        return (horizontal && vertical);
    }
}


/** canvasの設定 */
const canvas = document.createElement('canvas');
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
canvas.setAttribute('style', 'background-color:gray;');
/** HTMLのbodyにcanvasを追加 */
document.body.appendChild(canvas);

/** canvasを操作するためのコンテキストを取得 */
const context = canvas.getContext('2d');


/** 四角形オブジェクトのインスタンス生成 */
let objectArray = [];
for (let i = 0; i < OBJECT_NUM_MAX; i++) {
    let rect = new RectangleObject(i * 20, i * 20, OBJECT_WIDTH, OBJECT_HEIGHT, context);
    objectArray.push(rect);
}
/** 当たり判定の動きが分かり易いように色固定のサイズ違いを入れておく */
let rect1 = new RectangleObject(0, 0, OBJECT_WIDTH*2, OBJECT_HEIGHT*2, context, true);
objectArray.push(rect1);


/** 当たり判定器のインスタンス生成 */
let detector = new CollisionDetector();


/** メインループ */
function draw() {
    /** オブジェクトの座標更新 */
    for (let i = 0; i < objectArray.length; i++) {
        objectArray[i].update();
    }

    /** 当たり判定 */
    for (let i = 0; i < objectArray.length; i++) {
        const obj1 = objectArray[i];
        for (let j = 0; j < objectArray.length; j++) {
            // console.log(i, j);
            if (i <= j) {
                /** 同一オブジェクト、判定済みオブジェクトの場合は判定不要 */
                break;
            }
            const obj2 = objectArray[j];
            const hit = detector.detectCollision(obj1, obj2);
            // console.log(hit);
            if (hit) {
                obj1.hit();
                obj2.hit();
            }
        }
    }

    /** オブジェクトの描画処理 */
    context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); /** 1フレーム前の描画をクリア */
    for (let i = 0; i < objectArray.length; i++) {
        objectArray[i].render();
    }

    /** 次フレームの要求 */
    requestAnimationFrame(draw);
}

/** 描画開始 */
draw();

</script>
</body>
</html>

実行結果

これを実際に動かすと以下のようになります。

一応動いてはいますが、いくつか問題があります。
・衝突後の移動方向が、マイナスを付けているだけなのでカクカクした動きになっている
・3つ以上のオブジェクトが重なった場合に動作がおかしくなる場合がある
・1フレーム当たりの移動速度がオブジェクトのサイズを超過した場合に当たり判定が行われず貫通する


次回以降で上記の改善を調べていきたいと思います。

今回の記事は以上です。
お読みいただきありがとうございました。

参考

https://sbfl.net/blog/2017/12/03/javascript-collision/

https://hakuhin.jp/as/collide.html

コメント

タイトルとURLをコピーしました