JavaScript 大量のオブジェクト同士で衝突判定

大量の粒子が各々移動し、他の粒子と接触したら反発する方向へ弾ける、という動作で。

![movie.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618708/b5ded5d0-c8e1-aae6-3dad-43108ba31921.gif)

**[実働サンプル](https://akebi.jp/temp/collision_detection/)**

接触判定方法としては四分木空間分割などが速いそうですが、今回はとりあえずX軸座標をキーとしたバッファにオブジェクト番号リストを作成して、自分自身の軸上に存在する他のオブジェクト番号を逆引きする単純な方法で。

###スクリプト###

~~~html:index.html
<!DOCTYPE html>
<html lang='ja'>
    <head>
        <meta http-equiv='Content-Type' content='text/html; charset=utf-8'/>
        <meta name='viewport' content='user-scalable=no,width=device-width,initial-scale=1'/>
        <title>オブジェクト間衝突判定</title>
        <style>
            #c1 {
                position: fixed;
                left: 0px;
                top: 0px;
            }

            #fps {
                position: fixed;
                color: #ffffff;
            }

            #info {
                position: fixed;
                color: #ffffff;
                width: 100%;
                bottom: 32px;
                opacity: 0.6;
            }

            input[type='range'] {
                width: 95%;
            }
        </style>
    </head>
    <body>
        <canvas id='c1'></canvas>
        <div id='info'>
            <div id='fps'></div>
            <br/>
            <br/>
            粒子数(<span id='vnum'></span>)<br/>
            <input type='range' id='num' min='1' max='10000' value='200' oninput="chgv(this, 'vnum');" onchange='numUpdate(this.value)'>
            <br/>
            粒子サイズ(<span id='vs'></span>)<br/>
            <input type='range' id='s' min='1' max='10' value='5' step='0.1' oninput="chgv(this, 'vs');">
            <br/>
            移動方向揺らぎ(<span id='vb'></span>)<br/>
            <input type='range' id='b' min='0' max='360' value='180' step='0.1' oninput="chgv(this, 'vb');">
            <br/>
            接触時反発速度(<span id='va'></span>)<br/>
            <input type='range' id='a' min='10' max='50' value='10' step='0.1' oninput="chgv(this, 'va');">
            <br/>
            減速度(<span id='vd'></span>)<br/>
            <input type='range' id='d' min='1' max='5' value='1.05' step='0.01' oninput="chgv(this, 'vd');">
            <br/>
            最低速度(<span id='vl'></span>)<br/>
            <input type='range' id='l' min='0' max='5' value='0.2' step='0.02' oninput="chgv(this, 'vl');">
            <br/>
            <label>
                常時表示<input type='checkbox' id='v'>
            </label>
        </div>
        <script src='./main.js'></script>
    </body>
</html>
~~~

~~~js:main.js
const canvas = document.getElementById('c1');
const ctx = canvas.getContext('2d');

const p2 = Math.PI * 2;
const px = p2 / 360;

const pos = {};

let num = 200;
let ss = 5;

let fps = 0;

setv('num', 'vnum');
setv('s', 'vs');
setv('b', 'vb');
setv('a', 'va');
setv('d', 'vd');
setv('l', 'vl');

resizeCanvas(1);
moveObjects();

window.onresize = function() {
    resizeCanvas();
}

function resizeCanvas(n) {
    canvas.width = document.documentElement.clientWidth;
    canvas.height = document.documentElement.clientHeight;

    if (n) {
        for (let i = 0; i < num; i++) {
            pos[i] = {
                x: Math.random() * canvas.width,
                y: Math.random() * canvas.height,
                k: Math.random() * 360,
                s: 1,
            };
        }
    }
}

function numUpdate(n) {
    for (let i = 0; i < n; i++) {
        if (pos[i] === undefined) {
            pos[i] = {
                x: Math.random() * canvas.width,
                y: Math.random() * canvas.height,
                k: Math.random() * 360,
                s: 1,
            };
        }
    }
    num = n;
}

setInterval(function() {
    document.querySelector('#fps').textContent = fps + 'fps';
    fps = 0;
}, 1000);

function moveObjects() {
    // 描画
    ctx.beginPath();
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const sb = document.querySelector('#b').value;
    const sd = document.querySelector('#d').value;
    const sl = document.querySelector('#l').value;
    const sa = document.querySelector('#a').value;
    ss = document.querySelector('#s').value;
    const ssh = ss / 2;
    const ssl = Math.ceil(ss);

    const s = {};

    // 移動
    for (let i = 0; i < num; i++) {
        const _ = pos[i];
        // 方向揺らぎ
        _.k += Math.random() * sb - (sb / 2);

        // 移動
        const p = ((_.k + 270) % 360) * px;
        _.x += Math.cos(p) * _.s;
        _.y += Math.sin(p) * _.s;

        // はみ出し時処理
        if (_.x < 0 || _.x > canvas.width) {
            _.k = -_.k + 360;
            const p = ((_.k + 270) % 360) * px;
            _.x += (Math.cos(p) * _.s) * 2;
            _.y += (Math.sin(p) * _.s) * 2;
            if (_.x > canvas.width)
                _.x = canvas.width;
        }
        if (_.y < 0 || _.y > canvas.height) {
            _.k = -_.k + 540;
            const p = ((_.k + 270) % 360) * px;
            _.x += (Math.cos(p) * _.s) * 2;
            _.y += (Math.sin(p) * _.s) * 2;
            if (_.y > canvas.height)
                _.y = canvas.height;
        }

        // 減速
        if (_.s > sl)
            _.s /= sd;
        if (_.s < sl)
            _.s = sl;

        // X軸バッファへオブジェクト番号追加
        for (let h = _.x - ssl; h <= _.x + ssl; h++) {
            if (h >= 0 && h < canvas.width) {
                const ax = h << 0;
                if (s[ax] === undefined) {
                    s[ax] = [i];
                } else {
                    s[ax].push(i);
                }
            }
        }
    }

    for (let i = 0; i < num; i++) {
        // 衝突判定
        const _ = pos[i];
        const ax = _.x << 0;
        // X軸バッファ取得
        let c = (s[ax] !== undefined) ? s[ax] : [];
        // バッファ内に自分自身以外のオブジェクトがある場合
        if (c.length > 1) {
            // バッファからオブジェクト番号取得
            for (let j = 0; j < c.length; j++) {
                const l = c[j];
                // 相手オブジェクトとのY軸距離がオブジェクトの直径以下
                if (l !== i && Math.abs(pos[l].y - _.y) <= ss) {
                    // オブジェクト間距離がオブジェクトの直径以下
                    if (Math.sqrt(Math.pow(pos[l].x - _.x, 2) + Math.pow(pos[l].y - _.y, 2)) <= ss) {
                        // 相手オブジェクトに対して反発方向へ角度設定
                        const at = Math.atan2(pos[l].y - _.y, pos[l].x - _.x);
                        _.k = ((at * (360 / (Math.PI * 2))) + 90) % 360;
                        // 反発速度
                        _.s = sa / 2 + Math.random() * sa;
                        _.c = '#ffffff';
                        break;
                    }
                }
            }
        }
        // 描画
        ctx.beginPath();
        if (_.c !== undefined) {
            ctx.strokeStyle = _.c;
            delete (_.c);
        } else
            ctx.strokeStyle = '#5588ff';
        ctx.arc(_.x, _.y, ssh, 0, p2, false);
        ctx.stroke();
    }

    fps++;
    requestAnimationFrame(moveObjects);
}

let timeoutId;
let pointerHide = false;
window.onmousemove = function() {
    if (pointerHide) {
        document.querySelector('body').style.cursor = '';
        document.getElementById('info').style.display = '';
        pointerHide = false;
    }
    if (timeoutId)
        clearTimeout(timeoutId);
    if (!document.querySelector('#v').checked) {
        timeoutId = setTimeout(function() {
            document.querySelector('body').style.cursor = 'none';
            document.getElementById('info').style.display = 'none';
            pointerHide = true;
        }, 2000);
    }
}

function setv(e, te) {
    document.getElementById(te).textContent = document.getElementById(e).value;
}

function chgv(e, te) {
    document.getElementById(te).textContent = e.value;
}
~~~