ゲーム開発入門① ゲームループの仕組みを知ろう
入力・更新・描画の順番を、キャラクターの位置が変わる数値で追います。FPSで速さが変わる原因、経過時間の使い方、ブラウザーの繰り返し、練習の解答を解説します。
右キーを押すとキャラクターが動く。遊ぶ側では当たり前でも、作る側になると「押している間って、どうやって調べ続けるの?」と気になりますよね。
今回は、ゲームが動き続ける仕組みを3段階に分けます。難しい画像や3Dモデルは使いません。キャラクターの位置を数字で追いながら、「画面の裏で何をしているか」をつかみましょう。
1回分の処理をフレームと呼ぶ
ゲームは、短い時間ごとに状況を調べ直しています。基本形は次のとおりです。
入力を読む → 状態を更新する → 画面へ描く → 次の回へ
この繰り返しがゲームループです。連続する画面の1枚をフレームと呼びます。まずは「1回更新し、1枚描く」という単純な形を考えます。実際のエンジンでは物理計算と描画を別の間隔で動かす場合もあるので、必ず1対1というわけではありません。
ここでは右方向の位置を x、移動の速さを speed とします。座標は、位置を数で表したものです。詳しくは座標と当たり判定で扱います。
入力を読む
右キーについて知りたいことにも、2種類あります。
| 知りたいこと | 使う場面 |
|---|---|
| 今、押し続けられているか | 歩く、車を加速する |
| 今回、押された瞬間か | 決定する、1発だけ撃つ |
移動は「押している間ずっと」が自然です。一方、決定は1回押したら1回だけ動いてほしいですよね。ここを同じにすると、キーを長く押しただけで画面が何度も切り替わることがあります。
仕組みを小さく考えるなら、右キーを押したときに rightHeld = true、離したときに false とします。true と false は、はい・いいえに相当する真偽値です。
ループはこの値を読み、「今回は右へ進むか」を判断します。入力を読むたびに画像を動かすのではなく、まず判断結果を次の更新へ渡します。
状態を更新する
状態は、位置・体力・得点・残り時間など、その瞬間のゲームを表すデータです。
1. 毎回2ずつ進める場合
let x = 10;
x = x + 2;
console.log(x);
x = x + 2;
console.log(x);
結果は 12、14 です。x = x + 2 は、今の x に2を足して新しい x にする操作です。等式ではなく代入なので、数学の式と読み方が違います。
ただし毎回2ずつ進むと、1秒に30回更新する端末では60、60回更新する端末では120進みます。同じ右キーなのに、速い端末の方が移動まで速くなる。これは困りますね。
2. 1秒あたりの速さへ変える
使いたい式は次です。
移動距離 = 1秒あたりの速さ × 前の更新から経過した秒数
速さを 120ピクセル/秒 にすると、1/60秒では2ピクセル、1/30秒では4ピクセル進みます。更新間隔が長い回は、その分だけ長く進むわけです。
前の更新からの経過時間は deltaTime、dt などと呼ばれます。ここでは dt とし、単位を秒にそろえます。
let x = 10;
const speed = 120;
const dt = 0.05;
const rightHeld = true;
if (rightHeld) {
x = x + speed * dt;
}
console.log(x);
結果は 16。120×0.05で6進み、10+6になるからです。
残り時間なら、同じ考え方で remainingTime = remainingTime - dt と減らせます。移動も時計も、「1回あたり」なのか「1秒あたり」なのかをそろえるのが大切です。
3. 数字だけで3回の更新を確かめる
次はJavaScriptの独立した実行例です。ブラウザーの開発者ツールのコンソール、またはNode.jsで実行できます。コンソールは、命令や確認結果を文字で表示する場所です。
let x = 10;
const speed = 120;
const inputs = [true, true, false];
const elapsedTimes = [0.05, 0.1, 0.05];
for (let i = 0; i < inputs.length; i++) {
const rightHeld = inputs[i];
const dt = elapsedTimes[i];
if (rightHeld) {
x += speed * dt;
}
console.log("x =", x);
}
x = 16
x = 28
x = 28
[] は複数の値を並べる配列です。i は0から順番に位置を選びます。1回目は6進み、2回目は12進み、3回目はキーが押されていないので止まります。
この例は実時間を待つゲームではなく、用意した経過時間で更新を再現する練習です。時計を使わずに同じ結果を確かめられるので、動きの計算を調べるときに便利です。
最後に画面へ描く
更新後のデータを使い、背景・キャラクター・得点などを表示します。この画面を作る処理が描画です。
背景を消して描き直す
新しいx座標へプレイヤーを描く
今の得点を表示する
更新前に描けば、古い位置を表示してから次の位置を計算することになります。まずは入力→更新→描画を分けると、どの回の位置を表示しているかが分かりやすくなります。
また、得点を増やす処理を描画へ入れないようにします。同じ状態をもう一度描き直しただけなのに得点が増えたら、表示とルールが混ざっているためです。更新はデータを変える、描画はそのデータを見せると分けておきましょう。
ブラウザーで繰り返すときの注意
JavaScriptのページで while (true) を回し続けると、ブラウザーが画面や操作を処理する余裕を失い、固まることがあります。無限ループを書けばゲームになるわけではありません。
ブラウザーでは requestAnimationFrame という機能に「次の描画のタイミングでこの関数を呼んで」と予約できます。
次は仕組みを読むための一部分の例です。update と draw は、自分で用意する更新・描画関数を表しているため、このまま単独では実行しません。
let previousTime;
function frame(timeMs) {
const dt = previousTime === undefined
? 0
: (timeMs - previousTime) / 1000;
previousTime = timeMs;
update(dt);
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
timeMs はミリ秒なので、差を1000で割って秒に変えます。最初の呼び出しには前の時間がないため、0秒として扱います。
requestAnimationFrame は1回分の予約です。継続するには、関数内で次を予約します。画面を隠したタブなどでは呼び出しが止まる場合があり、必ず60回/秒になるものではありません。MDNの公式説明でも、タイムスタンプを使って速さを計算する点が説明されています。
長い中断後に大きな dt をそのまま使うと、位置が急に飛ぶこともあります。復帰時に時間の基準を戻す、ゲームを一時停止するなどの対応が必要です。単に dt を小さく制限する方法は、その間の経過を捨てるので、実時間に正確な時計にはそのまま使えません。
FPSは1秒間のフレーム数
FPSは、1秒に何枚のフレームを処理・生成するかを表す指標です。測定する箇所によって、画面に実際に届く枚数と違う場合もあります。
60 FPSの時間幅は、1秒÷60で約0.0167秒、つまり約16.7ミリ秒です。平均が60でも、一部だけ極端に時間がかかると引っかかって見えることがあります。
高いFPSだけが目標ではありません。安定した処理、端末への負担、電池の持ちなども考えます。ゲームジャンルの「一人称視点のシューティング」もFPSと略すので、文脈で見分けてください。
さらに物理計算では、一定の時間刻みで複数回更新する方式もあります。単純に経過時間を掛けただけで、衝突まで完全に同じ結果になるわけではありません。まず今回の移動を理解してから、使うエンジンの更新方式へ進めば大丈夫です。
練習:正しい順番へ並べる
次の3つを並べ替えてください。
- プレイヤーを描く
- 左キーが押されたか調べる
- プレイヤーの座標を変える
さらに、x = 50 から左へ秒速80ピクセル、0.25秒進んだときの位置も求めてください。
模範解答
順番は「左キーが押されたか調べる → 座標を変える → 描く」です。
let x = 50;
const speed = 80;
const dt = 0.25;
x -= speed * dt;
console.log(x);
出力は 30 です。
解説
左へ進むので、X座標から距離を引きます。80×0.25は20、50−20は30。新しい30を使って描画すれば、今回の入力が反映された画面になります。
「2ずつ進む」を「速さ×時間」に言い換えられたら、この回の重要な部分はつかめています。
次は座標と当たり判定で、「どこにいるか」から「何に触れたか」へ進みましょう。