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で発信しています。