はじめに

カードゲーム開発がひと段落して、次は何を作ろうかと思った際に、元プログラマーの一言

ブロック崩しを作れれば、たいてい何でも作れるようになるよ

という情報を鵜呑みにし、数日前からブロック崩しゲーム制作に取り掛かっています。

基本のゲームロジックはもう完成して遊べる状態ではあるのですが、普通のブロック崩しでは当然つまらないので、アイデア出しをして独自のゲーム性を盛り込む試行錯誤をしてますが、公開できる状態になれば、注目プロジェクトにまた、記事を作成予定です。

カードゲーム制作では、基本的にDOM操作を主体とした、WEB制作とほぼ同じ手法を使ってSPAで作りましたが、

今回は、新たな試みとして Canvas を主体とした アニメーション処理をベースにして開発を進めています。

その際に、基本となるframe制御の概念をAIと壁打ちして学習した内容をざっとまとめました。

Canvasでのフレームレート制御とアニメーション処理の基本

Webゲームやアニメーションにおいて、フレームレートの制御は非常に重要です。

requestAnimationFrameperformance.now()を駆使して、スムーズで一貫性のあるアニメーションを実現する方法について詳しく解説します。

これらの基礎を理解し、開発中の躓きポイントを乗り越えるためのテクニックも紹介します。

1. フレームレートとは?

フレームレート(FPS)は、1秒間に描画されるフレームの数を示します。

例えば、60FPSは1秒間に60フレームが描画されることを意味します。

フレームレートが高ければ高いほど、アニメーションが滑らかに見えますが、パフォーマンスに負担がかかるため、適切な制御が必要です。


2. requestAnimationFrameの使い方

基本的な使い方

requestAnimationFrame は、ブラウザのリフレッシュレート(通常60Hz)に合わせて、次のフレームを描画するタイミングを最適化します。

この関数を使うことで、アニメーションの描画がブラウザの描画サイクルに同期し、無駄なCPU負荷を減らすことができます。

function animate() {
  // 描画処理
  draw();

  // 次のフレームをリクエスト
  requestAnimationFrame(animate);
}

animate(); // アニメーション開始

requestAnimationFrame は再帰的に呼び出すことで、アニメーションを次々と描画します。これにより、ブラウザが描画を最適化し、効率的にアニメーションを描写できます。

requestAnimationFrame の利点

  1. パフォーマンス最適化

    • requestAnimationFrame はブラウザのリフレッシュタイミングに合わせてフレームを描画するため、CPUやGPUの無駄な負荷を抑え、効率的にアニメーションを描画できます。
      これにより、ゲームやアニメーションが滑らかに動作し、PCやモバイル端末のリソース消費を最小限に抑えることができます。

    • 特に、setInterval や setTimeout を使ってフレーム描画を行う場合、描画間隔が一定でなくなることがありますが、requestAnimationFrame はブラウザのリフレッシュタイミングに合わせて描画されるため、タイミングが整い、フレームレートが安定します。

  2. スムーズなアニメーション

    • requestAnimationFrame は、ブラウザがフレーム描画を制御するため、カクつきやフレームの飛び跳ねが発生しません。
      これにより、アニメーションが滑らかに描画され、ユーザー体験が向上します。

    • 特に、動きの速いアニメーションや複雑なエフェクトの場合、フレームの描画がスムーズであることが重要です。
      requestAnimationFrame はブラウザの描画サイクルに最適化されており、アニメーションが途切れず、一貫したパフォーマンスを提供します。

  3. 省電力モード

    • モバイルデバイスやノートPCでは、タブが非アクティブになると requestAnimationFrame が停止するため、無駄なリソース消費を抑えることができます。
      これにより、バッテリー駆動のデバイスでの効率的な動作が可能になります。

cancelAnimationFrameによる停止

描画が不要な場合、またはアニメーションを一時停止したい場合は、cancelAnimationFrame を使ってアニメーションを停止できます。

これにより、必要ない描画を止めることができ、リソースを節約することができます。

let animationId;

function animate() {
  // 描画処理
  draw();

  // 次のフレームをリクエスト
  animationId = requestAnimationFrame(animate);
}

// アニメーションを停止
function stopAnimation() {
  cancelAnimationFrame(animationId);
}

cancelAnimationFrame は、指定されたアニメーションフレームのIDをキャンセルします。requestAnimationFrame が返すIDを保持し、そのIDを指定することで、不要なアニメーションを停止できます。

この機能は、特にページを閉じたり、ユーザーが他の操作を行った際にアニメーションを停止させるのに便利です。


まとめ

requestAnimationFrame は、ブラウザの描画タイミングに最適化されたアニメーションを実現するための強力なツールです。

これを使うことで、無駄なCPU負荷を減らし、滑らかでスムーズなアニメーションを描画できます。また、cancelAnimationFrame を利用してアニメーションを停止し、リソースの無駄を防ぐこともできます。この機能を活用することで、効率的かつ高品質なアニメーションを実現することができます。

こちらの内容を深堀して、さらに詳しくまとめます。


3. performance.now() による精度の高い時間管理

時間管理の重要性

ゲームやアニメーションでは、フレーム間の経過時間を正確に計測することが非常に重要です。

なぜなら、ゲーム内のオブジェクト(例えば、ボールの動きやキャラクターの移動)がフレームレートに依存せず、一定の速度で動くことが求められるからです。

performance.now()は、ミリ秒単位で高精度なタイムスタンプを取得できるAPIで、Date.now()と異なり、実行中のコードの遅延やブラウザの処理に影響されることなく、正確な経過時間を得ることができます。

これにより、フレーム間隔を正確に管理し、アニメーションやゲームの動作が一貫して行われるようになります。

const lastTime = performance.now(); // 最初のフレームの時間

function animate() {
  const now = performance.now(); // 現在の時間
  const deltaTime = now - lastTime; // 経過時間

  // 描画の更新処理
  updateGame(deltaTime); // ゲームのロジックを更新

  requestAnimationFrame(animate); // 次のフレームをリクエスト
}

deltaTime を使ったフレームレート調整

deltaTime(前回フレームと現在フレームの時間差)を使うことにより、フレームレートに依存せずにゲーム内の動作を一定に保つことができます。
これにより、PCやスマートフォンなど、異なるデバイスで一貫した挙動を実現できます。

例えば、60FPSを目指している場合、1フレームあたり16.67ミリ秒(1000ms ÷ 60)で描画を進めたいところです。しかし、デバイスの性能や負荷によって、フレームごとの描画間隔がわずかに異なることがあります。

そこで、deltaTime を使って、時間差を調整し、ゲーム内の動きをフレームレートに依存せず一定の速さで進めます。

let lastTime = performance.now();
const deltaTime = 16.67; // 60FPSを目指す場合のフレーム間隔(ミリ秒)

function animate() {
  const now = performance.now();
  const elapsedTime = now - lastTime; // 経過時間(フレームごとの時間差)

  // フレーム間隔が16.67ms(60FPS)を超えた場合のみゲームを更新
  if (elapsedTime > deltaTime) {
    lastTime = now - (elapsedTime % deltaTime); // 次のフレーム更新時間を調整

    // ゲームロジックを更新
    updateGame();
  }

  requestAnimationFrame(animate); // 次のフレームをリクエスト
}

deltaTime を使用する利点

  1. フレームレートの変動を補正deltaTimeを使うことで、フレームレートが安定していなくても、ゲームの動きが常に一定の速度で進みます。これにより、フレームレートが低下しても、プレイヤーの体験が一定に保たれます。

  2. 異なるデバイスでの一貫した挙動: 異なる性能のデバイス(PC、モバイル、低スペックのデバイスなど)では、フレームレートが異なることがあります。deltaTimeを使用すると、デバイス間で動作が一貫し、同じ動きが確実に再現されます。

  3. 物理演算やキャラクターの動きに応用: ゲーム内で物理演算やキャラクターの移動を行う場合、deltaTimeを使って速度や加速度を調整できます。これにより、フレームレートが異なる環境でも、キャラクターの動きや衝突判定が一貫した速度で行われます。

const speed = 200; // 単位: px/sec

function moveObject(deltaTime) {
  object.x += speed * deltaTime / 1000; // 毎フレームの移動距離
}

deltaTime で物理演算を制御する方法

例えば、物理エンジンを使ったゲームで、オブジェクトが一定速度で移動することを保証するには、deltaTimeを使って計算を調整します。フレームレートが高いときにはオブジェクトが速すぎ、低いときには遅すぎるため、deltaTimeを使って調整することで、速度をフレームに依存しないようにできます。

4. フレームレート制御のテクニック

ゲームやアニメーションをスムーズに動かすためには、フレームレートの管理と最適化が非常に重要です。

以下では、フレームレート制御におけるテクニックをいくつか紹介します。

可変フレームレートと固定フレームレート

ゲームのフレームレートは、一定に保つことが望ましい場合が多いですが、時には可変フレームレートを採用することもあります。それぞれのメリットとデメリット、使い方について説明します。

固定フレームレート

固定フレームレートでは、ゲームの更新(ゲームロジックや描画処理)が決められた間隔で行われます。たとえば、60FPS(1秒間に60フレーム)を維持することを目指します。

  • deltaTimeによる調整: 固定フレームレートでは、毎フレームがほぼ同じ時間間隔で実行されるため、deltaTimeを使ってアニメーションやゲームロジックの更新をフレームレートに依存しない形で管理します。deltaTimeを用いることで、たとえば60FPSと30FPSの違いに関わらず、ゲーム内の動きが一貫して行われます。
const deltaTime = 16.67; // 固定フレームレート(60FPSを目指す)

function updateGame() {
  // ゲームロジック更新
  moveObject(deltaTime);
}

可変フレームレート

可変フレームレートでは、フレーム更新のタイミングがrequestAnimationFrameにより動的に決定されます。これにより、デバイスのパフォーマンスやフレームレートに応じて、描画の間隔が調整されます。

  • requestAnimationFrameの活用: 可変フレームレートでは、requestAnimationFrameで取得した時間を基に、フレーム間隔を調整します。この方法では、CPU負荷を最小限に抑えつつ、滑らかなアニメーションを実現できます。deltaTimeを使用することで、フレームレートが変動してもゲームの動きが一定になります。
let lastTime = performance.now();

function animate() {
  const now = performance.now();
  const elapsedTime = now - lastTime;

  lastTime = now;

  // ゲームロジックの更新
  moveObject(elapsedTime / 1000);

  requestAnimationFrame(animate);
}

animate();

ゲームの物理演算の安定化

ゲーム内での物理演算(ボールの動きやキャラクターの移動など)は、フレームレートに依存しないように設計することが重要です。これを実現するために、deltaTimeを使って速度や加速度を調整し、どんなフレームレートでも動作が一貫するようにします。

deltaTime を使った物理演算

物理演算をフレームレートに依存しないようにするためには、deltaTimeを使って時間に基づいて位置や速度を更新します。これにより、異なるデバイスやフレームレートでも、オブジェクトの動きが一定の速度で進行します。

const speed = 200; // 単位: px/sec

function moveObject(deltaTime) {
  object.x += speed * deltaTime; // 毎フレームの移動距離
}
  • 速度や加速度の調整deltaTimeを使って、物理シミュレーションや動きの速度をフレームに依存せず安定させます。これにより、低FPSでもオブジェクトが遅くなることなく動き、60FPSでも速くなりすぎることがありません。

アニメーションの最適化

アニメーションのパフォーマンスを最適化することも、ゲームやアニメーションの効率を高めるためには重要です。以下のテクニックを使うことで、パフォーマンスが向上し、スムーズな動作を実現できます。

必要な時だけ描画

背景や静止したオブジェクトを毎フレーム描画するのではなく、動きがある部分のみを更新するようにします。これにより、描画の負荷が軽減され、パフォーマンスが向上します。

  • オフスクリーンキャンバス: オフスクリーンキャンバスを利用することで、描画処理をバックグラウンドで行い、実際に画面に表示する際に最小限のリソースで済むようにできます。これにより、描画の負荷を軽減し、効率よくアニメーションを実行できます。
const offscreenCanvas = new OffscreenCanvas(800, 600);
const offscreenCtx = offscreenCanvas.getContext('2d');

// バックグラウンドで描画
offscreenCtx.clearRect(0, 0, 800, 600);
offscreenCtx.drawImage(image, x, y);

// 必要なときに画面に表示
ctx.drawImage(offscreenCanvas, 0, 0);

アニメーションのフレーム更新を制限

アニメーションを常に全フレーム描画するのではなく、動きがある部分のみ更新することで、処理負荷を減らします。たとえば、アニメーションのフレーム更新間隔を調整し、必要に応じて描画頻度を下げることも効果的です。

5. 開発中に遭遇しやすい躓きとその対策

ゲームやアニメーションの開発では、さまざまな問題に直面することがあります。特にパフォーマンスやフレームレートに関連する問題はよく発生するので、これらに対する対策を理解しておくことが重要です。以下では、開発中に遭遇しやすい躓きとその解決方法を紹介します。

1. FPSが低すぎてアニメーションがカクつく

原因:

描画処理が重い場合や、ゲーム内で複雑な計算が行われていると、フレームレートが低下し、アニメーションがカクつくことがあります。これにより、ゲーム体験が不快なものになることがあります。

対策:

  • 必要なときだけ描画:背景や静止したオブジェクトを毎フレーム描画するのではなく、動きがある部分のみ更新します。例えば、動いているキャラクターやボールなど、ゲーム内で変化する部分だけを更新するようにしましょう。

  • オフスクリーンキャンバスの活用:オフスクリーンキャンバスを使って、描画処理をバックグラウンドで行い、計算負荷を分散させます。描画結果をオフスクリーンキャンバスに描き、必要なタイミングでメインのキャンバスに表示することで、フレームレートの低下を防ぎます。

    const offscreenCanvas = new OffscreenCanvas(800, 600);
    const offscreenCtx = offscreenCanvas.getContext('2d');
    
    // バックグラウンドで描画処理
    offscreenCtx.clearRect(0, 0, 800, 600);
    offscreenCtx.drawImage(image, x, y);
    
    // メインキャンバスに描画
    ctx.drawImage(offscreenCanvas, 0, 0);
    
  • 計算処理の分割:重い計算処理を1フレームに詰め込むのではなく、複数フレームに分けて処理する方法を採ることも有効です。例えば、物理エンジンや衝突判定など、計算量が多い処理は少しずつ行い、アニメーションのフレームレートに影響を与えないようにします。


2. 異なるデバイス間で動作が異なる

原因:

異なる性能のデバイス(高性能PC、スマートフォン、低スペックPCなど)では、フレームレートや描画能力が異なります。このため、動作に差が出てしまい、ゲーム体験が一貫しないことがあります。

対策:

  • deltaTimeを使用して動作をフレームレートに依存しないようにするdeltaTimeを使うことで、異なるデバイスやフレームレートでも、動作が一定の速度で進行するように調整できます。これにより、低FPSのデバイスでも、高FPSのデバイスでも、オブジェクトの動きやゲーム内のアクションが一貫して行われます。

    let lastTime = performance.now();
    
    function animate() {
      const now = performance.now();
      const elapsedTime = now - lastTime;
      lastTime = now;
    
      // 動きの調整にdeltaTimeを使用
      moveObject(elapsedTime / 1000); // 毎秒200pxで移動
    
      requestAnimationFrame(animate);
    }
    
    animate();
    
  • フレームレートに基づくロジックの変更:動作が遅くなるときや、高性能デバイスで負荷が高い場合、アニメーションやロジックを調整するための処理を加えることも検討しましょう。例えば、高性能なデバイスでは、詳細なグラフィックやエフェクトを表示し、低性能なデバイスでは簡易的な描画を行うといった調整が可能です。


3. requestAnimationFrame が効かない場合

原因:

requestAnimationFrameが効かない場合、アニメーションループの設定が正しくないか、requestAnimationFrameが無効化されている可能性があります。これが原因で、アニメーションが止まったり、描画されないことがあります。

対策:

  • cancelAnimationFrameを使ったアニメーション管理requestAnimationFramecancelAnimationFrameを組み合わせることで、アニメーションの開始と停止を適切に管理できます。cancelAnimationFrameを使うことで、不要なアニメーションをキャンセルし、リソースを無駄に消費するのを防ぎます。

    let animationId;
    
    function animate() {
      // 描画処理
      draw();
    
      // 次のフレームをリクエスト
      animationId = requestAnimationFrame(animate);
    }
    
    function stopAnimation() {
      // アニメーションを停止
      cancelAnimationFrame(animationId);
    }
    
    animate(); // アニメーション開始
    
  • ブラウザのタブが非アクティブな場合の動作確認requestAnimationFrameはブラウザが非アクティブなタブに切り替わった場合に描画を停止します。これにより、無駄なリソース消費を防ぎますが、開発中に予期しない動作を引き起こすこともあります。この場合、デバッグ時にはタブの状態を確認し、描画が停止していることを理解する必要があります。

  • requestAnimationFrameがサポートされていない場合の対策:古いブラウザや互換性のないブラウザではrequestAnimationFrameがサポートされていないことがあります。その場合、ポリフィル(互換性のない環境に機能を追加するコード)を使用することで、requestAnimationFrameをサポートしない環境でも動作を保証できます。

    if (!window.requestAnimationFrame) {
      window.requestAnimationFrame = function(callback) {
        return setTimeout(callback, 1000 / 60); // 60FPSの代替
      };
    }