毎秒120単位で動く物体があるとします。ブラウザのタブを20秒間離れ、戻ってきたとき、その物体は2,400単位先に進むべきでしょうか。元の位置に留まるべきでしょうか。それとも、サーバーから届いた位置に移るべきでしょうか。
答えはゲームの設計によって変わります。放置ゲームなら離席中の進行を反映したい場合があり、共有ワールドならプレイヤーがいなくても時間は進みます。ここで扱うのは、プレイヤーを待つローカルのシングルプレイヤーゲームです。タブが非表示になったら一時停止し、離席中の時間を捨て、再開操作があるまで待つ方針に絞ります。
多くのブラウザは、バックグラウンドのタブで requestAnimationFrame() のコールバックを停止します。ただし、ブラウザが処理を止めることと、ゲーム内の時間をどう扱うかは別の話です。復帰後の更新で古いフレームの時刻を引くと、離席中の時間を一度にシミュレーションへ渡してしまうことがあります。
再開時に残す状態と、捨てる時刻
この方針では、位置とスコアは一時停止をまたいで保持します。一方、前回のフレーム時刻は保持しません。時刻の基準を消しておけば、再開後の最初のフレームは新しい基準を記録するだけで、シミュレーションを進める時間は0になります。移動はその次のフレームから始まります。
実時間とゲーム内の時間は一致しなくなります。実時間が5秒過ぎても、ゲーム内では0秒しか進まないことがあります。正午に終了するイベントのような実時間の期限は、別の締切時刻で管理する必要があります。一時停止するシミュレーションの時計に任せることはできません。
人工的なタイムスタンプから計算し、AIの補助を用いて作成した説明用のグラフです。実時間の2〜7秒はタブが非表示、7〜8秒は表示状態に戻っていても一時停止中です。ブラウザや製品の性能測定ではありません。図中の英語表記は元のままです。
この図は、実時間10秒のうちゲームが4秒だけ進む方針を示しています。横軸は経過した実時間、縦軸はシミュレーションに加算された時間です。水平な区間には、一時停止画面を見ている1秒間も含まれます。タブに戻っただけでは再開しません。
タイムスタンプだけで確かめる
次のコードは、その計算だけを確認できる完全な例です。Node.js、または新しいJavaScriptコンソールで実行できます。入力は有限のミリ秒単位のタイムスタンプです。last を null に戻す操作が、一時停止と再開の境界を表します。この例はブラウザのイベントやアニメーションループを登録しません。
let last = null;
function step(timestampMs) {
if (last === null) {
last = timestampMs;
return 0;
}
const dt = Math.max(
0,
Math.min(
(timestampMs - last) / 1000,
0.05
)
);
last = timestampMs;
return dt;
}
// 0: 時刻の基準を記録
console.log(step(1000));
// 0.016: 通常のフレーム
console.log(step(1016));
// 一時停止と再開で古い時刻を破棄
last = null;
// 0: 離席中の時間は加算しない
console.log(step(21016));
// 0.016: 再び通常のフレーム
console.log(step(21032));
// 0.05:
// 表示中の長い停止には上限を適用
console.log(step(26032));
出力は順に 0、0.016、0、0.016、0.05 です。
最後の行には、離席中の時間を捨てる処理とは別の判断が含まれています。0.05秒という上限によって、表示中に5秒止まった場合でも、その大部分を捨てます。last は常に最新のタイムスタンプへ更新するため、捨てた時間を後のフレームで取り戻すことはありません。毎秒120単位なら、この更新で進むのは600単位ではなく6単位です。
その代わり、処理落ちが続くとシミュレーションは実時間より遅くなります。0.05秒はすべての物理計算に適した値ではありません。また、この上限だけで、移動前後の位置の間にあるものを衝突判定が必ず検出できるわけでもありません。移動方法と衝突判定の仕組みに合わせて選ぶ値です。
時計が止まっても、ゲーム全体が止まるとは限らない
ブラウザへ組み込むときは、この計算の外側に一時停止と再開の状態管理が必要です。ウィンドウのフォーカスが外れたことを、そのままタブの非表示とみなさず、Page Visibility API を使います。
非表示になったら、一時停止状態に切り替え、予約済みのアニメーション要求を取り消し、時刻の基準を消します。表示に戻ったら、一時停止画面を描画します。再開操作で再び基準を消し、フレームを1回要求します。予約済みの要求IDを管理し、再開ボタンを2回押してもループが2本走らないようにします。
同じ切り替えで、押下中のキーとドラッグ状態も解除します。キーを離したイベントを受け取れなかった場合、再開後もキャラクターが動き続けることがあるためです。ダメージ用のタイマーや音声をどう扱うかも、明示的に決める必要があります。移動だけを止めても、ダメージのタイマーが動き続けていれば、プレイヤーの状態を保てません。
iframeの中でも同じ手順を確認する
埋め込み先のページには、もう一つ境界があります。Rosebudのゲーム埋め込みガイド はiframeを使う手順を説明しています。そのため、ゲーム単独のタブだけでなく、実際の埋め込み先でも一時停止の手順を確認します。この2024年11月のガイドが説明するのは埋め込み方法であり、現在のゲームがどのように時間を管理しているかではありません。
iframeをCSSで隠すだけでは、ドキュメント全体が非表示になったときと同じ可視性の変化は起きません。親ページがパネルを切り替える場合は、ゲームに一時停止を伝える仕組みを別に用意します。
確認手順は、移動する、タブを隠す、戻る、再開ボタンを押す前に少し待つ、という順番です。待っている間もゲーム内の状態は進まないことを確認します。再開後の最初のフレームでは加算時間が0になり、その後のフレームで保持した位置から動き始めるはずです。移動は止まっているのに、ダメージ、音声、押下状態が続いているなら、見直すべきなのは時刻の引き算ではなく、一時停止への切り替えです。
