ジャンプログの実装を解説
枝データの生成、制限時間、左右へのジャンプ、木のスクロール、Canvas描画、入力、BGM・効果音まで、タイミングゲームの仕組みを順番に紹介します。
ジャンプログは、木の幹から左右へジャンプし、次の枝がない側へ移動して連続成功を目指すゲームです。枝の位置を見て判断するシンプルなルールですが、ゲームの状態管理、アニメーション、制限時間、入力方法、音声など、ブラウザゲームの基本をまとめて使っています。
ゲームページはPHPで共通のヘッダーやフッターを読み込み、HTMLでCanvasと操作パネルを用意しています。ゲームの進行と描画はJavaScript、画面全体の配色やレスポンシブ対応はCSSが担当します。ここでは主にgame/timber-jump/game.jsの処理を見ていきます。
ゲームの状態を変数で管理する
プレイヤー、枝、スコア、時間、ゲームの状態を、JavaScriptの変数としてまとめています。枝は配列、プレイヤーは位置とジャンプの進行状況を持つオブジェクトです。
const player = {
side: 0,
x: W / 2,
y: groundY - 48,
jumpFrom: W / 2,
jumpTo: W / 2,
jumpStart: 0,
jumping: false
};
let logs = [];
let score = 0;
let streak = 0;
let state = 'ready';
let deadline = 0;stateにはready、playing、paused、overを保存します。更新処理でplaying以外を止めるため、開始前や一時停止中にゲームが進み続けることを防げます。
枝をオブジェクトとして生成する
枝は高さyと左右の向きbranchを持つオブジェクトです。branchを-1または1にすることで、左枝と右枝を同じ描画処理で扱えます。新しく追加された枝だけはfreshをtrueにして、一時的に強調します。
function makeLog(y, fresh = false) {
const branch = Math.random() < 0.5 ? -1 : 1;
return { y, branch, fresh, pulse: fresh ? 1 : 0, hit: false };
}
function resetLogs() {
logs = [makeLog(92), makeLog(190),
makeLog(288), makeLog(386)];
}最初に複数の枝を用意しておくことで、プレイヤーは画面内の次の判断材料を確認できます。成功すると一番下の枝を判定に使い、上へ新しい枝を追加します。
左右のジャンプを時間で補間する
ジャンプ開始時に出発地点と到着地点、開始時刻を保存します。更新処理では、開始からの経過時間を360ミリ秒で割って進行度を求め、プレイヤーの位置を少しずつ変更します。
const progress = Math.min(1,
(now - player.jumpStart) / 360);
const arc = Math.sin(progress * Math.PI);
player.x = player.jumpFrom
+ (player.jumpTo - player.jumpFrom) * progress;
player.y = groundY - 48 - arc * 110;横方向は線形補間、縦方向はsinを使った弧にしています。progressが0と1のときは弧の高さが0になり、途中で最も高くなるため、左右へ飛んで元の高さへ着地する動きになります。フレーム数ではなく時刻から進行度を計算するため、画面の更新速度に依存しにくい処理です。
枝のある側を判定する
ジャンプが終わると、画面下側にある枝と、プレイヤーが選んだ方向を比較します。値が一致した場合は枝にぶつかるためゲームオーバー、異なる場合は安全に通過したと判断します。
function resolveJump() {
const current = logs[logs.length - 1];
current.hit = current.branch === player.side;
if (current.hit) {
gameOver('選んだ側に枝がありました。');
return;
}
advanceTree();
}枝の向きと選択方向を同じ-1、1の値で表しているため、複雑な座標計算をしなくても判定できます。衝突した枝にはhitを設定し、描画時に赤く表示します。
成功するたびに木をスクロールする
通過に成功したらスコアと連続成功数を増やし、枝の配列を一つ上へ移動します。新しい枝は画面上部の外側から追加し、320ミリ秒かけて各枝を次の位置へ移動させます。
function advanceTree() {
score += 1;
streak += 1;
const previousLogs = logs.slice(0, -1);
logs = [makeLog(-8, true), ...previousLogs];
scrollFrom = logs.map(log => log.y);
scrolling = true;
scrollStart = performance.now();
deadline = performance.now()
+ Math.max(820, 1800 - score * 18);
}制限時間は成功回数に応じて短くなります。ただしMath.maxで820ミリ秒を下限にしているため、成功を重ねても極端に短くなりません。スクロール中は次の入力をqueuedSideに予約でき、移動が終わってから次のジャンプを始められます。
制限時間とゲームループ
制限時間を過ぎたとき、プレイヤーがジャンプ中でも木のスクロール中でもなければ、現在立っている側へ枝が届いたかを調べます。入力しなかった場合にも結果が決まるため、画面を放置して待ち続けられない設計です。
function update(now) {
if (now >= deadline
&& !player.jumping && !scrolling) {
resolveStanding();
return;
}
// スクロールとジャンプを更新する
updateHud();
}
function loop(now) {
if (state !== 'playing') return;
update(now);
draw();
animationId = requestAnimationFrame(loop);
}一時停止から再開するときは、停止していた時間をdeadlineに加えています。これにより、一時停止しただけで制限時間が減ることを防げます。ゲームループは状態を更新してからCanvasを描画する順番です。
Canvasへ背景、枝、プレイヤーを描く
draw関数は、毎フレームCanvas全体を背景色で塗り直し、木、枝、プレイヤーの順に描画します。枝の描画はdrawLogへ分け、枝の向きや新しい枝、衝突状態に応じて色を変更しています。
ゲームの状態と描画処理を分けておくと、判定処理を変更しても見た目のコードへ影響しにくくなります。requestAnimationFrameを使うことで、ブラウザの描画タイミングに合わせてゲームを更新できます。
キーボードとタッチ操作を同じ処理へ集約する
左右ボタンはchoose(-1)とchoose(1)を直接呼び出します。キーボードでも同じ関数を呼ぶため、入力方法が違ってもジャンプ開始の判定や予約処理が一箇所にまとまります。
el.left.addEventListener('click', () => choose(-1));
el.right.addEventListener('click', () => choose(1));
document.addEventListener('keydown', event => {
if (event.code === 'ArrowLeft'
|| event.code === 'KeyA') {
event.preventDefault();
choose(-1);
}
if (event.code === 'ArrowRight'
|| event.code === 'KeyD') {
event.preventDefault();
choose(1);
}
});ジャンプ中にもう一度入力した場合は、すぐに位置を変えず、次の方向として保存します。この仕組みにより、連続入力を受け付けながら、現在のジャンプの途中でプレイヤーが突然移動することを防いでいます。Pキーは一時停止と再開に割り当てています。
Web Audio APIとlocalStorageを使う
音声ファイルを読み込む代わりに、Web Audio APIのOscillatorNodeとGainNodeで短い音を生成しています。ジャンプ、成功、衝突などで周波数や波形を変え、BGMは一定間隔で音程の配列を読み出します。ブラウザの自動再生制限に対応するため、AudioContextはゲーム開始やボタン操作の後に初期化します。
ベストスコアはブラウザのlocalStorageへ保存します。ゲーム開始時に読み込み、ゲームオーバー時に今回のスコアが上回っていれば更新します。
const BEST_KEY = 'timber-jump-best';
let best = Number(localStorage.getItem(BEST_KEY) || 0);
if (score > best) {
best = score;
localStorage.setItem(BEST_KEY, String(best));
}ジャンプログの実装をまとめる
ジャンプログでは、枝の向きを小さなデータとして管理し、時間を基準にプレイヤーを動かし、成功時に枝の配列をスクロールさせています。Canvasは状態を画面へ描く役割、JavaScriptは状態を更新する役割に分けることで、シンプルなルールでもゲームらしい動きを作れます。
実際に遊ぶときはジャンプログのゲームページを開き、枝のない側を選ぶタイミングを試してみてください。入力の予約、一時停止、音量変更、ベストスコア保存まで確認すると、実装と操作の対応も分かりやすくなります。