UnityのC#スクリプトでGameObjectの参照の取得のベストプラクティスを考える
UnityのC#スクリプトでGameObjectの参照を取得する方法はいくつかあります。
とりあえず方法を列挙して、その後ベストプラクティスを考えたいと思います。
筆者はUnityを触るようになってまだ半年たってないレベル感ですので、適宜詳しい方の指摘をお待ちしております。
またゲーム開発ではなく、モバイルアプリ開発のためにUnityを利用しています。
1. Inspectorから指定する
一番楽な方法だと思います。
public class Sample : MonoBehaviour
{
public GameObject AnyObject;
}
↑ここにドラッグアンドドロップで指定したいGameObjectを持ってくるだけです。
基本これでやってるんですが、いくつか問題があると感じています。
- ドラッグアンドドロップした後にScene保存し忘れてると無慈悲なNullreferenceException
- 参照外れてるのがテストではじめてわかる
- スクリプトだけ見てると参照入れてるタイミングがわからない
- どこでも参照できるので、参照関係がスパゲッティ化しやすい
- 数が増えてくるとごちゃつく
- レビューしづらい
あとこれは先程気づいたのですが、EditModeでテストコード書いたときに、この方法だとNullreferenceExceptionが不可避になります。
2. GameObject.Find("name")
2〜7はC#スクリプトで完結させるやり方です。
void Example()
{
// This returns the GameObject named Hand.
hand = GameObject.Find("Hand");
}
GameObject名でScene内(Hierarchy)のGameObjectを検索して、参照を返します。
ヒットしなければnullを返します。
弱点は多くて、以下があげられます。
- 処理が重い
- アクティブなオブジェクトのみが検索対象
- オブジェクト1つしか返さない
- 検索順序は明言されていない
- Prefabで生成したオブジェクトは"name(Clone)"となるので、普通にやると検索対象外
僕は使わないです。
詳しくは公式マニュアルを参照してください。
3. GameObject.Find("path")
Find関数は、パス指定もできます。
void Example()
{
// This returns the GameObject named Hand.
hand = GameObject.Find("/Hand");
}
オブジェクト名よりかはパフォーマンスいいと思っています。
ただUnityのGUI操作で階層いじると、Find関数が空振りしてしまうことになり、変更が多いところで使うとめんどくさいと思います。
「アクティブなオブジェクトのみが検索対象」という制約は同じで、これが結構致命的だと思っています。
僕個人で言うと、何回か使いました。
「これは階層いじらないだろう……」みたいなオブジェクトに対して、でした。
詳しくは公式マニュアルを参照してください。
4. Transform.Find("name")
僕の開発してるものが、オブジェクトのアクティブ/非アクティブを頻繁に切り替える処理が入るので、「アクティブなオブジェクトのみが検索対象」という縛りが結構キツいです。
そこで、Transform
のFind関数を使うという手があります。
これは非アクティブでも取得できます。
公式のサンプルがこちら。
using UnityEngine;
using System.Collections;
public class ExampleClass : MonoBehaviour
{
public GameObject player;
public GameObject gun;
public Transform ammo;
//Invoked when a button is clicked.
public void Example()
{
//Finds and assigns the child of the player named "Gun".
gun = player.transform.Find("Gun").gameObject;
//If the child was found.
if (gun != null)
{
//Find the child named "ammo" of the gameobject "magazine" (magazine is a child of "gun").
ammo = gun.transform.Find("magazine/ammo");
}
else Debug.Log("No child with the name 'Gun' attached to the player");
}
}
player
というGameObjectの子オブジェクトにGunという名前があれば、そのTransformを返します。
Transform
はgameObject
という変数で自分の親オブジェクトを返すので、これでGameObjectが取得できます。
GameObjectのFind関数は検索範囲がHierarchy全体ですが、Transformは自分の子オブジェクトだけが検索範囲になります。
詳しくは公式マニュアルを参照してください。
5. Transform.Find("path")
- とほぼ同じなので割愛します。
4は使ってませんが、5は今見たら一度使ってました。
たぶん絶対に存在するような構成にしてる子オブジェクトにちょっとアクセスしたい、みたいなときに便利でした。
6. GameObject.FindGameObjectWithTag("tagname")
オブジェクト名で検索したとき、オブジェクトが1つしか返ってこないのは前述の通りです。
これだと複数同名のオブジェクトが存在するとき、やりづらいですね。
そういう場合、タグ名でやる方法が用意されています。
ここでは詳述しませんので、気になる方は↑を参照してください。
7. Prefab化して、Instantiate()
GameObjectの参照の仕方、とはちょっと違うんですが、Prefab化するとInstantiate()
で新オブジェクトが生成できます。
"name(Clone)"というGameObjectになります。
Unityを触りはじめた頃はよくわかってなくて、GameObjectがInstantiate()
できないことに混乱したので、一応書いておきます。
ベストプラクティスを考える
必ずしも正解はないと思うんですが、今の僕なりの見解を書きます。
基本的には「1. Inspectorから指定する」で対応して、その他特別な要件があったときに、他の選択肢をとればいいかと思います。
想定外のNullreferenceExceptionが気になるのであれば、スクリプト側のStart()
にNullチェック入れるしかないですかねえ。
2,4のオブジェクト名で指定する、というのは、ゲーム制作ならそういう場面もなくはないかと思うものの、個人的にはちょっと受け入れがたい方法です。
せめてパスにして欲しい、という感覚があります。
個人的にInspectorウィンドウにずらずらオブジェクト名あるのイマイチだと思っていた時期があって、
コードで完結できるならできたほうがいいと思って、一時期3、5が使えるなら使ってたんですが、今はもうどっちでもいいかなと思いはじめています。
6, 7については使ったことがありません。
ゲーム作成だと結構お世話になるんでしょうね。
というわけで、個人的には
1>>3=5>>>>>>4>>2
ぐらいの感覚です。
(追記)EditModeのテスト
UnityのEditModeでテストコードを書くと、Inspectorから指定している参照はnullになってしまいます。
仕方ないので、テストを諦めるか、テストコード側で参照を入れてやる必要があります。
僕がテストしようとしていたスクリプトはデフォルトで非アクティブ状態のGameObjectのComponentとして使っているものでして、GameObject.Find()
は使えません。
Transform.Find()
でやるかあ、と思って動かしてみると、NullreferenceException。
Find()
が空振ったのかなと思って調べたら、そもそもTransform
がnullでした。。。
Scene上で一度もアクティブにならないGameObjectのTransformは、どうやらnullみたいです。
キツいなあ〜と思いながら、結局テストコードの中でテスト対象のスクリプトがくっついてるGameObjectをActiveにする処理を書きました。
たぶん非Active状態のままGameObjectの参照をスクリプトからとる方法はないと思われます。
(そもそもインスタンス化されてない雰囲気がしますね)
Discussion