質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.47%

  • Unity

    5656questions

    Unityは、ユニティテクノロジーが開発したゲームエンジンです。 主にモバイルやブラウザ向けのゲーム製作に利用されていましたが、3Dの重力付きゲームが簡単に作成できることから需要が増え、現在はマルチプラットフォームに対応しています。 言語はC言語/C++で書かれていますが、C#、JavaScript、Booで書かれたコードにも対応しています。

コライダーと階層構造。

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 383

tkmnusr

score 227

 前提・実現したいこと

衝突や検知などでゲームオブジェクトのコライダーを取得したとき、
そのコライダーのゲームオブジェクトが単一のゲームオブジェクトでなく、
キャラクターなどの階層構造でコライダーも階層の中にあり、
その取得した相手のゲームオブジェクトにスクリプトで何かしらの振る舞いをさせたい場合、
どういった設計がよいのかご教示お願い致します。

 試したこと

例えば、キャラクターの手の部分のゲームオブジェクトにコライダーがアタッチしていて、
その手の部分のゲームオブジェクトは、キャラクターのルートオブジェクトでない場合を想定します。

このような場合、キャラクターのゲームオブジェクトの設計と、
その取得した手のゲームオブジェクトから、キャラクターに何かしらの振る舞いをさせる方法としては、
次のようなものがよいのかと考えました。

・キャラクターの振る舞いをするスクリプトはキャラクターのルートオブジェクトにアタッチさせる。

・取得したコライダーの手のゲームオブジェクトからルートのゲームオブジェクトを取得して、
 GetComponentでキャラクターのスクリプトにアクセスし、そのスクリプトで振る舞いを実行する。

このような設計が良いと考えているのですが、いかがでしょうか?
もしくは他にも良い設計がある場合、ご教示お願い致します。

また、GetComponentは負荷がかかるという認識がある為、なるべく避けたいのですが、
コライダーからスクリプトを取得する場合は、GetComponentしか方法がないと思うのですが、いかがでしょうか?

すみません、もう1点質問です。
今回のコライダーに限らず、そしてキャラクターにも限らず、基本的に階層構造のゲームオブジェクトは、
基本的にルートのゲームオブジェクトにスクリプトをアタッチすべきと考えているのですが、いかがでしょうか?

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

+1

最終的には「プロジェクトによる」が結論になります。

コンポーネント志向では、出来るだけ単独クラス内で処理が完結することが原則です。
例えば「手に当たった物を吹き飛ばす(rigidbody.AddForceする)」という処理があった場合、それは「手オブジェクトに付けたスクリプト」で処理すべきだと思います。
(特にColliderの場合は原則として「OnCollisionEnterが呼ばれるのは、そのColliderが付いたGameObjectに付いたスクリプトが対象」なのでこの方針が顕著)

ただこのようにスクリプトを複数オブジェクトに分散させた場合、ヒエラルキー上で見通しが悪くなるのは確かなので、「必要が無ければ1つのオブジェクトに集約する」というのも良いやり方です。

なおその場合、参照さえ取れていれば動くので、必ずしも「キャラクターのルートオブジェクト」である必要もありません。
よくある手法は、シーン直下に「〇〇Manager」という単独オブジェクトを置いておき、そこで管理する方法です。(1つ集約させすぎると逆に見にくくなるので、何事も適度に、ですが)

この辺はインスタンスをどう持つのか?(プレハブにしてるか等)にもよるので、結論としては「プロジェクトによる」なのです。


あるオブジェクトから別オブジェクトのコンポーネントを取得する方法は大きく分けて以下の通り。
なのでGetComponentを使いたくないなら他の方法を検討になります。

  • GetComponent系列のスクリプトで取得して変数に格納
  • publicな変数にインスペクターからドラッグ&ドロップ
  • 別のスクリプトから渡す
  • InstantiateまたはAddComponentしたインスタンスを変数に格納

ただ、GetComponentも最小限に使う分には問題無いです(というか使わないとどうしようもないケースが実際ある)。
AwakeやStartなどで一度だけ呼ぶ他、使用頻度が低いなら「必要になったタイミングの最初の1回だけ取得」等にすることで負荷を減らせます。以下一例。

public class Sample : MonoBehaviour {
    Hoge hoge;

    // 滅多に呼ばれない処理
    public void HogeHoge () {
        if (hoge == null) hoge = GetComponent<Hoge>();
        // 以下、hogeを使った処理
    }
}

コメントを受けて追記。

検知エリア内にプレハブから取得した不特定多数の敵キャラがいる状況でGetComponentを使わない方法の一例。
(GetComponentを使う方法とどっちがいいかはプロジェクトによる。単純にルートのクラス取りたいだけならGetComponentの方が楽チン。一方、今回のように事前にリストを持つ方法の利点は、任意のタイミングで「全ての敵に何かする」等の処理が楽。但し敵が消える場合は下手に組むとnullエラーを引き起こすので要注意。あくまでテスト用の例なので実際に使う場合は適宜手を加えてください)

//敵キャラ生成スクリプト
public class EnemyManager : MonoBehaviour {
    //敵のプレハブの一番親(ルート)にEnemyクラスが付いている想定
    public Enemy prefab;    //プレハブ作ってインスペクタからセットしておく

    //生成した敵のリスト
    public Dictionary<string, Enemy> enemyList;

    //敵キャラ生成
    public void Make () {
        //とりあえずDictionaryの初期化をここでやっているが実際はちゃんと考えるべき
        enemyList = new Dictionary<string, Enemy> ();

        //敵を10体作る。GameObject名を「Enemy_数字」にしておく。そしてDictionaryに入れる。
        //(最初の回答内で挙げた「InstantiateまたはAddComponentしたインスタンスを変数に格納」を使っている)
        for (int i = 0; i < 10; i++) {
            Enemy enemy = Instantiate(prefab);
            enemy.name = "Enemy_"+i;
            enemyList.Add(enemy.name, enemy);
        }
    }
}

//検知エリアスクリプト
public class Area : MonoBehaviour {
    public EnemyManager enemyManager;    //インスペクタからセットしておく

    void OnTriggerEnter (Collider other) {
        //DictionaryにあるかチェックしてからEnemy取得
        if (enemyManager.enemyList.ContainsKey(other.transform.root.name)) {
            Enemy enemy = enemyManager.enemyList[other.transform.root.name];
            //以下、enemyに対する処理
        }

        //「transform.root」はそのオブジェクトのルートオブジェクトを示す。
        //DictionaryのKeyには「Enemyクラスが付いているオブジェクト(つまり衝突対象のルートオブジェクト)の名前」、valueには「Enemyクラス」が入っている。
        //なので上記の方法で「衝突対象のルートオブジェクトに付いているEnemyクラス」が取得出来る。
    }
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/11/30 02:47 編集

    ご回答ありがとうございます。
    すみません、自分の質問の説明が下手で、もしかしたら間違って伝わっているかもしれないので、
    念の為、質問のコライダーについてなのですが、
    手オブジェクトのコライダーで検知するのではなく、検知したものが手オブジェクトのコライダーだったら、その手を持っているキャラクターに何らかの振る舞いをさせたいという状況です。
    例えば、検知エリア内に侵入者の手のコライダーを検知したら、侵入者の行動を変えるとかです。

    >なおその場合、参照さえ取れていれば動くので、必ずしも「キャラクターのルートオブジェクト」
    >である必要もありません。
    >よくある手法は、シーン直下に「〇〇Manager」という単独オブジェクトを置いておき、
    >そこで管理する方法です。
    これは、一般的なオブジェクトの設計としてのお話ですか?(自分が最後に質問した方のご回答ですか?)
    衝突や検知などによって、相手のコライダーを取得した場合は、動的に取得となるので、
    「参照さえ取れていれば」は不可能と捉えてよいですか?

    別コンポーネントを取得する4点の方法のうち、
    >別のスクリプトから渡す
    の方法がわからなかったのですが、どういったコードの書き方になりますか?(他3点はわかりました。)

    必要になったタイミングで取得するコードのご提示ありがとうございます。
    勉強になりました。

    キャンセル

  • 2018/11/30 10:02 編集

    > 例えば、検知エリア内に侵入者の手のコライダーを検知したら、侵入者の行動を変えるとかです。
    なるほど、誤解してました。
    ですがその場合でも、
    ・検知エリアのスクリプトで手Colliderを接触検知→手Colliderの親オブジェクトにあるベースクラスをGetComponent→ベースクラスのメソッドを呼ぶ
    ・手のスクリプトで検知エリアColliderを接触検知→参照取得済みのベースクラスのメソッドを呼ぶ
    のどちらも考えられます。(どっちがよいかは他の要件次第だと思います)

    > シーン直下に「〇〇Manager」という単独オブジェクトを置いておき〜
    一般的なオブジェクトの設計としての話です。

    そしてColliderが絡む場合でも、用途によっては事前に参照を取った方が楽な場合があります。
    例えば「シーン上に必ずプレイヤーは1人、シーンの最初から存在していて削除はされない」という前提で「プレイヤーとエリアが接触したらプレイヤーを停止させる」という実装をする場合、エリア側のスクリプトは以下のように書けます。

    //接触の度に動的に参照を取る必要が無いケースの例
    public class Area : MonoBehaviour {
    public Player player; //今回はインスペクターから設定しておく

    void OnTriggerEnter (Collider other) {
    //今回はタグで判定:衝突対象がPlayerタグを持っていたら
    if (other.tag == "Player") {
    player.Stop(); //プレイヤーを停止させる
    }
    }
    }

    > 別のスクリプトから渡す
    izmktrさんの回答にある「hit.player = player;」の部分が正にそれです。
    親で取得したコンポーネントを子に渡すことで、GetComponent自体は一度で済むようになります。

    キャンセル

  • 2018/12/01 01:55

    ご回答ありがとうございます。
    ご提示いただいたコードのような方法があるのですね。
    これならば確かに、Colliderを取得しつつも、GetComponentを使用せずにスクリプトの呼び出しができますね。斬新でした。勉強になります。

    >・手のスクリプトで検知エリアColliderを接触検知→参照取得済みのベースクラスのメソッドを呼ぶ
    この参照取得済みのベースクラスも、今回ご提示いただいコードのような方法で参照取得済みになるということでしょうか?

    別のスクリプトから渡す方法のご教示ありがとうございます。

    検知エリア内に、今回の条件のようにプレイヤーが1人で限定されていたら、ご提示いただいたように参照を予め設定しておくことができることがわかりましたが、
    一応、確認の為、質問させていただきたいのですが、
    これがもし、検知エリア内に不特定多数の敵キャラ(しかもプレハブから生成したとかだったら)、
    >・検知エリアのスクリプトで手Colliderを接触検知→手Colliderの親オブジェクトにあるベースクラスを>GetComponent→ベースクラスのメソッドを呼ぶ
    のように、GetComponentを使う方法しかないですよね?

    キャンセル

  • 2018/12/01 11:42

    > この参照取得済みのベースクラスも、今回ご提示いただいコードのような方法で参照取得済みになるということでしょうか?
    敢えて確認するということは「参照取得済み」という状態の定義がよく分からないということでしょうか?
    「あるオブジェクトから別オブジェクトのコンポーネントを取得する方法」の2つ目「publicな変数にインスペクターからドラッグ&ドロップ」を使っただけですが?

    > 検知エリア内に不特定多数の敵キャラ
    GetComponentを使わない方法も無くはないです。回答欄に追記しました。

    Unityは「1つのやりたいこと」に対し「複数の解決方法」がある場合が多いです。
    今回の件に限らず、「この方法しか無い」と決め付けるのではなく「他に方法は無いだろうか?」「この方法は使えないだろうか?」と常に考えるといいと思います(もちろん考えて検証した結果「この方法しか無いわ」となる場合もありますが)。

    キャンセル

  • 2018/12/01 14:45

    ご回答ありがとうございます。
    >敢えて確認するということは「参照取得済み」という状態の定義が
    >よく分からないということでしょうか?
    「publicな変数にインスペクターからドラッグ&ドロップ」以外にも方法があるか確認したく、質問致しました(質問の仕方が下手ですみません)。
    しかもよく考えたら、それがGetComponentとインスペクタの参照以外だったら、
    >・別のスクリプトから渡す
    >・InstantiateまたはAddComponentしたインスタンスを変数に格納
    の2点でしたね。失礼しました。

    追記のコードありがとうございます。
    このような方法もあるのですね。目からウロコでした。
    本題と少しずれますが、まず複数のキャラ管理は自分はListを使っていたのですが、Dictionaryを使えば、ゲームオブジェクト名でキャラ管理ができるのですね。
    Dictionaryについて調べましたが、Removeメソッドがあるみたいなので、敵が消える場合は、Removeを使って管理するということですね。
    なるほど、そしてDictionaryでゲームオブジェクト名で管理することと、コライダーからもルートのゲームオブジェクト名が取って来れるので、GetComponentを使わない方法で実装できますね。

    コライダーからはGetComponentするしか主な情報は取って来れないという固定観念で頭が固かったことを自覚しました。
    コライダーから、ゲームオブジェクト名やタグを取ってきて、それを利用すればGetComponentをしなくてもスクリプトを取得できるんですね。
    とは言え、Bongo様がおっしゃったようにGetComponentも悪ではないみたいなので、
    sakura様のおっしゃるようにプロジェクトに合わせて、その時々で適切な使い方をしていきたいと思います。
    今回のご教示で自分の中の設計方法の幅が広がりました。
    これからUnityの開発において色々な方法を模索していきたいと思います。
    ありがとうございました。

    キャンセル

  • 2018/12/01 15:11

    念の為の補足。
    Dictionaryは「好きな型のKey(今回はGameObject名=string型)」と「好きな型のValue(今回はEnemy型)」を持った配列の一種です。
    「ゲームオブジェクト名でキャラ管理ができる」というのは「そういう設計にしたから」なので、それ専門という訳ではありません。
    何となく分かりやすいかなと思って例に使いましたが、クラス(インスタンス)から自身のGameObject名も取れるのでListでも同じような作りに出来るでしょう。
    Dictionaryの仕様(Listと違って順番を持たない、同じKeyがあってはいけない等)や負荷が足を引っ張る場合があるので、単純な「配列」も含めてどれを使うか色々検討・検証が必要ですね、というお話なのでした。

    キャンセル

  • 2018/12/01 15:16

    ご回答ありがとうございます。
    なるほど、クラス(インスタンス)からも自身のGameObject名が取得できましたね。
    やはり、これもどれを使うか色々検討ということですね、勉強になります。
    ありがとうございます。

    キャンセル

+1

GetComponentは重いので、複数回取得するのなら、変数に入れておきましょう、ということでしょう

とりあえず、私なら親のStartで自分自身に処理を移譲してくれるようにかきますかね

public class Hit : MonoBehaviour {
    public Player player;
    void OnTriggerEnter(Collider collision){
        player.TriggerEnter(collision);
    }

    void OnCollisionEnter(Collision collision){
        player.CollisionEnter(collision);
    }

}

親のスタート
void Start(){
    var player = GetComponent<Player>();
    foreach(var c in GetComponentsInChildren<Collider>()){
        var hit = c.gameObject.AddComponent<Hit>() as Hit;
        hit.player = player;
    }
}

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/11/30 03:17

    ご回答ありがとうございます。
    そのような移譲の方法ができるのですね、勉強になりました。
    ちなみにAddComponent<Hit>()にasを付けるとどのような効果になりますか?
    普段、asを使わずAddComponentしていました。

    キャンセル

  • 2018/11/30 10:05

    公式サンプルがそうだったから、いらないんじゃね?って思いながらもそう書いてました 多分いらないです
    https://docs.unity3d.com/ja/2017.4/ScriptReference/GameObject.AddComponent.html

    キャンセル

  • 2018/12/01 02:04

    ご回答ありがとうございます。
    asは不要ということで勉強になりました。
    ありがとうございます。

    キャンセル

+1

GetComponentは排除するべきか...という点が気になりましたので、ご参考までに思いついた手を比較してみました。あくまでも一例ですので、条件が変われば優劣も変わる可能性があります。

下図のように、青球と赤キューブが接触すると、赤キューブが衝突を検出し緑シリンダーのメソッドを実行するという状況を用意しました。

シーン

緑シリンダーのスクリプト

using UnityEngine;
using UnityEngine.EventSystems;

public class MessageResponder : MonoBehaviour, MessageResponder.IHitHandler
{
    public int HitCount;

    public void OnHit()
    {
        this.HitCount++;
    }

    public interface IHitHandler : IEventSystemHandler
    {
        void OnHit();
    }
}

赤キューブのスクリプト

using System;
using UnityEngine;
using UnityEngine.EventSystems;

public class MessageSender : MonoBehaviour
{
    public enum MessageMode
    {
        DirectCall,
        DirectCallGetTagAndCompare,
        DirectCallCompareTag,
        ReferRoot,
        TraverseParent,
        GetComponentInParent,
        SendMessageUpwards,
        ExecuteHierarchy,
        Find,
        FindObjectOfType
    }

    private const int MessageCount = 1024 * 1024;

    public MessageMode Mode;
    public SceneResetter Resetter;
    private MessageResponder responder;

    private void Awake()
    {
        this.responder = GameObject.Find("Player").GetComponent<MessageResponder>();
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (this.Resetter.Reset)
        {
            return;
        }

        Debug.LogFormat("Hit! Mode:{0}", this.Mode.ToString());
        this.responder.HitCount = 0;
        var time = Time.realtimeSinceStartup;
        switch (this.Mode)
        {
            case MessageMode.DirectCall:
                // 衝突相手が誰であろうと、事前に取得したresponderを直接参照する
                // タイム0.12秒

                // 相手がシーンで唯一ならいいですが、色々な相手と当たりうるのなら相手判定が必要になるでしょう
                // 柔軟性に欠けますが、さすがに直接呼び出しだけのことはあって高速です
                for (var i = 0; i < MessageCount; i++)
                {
                    this.responder.OnHit();
                }

                break;
            case MessageMode.DirectCallGetTagAndCompare:
                // 相手が特定のタグを持っているかを調べ、その上で事前に取得したresponderを直接参照する
                // タイム2.6秒

                // タグ比較を一つ入れただけで、速度はだいぶ低下してしまいました
                for (var i = 0; i < MessageCount; i++)
                {
                    if (collision.gameObject.tag == "Player")
                    {
                        this.responder.OnHit();
                    }
                }

                break;
            case MessageMode.DirectCallCompareTag:
                // 相手が特定のタグを持っているかを調べ、その上で事前に取得したresponderを直接参照する
                // 特定のタグを持っているか調べる部分をCompareTagに変更したバージョン
                // タイム1.9秒

                // 少しだけ高速になりました
                // ==による比較は相手のタグをC#側に取り寄せて比較するのに対し、CompareTagは比較対象のタグを
                // ネイティブコード側に送りつけてそちらで比較するらしいですが、その辺の違いが影響するのかもしれません
                for (var i = 0; i < MessageCount; i++)
                {
                    if (collision.gameObject.CompareTag("Player"))
                    {
                        this.responder.OnHit();
                    }
                }

                break;
            case MessageMode.ReferRoot:
                // ルートトランスフォームからMessageResponderの取得を試みる
                // タイム3.1秒

                // ルートをいきなり参照するという都合上、プレイヤーは他のオブジェクトの子であってはならないという制限が生じます
                // MessageResponderの取得のために1回GetComponentを使っており、合計約100万回のGetComponentが行われます
                // 留意事項として、GetComponentの速度はゲームオブジェクトにアタッチされたコンポーネントの数に影響されうるらしいです
                // とはいえ、一つのオブジェクトに数百個のコンポーネントをアタッチするなんてことをしない限りは大丈夫ではないでしょうか
                for (var i = 0; i < MessageCount; i++)
                {
                    var r = collision.transform.root.GetComponent<MessageResponder>();
                    if (r != null)
                    {
                        r.OnHit();
                    }
                }

                break;
            case MessageMode.TraverseParent:
                // 相手からMessageResponderの取得を試みて、失敗したら順次親をたどり、見つかるまで取得を試みる
                // タイム64秒

                // 急に遅くなってしまいました
                // プレイヤーがルートでなくても対応可能ですが、階層が深い場合何度もGetComponentを行わねばならないのが難点かと思います
                // 今回の場合、合計約400万回のGetComponentが行われるはずです
                for (var i = 0; i < MessageCount; i++)
                {
                    var t = collision.transform;
                    var r = t.GetComponent<MessageResponder>();
                    while (r == null)
                    {
                        t = t.parent;
                        if (t == null)
                        {
                            break;
                        }

                        r = t.GetComponent<MessageResponder>();
                    }

                    if (r != null)
                    {
                        r.OnHit();
                    }
                }

                break;
            case MessageMode.GetComponentInParent:
                // 相手、またはその親へと再帰的にMessageResponderの取得を試みる
                // タイム3.0秒

                // TraverseParentと同様の動作になるはずですが、見た目もシンプルでずっと高速です
                // どうやらネイティブコード側で探索処理を行っているようです
                for (var i = 0; i < MessageCount; i++)
                {
                    var r = collision.gameObject.GetComponentInParent<MessageResponder>();
                    if (r != null)
                    {
                        r.OnHit();
                    }
                }

                break;
            case MessageMode.SendMessageUpwards:
                // 相手にOnHitメッセージを送信する
                // SendMessageUpwardsを使っているので、メッセージがヒエラルキーを上って伝達されていきプレイヤーまで到達する
                // タイム4.0秒

                // ヒエラルキーをさかのぼるのは先の二つと同様ですが、メッセージ伝達を行っているという意図が
                // コードから読み取りやすく、見た目もシンプルです
                // また、こちらはヒエラルキー上流のすべてのオブジェクトにメッセージが伝達されるのに対し、先の方法では
                // 最初に見つかったMessageResponderにしかOnHitが発生しないという違いがあります
                for (var i = 0; i < MessageCount; i++)
                {
                    collision.gameObject.SendMessageUpwards("OnHit", SendMessageOptions.DontRequireReceiver);
                }

                break;
            case MessageMode.ExecuteHierarchy:
                // 相手にOnHitメッセージを送信する
                // ExecuteHierarchyを使っているので、メッセージがヒエラルキーを上って伝達されていきプレイヤーまで到達する
                // タイム53秒

                // こちらはSendMessage系メソッドよりも新しいメッセージ伝達機構で、より高機能です
                // メッセージが文字列でなくなったので、プログラマーのタイプミスなどに対して強そうですね
                // 他にも渡せる引数に制限がなかったり便利ですが、C#側で処理している部分が多いためか、今回の対決では
                // SendMessageUpwardsにだいぶ負けてしまいました
                for (var i = 0; i < MessageCount; i++)
                {
                    ExecuteEvents.ExecuteHierarchy<MessageResponder.IHitHandler>(
                        collision.gameObject,
                        null,
                        (responder, _) => responder.OnHit());
                }

                break;
            case MessageMode.Find:
                // シーン内のどこかにあるであろうプレイヤーを探し、GetComponentでMessageResponderを取得する
                // タイム3.0秒

                // あまり実用的な場面が思いつきませんが、Find系メソッドを乱発した場合どうなるかを試してみようと思いました
                // 遅い遅いと言われるFindですが、かなり高速に処理を終えることができました
                // 今回は検索対象がヒエラルキーのルートにいますが、もしヒエラルキーの深い場所にいる場合は速度が変わってくるかもしれません
                // あるいは内部的に検索結果をキャッシュする機構があるのでしょうか(未確認です)?
                for (var i = 0; i < MessageCount; i++)
                {
                    var p = GameObject.Find("Player");
                    if (p == null)
                    {
                        continue;
                    }

                    var r = p.GetComponent<MessageResponder>();
                    if (r != null)
                    {
                        r.OnHit();
                    }
                }

                break;
            case MessageMode.FindObjectOfType:
                // シーン内のどこかにあるであろうMessageResponderを探す
                // タイム30秒

                // Findと似た結果になるはずですが、FindObjectOfTypeに変えるとむしろ遅くなってしまいました
                for (var i = 0; i < MessageCount; i++)
                {
                    var r = FindObjectOfType<MessageResponder>();
                    if (r != null)
                    {
                        r.OnHit();
                    }
                }

                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        time = Time.realtimeSinceStartup - time;
        Debug.LogFormat("Finished! Count:{0} Time:{1}", this.responder.HitCount, time);
        this.Resetter.Reset = true;
    }
}

試行を終えるたびに腕を元に戻すためのスクリプト

using System;
using System.Collections;
using UnityEngine;

public class SceneResetter : MonoBehaviour
{
    public Rigidbody Forearm;
    public Rigidbody Hand;
    public Rigidbody UpperArm;
    public MessageSender Sender;
    public bool Reset;
    private Vector3 forearmPosition;
    private Vector3 handPosition;
    private Vector3 upperArmPosition;

    private IEnumerator Start()
    {
        this.handPosition = this.Hand.position;
        this.forearmPosition = this.Forearm.position;
        this.upperArmPosition = this.UpperArm.position;
        var modeIndex = 0;
        while (true)
        {
            this.UpperArm.isKinematic = true;
            this.Forearm.isKinematic = true;
            this.Hand.isKinematic = true;
            this.UpperArm.MovePosition(this.upperArmPosition);
            this.Forearm.MovePosition(this.forearmPosition);
            this.Hand.MovePosition(this.handPosition);
            if (!Enum.IsDefined(typeof(MessageSender.MessageMode), modeIndex))
            {
                break;
            }
            this.Sender.Mode = (MessageSender.MessageMode)Enum.ToObject(typeof(MessageSender.MessageMode), modeIndex);
            modeIndex++;
            yield return new WaitForSeconds(5.0f);

            this.UpperArm.isKinematic = false;
            this.Forearm.isKinematic = false;
            this.Hand.isKinematic = false;
            yield return new WaitUntil(()=>this.Reset);
            this.Reset = false;
        }
    }
}

実行時間は下記のようになりました。

方法 タイム
衝突相手に関わらず直接呼び出し 0.12秒
タグ比較を行ってから直接呼び出し 2.6秒
CompareTagでタグ比較を行ってから直接呼び出し 1.9秒
相手のルートを取得しGetComponent 3.1秒
親をたどってGetComponent 64秒
GetComponentInParent 3.0秒
SendMessageUpwards 4.0秒
ExecuteHierarchy 53秒
Findでプレイヤーを探しGetComponent 3.0秒
FindObjectOfType 30秒

私もsakura_hanaさんのご意見に同意いたします。明らかに同じオブジェクトが取得されると分かっているのに何度もFindやGetComponentを行うのは非効率的に思いますが、かといってGetComponentを見つけるたびに代替手法に置き換えようとするのはやりすぎな気がします。やりようによってはかえって遅くなってしまうかもしれません。
GetComponentは十分高速に作られているようですので、あんまり毛嫌いせずに使ってしまっていいんじゃないでしょうか。

【Unity】GameObject.Find 系関数の処理速度の検証結果 - コガネブログではFind系メソッド同士の比較を行っており、FindGameObjectWithTagが圧倒的に高速でFindObjectOfTypeは惨敗だったようですね。同じ「Find...」という名前の付いたメソッドでも違いがあって面白いです。
VS GetComponentおじさん - QiitaではGetComponentとDictionary引きを対決させているようです。これも興味深い結果だと思いました。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/12/01 12:32

    詳しいデータありがとうございます! やっぱり検証大事ですね。
    「VS GetComponentおじさん」も面白かったです。まぁDictionary/ListのライバルとなるのはGetComponentではなくFindやFindObjectsOfTypeとかでしょうから……それよりは早い、と思いたい……(未検証)

    キャンセル

  • 2018/12/01 14:06

    ご回答ありがとうございます。
    ご検証ありがとうございます。
    いろいろな方法があるのですね。
    GetComponentInParentとかSendMessageUpwardsとかExecuteHierarchyとか、知らなかった方法がたくさんありました。勉強になります。
    GetComponentが思いのほか速かったということもわかり、必要に応じて使っていきたいと思いました。
    ありがとうございます。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.47%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る

  • Unity

    5656questions

    Unityは、ユニティテクノロジーが開発したゲームエンジンです。 主にモバイルやブラウザ向けのゲーム製作に利用されていましたが、3Dの重力付きゲームが簡単に作成できることから需要が増え、現在はマルチプラットフォームに対応しています。 言語はC言語/C++で書かれていますが、C#、JavaScript、Booで書かれたコードにも対応しています。