フレームと秒の換算:FPSやBPMで起きる丸めのズレを数値で整理

フレームと秒の換算式、60fpsと30fpsの違い、BPMをフレームに直すときの端数によるズレ、固定タイムステップの考え方を、検算済みの数値例で分かりやすく解説します。

「無敵時間は30フレーム」「クールダウンは8秒」。ゲームの仕様書には、フレームで書かれた数値と秒で書かれた数値が混在しがちです。この2つを行き来するときに必要なのは、FPS(1秒あたりのフレーム数)という換算レートだけです。ところが、FPSを変えたときに体感の長さが変わってしまったり、BPMに合わせた演出が少しずつズレていったりと、換算まわりの不具合は意外と多く見かけます。この記事では、フレームと秒の換算の基本、端数の丸めで起きるズレの正体、そして固定タイムステップという考え方まで、数値例を交えて整理します。

基本の考え方:フレームは「時間の単位」ではなく「回数」

フレームは1秒を何等分したかによって長さが決まる単位です。換算式は2本だけです。

主なFPSでの1フレームの長さは次のとおりです。24fpsは約41.667ms、30fpsは約33.333ms、60fpsは約16.667ms、120fpsは約8.333ms、144fpsは約6.944msです。60fpsの1フレームは30fpsの半分の長さなので、「30フレームの無敵時間」は60fpsなら0.5秒、30fpsなら1秒になります。同じ数字でも、動いているFPSが違えば実時間が倍違うということです。

60fpsと30fpsの違いはどこに出るか

入力から画面に反映されるまでの最小単位が、60fpsなら約16.7ms、30fpsなら約33.3msになります。格闘ゲームやアクションゲームで60fpsが好まれるのは、判定やアニメーションを細かい刻みで扱えるためです。一方で、処理が重い場面では30fpsに落として安定させる設計も珍しくありません。大切なのは、仕様をフレームで書くのか、秒で書くのかを最初に決めておくことです。

具体的な数値例:フレームと秒を行き来する

まず、秒からフレームへの換算です。60fpsで1.5秒の演出は 60 × 1.5 = 90フレーム、30fpsなら45フレームです。0.35秒のヒットストップなら、60fpsで21フレーム、30fpsで10.5フレームになります。ここで半端が出ます。10.5フレームという表示はできないので、10か11に丸める必要があり、10なら約0.333秒、11なら約0.367秒と、元の値から最大で約17ms動きます。24fpsの映像寄りの設定では、0.35秒は8.4フレームとなり、8フレームに丸めると約0.333秒です。

逆に、フレームから秒への換算は割り算だけです。無敵時間12フレームは、60fpsで0.2秒、30fpsでも12フレームのままなら0.4秒になります。「0.2秒にしたい」という意図があるなら、30fpsでは6フレームに書き換えなければなりません。

クールダウンをフレームに直す

クールダウン短縮を含む例も確認しましょう。基本クールダウン8秒、短縮率20%、実効クールダウンを 基本CD ÷ (1 + 短縮率) で求める方式だと、8 ÷ 1.2 ≒ 6.667秒です。60fpsなら 6.667 × 60 = 400フレーム、30fpsなら200フレームで、どちらもきれいな整数になります。60秒間に使える最大回数は 60 ÷ 6.667 = 9回です。単純に 8 × (1 − 0.2) とする方式だと6.4秒(384フレーム)になり、結果が変わります。短縮率を積み上げたときの挙動も違うので、どちらの式を使うかは仕様として明記しておきましょう。

BPMとフレーム:端数が積み重なると音楽とズレる

リズムゲームや、曲に合わせて動く演出では、BPMをフレームに換算します。式は 1拍(秒) = 60 ÷ BPM、フレーム数 = 1拍(秒) × FPS です。BPM120なら1拍は0.5秒で、60fpsでは30フレーム、8分音符なら15フレームとなり、割り切れるので問題は起きません。

厄介なのは割り切れないテンポです。BPM128では1拍が0.46875秒で、60fpsでは28.125フレームです。これを28フレームに丸めて「28フレームごとに拍を刻む」実装にすると、1拍あたり0.125フレーム(約2.08ms)ずつ早くなります。2.08msは気付かない小ささですが、64拍(16小節)で8フレーム、約133msのズレになり、400拍では約833msです。BPM140の場合は1拍が25.714フレームで、26に丸めると逆に1拍あたり約4.76msずつ遅れ、64拍で約305msに達します。

1回ごとの誤差が小さくても、同じ向きに繰り返すと線形に積み上がるのが落とし穴です。対策は、「前回からのフレーム数」を数えて判定するのではなく、曲の再生開始からの経過時間(秒)を基準に「いま何拍目か」を計算し直すことです。拍番号を経過時間から求めれば、丸め誤差は毎回リセットされて、ズレが蓄積しません。

固定タイムステップの考え方

端数の問題をもう一段根本から避けるのが、固定タイムステップです。ゲームロジックは常に一定の刻み(たとえば1/60秒)でのみ進め、描画は端末が出せるだけ出す、という分け方をします。ロジック側の「1ステップ」の長さが固定なので、フレームで書いた仕様(無敵30フレームなど)が、端末の性能に左右されず同じ実時間になります。典型的な骨組みは次のとおりです。

const DT = 1 / 60;       // ロジックの刻み(秒)
let acc = 0;
function loop(dtReal) {   // dtReal: 前回からの実経過秒
  acc += Math.min(dtReal, 0.25); // 巨大な値は上限を設ける
  while (acc >= DT) {
    update(DT);           // ロジックは常に一定の刻み
    acc -= DT;
  }
  render(acc / DT);       // 余りで描画を補間してもよい
}

この方式では、144Hzの画面でもロジックは1秒間に60回しか進みません。144 ÷ 60 は2.4で割り切れないため、描画側では「ロジックが進む回と進まない回」が混ざります。余り acc を使った補間をしないと、動きがわずかにカクつくことがあります。ここは、作品の性格に合わせて補間の有無を選ぶところです。

よくある落とし穴と判断基準

関連ツールの使い方とまとめ

秒数とFPSから必要なフレーム数を知りたいときはFPS→フレーム計算機、逆にフレームで決めた値の実時間を確かめたいときはフレーム→秒数計算機が便利です。BPMと音符の長さからフレーム数と丸めによるズレを見たい場合はBPM→フレーム計算機、短縮率つきのクールダウンはクールダウン計算機で確認できます。入力を1つずつ変えて、結果の変わり方を見るのがコツです。

まとめ

フレームと秒の換算は、FPSを掛けるか割るかだけです。しかし、FPSが変われば同じフレーム数の実時間も変わること、割り切れない値は丸めのたびに誤差が出て繰り返しで蓄積することに注意が必要です。BPMに同期させる処理は経過時間から毎回計算し、ロジックは固定タイムステップで進める。この2点を押さえれば、環境が変わっても同じ手触りを保てます。最後は実機でのテストプレイで確かめてください。

この記事に関連するツール