今回はUnityで毎フレーム実行している処理の実行回数を減らすということをしてみたいと思います。
毎フレーム実行すると言えばUpdateメソッドですが、Updateメソッドで何らかの処理をする他のメソッドを呼び出すようにすると1秒間に何十回もそのメソッドが実行されます。
呼び出したメソッドの処理が重いものであれば毎フレーム処理を実行するとゲームの処理速度が落ちてしまう事があります。
そんなわけで今回はその実行回数を減らすということをしていきます。
Updateメソッドで何らかのメソッドを呼んでFPSが落ちるのを確認する
FPSを確認する為にシーン上にテキストUIを設置しTextにAssets/StandardAssets/Utility/FPSCounterを取り付けて確認出来るようにしておきます。
まずはUpdateメソッドで処理が重い別のメソッドを毎回呼び出すようにし、FPSを確認してみます。
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.Collections; using System.Collections.Generic; using UnityEngine; public class DoSomethingScript : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { DoSomething(); } void DoSomething() { string concatenatedString = ""; for (int i = 0; i < 5000; i++) { concatenatedString += i.ToString(); } } } |
上のようなスクリプトを作成し、UpdateメソッドでDoSomethingメソッドを毎回呼び出すようにしました。
DoSomethingメソッドでは空の文字列に0~4999までの数値を文字列に変換し、concatenatedStringという変数に足す処理をしています。
012345・・・・
という文字列が作られるという何ら意味のない重い処理です。
パソコンによっては、この5000の数値だとUnityまたはパソコンがフリーズする可能性もあるので低い値を設定してください。
Updateメソッドが呼ばれる度にDoSomethingメソッドが実行される為FPS(1秒間に実行されるフレーム数)が非常に落ちます。
こういった重い処理を毎フレーム実行するとゲームの処理落ちが発生するので、マイフレーム実行する必要がない時はこの実行回数を少し減らしたいところです。
実行してみると以下のようにFPSが極端に落ちてしまいます。
一定時間毎にメソッドを実行する
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 27 28 29 30 31 32 33 34 35 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class DoSomethingScript : MonoBehaviour { private float elapsedTime; [SerializeField] private float timeToDoSomething = 1f; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { elapsedTime += Time.deltaTime; if(elapsedTime >= timeToDoSomething) { elapsedTime = 0f; DoSomething(); } } void DoSomething() { string concatenatedString = ""; for (int i = 0; i < 5000; i++) { concatenatedString += i.ToString(); } } } |
上のようにマイフレームTime.deltaTimeをelapsedTimeに足していき、timeToDoSomethingの時間以上になったらDoSomethingメソッドを実行するようにします。
elapsedTimeは0に初期化するので、またtimeToDoSomethingの時間以上になるまでDoSomethingは実行しません。
コルーチンを使って一定時間後にメソッドを実行
次にコルーチンを使って一定時間後にメソッドを実行するやり方です。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class DoSomethingScript : MonoBehaviour { [SerializeField] private float timeToDoSomething = 1f; // Start is called before the first frame update void Start() { StartCoroutine("DoSomething"); } // Update is called once per frame void Update() { } IEnumerator DoSomething() { while (true) { string concatenatedString = ""; for (int i = 0; i < 5000; i++) { concatenatedString += i.ToString(); } yield return new WaitForSeconds(timeToDoSomething); } } } |
上のようにtomeToDoSomethingで指定した秒数が経過したらStartCoroutineを使ってDoSomethingメソッドを実行するようにします。
DoSomethingメソッド内ではwhileを使って無限ループで処理を実行した後にyield return new WaitForSecondsを使ってtimeToDoSomething秒待機した後再び処理を実行しています。
これらを実行してみます。
timeToDoSomethingを1にして実行すると1秒毎にDoSomethingメソッドが実行され、その時だけFPSが極端に下がります。
敵をサーチする機能を別のやり方で作成する
以前、敵が主人公を検知したら追いかけるようにする機能を作成しました。
上の記事では敵の子要素にSearchAreaという空のゲームオブジェクトを作成し、それにSphereColliderを取り付けて主人公を検知するようにしています。
主人公を検知する時にOnTriggerStayを使っていたとしたら毎フレーム何らかのコライダが範囲内にいるかどうかを判定します。
毎フレーム確認するので処理が重くなりますね。
また、別の記事でRigidbodyを持たないコライダで敵を検知する方法だと処理が重いというコメントを頂きましたので、それについて書いていきます。
静的コライダだと処理が重くなる?
敵をサーチする機能をあらかじめ取り付けたコライダだけで判断すると処理が遅くなる可能性があります。
これはRigidbodyを持たないコライダ(静的コライダ)は本来動く事を想定せず最適化されている為で、動かすとその分余計な処理が必要になる為です。
UnityメニューのWindow→Analysis→Profilerで動作を確認してみます。
敵を100体ほど登場させると
上のように敵の数のSearchArea分のStatic Colliderが判定され、その分余計な処理が増えています。
ゲームオブジェクトを動かす事を前提としている場合は
Rigidbody+IsKinematicをfalse(物理的な作用をする)
Rigidbody+IsKinematicをtrue(物理的な作用をしない)
にしておくと良いようです。
なのでSearchAreaを敵の検知範囲として使用する場合はRigidbodyコンポーネントを取り付けた上でIsKinematicにチェックを入れるといいかも?
(ただRigidbody自体が重い処理のようで、逆に付けない方が早いこともあるかもしれません)。
ここら辺はUnityマニュアルを参照してください。
サーチエリアにコライダを使うよりも一定時間ごとにサーチする相手が指定した範囲内にいるかどうかを判定した方が処理が早くなる可能性があります。
なので別の方法でキャラクターをサーチする方法を作成してみます。
自前で時間を計測しキャラクターをサーチする方法
まずは自前で時間を計測する方法です。
敵のゲームオブジェクトに取り付けます。
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 SearchCharacter2 : MonoBehaviour { private CheckEnemyZombie checkEnemyZombie; [SerializeField] private float zombieCheckInterval = 0.2f; private float elapsedTime; [SerializeField] private float searchRadius = 3f; // Start is called before the first frame update void Start() { checkEnemyZombie = GetComponent<CheckEnemyZombie>(); } // Update is called once per frame void Update() { elapsedTime += Time.deltaTime; // 一定時間が経過したら if(elapsedTime >= zombieCheckInterval) { elapsedTime = 0f; CheckCharacter(); } } public void CheckCharacter() { var hitCollider = Physics.OverlapSphere(transform.position, searchRadius, LayerMask.GetMask("Player")); if(hitCollider.Length > 0) { checkEnemyZombie.SetState(CheckEnemyZombie.State.Chase, hitCollider[0].transform); } else { checkEnemyZombie.SetState(CheckEnemyZombie.State.Normal); } } } |
CheckEnemyZombieは敵の行動処理をするスクリプトとして作っていて、SearchCharacter2スクリプトと同じ敵のルートのゲームオブジェクトに取り付けられているとします。
zombieCheckIntervalは敵が主人公を検知する間隔時間を設定します。
elapsedTimeは経過時間を入れます。
searchRadiusは検知する球の半径を設定します。
UpdateメソッドでelapsedTimeがzombieCheckIntervalを越えた時にCheckCharacterメソッドを呼び出します。
CheckCharacterメソッドではPhysics.OverlapSphereを使って敵の位置から半径searchRadiusの球の範囲内にいるPlayerレイヤーが設定された全てのコライダを取得します。
取得したコライダがひとつでもあれば敵の状態とコライダの配列の0番目の敵のTransformを引数として渡し、敵のSetStateメソッドを呼び出して状態を追いかける状態にします。
範囲内のPlayerレイヤーを持つゲームオブジェクトのコライダを全て取得する事が出来ますが、追いかけるのは一体だけなので配列の0番目のTransformを渡してそのゲームオブジェクトを追いかけるようにしています。
検知した全てのPlayerレイヤーを持つゲームオブジェクトに何らかの処理をさせる場合もforeachを使って実行出来そうですね。
コライダがひとつもなければ敵の状態をノーマル状態にします。
敵の移動スクリプトや敵の状態を変更する処理はは他の記事に書いてあるのでブログ右上の検索窓で検索し、参照してみてください。
主人公にしたキャラクターのCharacterController等のコライダを設定したゲームオブジェクトのレイヤーは必ずPlayerにしておきます(でないと検知出来ません)。
Playerレイヤーがない場合は作成し設定します。
コルーチンを使ってキャラクターをサーチする方法
コルーチンを使った方法もやっていることは同じです。
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 SearchCharacter3 : MonoBehaviour { private CheckEnemyZombie checkEnemyZombie; [SerializeField] private float zombieCheckInterval = 0.2f; [SerializeField] private float searchRadius = 3f; // Start is called before the first frame update void Start() { checkEnemyZombie = GetComponent<CheckEnemyZombie>(); StartCoroutine("CheckCharacter"); } public IEnumerator CheckCharacter() { while (true) { var hitCollider = Physics.OverlapSphere(transform.position, searchRadius, LayerMask.GetMask("Player")); if (hitCollider.Length > 0) { checkEnemyZombie.SetState(CheckEnemyZombie.State.Chase, hitCollider[0].transform); } else { checkEnemyZombie.SetState(CheckEnemyZombie.State.Normal); } yield return new WaitForSeconds(zombieCheckInterval); } } } |
処理速度を確認
キャラクターの子要素にサーチエリアを作り、キネマティックなRigidbodyを取り付けたコライダで判定する場合と、コルーチンなどを使って一定時間ごとに範囲内に敵がいるかどうかを判定する方法でどれだけ処理速度が変わるか確認してみました。
上のようになりました。
比較してみると全然処理速度が違いますね(^^)/
今回の場合は一定時間後にPhysics.OverlapSphereを使ってその範囲内にPlayerレイヤーを持つコライダがいるかどうかで判定していますが、単純に探す相手が決まっている場合はその相手との距離を計算し、一定の距離内にいる場合は追いかけるという方法も出来そうですね。