はじめに
はめまして、カヤックの技術基盤チームの 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 から調査します。
グラフィック関連のチューニングについて
Dynamic batchingを増やして、DrawCall数を減らします。Drawcall batchingについて。Spriteを使ってるならpacking tagを利用して、DrawCallが減らせます。それについてはこのテクニカルブログ 『【Unity開発】Sprite画像とSprite Packerまとめ【ひよこエッセンス】』が参考になります。
Texture サイズやフォーマットを調整します。 適切なサイズやフォーマットはどのようなTextureを使うかによって違います。パーティクル用のTextureはできるだけ小さくします。サイズの調整はTexture Import Settingsのmax sizeで調整した方が良いです。適切なテクスチャーフォーマットはターゲットプラットフォームや画像の模様によります。テクスチャーフォーマットについて
もっとシンプルなShaderを使います。Unityのシェーダーを使うのであれば、一番シンプルなシェーダーグループ
Unlit
又はMobile
のシェーダーを使います。Quality Settingsを調整します。使ってないところや必要ないところの設定に disable または 0 にセットします。
CPUネック
CPUでプロセスする処理:ゲームコード、物理シミュレーション、パーティクル、スキニングなどの重さを Unity Profiler で調べて、一番重い(時間かかる)処理から調査します。対策はどんな処理かによって異なります。
ゲームコードでよくある原因とその対策
GC処理発生しないように
Garbage Collector, ゴミ掃除的な処理。GC処理はコストが高くて、コードを書くときに気をつけないとゴミ発生しやすいので、FPS落ちる理由になりがちな原因の一つです。可能な限りゴミ発生しないように(特に毎フレームに発生するゴミ)コードを書いた方が良いです。以下はゴミが発生することによくあるパターン:
毎フレームに呼ばれるメソッド (Update
、LateUpdate
、FixedUpdate
、Coroutine
) に入るとよくないもの:
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の重いメソッドをできるだけ少なくする
Instantiate
、Destroy
(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して参照を保存します。
物理シミュレーションが原因の場合
- Physics Manager の Physics.solverIterationCount 数を下げます。
- Collidersの関係性でレイアーを分けて、Physics Manager の Layer Collision Matrix を設定します。
終わりに
ゲームがサクサク動けるならきっとプレイヤーが喜んで遊べますので、FPSが落ちないように、パフォーマンスチューニングをしましょう。ただし、最初からずっとパフォーマンスのことを考えていたら、開発のペースが落ちる可能性があるので、特定の期間でやった方が良いです。
パフォーマンスチューニングはかなりでかいトピックなので、それについての記事がたくさんあります (例: Unite 2016 Optimizing Mobile Applications) 。ぜひ参考にしてください。
明日はGitでUnityプロジェクトの管理についての記事になります。担当は mada です。お楽しみに!