Unityでの最適化について

今回はUnityLearnのパフォーマンス問題の修正のページを見てゲームを最適化する時に必要な処理を見ていこうと思います。

Fixing Performance Problems - 2019.3 - Unity Learn
Once you've discovered a performance problem in your game, how should you go about fixing it? This tutorial discusses some common issues and optimization techni...

最適化例は上記のリンク先を元にしています。

他の参考サイトは記事の最後に記載してありますので、不明な点はそちらを参照してみてください。

わたくしが誤解して記述しているものや、その誤解に準じて作ったサンプルもあるかもしれません。(^_^;)

ご了承ください。(._.)

スポンサーリンク

最適化する前に

最適化の為にむやみやたらと修正するのでは埒があきません。

Unityプロファイルやフレームデバッガーや外部のアプリケーション等を用いてどこで問題が起きているかを把握し、影響が大きい部分に焦点を当てて原因を突き止めて問題を解消していく必要があります。

Unityエディター上で確認することが出来ますが、正確に判断するにはビルド時にDevelopment BuildとAutoconnect Profilerにチェックを入れてビルドすることで実機で動かした時のプロファイルを確認することが出来ます。

詳細はUnityマニュアルを参照してください。

アプリケーションのプロファイリング - Unity マニュアル
ターゲットのリリースプラットフォームでアプリケーションのプロファイリングを行うには、ターゲットデバイスをネットワークに接続するか、ケーブルで直接コンピューターに接続します。また、Unity エディターでアプリケーションを直接プロファイリングして、 アプリケーション開発中のおおよそのプロファイリング結果を確認することもで...

ゲームのFPSを60以上で維持したい場合は60FPSは60フレーム/秒なので60フレーム/1000ミリ秒→16.666・・・フレーム/ミリ秒となり、1フレームで16.666・・・ミリ秒以内に処理が実行されるように修正していきます。

なので、以下のように1フレームで182.45msも時間がかかっている場合は何らかの対処が必要です。

プロファイルの1フレームの結果

問題の診断の仕方としては

Diagnosing Performance Problems - 2019.3 - Unity Learn
A profiling tool gives detailed information about how a game is performing. The Profiler window is a powerful profiling tool that is built into Unity. This tuto...

パフォーマンス最適化 入門~2021年版~ - Unityステーション
Unityで作ったゲームが何か重い、こういうときにはパフォーマンス計測することが大事です。今回は3つのケースを取り上げ、パフォーマンス計測ならびに処理の改善を図るデモをいたします。00:00:00 放送開始00:01:07 処理が重い時は計測しよう00:05:23 UnityProfilerを使ってみよう00:1...

が非常に参考になります。

また、実際に重いプロジェクトパッケージをインポートして自分で改善していくことが出来るUnityLearnのコンテンツもあります。

Optimizing for Performance - Unity Learn
In this simple maze game, the player has to navigate through the maze, while avoiding enemy NPCs. The application is experiencing some performance issues, that ...

最適化例の処理を施したとしても必ず最適化されるわけではなく、他の部分の影響があって却って遅くなる可能性もあります。

なので、最適化処理をする前のデータと最適化処理をした後のデータを見比べて良くなっているかどうかを判断する必要があります。

最適化をするのは自己責任でお願いいたします。(._.)

最適化例

それでは最適化例を見ていきます。

スクリプト関連の最適化

intやfloatの計算を先にする

intやfloatの数値の計算よりもベクトルやマトリックス、クォータニオンを計算する方が処理に時間がかかります。

とするとspeedとtransform.fowardでfloat × Vector3というベクトルの計算をし、その値とTime.deltaTimeを掛けるのでVector3 × floatのベクトルの計算になるのでベクトルの計算が2回あって時間がかかります。

そこで先にspeed * Time.deltaTimeを計算し、その後にVector3と掛けるようにします。

こうすることでベクトルの計算が1回になります。

多次元配列よりジャグ配列の方が速い

多次元配列(例えばmultidimensionalArray[ , ])は関数呼び出しを必要とするので、ジャグ配列(例えばjagArray[][])の方が速い

ループ内で同じ条件判定をしない

毎フレーム実行されるUpdateメソッド内(毎フレーム実行しなくても)でfor文を使ってループ処理をする時に、ループ内で毎回条件を判定していて無駄に処理を走らせている。

上の場合はエリア内にいる時にfor文で処理をしたいのですが、for文内でinTheAreaかどうかをループ回数分比較しているので、ループの前に条件で判定する。

ループ内で毎回チェックをするのをやめループ外で1回だけ判定するようにします。

状況が変化した場合のみ実行する

Updateメソッド内でスコアを表示するDisplayScoreメソッドを毎回表示する無駄な処理をしている。

スコアは更新された時だけ表示すればいいので、敵を倒した時等にスコアを設定するメソッドを呼んで、そこでDisplayScoreメソッドを呼べば敵を倒した時だけスコアの更新が出来る。

Updateメソッドで毎回実行しない

毎フレーム実行する必要がない場合は一定フレーム毎に実行する。

Updateメソッドは毎フレーム実行されるので、毎フレーム実行する必要がない場合は間引いて一定のフレーム毎に実行するようにします。

キャッシュを利用する

Updateメソッドで毎回コンポーネントを取得して利用すると時間がかかる。

Updateメソッドで毎回GetComponentでコンポーネントを取得するのは処理に時間がかかるので、何度も使用するコンポーネントはあらかじめStartメソッド等でキャッシュをしておく。

オブジェクトをプーリングする

シューティングゲーム等で弾を撃つたびに弾のプレハブからインスタンスを生成すると非効率的。

あらかじめ弾のインスタンスとそのRigidbodyを取得して非アクティブにして用意しておき、必要になったらアクティブにして再利用する。

上の例では弾を無効化する処理と弾がプールされていない時に新たにインスタンス化する処理は書いていませんが、もし気になる方は以下の記事を参照してください。

Unityでゲームオブジェクトのプーリングを行ってゲームオブジェクトを再利用する
Unityで弾を発射する時にプーリングした弾のインスタンスを再利用する機能を作成していきます。

Startメソッドで弾のインスタンス化とRigidbodyの取得処理も時間がかかる場合はインスペクタで設定しておく方が速いかもしれません。

UnityAPIの時間がかかる処理へのアクセスを減らす

UnityのAPIへのアクセスに時間がかかることがあります。

SendMessageやBroadCastMessageを使わない

SendMessageやBroadCastMessageは高負荷な処理です。

なので、SendMessageを使わず実行したいメソッドを持つゲームオブジェクトのスクリプトを直接指定して実行するか、イベントやデリゲートなどを使います。

Findメソッドを使用しない

GameObject.Findメソッドは全てのゲームオブジェクトから指定したゲームオブジェクトを探す便利な処理ですが毎フレーム実行するには重いです。

なので、キャッシュを使用したり、これより処理が速いFindWithTagメソッドでタグから探したり、あらかじめインスペクタで設定出来るようにして探す処理をしないようにします。

Transformの位置と回転の更新

transform.positionを更新すると内部のOnTransformChangedイベントがその子に全て送られ更新するので多くの子を持っている場合は処理が多くなります。

なので、以下のようにtransform.positionの値を何度も更新するとそれだけ処理に時間がかかります。

transform.positionの位置を更新する前に移動先の計算をすませて、最後にtransform.positionの値に入れたり、OnTransformChangedイベントが発生しないtransform.localPositionを使うようにします。

空のUpdateメソッド呼び出し

スクリプトを作成するとデフォルトでStartメソッドとUpdateメソッドが作成されていますが、UpdateやLateUpdate、イベントハンドラなどは隠れたオーバーヘッドがあります。

なので使用していないUpdateメソッド等を持つスクリプトが膨大になると影響を及ぼすかもしれません。

使用していないUpdateやLateUpdate、イベントハンドラ等のメソッドは削除しておきます。

もしくはUpdateメソッド等を使わずに更新を管理するスクリプトを作成し、そこから定期的に処理が必要なゲームオブジェクトに対してだけなんらかの処理を実行させるようにします。

毎フレーム実行したいゲームオブジェクトに取り付けられたスクリプトを登録しておき、管理しているスクリプト(この場合はOptimizeScript9)のUpdateメソッドで一生に実行させています。

Vector2やVector3の計算

Vector2やVector3での計算処理は時間がかかるものがあります。

Vector2.magnitudeやVector3.magnitudeはベクトルの長さを計算出来ますが、

$$\sqrt{x^2+y^2}$$

上のように平方根の計算が入り、時間がかかります。

比較をするだけならばVector2.sqrMagnitudeやVector3.sqrMagnitudeを使った方が速いです。

Vector2.sqrMagnitudeやVector3.sqrMagnitudeは

$$x^2+y^2$$

のようになり、平方根の計算がないのでmagnitudeより計算が早く比較にも使えます。

Camera.mainをそのまま使わない

Camera.mainは内部でFindメソッドと同様の処理が走るので処理が遅くなる。

使う場合はキャッシュするか、インスペクタであらかじめカメラを設定出来るようにします。

必要のない処理は削除する

当たり前のことですが使う事がない必要のない処理は削除します。

何も書かない事こそが最速です。(´Д`)

相手がカメラに映っていなければ処理をしない

カメラに映る範囲にそのゲームオブジェクトがない場合は処理をしないことで早くなります。

上の例ではインスペクタでカメラに映っているかどうかを判定するゲームオブジェクトを設定します。

gameObjectRenderer.isVisibleでの判定は、どのカメラに映っていなくてもレンダリングが必要な時はtrueとなるので、厳密にはカメラに映っているかどうかではありません。

ゲームオブジェクトがカメラに映っている時は何らかの処理をする。それ以外の時は処理をしない事で早くなります。

ガベージコレクションの問題

ガベージコレクションの問題について見ていきます。

スタックとヒープ

変数は値型(intやfloat等)の場合はスタック領域、それ以外(stringやクラス等)は全てヒープ領域に保存されます。

スタックの場合はスコープ終了時にメモリから解放されますが、ヒープの場合はスコープ終了後もガベージコレクションが実行されるまでメモリから解放されません。

ガベージコレクション

ヒープ領域には長期保存するものや大きなデータ領域やデータの幅が確定されていないものなどが保存されます。

ヒープ領域が埋まっていくと、データとデータの間に使われない領域が出来たり、使わなくなったデータ領域を解放したり、データ領域を拡張したりといったことを行う必要があります。

それを行うのがガベージコレクションという機能です。

ガベージコレクションが行われるとゲーム中に処理が遅くなる可能性があります。

それを回避する為に以下のような事をします。

  • ヒープの割り当てとヒープに割り当てられるオブジェクト参照を減らす
  • パフォーマンスが重要な時にヒープの割り当てと解放の頻度を減らす
  • ガベージコレクションとヒープ領域の拡張のタイミングを調整する
  • キャッシュする

    これは既に前の項目でやりましたが、実行時にコンポーネントを取得するよりも、あらかじめフィールド等にキャッシュを保持しておいてそれを利用します。

    毎フレーム実行している処理内でヒープの割り当てを少なくする

    UpdateやLateUpdateメソッド等の毎フレーム実行する処理内でヒープの割り当てが起きる場合は必要のない時は実行しないようにします。

    これは前の項目でやったように変化が起きた時だけ処理を実行するようにします。

    文字列の連結

    文字列型のstringは値型ではなく参照型なので、文字列を足す処理は既存の文字列型に足すのではなく新たにインスタンスが生成されます。

    なので文字列の連結が多い場合はStringBuilderを使います。

    上の例では1回しか文字列の連結をしていないのでそんなに変わりません。

    Debug.Logを使わない場合は削除する

    Debug.Logメソッドを使うと引数に何も渡していない場合も全てのビルドで実行されます。

    なので使用しない場合は削除するか、デバッグ中のみ実行するようにします。

    プラットフォーム依存コンパイル - Unity マニュアル
    Unity’s Platform Dependent Compilation feature consists of some preprocessor directives that let you partition your scripts to compile and execute a secti...

    更新しないテキストと更新するテキストを分ける

    時間を計測してUIのテキストにその時間を表示する場合に更新する時間以外の何を表すかのタイトルの文字列を連結してUIのテキストに入れる場合があります。

    この場合はタイトルの文字列である「経過時間:」と経過した時間を足して新しい文字列を作成し、それをUIのテキストに入れています。

    毎回タイトルの文字列と計算した時間を足すのでヒープ領域の割り当てが起きます。

    そこで、「経過時間:」という表示するタイトルのUIを別に作り、timerText.textには計算した時間のみを入れるようにします。

    タイトル部分を別にUIとして作ったのでタイトル文字列を毎回連結する必要がなくなります。

    CompareTagを使う

    ゲームオブジェクトの名前を取得するgameObject.nameやゲームオブジェクトに設定されたタグを取得するgameObject.tagは毎回新しい文字列を作成します。

    何回も同じ名前を使用する場合はその名前をキャッシュしておきます。

    タグを比較する場合はgameObject.tagを使用するのではなくCompareTagメソッドを使用します。

    ボクシングを避ける

    intなどの値型をobject型等の参照型の変数に入れる場合にintの値をヒープに保存し、それを参照するという形に変換します。

    これがボクシングという機能で、ヒープを使うのでガベージが発生します。

    ボクシングが発生する状況や、裏でボクシングが発生する状況を避けます。

    コルーチンのyield

    コルーチンを使った時に次のフレームを実行させる為に以下のように記述するとint型の値がボクシングされる為にガベージが発生します。

    次のフレームに単純に飛ばす場合はnullを指定します。

    また一定時間待機させる場合にwhileループ内で毎回WaitForSecondsのインスタンスを生成するとガベージが発生します。

    なのでループ外でキャッシュします。

    Unity5.5より前の配列以外のforeachループ

    Unity5.5より前のバージョンのUnityの配列以外のforeachループだとループが終了する度にボクシングが発生します。

    forで代替します。

    関数の参照

    匿名メソッドや名前付きメソッドも参照型なのでヒープを使います。

    必要がなければ使わない。

    LINQと正規表現

    LINQと正規表現も裏でボクシングが行われる為ヒープを使います。

    必要がなければ使わない。

    構造体が参照型の変数を持つ場合

    構造体は値型ですが、参照型のフィールド等を持つと構造体全体がガベージコレクターのワークフローに追加される可能性があります。

    Dataという構造体では参照型であるstringのnameを持っています。

    それを使用する時にDataの配列型を作るのではなく、Dataの個々のデータ自体を配列として別に持つようにします。

    Animator等のメソッドで参照型ではなく値型で渡す

    AnimatorコンポーネントのSetFloatメソッド等を使ってアニメーションパラメーターを操作することは良くあります。

    上の例ではアニメーションパラメーターのSpeedを操作する時に文字列のSpeedを渡しています。

    そこであらかじめSpeedアニメーターパラメーターの値のハッシュ値を計算し、int型の値にキャッシュして利用します。

    StartメソッドであらかじめアニメーションパラメーターのSpeedのハッシュ値を取得して保持し、AnimatorのSetFloatメソッドでアニメーションパラメーター名を渡していた所にそのハッシュ値を入れたanimSpeedHashを入れるようにします。

    Animatorのメソッド以外でも文字列ではなくintやfloat等で引数を渡せるものはハッシュ値に変換してキャッシュしたものを渡すようにします。

    ガベージコレクションを自分で実行する

    ガベージコレクションがゲームに影響しないタイミングで自分でガベージコレクションを行う事が出来ます。

    物理系の最適化

    Physicsの衝突した相手を全て取得するメソッド系の処理

    Physics.BoxCastAll等のメソッドは指定した範囲に衝突したコライダを検出することができます。

    ただしゴミが出ます。

    なので、Physics.BoxCastNonAlloc等のゴミが発生しない方のメソッドを使用します。

    Physicsの衝突した相手をチェックするメソッドの処理では相手のレイヤーを指定する

    Physics.BoxCast等で指定した範囲と衝突した相手を探す場合はレイヤー指定をした方がやみくもに全てのレイヤーとの衝突を検知するより処理が速いです。

    ゲームオブジェクトにはレイヤーを指定する

    ゲームオブジェクトを作成するとDefaultレイヤーがデフォルトで設定されていますが、このままだとレイヤー判定をする時に必ず判定するレイヤーとされてしまうので、ゲームオブジェクトにはPlayerやGround等のレイヤー設定をしておきます。

    Layer Collision Matrixで衝突しない相手とのチェックを外す

    UnityメニューのEdit→Project Settings→PhysicsのLayer Collision Matrixで衝突しない相手のレイヤーとのチェックを外し無駄な衝突計算をなくします。

    Unityのレイヤー毎に接触判定の設定をする
    UnityのLayer Collision Matrixでレイヤーそれぞれの接触判定の設定をしていきます。

    メッシュコライダではなくプリミティブなコライダを使用する

    メッシュ形状と同じコライダであるメッシュコライダを使用すると衝突の計算が複雑になり時間がかかるので、プリミティブなコライダ(例えばカプセルコライダ)の組み合わせで代用する。

    複数のコライダを組み合わせた場合はコライダ全部の親であるゲームオブジェクトのRigidbodyで制御できる。

    動かないゲームオブジェクトはStaticにする

    動かないゲームオブジェクト(建物等)はインスペクタでStaticにチェックを入れます。

    動かすゲームオブジェクトにはRigidbodyを取り付ける

    動かす事を想定しているゲームオブジェクトにはRigidbodyを取り付けます。

    Rigidbodyが取り付けられていないゲームオブジェクトは静的な(動かない)ゲームオブジェクトととらえられて、動かそうとすると余分な処理が必要になる。

    グラフィック系の最適化

    レンダリングの最適化ではバッチ(同じ設定を共有するゲームオブジェクトの描画をまとめる事)の数とSetPassコール(CPUがレンダリングの設定をGPUに送る事)の数を減らす事で最適化出来ます。

    SetPassはレンダリングされる次のメッシュが前のメッシュからのレンダリング状態の変更がある場合のみ実行されます。

    使用するマテリアルを少なくする

    マテリアルが多くなるとバッチングが多くなるので使用するマテリアルは少なくします。

    テクスチャアトラスを使用する

    個別のテクスチャではなく複数のテクスチャを合わせたテクスチャアトラスを使うとロードが速くなりバッチ処理にも有利になります。

    スプライトアトラスを使用する

    2DやUIのスプライトにはスプライトをまとめたスプライトアトラスを使用すると速くなります。

    スプライトアトラスを作成するにはSprite Atlasを使用すると出来ます。

    UnityメニューのAssets→Create→2D→Sprite Atlas、もしくはAssetsフォルダ内で右クリックからCreate→2D→Sprite Atlasを選択します。

    作成したスプライトアトラスを選択し、インスペクタでまとめたいスプライトを追加し、Pack Previewボタンを押しパックします。

    スプライトアトラスからスプライトを取得して設定するのは以下のようにします。

    spriteAtlasのGetSpriteメソッドに取得したいスプライトの名前を設定するとスプライトが得られるのでそれをImageのスプライトに入れる事で設定出来ます。

    スプライトアトラスにパッキングしたアトラスが丸型だとスプライトを設定した時に他のスプライトの一部が表示されることがあります。

    GPUインスタンシングを使う

    GPUインスタンシングを使用すると同じメッシュの複数の描画を一遍に出来ます。

    マテリアルのシェーダーがGPUインスタンシングをサポートする場合はマテリアルのインスペクタのEnable GPU Instancingが表示されるのでチェックを入れます。

    マテリアルのEnable GPU Instancingにチェックを入れる

    GPUインスタンシングに対応したプラットフォームとAPIはUnityマニュアルを参照してください。

    GPU インスタンシング - Unity マニュアル
    GPU インスタンシングを使うと、少ない ドローコール で、同じメッシュの複数のコピーをいっぺんに描画 (またはレンダリング) できます。 これは、建物、樹木、草などのオブジェクトを描画したり、シーンに繰り返し登場するものを描画する場合に便利です。

    テクスチャのサイズと圧縮をする

    テクスチャが不必要にサイズが大きい場合はテクスチャのMax Sizeを小さくしたり、圧縮をすることで処理が早くなります。

    テクスチャのサイズを小さくし、圧縮する

    上の例ではキャラクターのテクスチャで1.3MB使っていますが、複雑な模様を描いていないのでテクスチャのサイズを減らしてもそれほど見た目が変わりません。

    Max Sizeを小さくしたり圧縮することで容量を減らせます。

    テクスチャ圧縮後

    見た目に影響しない範囲で変更する必要あります。

    モデルのメッシュを圧縮する

    モデルのメッシュを圧縮することでサイズを縮小する事が出来、最適化出来ます。

    モデルのメッシュの圧縮

    圧縮は見た目に影響を与えない範囲でする必要があります。

    メッシュのLODを使用する

    メッシュが詳細に作られていてカメラから遠くにある場合も詳細なメッシュを表示するのは無駄なので、LODを使って遠めにある時は単純な物を表示するようにします。

    UnityのLODを使ってレンダリング性能を向上させる
    UnityのLOD(Level Of Detail)を使ってハードウェアの負荷を軽減させ、レンダリング性能を向上させる

    オクルージョンカリングを行う

    壁等の後ろに隠れているゲームオブジェクトは描画する必要がない為、オクルージョンカリングを行って見えない部分の描画を減らすと処理が速くなります。

    Unityでオクルージョンカリングを使う
    Unityのオクルージョンカリングを使って描画にかかる処理負荷を軽減する方法を学びます。

    ライトをベイクする

    動かないゲームオブジェクトはインスペクタでStaticにチェックを入れ、ライトのベイクをしてライトの効果を焼き付ける事で処理が速くなります。

    UnityのLightingウインドウについて
    UnityのLightingウインドウのグローバルイルミネーション(GI)、や環境光、霧の設定等を見ていきます。

    UIの最適化

    UIの最適化について見ていきます。

    UIを頻繁に表示と非表示を切り替える時はCanvasコンポーネントを無効化する

    UIを非表示にする時はゲームオブジェクトを非アクティブにすることで出来ますが、CanvasゲームオブジェクトのCanvasコンポーネントのオンとオフを切り替える場合はメモリにバッチが常駐します。

    なので頻繁にUIを表示・非表示する場合はCanvasコンポーネントの切り替えをする方がいいです。

    UIの更新するものと更新しないものでCanvasを分ける

    Canvasの子のUI要素が更新する度に再構築が起きるので、更新されないUIと更新されるUIのCanvasを分けるかCanvasの子にCanvasを配置して分けて使用します。

    ただCanvasを分けるとその分バッチの数も増えるのでUIが少ない場合は余計に遅くなる可能性があります。

    UIのCanvasのイベントカメラやレンダリングカメラを設定する

    CanvasのRender ModeをWorld Spaceに設定している場合はイベントカメラ、Screen Space – Cameraに設定している場合はレンダーカメラを設定する項目が現れます(Screen Space – Overlayの場合はありません)。

    これらには該当するカメラを設定しておきます(例えばMain Camera)。

    設定しない場合はGameObject.FindWithTagでMain Cameraを少なくとも1回は検索して実行するのでその分遅くなります。

    オーディオの最適化

    オーディオのLoad Typeを適切に設定します。

    Load TypeはUnityがランタイムに音声を読み込む方法です。

    短いオーディオクリップ(効果音等)の場合はDecompress On Loadを選択します。オーディオファイルが読み込まれるとすぐに展開します。
    通常のオーディオクリップの場合はCompressed In Memoryを選択します。メモリ上に圧縮し再生中に展開します。
    長いオーディオクリップ(BGM等)の場合はStreamingを選択します。データはディスクから段階的に読み込まれて必要に応じてデコードされます。

    終わりに

    最適化する方法は色々ありますね。

    全てが出来るとは思いませんが、頭の片隅にでもあるといざという時に使えるかもしれません。

    わたくしの場合はパソコンのスペックが低いので最適化をしてもパソコンでもまともに動かない場合が多いです。(^_^;)

    ここに載せている最適化の方法の他にもやり方があったり、非同期処理やマルチスレッドを使うことでも処理を速くする方法はあるのでがんばって快適に動作するゲームを作りたいですね。(´Д`)

    参考サイト

    UnityLearn-パフォーマンスの問題修正-

    UnityLearn-UnityUIの最適化-

    UnityLearn-Unityでのグラフィックの最適化-

    【Unite Tokyo 2018】実践的なパフォーマンス分析と最適化

    UnityLearn-パフォーマンスの問題の診断-2019.3-

    パフォーマンス最適化 入門~2021年版~ – Unityステーション

    タイトルとURLをコピーしました