1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Canvasゲームのループ管理:再開始・停止・時間刻み・AABBを検証する

1
Last updated at Posted at 2025-03-07

2026-10-12 訂正:旧ゲームコードは比較式・弾・敵・画像生成・pool等が途中欠落し、複数の必要メソッドも未実装でした。完全解説/完成ゲームという説明を撤回し、ループの単一所有と衝突判定を独立して検証します。

元の作品

See the Pen Untitled by Yushi Yamamoto (@yamamotoyushi) on CodePen.

旧埋込みを維持しています。CodePen内の現在のコード・実動作と、この掲載例の一致は今回未確認です。以下はインベーダーゲームの完成版ではありません。敵生成、射撃、スコア、保存、音、難易度UI、Canvas描画は別実装です。

旧実装の不足を完成扱いにしない

resizeCanvas/loadHighScore/showStartScreen/restartGame/spawnEnemy等が未実装でした。弾の境界判定とAABBの比較式も欠落し、HTMLにCSS/JSを読み込むlink/scriptがありません。3ファイルを保存するだけで動くという説明を撤回します。

最初はplayerがnullなのに、開始前のSpaceでplayer.shootへ到達できました。入力はrunning/paused/gameover等のphaseとplayerの存在で制限します。ゲームにfocusがある時だけ該当キーの既定操作を抑止し、input等での通常操作を妨げません。blur/pointercancelで押下状態を解除します。HTMLのmobile操作案内だけでその動作が実装済みとはしません。

ループの所有者を一つにする

旧startGameは既存requestAnimationFrameを解除せず再起動していました。以下のgame-loop.mjsはstartの前にstopし、古いcallbackが呼ばれても世代の不一致で状態を更新しません。update中にstopした場合もrenderと次の予約を行いません。

export class GameLoop {
  constructor(request, cancel, update, render) {
    this.request=request;this.cancel=cancel;this.update=update;this.render=render;
    this.frame=null;this.last=null;this.running=false;this.generation=0;
  }
  stop() {
    this.running=false;this.generation+=1;
    if(this.frame!==null)this.cancel(this.frame);
    this.frame=null;this.last=null;
  }
  start() {
    this.stop();this.running=true;
    const generation=this.generation;
    const tick=timestamp=>{
      if(!this.running||generation!==this.generation)return;
      this.frame=null;
      const dt=this.last===null?0:Math.min(0.1,Math.max(0,(timestamp-this.last)/1000));
      this.last=timestamp;
      try { this.update(dt);if(this.running&&generation===this.generation)this.render(); }
      catch(error) { this.stop();throw error; }
      if(this.running&&generation===this.generation)this.frame=this.request(tick);
    };
    this.frame=this.request(tick);
  }
}
export function overlaps(a,b) {
  for(const r of [a,b]) {
    if(!r||![r.x,r.y,r.width,r.height].every(Number.isFinite)||r.width<0||r.height<0)throw new Error('rectangle');
  }
  if(a.width===0||a.height===0||b.width===0||b.height===0)return false;
  return a.x < b.x+b.width && a.x+a.width > b.x &&
         a.y < b.y+b.height && a.y+a.height > b.y;
}

GameLoopへブラウザのrequest/cancelとゲームのupdate/renderを渡す例は次です。updateGame/renderGameは別途実装する関数であり、この断片だけでゲームは動きません。

const loop = new GameLoop(
  callback => requestAnimationFrame(callback),
  id => cancelAnimationFrame(id),
  updateGame,
  renderGame,
);
// プレイヤーと各配列の初期化、画面の準備に成功してから loop.start()
// menu/gameover/画面破棄では loop.stop()

requestAnimationFrameはone-shotで、refresh rate等に依存します。cancelAnimationFrameは予約IDを解除します。60FPS固定ではありません。例は停止後の再開で最初のdtを0にし、大きな経過時間を0.1秒までに抑えます。これはbackground中のゲーム時間を追い付かせない仕様です。固定時間刻み等が必要なら別途設計します。

update/renderの例外では停止し、呼出し元へ例外を伝えます。画面には停止状態と復旧操作を別途表示します。モックでのテストだけでブラウザの描画/操作まで確認したことにはしません。

速度の単位をそろえる

旧Player/EnemyはdeltaTimeを渡されても毎frameで固定量を加えていました。speedをpx/秒へ換算し、移動とcooldownの単位を統一します。例えばupdate(dtSeconds)なら次は処理の断片です。

player.x += direction * player.speed * dtSeconds;
player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));
player.cooldownSeconds = Math.max(0, player.cooldownSeconds - dtSeconds);

player、direction、dtSeconds、canvasを持つ呼出し元が必要です。旧speed 5px/frameを単に5px/秒へ変更すると動作速度が変わるため、意図するpx/秒を決めます。Canvas幅はplayer.width以上を保証し、resize後も位置と当たり判定を調整します。

AABBの範囲と限界

上のoverlapsはMDN 2D collision detectionのAABBの形に沿い、x/yを左上、width/heightを大きさとして比較します。この仕様では辺が接するだけ、面積0は重なりに含めません。回転した図形やpixel単位の形状は扱いません。

弾の移動後のAABBだけでは高速移動で敵を飛び越す場合があります。時間刻みか移動軌跡の判定が必要です。命中時は弾を一度だけ除去し、倒れた敵の得点を一度だけ加算します。poolを導入するなら再利用時の全状態の初期化と上限をテストし、計測なしに高速化を断言しません。

難易度・画像・音を初期状態へ戻す

旧currentDifficultyは基準設定への参照を直接持ち、levelUpがその値を更新していました。再試行では基準設定の独立copyから始め、確率・spawn interval・速度の上下限を定義します。現状のflatな設定ならspread copyで足りますが、nested値は別copyが必要です。

画像はcompleteだけで成功判定しません。load/errorとnaturalWidth > 0、失敗時の図形等の代替表示を使います。audio.playのPromiseが拒否されてもゲームを壊さず、音の利用条件とmuteを確認します。今回旧外部音声/画像は新しい実装例で利用していません。

今回確認したこと

Node.jsのモックframe schedulerで22項目を確認しました。再開始して予約が一つ、旧callbackの抑止、秒数・dt上限、停止/再開、例外停止、更新中停止後の描画抑止、AABBの重なり/辺/包含/0面積/不正値を対象にしています。

実browserのrAF、Canvas、射撃/敵/得点/難易度全体、音声/画像、保存、mobile/読み上げ、CodePen、長時間プレイは未検証です。部分的な部品テストを完成ゲームの動作確認へ拡張しません。

この記事を書いた人✏️@YushiYamamoto
ITPRODX.com代表 / AIアーキテクト
Next.js / TypeScript / n8nを活用した自律型アーキテクチャ設計を専門としています。
日々の自動化の検証結果や、ビジネス側の視点(ROI等)に関するより深い考察は、以下の公式サイトおよびnoteで発信しています。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?