GameObject.Find(“car_0”) の1行から学ぶ ― 名前でオブジェクトを探す仕組みと注意点(Unity 6)

広告

この記事は、北村愛実 著『Unityの教科書 Unity 6完全対応版』(SBクリエイティブ)で扱われているSwipeCar制作を題材に、授業でつまずきやすい点や、コードの意味を補足するために作成した学習記事です。コードおよび制作手順の詳細は、書籍もあわせてご参照ください。

車を滑らせて旗の手前で止めるゲームでは、画面に「旗までの残り距離」を表示します。その役目を担うのが、次の GameDirector スクリプトです。ゲーム全体の作り方は、こちらの記事で解説しています。

Unity 6でスワイプして車を走らせる ― SwipeCarで学ぶスワイプ判定・効果音・距離表示(PC版)

using UnityEngine;
using TMPro;   // TextMeshPro を使うために必要

public class GameDirector : MonoBehaviour
{
    GameObject car;
    GameObject flag;
    GameObject distance;

    void Start()
    {
        // シーンの中から、名前でオブジェクトを探す
        car = GameObject.Find("car_0");
        flag = GameObject.Find("flag_0");
        distance = GameObject.Find("distance");
    }

    void Update()
    {
        // 旗と車のX座標の差が、残りの距離
        float length = flag.transform.position.x - car.transform.position.x;

        // 小数点以下2桁にそろえて表示する
        distance.GetComponent<TextMeshProUGUI>().text = "Distance:" + length.ToString("F2") + "m";
    }
}

今回注目するのは、Start の中にあるたった1行です。

car = GameObject.Find("car_0");

何気なく書いているこの1行には、初心者がつまずきやすいポイントがいくつも隠れています。順に見ていきましょう。

GameObject.Find は何をしているのか

GameObject.Find は、シーンの中から指定した名前のオブジェクトを探して、見つかったものを返す命令です。

イメージとしては、教室で「小西さん、いますか?」と名前を呼んで人を探すのに近いです。名前を呼ばれた人が手を挙げれば、その人を見つけられます。

この1行を日本語にすると、次のようになります。

  1. シーンの中から「car_0」という名前のオブジェクトを探す
  2. 見つかったオブジェクトを、変数 car に入れておく

ここで大事なのは、探す手がかりが名前(文字列)だけだということです。スクリプトは、Hierarchyウィンドウに表示されている名前を頼りにオブジェクトを探しています。

一度変数に入れてしまえば、あとは car.transform.position のように、そのオブジェクトの位置やコンポーネントに自由にアクセスできます。GameDirector のような「監督役」のスクリプトは、自分以外のオブジェクトの状態を知る必要があるので、最初にこうして相手を見つけておくわけです。

“car_0" という名前はどこから来たのか

「車なら "car" でよさそうなのに、なぜ _0 が付いているの?」と思った人もいるでしょう。

画像をProjectウィンドウからシーンにドラッグ&ドロップすると、オブジェクト名が car_0 のように「画像名_番号」になることがあります。今回のプロジェクトでは、シーンに置いた車のHierarchy上の名前が「car_0」になっているため、スクリプトも "car_0" で探しているのです。

つまり、Find に渡す文字列は「自分が付けたつもりの名前」ではなく、Hierarchyに実際に表示されている名前です。次のような小さな違いでも、別の名前として扱われて見つかりません。

Hierarchyの名前スクリプトに書いた文字列よくある原因
car_0"car"_0 を書き忘れた
car_0"Car_0"大文字と小文字の違い
car_0 (1)"car_0"Ctrl+Dで複製したときに番号が付いた
distance(後ろに全角スペース)"distance"名前の後ろに余計な空白が入っている

名前を入力するときは、Hierarchyでオブジェクトを選んで名前をコピーし、スクリプトに貼り付けると確実です。

見つからないときに起きること

名前を間違えたとき、GameObject.Find はエラーを出しません。代わりに「何も見つからなかった」という意味の null を返します。

たとえば、旗の名前を次のように間違えたとします。

flag = GameObject.Find("flag");   // 正しくは "flag_0"

この行は何事もなく実行され、flag には null が入ります。エラーが出るのはそのあと、Update で flag.transform を使おうとした瞬間です。Consoleウィンドウには次のようなエラーが表示されます。

NullReferenceException: Object reference not set to an instance of an object
GameDirector.Update () (at Assets/GameDirector.cs:21)

初心者が混乱するのは、エラーが出た行と、本当の原因がある行が違うという点です。

  • エラーが出た行 → Update の中の flag.transform.position.x を使っている行
  • 本当の原因 → Start の中の GameObject.Find("flag") の名前間違い

NullReferenceException は「中身が空っぽの変数を使おうとした」という意味です。このエラーが出たら、「その変数にはどこで値を入れていたか?」とさかのぼって調べるのが近道です。

原因を早く見つけたいときは、Start に確認用のコードを一時的に足しておくと便利です。

flag = GameObject.Find("flag_0");
if (flag == null)
{
    Debug.LogError("flag_0 が見つかりません。Hierarchyの名前を確認してください");
}

使うときの3つの注意

1. 名前は1文字も違わずに一致させる

先ほど見たとおり、大文字・小文字、番号、スペースまで含めて完全に同じでないと見つかりません。また、あとからHierarchyでオブジェクト名を変更すると、スクリプトの文字列は自動では変わらないので、その瞬間に動かなくなります。名前を変えたときは、Find している箇所も必ず確認しましょう。

2. 非アクティブなオブジェクトは見つからない

Inspectorの一番上にあるチェックボックスを外して非表示(非アクティブ)にしたオブジェクトは、GameObject.Find では探せません。「最初は隠しておいて、あとで表示したい」オブジェクトを Find で探そうとすると、null が返ってきてしまいます。このような場合は、後で紹介する [SerializeField] で割り当てる方法を使います。

3. 同じ名前が複数あると、どれが返るか分からない

シーンに「car_0」が2つあった場合、GameObject.Find("car_0") はどちらか1つだけを返します。どちらが返るかは決まっていないので、「動くときと動かないときがある」という原因の分かりにくい不具合につながります。Find で探すオブジェクトには、シーン内で重ならない名前を付けておきましょう。

なぜ Start で呼ぶのか

GameObject.Find は、シーンのオブジェクトを順番に調べて名前を比べる処理です。オブジェクトが少ないうちは気になりませんが、ゲームが大きくなるほど探す手間も増えます。

そこでこのスクリプトでは、最初に1回だけ呼ばれる Start で探し、結果を変数に取っておく書き方をしています。Update は毎フレーム(1秒に数十回)呼ばれるので、そこで毎回 Find すると、同じ人を何度も名前で呼び出し続けることになります。

// こうは書かない(毎フレーム探し直している)
void Update()
{
    GameObject car = GameObject.Find("car_0");
    // ...
}

一度見つけた相手は変数に覚えておき、あとはその変数を使い回す。教科書のコードは、この基本をきちんと守った書き方になっています。

もう一歩:GetComponent も同じ考え方でできる

同じ目で Update を見直すと、もう1か所気づくことがあります。

distance.GetComponent<TextMeshProUGUI>().text = "Distance:" + length.ToString("F2") + "m";

GetComponent も「オブジェクトに付いているコンポーネントを探す」処理なので、毎フレーム探さなくても、最初に1回取っておけば十分です。次のように書き換えられます。

using UnityEngine;
using TMPro;

public class GameDirector : MonoBehaviour
{
    GameObject car;
    GameObject flag;
    TextMeshProUGUI distanceText;   // テキストのコンポーネントを直接持っておく

    void Start()
    {
        car = GameObject.Find("car_0");
        flag = GameObject.Find("flag_0");
        // 見つけたオブジェクトから、最初に1回だけコンポーネントを取り出す
        distanceText = GameObject.Find("distance").GetComponent<TextMeshProUGUI>();
    }

    void Update()
    {
        float length = flag.transform.position.x - car.transform.position.x;
        distanceText.text = "Distance:" + length.ToString("F2") + "m";
    }
}

この規模のゲームでは体感できるほどの差は出ません。ただ「探す処理は最初に1回、毎フレームは使うだけ」という型を今のうちに身につけておくと、あとで大きなゲームを作るときに役立ちます。

もう一つの方法:[SerializeField] で割り当てる

名前で探す以外に、Inspectorでオブジェクトをドラッグ&ドロップして割り当てる方法もあります。

using UnityEngine;
using TMPro;

public class GameDirector : MonoBehaviour
{
    // Inspectorに欄が表示されるので、そこへドラッグ&ドロップする
    [SerializeField] GameObject car;
    [SerializeField] GameObject flag;
    [SerializeField] TextMeshProUGUI distanceText;

    void Update()
    {
        float length = flag.transform.position.x - car.transform.position.x;
        distanceText.text = "Distance:" + length.ToString("F2") + "m";
    }
}

スクリプトを保存してGameDirectorオブジェクトを選ぶと、Inspectorに Car、Flag、Distance Text の欄が現れます。そこへHierarchyから car_0、flag_0、distance をそれぞれドラッグすれば準備完了です。Start で探す処理はもう必要ありません。

両者の違いをまとめると次のとおりです。

比べる点GameObject.Find[SerializeField]
準備スクリプトだけで完結Inspectorでの割り当てが必要
名前を変えたとき動かなくなるそのまま動く
非アクティブのオブジェクト探せない割り当てられる
割り当て忘れ・名前間違い実行中に初めて null と分かるInspectorで「None」と表示され、目で確認できる

教科書が最初に GameObject.Find を使うのは、コードを読むだけで「どのオブジェクトを使っているか」が分かり、手順も少なくて済むからだと考えられます。まずは Find で仕組みを理解し、慣れてきたら [SerializeField] も使い分けられるようになるとよいでしょう。

まとめ

  • GameObject.Find は、Hierarchyに表示されている名前でオブジェクトを探す
  • 名前は大文字・小文字や番号まで完全に一致させる。迷ったらHierarchyからコピーする
  • 見つからないと null が返り、あとで別の行で NullReferenceException が出る
  • 非アクティブなオブジェクトは探せず、同名が複数あるとどれが返るか分からない
  • 探す処理は Start で1回だけ行い、結果を変数に取っておく(GetComponent も同じ)
  • Inspectorで割り当てる [SerializeField] という選択肢もある

たった1行の GameObject.Find("car_0") でも、「なぜこの名前なのか」「なぜここで呼ぶのか」を考えると、たくさんのことが学べます。教科書のコードを写すときは、ぜひ一行ごとに「なぜ?」を考えてみてください。

このシリーズの記事一覧は、Unity 6 ゲーム制作 補足解説シリーズまとめからご覧いただけます。

参考書籍

北村愛実 著
『Unityの教科書 Unity 6完全対応版』
SBクリエイティブ
書籍サポートページ(サンプルファイル)

本記事では、書籍で扱われているSwipeCar制作を題材に、初学者向けの補足説明やUnity 6での学習上のポイントを加えて解説しています。

訪問数 4 回, 今日の訪問数 4回

広告

C#,Unity,Unity6

Posted by hidepon