前の記事でGameObjectを取得する場合は非常に多いと書きました。
この記事ではGameObjectを取得する例をいくつか記述します。
Find()関数で取得する
GameObjectにはFind()という関数があり、これで他のGameObjectを検索して取得できます
Enemyという名前にしたGameObject
├GameObjectの持っている関数A
:
└GameObjectの持っているFind()という関数
公式のドキュメントは以下です
GameObject - Unity スクリプトリファレンス
>GameObject.Find - Unity スクリプトリファレンス
EnemyにアタッチしたShotクラスからのFind()関数の使用例は次です。
// GameObject型のplayerという変数を用意する
GameObject player;
// 「Player」と言う名称のゲームオブジェクトを探してきてplayerに代入する
player = GameObject.Find("Player");
ちなみに今はこんな状態です。
Enemy(GameObject)
└Shot.cs ←ここから自機のゲームオブジェクトを探しに行った
Player(GameObject) ←これを探しに行った
├色々コンポーネント
playerに自機のゲームオブジェクトを取得したので、この後色々できます。例えばこんな感じです。
// 自機を非アクティブにしたり(ゲームの画面に表示されなくなります)
player.SetActive(false);
// アクティブに戻したり
player.SetActive(true);
実際はPlayerというGameObject自体ではなく、Playerが持っているコンポーネントに対して色々何かする場合がほとんどなのですが、それについては別の記事で記述します。
Find()の検索では親子関係のパスを指定して検索する事も出来ますが、詳細は省略します。
Find()は重い
公式ドキュメントにも書いてある通りFind()関数は重いので、実行回数を多くし過ぎないようにしましょう。
例えば、Update内で毎フレームFind()を使用する等は止めましょう。
GameObjectを検索する他の関数
Find()以外にもGameObjectを探してきて取得する関数があります。
- FindGameObjectsWithTag()
ゲームオブジェクトの名前(name)ではなく、ゲームオブジェクトについているTagで探してきます。これは、配列で帰ってきます。
- transform.Find()
子供だけから検索します(詳細は省略しますが、Find()とちょっぴり違う動作をする部分があります)
GameObject.Find()の問題点
GameObject.Find()は変更に弱いです。
Find()は名前、つまり文字列で検索します。
ですので一度スクリプトを書いた後にGameObjectの名称を変更すると検索に失敗します。
例えば「あとから二人同時プレイ可能にすることにした。なのでPlayerというGameObjectをPlayer1というGameObjectとPlayer2というGameObjectにする事にした」(良い例ではありませんがそれは置いといて・・・。)
この場合、"Player"という文字列でFind()していた部分は全て空振りするようになります。
ここで考えて欲しいのですが、プログラム全体でFind("Player")が書いてある箇所は一か所だけではなさそうですよね。複数のスクリプトからFind("Player")をやってる場合が多いでしょう。これが全部空振りするようになってしまいます。
Find()は空振りするとnullを返します。恐ろしいNullReferenceException地獄、ヌルポ地獄の始まりです。
上の例では文字列をハードコーディングすること自体がまずいのでもう少しましにする方法があるでしょうが、何をしても何らかの二重メンテになるのは避けられないかと思います。
また、同じ名称のGameObjectを作れなくなるというのもきついです(全てのGameObjectの名前を覚えていろと!?)
ですので、個人的にはGameObject.Find()の使用回数は少なくしています。
public GameObject ~ に設定しておく
その場でGameObjectを探すのではなく、あらかじめGameObjectをどこかに書いておく、設定しておくことができます。
GameObject型のPublic変数を定義します。するとInspector上にGameObjectを設定する事ができます。
using UnityEngine;
public class Enemy : MonoBehaviour
{
// 以下の行によりInspectorのEnemyのScriptにPlayerとの欄がでます
// InspectorにはplayerではなくPlayerと大文字で表示されます・・・
public GameObject player;
Start()
{
// この後playerに対してごにょごにょする
}
}
ドラッグアンドドロップしたGameObjectの元の名を変更した時、再度のドラッグアンドドロップは不要です。名称もUnityが勝手にやってくれて付いてきてくれます。二重メンテが不要になりました!
(もちろん、GameObjectを新規に作り直した場合は再度のドラッグアンドドロップが必要です)
いちいちInspectorにドラッグアンドドロップするのは面倒ですが、こちらの方がFind()よりましのように思います。
public GameObject ~の問題点
上に記述した「public GameObject ~」の問題点です。
publicにしているという点自体が問題です。publicという事は公開されていますから、初期設定こそInspectorでやっても他のソースからゴリゴリ変更可能です。
プログラマーが一人なら問題は少ないかもしれませんが、複数人でプログラムしていると次のような事はよくあるでしょう。
「ん?このplayerって公開されてるのか。公開してるという事は色々代入して使っていいという事だな。ちょっとこっちのソースで値変えて使うべ。」
「そ、そんなつもりはなかったの、触らないで、やめてー」
はいはい、またバグ地獄の始まりです。
色々な言語が進化してオブジェクト指向言語言語ができたのは「隠せるものは隠して(隠蔽して)トラブルを減らす」ためです。publicにしてしまっては前近代に戻ってしまいます。
(もちろん本当に意図してpublicにしたいならばそれで良いですが、そのような場合は少ないかと・・・)
しかしpublicにするのがまずいと言われても、publicにしておかないとInspector上に表示されません。ジレンマです。困りました。Find()するしかないのでしょうか?
このような場合、次の項が定番になります。
[SerializeField] private GameObject ~ に設定しておく
「public GameObject ~」を改善しましょう。
「public GameObject ~」の代わりに「[SerializeField] private GameObject ~」を書きます。
これにより、前述同様にInspector上にドラッグアンドドロップできますが、他のスクリプトからは隠蔽できます。
using UnityEngine;
public class Enemy : MonoBehaviour
{
// 以下の行によりInspectorのEnemyのScriptにPlayerとの欄がでます
// このPlayerは他のスクリプトから変更できない!やった!
[SerializeField] private GameObject player;
Start()
{
// この後playerに対してごにょごにょする
}
}
第一候補は[SerializeField] private GameObject ~
ということで、GameObjectを取得するというかあらかじめ設定しておく第一候補は、今まで書いた中では「[SerializeField] private GameObject ~」になります。
(おまけ)GameObjectには限らない
ちなみに、[SerializeField]を指定できるのはGameObjectには限りません。他の変数も可能です。