はじめに
Unity、C#の最適化に関してまとめられている記事がそんなに多く見られないため、自分が業務でUnityを使用していて得た最適化の知見をまとめようと思います。
よく言われている基礎的な事から、最近のUnity・C#のアップデートで追加された機能を使用した新しめの最適化手法まで幅広くまとめようと思います。
このページは気づいたら随時更新しようと思っておりますので、ブックマークなどをおすすめします。
2023/07/04 更新
Unity系最適化
Unityから呼ばれるイベント関数を使用しない
Awake、Update等のUnity関数を、以下のように統括するクラスを作成して代わりの関数を呼ぶようにしましょう。
// マネージャクラス
public class UpdateManager : MonoBehavior
{
// Updateを呼んで欲しいキャラクタークラスのリスト
private List<Character> _ActiveCharacters = new();
// Updateを呼ぶリストに登録
public void RegisterCharacter(Character character)
{
_ActiveCharacters.Add(character);
}
// Unityから呼ばれる
private void Update()
{
foreach(var character in _ActiveCharacters)
{
character.OnUpdate();
}
}
}
// キャラクタークラス
public class Character : MonoBehavior
{
// 移動速度
private float _MoveSpeed = 0.01f;
// マネージャクラスから呼ばれる更新処理
public void OnUpdate()
{
// サンプルなので右にひたすら移動するだけ
var pos = transform.position;
transform.position = new Vector3(pos.x + _MoveSpeed * Time.deltaTime, pos.y, pos.z);
}
}
Debug関数を直接読んでいる箇所をビルド時に含めないようにする
UnityのProjectSettings > Player > Other Settings内のScripting Define Symbolsに、デバッグ用のシンボルを追加しておきます。
実際のコードでは、Debug関数を呼んでいる個所を下記のようにifdefで括ると、該当のシンボル名がScripting Define Symbolsに記載されていれば有効、なければ処理が実行されないようになります。
private void OnDamage(int attack)
{
_Hp -= attack;
#if PROJECT_DEBUG
Debug.Log($"Damage : {attack}")
#endif
}
ただ、この書き方ですと、全Debug部分にいちいち処理を記載しなければならず面倒くさいです……。
そのため、以下のようにConditional属性を付けた自前のLog関数を作成し、Debug.Logを呼ぶ代わりに下記のLog関数を呼ぶようにすると、同様にシンボル有効時のみ処理が行われます。
public static class DebugExtension
{
[Conditional("PROJECT_DEBUG")]
public static void Log(string message)
{
Debug.Log(message);
}
}
Transformの座標、回転値の更新を1Fに複数回行わないようにする
Transformの値を直接代入しないようにするとパフォーマンスが向上します。
例として以下のようにVector3の座標、回転の変数を用意して、それを書き換えるようにし、LateUpdateなど諸々の処理が終わったタイミングでtransformに代入するとよいでしょう。
public class CharacterLocator : MonoBehavior
{
public Vector3 Position{get;set;}
public Vector3 Rotation{get;set;}
private void LateUpdate()
{
transform.position = Position;
transform.rotation = Quaternion.Euler(Rotation);
}
}
Transform最適化一覧
こちらの記事に最適化Tipsがたくさん書かれている。
https://qiita.com/sator_imaging/items/ff5811885f515a0a4998
空メソッドの削除
特にUnityから呼ばれるようなAwake, Updateやoverride関数は、中身が記載されていなくてもコールされるだけで処理負荷が発生するので、不要な関数は積極的に消しましょう。
GetComponents<T>の置き換え
GetComponents<T>は、GetComponents(readonly List<T>)関数に置き換えることができます。
https://silverweed.github.io/Optimization_tips_for_Unity/
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を参照
構造体かクラスどちらを選ぶか
一定以上のバイト数ならクラスを選ぶとよい。
それ以外だと値型、参照型の違いなどがある。
readonly struct, ref, inを適切に扱う
後々ちゃんと記事化する
参照型の変数を持つ構造体を極力減らす
構造体全体がGCの対象になる可能性がある。stringとか特に気を付けるべきかも。
参照型の変数を持つ場合はクラスにした方がよいかも。
https://gametukurikata.com/basic/optimiz
C#最適化
List, Dictionaryの初期化時にキャパシティを設定
要素数が事前に分かっているものは、初期化時にキャパシティを設定してあげるとよいです。
private List<Character> _characters = new List<Charcter>(10);
以下の記事をみると、50000要素くらいまでなら指定した方が速いみたい
Listに初期サイズを指定した場合としてない場合で処理時間(パフォーマンス)を比べてみた – Qiita
ラムダ式を書く際にGC Allocを発生しないようにする
以下の記事を参考に修正する。
Unity での GC Alloc対策 ダイジェスト – Qiita
intやfloatの計算を先にする
ベクトルを含む計算式のときは、intやfloatの計算を先にしておくと計算効率が上がるらしい。
ArrayPoolを使用する
https://notnullvariable.com/2021/reduce-memory-allocation-for-array/
Collectionをnewで初期化しなおしている個所をClearに置き換え
ListやDictionary等のCollectionをリセットしたいときに、newで初期化しなおしている個所がもしあれば、Clearを呼ぶ。
Collectionの最適化
ListをHashSetに置き換えるか、BinarySearch(Sortされている必要がある)にする
https://qiita.com/ttake/items/518bf36bf165fa9afb59
List以外のCollectionをforeachで回している場合はgcが発生するためfor文に置き換える
ListはUnityのコンパイラでfor文に自動変換されるためgcが発生しないが、IEnumerableやIList等のほかのCollectionは最適化が発生しないため、for文に置き換える。(for文だとGCが発生しない)
要素数が少ないコレクションをFrugalObjectパターンに置き換える
要素数が少ない(1つとか)ときは内部的にCollectionではなく単一のクラスの変数を利用し、要素数が増えたらCollectionを作成するようなFrugalObjectパターンというものがある。
以下の記事で解説が載っている。
https://notnullvariable.com/2021/reduce-memory-allocation-for-array/
JetBrainsではCompactListという名前で用意されている。
https://github.com/JetBrains/rd/blob/master/rd-net/Lifetimes/Collections/CompactList.cs
sealedを付ける
※現状のUnityでは高速化されない
sealed修飾子は、このクラスはoverrideされないことを示す修飾子。本来はこれを付けることでコンパイラが最適化されるがどうやら現状は最適化されないらしい。。。
https://notnullvariable.com/2021/sealed-have-no-effect-on-il2cpp/
今後のUnityのアプデ(.Net 6対応時)で高速化される可能性があるので、付けても良い。
Dispose漏れ対応
UniRxなどで、Dispose漏れがあったら随時修正する。
コメント