Unity・C#を簡単に最適化できるTips集

Unity
スポンサーリンク
スポンサーリンク

はじめに

Unity、C#の最適化に関してまとめられている記事がそんなに多く見られないため、自分が業務でUnityを使用していて得た最適化の知見をまとめようと思います。

よく言われている基礎的な事から、最近のUnity・C#のアップデートで追加された機能を使用した新しめの最適化手法まで幅広くまとめようと思います。

このページは気づいたら随時更新しようと思っておりますので、ブックマークなどをおすすめします。

2023/07/04 更新

Unity系最適化

Unityから呼ばれるイベント関数を使用しない

Awake、Update等のUnity関数を、以下のように統括するクラスを作成して代わりの関数を呼ぶようにしましょう。

Update Manager
// マネージャクラス
publicclassUpdateManager :MonoBehavior
{
   // Updateを呼んで欲しいキャラクタークラスのリスト
   privateList<Character> _ActiveCharacters=new();

   // Updateを呼ぶリストに登録
   publicvoidRegisterCharacter(Character character)
    {
        _ActiveCharacters.Add(character);
    }

   // Unityから呼ばれる
   privatevoidUpdate()
    {
       foreach(var characterin _ActiveCharacters)
        {
            character.OnUpdate();
        }
    }
}
Character
// キャラクタークラス
publicclassCharacter :MonoBehavior
{
   // 移動速度
   privatefloat _MoveSpeed=0.01f;

   // マネージャクラスから呼ばれる更新処理
   publicvoidOnUpdate()
    {
       // サンプルなので右にひたすら移動するだけ
       var pos= transform.position;
        transform.position=newVector3(pos.x+ _MoveSpeed* Time.deltaTime, pos.y, pos.z);
    }
}

Debug関数を直接読んでいる箇所をビルド時に含めないようにする

UnityのProjectSettings > Player > Other Settings内のScripting Define Symbolsに、デバッグ用のシンボルを追加しておきます。

実際のコードでは、Debug関数を呼んでいる個所を下記のようにifdefで括ると、該当のシンボル名がScripting Define Symbolsに記載されていれば有効、なければ処理が実行されないようになります。

C#
privatevoidOnDamage(int attack)
{
   _Hp-= attack;
  
   #if PROJECT_DEBUG
   Debug.Log($"Damage : {attack}")
   #endif
}

ただ、この書き方ですと、全Debug部分にいちいち処理を記載しなければならず面倒くさいです……。
そのため、以下のようにConditional属性を付けた自前のLog関数を作成し、Debug.Logを呼ぶ代わりに下記のLog関数を呼ぶようにすると、同様にシンボル有効時のみ処理が行われます。

C#
publicstaticclassDebugExtension
{
    [Conditional("PROJECT_DEBUG")]
   publicstaticvoidLog(string message)
    {
        Debug.Log(message);
    }
}

Transformの座標、回転値の更新を1Fに複数回行わないようにする

Transformの値を直接代入しないようにするとパフォーマンスが向上します。
例として以下のようにVector3の座標、回転の変数を用意して、それを書き換えるようにし、LateUpdateなど諸々の処理が終わったタイミングでtransformに代入するとよいでしょう。

C#
publicclassCharacterLocator :MonoBehavior
{
   publicVector3 Position{get;set;}
   
   publicVector3 Rotation{get;set;}
   
   privatevoidLateUpdate()
    {
        transform.position= Position;
        transform.rotation= Quaternion.Euler(Rotation);
    }
}

Transform最適化一覧

こちらの記事に最適化Tipsがたくさん書かれている。

Unity の Transform のパフォーマンス最適化まとめ - Qiita
(今更ですが)毎フレーム大量の Transform を扱う機会があったので、Unity 上で Transform を更新する際の手法とパフォーマンスの比較・最適化の方法のまとめを。 毎フレーム数百かそれ以上の Transform の...

空メソッドの削除

特にUnityから呼ばれるようなAwake, Updateやoverride関数は、中身が記載されていなくてもコールされるだけで処理負荷が発生するので、不要な関数は積極的に消しましょう。

GetComponents<T>の置き換え

GetComponents<T>は、GetComponents(readonly List<T>)関数に置き換えることができます。

Optimization tips for Unity
I don’t use Unity very much (especially since I mostly switched to Godot Engine for hobby projects), but every now and then I come back to it for one reason or ...

IL2CPPの最適化

AggressiveInliningをつける。

IL2CPPにおけるAggressiveInliningの効果 – NotNullVariable

構造体の最適化

structは原則refなどの修飾子を使用して参照渡しをする

refなどの修飾子をつけずに引数で受け渡すと、構造体の仕様として引数で渡す際に内部的にコピーを生成して受け渡すため、受け渡し先の関数で書き換えても変更されず、無駄にコピーの処理も走ってしまいます。

このため、refやin修飾子を活用して、コピーではなく参照渡しを使用するようにしましょう。
(structの変更を反映させたくない場合は修飾子を使わずに渡してください)

Dictionaryのキーにstructを指定する場合

Dictionaryのキーにstructを指定する場合、IEquatable<T>とGetHashCodeの実装を行うと、不要なGCを抑えられます。(Slide p.34)

StructLayoutを適切に設定する

p.36を参照

構造体かクラスどちらを選ぶか

一定以上のバイト数ならクラスを選ぶとよい。

それ以外だと値型、参照型の違いなどがある。

C#高速/最適化メモ - Qiita
概要 色々な高速化などの噂について検証してみました 探せばいくらでもあるforとforeachの速度などは載せていません なるべく二進数換算で小さい値に 2進数換算をした時に、1の数がなるべく少ない方が高速らしいという噂を...

readonly struct, ref, inを適切に扱う

後々ちゃんと記事化する

参照型の変数を持つ構造体を極力減らす

構造体全体がGCの対象になる可能性がある。stringとか特に気を付けるべきかも。
参照型の変数を持つ場合はクラスにした方がよいかも。

Unityでの最適化について
Unityでゲームを作ったけど動作が重くてまともにゲームを進行出来ない!という時に最適化をしてゲームプレイに支障が出ないような方法を見ていきます。

C#最適化

List, Dictionaryの初期化時にキャパシティを設定

要素数が事前に分かっているものは、初期化時にキャパシティを設定してあげるとよいです。

C#
privateList<Character> _characters=newList<Charcter>(10);

以下の記事をみると、50000要素くらいまでなら指定した方が速いみたい
Listに初期サイズを指定した場合としてない場合で処理時間(パフォーマンス)を比べてみた – Qiita

ラムダ式を書く際にGC Allocを発生しないようにする

以下の記事を参考に修正する。

Unity での GC Alloc対策 ダイジェスト – Qiita

intやfloatの計算を先にする

ベクトルを含む計算式のときは、intやfloatの計算を先にしておくと計算効率が上がるらしい。

ArrayPoolを使用する

配列のメモリアロケーションを抑える
C#で配列を使う際にメモリアロケーションを抑えるテクニックを3つ紹介します。 stackalloc stackalloc...

Collectionをnewで初期化しなおしている個所をClearに置き換え

ListやDictionary等のCollectionをリセットしたいときに、newで初期化しなおしている個所がもしあれば、Clearを呼ぶ。

Collectionの最適化

ListをHashSetに置き換えるか、BinarySearch(Sortされている必要がある)にする

一覧で検索するならListではなくHashSetにしましょう - Qiita
Javaで一覧に検索(contains)をかけるならListではなくSet使うのですが、 きっとC#も同じだよなーと思い計ってみました。 10万の文字列から重複チェックをしながら新しい一覧を作るのにかかった時間です。 メソッ...

List以外のCollectionをforeachで回している場合はgcが発生するためfor文に置き換える

ListはUnityのコンパイラでfor文に自動変換されるためgcが発生しないが、IEnumerableやIList等のほかのCollectionは最適化が発生しないため、for文に置き換える。(for文だとGCが発生しない)

要素数が少ないコレクションをFrugalObjectパターンに置き換える

要素数が少ない(1つとか)ときは内部的にCollectionではなく単一のクラスの変数を利用し、要素数が増えたらCollectionを作成するようなFrugalObjectパターンというものがある。

以下の記事で解説が載っている。

配列のメモリアロケーションを抑える
C#で配列を使う際にメモリアロケーションを抑えるテクニックを3つ紹介します。 stackalloc stackalloc...

JetBrainsではCompactListという名前で用意されている。

rd/rd-net/Lifetimes/Collections/CompactList.cs at master · JetBrains/rd
Reactive Distributed communication framework for .NET, Kotlin, C++. Inspired by Rider IDE. - rd/rd-net/Lifetimes/Collections/CompactList.cs at master · JetBrain...

sealedを付ける

※現状のUnityでは高速化されない

sealed修飾子は、このクラスはoverrideされないことを示す修飾子。本来はこれを付けることでコンパイラが最適化されるがどうやら現状は最適化されないらしい。。。

IL2CPPでsealedを付けても速くならない
Unityでclassを定義するときにsealedを付けると高速化すると言う話があります。この話の大本のソースはIL2C...

今後のUnityのアプデ(.Net 6対応時)で高速化される可能性があるので、付けても良い。

Dispose漏れ対応

UniRxなどで、Dispose漏れがあったら随時修正する。

コメント

タイトルとURLをコピーしました