1枚のHTMLファイルだけで動くロボットバトルゲーム『ロボットクラッシュ』を、v1からv7まで作り直しながら育ててきた。今回はその中でも特に苦労した「タッチ入力の扱い」と「衝突判定・ノックバック」、そして「ショップ・パーツ拡張システム」の実装について、開発記としてまとめておく。
作ったものの概要
プレイヤー同士(あるいはCPU)がロボットを操作してぶつかり合う、シンプルな対戦アクション。外部ライブラリを使わず、Canvas+Vanilla JSのみの単一HTMLファイルで完結させているのが一貫した方針。スマホのブラウザでもそのまま遊べることを最初から目標にしていたため、タッチ操作まわりの作り込みに最も時間がかかった。
タッチ入力でハマったポイント
PCでのマウス操作を前提に組んだ後、そのままタッチイベントを足すだけでは動かなかった。特につまずいたのが、タッチ操作だとブラウザ標準のスクロールやピンチズームが同時に発火してしまい、操作がゲームに反映される前に画面ごと動いてしまう問題。touchstartのイベントリスナーを{ passive: false }で登録した上でpreventDefault()を呼ぶことで、ようやく意図通りの入力だけを拾えるようになった。
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
handleInputStart(x, y);
}, { passive: false });
また、マウス用の入力処理とタッチ用の入力処理を別々に書いていると分岐が増えて破綻しやすいので、最終的には「座標を受け取って処理する共通関数」を用意し、mousedownとtouchstartの両方からその共通関数を呼ぶ形に統一した。
衝突判定とノックバックの実装
ロボット同士の当たり判定は、精密さよりも処理の軽さを優先して円同士の距離判定(円形コリジョン)で組んでいる。
function isColliding(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance < (a.radius + b.radius);
}
function applyKnockback(a, b, power) {
const angle = Math.atan2(a.y - b.y, a.x - b.x);
a.vx += Math.cos(angle) * power;
a.vy += Math.sin(angle) * power;
}
矩形同士の判定(AABB)も試したが、ロボットの見た目が円形に近い当たり判定のほうが操作感として違和感が少なかったため、最終的に円形判定に落ち着いた。ノックバックは単純に衝突した角度方向へ速度を加算するだけのシンプルな実装だが、体感の気持ちよさへの影響が大きく、パラメータ調整に一番時間をかけた部分でもある。
ショップシステムとパーツ拡張
バージョンを重ねる中で、単発の対戦だけでは飽きが来やすいと感じ、バトルで得た資源でパーツを購入・強化できるショップシステムを追加した。パーツの種類が増えるほど組み合わせのバランス調整が難しくなり、特定のパーツ構成が強すぎる・弱すぎるといった調整をv4〜v7にかけて繰り返している。
v7までの反復で変わったこと
- 入力処理をマウス・タッチ共通の関数に統一(v1〜v3)
- 衝突判定を矩形から円形に変更し、操作感を改善(v3〜v4)
- ショップ・パーツ拡張システムを追加(v4〜v5)
- パーツバランスの調整とバグ修正を継続(v5〜v7)
まとめ
単一HTMLファイルでの個人開発は身軽な反面、機能を足すたびに全体の見通しが悪くなりやすい。タッチ対応と衝突判定という基礎部分を早い段階で共通化しておいたことが、後からショップやパーツ拡張を足すときの土台として効いた。