【Unity】Unite 2016「モバイル端末向けのUnityアプリケーションの最適化実践テクニック」翻訳


- いくつか翻訳が間違っている可能性があるためご了承ください
- 一部の項目は英語力が及ばす翻訳できませんでした
    - 講演動画が公開されたら追記するかもしれません

# 講演資料

http://japan.unity3d.com/unite/unite2016/files/DAY1_1330_Room1_HarknessDundore_Long_Big.pdf

# Profiling

## オススメのツール

- iOS：Instruments
- Android：VTune, Snapdragon Profiler
- Unity Editor：タイムライン
- 5.3以降：Memory Profiler

## Instruments

- 無料でXcodeに内蔵されています
- IL2CPPビルドでも動作します
- モバイルCPUのプロファイリングに最適なツールです
- 起動時間のプロファイリングに最適なツールです
- Instrumentsの使い方  
http://blogs.unity3d.com/2016/02/01/profiling-withinstruments/

## InstrumentsでPlayerLoopを読む

- `BaseBehaviourManager::CommonUpdate`
    - `Update`、`FixedUpdate`、`LateUpdate`
- `PhysicsManager::FixedUpdate`
    - `OnCollision*`、`OnTrigger*`
    - `Physics2DManager::FixedUpdate`
- `DelayedCallManager::Update`
- `PlayerRender`
    - `OnWillRender`
- `UI::CanvasManager::WillRenderCanvases`
- `EnlightenRuntimeManager::Update`

## InstrumentsにおけるCoroutineの扱い

- Coroutineの実行は2つに分割されます
    - `StartCoroutine`を呼び出した場所
    - `DelayedCallManager`

## Instrumentsにおける検索方法

- 検索欄にメソッド名を入力します
- オススメの検索ワード
    - `::Box`、`Box(`、`_Box`
    - `String_`

## 5.3以降：Memory Profiler

- BitbucketからソースコードをDL可能です  
https://bitbucket.org/Unity-Technologies/memoryprofiler
- 重複しているテクスチャが調査できます

### 導入方法

1. Unityプロジェクトの「Assets」フォルダに「Editor」フォルダを追加
2. メニューの「Window>MemoryProfilerWindow」を選択
3. 「Take Snapshot」を選択

# Assets

## Assetの設定ミス

- 開発者は人間なのでヒューマンエラーが発生します
- ミスは開発時間を浪費します
- ミスを防ぐために設定を自動化することをオススメします

## Assetのよくある設定ミス

- テクスチャサイズがおかしい
- 圧縮形式がおかしい
- アバターのリグの設定がおかしい
- アセットによって設定規則が違うことがある

## Assetの設定の自動化

```csharp
public class AssetAuditExample : AssetPostprocessor
{
    public void OnPreprocessTexture() 
    {
        // …
    }
    public void OnPreprocessModel() 
    {
        // …
    }
}
```

- `AssetPostprocessor`のコールバック関数はAssetのインポート時に呼び出されます
- `OnPreprocess*`関数を実装する必要があります
- `assetImporter`インスタンスを使用してAssetの設定を変更します

```csharp
public class ReadOnlyModelPostprocessor : AssetPostprocessor
{
	public void OnPreprocessModel()
	{
		ModelImporter modelImporter = (ModelImporter)assetImporter;
		if(modelImporter.isReadable)
		{
			modelImporter.isReadable = false;
			modelImporter.SaveAndReimport();
		}
	}
}
```

## テクスチャの汎用ルール

- Read/Writeを無効にする
- 可能ならミップマップを無効にする
- テクスチャが圧縮されているかどうかを確認する
- テクスチャのサイズが大きすぎないように注意する
    - UIのアトラスは2048x2048や1024x1024で問題ない
    - モデルのテクスチャは512x512以下で作成する

## モデルの汎用ルール

- Read/Writeを無効にする
- キャラクターではないモデルのリグを無効にする
- メッシュの圧縮を有効にする

## オーディオの汎用ルール

- iOSではMP3の圧縮を有効にする
- AndroidではVorbisの圧縮を有効にする
- モバイルゲームではモノラルを使用する
- 可能な限りビットレートを落とす

# Unityにおけるメモリ

## Unityにおけるメモリ管理の問題点

- ヒープ領域が縮小されることはない
- 一時的なメモリ割り当てが問題になる
- 60FPSのゲームで1KBの割り当てが毎フレーム行われた場合、1秒で60KBのメモリ割り当てが発生する
- もし1分ごとにGCが動いた場合ゲームパフォーマンスに影響してしまう
- 3,600KBのメモリが必要になってしまう

## GC Allocを追う

- UnityのProfilerを開いてGC Allocでソートする
- もし0Bじゃない項目があれば、可能な限り0Bに近づける
- ただし、シーンの読み込み中のGC Allocは仕方がない

## メモリの安全活用

- `List`や`HashSet`を使い回す
- stringを避ける
    - `StringBuilder`を再利用すること
- デリゲートやラムダ式による匿名関数を避ける

## ボックス化を避ける

- ボックス化は値型を参照型として渡す時に発生します
- 値が一時的にヒープ領域に割り当てられます

```csharp
int x = 1;
object y = new object();
y.Equals(x); // ここで「x」がボックス化されてヒープ領域に割り当てられます
```

- `Dictionary`のキーに列挙型を使用した時にも発生します

```csharp
enum MyEnum { a, b, c }

// ...

var myDictionary = new Dictionary<MyEnum, object>();
myDictionary.Add(MyEnum.a, new object()); // ここで「MyEnum.a」がボックス化されます
```

- `IEqualityComparer`を使用することで回避可能です

## foreachを避ける

- ループ開始時に`Enumerator`が割り当てられてしまいます
- `foreach`ではなく`for`を使用すること

## UnityのAPIに気をつける

- 配列を返すAPIは毎回配列を作成してしまう
    - 値が変化していない場合でも
- 次のコードでは`Touch[]`が大量にメモリに割り当てられます

```csharp
for ( int i = 0; i < Input.touches.Length; i++ )
{
    Touch touch = Input.touches[i];
    // …
}
```

- 下記のように書き換えることで`Touch[]`のメモリ割り当てを1度に抑えることができます

```csharp
Touch[] touches = Input.touches;
for ( int i = 0; i < touches.Length; i++ )
{
    Touch touch = touches[i];
    // …
}
```

# CPUパフォーマンスにおけるTips（読み込み周り）

## XMLやJSONなどのテキストフォーマット

- テキストの変換は非常に遅い
- よくあるJSONパーサは内部でリフレクションを使用しているため遅い
    - Unity 5.3以降であれば`JsonUtility`を使用することがオススメ
    - `JsonUtility`はリフレクションではなくシリアライズを使用しているため速い

## XMLやJSONの使用量を減らす

- 戦略その1：テキストの変換は行わない
    - ScriptableObjectを使用する
    - 頻繁に変更されないマスタデータなどに対して有効

- 戦略その2：必要な部分のみ変換する
    - テキストの変換が必要な場合は可能な限り小さい範囲で変換する
    - 変換後の結果をキャッシュして使い回す

- 戦略その3：スレッドを使用する
    - テキストの変換を別スレッドに逃がす
    - ただし、別スレッドではUnity APIが使用できないので注意が必要

## 巨大なプレハブ

※上手に翻訳できませんでした

## Resourcesフォルダ

- ゲーム開始時にResourcesフォルダ内のすべてのアセットにインデックスが割り振られます
- これは回避したり遅延させたりすることができません
- Resourcesフォルダのアセットをアセットバンドルに逃がして解決します

# CPUパフォーマンスにおけるTips（ランタイム）

## Material/Animator/Shaderのプロパティ

- MaterialやAnimator、Shaderのプロパティには名前でアクセスします
- 内部的には指定された名前をint型のハッシュ値に置き換えます
- 次のコードのような処理を書いてはいけません

```csharp
material.SetColor(“_Color”, Color.white);
animator.SetTrigger(“attack”);
```

- 最初にハッシュ値をキャッシュしてそれを利用します

```csharp
static readonly int material_Color = Shader.PropertyToID(“_Color”);
static readonly int anim_Attack = Animator.StringToHash(“attack”);
material.SetColor(material_Color, Color.white);
animator.SetTrigger(anim_Attack);
```

## string型のボックス化

- 正規表現や`string.StartsWith`、`string.EndWith`は遅い
- Instrumentsでボックス化が発生している場所を検索可能です
    - `::Box`や`_Box`で検索
    - `String_`で検索

# Unity UI

※上手に翻訳できませんでした

# Trampolines

## はじめに

- 必要なときにだけこれらの技術を適用します

## Unityのコールバックがどのように呼び出されているか

- コンポーネントやそれらに紐付くコールバック関数は、内部的にはC++でLinked Listで管理されています
    - `Update`、`LateUpdate`など
- Linked Listを反復処理して各コールバック関数を呼び出しています
- これら小さなオーバーヘッドになります
- コールバック関数の数が非常に多くなると、オーバーヘッドも無視できなくなります

## `UpdateManager`を作成してコールバックを置き換える

```csharp
public class UpdateManager : MonoBehaviour
{
	public static UpdateManager Instance { get; set; }
	
	void Awake() { Instance = this; }
	
	public UnityEvent OnUpdate = new UnityEvent();
	
	void Update() 
	{
		OnUpdate.Invoke();
	}
}
```

## メリット

- Trampolineのオーバーヘッドが消去できます
- 登録したコールバックは解除することができます