またまたニッチな記事を書かせていただきました。
相変わらず長い記事ですが、興味があればお時間のある時にでもぜひお読みください。
2025/02/23 本記事の執筆状況
- 冒頭から「# 4. 総括」まで:執筆完了
- 「# 5. 本アプローチの課題・制約」:項目を追加予定
- 「# 6. 本アプローチの実現に至るまでの試行錯誤」:執筆中
- 本アプローチのサンプルプロジェクト:後日Githubのリンクを追記予定
筆者の開発経験
- C# 10年以上
- Unity 7年以上
Unityの最適化と暗号化と難読化を独学で7年かけて試行錯誤してきた筆者です。よろしくお願いします。
▼最適化の記事
▼暗号化と難読化の記事
想定している読者
- Unityゲームを開発している人
- UnityやC#の基礎知識がある人(C++の基礎知識も必要かもしれません)
- ステージから別ステージへの遷移やアウトゲームからインゲームへ遷移する際にロード画面が挟まり、数秒間の待ち時間が発生してしまうため困っている人
あるいはこの待ち時間にミニゲームを遊ばせたり凝ったロード画面を流す手法に興味のある人
参考文献(Unityの基礎知識)
サイバーエージェント社がGithubで公開している「パフォーマンスチューニングバイブル」というpdf形式の電子書籍があります。
主にパフォーマンスの最適化に関する内容を取り扱っていますが、Unityの基本的な仕様についても詳しく書かれており、筆者が個人的に全Unityゲーム開発者へ薦めたい必読書となっております。
本記事でもやはり「Unityがこれこれこういう仕様なので〜」という文脈が多々ありますが、本記事でUnityの基本的な仕様についてはあまり深掘りしないので、Unityの各仕様の詳細が気になる方はこの書籍やインターネットの情報で各自補完していただければと思います。
# 1. 概要(本記事の目的と結論)
## 1.1. 目的(ざっくり)
筆者のUnityゲームはロード画面に入ると、アセットの読み込みの負荷でUnity全体が固まってしまう
アセット読み込み中にミニゲームを遊ばせたり凝ったロード画面を流して、ユーザーを少しでも楽しませたい
しかしアセット読み込み中は負荷の影響でロード画面が断続的に固まってしまうため、「(アセット読み込みの)負荷の影響を受けないロード画面」を実現したい
## 1.2. アプローチ(ざっくり)
Unityゲーム内でどんな工夫を凝らそうと、Unityゲームのメインスレッドが固まっている間に画面が固まってしまう仕様はどうすることもできず、Unityゲーム単体でこれを避けることはできない
なのでUnityゲームとは別の独立したアプリにUnityゲームの画面を映し出して、Unityの負荷の影響を受けないミラーウィンドウを作成する
そこへ更に別のミニゲームや凝ったロード画面用のアプリを用意してミラーウィンドウに映せば、Unityゲームのアセット読み込み中でもミニゲームや凝ったロード画面の再生は支障なく実行できるのではないか
## 1.3. 結論(ざっくり)
結果として、DirectX11限定であることやWindowsのスタンドアロンアプリ限定(Macもいける?未検証)であることなど複数の制約は発生したものの、筆者が開発中のゲームはこれらの制約を許容できたので当初の目的を達成することができた
描画負荷を含む実行中のパフォーマンスも、通常通りUnityゲームを実行した時と比較して大きな差はなく、改善の余地はあるがそこそこ高負荷なUnityゲームのプレイにも支障をきたさないという(筆者なりの)結論を得られた
## 1.4. 導入例(動画)
筆者が開発中のゲームに本アプローチを導入して、ゲームプレイ中の画面を録画した動画がこちらになります。
このゲームは1体あたり数万ポリゴンの3Dモデルが10体近く入り乱れてバトルをするゲームとなっており、非常に負荷が高いです。
動画ではグラフィック設定をモリモリにして更に負荷を上げて、設定上は120FPSなのに負荷が高すぎて120FPSを維持できない状況に追い込んでいます(画面右下のFPSを参照)。
そしてこのゲームでは、通常のUnityゲームの上からロード画面用の2つ目のUnityゲームを重ねて表示しています。動画では画面右上でクルクル回っているキューブになります(透過されているだけで2つ目のUnityゲームは画面全体を覆っています)。
動画はメニュー画面から始まり、メニュー画面を閉じてバトルが再開されると負荷で一気にゲームのFPS(画面右下)が低下していますが、ロード画面用のUnityゲームにあたる画面右上のキューブはその影響を一切受けず120FPSを維持できていることが分かります。
定常的な負荷の影響を受けないのは勿論のこと、アセットの大量読み込みなどで高負荷がかかっている状況でもロード画面用のUnityゲームはその影響を受けないため、本アプローチは実現できていると言ってよいでしょう。
以上が本記事のざっくりとした概要になります。
本アプローチは前述の動画の通り 筆者が開発中のゲームに既に導入しており、もう少し検証を重ねてから 本アプローチを実現したライブラリを含むサンプルプロジェクトをGithubで公開する予定です。
公開したら本記事にリンクを追記予定ですので、ご興味があればぜひ使っていただければと思います。
# 2. 経緯
筆者が現在開発中のUnityゲームにはバトル要素があり、明確なアウトゲームとインゲームがあります。
アウトゲーム部分ではユーザーはバトルに向けた準備を行い、インゲーム部分ではバチバチに敵キャラと戦います。
そして筆者のゲームは3Dモデルを大量に使用しており、インゲームでは10体近くの3Dのキャラクターが画面上を動き回るため非常に負荷が高いです。
このためバトル中の定常的な負荷もかなり高いのですが、こちらを低減させるための最適化手法も別の記事でご紹介させていただいております。
▼最適化の記事
これにより定常的な負荷はかなり抑えられているのですが、それでもいついかなる時でも画面をヌルヌル動かし続けられているわけではありません。
3DモデルやBGMなどのアセットを読み込む瞬間にゲームが重くなってしまうのはどうしようもありません。
上の記事では、ロード画面のようなゲームが固まってよい特定のタイミングで高負荷な処理をまとめて実行しようという趣旨の話をしていますが、この ”ロード画面でゲームが固まっている間” にユーザーが待たされてしまうことを筆者は問題視した、というのが本記事の話の始まりとなります。
# 3. アプローチ
というわけで(前置き終了)、とっとと本題へ入りたいと思います。
## 3.1. Unityゲーム単体での解決は不可能
まず最初に1つハッキリさせなければならなかったのは、Unityゲーム単体でこの問題を解決することは不可能だということです。
Unityには大きく分けて、以下の3種類のスレッドが存在します。
- メインスレッド:UnityのAPIを扱いUnityゲームの主要な処理を行うスレッド
- レンダースレッド:Unityのオブジェクトの描画命令をCPUからGPUに行うためのスレッド
- ワーカースレッド:その他の多様な処理を取り扱うスレッド
この中でメインスレッドとレンダースレッドは密接に関係しており(詳しい仕様はパフォーマンスチューニングバイブル等を参照)、片方が処理の負荷で止まっている間は両方とも次のフレームの処理に進めなくなります。
例えば目標フレームレートが60FPSの場合、1フレームあたり約16.67ミリ秒なので、メインスレッドまたはレンダースレッドの1フレームあたりの処理に16.67ミリ秒以上かかってしまうと次のフレームの処理の開始が遅れてしまい、結果として60FPSを維持できなくなります。
このため、"アセットの読み込み中でも固まらないロード画面" を実現するには、固まる要因となるアセットの読み込み処理をメインスレッドから切り離す、つまりワーカースレッドに逃がす必要があります。
しかし残念ながら、Unityでアセットを読み込んでオブジェクトを生成する一連の処理を全てワーカースレッドに逃がすことはできません。
アセットの読み込み自体は非同期系のメソッドを使えばある程度の負荷分散はできますが、1つのアプリが許容できる負荷にも限度があるため、分散した負荷のトータルがデカすぎるとメインスレッドも影響を受けてしまいます。
メインスレッドにまで負荷が波及しないよう、アセットの読み込みやオブジェクトの生成を少しずつ行うといった手法も考えられますが、読み込み時間が延びてしまううえ確実にメインスレッドが固まらないという保証がないためお勧めできません。
また、2022.3.20f1からUnityEngine.Object.InstantiateAsync
というAPIが追加されており、これを使えばUnityオブジェクトの生成自体は非同期化することができます。ただし、そのオブジェクトに付随するコンポーネントのAwake関数やStart関数、Renderer系のMaterialのインスタンス化はメインスレッド上で行われてしまうため、完全な非同期化はできないのが現状です。
なのでUnityゲームのロード画面は、一般的に負荷の影響でゲームが断続的に固まっても問題ないような簡易的な作りにするのが主流です。
どんなにロード画面のパフォーマンスを最適化して軽量な作りにしても、メインスレッドが固まっている間はロード画面ごと固まってしまいます。
ロード画面をUnityの画面上に描画するためにはUnityのAPIを使用せざるを得ませんが、UnityのAPIはメインスレッド上でのみ実行可能なため、UnityのAPIに依存している限りロード画面はメインスレッドの負荷から逃れられません。
本節の結論として、「負荷の影響を受けないロード画面」をUnity単体で実現することは不可能です。
## 3.2. 完成(仮)したアプローチ
単純な発想ですが、Unity単体で無理なら別のアプリに逃がしましょう。
このアプローチを実現するに至るまでに様々なトライアンドエラーが繰り広げられましたが、その試行錯誤の過程は後の章で深堀りするとして、本章では最終的に実用的な域に達したアプローチについてご説明いたします。
### 3.2.1. 対応しているUnityバージョン
本アプローチが技術的に実現可能なUnityのバージョンが下表の通りとなります。
本記事投稿時点ではUnity6のみ確認済みですが、理論的には2018以降で実現可能(のはず)です。
筆者の環境で確認次第、対応有無を追記いたします。
Unityバージョン | 対応有無 |
---|---|
6000.0.*f1 | ✅ |
2022.3.*f1 | (確認次第追記) |
2021.3.*f1 | (確認次第追記) |
2020.3.*f1 | (確認次第追記) |
2019.4.*f1 | (確認次第追記) |
### 3.2.2. グラフィックスAPIの「共有テクスチャ」
本アプローチを実現するにあたって最大にして重要な課題は、独立した異なるプロセス間で画面をどうやって共有するかです。
画面を共有する際に求められることとして、
- 毎秒何十回何百回も更新される画面の情報をリアルタイムに共有できること
- 画面の共有が負荷となってゲームのプレイに影響を及ぼさないこと
の2点が重要であると考えます。
DirectXなどのグラフィックスAPIには共有テクスチャというものがあり、これを使うと異なるアプリケーション間でGPUメモリ上の同一のテクスチャデータにアクセスすることができます。
この共有テクスチャが上記の要件を満たしていたため、本アプローチでは共有テクスチャを介してUnityの画面を異なるプロセスのウィンドウに映す手法を採用しました。
以降本記事では度々「共有テクスチャ」という単語が頻出するので押さえておいていただければと思います。
また、"共有テクスチャをUnityからミラーアプリに連携するための実装" がかなり複雑なので、本記事では広範な互換性と安定性を持つDirectX11に絞って本アプローチを実現しました。
理論的にはDirectX12やVulkanなど他のグラフィックスAPIでも可能(のはず)ですが、その場合大幅に実装が変わってしまううえに筆者はレンダリング周りに疎いので、DirectX11以外での本アプローチの実現性は確認しておりません。
### 3.2.3. 登場人物
本アプローチでは以下の3つのアプリが登場します。
-
メインのUnityゲーム
ゲーム本編を実装したUnityゲームです。
念のためコレ単体でも問題なく遊べる実装にしておき、後述するミラーアプリを介してメインのUnityゲームを起動した時はロード画面が豪華になる、という運用を想定します。 -
サブのUnityゲーム
ロード画面用のUnityゲームです。メインのUnityゲームの上から重ねて描画する想定で、オブジェクトが何もない部分は透過に対応しています。
本記事ではUnityゲームがDirectX11で描画されているという前提で本アプローチを実現しましたが、これはメインのUnityゲームに限らずサブのUnityゲームでも同様です。
裏を返すとDirectX11で実装されていれば、ロード画面を担当するこのアプリはUnityである必要はないのですが、本記事ではUnityで作るものとします。 -
ミラーアプリ
ミラーアプリの主な役割は、上2つのUnityゲーム画面を共有テクスチャを介して専用のウィンドウに映し出すことです。
ただ単純に映し出すだけでなく、キーボードやマウスの入力を受け取りUnityゲームへ連携する、ウィンドウサイズの変更に対応する、メインとサブのUnityゲーム本体のウィンドウは隠すなど様々な実装を施して、ユーザーのディスプレイにはこのミラーアプリ1つだけが表示され、ユーザーはミラーアプリを操作することで通常のUnityゲームと変わりない操作感で遊べるようにする必要があります。
また、本アプローチでは以下の3つのプロジェクトを開発しました。
-
ミラーアプリ(C++)
前述のミラーアプリのことです。このアプリはC++で実装します。
理論的にはC#でも可能ですが(筆者も試した)、C#のWPFで共有テクスチャを取り扱うためにはDirectX11をDirectX9に変換する必要があり、パフォーマンスの点でC++に劣ってしまうため、今回はDirectX11が使用できパフォーマンスも優れているC++で実装しました。
この辺りの試行錯誤の過程は後の章で解説いたします。 -
Unityのネイティブプラグイン(C++)
Unityのゲーム画面の描画内容を持つRenderTextureをもとに共有テクスチャを作成して、それをミラーアプリに連携するための仲介人が必要となるため、それをUnityのネイティブプラグインで実装します。 -
Unityのコンポーネントや拡張機能(C#)
ミラーアプリから受け取ったキーボードやマウスの入力、ウィンドウサイズの変更イベントなどをUnity側で処理したり、Unityゲームをエディタでビルドする際にミラーアプリの出力を行うなど、要はUnityプロジェクト内で管理するC#のスクリプト群になります。
### 3.2.4. Pipe(プロセス間通信)
本アプローチで重要な要素の1つが、独立した異なるプロセス間でのデータの送受信方法です。
送受信するデータは主に、キーボードやマウスの入力データや、ウィンドウサイズの変更イベントなどです。
プロセス間の通信方法にもいくつか選択肢がありますが、本アプローチではパフォーマンスやリアルタイム性などの観点からPipeが最も適していると判断しました。
Pipeが適している理由や他の選択肢が没になった理由などの細かい話も、後の章で解説いたします。
### 3.2.5. ゲーム起動の流れ
ユーザーが直接実行するのはミラーアプリのexeファイルで、ミラーアプリからメインとサブのUnityゲームを呼び出して初期化します。
ゲーム起動時の処理の流れはざっくり以下の通りです。(長くなって申し訳ないですが画像にして2枚分のフローになります)
-
ミラーアプリを起動
ユーザーがミラーアプリのexeファイルを起動します。 -
ミラーアプリ←Unity のPipeを開く
Pipeにはサーバー側とクライアント側の2つの役割があります。
詳細は後の章で解説いたしますが、受信側のアプリをサーバー側としたほうがなにかと都合が良いので、ミラーアプリをサーバー側としてUnityからのデータを受信するPipeをミラーアプリ上で作成します。
Pipeを作成すると接続待ち状態になり、後でUnity側からこのPipeに接続することでプロセス間の通信が可能となります。 -
メインのUnity&サブのUnityを起動
メインのUnityとサブのUnityを同時に並行して起動します。
本アプローチではミラーアプリとUnityがそれぞれ独立した異なるプロセスであることが重要のため、C++のCreateProcess
でUnityのプロセスを立ち上げます。
この時、起動引数に 2 で作成したPipeの識別子を渡してあげます。 -
メインのUnity←ミラーアプリ のPipeを開く
Pipeはその特性上、1つのPipeで同時に送受信を行おうとするとデッドロックが発生してしまうため、Unityからミラーアプリへデータを送信するPipeとミラーアプリからUnityへデータを送信するPipeは分けて作成します。
2 で作成したPipeはUnityからミラーアプリへデータを送信するPipeでしたが、今回は逆にミラーアプリからのデータをメインのUnityが受信するPipeを作成します。
ここでどさくさに紛れて 2 で作成したPipeに接続する等、現段階で可能なUnity側の初期化処理も実行します。
ある程度Unity側の初期化が完了したら、この時点で既に ミラーアプリ←Unity のPipeは接続が完了しているので、早速このPipeを使ってUnity側の初期化が完了した旨をミラーアプリに通知します(ついでに メインのUnity←ミラーアプリ のPipeの識別子を渡してあげます)。 -
サブのUnity←ミラーアプリ のPipeを開く
4 と同じことをサブのUnityゲームでも行います。
ちなみにメインのUnityだけで動作確認したいケースを考慮して、サブのUnityが存在しない場合はサブのUnity周りの処理をスキップする設計になっています。 -
初期化処理&初期化完了通知
全てのUnityの初期化完了通知を受け取ったら、Unity←ミラーアプリ へ接続するなどの初期化処理を行います。
まだこの段階では全ての起動処理を完了できませんが、一旦完了したらその旨をメインのUnityにのみPipeで通知します。
続いて2枚目のフローが以下になります。
2枚のフローを通しで見ると、あっちいったりこっちいったり回りくどい感じになってしまってますが、1枚目のフローは全てのアプリのPipe通信を接続しつつ初期化処理を完了させるまでの最短ルートとなっており、2枚目はメインのUnityのウィンドウサイズ変更をミラーアプリとサブのUnityに適用するまでの最短ルートになります。
単純な初期化だけならもう少し工程を圧縮できると思いますが、ウィンドウサイズの変更はゲーム起動後にも発生しうるイベントであり、加えてウィンドウサイズの変更はメインのUnityが起点になるケースとミラーアプリが起点にあるケースが存在するので、どのケースのウィンドウサイズ変更でも極力同じロジックを流用するための最短ルートを構築しました。
それではここから、2枚目のフローについて解説いたします。
-
メインのUnityのウィンドウサイズの初期値をミラーアプリに通知
ミラーアプリから初期化完了の通知をPipeで受信したら、今度はメインのUnityのウィンドウサイズの初期値をミラーアプリにPipeで通知します。
メインのUnityでは共有テクスチャを作成して、その共有テクスチャをミラーアプリからアクセスするための識別子もついでにPipeで送ります。 -
ウィンドウサイズ変更をサブのUnityに通知
メインのUnityのウィンドウサイズの情報をミラーアプリが受け取り、それをサブのUnityに通知します。 -
サブのUnityの画面の初期化&初期化完了通知
メインのUnityのウィンドウサイズの情報をサブのUnityが受け取り、ウィンドウサイズの変更を適用します。
ここでサブのUnityの共有テクスチャを作成して、その共有テクスチャをミラーアプリからアクセスするための識別子をPipeで送ることで、サブのUnityのウィンドウサイズ変更が完了したことをミラーアプリに通知します。 -
ミラーアプリの画面の初期化&初期化完了通知
ここでようやく全てのUnityの共有テクスチャが揃いましたので、ミラーアプリの画面の初期化を行い共有テクスチャをミラーアプリの画面に描画できるようにします。 -
初期化完了
ミラーアプリの画面の初期化(≒ウィンドウサイズの変更)が完了したことを全てのUnityにPipeで通知して、起動後のセットアップは完全に終了となります。
以上がゲーム起動時の大まかな流れとなります。
最後の初期化完了通知にも意味があり、大前提としてUnity単体で実行した時のウィンドウサイズ変更は同期的に一瞬で行われますが、本アプローチではプロセス間で整合性を保たなければいけないため非同期的に数フレーム程度のラグを伴って行われます。
これによりウィンドウサイズの変更処理中に別のウィンドウサイズ変更処理が走ってしまうと、各々のアプリが認識しているウィンドウサイズに不整合が生じてしまいます。
これを防ぐため、ウィンドウサイズの変更が開始されたらフラグを立てて、このウィンドウサイズ変更完了通知を受け取りフラグを解除することにより、フラグが立っている間はウィンドウサイズの変更を行わず待機させています。
複数のプロセス間で整合性を保つためにはこういった点も考慮しなければならず、これが地味に大きな課題の1つでした…。
### 3.2.6. ウィンドウサイズ変更の流れ(メインのUnity起点)
ゲームプレイ中にゲーム内の設定変更などによりウィンドウサイズを変更した際の処理の流れになります。
(例:UnityEngine.Screen.SetResolution
を実行した時)
処理の流れはざっくり以下の通りです。
フローの解説ですが、前項のゲーム起動時のウィンドウサイズ変更のロジックと同一であり、処理内容は前項の ゲーム起動の流れ の 7. メインのUnityのウィンドウサイズの初期値をミラーアプリに通知 以降とほぼ同じになるため省略いたします。
ゲーム起動時は初回のウィンドウサイズ変更用の処理がいくつか紛れ込んでいましたが、今回は純粋にウィンドウサイズの変更を適用するだけの処理のみ実行されます。
### 3.2.7. ウィンドウサイズ変更の流れ(ミラーアプリ起点)
今度はミラーアプリのウィンドウの操作によりウィンドウサイズを変更した際の処理の流れになります。
(例:ウィンドウの境界線をユーザーがマウスでドラッグ操作してウィンドウサイズを変更した時)
処理の流れはざっくり以下の通りです。
こちらもミラーアプリからメインのUnityにウィンドウサイズ変更を通知して以降は メインのUnity起点 と全く同じ流れなので、解説は省略いたします。
### 3.2.8. キーボードやマウスの入力の流れ
これも本アプローチに欠かせません。
ユーザーのキーボードやマウスの入力を直接受け取るのはUnityではなくミラーアプリなので、ミラーアプリからUnityに転送する必要があります。
筆者のライブラリでは新しいInputSystemにのみ対応しましたが、古いInputManagerも実装すれば対応可能のはずです(未検証)。
処理の流れはざっくり以下の通りです。
-
ミラーアプリがユーザーの入力を受け取る
ユーザーが入力したキーボードやマウスの操作をミラーアプリが受け取ります。Unityの新しいInputSystemはRawInputAPIと呼ばれるもので実装されているため、今回C++で実装したミラーアプリもRawInputAPIで入力を受け取ります。 -
フォーマットしてUnityに転送する
C++のミラーアプリが受け取ったRawInput用の入力情報を、Unityの新しいInputSystemに連携できるようにデータを加工してからUnityに送信します。
Unity公式ドキュメントのこちらを参考にさせていただきました。
https://docs.unity3d.com/6000.0/Documentation/ScriptReference/Windows.Input.ForwardRawInput.html -
Unityでキーボードやマウスの入力データを解析する
C++のミラーアプリ側で加工した入力データをPipeで受け取り、それをUnityで解析します。 -
InputSystemに反映
あとは入力データをInputSystemに反映することで、Unity単体でゲームプレイ中にキーボードやマウスを入力した時と同じようにInputSystemの入力イベントを発火させることができます。
以上がキーボードやマウスの入力の大まかな流れとなります。
これを実現するにあたり大きめの課題が2つありましたので、そちらについて解説いたします。
-
パフォーマンス
キーボードやマウスの入力(特にマウスの移動)は1フレームあたり何十回も行われることが想定されるため、パフォーマンスは可能な限り最適化しなければいけません。
ミラーアプリとUnity間で入力データをやり取りするためのプロセス間通信にもPipeを使用していますが、Pipeは予め確保しておいたバッファ上に対して読み書きを行うため、通信する度にGC.Allocが発生するという最悪の事態は避けられています。
そしてミラーアプリからPipeに書き込むデータや、UnityでPipeから読み込んだデータを取り扱う際も、予め確保しておいたバイト配列に対して読み書きを行うよう設計しており、パフォーマンス的にも無駄のない実装となっております。
よってパフォーマンス上の観点から見ても問題のない実装ができております。 -
リアルタイム性
Pipeの通信は非同期ですが、受信側はデータを受け取ったら即座に後続処理が走り出すので、この点に関しては遅延はほぼゼロとなっています。
ですが問題はInputSystemのイベント発火はメインスレッドでのみ実行可能という点です。
Pipeの受信はワーカースレッド上で行います。そうしないと受信を待機している間Unityゲームが固まってしまうためです。
そうなると、受信する(ワーカースレッド)→受信後の後続処理を実行する(ワーカースレッド) までは遅延ゼロですが、受信後の後続処理を実行する(ワーカースレッド)→受信したデータをInputSystemに流す(メインスレッド) までの間に数ミリ秒の遅延が生じます。
結論としてこの遅延をゼロにすることは諦めました(筆者が開発してるゲームもそこまでのリアルタイム性は必要でなかったため妥協可能な範疇だった)。
Pipeで受信した入力データをワーカースレッドで専用のキューに格納して、メインスレッドでそのキューを読み取りInputSystemを発火させるのですが、メインスレッドではUniTaskを使ってPlayerLoopTiming.EarlyUpdate
とPlayerLoopTiming.PreLateUpdate
の2つのタイミングでキューの読み取り&InputSystemの発火を行います。
こうすることでキーボードやマウスの入力を1フレームあたり2回処理することができるので、入力の遅延を抑えることにはある程度成功しています。
0.5フレームの遅延も許せないゲームでは、残念ながら本アプローチの導入は断念せざるを得ないかもしれません。
### 3.2.9. UnityのRunInBackground設定
Unityがフォーカスされてない間にUnityの動作を停止させるか動作を継続するかを選べるRunInBackground設定ですが、今はまだ試行錯誤中で、バックグラウンドでも設定を無視して常に動作し続ける状況です…。
実は検証過程で、ミラーアプリからフォーカスが外れるとUnity側もRunInBackground設定に従って動作が止まってくれてたことがあったのですが、いつの間にかRunInBackground設定を無視して動き続けてしまうようになっていて、それっきり解決できていません。
筆者の開発しているゲームは常に動きっぱなしでも構わないので、最悪これは本アプローチ特有の制約だ、ということにしてしまうかもしれませんが、サンプルプロジェクトを公開するまでにできれば解決したいところです…。
## 3.3. 本アプローチの実用性の検証
本アプローチが実用的であることを、機能面と性能面の2つの観点から確認した結果を以下にまとめました。
### 3.3.1. 機能面
本アプローチを実現したライブラリがサポートする主要な機能は、前項でご紹介した通りです。
本記事投稿時点での目ぼしい課題はUnityのRunInBackground設定が効いてないことくらいですが、これを含めて筆者個人としては妥協できる範疇の課題のみで、それ以外の ウィンドウサイズの変更 や キーボードやマウスの入力 といったクリティカルな機能はUnityの仕様に沿って機能しており、機能面は概ね問題ない と筆者は考えます。
機能面に関しては実装しながら検証を進めてきたので、本ライブラリが完成(仮)したことで必要な機能は一通り実現できているため、改まっての検証は省略いたします。
今後も引き続き本アプローチを使用する前提で筆者はゲーム開発を進めるので、もし何か追加で課題が挙がった際は本記事に追記いたします。
### 3.3.2. 性能面(パフォーマンス)
本アプローチでは複数のプラグインやアプリを並行して動かすことになるので、従来通りUnity単体で実行した時に比べてパフォーマンスが低下してしまうことが懸念されます。
そこでUnity単体で実行した時とミラーアプリから実行した時のパフォーマンスを比較検証してみよう、というのが本項の趣旨となります。
#### 3.3.2.1. 性能検証対象のUnityの環境
今回の性能検証を行うUnity側の環境は以下の通りです。
- メインのUnityゲーム
- Unityバージョン:6000.0.23f1
- 垂直同期設定:オフ
- フレームレート設定:120FPS
- ゲームの内容:1体あたり数万ポリゴンの3Dモデルが10体近く入り乱れて戦う、負荷が高めのゲーム
- サブのUnityゲーム
- Unityバージョン:6000.0.23f1
- 垂直同期設定:オフ
- フレームレート設定:120FPS
- ゲームの内容:3Dのキューブがポツンと1個置いてあり、その場で回転し続けるだけ
ちなみに性能検証から話が逸れますが、サブのUnityゲーム画面に表示されているキューブの回転を目視確認して、本アプローチの主目的である "(メインのUnityが)アセットの読み込み中でも(サブのUnityの)ロード画面が固まらない" が実現可能であることは確認済みです。(サラッと超重要な情報をぶっ込む)
#### 3.3.2.2. 性能検証の確認観点
今回の性能検証の確認観点は以下の2点とします。
- (可能な限り)同じシナリオでゲームをプレイした時の1フレームあたりの所要時間の相対比較
- UnityのProfilerを使用して、極端なパフォーマンスの低下や余分なGCが発生してないか等の目視確認
#### 3.3.2.3. 性能検証のシナリオ
今回の性能検証の目的は Unity単体で実行した時に比べてミラーアプリから実行した時のパフォーマンスがどう変わるかを確認すること なので、今回は相対的な比較を行います。
性能検証の確認観点の1つ目である「(可能な限り)同じシナリオでゲームをプレイした時の1フレームあたりの所要時間の相対比較」の具体的な検証方法ですが、
メインのUnityゲームの中で、インゲームの特定のタイミングから検証を開始して、開始から0.25秒ごとのフレームレートを計測します。
計測するのはメインのUnityゲームでバトルが開始した瞬間から5秒間で、1体辺り数万ポリゴンの3Dキャラクターが10体近く入り乱れて戦っているため、非常に負荷が集中している場面になります。
そして計測したFPSを1フレームあたりの所要時間に置き換え、その所要時間の平均値を比較しました。
より正確な計測を行うのであれば直接1フレームあたりの所要時間を計測すべきだと思いますが、今回は相対比較なのでフレームレートをベースにした計測で充分な結果を得られたのに加えて、所要時間の計測だと地味に手間がかかりそうだったため今回の計測方法を取らせていただきました。
なお、メイン及びサブのUnityゲームは前述の通り Unity側の設定により120FPSで動作するようにしてありますが、メインのUnityゲームは負荷が高めのグラフィック設定にしてあることに加えて、バトルが白熱している状態で計測を行うため、設定上は120FPSだが負荷によって120FPSを出し切れない という状況に追い込んで計測しています。
#### 3.3.2.4. ハイエンド端末の仕様
今回の性能検証では ハイエンドな端末 と ローエンドな端末 の2台で検証を行いました。
まずはハイエンドな端末の仕様からご紹介いたします。
開発環境を兼ねたハイエンドなゲーミングデスクトップPCです。
#### 3.3.2.5. ハイエンド端末の検証結果
まずは「(可能な限り)同じシナリオでゲームをプレイした時の1フレームあたりの所要時間の相対比較」の検証結果です。
フレームレート(UnityProfilerあり)
経過時間 | Unity単体 | ミラーアプリ メインのみ |
ミラーアプリ メイン&サブ |
---|---|---|---|
0.00 | 50.6 | 46.5 | 48.8 |
0.25 | 39.8 | 41.0 | 42.0 |
0.50 | 47.7 | 47.1 | 42.9 |
0.75 | 48.9 | 47.2 | 45.7 |
1.00 | 51.0 | 48.5 | 47.8 |
1.25 | 51.2 | 47.2 | 47.0 |
1.50 | 48.4 | 49.4 | 43.3 |
1.75 | 54.2 | 48.2 | 49.1 |
2.00 | 51.9 | 50.1 | 42.4 |
2.25 | 48.3 | 40.6 | 46.1 |
2.50 | 48.8 | 49.1 | 45.7 |
2.75 | 51.8 | 47.3 | 45.3 |
3.00 | 50.9 | 48.6 | 46.2 |
3.25 | 48.5 | 48.8 | 45.2 |
3.50 | 49.6 | 47.8 | 47.7 |
3.75 | 51.6 | 39.5 | 39.6 |
4.00 | 50.3 | 43.5 | 42.8 |
4.25 | 50.5 | 42.5 | 40.7 |
4.50 | 46.9 | 38.0 | 39.1 |
4.75 | 50.8 | 40.7 | 37.4 |
5.00 | 53.3 | 47.0 | 45.8 |
平均値 | 49.76 | 45.64 | 44.31 |
1フレームあたりの所要時間(ms)(UnityProfilerあり)
経過時間 | Unity単体 | ミラーアプリ メインのみ |
ミラーアプリ メイン&サブ |
---|---|---|---|
0.00 | 19.76 | 21.51 | 20.49 |
0.25 | 25.13 | 24.39 | 23.81 |
0.50 | 20.96 | 21.23 | 23.31 |
0.75 | 20.45 | 21.19 | 21.88 |
1.00 | 19.61 | 20.62 | 20.92 |
1.25 | 19.53 | 21.19 | 21.28 |
1.50 | 20.66 | 20.24 | 23.09 |
1.75 | 18.45 | 20.75 | 20.37 |
2.00 | 19.27 | 19.96 | 23.58 |
2.25 | 20.70 | 24.63 | 21.69 |
2.50 | 20.49 | 20.37 | 21.88 |
2.75 | 19.31 | 21.14 | 22.08 |
3.00 | 19.65 | 20.58 | 21.65 |
3.25 | 20.62 | 20.49 | 22.12 |
3.50 | 20.16 | 20.92 | 20.96 |
3.75 | 19.38 | 25.32 | 25.25 |
4.00 | 19.88 | 22.99 | 23.36 |
4.25 | 19.80 | 23.53 | 24.57 |
4.50 | 21.32 | 26.32 | 25.58 |
4.75 | 19.69 | 24.57 | 26.74 |
5.00 | 18.76 | 21.28 | 21.83 |
平均値 | 20.17 | 22.06 | 22.69 |
フレームレート(UnityProfilerなし)
経過時間 | Unity単体 | ミラーアプリ メインのみ |
ミラーアプリ メイン&サブ |
---|---|---|---|
0.00 | 79.3 | 70.7 | 70.4 |
0.25 | 58.3 | 79.2 | 72.9 |
0.50 | 75.1 | 77.2 | 70.6 |
0.75 | 80.1 | 75.9 | 73.4 |
1.00 | 83.2 | 76.0 | 79.3 |
1.25 | 82.4 | 85.2 | 76.9 |
1.50 | 80.0 | 81.3 | 71.8 |
1.75 | 83.1 | 73.8 | 72.7 |
2.00 | 80.6 | 73.3 | 70.3 |
2.25 | 79.8 | 79.5 | 74.6 |
2.50 | 79.6 | 74.0 | 73.0 |
2.75 | 80.7 | 78.4 | 71.3 |
3.00 | 79.6 | 75.1 | 75.6 |
3.25 | 82.4 | 79.8 | 72.7 |
3.50 | 87.2 | 75.1 | 67.6 |
3.75 | 74.9 | 78.9 | 71.9 |
4.00 | 77.5 | 80.2 | 72.0 |
4.25 | 81.2 | 79.6 | 74.6 |
4.50 | 79.3 | 76.6 | 73.6 |
4.75 | 82.7 | 77.7 | 75.7 |
5.00 | 74.5 | 84.3 | 77.1 |
平均値 | 79.12 | 77.70 | 73.24 |
1フレームあたりの所要時間(ms)(UnityProfilerなし)
経過時間 | Unity単体 | ミラーアプリ メインのみ |
ミラーアプリ メイン&サブ |
---|---|---|---|
0.00 | 12.61 | 14.14 | 14.20 |
0.25 | 17.15 | 12.63 | 13.72 |
0.50 | 13.32 | 12.95 | 14.16 |
0.75 | 12.48 | 13.18 | 13.62 |
1.00 | 12.02 | 13.16 | 12.61 |
1.25 | 12.14 | 11.74 | 13.00 |
1.50 | 12.50 | 12.30 | 13.93 |
1.75 | 12.03 | 13.55 | 13.76 |
2.00 | 12.41 | 13.64 | 14.22 |
2.25 | 12.53 | 12.58 | 13.40 |
2.50 | 12.56 | 13.51 | 13.70 |
2.75 | 12.39 | 12.76 | 14.03 |
3.00 | 12.56 | 13.32 | 13.23 |
3.25 | 12.14 | 12.53 | 13.76 |
3.50 | 11.47 | 13.32 | 14.79 |
3.75 | 13.35 | 12.67 | 13.91 |
4.00 | 12.90 | 12.47 | 13.89 |
4.25 | 12.32 | 12.56 | 13.40 |
4.50 | 12.61 | 13.05 | 13.59 |
4.75 | 12.09 | 12.87 | 13.21 |
5.00 | 13.42 | 11.86 | 12.97 |
平均値 | 12.71 | 12.89 | 13.67 |
続いて「UnityのProfilerを使用して、極端なパフォーマンスの低下や余分なGCが発生してないか等の目視確認」です。
一応画像を貼りましたが、Profilerの情報は画像程度では伝わらないと思うので、最初に述べた通りProfilerはGC.Allocや処理時間などを目視で確認して、ミラーアプリの性能が極端に低下していないことを確認しました。
なので本記事としては、前述の1フレームあたりの所要時間のほうを掘り下げてじっくり比較してみたいと思います。
#### 3.3.2.6. ハイエンド端末の検証結果抜粋
FPSの平均値 | Unity単体 | ミラーアプリ メインのみ |
ミラーアプリ メイン&サブ |
---|---|---|---|
Profilerあり | 49.76 | 45.64 | 44.31 |
Profilerなし | 79.12 | 77.70 | 73.24 |
1Fあたりの 所要時間(ms) |
Unity単体 | ミラーアプリ メインのみ |
ミラーアプリ メイン&サブ |
---|---|---|---|
Profilerあり | 20.17 | 22.06 | 22.69 |
Profilerなし | 12.71 | 12.89 | 13.67 |
#### 3.3.2.7. ハイエンド端末の検証結果分析
今回の相対比較では1フレームあたりの所要時間に着目して、検証結果から分かることをピックアップしていきます。
-
UnityProfilerを起動しているとミラーアプリでは遅い
Profilerあり×Unity単体 と、Profilerあり×ミラーアプリ(メインのみ) の差は1.89ミリ秒もありますが、
Profilerなし×Unity単体 と、Profilerなし×ミラーアプリ(メインのみ) の差は0.18ミリ秒しかありません。
どういうわけかUnityProfilerを起動していると、ミラーアプリからUnityゲームを実行した場合は処理時間が無駄に伸びてしまうようです。
-
Unity単体 と ミラーアプリ(メインのみ) の差は 0.18ミリ秒
最終的にユーザーに提供するアプリはProfilerなしのほうが近いのでProfilerなしの結果に着目してみると、ミラーアプリのほうが1フレームあたり0.18ミリ秒遅かったという結果が読み取れます。
-
ミラーアプリ(メインのみ) と ミラーアプリ(メイン&サブ) の差は 0.78ミリ秒
この結果はサブのUnityゲームがどれほど作り込まれているかによって変動するはずなので、比較する意味はあまり無い気がします。
ですがサブのUnityゲームは3Dのキューブ1つがその場で回転してるだけの簡素な作りなので、それを起動してるだけでもこれほどの差が出るという参考記録にはなるかと思います。
-
Unity単体 と ミラーアプリ(メイン&サブ) の差は 0.96ミリ秒
この結果も上に同じく、サブのUnityゲームがどれほど作り込まれているかによって変動するはずなので、比較する意味はあまり無い気がします。
#### 3.3.2.8. ハイエンド端末の検証結果まとめ
今回の検証結果で得られた一番重要な情報は、Unity単体で実行した時に比べてミラーアプリを介してメインのUnityゲームを実行した時のほうが1フレームあたり0.18ミリ秒遅かったという点だと思います。
要するにミラーアプリ自体の負荷は1フレームあたり0.18ミリ秒程度ということになりますが、筆者個人としては十分許容範囲内です。
#### 3.3.2.9. ローエンド端末の仕様
続いてローエンド端末の仕様です。
こちらはプリインストール直後のローエンドなゲーミングノートPCで検証しました。
GPUの性能はグラボに大きく依存していますが、CPU内蔵GPUの機能によって解像度を疑似的にFHDからWQHDにしたり、画面のリフレッシュレートを60Hzから144Hzに引き上げたりしています。
自作ゲームの動作確認用にとりあえず確保したローエンド端末なのですが、ゲーミングノートPCも凄い進化してるんですね~(超他人事っぽい口調)
#### 3.3.2.10. ローエンド端末の検証結果
まずは「(可能な限り)同じシナリオでゲームをプレイした時の1フレームあたりの所要時間の相対比較」です。
▼検証結果(UnityProfilerあり)
ハイエンド端末の検証結果を受けて、Profilerが介在すると正確な結果が取れないことが分かったため、ローエンド端末ではProfilerありの検証は割愛します。
また同様に、ミラーアプリ(メイン&サブ) も比較する意味がないためローエンド端末での検証は割愛します。
フレームレート(UnityProfilerなし)
経過時間 | Unity単体 | ミラーアプリ メインのみ |
---|---|---|
0.00 | 67.5 | 63.2 |
0.25 | 72.3 | 65.7 |
0.50 | 67.7 | 67.3 |
0.75 | 75.6 | 62.4 |
1.00 | 68.6 | 66.7 |
1.25 | 71.7 | 68.8 |
1.50 | 72.6 | 69.0 |
1.75 | 68.7 | 55.3 |
2.00 | 66.5 | 61.4 |
2.25 | 72.2 | 69.5 |
2.50 | 69.5 | 63.8 |
2.75 | 70.3 | 63.2 |
3.00 | 71.4 | 67.8 |
3.25 | 67.5 | 63.3 |
3.50 | 71.8 | 63.7 |
3.75 | 73.0 | 61.0 |
4.00 | 71.8 | 65.2 |
4.25 | 63.8 | 57.2 |
4.50 | 71.9 | 47.7 |
4.75 | 78.2 | 65.1 |
5.00 | 80.2 | 66.0 |
平均値 | 71.09 | 63.49 |
1フレームあたりの所要時間(ms)(UnityProfilerなし)
経過時間 | Unity単体 | ミラーアプリ メインのみ |
---|---|---|
0.00 | 14.81 | 15.82 |
0.25 | 13.83 | 15.22 |
0.50 | 14.77 | 14.86 |
0.75 | 13.23 | 16.03 |
1.00 | 14.58 | 14.99 |
1.25 | 13.95 | 14.53 |
1.50 | 13.77 | 14.49 |
1.75 | 14.56 | 18.08 |
2.00 | 15.04 | 16.29 |
2.25 | 13.85 | 14.39 |
2.50 | 14.39 | 15.67 |
2.75 | 14.22 | 15.82 |
3.00 | 14.01 | 14.75 |
3.25 | 14.81 | 15.80 |
3.50 | 13.93 | 15.70 |
3.75 | 13.70 | 16.39 |
4.00 | 13.93 | 15.34 |
4.25 | 15.67 | 17.48 |
4.50 | 13.91 | 20.96 |
4.75 | 12.79 | 15.36 |
5.00 | 12.47 | 15.15 |
平均値 | 14.11 | 15.86 |
続いて「UnityのProfilerを使用して、極端なパフォーマンスの低下や余分なGCが発生してないか等の目視確認」です。
やはりローエンド端末なので、GPUに負荷がかかっているのが分かります。(画像上の黄色い部分が GPUに高負荷がかかっていることを示している)
ですがそれはUnity単体でもミラーアプリもお互い様なので、Profilerの目視確認では特段著しいパフォーマンスの偏りを観測しませんでした。
というわけでハイエンド端末と同様に、1フレームあたりの所要時間のほうを掘り下げてローエンド端末の検証結果をじっくり比較してみたいと思います。
#### 3.3.2.11. ローエンド端末の検証結果抜粋
Unity単体 | ミラーアプリ メインのみ |
|
---|---|---|
FPSの平均値 | 71.09 | 63.49 |
1Fあたりの 所要時間(ms) |
14.11 | 15.86 |
#### 3.3.2.12. ローエンド端末の検証結果まとめ
こちらの相対比較もハイエンド端末と同様に1フレームあたりの所要時間に着目しますが、こちらは情報量が少ないので詳しい分析は行わず、いきなりまとめに入ってしまいます。
-
Unity単体 と ミラーアプリ(メインのみ) の差は 1.75ミリ秒
ローエンド端末の検証結果で得られた一番重要な情報は、Unity単体で実行した時に比べてミラーアプリを介してメインのUnityゲームを実行した時のほうが1フレームあたり1.75ミリ秒遅かったという点だと思います。
要するにミラーアプリ自体の負荷は1フレームあたり1.75ミリ秒程度ということになり、ハイエンド端末は1フレームあたり0.18ミリ秒だったので 随分と差が大きく開いてしまいました。
#### 3.3.2.13. ローエンド端末の検証結果2
ローエンド端末はProfilerの画像から分かるようにGPU負荷がかなり高かったので、ミラーアプリの画面描画負荷がメインのUnityに影響してしまっていると考え、Unityゲーム&ミラーアプリのウィンドウサイズを下げた状態で検証してみました。
ここまでの検証ではUnityゲーム&ミラーアプリのウィンドウサイズは1280x720でしたが、こちらの検証では800x450に落としています。これで描画面積が40%程度(39.0625%)になりました。
フレームレート(UnityProfilerなし)
経過時間 | Unity単体 | ミラーアプリ メインのみ |
---|---|---|
0.00 | 74.7 | 71.9 |
0.25 | 79.1 | 73.0 |
0.50 | 78.0 | 75.6 |
0.75 | 76.7 | 75.7 |
1.00 | 80.9 | 77.5 |
1.25 | 82.9 | 79.6 |
1.50 | 79.9 | 79.0 |
1.75 | 73.4 | 63.4 |
2.00 | 77.0 | 71.8 |
2.25 | 76.9 | 71.9 |
2.50 | 72.3 | 70.8 |
2.75 | 74.6 | 71.7 |
3.00 | 68.2 | 73.8 |
3.25 | 66.1 | 73.3 |
3.50 | 66.6 | 77.5 |
3.75 | 72.7 | 73.7 |
4.00 | 76.6 | 67.0 |
4.25 | 78.7 | 61.9 |
4.50 | 75.1 | 74.8 |
4.75 | 77.8 | 77.0 |
5.00 | 79.5 | 81.3 |
平均値 | 75.60 | 73.44 |
1フレームあたりの所要時間(ms)(UnityProfilerなし)
経過時間 | Unity単体 | ミラーアプリ メインのみ |
---|---|---|
0.00 | 13.39 | 13.91 |
0.25 | 12.64 | 13.70 |
0.50 | 12.82 | 13.23 |
0.75 | 13.04 | 13.21 |
1.00 | 12.36 | 12.90 |
1.25 | 12.06 | 12.56 |
1.50 | 12.52 | 12.66 |
1.75 | 13.62 | 15.77 |
2.00 | 12.99 | 13.93 |
2.25 | 13.00 | 13.91 |
2.50 | 13.83 | 14.12 |
2.75 | 13.40 | 13.95 |
3.00 | 14.66 | 13.55 |
3.25 | 15.13 | 13.64 |
3.50 | 15.02 | 12.90 |
3.75 | 13.76 | 13.57 |
4.00 | 13.05 | 14.93 |
4.25 | 12.71 | 16.16 |
4.50 | 13.32 | 13.37 |
4.75 | 12.85 | 12.99 |
5.00 | 12.58 | 12.30 |
平均値 | 13.27 | 13.68 |
#### 3.3.2.14. ローエンド端末の検証結果2抜粋
Unity単体 | ミラーアプリ メインのみ |
|
---|---|---|
FPSの平均値 | 75.60 | 73.44 |
1Fあたりの 所要時間(ms) |
13.27 | 13.68 |
#### 3.3.2.15. ローエンド端末の検証結果2まとめ
-
Unity単体 と ミラーアプリ(メインのみ) の差は 0.41ミリ秒
ローエンド端末でウィンドウサイズを1280x720から800x450に落としたところ、Unity単体で実行した時に比べてミラーアプリを介してメインのUnityゲームを実行した時のほうが1フレームあたり0.41ミリ秒遅かったという結果になりました。
1280x720の時に比べて差が縮まったので、やはりミラーアプリ自体の描画負荷もそこそこありそうです。
ローエンド端末で0.41ミリ秒なら筆者個人的には許容範囲内かなぁ……という感じですが、できれば大きなウィンドウサイズでも差が広がらないようにしたいところです。
#### 3.3.2.16. 性能検証結果の考察
性能の話が長くなってしまいましたが、筆者個人としての結論は できればもう少しパフォーマンスを改善したいが、今の段階でも許容範囲ではある といったところです。
今回の性能検証の結果を端的に言えば、ミラーアプリはハイエンド端末ではフレーム毎に0.18ミリ秒程度の負荷があり、ローエンド端末では1280x720なら1.75ミリ秒、800x450なら0.41ミリ秒程度の負荷があることになります。
ローエンド端末でウィンドウサイズを変更しての検証結果から分かるように、ミラーアプリ自体の負荷はミラーアプリの画面の描画負荷が大きく影響しています。
ミラーアプリの画面も当然ながらUnityの画面とは独立しており、ミラーアプリの画面はDirectXの仕様に則って描画する必要があります。
現在の実装では垂直同期で描画しており、例えば今回の検証で使用したローエンド端末は画面のリフレッシュレートが144Hzなので、DirectXの画面の描画が1秒あたり144回行われます。
Unityのフレームレートが下がっていても構わず144回描画しようとするのは無駄なので、できればUnityの現在のフレームレートに合わせて描画回数を制御したいところですが、それを実現するためにはUnityの現在のフレームレートをリアルタイムに把握する必要があり、それを知るためにPipe通信を毎秒何十回と行うのもそれはどうなんだという気がします…(余力があればそのうち試してみます)
というよりそれをやろうとすると、極端な例を挙げるとUnityゲームは60FPSで描画されているのにミラーアプリは30FPSで描画されているため実質30FPSになっているといった状況が発生しそうなので、ミラーアプリ側のフレームレートはなるべく下げないほうが良さそうです。
筆者がコーディングしたDirectXの描画ロジック自体に問題があるのかもしれませんが、残念ながら筆者はDirectXには疎いので、筆者の技量だとミラーアプリの描画の最適化は一朝一夕にはいかなさそうです。
そもそも現在の実装が既に簡素なつもりなので、数日かけて調べたけどこれ以上最適化の余地はなさそう!(曖昧) という結論になるのがオチのような気がします…
(本アプローチのサンプルプロジェクトを公開後にソースコードをご覧になられた方で、DirectXの描画周りで何か分かる方がいらっしゃいましたらコメントをいただければ幸いです……!)
### 3.3.3. 本アプローチの実用性の総括
性能検証のボリュームがそこそこあったので記事が長くなってしまいましたが(論文の執筆とか向いてなさそう)、本アプローチが実用的かどうかの結論としては 概ね実用的である(改善の余地あり) ということで本検証を締め括りたいと思います。
# 4. 総括
Unityで "ロード中の読み込み負荷でUnity全体が固まってる間" にアニメーションを再生する
と銘打った本記事ですが、前章にて述べた通り本記事で提案させていただいたアプローチは 改善の余地はあれど概ね実用的である と筆者は考えます。
とはいえ性能検証では メインのUnity単体 VS ミラーアプリ&メインのUnity の比較を中心に行ったため、そこにロード画面を担うサブのUnityが加わった途端に サブのUnityが足を引っ張って平時のパフォーマンスに影響を及ぼすようであれば本末転倒になってしまいます。
こればかりはサブのUnityをどれだけ作り込むかによって比較結果が変わる(≒人によって結果が異なる)ので本記事では検証できませんでしたが、メインのUnityを表示中にサブのUnityのプロセスを停止させておくなど何かしらの手法はありそうなので、サブのUnityがどれだけ凝ってても大局に影響はないんじゃないかなあ(曖昧) と筆者は割と甘く考えてます。
本記事の冒頭でも述べましたが、本アプローチを実現したライブラリを含むサンプルプロジェクトをそのうちGithub上で公開予定となります。
明確な公開日時は未定で、筆者なりのペースで本アプローチの検証を引き続き進めつつサンプルプロジェクトを作り次第公開、という感じにはなってしまいますが、公開したらリンクを本記事に追記予定となりますので、興味がおありでしたら是非お使いいただければと思います。
# 5. 本アプローチの課題・制約
ここからは筆者個人的にクリティカルではない話になってくるので、一旦前章で本記事の総括を挟ませていただきました。
本章以降は、本アプローチをもっと詳しく知りたいという方のみお読み頂ければと思います。
一旦ここまで本記事をお読みいただきありがとうございました。
というわけで本章では、本アプローチに関する既知の課題や制約を書き連ねていきます。
UnityやC#・C++の仕様上どうしようもないモノの他、筆者が公開予定のパッケージに手を加えれば対処可能なものも含まれます。
## 5.1. Unity謹製のアプリではなくなる
Unityゲーム自体はUnityエディタよりビルドされたごく普通のUnityアプリですが、ミラーアプリはC++で開発した独自のアプリケーションとなるため、Unity謹製アプリだったからこそ問題なかったこと が問題になる可能性はあります。
詳しくは確認してませんが、例えばSteamでリリースする際に独自のexeを使う点が何らかの理由で審査に引っかかるかもしれません。
また、開発者の端末では問題が無かったゲームをリリースした際、ダウンロードした第三者の環境特有の不具合などが生じる可能性も多分にあります。
こういった類の不確定要素が発生するというのが、本アプローチの課題の1つとなります。
## 5.2. DirectX11限定
本アプローチの要となる共有テクスチャですが、本アプローチのような使い方は恐らくイレギュラーと思われます(曖昧)。
DirectX11で実現できたのも偶然なのではないかと思うほど情報が少なく、そんな少ない情報を掻き集めてなんとか実現に至ったので、例えばDirectX12やVulkanで本アプローチを同じことができるかは分かりません。
筆者個人としてはDirectX11で実現できればとりあえずいいやという感じなので、他のグラフィックスAPIの検証予定は今のところありません。
## 5.3. Windowsのスタンドアロンアプリ限定
Unityはマルチプラットフォームが武器の1つですが、本アプローチは複数のプロセスを並行して動かすことで成立しているため、それができないプラットフォーム(モバイルプラットフォームやWebGL、Switch、PS4など)では実現不可能となります。
筆者が動作確認したのはWindowsのスタンドアロンアプリのみなので、それ以外の複数プロセスの並列実行が可能なプラットフォームで本アプローチを導入したい場合は実現性を別途確認する必要があります。
MacOSは単に筆者が所持していないため動作確認できていないだけですが、MacOSはDirectXを標準サポートしておらずMetalというグラフィックスAPIが主流らしいので、MacOSで実現するにはMetalに対応しなければいけません。
## 5.4. 64bitビルド限定
これは単純に筆者が32bit対応を必要としておらず未検証なだけなので、ライブラリを改造すれば32bit対応は可能かもしれません。
主にC++のポインタ操作やC#のunsafeなコードでのポインタ操作が64bitの影響を受けていると思われます(曖昧)。
# 6. 本アプローチの実現に至るまでの試行錯誤
本アプローチの実現は一筋縄ではいかず、様々なトライアンドエラーを重ねて実現させることができました。
本記事をお読みになられた方の中には「この方法は使えないの?」「なんでわざわざこんなやり方したの?」と思われた方もいらっしゃると思うので、本アプローチの随所でどうしてそのような選択をしたのかを、本章にて詳しく解説させていただければと思います。
2025/02/23 本章は後日追記予定となります。