【JavaScript】requestAnimationFrameでゲームループを作る【ゲーム制作】

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>

コメント

タイトルとURLをコピーしました