ゲームループとTick処理 — 遅いPCでも、壁をすり抜けないために
ゲームの画面は、パラパラ漫画です。1秒間に何十回も「入力を読む → 世界を1コマ進める → 絵を描く」を繰り返していて、この繰り返しをゲームループ、1回分を1フレームと呼びます。60fpsなら1秒に60回、1フレームに使える時間はわずか16.7ミリ秒です。
やっかいなのは、1秒に何回まわせるかがPCによって違うこと。速いPCと遅いPCで同じゲームを動かすと、キャラの速さが変わってしまったり、壁をすり抜けてしまったりします。下の図1で、フレームレートの違う2台のPCに同じキャラを同じコードで走らせてみてください。キャラは壁で止まるはずです。
① フレームごとに固定量 — 遅いPCでは、キャラも遅い
いちばん素朴な書き方は、x += 3 のように1フレームで進む距離を決めてしまうことです。これだと、1秒に60回まわるPCは60回、20回しかまわらないPCは20回しか進みません。図1で走らせると、PC Bのキャラは3倍遅い。昔のゲームには、速いPCで遊ぶと敵まで速くなってしまうものがありましたが、原因はこれです。
② 経過時間(dt)をかける — 速さはそろうが、飛ぶ
そこで、前のフレームから実際に何秒たったか(dt)を測り、x += 速さ × dt とします。フレームが少ないPCは1回でたくさん進むので、1秒あたりの進み方がそろいます。
ところが、ディスクの読み込みや別アプリのせいで1フレームだけ0.5秒止まったらどうなるか。次のフレームの dt は0.5秒になり、キャラは一気に大きく瞬間移動します。当たり判定は「いまの位置で壁と重なっているか」しか見ていないので、壁の手前から向こう側へ飛んでしまうと、一度も重ならずにすり抜けます。図1の「カクつかせる」で起きるのがこれです。
③ 固定Tick — 時間を貯めて、1/60秒ずつ払う
解決策は、世界を進める単位をいつも同じ1/60秒(1Tick)に固定することです。経過時間は貯金箱(accumulator)に入れ、1Tick分たまるごとに1回ずつ世界を進めて当たり判定をします。0.5秒止まったあとのフレームでは、30Tick分をまとめて30回に分けて進めるので、壁の手前で必ず止まれます。描画は今まで通り1フレームに1回なので、描画の回数と世界を進める回数を切り離した、ということです。
物理演算や対戦ゲームのように、どのPCでも同じ結果になってほしい処理はたいていこの方式です。ただし、止まりすぎると取り戻しのTickが増えすぎて余計に重くなる(spiral of death)ので、1フレームで進めるTick数には上限を設けます。また、Tickの途中のタイミングで描くとカクついて見えるため、前後のTickの位置を補間して描く工夫もよく使われます。
- ゲームループ
- 入力・更新・描画を、ゲームが終わるまで繰り返すループ。
- フレーム / fps
- ループ1回分(=画面1枚)。fpsは1秒あたりのフレーム数。
- dt(デルタタイム)
- 前のフレームから今のフレームまでに経過した時間。
- Tick / 固定タイムステップ
- 世界を進める一定の時間単位。描画のフレームとは別に、決まった刻みで更新する方式。
- すり抜け(トンネリング)
- 1回の更新での移動が大きすぎて、薄い壁と一度も重ならずに通り抜けてしまう現象。
まとめ
ゲームループは入力 → 更新 → 描画の繰り返しで、その速さはPCや状況によって変わります。1フレームに固定量進めると速さがPC次第になり、経過時間をかけるとカクついた瞬間に大きく飛ぶ。経過時間を貯めて決まった刻み(Tick)で世界を進めると、フレームレートに関係なく同じ動きになります。「絵を描く回数」と「世界を進める回数」を分けて考えることが、ゲームループの核心です。