three.jsで円形ブロック崩しを作る — 扇形ブロックと極座標の当たり判定
three.jsで円形のブロック崩しを作る方法を、実際に動くコードで説明します。扇形のブロックの作り方、極座標での当たり判定、回転する輪の扱いまで。JavaScriptの基本が分かれば読める内容にしています。
用意するもの
HTMLファイル1枚とthree.jsだけです。ビルド工程は要りません。
<canvas id="gl"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('gl'), antialias: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(40, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 0, 3.1);
</script>
盤面は半径1の円とし、すべての座標をその比率で扱います。画面サイズが変わっても、盤面の大きさだけを調整すれば済むようになります。
扇形のブロックを作る
three.jsに扇形のジオメトリはないので、頂点を自分で並べます。内側の半径、外側の半径、開始角度、終了角度、厚みを受け取り、板状の立体を返す関数を作ります。
function sector(r1, r2, a1, a2, depth, seg = 6) {
const pts = [];
for (let i = 0; i <= seg; i++) {
const a = a1 + (a2 - a1) * i / seg;
pts.push(new THREE.Vector2(Math.cos(a) * r2, Math.sin(a) * r2));
}
for (let i = seg; i >= 0; i--) {
const a = a1 + (a2 - a1) * i / seg;
pts.push(new THREE.Vector2(Math.cos(a) * r1, Math.sin(a) * r1));
}
const shape = new THREE.Shape(pts);
return new THREE.ExtrudeGeometry(shape, { depth, bevelEnabled: false });
}
円弧を何本の線分で近似するかが seg です。小さいブロックなら4〜6で十分きれいに見えます。
輪を並べる
1つの輪は、同じ形のブロックを角度をずらして並べたものです。ジオメトリは1つ作って使い回し、配置はメッシュの回転で行います。こうするとメモリも描画負荷も抑えられます。
function makeRing(r1, r2, count, color) {
const group = new THREE.Group();
const step = Math.PI * 2 / count;
const hw = step / 2 - 0.01; // 隙間を少し空ける
const geo = sector(r1, r2, -hw, hw, 0.07); // 1つ作って共有
const bricks = [];
for (let i = 0; i < count; i++) {
const mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ color }));
const tc = i * step + step / 2; // このブロックの中心角
mesh.rotation.z = tc;
group.add(mesh);
bricks.push({ tc, hw, mesh, alive: true });
}
scene.add(group);
return { r1, r2, group, bricks, rot: 0, omega: 0.2 };
}
輪を回すのは簡単で、毎フレーム group.rotation.z を進めるだけです。ブロックの側は角度を持っているので、判定のときにこの回転量を足し引きします。
当たり判定は極座標で
ここが円形ブロック崩しの肝です。3Dの衝突判定ライブラリは要りません。ボールの位置を「中心からの距離」と「角度」に変換すれば、輪との判定は数値の比較だけで済みます。
const TAU = Math.PI * 2;
const wrap = a => ((a + Math.PI) % TAU + TAU) % TAU - Math.PI; // -π〜πに正規化
function hitTest(ball, ring) {
const r = Math.hypot(ball.x, ball.y);
if (r < ring.r1 - ball.r || r > ring.r2 + ball.r) return null; // 輪の外
const th = Math.atan2(ball.y, ball.x);
for (const b of ring.bricks) {
if (!b.alive) continue;
const off = wrap(th - ring.rot - b.tc); // 回転量を引いて比較
if (Math.abs(off) <= b.hw + ball.r / r) return b;
}
return null;
}
ball.r / r を足しているのは、ボールの半径を角度に換算するためです。中心に近いほど同じ距離が大きな角度になるので、この補正がないと内側の輪ですり抜けます。
跳ね返る向きを決める
当たった面が「輪の内側か外側か」で反射の向きが変わります。ボールの進行方向と中心方向の関係を見れば判定できます。
function bounce(ball, ring) {
const r = Math.hypot(ball.x, ball.y);
const nx = ball.x / r, ny = ball.y / r; // 中心から外向きの単位ベクトル
const dot = ball.vx * nx + ball.vy * ny;
ball.vx -= 2 * dot * nx; // 法線方向の成分を反転
ball.vy -= 2 * dot * ny;
}
これは「半径方向の壁で跳ね返る」という近似です。厳密には側面に当たる場合もありますが、ブロックが薄い輪であればこの近似で違和感は出ません。
パドルを円周上で動かす
パドルも扇形です。違うのは、プレイヤーの操作で中心角が変わることと、当たったときに返す角度を操作できることです。
function hitPaddle(ball, paddle) {
const r = Math.hypot(ball.x, ball.y);
if (r < paddle.r1 - ball.r) return false;
const th = Math.atan2(ball.y, ball.x);
const off = wrap(th - paddle.angle);
if (Math.abs(off) > paddle.hw) return false;
const rel = off / paddle.hw; // -1(左端)〜1(右端)
const inward = th + Math.PI; // 中心へ向かう向き
const dir = inward - rel * 0.95; // 端ほど斜めに返す
const sp = Math.hypot(ball.vx, ball.vy);
ball.vx = Math.cos(dir) * sp;
ball.vy = Math.sin(dir) * sp;
return true;
}
rel が当たった位置、0.95 が「端でどれだけ角度を変えるか」の係数です。ここを大きくすると鋭く返せて爽快ですが、操作が難しくなります。0.8〜1.0あたりで調整すると手触りが決まります。
すり抜けを防ぐ
ボールが速いと、1フレームの移動でブロックを飛び越えてしまいます。移動を細かく分割して、そのつど判定するのが確実です。
function step(ball, dt) {
const move = Math.hypot(ball.vx, ball.vy) * dt;
const n = Math.max(1, Math.ceil(move / (ball.r * 0.6))); // ボール半径より細かく
const h = dt / n;
for (let i = 0; i < n; i++) {
ball.x += ball.vx * h;
ball.y += ball.vy * h;
checkCollisions(ball);
}
}
分割数はボールの半径を基準に決めます。「1回の移動量がボール半径の6割を超えない」ようにしておけば、通常の速度では抜けません。
手触りを良くする調整
距離で速度を変える
中心に近いほど遅く、外周へ向かうほど速くすると、危険な局面ほど反応する時間が生まれます。速さの絶対値ではなく、プレイヤーが判断できる時間を設計する考え方です。
角度が浅くなりすぎないようにする
反射を繰り返すうちに、ボールが外周に沿ってほぼ真横に飛ぶ状態になることがあります。こうなると延々と戻ってこないので、半径方向の速度成分が一定以下になったら強制的に角度を付け直します。
止まったボールを直す
何らかの理由で速度がゼロになったボールは、そのまま放置すると進行不能の原因になります。速度の大きさを毎フレーム確認し、極端に小さければ中心方向へ向け直す処理を入れておくと安全です。
まとめ
円形ブロック崩しは、極座標に置き換えて考えると驚くほど単純です。必要なのは次の4つだけでした。
- 扇形のジオメトリを1つ作って使い回す
- 距離と角度で当たり判定をする
- 中心方向の単位ベクトルで反射させる
- 移動を分割してすり抜けを防ぐ
ここまで動けば、あとはブロックの並べ方やアイテムの設計といった、ゲームとして楽しい部分に時間を使えます。実際に動いているものを触ってみたい方は、この記事で説明した仕組みで動いているゲームを下のリンクから遊べます。
「A旋状のバリア」を遊ぶブラウザですぐ遊べます・登録不要