【Unity】GameObjectからのコンポーネント取得方法の速度比較【検証】

どうもこんにちは、ちはるです。

Unityには、GameObjectクラスを使ってScene中のオブジェクトのコンポーネントを取得する方法がいくつかあります。

今回は、その各取得方法ごとに、どれだけ処理に時間がかかるのか検証していきます。

検証準備

検証対象について

今回はUnity*1で次のような環境を用意しました。

検証環境その1

Hierarchy内に、UI要素として

  • Image
  • Text
  • Button

を各10個ずつ配置(各要素が重なっているため、Game画面には1個ずつしか見えてない)しています。

検証環境その2

そして、10個あるButtonのうち1個をTargetButtonとし、その中の子要素であるTextをTargetTextとしています。

今回の検証では、このTargetTextのコンポーネントをあの手この手で取得していこうと思います。

なお、TargetButtonTargetTextには、それぞれ以下のようにオブジェクト名と同じTagを付与してあります。

検証環境その3

検証環境その4

検証する関数について

使用関数1:GameObject#Find()

まず、オブジェクトを取得する方法その1。

これはGameObjectクラスのFind関数を使用して、対象オブジェクト(ここで言うTargetText)を取得し、そのオブジェクトからコンポーネントを取得してやるという方法です。

using UnityEngine;
using UnityEngine.UI;

public class TestScript : MonoBehaviour
{
    private void Start() {

        // 対象オブジェクト名を指定して取得
        GameObject.Find("TargetText").GetComponent<Text>();
    }
}

この関数はアクティブなオブジェクト全てに対し、指定された名前に一致するものを総当たりで探します。

上記のようなStartメソッド内でならまだしも、Updateメソッドなんかで使った日には……、恐ろしいですね。

使用関数2:GameObject#FindWithTag()

続いて検証する関数その2。

この関数はFind関数と異なり、指定したTagが付与されたアクティブオブジェクトを取得します。

using UnityEngine;
using UnityEngine.UI;

public class TestScript : MonoBehaviour
{
    private void Start() {

        // Tagを指定して取得
        GameObject.FindWithTag("TargetText").GetComponent<Text>();
    }
}

Find関数と見た感じはかなり似ていますが、Tagを付与してやるだけで総当たり検索を回避できるようです。

オブジェクト取得後のコンポーネント取得方法は方法1と同じです。

使用関数3a:GameObject#GetComponentInChildren()

つづいてがコチラ。

このGetComponentInChildren関数は、深さ優先探索でゲームオブジェクトの子要素のコンポーネントを取得します。

ここでは、子要素の探索を行う対象のゲームオブジェクト(親要素)を、方法1のFind関数にて取得します。

using UnityEngine;
using UnityEngine.UI;

public class TestScript : MonoBehaviour
{
    private void Start() {

        /**
         * 1.親要素(Button)を取得
         * 2.Buttonに含まれる子要素のTextを取得 */
        GameObject.Find("TargetButton").GetComponentInChildren<Text>();
    }
}

 

使用関数3b:GameObject#GetComponentInChildren()

こちらは、コンポーネントを取得する際に使用する関数はそのままで、親要素となるゲームオブジェクトの取得に、FindWithTag関数を用います。

using UnityEngine;
using UnityEngine.UI;

public class TestScript : MonoBehaviour
{
    private void Start() {

        /**
         * 1.親要素(Button)をTag指定で取得
         * 2.Buttonに含まれる子要素のTextを取得 */
        GameObject.FindWithTag("TargetButton").GetComponentInChildren<Text>();
    }
}
使用関数4:Transform#Find()

最後に紹介する方法は、これまでとはチョット趣の違ったもの。

というのも、まず親要素を取得し、この親要素にアタッチされているTransformから、Transformの子要素を名前指定で取得するという方法です。

using UnityEngine;
using UnityEngine.UI;

public class TestScript : MonoBehaviour
{
    private void Start() {

        /** 
         * 1.親要素(Button)をTag指定で取得
         * 2.親要素のTransformから子要素を取得
         * */
        GameObject.FindGameObjectWithTag("TargetButton").GetComponent<Button>().transform.Find("TargetText").GetComponent<Text>();
    }
}

ちなみに、ここで使用しているFind関数はGameObject#Find()とは別物なので、当然のことながらFindWithTag関数は使えません。

検証方法について

実際に利用するスクリプト

これまで列挙してきた手段を用い、TargetTextのコンポーネントを取得していきます。

実際に実行するソースは以下のもの。

using System.Diagnostics;
using UnityEngine;
using UnityEngine.UI;
using Debug = UnityEngine.Debug;

public class TestFindGameObjectScript : MonoBehaviour
{

    private void Start() {

        Debug.Log("▼▼▼GetObjectFind▼▼▼");
        GetObjectFind();
        Debug.Log("▼▼▼GetObjectFindWithTag▼▼▼");
        GetObjectFindWithTag();
        Debug.Log("▼▼▼GetChildrenObjectFind▼▼▼");
        GetChildrenObjectFind();
        Debug.Log("▼▼▼GetChildrenObjectFindWithTag▼▼▼");
        GetChildrenObjectFindWithTag();
        Debug.Log("▼▼▼GetObjectTransForm▼▼▼");
        GetObjectTransForm();
    }

    private void GetObjectFind() {

        var sw = new Stopwatch();

        sw.Start();

        for (int i = 0; i < 100000; i++) {
            GameObject.Find("TargetText").GetComponent<Text>();
        }

        sw.Stop();
        string str = GameObject.Find("TargetText").GetComponent<Text>().text;
        Debug.Log(str);
        Debug.Log($"1回目:{sw.Elapsed}");

        sw.Restart();

        for (int i = 0; i < 100000; i++) {
            GameObject.Find("TargetText").GetComponent<Text>();
        }

        sw.Stop();
        Debug.Log($"2回目:{sw.Elapsed}");

        sw.Restart();

        for (int i = 0; i < 100000; i++) {
            GameObject.Find("TargetText").GetComponent<Text>();
        }

        sw.Stop();
        Debug.Log($"3回目:{sw.Elapsed}");
    }

    private void GetObjectFindWithTag() {

        var sw = new Stopwatch();

        sw.Start();

        for (int i = 0; i < 100000; i++) {
            GameObject.FindWithTag("TargetText").GetComponent<Text>();
        }

        sw.Stop();
        string str = GameObject.FindWithTag("TargetText").GetComponent<Text>().text;
        Debug.Log(str);
        Debug.Log($"1回目:{sw.Elapsed}");

        sw.Restart();

        for (int i = 0; i < 100000; i++) {
            GameObject.FindWithTag("TargetText").GetComponent<Text>();
        }

        sw.Stop();
        Debug.Log($"2回目:{sw.Elapsed}");

        sw.Restart();

        for (int i = 0; i < 100000; i++) {
            GameObject.FindWithTag("TargetText").GetComponent<Text>();
        }

        sw.Stop();
        Debug.Log($"3回目:{sw.Elapsed}");
    }

    private void GetChildrenObjectFind() {

        var sw = new Stopwatch();

        sw.Start();

        for (int i = 0; i < 100000; i++) {
            GameObject.Find("TargetButton").GetComponentInChildren<Text>();
        }

        sw.Stop();
        string str = GameObject.Find("TargetButton").GetComponentInChildren<Text>().text;
        Debug.Log(str);
        Debug.Log($"1回目:{sw.Elapsed}");

        sw.Restart();

        for (int i = 0; i < 100000; i++) {
            GameObject.Find("TargetButton").GetComponentInChildren<Text>();
        }

        sw.Stop();
        Debug.Log($"2回目:{sw.Elapsed}");

        sw.Restart();

        for (int i = 0; i < 100000; i++) {
            GameObject.Find("TargetButton").GetComponentInChildren<Text>();
        }

        sw.Stop();
        Debug.Log($"3回目:{sw.Elapsed}");
    }

    private void GetChildrenObjectFindWithTag() {

        var sw = new Stopwatch();

        sw.Start();

        for (int i = 0; i < 100000; i++) {
            GameObject.FindWithTag("TargetButton").GetComponentInChildren<Text>();
        }

        sw.Stop();
        string str = GameObject.FindWithTag("TargetButton").GetComponentInChildren<Text>().text;
        Debug.Log(str);
        Debug.Log($"1回目:{sw.Elapsed}");

        sw.Restart();

        for (int i = 0; i < 100000; i++) {
            GameObject.FindWithTag("TargetButton").GetComponentInChildren<Text>();
        }

        sw.Stop();
        Debug.Log($"2回目:{sw.Elapsed}");

        sw.Restart();

        for (int i = 0; i < 100000; i++) {
            GameObject.FindWithTag("TargetButton").GetComponentInChildren<Text>();
        }

        sw.Stop();
        Debug.Log($"3回目:{sw.Elapsed}");
    }

    private void GetObjectTransForm() {

        var sw = new Stopwatch();

        sw.Start();

        for (int i = 0; i < 100000; i++) {
            GameObject.FindGameObjectWithTag("TargetButton").GetComponent<Button>().transform.Find("TargetText").GetComponent<Text>();
        }

        sw.Stop();
        string str = GameObject.FindGameObjectWithTag("TargetButton").GetComponent<Button>().transform.Find("TargetText").GetComponent<Text>().text;
        Debug.Log(str);
        Debug.Log($"1回目:{sw.Elapsed}");

        sw.Restart();

        for (int i = 0; i < 100000; i++) {
            GameObject.FindGameObjectWithTag("TargetButton").GetComponent<Button>().transform.Find("TargetText").GetComponent<Text>();
        }

        sw.Stop();
        Debug.Log($"2回目:{sw.Elapsed}");

        sw.Restart();

        for (int i = 0; i < 100000; i++) {
            GameObject.FindGameObjectWithTag("TargetButton").GetComponent<Button>().transform.Find("TargetText").GetComponent<Text>();
        }

        sw.Stop();
        Debug.Log($"3回目:{sw.Elapsed}");
    }
}

こいつをMainCameraにアタッチしてやり、シーンを実行した際に処理が走るようにします。

スクリプトの軽い解説

紹介した5パターンのコンポーネント取得処理を、10万回3セット行います。

実際の時間計測にはStopWatchクラスを使用し、各セット毎に処理に要した時間をログ出力します。

また、ちゃんとTargetTextを取得できてるよね?という確認の意味を込めて、StopWatchでの時間計測範囲外で、それぞれコンポーネントのTextデータもログ出力するようにしてみました。

この場合、ログにボタンの文言「OK」が出力されれば、問題なくTargetTextを取得出来ているということになります。

 

 

検証実行結果

結果発表

以下、実行結果の画像。

検証実行結果の画像

ログ全体を見やすくまとめてみると、以下のような結果になりました。

▼▼▼GetObjectFind▼▼▼
OK
1回目:00:00:00.0618017
2回目:00:00:00.0593581
3回目:00:00:00.0615687

▼▼▼GetObjectFindWithTag▼▼▼
OK
1回目:00:00:00.0418070
2回目:00:00:00.0435499
3回目:00:00:00.0441453

▼▼▼GetChildrenObjectFind▼▼▼
OK
1回目:00:00:00.0729913
2回目:00:00:00.0694690
3回目:00:00:00.0693467

▼▼▼GetChildrenObjectFindWithTag▼▼▼
OK
1回目:00:00:00.0540094
2回目:00:00:00.0555428
3回目:00:00:00.0556334

▼▼▼GetObjectTransForm▼▼▼
OK
1回目:00:00:00.0875103
2回目:00:00:00.0783563
3回目:00:00:00.0798122

結果について

今回検証した内だと、GameObject#FindWithTag()を使用するのが最速であることが分かりました。
Tagを指定して決め打ちなので、さもありなんといったところでしょうか。

逆に親要素のオブジェクトを取得後、Transformを参照してどーのこーのやっているTransform#Find()が最も遅い結果となりました。

ちょっと面白いと思ったのが、親要素をFindWithTagで取得してやってから、その子要素をGetComponentInChildren()で取り出す手段が2番目に早いという点でしょうか。
GameObject#FindWithTag()を使えばそりゃ早いけど、Tagの設定数が膨大になりそう……、といった時に、親要素分だけTagを設定し、子要素は親から取り出してやると言った手法が処理速度的にベターと言えそうです。

おわりに

とは言え、ゲーム制作で今回の検証で用いたように、一回一回GameObjectを取得して~コンポーネントを取り出して~、といった方法は用いないかな、と。

  • そのシーン中で一度しか使わないようなオブジェクトに対しては、GameObject#FindWithTag()

で十分でしょうが、一つのオブジェクトを複数回使い回す場合や、親要素と子要素どちらに対しても何かしら処理を行う場合など、場合によって使い分けが重要そうです。

  • 複数回使い回す場合
    1. 親要素をGameObject#FindWithTag()で取得し、メンバ変数に保持
    2. 必要な場所ごとに、GetComponentInChildren()で取得

とか。

オブジェクトのTransformを何かしら弄りたい場合なんかは、むしろTransform#Find()が最適なのかもしれません。
まあ、今適当に考えただけなので分かりませんが()


ということで。

ゲームを制作する段階で、どのようにGameObjectを取得し、コンポーネントを取り出してやれば良いのか非常に気になっていたので、今回は『Unity オブジェクト 取得方法』などでググるとよく目にする代表的(?)な手段について、検証してみました。

これらの手段以外にももっと良い方法があるよ!などなどありましたら、教えて頂けると嬉しいです。

GameObject#GetComponentInParent()はどうしたんだよ!という声は聞こえません

 

では、そんな感じで今回の記事を終えます。
ここまで読んで下さり、ありがとうございました!

 

 

 

 

*1:2019.3.0f6