Unity 最適化メモ
プロファイラ
最適化を始める前にプロファイラでログを取得する。スマホやコンソールで実行されるなら、実機で最適化の結果を確認する。
メモリ
文字列処理には StringBuilder を使う
String クラスは Immutable なので、文字を結合したり変更するたびにメモリにゴミが生成される。
GameObject.tag を文字列と比較するのではなく、GameObject.CompareTage を使う
GameOject.tag を文字列と比較すると、文字列を作成するのでメモリにゴミがたまる。
WaitForSeconds をキャッシュする
yield return new WaitForSeconds(1.0f); は呼び出されるたびにゴミを生成する。なのでフィールドに WaitForSeconds をキャッシュする。
private WaitForSeconds wfs = new WaitForSeconds(1.0f);
IEnumerator test(){
yield return wfs;
}
コルーチンを使わない
コルーチンはメソッド呼びだしより遅く、メモリ使用量も多い。ゲームオブジェクトの状態に関係なく呼ばれるため、追加の状態管理が必要になる。
LINQ と 正規表現オブジェクトを使わない
LINQ は for ループで代替する。foreach はコレクションによってはゴミがたまる。
正規表現オブジェクトはパターンが同じならキャッシュで対応できる。
System.GC.Collect でガーベジコレクターを呼び出す
ガーベジコレクターのプチフリが問題にならない場所では、System.GC.Collect でゴミ掃除ができる。
外部リンク
Understanding Automatic Memory Management
incremental garbage collector を使う
incremental garbage collector は負荷を複数フレームに分散することで、ガーベジコレクション一回の時間を短くする。
「Project Settings > Player > Configuration > Use Incremental GC」にチェックを入れると使用できる。
ゲームロジック
アップデートスキップを使う
毎フレームアップデートする必要のない部分は以下のようにして、負荷を分散できる。
private int interval = 3;
void Update(){
if (Time.frameCount % interval == 0){
// do something...
}
}
Start, Awake, OnEnable で負荷の重い処理をしない
シーンがロードされるとシーンの内のすべてのオブジェクトの Start, Awake, OnEnable が呼び出される。シーンに大量のオブジェクトを配置したり、Start、Awake、OnEnable に重い処理を書くと、ロード時間が長くなる。
空の Update()、Start() を削除する
空の Update()、Start() もリソースを消費するため、必ず削除する。テスト用の Update() が使いたい場合は、以下のようにすればエディタ内で実行する場合のみ Update が生成される。
#if UNITY_EDITORvoid Update(){
}
#endif
Debug.Log を削除する
Debug.Log は自動的に削除されない。以下のようなログクラスを作成し、リリース時に削除されるようにする。"ENABLE_LOG" シンボルは Player Settings で追加できる。リリース時には "ENABLE_LOG" シンボルを削除する。
public static class Logging{
[System.Diagnostics.Conditional("ENABLE_LOG")]
static public void Log(object message){
UnityEngine.Debug.Log(message);
}
}
文字のパラメーターではなくハッシュ値を使う
Animator, Material, Shader クラスはプロパティの読み書きの際にハッシュ値を使う。これはアクセスの高速化のためにこのような仕様になっている。Animator, Material, Shader クラスで Set や Get メソッドを使う場合は、Animator.StringToHash や Shader.PropertyToID(Material もこれを使う)で変換したハッシュをキャッシュしておくと無駄な変換処理を削減できる。
実行時にコンポーネントを追加しない
AddComponent は重いので使わない。コンポーネント設定済みのプレハブを Instantiate() した方が速い。
SendMessage() を使わない
単純に遅い。
ゲームオブジェクトやコンポーネントをキャッシュする
GameObject.Find, GameObject.GetComponent, Camera.main は遅い。これらのメソッドは Update で呼び出すべきではなく、Start() で取得してフィールドにキャッシュしておく。
文字列版の GetComponent は遅いので使わない。GetComponent<T>() か GetComponent(typeof(T)) を使う。
オブジェクトプールを使う
プレハブを Instantiate()、Destroy() するのは遅い。オブジェクトを管理するクラスを作って、不要なオブジェクトを非表示にしておき、必要になったら初期化して表示する。不要になったらまた非表示にする。
Unity の Update() 呼び出しはメソッド呼びだしよりも遅いので、オブジェクトプールで管理されているインスタンスは Update() の管理も行う方がいい。Update() を削除して OnUpdate(float dt) のようなメソッドを作成し、管理クラスから OnUpdate() を直接呼び出すようにする。
Update() を使わない
大量のオブジェクトがシーン内に存在する場合、Update() 呼び出しのオーバーヘッドが問題になる。Ryzen 5 2600、メモリは 16 GB のマシンで、100,000 個のオブジェクトが空の Update() を呼び出すだけで 42ms かかる。対して自作の OnUpdate() を呼び出す場合は、3.7ms。
大量のオブジェクトはマネージャクラスを作成して管理する。
ScriptableObject を使う
プレハブのスクリプトの変化しないフィールドを ScriptableObject へ移動させる。こうすることでメモリ使用量と、初期化時間とを削減できる。
外部リンク
Transform をキャッシュする
Transform の読み書きは遅い。一フレーム中に何度も位置・回転・拡縮が更新される場合は、何度も Transform にアクセスせずに、最終結果の書き込みに時にのみアクセスする。
private bool is_position_changed;
private Vector3 new_position;
public void SetPosition(Vector3 pos){
new_position = pos;
is_position_changed = true;
}
public FixedUpdate(){
if (is_position_changed){
transform.position = new_position;
is_position_changed = false;
}
}
localLocation、localRotation、localScale が使える場合は、そちらの方が余計な行列計算がないので速い。
不可視のオブジェクトのスクリプトを停止する
OnBecameVisible() と OnBecameInvisible() を使う。ただしゲームオブジェクトは Mesh や SkinnedMesh のような描画可能なコンポーネントがついている必要がある。一番単純なのは SetActive を使う方法だ。
void OnBecameVisible() { gameObject.SetActive(true); }
void OnBecameInvisible() { gameObject.SetActive(false); }
AI やアニメーションの LOD を実装する
遠景の敵の AI やアニメーションをスキップするか、低負荷なものに切り替える。
[SerializeField] GameObject _target;
[SerializeField] float _maxDistance;
[SerializeField] int _coroutineFrequency;
WaitForEndOfFrame wfeof = new WaitForEndOfFrame();
void Start() {
StartCoroutine(DisableAtADistance());
}
IEnumerator DisableAtADistance() {
while(true) {
float distSqrd = (Transform.position - _target.transform.position).sqrMagnitude;
enabled = (distSqrd < _maxDistance * _maxDistance);
for (int i = 0; i < _coroutineFrequency; ++i) {
yield return wfeof;
}
}
}
レンダリング
静的バッチ
静的バッチは初期化時にマテリアルを共有している動かないメッシュを結合して、ドローコールを削減する機能だ。ドローコールを削減する代わりに、初期化時のメッシュ結合と、追加のメモリ容量を必要とする。なので仕様が固まったなら、手動でメッシュを結合すると単純にドローコールを削減できる。
インスペクタで static にチェックを入れると、同じマテリアルを参照しているメッシュは静的バッチ処理される。
静的バッチはメモリを余計に使う。例えば 1,000 本の木のメッシュを静的バッチすると、その木のメッシュのコピーが 1,000 本コピーされたメッシュを生成する。
欠点
静的バッチは以下の欠点がある。
- 動的に追加するメッシュはバッチできない
- 実行時に移動・回転・拡縮できない
- オブジェクト単位のカリングができなくなる
オブジェクト単位のカリングは室内のメッシュでは重要になる。室内のメッシュは大量のオブジェクトが不可視になるので、静的バッチを使わない方がパフォーマンスがよいことも多い。
動的バッチ条件をチェックする
動的ドローコールバッチングを行うには以下の条件を満たす必要がある。
- バッチされるメッシュが同じマテリアルを参照している
- パーティクルとメッシュレンダラーのみバッチされる。スキンメッシュレンダラー等のコンポーネントはバッチされない
- シェーダの Vertex Attribute の上限は 900
- 正規拡縮(XYZ すべて2倍や3倍等)と非正規拡縮(XYZ の拡縮が一致しない)とは混在できない
- メッシュインスタンスが同じライトマップファイルを参照している
- マテリアルのシェーダが複数パスに依存していない
- メッシュがリアルタイムシャドウの影響を受けない
- バッチできるメッシュ数の上限は 300
- バッチできるポリゴン数の上限は 32,000
Vertex Attribute の上限が 900 のため頂点数の少ないメッシュしかバッチされない。例えば頂点が位置・法線・UV を持つ場合、バッチできる頂点数は 300 になる。
コンソールや Apple の Metal のような最新の API では通常、ドローコールの処理負荷がかなり低く、動的バッチ処理を使用する利点が全くないことがしばしばあります。
出典:https://docs.unity3d.com/ja/current/Manual/DrawCallBatching.html
テクスチャ
テクスチャは正方形かつ2のべき乗にする
テクスチャが正方形でない、もしくは2のべき乗(128, 256, 512, 1025, 2048, 4096など)でない場合、メモリに無駄が発生する。
適切な圧縮フォーマットを選ぶ
通常は RGB Compressed DXT1(圧縮率1/6)、法線など高い品質が要求されるなら RGB(A) Compressed BC7(圧縮率1/3)を使う。圧縮フォーマットのテクスチャは VRAM にそのままロードされるため、VRAM 使用量の削減にもなる。
Crunch 圧縮は実行時に CPU で DXT か ETC に解凍され、画質が劣化する。テクスチャサイズが小さくなることでダウンロード時間が短縮されるため、地形や背景のテクスチャによく適用される。
外部リンク
DirectX 11の圧縮フォーマットBC1~BC7について(前編)
DirectX 11の圧縮フォーマットBC1~BC7について(後編)
ミップマップを使う場合
ミップマップはモアレを抑制し、遠景のレンダリング速度を向上させる。ただしメモリ使用量が 33% 増える。以下のようなケースではミップマップは不要だ。
- 2D ゲームのテクスチャ
- GUI テクスチャ
- パーティクルエフェクト
- 遠景で非表示になるメッシュのテクスチャ
異方性フィルタリングを使う場合
異方性フィルタリングはテクスチャを斜めから見た場合に、テクスチャがぼけたりゆがんだりするのを防ぐ。異方性フィルタリングも追加の実行コストが発生するので、必要なテクスチャにのみ設定する。地形や床・壁・天井のテクスチャに適用されることが多い。
テクスチャアトラスを使う
小さいテクスチャは大きい1枚のテクスチャにまとめる。
ミドル未満の端末(PC・スマホ両方)では巨大なテクスチャを使う方が遅い場合があるので注意。そのような端末はドローコールのボトルネック以前に地力が足りないことが多い。
外部リンク
PSD ファイルを直接インポートしない
Unity は PSD ファイルを直接インポートできるが、品質が悪い。PSD を使う場合は開発中にのみ限定し、リリース時には Photoshop や GIMP で出力した PNG や JPG を使う。
メッシュのインポート
非キャラクターモデルのリグは無効にする
メッシュをインポートするとデフォルトで Animator コンポーネントが追加される。これは毎フレーム1回呼ばれるので不要ならば必ず削除する。
余計な法線・接線(Tangent)を計算しない
シェーダで接線を使わない場合や法線マップを使用する場合は、メッシュをインポートする際に None にして、それらを生成しないようにする。
メッシュ圧縮(Mesh Compression)を使う
メッシュ圧縮は頂点や法線、UV 座標のビット精度を下げてメモリ使用量を削減する。モデルの品質が劣化するので、使用する前に許容レベルかどうかチェックする。
アニメーションモデルの Optimize Game Objects オプションを有効にする
Optimize Game Objects が無効な場合、Unity は、モデルがインスタンス化されるたびに、そのモデルのボーン構造をミラーリングする大きな Transform ヒエラルキーを作成する。この Transform ヒエラルキーの更新には高い負荷が掛かる。Particle System や Collider などの別のコンポーネントが添付されている場合は特にそうだ。またこれにより、Unity の、メッシュ スキニングやボーンアニメーション計算をマルチスレッド処理する能力に制限が掛かる。
Optimize Game Objects は Rig タブにある。
Optimize Mesh を有効にする
Optimize Mesh は Model タブにあり、デフォルトで有効(Everything)になってる。
Optimize Mesh はレンダリングを高速化するため、頂点データの並び替え等を行う。
Mesh Filter コンポーネントの Optimize() でプロシージャルメッシュも最適化できる。
そのほか
可能ならば長さの二乗を使う
ベクトルの長さの計算に使うルートは計算負荷が高い。距離の大小を比較するなら二乗長(sqrMagnitude)を使うと速い。
float hit_radius = 2.0f;
GameObject target;
void Update(){
float dist_sqrd = (Transform.position - target.transform.position).sqrMagnitude;
if (dist_sqrd < hit_radius * hit_radius){
// hit
}
}
ゲームオブジェクトの Null チェックに ReferenceEquals を使う
if (gameObject != null)
は余計な変換処理が入るため、非効率的だ。代わりに ReferenceEquals を使う。
if (!System.Object.ReferenceEquals(gameObject, null))
ReferenceEquals はゲームオブジェクトだけでなくコンポーネントの Null チェックにも使える。
外部リンク
MonoBehaviourが本当にnullか否かを確認するにはReferenceEqualsとis演算子を使え
外部リンク
【無料公開】社内研修書籍『Unity パフォーマンスチューニングバイブル』のPDF公開&オープンソース化しました!
モバイルゲームのパフォーマンスを最適化しよう:Unity のトップエンジニアが語るプロファイリング、メモリ、コードアーキテクチャのヒント
Optimize Your Mobile Game Performance