今回はUnityLearnのパフォーマンス問題の修正のページを見てゲームを最適化する時に必要な処理を見ていこうと思います。
最適化例は上記のリンク先を元にしています。
他の参考サイトは記事の最後に記載してありますので、不明な点はそちらを参照してみてください。
わたくしが誤解して記述しているものや、その誤解に準じて作ったサンプルもあるかもしれません。(^_^;)
ご了承ください。(._.)
最適化する前に
最適化の為にむやみやたらと修正するのでは埒があきません。
Unityプロファイルやフレームデバッガーや外部のアプリケーション等を用いてどこで問題が起きているかを把握し、影響が大きい部分に焦点を当てて原因を突き止めて問題を解消していく必要があります。
Unityエディター上で確認することが出来ますが、正確に判断するにはビルド時にDevelopment BuildとAutoconnect Profilerにチェックを入れてビルドすることで実機で動かした時のプロファイルを確認することが出来ます。
詳細はUnityマニュアルを参照してください。
ゲームのFPSを60以上で維持したい場合は60FPSは60フレーム/秒なので60フレーム/1000ミリ秒→16.666・・・フレーム/ミリ秒となり、1フレームで16.666・・・ミリ秒以内に処理が実行されるように修正していきます。
なので、以下のように1フレームで182.45msも時間がかかっている場合は何らかの対処が必要です。
問題の診断の仕方としては
や
が非常に参考になります。
また、実際に重いプロジェクトパッケージをインポートして自分で改善していくことが出来るUnityLearnのコンテンツもあります。
最適化例の処理を施したとしても必ず最適化されるわけではなく、他の部分の影響があって却って遅くなる可能性もあります。
なので、最適化処理をする前のデータと最適化処理をした後のデータを見比べて良くなっているかどうかを判断する必要があります。
最適化をするのは自己責任でお願いいたします。(._.)
最適化例
それでは最適化例を見ていきます。
スクリプト関連の最適化
intやfloatの計算を先にする
intやfloatの数値の計算よりもベクトルやマトリックス、クォータニオンを計算する方が処理に時間がかかります。
1 2 3 4 | float speed = 3f; Vector3 velocity = speed * transform.forward * Time.deltaTime; |
とするとspeedとtransform.fowardでfloat × Vector3というベクトルの計算をし、その値とTime.deltaTimeを掛けるのでVector3 × floatのベクトルの計算になるのでベクトルの計算が2回あって時間がかかります。
そこで先にspeed * Time.deltaTimeを計算し、その後にVector3と掛けるようにします。
1 2 3 4 | float speed = 3f; Vector3 velocity = speed * Time.deltaTime * transform.forward; |
こうすることでベクトルの計算が1回になります。
多次元配列よりジャグ配列の方が速い
多次元配列(例えばmultidimensionalArray[ , ])は関数呼び出しを必要とするので、ジャグ配列(例えばjagArray[][])の方が速い
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript26 : MonoBehaviour { // ジャグ配列 private int[][] jagArray = { new int[] {1, 2}, new int[] {3, 4} }; // 多次元配列 private int[,] multidimensionalArray = { {1, 2}, {3, 4} }; // Start is called before the first frame update void Start() { Debug.Log(jagArray[0][1]); Debug.Log(jagArray[1][0]); Debug.Log(multidimensionalArray[0, 1]); Debug.Log(multidimensionalArray[1, 0]); foreach (var items in jagArray) { foreach (var item in items) { Debug.Log(item); } } foreach (var item in multidimensionalArray) { Debug.Log(item); } } } |
ループ内で同じ条件判定をしない
毎フレーム実行されるUpdateメソッド内(毎フレーム実行しなくても)でfor文を使ってループ処理をする時に、ループ内で毎回条件を判定していて無駄に処理を走らせている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript1 : MonoBehaviour { // エリア内にいる private bool inTheArea = true; private string allText; // Update is called once per frame void Update() { for (int i = 0; i < 10000; i++) { if (inTheArea) { allText += i; } } } } |
上の場合はエリア内にいる時にfor文で処理をしたいのですが、for文内でinTheAreaかどうかをループ回数分比較しているので、ループの前に条件で判定する。
1 2 3 4 5 6 7 | if (inTheArea) { for (int i = 0; i < 10000; i++) { allText += i; } } |
ループ内で毎回チェックをするのをやめループ外で1回だけ判定するようにします。
状況が変化した場合のみ実行する
Updateメソッド内でスコアを表示するDisplayScoreメソッドを毎回表示する無駄な処理をしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class OptimizeScript2 : MonoBehaviour { // スコア表示用テキスト private Text scoreText; // スコア private int score; // Update is called once per frame void Update() { DisplayScore(); } public void DisplayScore() { scoreText.text = score.ToString(); } } |
スコアは更新された時だけ表示すればいいので、敵を倒した時等にスコアを設定するメソッドを呼んで、そこでDisplayScoreメソッドを呼べば敵を倒した時だけスコアの更新が出来る。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class OptimizeScript2 : MonoBehaviour { // スコア表示用テキスト private Text scoreText; // スコア private int score; // Update is called once per frame void Update() { } // 敵を倒した public void DefeatedEnemy() { SetScore(1); } public void SetScore(int addScore) { score += addScore; DisplayScore(); } public void DisplayScore() { scoreText.text = score.ToString(); } } |
Updateメソッドで毎回実行しない
毎フレーム実行する必要がない場合は一定フレーム毎に実行する。
Updateメソッドは毎フレーム実行されるので、毎フレーム実行する必要がない場合は間引いて一定のフレーム毎に実行するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript3 : MonoBehaviour { private int interval = 3; private int count1; private int count2; // Update is called once per frame void Update() { // 3フレームに1回実行 if (Time.frameCount % interval == 0) { count1++; Debug.Log(count1); } count2++; Debug.Log(count2); } } |
キャッシュを利用する
Updateメソッドで毎回コンポーネントを取得して利用すると時間がかかる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript4 : MonoBehaviour { // Update is called once per frame void Update() { CharacterController characterController = GetComponent<CharacterController>(); if(characterController.isGrounded) { // キャラクターの移動処理 } } } |
Updateメソッドで毎回GetComponentでコンポーネントを取得するのは処理に時間がかかるので、何度も使用するコンポーネントはあらかじめStartメソッド等でキャッシュをしておく。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript4 : MonoBehaviour { private CharacterController characterController; // Start is called before the first frame update void Start() { characterController = GetComponent<CharacterController>(); } // Update is called once per frame void Update() { if(characterController.isGrounded) { // キャラクターの移動処理 } } } |
オブジェクトをプーリングする
シューティングゲーム等で弾を撃つたびに弾のプレハブからインスタンスを生成すると非効率的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript5 : MonoBehaviour { [SerializeField] private GameObject bulletPrefab; [SerializeField] private float addForcePower = 1000f; // Update is called once per frame void Update() { if(Input.GetKeyDown(KeyCode.Space)) { Shot(); } } private void Shot() { var bulletIns = Instantiate(bulletPrefab, transform.position, Quaternion.identity); var bulletRigidbody = bulletIns.GetComponent<Rigidbody>(); bulletRigidbody.AddForce(bulletRigidbody.mass * addForcePower * Vector3.forward); } } |
あらかじめ弾のインスタンスとそのRigidbodyを取得して非アクティブにして用意しておき、必要になったらアクティブにして再利用する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript5 : MonoBehaviour { [SerializeField] private GameObject bulletPrefab; [SerializeField] private float addForcePower = 1000f; [SerializeField] private int poolingObjectCount = 30; [SerializeField] private GameObject[] poolingObjects; [SerializeField] private Rigidbody[] poolingRigidbody; // Start is called before the first frame update void Start() { poolingObjects = new GameObject[poolingObjectCount]; poolingRigidbody = new Rigidbody[poolingObjectCount]; for (int i = 0; i < poolingObjectCount; i++) { poolingObjects[i] = Instantiate(bulletPrefab, transform.position, Quaternion.identity); poolingObjects[i].SetActive(false); poolingRigidbody[i] = poolingObjects[i].GetComponent<Rigidbody>(); } } // Update is called once per frame void Update() { if(Input.GetKeyDown(KeyCode.Space)) { Shot(); } } private void Shot() { for (int i = 0; i < poolingObjectCount; i++) { // 使っていない弾のプレハブを再利用 if(!poolingObjects[i].activeSelf) { poolingObjects[i].SetActive(true); poolingRigidbody[i].AddForce(poolingRigidbody[i].mass * addForcePower * Vector3.forward); break; } } } } |
上の例では弾を無効化する処理と弾がプールされていない時に新たにインスタンス化する処理は書いていませんが、もし気になる方は以下の記事を参照してください。
Startメソッドで弾のインスタンス化とRigidbodyの取得処理も時間がかかる場合はインスペクタで設定しておく方が速いかもしれません。
UnityAPIの時間がかかる処理へのアクセスを減らす
UnityのAPIへのアクセスに時間がかかることがあります。
SendMessageやBroadCastMessageを使わない
SendMessageやBroadCastMessageは高負荷な処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript6 : MonoBehaviour { // Start is called before the first frame update void Start() { SendMessage("DoSomething"); BroadcastMessage("DoSomething"); } public void DoSomething() { Debug.Log("DoSomething"); } } |
なので、SendMessageを使わず実行したいメソッドを持つゲームオブジェクトのスクリプトを直接指定して実行するか、イベントやデリゲートなどを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript6 : MonoBehaviour { // Start is called before the first frame update void Start() { } public void DoSomething() { Debug.Log("DoSomething"); } private void OnTriggerEnter(Collider other) { if (other.CompareTag("Enemy")) { DoSomething(); } } } |
Findメソッドを使用しない
GameObject.Findメソッドは全てのゲームオブジェクトから指定したゲームオブジェクトを探す便利な処理ですが毎フレーム実行するには重いです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript7 : MonoBehaviour { // Update is called once per frame void Update() { GameObject.Find("MyCharacter/MyObject"); } } |
なので、キャッシュを使用したり、これより処理が速いFindWithTagメソッドでタグから探したり、あらかじめインスペクタで設定出来るようにして探す処理をしないようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript7 : MonoBehaviour { private GameObject obj; // インスペクタで設定出来るようにする [SerializeField] private GameObject obj2; // Start is called before the first frame update void Start() { // キャッシュする obj = GameObject.Find("MyCharacter/MyObject"); // タグで検索しキャッシュ obj = GameObject.FindWithTag("MyObjectTag"); } // Update is called once per frame void Update() { } } |
Transformの位置と回転の更新
transform.positionを更新すると内部のOnTransformChangedイベントがその子に全て送られ更新するので多くの子を持っている場合は処理が多くなります。
なので、以下のようにtransform.positionの値を何度も更新するとそれだけ処理に時間がかかります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript8 : MonoBehaviour { void Update() { // 何度も位置を更新する transform.position += new Vector3(1f, 0f, 0f); transform.position += new Vector3(0f, 0f, 1f); } } |
transform.positionの位置を更新する前に移動先の計算をすませて、最後にtransform.positionの値に入れたり、OnTransformChangedイベントが発生しないtransform.localPositionを使うようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript8 : MonoBehaviour { void Update() { // 位置を更新する前に移動する位置を計算しておく Vector3 newPosition = new Vector3(1f, 0f, 0f) + new Vector3(0f, 0f, 1f); transform.position += newPosition; // transform.positionではなくtransform.localPositionを使う transform.localPosition += newPosition; } } |
空のUpdateメソッド呼び出し
スクリプトを作成するとデフォルトでStartメソッドとUpdateメソッドが作成されていますが、UpdateやLateUpdate、イベントハンドラなどは隠れたオーバーヘッドがあります。
なので使用していないUpdateメソッド等を持つスクリプトが膨大になると影響を及ぼすかもしれません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript9 : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } private void OnTriggerEnter(Collider other) { if(other.CompareTag("Enemy")) { // 敵が侵入したよ } } } |
使用していないUpdateやLateUpdate、イベントハンドラ等のメソッドは削除しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript9 : MonoBehaviour { private void OnTriggerEnter(Collider other) { if(other.CompareTag("Enemy")) { // 敵が侵入したよ } } } |
もしくはUpdateメソッド等を使わずに更新を管理するスクリプトを作成し、そこから定期的に処理が必要なゲームオブジェクトに対してだけなんらかの処理を実行させるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript9 : MonoBehaviour { // 処理を実行させるゲームオブジェクトに取り付けられたスクリプト [SerializeField] private MessageTestScript[] myScript; // Update is called once per frame void Update() { // 指定したゲームオブジェクトのDisplayLogメソッドを毎フレーム実行する foreach (var script in myScript) { script.DisplayLog(); } } } |
毎フレーム実行したいゲームオブジェクトに取り付けられたスクリプトを登録しておき、管理しているスクリプト(この場合は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より計算が早く比較にも使えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript10 : MonoBehaviour { // 指定距離 private float specifiedDistance = 1f; // 相手のTransform [SerializeField] private Transform partner; // Update is called once per frame void Update() { // 指定距離内かどうか var distance1 = (transform.position - partner.position).magnitude; Debug.Log(distance1); if (distance1 < specifiedDistance) { Debug.Log("接近した"); } // 指定距離内かどうか var distance2 = (transform.position - partner.position).sqrMagnitude; Debug.Log(distance2); if (distance2 < specifiedDistance * specifiedDistance) { Debug.Log("接近した"); } } } |
Camera.mainをそのまま使わない
Camera.mainは内部でFindメソッドと同様の処理が走るので処理が遅くなる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript11 : MonoBehaviour { // Update is called once per frame void Update() { // 直接使うと遅くなる var ray = Camera.main.ScreenPointToRay(Input.mousePosition); Debug.Log(ray.direction); } } |
使う場合はキャッシュするか、インスペクタであらかじめカメラを設定出来るようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript11 : MonoBehaviour { private Camera mainCamera1; // インスペクタで設定出来るようにする [SerializeField] private Camera mainCamera2; // Start is called before the first frame update void Start() { mainCamera1 = Camera.main; } // Update is called once per frame void Update() { // キャッシュしたカメラを使う var ray2 = mainCamera1.ScreenPointToRay(Input.mousePosition); Debug.Log(ray2.direction); } } |
必要のない処理は削除する
当たり前のことですが使う事がない必要のない処理は削除します。
何も書かない事こそが最速です。(´Д`)
相手がカメラに映っていなければ処理をしない
カメラに映る範囲にそのゲームオブジェクトがない場合は処理をしないことで早くなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript12 : MonoBehaviour { // ゲームオブジェクトのレンダラー [SerializeField] private Renderer gameObjectRenderer; // Update is called once per frame void Update() { if(gameObjectRenderer.isVisible) { Debug.Log("見えてる"); } } } |
上の例ではインスペクタでカメラに映っているかどうかを判定するゲームオブジェクトを設定します。
gameObjectRenderer.isVisibleでの判定は、どのカメラに映っていなくてもレンダリングが必要な時はtrueとなるので、厳密にはカメラに映っているかどうかではありません。
ゲームオブジェクトがカメラに映っている時は何らかの処理をする。それ以外の時は処理をしない事で早くなります。
ガベージコレクションの問題
ガベージコレクションの問題について見ていきます。
スタックとヒープ
変数は値型(intやfloat等)の場合はスタック領域、それ以外(stringやクラス等)は全てヒープ領域に保存されます。
スタックの場合はスコープ終了時にメモリから解放されますが、ヒープの場合はスコープ終了後もガベージコレクションが実行されるまでメモリから解放されません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript13 : MonoBehaviour { // Start is called before the first frame update void Start() { // 値型 int intValue = 3; // 参照型 CharacterController characterController = GetComponent<CharacterController>(); GameObject obj = GameObject.Find("MyObj"); } } |
ガベージコレクション
ヒープ領域には長期保存するものや大きなデータ領域やデータの幅が確定されていないものなどが保存されます。
ヒープ領域が埋まっていくと、データとデータの間に使われない領域が出来たり、使わなくなったデータ領域を解放したり、データ領域を拡張したりといったことを行う必要があります。
それを行うのがガベージコレクションという機能です。
ガベージコレクションが行われるとゲーム中に処理が遅くなる可能性があります。
それを回避する為に以下のような事をします。
キャッシュする
これは既に前の項目でやりましたが、実行時にコンポーネントを取得するよりも、あらかじめフィールド等にキャッシュを保持しておいてそれを利用します。
毎フレーム実行している処理内でヒープの割り当てを少なくする
UpdateやLateUpdateメソッド等の毎フレーム実行する処理内でヒープの割り当てが起きる場合は必要のない時は実行しないようにします。
これは前の項目でやったように変化が起きた時だけ処理を実行するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript14 : MonoBehaviour { [SerializeField] private float walkSpeed = 4f; // 前の位置 private float prePositionX; // 新しい位置 private float newPosX; // 入力値 private float input; // Update is called once per frame void Update() { input = Input.GetAxis("Horizontal"); float transformPositionX = transform.position.x; newPosX = transformPositionX + input * walkSpeed * Time.deltaTime; if (newPosX != prePositionX) { Debug.Log("位置が変わった"); DoSomething(newPosX); prePositionX = newPosX; } } void DoSomething(float x) { // ガベージが行われる何らかの処理 } } |
文字列の連結
文字列型のstringは値型ではなく参照型なので、文字列を足す処理は既存の文字列型に足すのではなく新たにインスタンスが生成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript16 : MonoBehaviour { // Start is called before the first frame update void Start() { string text1 = "1"; // 新たにインスタンスが生成されたものがtext2に入る string text2 = text1 + "23"; } } |
なので文字列の連結が多い場合はStringBuilderを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; public class OptimizeScript16 : MonoBehaviour { // Start is called before the first frame update void Start() { string text1 = "1"; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(text1); stringBuilder.Append("23"); string text2 = stringBuilder.ToString(); Debug.Log(text2); } } |
上の例では1回しか文字列の連結をしていないのでそんなに変わりません。
Debug.Logを使わない場合は削除する
Debug.Logメソッドを使うと引数に何も渡していない場合も全てのビルドで実行されます。
なので使用しない場合は削除するか、デバッグ中のみ実行するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; public class OptimizeScript16 : MonoBehaviour { // Start is called before the first frame update void Start() { string text1 = "1"; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(text1); stringBuilder.Append("23"); string text2 = stringBuilder.ToString(); #if UNITY_EDITOR Debug.Log(text2); #endif } } |
更新しないテキストと更新するテキストを分ける
時間を計測してUIのテキストにその時間を表示する場合に更新する時間以外の何を表すかのタイトルの文字列を連結してUIのテキストに入れる場合があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class OptimizeScript17 : MonoBehaviour { [SerializeField] private Text timerText; private float startTime; // Start is called before the first frame update void Start() { startTime = Time.realtimeSinceStartup; } // Update is called once per frame void Update() { timerText.text = "経過時間: " + (Time.realtimeSinceStartup - startTime); } } |
この場合はタイトルの文字列である「経過時間:」と経過した時間を足して新しい文字列を作成し、それをUIのテキストに入れています。
毎回タイトルの文字列と計算した時間を足すのでヒープ領域の割り当てが起きます。
そこで、「経過時間:」という表示するタイトルのUIを別に作り、timerText.textには計算した時間のみを入れるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class OptimizeScript17 : MonoBehaviour { [SerializeField] private Text timerTextTitle; [SerializeField] private Text timerText; private float startTime; // Start is called before the first frame update void Start() { startTime = Time.realtimeSinceStartup; } // Update is called once per frame void Update() { timerText.text = (Time.realtimeSinceStartup - startTime).ToString(); } } |
タイトル部分を別にUIとして作ったのでタイトル文字列を毎回連結する必要がなくなります。
CompareTagを使う
ゲームオブジェクトの名前を取得するgameObject.nameやゲームオブジェクトに設定されたタグを取得するgameObject.tagは毎回新しい文字列を作成します。
何回も同じ名前を使用する場合はその名前をキャッシュしておきます。
1 2 3 | string objName = gameObject.name; |
タグを比較する場合はgameObject.tagを使用するのではなくCompareTagメソッドを使用します。
1 2 3 4 5 | if(gameObject.CompareTag("Player")) { } |
ボクシングを避ける
intなどの値型をobject型等の参照型の変数に入れる場合にintの値をヒープに保存し、それを参照するという形に変換します。
これがボクシングという機能で、ヒープを使うのでガベージが発生します。
1 2 3 4 | int intValue = 1; object obj = intValue; |
ボクシングが発生する状況や、裏でボクシングが発生する状況を避けます。
コルーチンのyield
コルーチンを使った時に次のフレームを実行させる為に以下のように記述するとint型の値がボクシングされる為にガベージが発生します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript19 : MonoBehaviour { // Start is called before the first frame update void Start() { StartCoroutine(DoCoroutine()); } IEnumerator DoCoroutine() { while(true) { Debug.Log("DoCoroutine"); yield return 0; } } } |
次のフレームに単純に飛ばす場合はnullを指定します。
1 2 3 | yield return null; |
また一定時間待機させる場合にwhileループ内で毎回WaitForSecondsのインスタンスを生成するとガベージが発生します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript19 : MonoBehaviour { // Start is called before the first frame update void Start() { StartCoroutine(DoCoroutine()); } IEnumerator DoCoroutine() { while (true) { yield return new WaitForSeconds(3f); Debug.Log("3秒後"); } } } |
なのでループ外でキャッシュします。
1 2 3 4 5 6 7 8 9 10 | IEnumerator DoCoroutine() { var waitTime = new WaitForSeconds(3f); while (true) { yield return waitTime; Debug.Log("3秒後"); } } |
Unity5.5より前の配列以外のforeachループ
Unity5.5より前のバージョンのUnityの配列以外のforeachループだとループが終了する度にボクシングが発生します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript20 : MonoBehaviour { // Start is called before the first frame update void Start() { var list = new List<int> {0, 1, 2, 3, 4, }; Loop(list); } public void Loop(List<int> list) { foreach (var item in list) { Show(item); } } public void Show(int num) { Debug.Log(num); } } |
forで代替します。
1 2 3 4 5 6 | for (int i = 0; i < list.Count; i++) { int intValue = list[i]; Debug.Log(intValue); } |
関数の参照
匿名メソッドや名前付きメソッドも参照型なのでヒープを使います。
必要がなければ使わない。
LINQと正規表現
LINQと正規表現も裏でボクシングが行われる為ヒープを使います。
必要がなければ使わない。
構造体が参照型の変数を持つ場合
構造体は値型ですが、参照型のフィールド等を持つと構造体全体がガベージコレクターのワークフローに追加される可能性があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript21 : MonoBehaviour { public struct Data { public string name; public int age; } private Data[] data; } |
Dataという構造体では参照型であるstringのnameを持っています。
それを使用する時にDataの配列型を作るのではなく、Dataの個々のデータ自体を配列として別に持つようにします。
1 2 3 4 5 6 7 8 9 10 11 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript21 : MonoBehaviour { private string[] names; private int[] ages; } |
Animator等のメソッドで参照型ではなく値型で渡す
AnimatorコンポーネントのSetFloatメソッド等を使ってアニメーションパラメーターを操作することは良くあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript22 : MonoBehaviour { private CharacterController characterController; private Animator animator; private Vector3 input; private Vector3 velocity; [SerializeField] private float walkSpeed = 4f; // Start is called before the first frame update void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); } // Update is called once per frame void Update() { if (characterController.isGrounded) { velocity = Vector3.zero; input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); if(input.sqrMagnitude > 0f) { transform.LookAt(transform.position + input); velocity = walkSpeed * transform.forward; animator.SetFloat("Speed", input.normalized.magnitude); } else { animator.SetFloat("Speed", 0f); } } velocity.y += Physics.gravity.y * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } } |
上の例ではアニメーションパラメーターのSpeedを操作する時に文字列のSpeedを渡しています。
そこであらかじめSpeedアニメーターパラメーターの値のハッシュ値を計算し、int型の値にキャッシュして利用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript23 : MonoBehaviour { private CharacterController characterController; private Animator animator; private Vector3 input; private Vector3 velocity; [SerializeField] private float walkSpeed = 4f; private int animSpeedHash; // Start is called before the first frame update void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); animSpeedHash = Animator.StringToHash("Speed"); } // Update is called once per frame void Update() { if (characterController.isGrounded) { velocity = Vector3.zero; input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); if(input.sqrMagnitude > 0f) { transform.LookAt(transform.position + input); velocity = walkSpeed * transform.forward; animator.SetFloat(animSpeedHash, input.normalized.magnitude); } else { animator.SetFloat(animSpeedHash, 0f); } } velocity.y += Physics.gravity.y * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } } |
StartメソッドであらかじめアニメーションパラメーターのSpeedのハッシュ値を取得して保持し、AnimatorのSetFloatメソッドでアニメーションパラメーター名を渡していた所にそのハッシュ値を入れたanimSpeedHashを入れるようにします。
Animatorのメソッド以外でも文字列ではなくintやfloat等で引数を渡せるものはハッシュ値に変換してキャッシュしたものを渡すようにします。
ガベージコレクションを自分で実行する
ガベージコレクションがゲームに影響しないタイミングで自分でガベージコレクションを行う事が出来ます。
1 2 3 | System.GC.Collect(); |
物理系の最適化
Physicsの衝突した相手を全て取得するメソッド系の処理
Physics.BoxCastAll等のメソッドは指定した範囲に衝突したコライダを検出することができます。
ただしゴミが出ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript24 : MonoBehaviour { // Update is called once per frame void Update() { // ボックスを作りGroundレイヤーと衝突した相手の情報を保持する var raycastHits = Physics.BoxCastAll(transform.position, Vector3.one * 0.5f, Vector3.forward, Quaternion.identity, 1f, LayerMask.GetMask("Ground")); foreach (var raycast in raycastHits) { if (raycast.collider != null) { Debug.Log(raycast.collider.name); } } } private void OnDrawGizmos() { // ボックスの範囲を表示 Gizmos.DrawWireCube(transform.position, Vector3.one); Gizmos.DrawWireCube(transform.position + Vector3.forward, Vector3.one); } } |
なので、Physics.BoxCastNonAlloc等のゴミが発生しない方のメソッドを使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript24 : MonoBehaviour { // Update is called once per frame void Update() { RaycastHit[] raycastHits = new RaycastHit[10]; Physics.BoxCastNonAlloc(transform.position, Vector3.one * 0.5f, Vector3.forward, raycastHits, Quaternion.identity, 1f, LayerMask.GetMask("Ground")); foreach (var raycast in raycastHits) { if (raycast.collider != null) { Debug.Log(raycast.collider.name); } } } private void OnDrawGizmos() { // ボックスの範囲を表示 Gizmos.DrawWireCube(transform.position, Vector3.one); Gizmos.DrawWireCube(transform.position + Vector3.forward, Vector3.one); } } |
Physicsの衝突した相手をチェックするメソッドの処理では相手のレイヤーを指定する
Physics.BoxCast等で指定した範囲と衝突した相手を探す場合はレイヤー指定をした方がやみくもに全てのレイヤーとの衝突を検知するより処理が速いです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript25 : MonoBehaviour { // Update is called once per frame void Update() { // レイヤーを指定していないので全てのレイヤーを対象とする if(Physics.CheckSphere(transform.position, 3f)) { // なんらかの処理 } // Groundレイヤーのみを対象とする if(Physics.CheckSphere(transform.position, 3f, LayerMask.GetMask("Ground"))) { // なんらかの処理 } } } |
ゲームオブジェクトにはレイヤーを指定する
ゲームオブジェクトを作成するとDefaultレイヤーがデフォルトで設定されていますが、このままだとレイヤー判定をする時に必ず判定するレイヤーとされてしまうので、ゲームオブジェクトにはPlayerやGround等のレイヤー設定をしておきます。
Layer Collision Matrixで衝突しない相手とのチェックを外す
UnityメニューのEdit→Project Settings→Physicsの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ボタンを押しパックします。
スプライトアトラスからスプライトを取得して設定するのは以下のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.U2D; using UnityEngine.UI; public class OptimizeScript28 : MonoBehaviour { [SerializeField] private SpriteAtlas spriteAtlas; private string spriteName1 = "ButtonAcceleratorOverSprite"; private string spriteName2 = "ButtonArrowOverSprite"; private Image image; // Start is called before the first frame update void Start() { image = GetComponent<Image>(); image.sprite = spriteAtlas.GetSprite(spriteName1); } } |
spriteAtlasのGetSpriteメソッドに取得したいスプライトの名前を設定するとスプライトが得られるのでそれをImageのスプライトに入れる事で設定出来ます。
スプライトアトラスにパッキングしたアトラスが丸型だとスプライトを設定した時に他のスプライトの一部が表示されることがあります。
GPUインスタンシングを使う
GPUインスタンシングを使用すると同じメッシュの複数の描画を一遍に出来ます。
マテリアルのシェーダーがGPUインスタンシングをサポートする場合はマテリアルのインスペクタのEnable GPU Instancingが表示されるのでチェックを入れます。
GPUインスタンシングに対応したプラットフォームとAPIはUnityマニュアルを参照してください。
テクスチャのサイズと圧縮をする
テクスチャが不必要にサイズが大きい場合はテクスチャのMax Sizeを小さくしたり、圧縮をすることで処理が早くなります。
上の例ではキャラクターのテクスチャで1.3MB使っていますが、複雑な模様を描いていないのでテクスチャのサイズを減らしてもそれほど見た目が変わりません。
Max Sizeを小さくしたり圧縮することで容量を減らせます。
見た目に影響しない範囲で変更する必要あります。
モデルのメッシュを圧縮する
モデルのメッシュを圧縮することでサイズを縮小する事が出来、最適化出来ます。
圧縮は見た目に影響を与えない範囲でする必要があります。
メッシュのLODを使用する
メッシュが詳細に作られていてカメラから遠くにある場合も詳細なメッシュを表示するのは無駄なので、LODを使って遠めにある時は単純な物を表示するようにします。
オクルージョンカリングを行う
壁等の後ろに隠れているゲームオブジェクトは描画する必要がない為、オクルージョンカリングを行って見えない部分の描画を減らすと処理が速くなります。
ライトをベイクする
動かないゲームオブジェクトはインスペクタでStaticにチェックを入れ、ライトのベイクをしてライトの効果を焼き付ける事で処理が速くなります。
UIの最適化
UIの最適化について見ていきます。
UIを頻繁に表示と非表示を切り替える時はCanvasコンポーネントを無効化する
UIを非表示にする時はゲームオブジェクトを非アクティブにすることで出来ますが、CanvasゲームオブジェクトのCanvasコンポーネントのオンとオフを切り替える場合はメモリにバッチが常駐します。
なので頻繁にUIを表示・非表示する場合はCanvasコンポーネントの切り替えをする方がいいです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class OptimizeScript27 : MonoBehaviour { [SerializeField] private GameObject canvasGameObject; private Canvas canvas; // Start is called before the first frame update void Start() { canvas = canvasGameObject.GetComponent<Canvas>(); } // Update is called once per frame void Update() { if(Input.GetKeyDown(KeyCode.Space)) { // ゲームオブジェクトの非アクティブにすることも出来る //canvasGameObject.SetActive(!canvasGameObject.activeSelf); // Canvasコンポーネントの切り替え canvas.enabled = !canvas.enabled; } } } |
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を選択します。データはディスクから段階的に読み込まれて必要に応じてデコードされます。
終わりに
最適化する方法は色々ありますね。
全てが出来るとは思いませんが、頭の片隅にでもあるといざという時に使えるかもしれません。
わたくしの場合はパソコンのスペックが低いので最適化をしてもパソコンでもまともに動かない場合が多いです。(^_^;)
ここに載せている最適化の方法の他にもやり方があったり、非同期処理やマルチスレッドを使うことでも処理を速くする方法はあるのでがんばって快適に動作するゲームを作りたいですね。(´Д`)
参考サイト
【Unite Tokyo 2018】実践的なパフォーマンス分析と最適化