ことれいのもり

【PixiJS】ticker.addでasync/awaitを実現!ラッパークラスで簡単非同期処理

はじめに

ゲーム制作でPixiJSを使っていると、更新処理を ticker.add に登録して実行することがよくあります。

ただ、この処理が完了するのを待ちたい場面もありますよね。

この記事では、ticker.add を使った処理と async/await を簡単に組み合わせるためのラッパークラスを作成します。

前提

バージョン: PixiJS v8

参考リンク

PixiJSにおける非同期処理の問題点

PixiJSではゲームのフレーム更新のために Ticker を使用しています。

しかし、Tickerを使って非同期処理を実装すると、2つの問題が発生します!

問題点 1:非同期処理の完了を待つことができない

Ticker の処理は同期的に実行されます。

つまり、毎フレーム処理が進むため、非同期処理が完了したタイミングを正確に把握することが難しくなります。

問題点 2:コールバックが増えてしまう

非同期処理の結果を待つためにコールバックを実装すれば良いと思うかもしれません。

しかし、非同期処理の数だけコールバックの数も増えてしまい、処理がどんどん複雑になってしまいます。

コードの処理も読みにくくなってしまい、可読性が下がってしまいます。

これらの問題を解決するために async/await を使って非同期処理を同期的に扱うラッパークラスを作りました!

ラッパークラスの実装

TickerWrapperクラスを作成しました。

export class TickerWrapper {
  constructor(ticker) {
    this.ticker = ticker;
  }

  /**
   * 非同期にフレーム更新を待つラッパー関数
   * ticker.addで追加した処理の終了を待つ
   * @param {function} updateFunction フレームごとに実行したい処理の関数
   */
  async waitForUpdate(updateFunction) {
    return new Promise((resolve) => {
      this.ticker.add(async (ticker) => {
        await updateFunction(ticker);
        resolve();
      });
    });
  }
}

クラスの目的

TickerWrapperクラスの目的は、PixiJSで非同期のフレーム更新処理を簡単に扱えるようにすることです。

このクラスを使うことで、非同期処理を同期的に書くことができ、処理の流れも分かりやすくなります。

コンストラクタ

export class TickerWrapper {
  constructor(ticker) {
    this.ticker = ticker;
  }

コンストラクタは、PixiJS の Ticker を受け取り、それを this.ticker に保存します。

このクラスでは、 this.ticker.add を使ってフレームごとの処理を管理していきます。

waitForUpdate メソッド

 /**
   * 非同期にフレーム更新を待つラッパー関数
   * ticker.addで追加した処理の終了を待つ
   * @param {function} updateFunction フレームごとに実行したい処理の関数
   */
  async waitForUpdate(updateFunction) {
    return new Promise((resolve) => {
      this.ticker.add(async (ticker) => {
        await updateFunction(ticker);
        resolve();
      });
    });
  }
}

ここが一番大事なところです!

このメソッドは、asyncを使った非同期関数で、非同期処理を同期的に待つために使用します。

引数として受け取るのは、フレームごとに実行したい非同期処理の関数です。

this.ticker.add() を使うことで、毎フレーム処理が実行されますが、その処理の終了を await で待ちます。

処理が終わったら resolve() を呼び出し、 Promise を解決します。

この Promise は非同期処理が終了するまで待機するため、呼び出し元で処理が終了したタイミングを確認できます。

ラッパークラスの実装例

// ゲームオーバー処理
async handleGameOver(ticker) {
    // TickerWrapperを使う
    const tickerWrapper = new TickerWrapper(ticker);

    // カメラを振動させるクラス
    const cameraShake = new CameraShake(this.gameContainer);

    // カメラの振動の終了を待機
    await tickerWrapper.waitForUpdate(cameraShake.update.bind(cameraShake));

    // ゲームオーバーシーンへ遷移する  
    const sceneManager = SceneManager.instance;
    const gameOverScene = new GameOverScene(this.app);
    sceneManager.changeScene(gameOverScene);
  }

これは、実際に作成中のゲームで使用しているゲームオーバー処理の一部です。

プレイヤーが障害物と衝突した際、カメラを振動させた後にゲームオーバーシーンに遷移する流れです。

この流れを、非同期処理を使ってカメラの振動が終了するまで待機し、振動後にシーンを切り替えるようにしています。

thisのバインドについて

また、TickerWrapper は関数をバインドしたいときにも役立ちます。

例えば、cameraShake.update.bind(cameraShake) を使って、update メソッドの this を正しくバインドしています。

非同期処理だけでなく、thisのバインドにも対応している点が便利です。

終わりに

非同期処理の async/await を PixiJS で使うためのラッパークラス作成を行ないました。

実はまだまだ改善の余地はあるのですが、基本的な機能はできていると思います。

ラッパークラスという物を初めて作ったので間違えている点があるかとは思いますが、参考になったら嬉しいです。