【Unity】パフォーマンスチューニング

はじめに

はめまして、カヤックの技術基盤チームの Unity エンジニアのアファトです。この記事はカヤックUnityアドベントカレンダー2016の21日目の記事になります。

ゲーム開発には機能やゲームプレイを実装するたびにシーンやコードだんだんコンプレックスになって、ターゲットFPSまでパフォーマンスが出なくるというパターンは開発中にはよくありますが、リリースするまではターゲットデバイスのスペックに応じて、特に低スペックなデバイスでゲームが遊べないほどもっさりしてる可能性があります。そのためにパフォーマンスチューニングが必要です。なぜパフォーマンスが出ないか、どうやって改善できるか、軽く紹介します。

FPSについて

Frame per secondsというい用語は1秒の中にフレームが何回か更新されるという意味です。一般的なゲームには 30FPS か 60FPS か その間でフレームが更新されますので、1フレームの枠は数ミリセカンドぐらいしかありません(30FPS は 1s/30 = 33.3ms, 60FPS なら 1s/60 = 16.7ms)。ゲームがスムーズに見えるために、そういうターゲットFPSを守って、1フレームの中行なう処理全てがその枠に納めないといけません。

FPSが落ちる理由

1フレームに全て行った処理の時間がターゲットFPSの枠を超えたら、FPSが落ちると言います。よくあるパターンとして、だんだん処理が重くなってFPS落ちてる、一瞬的なフリーズ(コマ飛び・コマ落ち)、最初からFPSが低い、といったものがあります。そこで、ゲームに行なう処理は全体的に2種類があります。CPUの処理とGPUの処理です。CPU bound的なネック(CPU処理が重い)か、GPU bound的なネック(GPU処理が重い)か、両方とも処理が重いかどちらかになります。そのネックを把握するために、Unity Profilerが便利です。テラシュールウェアブログの『【Unity】CPUプロファイラでパフォーマンスを改善する 前編』が参考になるのでよろしければご覧下さい。CPU boundか、GPU boundか、そのネックを把握してから適切な最適化を実施した方が良いです。

GPUネック

Unity ProfilerのCPU Usage - Overviewのところにある Gfx.WaitForPresent はGPU boundの良い指標です。この処理時間が長いければ長いほど、GPU boundの可能性が高くなります。GPUの処理にどこの処理が重いのか、Unity Profilerで調べにくいことがありますが、一番簡単な方法はシーンにある GameObjects を disable/enable して、FPSがどう変わるかをチェックします。そこでFPSに一番大きな影響が出る GameObject から調査します。

image

グラフィック関連のチューニングについて

  • Dynamic batchingを増やして、DrawCall数を減らします。Drawcall batchingについて。Spriteを使ってるならpacking tagを利用して、DrawCallが減らせます。それについてはこのテクニカルブログ 『【Unity開発】Sprite画像とSprite Packerまとめ【ひよこエッセンス】』が参考になります。

  • Texture サイズやフォーマットを調整します。 適切なサイズやフォーマットはどのようなTextureを使うかによって違います。パーティクル用のTextureはできるだけ小さくします。サイズの調整はTexture Import Settingsのmax sizeで調整した方が良いです。適切なテクスチャーフォーマットはターゲットプラットフォームや画像の模様によります。テクスチャーフォーマットについて

image

  • もっとシンプルなShaderを使います。Unityのシェーダーを使うのであれば、一番シンプルなシェーダーグループ Unlit 又は Mobile のシェーダーを使います。

  • Quality Settingsを調整します。使ってないところや必要ないところの設定に disable または 0 にセットします。

image

CPUネック

CPUでプロセスする処理:ゲームコード、物理シミュレーション、パーティクル、スキニングなどの重さを Unity Profiler で調べて、一番重い(時間かかる)処理から調査します。対策はどんな処理かによって異なります。

ゲームコードでよくある原因とその対策

GC処理発生しないように

Garbage Collector, ゴミ掃除的な処理。GC処理はコストが高くて、コードを書くときに気をつけないとゴミ発生しやすいので、FPS落ちる理由になりがちな原因の一つです。可能な限りゴミ発生しないように(特に毎フレームに発生するゴミ)コードを書いた方が良いです。以下はゴミが発生することによくあるパターン:

毎フレームに呼ばれるメソッド (UpdateLateUpdateFixedUpdateCoroutine) に入るとよくないもの:

  • foreachのloop

Unityが使ってるMonoのバージョンがかなり古いので、foreachを使うたびに数bytesのゴミが発生します。代わりに IEnumerator を取得して while で loop します。詳しくはカヤックUnityアドベントカレンダー2016の7日目の記事が参考になります。

  • 文字列の変更や結合

文字列が長くて更新頻度が高い場合 (結合含めて)、StringBuilderを使った方が良いです。ミリセカンドタイマーとか毎フレーム更新必要な文字列はキャッシュできるならキャッシュします。

const int MaxNumber = 100;
string[] numberCache;
void GenerateNumberCache()
{
    numberCache = new string[MaxNumber];
    for (int i = 0; i < MaxNumber; ++i)
    {
        numberCache[i] = i.ToString();
    }
}

void Start()
{
    GenerateNumberCache();
}

void Update()
{
    timerMsecLabel.text = numberCache[currentTimerMsec];
}
  • オブジェクトまたは配列のallocation

一時的なリストならキャッシュ又はバッファーを作るか、可能であればclassをstruct化します。

// ----------- NG -----------
void LateUpdate()
{
    var tempHogeObject = new HogeClass();
    var tempScore = new int[10];
    var tempHogeList = new List<Hoge>();
}

// ----------- OK -----------
HogeClass _tempHogeObject = new HogeClass();
int[] _tempScore          = new int[10];
List<Hoge> _tempHogeList  = new List<Hoge>();
void LateUpdate()
{
    // バッファーやキャッシュをリセットする
}

// ----------- OK -----------
void LateUpdate()
{
    var tempHogeObject = new HogeStruct();
}
  • LINQを避けます

  • boxing キャスト (struct => object のキャスト)を避けます

interface IHoge
{
    void DoHoge();
}

public class HogeClass : IHoge
{
    public void DoHoge()
    {
        // ...
    }
}

public struct HogeStruct : IHoge
{
    public void DoHoge()
    {
        // ...
    }
}

public void DoHogeSafely(IHoge[] hogeList)
{
    for (int i = 0; i < hogeList.Length; ++i)
    {
        if (hogeList != null) // NG: HogeStructのobjectの場合、objectにキャストされる (boxing)
        {
            hogeList[i].DoHoge();
        }
    }
}

Unityの重いメソッドをできるだけ少なくする

  • InstantiateDestroy (Object Poolingテクニックで対策する)
  • GameObject.Find*Resources.Find*系なAPI。可能な限りマネージャー的なコンポーネントで参照を管理します

読み込みタイミングを調整して、ウォーミングアップする

リソースを使う直前にResources.Loadで読み込みはしないで、シーンロードなどに特定なタイミングで行なうようにします。テクスチャーがあるオブジェクトは事前にInsantiateしてウォーミングアップしないと、最初に画面に描画される時にコマ落ち発生する可能性があるので、メモリーが許す限りウォーミングアップした方が良いです。

public class GameScene : MonoBehaviour
{
    public GameObject hogeSkillEffectPrefab; // 大きなテクスチャーを使ってるオブジェクトのプレハブ
    public Vector3 warmingUpPos; // カメラのfrustumに入ってるけど画面に見えないところ

    void Start()
    {
        // シーンの読み込み
        var hogeSkillEffect = Instantiate(hogeSkillEffectPrefab, warmingUpPos, Quaternion.identity) as GameObject;
    }
}

参照キャッシュできるものをキャッシュする

参照をキープできるものは、毎回GetComponentをせず、一回getして参照を保存します。

物理シミュレーションが原因の場合

image

  • Physics Manager の Physics.solverIterationCount 数を下げます。
  • Collidersの関係性でレイアーを分けて、Physics Manager の Layer Collision Matrix を設定します。

終わりに

ゲームがサクサク動けるならきっとプレイヤーが喜んで遊べますので、FPSが落ちないように、パフォーマンスチューニングをしましょう。ただし、最初からずっとパフォーマンスのことを考えていたら、開発のペースが落ちる可能性があるので、特定の期間でやった方が良いです。

パフォーマンスチューニングはかなりでかいトピックなので、それについての記事がたくさんあります (例: Unite 2016 Optimizing Mobile Applications) 。ぜひ参考にしてください。

明日はGitでUnityプロジェクトの管理についての記事になります。担当は mada です。お楽しみに!