ゲーム化!tomo_manaのブログ

ゲーム化!tomo-manaのブログ

Unityでゲームを作る方法について紹介しています

Unity学習#14-2 (Unity 2019.4.1f1) 階層化されたゲームオブジェクトの構成

Unity学習#14では、リストの要素数でサイズが変わるリストの作成手順について書きました。この記事は、その前段として、リストの各階層にアクセスするために必要な GameObject、Component、Transform の関係性についてまとめます。

先に結論を書き、その後で経緯をまとめます。

GameObject、Component、Transform の関係

先に結論を書くと、Scene は GameObject の一覧(コレクション)を持ち、GameObject は Component の一覧を持っています。Transform は Component の一つですが、Transform 同士で親子の関係(階層)を持っています。
そのため、自分の属するGameObjectから親または子のGameObjectにアクセスするには、一旦 GameObject が持っている Transform にアクセスします。(詳細は後述しますが、Transform から GameObject への参照は、Transform が Component の継承クラスであることを利用します)

f:id:tomo_mana:20200903012129p:plain

尚、自分自身と階層関係を持たない GameObject へのアクセスは、Scene を使ってアクセスすることになりますが、通常は [SerializeField] で特定の GameObject への参照をあらかじめ渡しておく方法が使われると思います。

MonoBehaviour からのアクセス方法

MonoBehaviour(C#スクリプトに使用する最小クラス)から GameObject、Component、Transform にアクセスする方法を以下にまとめます。

MonoBehaviour → GameObject

(this.)gameObject;

MonoBehaviour → Component

(this.)gameObject.GetComponent();

MonoBehaviour → Transform

(this.)gameObject.transform;

MonoBehaviour → 親GameObject

(this.)gameObject.transform.parent.gameObject;

MonoBehaviour → 子GameObject

(this.)gameObject.transform.GetChild(int).gameObject;
子ゲームオブジェクトの並び順は、Hierarchyウィンドウの並び順

GameObject、Component、Transform の関係(クラス図)

ここからは、上記の結論にたどり着いた経緯を簡単にまとめます。

以下は、Unityのリファレンスから分かる範囲で読み解いた GameObject、Component、Transform の関係です(クラス図)。変数や関数はお互いの関係性を知るために必要なものだけをピックアップしています。

f:id:tomo_mana:20200903002234p:plain
クラス図 全体

Unity に限らず、オブジェクト指向のマニュアルは、継承された機能の出元を追うには少し分かりづらい書き方になっています。これはオブジェクト指向の最大の特徴であるカプセル化のためで、オブジェクトが継承した変数や関数を、その出元を知らなくても使えるようにするためです。今回はリファレンスから読み解ける範囲で、継承された変数や関数の出元をまとめました。

継承ライン

クラスの継承は先ほどのクラス図の縦のラインです。スクリプトの中心となるMonoBehaviourから見た継承、Transform、GameObjectから見た継承をそれぞれ追ってみると、以下のことが分かります。

Object → Component → Behaviour → MonoBehaviour

まず、スクリプトの基本になる MonoBehaviour の継承関係を見てみます。MonoBehaviour は Component の継承クラスで、Component は Object の継承クラスです。MonoBehaviour が Component の一つとして扱えるようになっていることが分かります。

f:id:tomo_mana:20200903002258p:plain

GameObject とは直接の継承関係にありませんが、Component が GameObject を持っています。後述しますが、これは Component が自身の配列の親である GameObject を参照できるようにしています。

Component → Transform

Transform は Component を継承しています。Transform が Component の継承クラスであるということは、Transform も Component の機能を使って GameObject にアクセスできることを意味します。

f:id:tomo_mana:20200903002308p:plain

Object → GameObject

GameObject は、Component や Transformとは継承関係にはなく、ただ単に Object の継承クラスです。

f:id:tomo_mana:20200903002318p:plain

ここまで継承関係を見ましたが、継承だけでは Component と GameObject の関係が分からないので、所持ラインを見る必要があります。

個人的には GameObject が基底クラスでないことに驚きました。GameObject の役割を知るまでは、Object → GameObject → Component、のような継承があるのではと予想していたからです。

所持ライン

継承ラインから、Component と Transform が GameObject を参照できることが分かりましたが、GameObject からComponent や Transform を参照するルートが分からないので、所持ラインを見ていきます。
所持ラインは先ほどのクラス図の横のラインです。

Scene → GameObject → Component

GameObject は Component をコレクションとして持ちます。GameObject が GetComponent() という、いかにもな関数を持っていることから、GameObject が Component をコレクションとして持っていると推測できます。
尚、GameObject 自体も、Sceneのコレクション要素になっています。本稿の最後に書きますが、Scene は 全てのGameObject でなく、一番上の階層にある(Root)GameObject のコレクションだけを持っています。

f:id:tomo_mana:20200903004151p:plain

違和感があるのは、GameObject を継承していない Component が、GameObject だけが使えそうな GetComponent() を持っていることです。Component は GameObject のコレクションなので、本来は Component から他の Component を参照できないはずです。しかしながら、Component もまた、自分の属する GameObject にぶら下がる仲間の Component を探せるようになっています。

この場合、少なくとも 2 つの実装方法が考えられるかと思います。

方法1)Component が GameObject の GetComponent() を呼んでいる(ラッパー関数)
方法2)Component が、次または前の Component への参照を持っている(Component 同士がお互いをリンクする)

単純に考えると、Component が自分の属する GameObject への参照を持っているため、方法1の方が軽装で済みます。Component に次または直前の Component へのリンクを持たせると、Component の並べ替えをする時にひと手間増えるからです。また、コレクションの子要素にコレクション機能を持たせることは、クラスの責務が増えるので通常はあまりしないかと思います(コレクション機能は通常、 ArrayList や LinkedList など、コレクション機能だけのクラスを定義する)。ただ、Rect Transform 関連のコンポーネントは、コンポーネント同士でお互いを制御することがあります。そのため、方法2も考えられなくはないのですが、それでも方法1で実現できるので、おそらく方法1で実装されていると考えます。

GameObject → (Component) → Transform

Transform は Component の継承のため、これまでの情報から、GameObject は Component の一つとして Transform にアクセスできそうです。しかし、GameObject は、わざわざ Transform への参照を持っています。これは Transform が Component の中でも別格と推測されます。

f:id:tomo_mana:20200903004201p:plain

参照パス

継承と所持を確認したところで、先ほどの参照についてルートを確認します。基本的に、スクリプトのいる位置である MonoBehaviour から 1) GameObject、2) 同じGameObjectに属する他のComponent、3) 同じGameObjectに属するTransform、4) 子GameObject、5) 親GameObject への参照ルートを確認します。

MonoBehaviour → GameObject

MonoBehaviour から 自分の属する GameObject の参照は、Component が持っている GameObject への参照を利用します。

f:id:tomo_mana:20200903004040p:plain

MonoBehaviour → Component

MonoBehaviour から 同じ GameObject に属する他の Component を参照する時は、自分が属する GameObject の GetComponent() を利用します。

f:id:tomo_mana:20200903004050p:plain

MonoBehaviour → Transform

MonoBehaviour から同じ GameObject に属する Transform への参照は、先述の MonoBehaviour → Component への参照の他に、GameObject が Transform への直接参照を持っていることを利用します。

f:id:tomo_mana:20200903004101p:plain

GetComponent() は検索が必要なので、GameObject が持っている Transform への参照を利用する方が早いと思われます。

MonoBehaviour → 親GameObject

MonoBehaviour から自分の属する GameObject の親 GameObject を参照するには、Transform に着目します。

1) GameObject が他の GameObject を参照するためには、GameObject をコレクションとして持っている Scene を使えば良いのでは、と考えましたが、Scene が持っているのは Root GameObject だけなので、親GameObjectを取得できない可能性もあります。

2) 次に、GameObject が static 関数で持っている Find() は、このクラス図を眺める限り少し不自然です。Find が他のGameObjectを見つける方法が、このクラス図の範囲からは見つけられないからです。

3) Transform に着目すると、Transform には変数 parent と 関数 GetChild() が見つかります。これは、Transform 自身が親子関係を記述できることを意味します。Transform は リファレンスからは、Component との継承以外、リストに関わる Interface なども実装していません。そのため、parent と GetChild() は Transform が出元です。

4) おそらく、GameObject.Find() は、Scene が持っている Root GameObject と、Transform が持っている GetChild() を組み合わせて、目的のGameObject を探索するものと思われます。(そのうち調べたいと思います)

5) また、このパスには Transform から GameObject へのアクセスが必要ですが、これは Transform が Component の継承クラスで、Component が 自分の属する GameObject への参照を持っていることを利用します。

f:id:tomo_mana:20200903004113p:plain

MonoBehaviour → 子GameObject

MonoBehaviour から子GameObject への参照は、親GameObjectとほぼほぼ同じルートになりますが、親一つに対して子は一つでないため、最後は Transform の GetChild() で探す必要があります。GetChild() は index でしか検索できないので、目的のGameObjectかどうかを特定するために、GameObject の基底クラスである Object.name を活用することになりそうです。

f:id:tomo_mana:20200903004123p:plain

Unity 画面との関係

Hierarchy ウィンドウ = Sceneの配列 + Transform

これまで見てきた内容から、HierarchyウィンドウはSceneが持っているRoot GameObject のコレクションと、各Root GameObject が持っている Transform の親子関係の両方を俯瞰できる機能だと分かります。
以下に、Hierarchyウィンドウと、Sceneが持つRoot GameObject のリストを示します。Hierarchy と、Root GameObject の並び順が一致していることが分かります。

f:id:tomo_mana:20200903005644p:plain
Hierarchyウィンドウ
f:id:tomo_mana:20200903005705p:plain
gameObject.scene.GetRootGameObjects()

また、Hierarchy と、Transform が持つ 子Transform の並び順も一致しています。

f:id:tomo_mana:20200903013842p:plain
Hierarchyウィンドウ RootでないGameObject
f:id:tomo_mana:20200903013911p:plain
gameObject.transform.GetChild()

Inspector ウィンドウ = GameObjectの配列

また、Inspectorウィンドウは、GameObjectが持っているComponentのコレクションのゲーム中の状態を俯瞰できる機能だと分かります。Inspectorは「監視役」の意味で、Wikipediaではプログラム実行中のデータの状態などを表示して確認するツールとあります。GetComponents() をコールしてみると、Inspectorウィンドウの並び順に一致していることが分かります。

f:id:tomo_mana:20200903005731p:plain
Inspectorウィンドウ
f:id:tomo_mana:20200903005749p:plain
gameObject.GetComponents<Component>()

(以上)