JavaScript

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

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

JavaScriptの勉強を兼ねてゲームを作り始めてみたのですが、そもそもゲームをまともに作ったことがなかったため、まずは当たり判定から勉強することにしました。

車輪の再生産的に当たり判定の実装を行うだけですが、勉強には結構いい気がします。

簡易な当たり判定

まずは2つの四角形オブジェクトで当たり判定を実装してみました。

通常当たり判定を行う場合は、ゲームキャラのような実際のオブジェクトの他に当たり判定用のオブジェクト(衝突器・Collider)を用意するのが一般的らしいです。

ゲームキャラのような複雑な形のオブジェクトの場合、当たり判定が難しくなってしまうので簡単な形の衝突器を用意して当たり判定を簡単にするためだそうです。

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

今回は実際のオブジェクトもただの四角形にしているためあまり意味はないのですが、例に習って衝突器込みで実装しています。

詳細な解説はコード内のコメントに記載しているのでそちらを見ていただければと思います。

150行くらいありますが、半分くらいはコメントなので実ステップは100行切るぐらいです。

サンプルコード

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

/** キャンバスの幅 */
const CANVAS_WIDTH = 100;
/** キャンバスの高さ */
const CANVAS_HEIGHT = 100;
/** オブジェクトが移動する速さ */
const OBJECT_SPEED = 2;

/** オブジェクトの色付けに使う関数 */
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) {
        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._collider = new RectangleCollider(this._width, this._height, x, y);
    }

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

    /**
     * フレーム毎に実行する更新処理
     * 今回は、壁との当たり判定とオブジェクトの位置の更新を行う
     */
    update() {
        /** 上下左右の壁との当たり判定 */
        if (this._x < 0 || this._x > (CANVAS_WIDTH - this._width)) {
            this._dx = -this._dx;
        }
        if (this._y < 0 || this._y > (CANVAS_HEIGHT - this._width)) {
            this._dy = -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);
    }

    /**
     * 衝突検出時に実行する処理
     * 今回は衝突したらY移動方向と色を変える
     */
    hit() {
        this._dy = -this._dy;
        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 rect1 = new RectangleObject(0, 0, 20, 20, context);
let rect2 = new RectangleObject(0, 50, 20, 20, context);

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

/** メインループ */
function draw() {
    /** オブジェクトの座標更新 */
    rect1.update();
    rect2.update();

    /** 当たり判定 */
    const hit = detector.detectCollision(rect1, rect2);
    if (hit) {
        rect1.hit();
        rect2.hit();
    }

    /** オブジェクトの描画処理 */
    context.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); /** 1フレーム前の描画をクリア */
    rect1.render();
    rect2.render();

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

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

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

実行結果

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


次に当たり判定の対象となるオブジェクトの数を増やしてみたいと思います。

コメント

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