Chapter 96

アニメーションループ・FPS

miku
miku
2021.11.23に更新

p5.jsの draw() のような、アニメーションを表現するために1秒間に何十回も呼ばれる関数の構造のことをアニメーションループと呼ぶ。JavaScriptでアニメーションループを作るには requestAnimationFrame の使用が推奨されており、こちらの使い方を解説する。それに加えて、非推奨である setTimeout を使用する旧式の考え方も扱っておきたい。

setTimeout

const delay = 1000 / 60;

function draw() {
  // ここに何かを描画

  setTimeout(draw, delay);
}

draw();

setTimeout(func, delay) を実行すると、delay ミリ秒後に func 関数が呼び出される。func の中に setTimeout(func, delay) を記述すると delay ミリ秒おきに func が呼び出され続けるので、アニメーションループを作ることができる。

この draw は今用意した関数で、 p5.jsの draw とは関係がない。今は素のJavaScriptのみで計測をしているので、p5.jsを読み込まないように注意しよう。

たとえば1秒間に60回程呼び出したい場合は、1秒 = 1000ミリ秒なので 1000 / 60 を指定すればいい。実際にアニメーションループの関数が1秒間あたりに呼ばれる回数のことをFPSと呼ぶ。

ただしこの方法でのアニメーションループは非推奨になっているので、後述する requestAnimationFrame を利用したほうがいい。

setInterval

setTimeout と似た機能として setInterval という関数がある。

const delay = 1000 / 60;

function draw() {
  // ここに何かを描画
}

setInterval(draw, delay);

setInterval(func, delay) と、引数に関数とディレイさせるミリ秒を指定するのは setTimeout と同様だが、setTimeout は一回限りの関数呼び出しに対して、setInterval は意図的に止めない限り delay ミリ秒おきに func を呼び出し続けるという特徴がある。

setTimeoutfunc の実行にどれだけ時間がかかろうが、func の終了後 delay ミリ秒待ってから次の func を呼び出す。それに対して setInterval は最初に決めた時間おきに関数を呼び出すので、ディレイの時間が一定ではない。ディレイより長い時間 func の方がかかるのなら、func の終了後すぐに次の func が呼ばれることになる。

requestAnimationFrame

setTimeout によりアニメーションループの構造を作ることができたが、この方法での表現は幾つか問題点がある。

ブラウザでは複数のタブを開くことができるので、単体のアプリケーションを実行させる環境だと思ってコードを書くべきではない。たとえばコードを実行しているタブがあり、そこから他のタブに移動してYouTubeを見たり、ゲームをしていたりすると、放置ゲームのようなものでない限り、裏でアニメーションループが動き続ける必要がない。しかし setTimeout でのアニメーションループではタブ切り替えをしても実行され続けるので他のタブにも影響を与える。

他にも setTimeout ではディレイを指定する必要があるので、たとえば 1000 / 60 を指定すると、理想となるFPSは 60 になってほしいということになるが、FPSの上限は利用者の端末のスペックやディスプレイの性能に依存するので、こちらがFPSを固定で指定すべきではない。

ここで setTimeout の代替となる requestAnimationFrame を利用すると上記の問題は解決する。

function draw() {
  // ここに何かを描画

  requestAnimationFrame(draw);
}

draw();

requestAnimationFrame(func)func が定期的に呼び出される。setTimeout と違い、ディレイを指定する必要が無く、ディスプレイの更新頻度に合わせてなるべく高いFPSの数値を出そうとする。それに加え、タブ切り替えをして裏に回ると関数の呼び出しが一時停止され、func による描画の更新とブラウザ側が画面を更新するタイミングを合わせるなどの機能があり、アニメーションループを実現するにはこの関数を使う以外に手はない。

FPSを測る

次にFPSの測り方を扱う。FPSはアニメーションループ用の関数が1秒間に呼ばれる回数なのだから、その関数の中にカウンタを入れておき、1秒経った時点でカウンタの値を出力すればいい。

let count = 0;
let prevTime = 0;

function draw(time) {
  if (time - prevTime >= 1000) {
    console.log(count);
    count = 0;
    prevTime = time;
  }

  count++;
  requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

requestAnimationFrame に指定した関数の引数には、タブを開いてから今この関数が呼び出されたときまでの経過時間がミリ秒単位で入っている。これを利用して、前回の経過時間を prevTime に入れておき、今の経過時間 time から prevTime を引いたものが1000以上なら前の計測から1秒経ったということでカウンタの値を出力する。

実用上ではほとんど問題ないだろうが、実は正確なFPSが出ているとはいえないかもしれない。というのは前回の時間から1秒以上経ったらカウンタを出力しているので、たとえば1.01秒経ってからカウンタを出力すると、それは1秒間あたりではなく、1.01秒あたりの関数呼び出しの回数になる。

let count = 0;
let prevTime = 0;

function draw(time) {
  const elapsedTime = time - prevTime;
  if (elapsedTime >= 1000) {
    console.log(count * (1000 / elapsedTime));
    count = 0;
    prevTime = time;
  }

  count++;
  requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

正確に1秒経った時点で関数呼び出しの回数を出力するのは事実上不可能なので、1000ミリ秒以上経った経過時間を elapsedTime だとすると、それを 1000 にするための比率 1000 / elapsedTime を計算して、その値を count に掛けたものをFPSにすればいい。