自作の小さなRPGでパフォーマンス上の問題を引き起こした原因とは?

ソフトウェア

ゲームの開発者が、自作の小さなRPGでパフォーマンス上の問題が発生し、原因を調査した結果について解説したことが話題になりました。当初は原因の見当が付かずコミュニティに助けを求める一方で自身でも調査を進めた結果、想定外の問題が複合的に作用していたことが明らかになりました。

What Caused Performance Issues in My Tiny RPG

https://jslegenddev.substack.com/p/what-caused-performance-issues-in

◆経緯

ソフトウェア開発者のJSLegendDev氏は、プライベートな趣味として小さなRPGを作成していました。使用言語はJavaScriptで、ゲーム開発ライブラリとしてKAPLAYを使用しました。一般的なRPGと同様に、自キャラをマップ上で移動させるには方向キーを用い、マップに現れた星に触れると接敵し戦闘が始まる、シンプルなシンボルエンカウント方式のゲームです。

戦闘はごく簡略化されたリアルタイム制バトルを採用し、バトルエリア内でキャラクターを操作して飛び回る刀を回避し、エリア内のどこかに現れる星を取得することで敵にダメージを与えます。

ゲームの開発は順調に進んでいましたが、ある時点でパフォーマンスの問題が発生しました。具体的には、戦闘時にゲームのフレームレートが低下し、操作が遅延するようになりました。JSLegendDev氏は、問題の原因を特定するために、ゲーム実行時のパフォーマンス低下問題に関する情報を公開して広くフィードバックを募る一方で、自身でも様々な調査を行いました。 最初にJSLegendDev氏が疑ったのは、自身の開発環境に依存する問題でした。というのも、同様の現象は過去に報告されておらず、JSLegendDev氏のマシンのみで発生する可能性が考えられたからです。しかしながら、使用しているマシンは16GBのRAMを搭載したMacBook Air M3であり、通常の使用においても、また当ゲームをFirefox上で動作させるにあたっても十分なスペックを有していました。ただ、JSLegendDev氏はこのゲームをSteamでリリースするつもりであったため、デスクトップアプリ化する必要がありました。すると、アプリ化によってパフォーマンスが大幅に低下してしまいました。

次に着手したのは、ゲーム全体に渡るパフォーマンスの改善でした。すると、問題点が3つに絞られることがわかりました。

◆問題1:徐々にパフォーマンスが低下する

ゲームを開始した直後はうまく動作するものの、時間が経つにつれて徐々にパフォーマンスが低下し、やがてほとんど動かなくなる傾向が明らかになりました。この現象は実際にゲームをプレイした人々からよく報告されました。そこで原因を調査したところ、KAPLAYの設定が適切に反映されない現象を発見しました。 KAPLAYライブラリを初期化する際のオプションの一つに、FPSを最大量に制限する「maxFps」があります。
kaplay({
     :
  【略】
     :
  maxFps: 60
});

このオプションは、一貫したフレームレートを強制することにより円滑なゲームプレイを持続させるためのものです。しかしながら、maxFpsの値を60にしてゲームをデバッグ実行したところ、デバッグFPSカウントには60FPSと表示されているにもかかわらず、ゲームは最終的にほとんど動かない状態になりました。そこでJSLegendDev氏は、「ゲームを実行するマシンが常に60FPS以上を維持できなくなった結果、パフォーマンスが低下しているのではないか?」という仮説を立てました。仮説に従ってmaxFpsの設定を削除したところ、問題は解決しました。結論として、maxFpsオプションがKAPLAYライブラリで正しく機能していなかったことが、パフォーマンス低下の一因であることが判明しました。

◆問題2:Mac版のパフォーマンスがWindows版と比べて低い

ゲームをデスクトップアプリとしてパッケージ化するにあたり、JSLegendDev氏はGemShellを採用しました。GemShellはTauriと同様に、OS依存のWebViewを使用してレンダリングを行います。従って、Mac版アプリのレンダリングを行うウェブエンジンはWebkitです。Chromiumベースと比較するとWebkitのパフォーマンスはよくないだろうとはJSLegendDev氏は予想していたものの、実際にMac版アプリを実行してみると、Windows版アプリと比べて著しくパフォーマンスが低いことがわかりました。 解決策の一つとして、GemShellの代わりにChromiumベースのレンダリングエンジンを利用するNW.jsを使用することも検討したそうですが、GemShellの開発者がこの状況を改善しようとしていることを知り、しばらく様子を見ることにしたとの事です。

◆問題3:ブラウザ上の動作で、ChromeやSafariよりもFirefoxのパフォーマンスが高かった JSLegendDev氏が「私が経験したうちで最も奇妙なパフォーマンスの問題」と語ったのが、ブラウザ上でゲームを実行した際に、ChromeやSafariよりもFirefoxのパフォーマンスが高かったことです。Safariのパフォーマンスが低い可能性については、Webkitエンジンを使用していることから予測できていましたが、ChromeのパフォーマンスがFirefoxよりも悪いことには驚いたそうです。

・KAPLAYゲームオブジェクトの過剰な生成

まず、パフォーマンスを低下させている原因を探るべく、Chromeのデベロッパーツールでパフォーマンスをプロファイリングしたところ、Chromeがゲーム内でテキストをレンダリングしている方法に問題があることがわかりました。

プレイグラウンドで提供されているサンプルコードを見ると、KAPLAYではテキストをレンダリングする場合、まずテキストコンポーネントを使用してゲームオブジェクトを生成し、そのコンポーネントにテキストを渡す必要があることがわかります。

// スクリーン中央に「Hello World!」をレンダリングする
const myTextObj = add([text("Hello World!"), pos(center())]);

しかし、パフォーマンスの面から考慮すると、ゲームオブジェクトの生成はコストが高いため、テキストをレンダリングするためだけにわざわざゲームオブジェクトを生成するのは非効率的です。単純なテキストをレンダリングするだけなら、ドローループ中に直接テキストを描画する関数を呼び出す方がはるかに効率的です。

onDraw(() => {
  // 画面描画が必要になるたびに、下の関数を呼び出す
  drawText({
    text: "Hello World",
    pos: center()
  });
});

同じことが、背景のような静止画像を表示する際にも当てはまります。以下のサンプルコードが非効率的なのは明らかです。

const myImage = add([sprite("someImage"), pos(center())]);

代替ロジックは以下の通りです。なお、バッチ処理を無効にしないよう、ドローループ中ではdrawTextでテキストレンダリングを行う前に、すべてのdrawSplite呼び出しを完了させておく必要があります。

onDraw(() => {
  drawSprite({
    sprite: "someImage",
    pos: center()
  });

  // ... 他のテキストレンダリング処理を呼び出す
});

・オブジェクト再利用の必要性 テキストレンダリングのパフォーマンス改善により、ChromeとSafari上でのパフォーマンスは大幅に向上しました。しかしながら、JSLegendDev氏はさらなるパフォーマンス改善を目指し、ゲームオブジェクトの生成と破棄を最小限に抑えるためのオブジェクトプーリングの実装に着手しました。 オブジェクトプーリングとは、必要なオブジェクトを事前に一定数生成しておき、必要に応じて再利用する手法です。JSLegendDev氏のゲームでは、飛来する大量の刀オブジェクトが戦闘中に生成および破棄されていました。これらの刀オブジェクトをオブジェクトプールに格納し、再利用することで、パフォーマンスの向上が期待できました。当初、刀の数自体はそれ程多くはないことから、この対応は後回しにする予定でした。しかしながら、ChromeとSafariでのパフォーマンスが依然として低い原因として考えられるのは、オブジェクトを破棄する際のガベージコレクタが適切に機能していない可能性があると予想し、対応に踏み切らざるを得なかったとのこと。 対応の結果、予想は的中し、ゲームのパフォーマンスはChromeとSafariで大幅に改善しました。プーリングシステムのストレステストのために、尋常ではない量の刀オブジェクトを乱舞させてみたところ、パフォーマンスは安定し、フレームレートの低下も見られませんでした。

◆まとめ 結果的にゲーム自体の不具合ではなく、使用しているライブラリやエンジンの問題であったことから、JSLegendDev氏は安心して開発に戻ることができました。ただ、最悪のシナリオとして「指定しているツールの制限により、ゲームのパフォーマンス向上を断念する可能性」も考慮していたそうです。もしそうなった場合、ゲームの実装を根本から見直したり、代替ツールの習得に時間を費やしたりして、開発が大幅に遅延するケースもあり得ました。今回の経験から、JSLegendDev氏は「習熟しているもの以外のフレームワークやゲームエンジンを学習しておくことの重要性」を再認識したと述べています。

関連記事: