02.無限ループ
ゲームと言えば無限ループ
基本的にゲームは電源オフまで無限ループです。ユーザの処理をずっと待ちつつ、定期的に再描画を行います。
パズルゲームなどでは操作をトリガーにするだけでも処理できなくはないのですが、ジャンルを問わない場合は無限ループ一択です。
とはいえ、本当に無限ループ while(true){} みたいなことをしてしまうと、CPUが休めないので今のブラウザでは警告が出るでしょう。
なので時間をトリガーとして無限に処理するような、非同期の再帰呼び出しで実現します。
/* 前略 */
function drawFrame() {
screen.redraw();
requestAnimationFrame(drawFrame);
}
requestAnimationFrame(drawFrame);
/* 以下略 */
requestAnimationFrame は描画できそうなタイミングで処理する、というものなので、これに描画を任せます。
環境にもよりますが、だいたい 60fps 基準で良きタイミングで動きます。
描画内容の変更
前述の requestAnimationFrame
は、コールバック関数の引数をもとに経過時間を取得できるため、処理や描画を行うタイミングを制御することが可能です。ただし気をつけないといけないのは、そもそものトリガーが厳密には一定ではなく環境依存です。環境上の最適時間で動作するからなめらかに見えるだけです。
つまり本当に60fpsで動くかどうかは分かりません。次に描画できるタイミングで、という処理であることからウィンドウやタブ非表示ではどうなっているかは分かりません。
だいたい「描画できるようになった」時点から描画以外の処理をしていては、いまいち時間を有効には使えていません。
操作性などを考慮すると、処理そのものは一定のタイミングで実施したいものです。というわけで、処理と描画は一緒にしない方針にします。
描画以外の処理については setTimeout を使うことにしました。こちらは 30fps にしています。
/* 前略 */
const spf = 1000 / 30; // msec per frame in 30fps
let px = 0, py = 0;
function gameFrame() {
const t = performance.now();
screen.setData(px, py, ' ');
px ++;
if (px >= screen.width) {
px = 0;
py++;
if (py >= screen.height) {
py = 0;
}
}
screen.setData(px, py, '@');
const delta = performance.now() - t;
setTimeout(gameFrame, spf - delta);
}
setTimeout(gameFrame, spf);
/* 以下略 */
performance.now() を使って、処理中の経過秒を考慮するような感じにして setTimeout の次回実施時間を調整しています。
なおタイミングを調整したところでブラウザがそれに近しい最短で実施されるだけなので、厳密ではないのですが、requestAnimationFrame
と違ってタブ状況などに左右されない(と私は信じている)ので、ゲーム内処理としてはこちらのほうが適していると考えました。
定期的な処理ですと setInterval を使用することも考えられますが、本当に定期的に実行されてしまい、前回処理が終わっていないまま並行処理されて、いわゆる「処理落ち」すらできずに意図しない状況になってしまいます。
処理自体は単純で、前の自分の座標に空白で上書きして座標を移動して「@」を出力することを繰り返し、動いているように見せているだけです。
リソース
gconsole.css
gconsole.js
g02.js
関連
00.画面表示
01.文字出力
03.画像出力
04.キー操作
05.速度を調整
06.敵の配置
07.ゲームにする
08.ゲームパッド