マガジン

スワイプ操作のゲームで誤操作を減らす判定の作り方

遊びをつくる

執筆にClaudeを使用しています。

試作の段ボールから入口やゴールを備えた完成形へつながるゲーム模型の写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

スワイプを使ったゲームで「意図していない方向に動いてしまう」「少し触れただけで反応してしまう」という声は、完成度の高いゲームでも起こりえます。原因のほとんどは、スワイプの「開始・方向・終了」をどう判定するかの設計にあります。 タップとスワイプの混在するゲームでは特に問題が顕在化します。ユーザーはタップのつもりで少し指がずれることがあり、そのずれをスワイプと誤って拾ってしまうと操作感が壊れます。逆に、しきい値を厳しくしすぎると「スワイプしたのに反応しない」という別の不満が生まれます。 この記事では、ブラウザゲームのJavaScriptでスワイプ判定を実装するときに設計すべき三つの基準——最小移動距離・方向角・時間制約——と、それぞれのチューニング方法を具体的に説明します。

この記事の目次
  1. なぜ「ずれ」が起きるのか
  2. 三つの判定基準と設計値の考え方
  3. 実装の手順
  4. よくある失敗パターン
  5. チューニングの進め方
  6. 次の行動

なぜ「ずれ」が起きるのか

スワイプ判定の誤操作は、大きく二種類に分かれます。一つはタップがスワイプと判定される誤検知、もう一つは斜め移動が隣接方向に取られる方向誤判定です。

誤検知は、touchstart から touchend までの移動距離をまったくチェックしないコードで起きます。指を画面に置いてそのまま離すだけでも、数ピクセル単位のブレは常に発生します。これを距離フィルターなしで touchmove の方向を拾うと、ほぼあらゆるタップがスワイプとして処理されます。

方向誤判定は、斜め45度付近の入力で発生しやすいです。4方向判定のゲームで右上方向に指を動かしたとき、「右」と「上」のどちらに割り当てるかが曖昧になります。角度の計算を単純に45度区切りにしているだけだと、境界付近は入力のわずかなブレで結果が変わります。

三つの判定基準と設計値の考え方

最小移動距離(デッドゾーン)

touchstart の座標と touchend の座標の距離が一定値以下ならスワイプと見なさない、という処理です。この値をデッドゾーンと呼びます。

distance = Math.hypot(endX - startX, endY - startY)
if (distance < threshold) { /* タップとして処理 */ return }

デッドゾーンの実用的な目安は 10〜20px です。小さすぎると誤検知が残り、大きすぎると短いフリックに反応しなくなります。画面解像度に依存しないよう、window.devicePixelRatio を使って物理ピクセルではなくCSSピクセルで統一して計算するのが基本です。デバイスによってdprが異なるため、px値をそのまま使う設計は実機テストで必ず破綻します。

方向角の計算と「死角帯」

方向の判定には Math.atan2 を使います。

const angle = Math.atan2(endY - startY, endX - startX) * (180 / Math.PI)

この角度を4方向に割り当てるとき、単純に90度ずつ区切るのではなく、境界前後に5〜10度の「不感帯」を設ける方法が有効です。たとえば右方向を -45度〜+45度とする代わりに、-40度〜+40度だけを「右確定」とし、±40〜±45度の範囲では前回の有効入力を維持する、あるいは入力を無視する設計にします。

これにより、斜めに入力したときに「右か上か」が揺れる問題を抑えられます。ただし不感帯を広げすぎると斜め操作がまったく受け付けられなくなるため、8方向対応ゲームでは方向ごとに不感帯の幅を変えるのも選択肢です。

時間制約(最大スワイプ時間)

touchstart から touchend までの経過時間に上限を設ける設計です。指を画面に長押ししたまま離した場合をスワイプではなくドラッグや長押しとして扱いたいゲームでは、この制約が有効です。

実用的な上限は 300〜500ms が多く使われます。これより長い接触は「ゆっくりなぞった」と判断してスワイプ判定から除外できます。一方、この制約が不要なパズルゲームなど「ゆっくり操作するUI」では省略するか値を大きく取ります。

実装の手順

  1. イベントの取得:touchstart で開始座標と開始時刻を記録する。touchend で終了座標と終了時刻を取得する。touchmove は必要なケース(リアルタイム追跡)以外では判定に使わない。
  2. デッドゾーンチェック:距離が閾値未満ならタップ処理に分岐してスワイプ判定を終了する。
  3. 時間チェック(必要な場合):経過時間が上限を超えていたらスワイプ判定を終了する。
  4. 角度計算と方向割り当て:atan2 で角度を求め、不感帯を考慮した分岐で方向を確定する。
  5. ゲームロジックへの引き渡し:確定した方向だけをゲーム側に伝える。判定コードはゲームロジックと分離して単体でテストできる状態にしておく。

判定コードを独立した関数にまとめておくと、閾値を変数として管理しやすくなります。開発中は閾値をURLパラメーターや画面上のスライダーで変えられるようにしておくと、実機でのチューニングが大幅に楽になります。

よくある失敗パターン

touchmove だけで方向を更新し続ける設計は、指が途中で方向を変えた場合に最後の方向が採用されてしまいます。多くのスワイプゲームでは「最初の動き出しの方向」が意図であることが多いため、touchmove の最初のイベント一回だけで方向を確定し、以降は無視する設計が安定します。

preventDefault() の呼び忘れもよくある問題です。touchmove で preventDefault() を呼ばないと、スワイプ中にページ全体がスクロールしてしまいます。ただし、{ passive: false } オプションなしでリスナーを登録すると preventDefault() が効かないブラウザがあります。リスナー登録時に明示的に指定してください。

PCでのマウスイベントとの二重実装を忘れることも多いです。スマホ向けに touch イベントだけ実装しても、PCブラウザでのプレイ時に操作できなくなります。mouse イベントで同じ判定ロジックを使えるよう、座標取得部分だけを差し替えられる構造にしておくと管理が楽です。

チューニングの進め方

デッドゾーンと不感帯の値は、一度決めたら終わりではありません。実機での操作感は机上の数値と必ずずれます。チューニングは次の順序で進めると効率的です。

まずデッドゾーンを0から始めて誤検知が出なくなる最小値を探すのが基本です。多くの場合10px前後で落ち着きます。次に方向誤判定が起きやすいシーンを再現して不感帯を広げる方向に調整します。最後に時間制約を試すのは、パズルやリズム系など操作の速度が重要なジャンルだけで十分です。

異なるOS・ブラウザ・画面サイズでの確認も欠かせません。特にAndroidとiOSではタッチイベントの挙動に差があることがあり、同じコードでもピクセルのブレ方が違うケースがあります。CSSピクセル基準で統一した計算をしていれば大幅なずれは防げますが、最終確認は実機で行うことを推奨します。

次の行動

まず手元のプロジェクトでデッドゾーンと atan2 を使った方向判定を切り出し、閾値を変数化してブラウザで動かしてみてください。誤操作が減った手応えを得たら、アソバースにZIPまたはHTMLファイルとして投稿して実際のプレイヤーにフィードバックをもらうのが最短の改善サイクルです。チューニングの判断基準はこの記事の数値を出発点にして、実機の感触で上下に調整してください。