Unityが無料で発行しているeBook"Optimaize Your Mobile Game Performance"を読み解いていこう!という企画で、今回はプログラミングとコード設計に関する章です。
Unityのコアの部分はC++で書かれていますが、スクリプト言語としてはC#が採用されています。近代的なオブジェクト指向言語であるC#は実際、扱いやすい言語ですが、Unity上ではそのコーディングに注意が必要な部分もあるので、今回の章で学べる知識はぜひ活かしていきたいです。
Programming and code architecture
UnityのPlayer Loopには、Unityのコア部分と相互にやり取りする関数が含まれています。この木のような構造は、初期化とフレーム毎のアップデートを扱う複数のシステムによって構成されています。
あなたの全てのスクリプト(※つまりC#スクリプト)は、このゲームプレイを生み出すPlayer Loopに依ることとなります。
Profiler上のPlayer Loopの箇所で、あなたのプロジェクト上の全てのスクリプトを確認できるでしょう。これから紹介する、豆知識やトリックでスクリプトを最適化することが出来ます。
(※以後、様々な豆知識やテクニックが説明されていきます)
UnityのPlayer Loopを理解しよう
Unityのフレーム・ループの実行順を理解しよう。Unityはいくつかのイベント関数をあらかじめ決まった順に実行します。Awake, Start, Updateの違いを理解しなければいけません。
Order Of Execution Of Event Function(イベント関数の実行順序)
毎フレーム実行されるコードを最小化しよう
そのコードが毎フレーム実行される必要があるのかを考えよう。必要のないコードはUpdate, FixedUpdate, LateUpdateの外に出そう。
Time.FrameCountなどを使ってNフレーム毎に処理する、ということも出来ます。
StartやAwake関数に重い処理を置かない
シーンが読み込まれた時、Awake, OnEnable, Start関数がそれぞれのゲームオブジェクト毎に実行されます。ここに重い処理を置くと、必要以上にローディング時間が掛かってしまう場合があります。
空のUnityイベントを避けよう
空のMonoBehavioursでもリソースを消費します。なので、空のStart, Updateは削除しよう。
プリプロセッサを使えば、ビルドを汚さず、エディター上においてのみ、Update関数を使う、ということも出来ます。
#if UNITY_EDITOR
void Update(){
}
#endif
Debug.Log文を削除しよう
Update, LateUpdate, FixedUpdate中のDebug.Logは、パフォーマンスを低下させます。ビルド前には取り除くようにしましょう。
Conditional Attributeを使うとより簡単にこれを行うことが出来ます。ログ出力のためのクラスを作成し、[System.Diagnostics.Conditional("ENABLE.LOG")]
アトリビュートを付けて、プレイヤー設定(Player Settings)でENABLE.LOGの定義シンボルをオフにすることで、コード全体からログ出力部を削除する、ということも可能です。
文字列ではなくてハッシュ値を使おう
unityではアニメーター、マテリアル、シェーダーのプロパティを内部では文字列として扱っていません。処理スピードのために、プロパティは全て、ID(整数値)に変換し、それで実際のプロパティを指定します。
文字列を使うと、結局内部ではハッシュ値への変換を行い、ハッシュ値を使う関数を呼び出しているので無駄です。
アニメーターではAnimator.StringToHashを、マテリアルやシェーダーではShader.PropertyToIDを使おう。(※スタート関数でこの処理を行い、変数に保存しておくのが安定だと思います)
正しいデータ構造を選ぶ
データ構造についてのあなたの選択は、フレーム毎に何千回も処理が反復されるにつれて、累積的な影響を与え得る。
リストや配列、ディクショナリを使うことに対して、注意を払っていますか?C#において正しいコレクションを選ぶのに、一般ガイドとなる・・・
を抑えておきましょう。
ランタイム中にコンポーネントを追加するのを避けよう
AddComponent関数をランタイム中に呼び出すのは幾分かのコストが掛かります。ランタイム中にコンポーネントを追加する際は毎回、Unityは複製や他に必要になるコンポーネントをチェックしなければならないからです。
プレハブをInstantiateするのは、既に必要なコンポーネントが準備、装着されているため、一般的には負荷が少なくて済みます。
ゲームオブジェクトやコンポーネントをキャッシュしよう
GameObject.FindやGetComponent関数はコストが高いので、Update関数で呼び出すのは避けたいです。代わりにStart関数でそれらを実行し、結果をキャッシュしましょう。(※キャッシュ"Cache"とは、一度読み込んだデータを直ぐに再利用できるように手元に置いておく、というような意味です)
そうすれば、一回だけの実行で済み、余計な呼び出しをせずにUpdate関数内でも何回でも使えます。
オブジェクト・プールを利用しよう
InstantiateやDestroyはガベージを生み、ガベージ・コレクションによるスパイクを発生させます。これは通常、プロセスを遅くします。これらの関数の代わりに、良く使うゲームオブジェクト(例えば、銃の弾やビーム)に対して、再利用や再生できるようにあらかじめ割り当てたオブジェクトのプールを使います。
CPUのスパイクがゲームに影響を与えないポイントで、ゲームオブジェクト群のインスタンスを作っておいて、そのプールをコレクションとして追跡、管理します。
ゲームプレイ中は単純に、必要になったら利用可能なゲームオブジェクトを有効にして、Destroyする代わりにそのゲームオブジェクトを無効にしてプールに戻します。
これはプロジェクト内部のメモリ割り当てを減らし、ガベージコレクションによる問題を防止することが可能になります。
・Unityにおける、簡潔なオブジェクト・プーリング・システムについて
ScriptableObjectを使おう
MonoBehaviourの代わりに、不変なデータや設定はScriptableObjectsに保存しよう。ScriptableObjectsは一回だけセットアップすればいい、プロジェクト内部に実存するアセットです。ゲームオブジェクトに直接アタッチすることは出来ません。
値や設定を保存するのに、ScriptableObjects内にフィールドを作成し、それをMonoBehavioursが参照します。
ScriptableObjectsからのフィールドを使うことで、MonoBehaviourによるゲームオブジェクトを生成する際に、余計なデータの複製を防ぐことが出来ます。
ScriptableObjectsがどのようにあなたのプロジェクトに活かせるかについて、
を視聴しましょう。ScriptableObjectsについてのドキュメントもあります。
以上です!ほぼ丸々翻訳になってしまいました・・・。捨てるところがほとんどない章だったので、しょうがないですが!実際、今回取り上げられた各テクニックは直ぐにでも使うべきものばかりだっだと思うので、活かしていきたいです。
ScriptableObjectsに関しては、いずれ単独テーマとしてとりあげて、みっちりやりたいと考えています。
次回はProject configuration編です。