前提・実現したいこと
衝突や検知などでゲームオブジェクトのコライダーを取得したとき、
そのコライダーのゲームオブジェクトが単一のゲームオブジェクトでなく、
キャラクターなどの階層構造でコライダーも階層の中にあり、
その取得した相手のゲームオブジェクトにスクリプトで何かしらの振る舞いをさせたい場合、
どういった設計がよいのかご教示お願い致します。
試したこと
例えば、キャラクターの手の部分のゲームオブジェクトにコライダーがアタッチしていて、
その手の部分のゲームオブジェクトは、キャラクターのルートオブジェクトでない場合を想定します。
このような場合、キャラクターのゲームオブジェクトの設計と、
その取得した手のゲームオブジェクトから、キャラクターに何かしらの振る舞いをさせる方法としては、
次のようなものがよいのかと考えました。
・キャラクターの振る舞いをするスクリプトはキャラクターのルートオブジェクトにアタッチさせる。 ・取得したコライダーの手のゲームオブジェクトからルートのゲームオブジェクトを取得して、 GetComponentでキャラクターのスクリプトにアクセスし、そのスクリプトで振る舞いを実行する。
このような設計が良いと考えているのですが、いかがでしょうか?
もしくは他にも良い設計がある場合、ご教示お願い致します。
また、GetComponentは負荷がかかるという認識がある為、なるべく避けたいのですが、
コライダーからスクリプトを取得する場合は、GetComponentしか方法がないと思うのですが、いかがでしょうか?
すみません、もう1点質問です。
今回のコライダーに限らず、そしてキャラクターにも限らず、基本的に階層構造のゲームオブジェクトは、
基本的にルートのゲームオブジェクトにスクリプトをアタッチすべきと考えているのですが、いかがでしょうか?
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答3件
0
ベストアンサー
最終的には**「プロジェクトによる」**が結論になります。
コンポーネント志向では、出来るだけ単独クラス内で処理が完結することが原則です。
例えば「手に当たった物を吹き飛ばす(rigidbody.AddForceする)」という処理があった場合、それは「手オブジェクトに付けたスクリプト」で処理すべきだと思います。
(特にColliderの場合は原則として「OnCollisionEnterが呼ばれるのは、そのColliderが付いたGameObjectに付いたスクリプトが対象」なのでこの方針が顕著)
ただこのようにスクリプトを複数オブジェクトに分散させた場合、ヒエラルキー上で見通しが悪くなるのは確かなので、**「必要が無ければ1つのオブジェクトに集約する」**というのも良いやり方です。
なおその場合、参照さえ取れていれば動くので、必ずしも「キャラクターのルートオブジェクト」である必要もありません。
よくある手法は、シーン直下に「〇〇Manager」という単独オブジェクトを置いておき、そこで管理する方法です。(1つ集約させすぎると逆に見にくくなるので、何事も適度に、ですが)
この辺はインスタンスをどう持つのか?(プレハブにしてるか等)にもよるので、結論としては**「プロジェクトによる」**なのです。
あるオブジェクトから別オブジェクトのコンポーネントを取得する方法は大きく分けて以下の通り。
なのでGetComponentを使いたくないなら他の方法を検討になります。
- GetComponent系列のスクリプトで取得して変数に格納
- publicな変数にインスペクターからドラッグ&ドロップ
- 別のスクリプトから渡す
- InstantiateまたはAddComponentしたインスタンスを変数に格納
ただ、GetComponentも最小限に使う分には問題無いです(というか使わないとどうしようもないケースが実際ある)。
AwakeやStartなどで一度だけ呼ぶ他、使用頻度が低いなら「必要になったタイミングの最初の1回だけ取得」等にすることで負荷を減らせます。以下一例。
C#
1public class Sample : MonoBehaviour { 2 Hoge hoge; 3 4 // 滅多に呼ばれない処理 5 public void HogeHoge () { 6 if (hoge == null) hoge = GetComponent<Hoge>(); 7 // 以下、hogeを使った処理 8 } 9}
コメントを受けて追記。
検知エリア内にプレハブから取得した不特定多数の敵キャラがいる状況でGetComponentを使わない方法の一例。
(GetComponentを使う方法とどっちがいいかはプロジェクトによる。単純にルートのクラス取りたいだけならGetComponentの方が楽チン。一方、今回のように事前にリストを持つ方法の利点は、任意のタイミングで「全ての敵に何かする」等の処理が楽。但し敵が消える場合は下手に組むとnullエラーを引き起こすので要注意。あくまでテスト用の例なので実際に使う場合は適宜手を加えてください)
C#
1//敵キャラ生成スクリプト 2public class EnemyManager : MonoBehaviour { 3 //敵のプレハブの一番親(ルート)にEnemyクラスが付いている想定 4 public Enemy prefab; //プレハブ作ってインスペクタからセットしておく 5 6 //生成した敵のリスト 7 public Dictionary<string, Enemy> enemyList; 8 9 //敵キャラ生成 10 public void Make () { 11 //とりあえずDictionaryの初期化をここでやっているが実際はちゃんと考えるべき 12 enemyList = new Dictionary<string, Enemy> (); 13 14 //敵を10体作る。GameObject名を「Enemy_数字」にしておく。そしてDictionaryに入れる。 15 //(最初の回答内で挙げた「InstantiateまたはAddComponentしたインスタンスを変数に格納」を使っている) 16 for (int i = 0; i < 10; i++) { 17 Enemy enemy = Instantiate(prefab); 18 enemy.name = "Enemy_"+i; 19 enemyList.Add(enemy.name, enemy); 20 } 21 } 22} 23 24//検知エリアスクリプト 25public class Area : MonoBehaviour { 26 public EnemyManager enemyManager; //インスペクタからセットしておく 27 28 void OnTriggerEnter (Collider other) { 29 //DictionaryにあるかチェックしてからEnemy取得 30 if (enemyManager.enemyList.ContainsKey(other.transform.root.name)) { 31 Enemy enemy = enemyManager.enemyList[other.transform.root.name]; 32 //以下、enemyに対する処理 33 } 34 35 //「transform.root」はそのオブジェクトのルートオブジェクトを示す。 36 //DictionaryのKeyには「Enemyクラスが付いているオブジェクト(つまり衝突対象のルートオブジェクト)の名前」、valueには「Enemyクラス」が入っている。 37 //なので上記の方法で「衝突対象のルートオブジェクトに付いているEnemyクラス」が取得出来る。 38 } 39} 40
投稿2018/11/29 02:25
編集2018/12/01 02:31総合スコア11425
0
GetComponentは排除するべきか...という点が気になりましたので、ご参考までに思いついた手を比較してみました。あくまでも一例ですので、条件が変われば優劣も変わる可能性があります。
下図のように、青球と赤キューブが接触すると、赤キューブが衝突を検出し緑シリンダーのメソッドを実行するという状況を用意しました。
緑シリンダーのスクリプト
C#
1using UnityEngine; 2using UnityEngine.EventSystems; 3 4public class MessageResponder : MonoBehaviour, MessageResponder.IHitHandler 5{ 6 public int HitCount; 7 8 public void OnHit() 9 { 10 this.HitCount++; 11 } 12 13 public interface IHitHandler : IEventSystemHandler 14 { 15 void OnHit(); 16 } 17}
赤キューブのスクリプト
C#
1using System; 2using UnityEngine; 3using UnityEngine.EventSystems; 4 5public class MessageSender : MonoBehaviour 6{ 7 public enum MessageMode 8 { 9 DirectCall, 10 DirectCallGetTagAndCompare, 11 DirectCallCompareTag, 12 ReferRoot, 13 TraverseParent, 14 GetComponentInParent, 15 SendMessageUpwards, 16 ExecuteHierarchy, 17 Find, 18 FindObjectOfType 19 } 20 21 private const int MessageCount = 1024 * 1024; 22 23 public MessageMode Mode; 24 public SceneResetter Resetter; 25 private MessageResponder responder; 26 27 private void Awake() 28 { 29 this.responder = GameObject.Find("Player").GetComponent<MessageResponder>(); 30 } 31 32 private void OnCollisionEnter(Collision collision) 33 { 34 if (this.Resetter.Reset) 35 { 36 return; 37 } 38 39 Debug.LogFormat("Hit! Mode:{0}", this.Mode.ToString()); 40 this.responder.HitCount = 0; 41 var time = Time.realtimeSinceStartup; 42 switch (this.Mode) 43 { 44 case MessageMode.DirectCall: 45 // 衝突相手が誰であろうと、事前に取得したresponderを直接参照する 46 // タイム0.12秒 47 48 // 相手がシーンで唯一ならいいですが、色々な相手と当たりうるのなら相手判定が必要になるでしょう 49 // 柔軟性に欠けますが、さすがに直接呼び出しだけのことはあって高速です 50 for (var i = 0; i < MessageCount; i++) 51 { 52 this.responder.OnHit(); 53 } 54 55 break; 56 case MessageMode.DirectCallGetTagAndCompare: 57 // 相手が特定のタグを持っているかを調べ、その上で事前に取得したresponderを直接参照する 58 // タイム2.6秒 59 60 // タグ比較を一つ入れただけで、速度はだいぶ低下してしまいました 61 for (var i = 0; i < MessageCount; i++) 62 { 63 if (collision.gameObject.tag == "Player") 64 { 65 this.responder.OnHit(); 66 } 67 } 68 69 break; 70 case MessageMode.DirectCallCompareTag: 71 // 相手が特定のタグを持っているかを調べ、その上で事前に取得したresponderを直接参照する 72 // 特定のタグを持っているか調べる部分をCompareTagに変更したバージョン 73 // タイム1.9秒 74 75 // 少しだけ高速になりました 76 // ==による比較は相手のタグをC#側に取り寄せて比較するのに対し、CompareTagは比較対象のタグを 77 // ネイティブコード側に送りつけてそちらで比較するらしいですが、その辺の違いが影響するのかもしれません 78 for (var i = 0; i < MessageCount; i++) 79 { 80 if (collision.gameObject.CompareTag("Player")) 81 { 82 this.responder.OnHit(); 83 } 84 } 85 86 break; 87 case MessageMode.ReferRoot: 88 // ルートトランスフォームからMessageResponderの取得を試みる 89 // タイム3.1秒 90 91 // ルートをいきなり参照するという都合上、プレイヤーは他のオブジェクトの子であってはならないという制限が生じます 92 // MessageResponderの取得のために1回GetComponentを使っており、合計約100万回のGetComponentが行われます 93 // 留意事項として、GetComponentの速度はゲームオブジェクトにアタッチされたコンポーネントの数に影響されうるらしいです 94 // とはいえ、一つのオブジェクトに数百個のコンポーネントをアタッチするなんてことをしない限りは大丈夫ではないでしょうか 95 for (var i = 0; i < MessageCount; i++) 96 { 97 var r = collision.transform.root.GetComponent<MessageResponder>(); 98 if (r != null) 99 { 100 r.OnHit(); 101 } 102 } 103 104 break; 105 case MessageMode.TraverseParent: 106 // 相手からMessageResponderの取得を試みて、失敗したら順次親をたどり、見つかるまで取得を試みる 107 // タイム64秒 108 109 // 急に遅くなってしまいました 110 // プレイヤーがルートでなくても対応可能ですが、階層が深い場合何度もGetComponentを行わねばならないのが難点かと思います 111 // 今回の場合、合計約400万回のGetComponentが行われるはずです 112 for (var i = 0; i < MessageCount; i++) 113 { 114 var t = collision.transform; 115 var r = t.GetComponent<MessageResponder>(); 116 while (r == null) 117 { 118 t = t.parent; 119 if (t == null) 120 { 121 break; 122 } 123 124 r = t.GetComponent<MessageResponder>(); 125 } 126 127 if (r != null) 128 { 129 r.OnHit(); 130 } 131 } 132 133 break; 134 case MessageMode.GetComponentInParent: 135 // 相手、またはその親へと再帰的にMessageResponderの取得を試みる 136 // タイム3.0秒 137 138 // TraverseParentと同様の動作になるはずですが、見た目もシンプルでずっと高速です 139 // どうやらネイティブコード側で探索処理を行っているようです 140 for (var i = 0; i < MessageCount; i++) 141 { 142 var r = collision.gameObject.GetComponentInParent<MessageResponder>(); 143 if (r != null) 144 { 145 r.OnHit(); 146 } 147 } 148 149 break; 150 case MessageMode.SendMessageUpwards: 151 // 相手にOnHitメッセージを送信する 152 // SendMessageUpwardsを使っているので、メッセージがヒエラルキーを上って伝達されていきプレイヤーまで到達する 153 // タイム4.0秒 154 155 // ヒエラルキーをさかのぼるのは先の二つと同様ですが、メッセージ伝達を行っているという意図が 156 // コードから読み取りやすく、見た目もシンプルです 157 // また、こちらはヒエラルキー上流のすべてのオブジェクトにメッセージが伝達されるのに対し、先の方法では 158 // 最初に見つかったMessageResponderにしかOnHitが発生しないという違いがあります 159 for (var i = 0; i < MessageCount; i++) 160 { 161 collision.gameObject.SendMessageUpwards("OnHit", SendMessageOptions.DontRequireReceiver); 162 } 163 164 break; 165 case MessageMode.ExecuteHierarchy: 166 // 相手にOnHitメッセージを送信する 167 // ExecuteHierarchyを使っているので、メッセージがヒエラルキーを上って伝達されていきプレイヤーまで到達する 168 // タイム53秒 169 170 // こちらはSendMessage系メソッドよりも新しいメッセージ伝達機構で、より高機能です 171 // メッセージが文字列でなくなったので、プログラマーのタイプミスなどに対して強そうですね 172 // 他にも渡せる引数に制限がなかったり便利ですが、C#側で処理している部分が多いためか、今回の対決では 173 // SendMessageUpwardsにだいぶ負けてしまいました 174 for (var i = 0; i < MessageCount; i++) 175 { 176 ExecuteEvents.ExecuteHierarchy<MessageResponder.IHitHandler>( 177 collision.gameObject, 178 null, 179 (responder, _) => responder.OnHit()); 180 } 181 182 break; 183 case MessageMode.Find: 184 // シーン内のどこかにあるであろうプレイヤーを探し、GetComponentでMessageResponderを取得する 185 // タイム3.0秒 186 187 // あまり実用的な場面が思いつきませんが、Find系メソッドを乱発した場合どうなるかを試してみようと思いました 188 // 遅い遅いと言われるFindですが、かなり高速に処理を終えることができました 189 // 今回は検索対象がヒエラルキーのルートにいますが、もしヒエラルキーの深い場所にいる場合は速度が変わってくるかもしれません 190 // あるいは内部的に検索結果をキャッシュする機構があるのでしょうか(未確認です)? 191 for (var i = 0; i < MessageCount; i++) 192 { 193 var p = GameObject.Find("Player"); 194 if (p == null) 195 { 196 continue; 197 } 198 199 var r = p.GetComponent<MessageResponder>(); 200 if (r != null) 201 { 202 r.OnHit(); 203 } 204 } 205 206 break; 207 case MessageMode.FindObjectOfType: 208 // シーン内のどこかにあるであろうMessageResponderを探す 209 // タイム30秒 210 211 // Findと似た結果になるはずですが、FindObjectOfTypeに変えるとむしろ遅くなってしまいました 212 for (var i = 0; i < MessageCount; i++) 213 { 214 var r = FindObjectOfType<MessageResponder>(); 215 if (r != null) 216 { 217 r.OnHit(); 218 } 219 } 220 221 break; 222 default: 223 throw new ArgumentOutOfRangeException(); 224 } 225 226 time = Time.realtimeSinceStartup - time; 227 Debug.LogFormat("Finished! Count:{0} Time:{1}", this.responder.HitCount, time); 228 this.Resetter.Reset = true; 229 } 230}
試行を終えるたびに腕を元に戻すためのスクリプト
C#
1using System; 2using System.Collections; 3using UnityEngine; 4 5public class SceneResetter : MonoBehaviour 6{ 7 public Rigidbody Forearm; 8 public Rigidbody Hand; 9 public Rigidbody UpperArm; 10 public MessageSender Sender; 11 public bool Reset; 12 private Vector3 forearmPosition; 13 private Vector3 handPosition; 14 private Vector3 upperArmPosition; 15 16 private IEnumerator Start() 17 { 18 this.handPosition = this.Hand.position; 19 this.forearmPosition = this.Forearm.position; 20 this.upperArmPosition = this.UpperArm.position; 21 var modeIndex = 0; 22 while (true) 23 { 24 this.UpperArm.isKinematic = true; 25 this.Forearm.isKinematic = true; 26 this.Hand.isKinematic = true; 27 this.UpperArm.MovePosition(this.upperArmPosition); 28 this.Forearm.MovePosition(this.forearmPosition); 29 this.Hand.MovePosition(this.handPosition); 30 if (!Enum.IsDefined(typeof(MessageSender.MessageMode), modeIndex)) 31 { 32 break; 33 } 34 this.Sender.Mode = (MessageSender.MessageMode)Enum.ToObject(typeof(MessageSender.MessageMode), modeIndex); 35 modeIndex++; 36 yield return new WaitForSeconds(5.0f); 37 38 this.UpperArm.isKinematic = false; 39 this.Forearm.isKinematic = false; 40 this.Hand.isKinematic = false; 41 yield return new WaitUntil(()=>this.Reset); 42 this.Reset = false; 43 } 44 } 45}
実行時間は下記のようになりました。
方法 | タイム |
---|---|
衝突相手に関わらず直接呼び出し | 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 02:51
総合スコア10807
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2018/12/01 05:06
0
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/29 02:26
総合スコア2856
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2018/11/29 18:17
2018/11/30 01:05
退会済みユーザー
2018/11/30 17:04
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2018/11/29 17:48 編集
2018/11/30 01:03 編集
退会済みユーザー
2018/11/30 16:55
2018/12/01 02:42
退会済みユーザー
2018/12/01 05:45
2018/12/01 06:11
退会済みユーザー
2018/12/01 06:16