読者です 読者をやめる 読者になる 読者になる

テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

Unityの2DやuGUIのパフォーマンス最適化についての7つのTips

Unity 2D 最適化・デバッグ Sprite GUI(uGUI/NGUI/旧UI)

最適化というか、改善する話です。

バイル向けに作った時に妙に遅かった場合、この辺りの要因が絡んでいるケースが多い印象です。この辺りは作る際に確認しながら作ると、後々痛みが少なくて良いかなと思います。

目次

プロファイラで確認する

何をどうするにせよ、プロファイラで最も負荷のかかっているボトルネックを見つけ出し殺す必要があります。

ここでCPUのカメラの負荷が高ければBatches周りを、GPUが高ければフィルレートや自作シェーダー周りを、CPUのその他が高ければ自分のスクリプトを疑う感じです。

接続方法は、Autoconnect Profilerにチェックが入ってる状態でアプリを起動後、ProfilerのAtiveProfilerから目的のデバイスを見つけて繋ぐだけです。

f:id:tsubaki_t1:20150324053809p:plain

f:id:tsubaki_t1:20150324051707p:plain

エディタ上で動かすプロファイラの注意点

ここで注意すべき事は、エディターで再生中のゲームをプロファイルすると、エディターの持つパラメータの幾つかが紛れ込む事です。例えばテクスチャのメモリ消費量が実際のゲームプレイのソレと比べて倍になります(エディターの持っているメモリとゲームの持っているメモリが表示されるため)

これを解決する最もシンプルな方法は、実際に実機に転送してプレイする事です。ですが、色々あってプレイできない場合、PC向けに転送してしまうアイディアがあります。普通のゲームはビルドすると別プロセスでゲームを起動するので、特に問題はないかと。ちょっとPlayerlogからコードへジャンプしにくいですが些細な問題でしょう( ˘ω˘)スヤァ

f:id:tsubaki_t1:20150324051346p:plain

メソッド毎の負荷について

各メソッド内の負荷はProfiler.BeginSampleでプロファイラに計上してもらうと、負荷の確認がやりやすいです。スクリプト周りの最適化はまた今度。

正確な値が知りたい場合はInstrumentsを使います。

Instruments を使って Unity アプリをプロファイリングする が素晴らしい - テラシュールブログ

iOSでパフォーマンスに悪影響を与えるボトルネックを確認 - テラシュールブログ

DrawMeshを抑える

DrawMeshとは描画命令で、バッチとは描画時に一度に描画する機能の事です。基本的にGPUOpenGLを通じて命令を通達する回数は少なければ少ないほど良いので、一度に描画できるものは一回の命令で描画できるようにします。

この機能はFrame Debuggerを使用すると、より直感的に理解できます。

例えば、f:id:tsubaki_t1:20150324004220p:plainf:id:tsubaki_t1:20150324004239p:plainを組み合わせて、ボタンf:id:tsubaki_t1:20150324004314p:plainを3つ作成します。

f:id:tsubaki_t1:20150324004418p:plain

単純な描画回数を考えると、(f:id:tsubaki_t1:20150324004220p:plainf:id:tsubaki_t1:20150324004239p:plain) * 3 なので6回の描画と考えられますが、実際は同じ階層の描画がまとめられるので、2回の描画で完結します。

これはUnityがバッチ処理で1回の描画内で描画出来るオブジェクトは、自動的にまとめて描画してくれる為です。この回数はFrameDebuggerとStatsのBatchesで確認出来ます。

f:id:tsubaki_t1:20150324004711g:plain

f:id:tsubaki_t1:20150324013218p:plain

バッチはより複雑な形状やUI同士が多少重なっていても同様に作用します。ですが、一度に描画できない…例えばオブジェクトが入れ子担っている場合は一括で描画する事はできません。バッチはあくまで「一度に描画できるのも」に機能するため、複数の階層を持つ場合は一括で描画する事ができません。

例えば、下のDrawMesh2回とDrawMesh4回の違いを見てみます。

f:id:tsubaki_t1:20150324005139p:plain

f:id:tsubaki_t1:20150324005645p:plain

 バッチングされない謎の理由

何故こんな事が起こるのか。

ポイントは、f:id:tsubaki_t1:20150324004220p:plainの上にf:id:tsubaki_t1:20150324004239p:plainを描画している事です。描画順はf:id:tsubaki_t1:20150324004239p:plainを先に描画してf:id:tsubaki_t1:20150324004220p:plainを描画。そのため、f:id:tsubaki_t1:20150324004239p:plainf:id:tsubaki_t1:20150324004220p:plainの上にあるため、f:id:tsubaki_t1:20150324004239p:plainが一括で描画出来ない事に問題があります。Frame Debuggerで確認してみます。

f:id:tsubaki_t1:20150324010102g:plain

 バッチングしやすくする為には

この解決法は2つあります。

一つは描画順が入れ子にならないように調整することです。入れ子になりさえしなければ、描画回数は保たれます。

f:id:tsubaki_t1:20150324010404p:plain

2つ目はf:id:tsubaki_t1:20150324004220p:plainf:id:tsubaki_t1:20150324004239p:plainを同じテクスチャに格納する事です。TexturePackerやUnity標準のSpritePackerでスプライトを一つのテクスチャへ格納し、そこから取得する形で表現する事で、1回のDrawMeshで入れ子構造なスプライトも描画出来ます。

f:id:tsubaki_t1:20150324011534p:plain

f:id:tsubaki_t1:20150324011610p:plain

フィルレートに気をつける

フィルレートとは「1秒辺りに何ドット描画する事ができるのか」という事です。2Dでも3Dでも画面に何らかのオブジェクトを描画するには色を塗る必要があるって事です。

これの何が問題かと言えば至極単純、モバイルの狂った解像度に対してGPUが貧弱な為、画面を何度も描画するような事があると描画が追いつかなくて死ぬって事です。

よくあるフィルレートで死ぬパターン

例えば、下のような「キャラクターの前に枠がある」絵があるとします。一見、問題が無いように見えますが、実際は透明な部分も「透明で描画」されており、フィルレートがかなり圧迫されます。

何度も描かれている部分はSceneViewのOverdrawで確認する事ができます。明るい色はヤバイ所です。最もヤバイのは白い部分で、パーティクルをやり始めた人は大体それを作り「パーティクルのパフォーマンスが悪い」と勘違いします。(パーティクルでパフォーマンス出したければ「大きく少なく」と認識してます)

f:id:tsubaki_t1:20150324015118p:plain

f:id:tsubaki_t1:20150324015133p:plain

フィルレート対策

フィルレート対策ですが、Unity 2DとuGUIでは若干対応が異なります。

uGUIにおけるフィルレート対策

まずuGUIですが、中央や周囲の色が不要な箇所を抜き取ります。やり方はSprite Editorでスライスを行う感じです。外枠も削りたいならSingleではなくMultiで行います。

UnityのuGUIとImageとSpriteとスライスについて - テラシュールブログ

スライス後、ImageのImageTypeをSlicedに設定、あとFillCenterのチェックを外す事で、中央が塗られなくなりフィルレートの節約になります。

f:id:tsubaki_t1:20150324015522p:plain

f:id:tsubaki_t1:20150324015616p:plain

f:id:tsubaki_t1:20150324015719p:plain

Unity 2Dにおけるフィルレート対策

Unity 2Dの場合は少し毛色が異なります。Unity 2D(Sprite Renderer)で描画したオブジェクトは、自動的にスプライトの形状にポリゴン的に繰り抜かれ透明部分が最低限の状態で描画されます。

f:id:tsubaki_t1:20150324020904p:plain

f:id:tsubaki_t1:20150324020915p:plain

解像度を下げる

ちなみに、これらとは別にもっと手っ取り早くフィルレートを稼ぎたければ解像度を下げるのが一番楽です。

単純に、描画範囲が半分になれば描画すべき範囲は1/4になります。元々フル解像度のリソースなんて用意する訳が無いので、用意したリソースに見合う解像度に調整する事で、かなりのフィルレートを稼ぐことが出来ます。

解像度を下げてパフォーマンスを得る - テラシュールブログ

 ゲーム画面は低解像度、UIは高解像度

また低解像度のRenderTextureにゲーム画面を描画しUIに描画するといった手法もアリと言えばアリです。

この手法はUI(というか文字)が潰れるのが嫌な場合や、ドット絵素材使っているのにテキストはフル解像度なエセ8bitゲーム(ピクセルアート)なゲームを作りたい場合や、「画面を埋め尽くす馬鹿みたいに重ねがけしたα付き画像」や「無駄にリッチなイメージエフェクト」がある場合に有効です。

上がパーティクルを1000000個ほど出した絵。下はそれを低解像度なRenderTextureで描画した結果です。極端に描画負荷をかけている場合は割と差がでますが、そうでない場合はそこまで露骨に差は出ません。

f:id:tsubaki_t1:20150324022414p:plainOverheadとRenderTextureコスト、あと結局は1回は全画面描画している点で使用不要を判断すると良さそうです。

特に今後は3Dも基本シェーダーリッチでImageEffect付けないとショボイという絵が増えると思うので、この手法はアリじゃないかなと思います。

解像度戦争早く終われ。

バッチについて注意すべき事

スプライトはバッチングを行うことで効率的に描画できますが、スプライトの数が大量にある場合、バッチングが効いているのにBatchesが増える事があります。

これは「1回にまとめられる最大数が900頂点まで(UV使うものは300)」という仕様(パフォーマンス的には無限にバッチングするよりある程度纏ったら次のバッチしたほうが良い)に寄る考え方です。

Unity - マニュアル: ドローコール バッチング

Unity 2Dのスプライトに使用する頂点は高解像度なUI程(綺麗に抜き取る為に)高くなる傾向にあり、高解像度かつ大量に配置すると割と簡単にBatchesを増やしてくれます。

この場合、MeshTypeをFullRectに設定しスプライトのポリゴン生成を止めるのもひとつの手かもしれません。

f:id:tsubaki_t1:20150324061615p:plain

f:id:tsubaki_t1:20150324061705p:plain

またバッチングは描画をまとめてくれる機能ですが、この「まとめる処理」にもCPUを使用します。このためバッチはある程度の精度は無視しており、また、バッチングが効きにくいゲームは逆にバッチングを切った方がパフォーマンスが上がることがあります。

他にも、RenderTextureにオフスクリーンで背景を描画して使いまわす手法や、スプライトメッシュの結合、オクルージョンかリングの2D版といった、描画回数を減らすアプローチは色々とありますが、こういった手法は基本的に何かとトレードオフです。実際に効果があるのかプロファイラで確認しながら試すのが良さそうです。

フォントの扱いについて

Unityは何時頃からかダイナミックフォントを取得し、 TrueType FontsもしくはOpenType Fontsを実行時にレンダリングし取得できるようになりました。

これにより膨大な文字数を誇る日本語で文字範囲を制限すること無く、文字が使えますチャンチャン…とは行きません。

確かにダイナミックフォントを使用することで使用できる文字数の割にアプリサイズを大幅に減らすことが可能となりましたが、同時に文字のレンダリングコストが発生します。

さて、本来日本語を使用できないOpenGL下でどうやって日本の文字を表現しているのかというと、単純にフォントをテクスチャへレンダリングして表現しています。

Unity4のダイナミック(or システム)フォントを使う - テラシュールブログ

f:id:tsubaki_t1:20150324035115p:plain

テクスチャ更新時のスパイク

これの問題は、文字数が多くなった時です。文字数が増えると、このフォントは使っていない文字を削除してフォントのテクスチャを作り直します。この際、サイズが2048x2048みたいなサイズになっていると、割と強力なスパイクとなります。

これは単純に考えればそこまで発生しないように思うかもしれませんが、モバイルのUI向けに大きめのフォントを使用して、そこにエフェクト用の巨大な文字を使用すると簡単に起こります。

f:id:tsubaki_t1:20150324040108p:plain

2つの対策

これの対抗案としては2つのアイディアがあります。

一つはFontをDynamicではなくUnicode等を指定することです。これで事前にフォントをレンダリングして提供する(所謂BMPフォントみたいな感じ)ので、レンダリング時に発生するスパイクは抑えられます。

Unity - マニュアル: フォント

もう一つのアイディアとしては少々トリッキーですが、同じフォントをサイズ別に用意する事です。大きなフォントは基本的に似たような文字しか使用せず、小さいフォントは会話やダイアログに使用するといった形で運用します。

2つのフォントを使い分けることで、大きなフォントが場所を取り、小さいフォントが大きいフォントを押し出し、さらに大きいフォントが場所を取り(スパイク発生)…といった流れを防ぐ事ができます。

f:id:tsubaki_t1:20150324041033p:plain

UIを動かす(変形させる)コストについて

複雑なUIを作成した後、UIを動かすとCanvas.BuildBatchが走りバカに出来ない負荷になることが有ります。これは、例えばスクロールビュー等々を動かしたり、○○Layoutを起動しっぱなしにすると発生することがあります。

f:id:tsubaki_t1:20150324041853p:plain

これは何かというと、どうもCanvasは一度Canvas内のUI(CanvasRenderer)を収集しビルドする(一つのメッシュ化する)事でパフォーマンスの最適化を図っているらしく、1個でもUIが動くとCanvas内のUIを再収集して作り直し…といった事を行うみたいです。このため、常に動いているUIが含まれている場合、これが高いコストになるケースが有ります。(World Spaceで指定CanvasのUIの一部でも入ると全部のUIが表示されてしまうのはこのためです)

この対策としては、幾つかのアイディアがあります。

まず、動かすUIの親にCanvasコンポーネントを追加する事。これでバッチングによる一括描画ができなくなりますが、Canvas.BuildBatchは軽くなります。

f:id:tsubaki_t1:20150324054046p:plain

リストビューのような要素に関しては、NGUIのリストビューと同じように、CanvasのWorld Spaceで要素の一部分を取得し描画するアイディアもあります。

また、いっそUnity 2Dを使ってUIの動く要素を表現するアイディアもあります。

※この辺りはUnity 5.2でかなり最適化が入りました。

物理演算について

色々と検証している感じですと、Unity 2Dのrigidbody2dは確かに「2dで物理演算をする」場合に最も効率的に動いてくれるのですが、どうも「プレイヤーを物理演算を使わず動かしたい場合」はPhysX3(rigidbody)の方が高速で動いてくれる気がします。弾幕とかはちゃんと作れる人は自前でスクリプト書いたほうが速くなりそうです。

 印象としては、こんな感じです。

2D向け物理(半物理)

 →rigidbody2d + collider2D

  もしくは rigidbody2d(velocity 直弄り)+collider2D

物理無し2D向け、複雑な形状の当たり判定

 →rigidbody(isKinematic) + collider(イベントを使いたい場合)

  もしくは collider + Physics.OverlapSphere

  もしくは collider + rigidbody ( velocity 直弄り)

その他、シンプルな形状、凄く多い

 →collider + rigidbody or Physics系。

  もしくは自作(作るものによる)

 

特に○○Collider2Dシリーズはrigidbody2d抜きで動かした場合のペナルティが致命傷レベルなので、rigidbody抜きの場合は(rigidbody2dのisKinematicを使った場合でも)colliderで制御した方が良さそうです。

パフォーマンスが出なければTimeのFixedTimeStepを落としましょう。

ちなみにTransformで動かした場合は、物理演算的なバウンドが出来ないので問答無用でめり込み貫通する。よく壁を貫通して走るミクさんを見るので一応…

 

その他…

VSYNCについて

vsyncはONにしたほうが電池とか色々と良いらしいです。あとフレームレートがdon'tsyncと比べて安定します(カクつきにくくなる)。多分。

初期値はDon't syncな場合もあったり無かったり。

Tweenとか

DOTweenが良さそうです。

 

基本的に最適化は何かとのトレードオフだと思うので、ココで書いたメモ以外にも色々と試してみると良いと思います。(出来ればアプローチはシェアしてくれると嬉しいです)

Pixel Perfect

動かすと馬鹿にならないコストが来ます。動かすならPixel Perfectは切ってから動かすのが良さそうです。

ScreenSpace-Cameraを使用した状態でCameraを動かす

UIが再構築されるので、結構高い負荷が返ってきます。

動かすカメラにScreenSpace-Cameraを設定するのは止めた方が良さそうです。

RectMask2Dを使う

tsubakit1.hateblo.jp

---

長いので一旦ここまで。

スクリプト編はまたそのうち。

関連

tsubakit1.hateblo.jp