マガジン

スマホのタッチ操作をゲームに実装する:タップ・スワイプ・長押し

遊びをつくる

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

大きな赤いボタンと指先、ばね、曲線のレールで押す・離す・待つを表す立体イメージ
記事のテーマをもとに制作したAI生成イラスト。

スマートフォンでブラウザゲームを公開するとき、「マウスクリックしか受け付けない」状態のままでは多くのプレイヤーが最初の操作で詰まります。タッチ操作の実装はそれほど難しくありませんが、「とりあえずclickイベントを使えば動くのでは」という認識のまま進むと、反応の遅延・誤判定・スワイプとスクロールの競合といった問題が後から重なってきます。 この記事では、タップ・スワイプ・長押しの三種類を正確に実装するための考え方と具体的なコードを示します。イベントの仕組みから判定ロジックまでを一通り押さえることで、自分のゲームに必要な操作を選んで組み込めるようになることを目指します。

この記事の目次
  1. タッチイベントの基本構造を理解する
  2. タップの実装:`click`との違いと遅延問題
  3. スワイプの実装:方向判定と誤検知の防止
  4. 長押しの実装:タイマーを使った確定ロジック
  5. 複数の操作を共存させるときの設計方針
  6. 次の行動
  7. 参考資料

タッチイベントの基本構造を理解する

ブラウザのタッチ操作は、touchstart・touchmove・touchend・touchcancelの四つのイベントで構成されています。マウスイベント(mousedownなど)と対応関係にありますが、タッチには複数の指を同時に扱うtouches・targetTouches・changedTouchesというリストが付いており、マウスとは構造が異なります。

各イベントオブジェクトにはchangedTouches(そのイベントで状態が変わった指)が必ず含まれます。シングルタッチ操作を扱う場合はchangedTouches[0]から座標を取得するのが基本です。clientX・clientYはビューポート基準、pageX・pageYはページ全体基準の座標です。CanvasゲームではclientXからCanvasのgetBoundingClientRect()のオフセットを引いてCanvas内座標に変換します。

const rect = canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;

一点注意が必要なのは、addEventListenerに{ passive: false }を渡さないとpreventDefault()が使えないブラウザがあることです。スクロールと競合させたくない要素にはこのオプションを明示してください。

タップの実装:`click`との違いと遅延問題

タップを検出する最も単純な方法はclickイベントを使うことです。ただし、モバイルブラウザは「ダブルタップでズーム」と区別するためにclickの発火を約300ミリ秒遅らせる実装を持っていました(現在はtouch-action: manipulationやメタタグで無効化できますが、環境によって挙動が異なります)。

ゲームで即応性が必要なら、touchstartかtouchendを直接使います。touchstartは押した瞬間に発火するため最速ですが、スワイプ開始とも重なります。「指を離した時点でタップ確定」にしたい場合はtouchendを使い、移動量が閾値を超えていたらタップではないと判定する方が誤操作が減ります。

js
let startX, startY;

element.addEventListener('touchstart', e => {
  startX = e.changedTouches[0].clientX;
  startY = e.changedTouches[0].clientY;
}, { passive: true });

element.addEventListener('touchend', e => {
  const dx = e.changedTouches[0].clientX - startX;
  const dy = e.changedTouches[0].clientY - startY;
  if (Math.abs(dx) < 10 && Math.abs(dy) < 10) {
    handleTap(e.changedTouches[0]);
  }
}, { passive: true });

閾値の10pxは一例です。端末の解像度や操作対象の大きさに合わせて調整してください。タップ対象のサイズは指の腹が当たる面積を考慮して、最低でも44×44px程度を確保することが推奨されています(AppleのヒューマンインターフェースガイドラインやWCAGに記載があります)。

スワイプの実装:方向判定と誤検知の防止

スワイプは「移動距離が閾値を超え、かつ方向が特定の軸に偏っている」ときに確定させるのが基本方針です。移動量だけで判定すると斜め方向の動きも拾ってしまい、意図しない方向への入力になります。

js
element.addEventListener('touchend', e => {
  const dx = e.changedTouches[0].clientX - startX;
  const dy = e.changedTouches[0].clientY - startY;
  const absDx = Math.abs(dx);
  const absDy = Math.abs(dy);
  const minDist = 30; // 最小移動距離(px)

  if (absDx < minDist && absDy < minDist) return; // 移動量が小さい=タップ

  if (absDx > absDy) {
    handleSwipe(dx > 0 ? 'right' : 'left');
  } else {
    handleSwipe(dy > 0 ? 'down' : 'up');
  }
}, { passive: true });

上下スワイプを使う場合、ページ全体のスクロールと競合します。ゲームのCanvas要素やコンテナにtouch-action: noneをCSSで指定すると、ブラウザのデフォルトスクロールを無効にできます。ただし、この指定はゲームエリア全体に効くため、本当にスクロールが必要な箇所と分離して管理してください。

スワイプ中にtouchmoveをリアルタイムで追いたい場合(ドラッグ操作など)は、touchmove内で現在座標と開始座標の差を毎フレーム計算して描画に反映します。このときpreventDefault()でブラウザのスクロールを止める必要があるため、passive: falseが必要になります。

長押しの実装:タイマーを使った確定ロジック

長押しはブラウザネイティブのイベントとして存在しないため、タイマーで自作します。touchstartでsetTimeoutを開始し、touchmoveやtouchendが先に来たらキャンセルする構造が基本です。

js
let longPressTimer = null;
const LONG_PRESS_MS = 500;

element.addEventListener('touchstart', e => {
  startX = e.changedTouches[0].clientX;
  startY = e.changedTouches[0].clientY;
  longPressTimer = setTimeout(() => {
    handleLongPress(e.changedTouches[0]);
  }, LONG_PRESS_MS);
}, { passive: true });

element.addEventListener('touchmove', e => {
  const dx = e.changedTouches[0].clientX - startX;
  const dy = e.changedTouches[0].clientY - startY;
  if (Math.abs(dx) > 10 || Math.abs(dy) > 10) {
    clearTimeout(longPressTimer);
  }
}, { passive: true });

element.addEventListener('touchend', () => {
  clearTimeout(longPressTimer);
}, { passive: true });

element.addEventListener('touchcancel', () => {
  clearTimeout(longPressTimer);
}, { passive: true });

touchcancelは電話着信やブラウザがタッチシーケンスを中断したときに発火します。キャンセル処理に必ず含めてください。長押し時間の500ミリ秒は操作感に直結するため、ゲームジャンルに合わせて調整します。アクション系では300〜400ms、メニュー操作では500〜700msが一般的な感覚値です。

複数の操作を共存させるときの設計方針

タップ・スワイプ・長押しを同じ要素で受け付けると、判定ロジックが混在して壊れやすくなります。次の原則を守ると整理しやすくなります。

状態機械として管理する

「待機中」→「タッチ中」→「スワイプ確定/長押し確定/タップ確定」という状態遷移を明示的に持つと、条件の競合を防げます。touchstartで状態を「タッチ中」に切り替え、touchmoveの移動量・touchendのタイミング・タイマーの発火それぞれが状態を次へ進める役割を持ちます。

操作の優先順位を決める

移動量が閾値を超えたらスワイプ確定としてタイマーをキャンセルする、あるいはタイマーが発火したらその後のtouchendをタップとして扱わない、といった優先順位を先に決めておきます。「どの操作が先に成立したら他を無効にするか」がコードの骨格になります。

デバッグはChrome DevToolsのタッチエミュレーションで

PC上でのタッチ操作シミュレーションはChrome DevToolsの「デバイスツールバー」で行えます。ただし実機と完全に同じではなく、touchcancelの発生タイミングや300ms遅延の有無が異なる場合があります。実機での確認を最終チェックとして組み込んでください。

次の行動

まずはCanvas要素かゲームのメインコンテナにtouch-action: noneを追加し、タップだけをtouchendで検出するところから始めてみてください。動作を確認したらスワイプ判定を加え、最後に長押しタイマーを組み込むと、段階的に検証できます。完成したゲームはHTMLファイルまたはZIPファイルとしてアソバースに投稿できます。

参考資料

  • MDN Web Docs「Touch events」
  • Apple Human Interface Guidelines「Gestures」
  • WCAG 2.1 Success Criterion 2.5.5(Target Size)