requestAnimationFrameとFPSの制御
ゲームプログラミングではゲームループ構造を作り、ループの中で画面の更新処理を実装する。更新処理を担当するゲームループ用関数を用意し、タイマー機能を使い一定間隔で実行し続ける。1秒間に何回更新するかをFPS(フレーム・パー・セコンド)という単位で表す。ゲーム業界では「フレームレート」と呼ばれることが多い。ゲームジャンルを表すFPS(ファーストパーソン・シューティングゲーム)とは別物。
JavaScriptでゲームループを作るには、setInterval、requestAnimationFrameのどちらかを使う場合が多い。setIntervalは更新間隔をミリ秒単位で渡せば、指定したコールバック関数を設定したミリ秒で呼び続けるので手軽。実際、ゲーム制作によく使われる。だが、ゲーム制作ではrequestAnimationFrameが好ましい。
setIntervalは設定した間隔で必ず実行される。requestAnimationFrameはブラウザの画面更新単位で実行される。setIntervalはブラウザの画面更新単位とズレることがあり、アニメーションがカクつきやすい。requestAnimationFrameでも発生するが、setIntervalよりは滑らかで自然な表現になる。
ブラウザはディスプレイのリフレッシュレート等を元に画面更新単位を決めるため、requestAnimationFrameを使うと環境の違いによりFPSが安定しない(とはいっても多くの場合60FPSとなる)。
Window.requestAnimationFrame() – Web API | MDN
リフレッシュレートは、そのディスプレイが1秒間に何回画面更新できるを表す。○Hzで表され、60Hzなら1秒間に60回画面更新可能。
ゲームループのスクリプト部分が以下。
let animID; let isAnim = 0; const FPS = 60; let frame = 0; let startTime; let nowTime; function init() { // ①開始時間を保存 startTime = performance.now(); mainLoop(); } function mainLoop() { // ②現在時間を取得 nowTime = performance.now(); // 経過時間を代入 let elapsedTime = nowTime - startTime; // ③描画処理する理想時間をidealTimeに代入 let idealTime = frame * (1000 / FPS); // ④経過時間が1フレーム分のミリ秒以上なら実行 if (idealTime < elapsedTime) { // この中に更新処理を描く // HTML側にあるpタグのテキストを変更 document.getElementById('frame_counter').innerText = frame; frame++; // ⑤経過時間が1秒を超えたらリセット if (elapsedTime >= 1000) { startTime = nowTime; frame = 0; } } animID = requestAnimationFrame(mainLoop); } function onClick() { if (!isAnim) { init(); } else { cancelAnimationFrame(animID); } isAnim = 1 - isAnim; }
まず、初期化用のinit()で①最初の時間を取得。mainLoop()内では、②現在時間を取得し、現在時間から1つ前の時間を引いて経過時間を取得。
次に、③現在のフレームに、1フレームにかかるミリ秒をかける。計算結果が次の描画を行う理想時間となる。
理想時間(ms) = 現在のフレーム × (1000 / FPS)
④経過時間が理想時間を過ぎていれば更新処理を行う。更新処理を行ったら1フレーム増やす。例えば、60FPSの場合、開始から500ミリ秒経過した時点で30フレーム表示しているのが理想(30 * (1000 / 60) = 500ms)。経過時間が500ミリ秒を超えていれば31回目の更新を行い、未満であれば行わない。
⑤経過時間が1秒を超えたら、次の1秒間を計算するためにstartTimeをnowTimeでリセットし、フレーム数も0にリセット。
以下がサンプルスクリプト。
実行と開始ボタンをクリックで消費フレーム数が延々表示される。もう一度クリックすると停止。
コード全文。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>requestAnimationFrameを使ったゲームループを実装</title>
</head>
<script type="text/javascript">
let animID;
let isAnim = 0;
const FPS = 60;
let frame = 0;
let startTime;
let nowTime;
function init() {
// ①開始時間を保存
startTime = performance.now();
mainLoop();
}
function mainLoop() {
// ②現在時間を取得
nowTime = performance.now();
// 経過時間を代入
let elapsedTime = nowTime - startTime;
// ③描画処理する理想時間をidealTimeに代入
let idealTime = frame * (1000 / FPS);
// ④経過時間が1フレーム分のミリ秒以上なら実行
if (idealTime < elapsedTime) {
document.getElementById('frame_counter').innerText = frame;
frame++;
// ⑤経過時間が1秒を超えたらリセット
if (elapsedTime >= 1000) {
startTime = nowTime;
frame = 0;
}
}
animID = requestAnimationFrame(mainLoop);
}
function onClick() {
if (!isAnim) {
init();
} else {
cancelAnimationFrame(animID);
}
isAnim = 1 - isAnim;
}
</script>
<body>
<p id="frame_counter"></p>
<input type="button" value="実行と停止" onclick="onClick();"/>
</body>
</html>
コメント