Language:
  • 英語 (米国)
  • 日本語
more 

[Unity] 他オブジェクトと連携-FindとかGetComponentとかSendMessageとか

今回はUnity始めたての人に向けて、他のオブジェクトとの連携を取る際の方法について書きます。Find系、GetComponent系、SendMessage系あたりで他オブジェクトの関数やメソッドを参照したり使いたいみたいな事です。オブジェクト指向のキモとなるところなので利便性のためいくつかの方法があり、それゆえ最初の頃に詰まる部分になってくる。自分がUnity触りはじめの頃にひとつの方法を調べたら他の方法も芋づるで出てきてどんどん調べ事が増えて困ったので、うまくまとめられれば過去の私のような方の時間短縮になるはず。

 まず系統で分けつつ方法を書き出してみる。

  1. public static
  2. Inspectorウインドゥでアサインする
  3. Find系
    • Find
    • FindWithTag(複数ならFindGameObjectsWithTag)
    • FindObjectOfType(複数ならFindObjectsOfType)
  4. SendMessage系
    • SendMessage
    • SendMessageUpwards
    • BroadcastMessage
  5. GetComponent系
    • GetComponent(複数ならGetComponents)
    • GetComponentInChildren(複数ならGetComponentsInChildren)
    • GetComponentInParent(複数ならGetComponentsInParent)

大別すると使うのはこの5系統かな。相手オブジェクトを探すためのものと何か実行したりするためのものがごっちゃになってるけど、組み合わせて使うことになったりして結局これらは全系統調べる羽目になると思うので一緒くたに説明していく。

あと、これら以外にもOnCollision系やOnTrigger系、Raycast系などで範囲にhitした相手を取得、という方法もあるがそっちは総じてある形状にぶつかったオブジェクトを取得するもので、ちょっと毛色が違うので今回は省く。

 

 

1.public static

変数やメソッドをpublic staticにしておくと、その世界で唯一のものになってどこからでも使えるようになる。簡単に言うとpublicによって他所のスクリプトからそこへのアクセスを許可して、staticでその世界でただ一つだよという事にしている。世界唯一と公言しているので住所を調べなくても名指しするだけでそいつだとわかって連絡できる、というイメージだろうか。具体例を挙げながら解説していく。一番使用率が高いであろうC#で話していくので、他の言語を使っている方は、まあその、頑張れ。

 この例でいけば、どこの他所のスクリプトからでも
test1.HealthPoints -= 5;
と書くだけで変数HealthPointsは95になるし、
test1.Attack();
と書くだけでAttackメソッドを実行させることができる。しかもこの方法は他の方法より軽くて最速。

えっ超簡単だしこれでいいじゃん、と思った方は罠です。この書き方はゲームの内容部分では基本的に忘れてしまって一度も使わないくらいのほうが無難です。なぜなら前述の「世界で唯一」「どこからでも簡単にアクセスできる」というメリットがそのままデメリットにもなるから。

まずその世界で唯一であるデメリット。仮にザコ敵の体力にこれを使った場合。世界で唯一の変数なので、ザコAの体力が0になるとコピーして作ったザコBの体力も0。つまりザコのグラフィックはコピーしたとしてもこの変数部分は全部一つ一つ別にしなければならない事になる。使い回しが全く効かない。こいつはボスで一体しかいないからいいんだい!と思っていたとしても、あとからボスラッシュモード作りたいなーとかなったら修正を入れることになる。

さらにどこからでも簡単にアクセスできることのデメリット。ゲームの流れを変更した場合、どこからその変数やメソッドにアクセスしていたか把握しきれなくなりバグの元になる。だってどこからでもアクセスできるから。特にチーム製作だと、別の人が手直ししようとした場合にどこからアクセスしているのかゲーム全体をチェックする羽目になるので、無計画にpublic staticにするのは非常に嫌がられます。一人での製作だとしても、数ヵ月後に見ると自分のプラグラムでも他人が書いたのを解読するようなもの(だから一人作業でもわかりやすく書こうという戒め)だから、無計画に使うとやはり未来の自分がげんなりする羽目になるかもしれない。

じゃあpublic staticいつ使うんだ、と言えばゲーム内容ではなくシステム的な部分が適していると思う。どのキャラでもどのセーブデータでも共通して持っておく値とか。簡単な例として「私」というアカウントの中に”あいつ””こいつ”という2キャラを作っているとして、あいつで10体、こいつで20体、とスライムを倒していき、分岐があったので”あいつ”のセーブデータを2つに分けて・・・となっても、全ての合計で通算100体倒したとき「スライムハンター」のトロフィーを「私」アカウントがもらう。このスライムを倒した通算数計算やスライムハンターのトロフィー付与処理をpublic staticで行えば、その世界で唯一であってどこからでもアクセスできる変数やメソッドであることが有意義であると思います。
最も良いのは、主要なゲーム内の情報を一括で管理しておく、いわゆるGameManagerみたいなクラスを作る場合。こいつがpublic staticで変数やメソッドを持っておいて使うときはそこから取ってくると決めておけば、スコアやら残機数といった世界で唯一であるべき処理を簡単に最速でやり取りできる。

ただしやはり慣れないうちはそういったゲームシステムの構築も紆余曲折したり行き当たりばったりになると思うので、むやみにpublic staticを使わず慣れてきてから有効な部分でのみ計画的に使うのをオススメしたい。public staticをまったく使わなくても大抵のゲームは問題なく作れる、と言われるくらいなので。

 

 

2.Inspectorウインドゥでアサインする

上のトピックでpublicはよそのスクリプトからそこへのアクセスを許可していると書いたが、変数をpublicにするとInspectorウインドゥ上でその変数にそったものを数値入力したりアサイン(指定)したりできる。

例えばこう変数の宣言を書いておくとInspectorウインドゥにexGOというGameObject用の欄ができるので、ドラッグドロップやその欄の右の丸をクリックしてGameObjectをアサインできる。それと同時に他所のスクリプトからその変数を参照できるようにもなるので、後述のGetComponentなどでこのスクリプトを指定して変数を他所から変えたりとかできる。

他所のスクリプトからその変数を参照する必要がないなら、publicの代わりに [SerializeField] とつければ単純にInspectorウインドゥに欄を表示することだけができるけど、最初の頃はpublic一辺倒でいいかも。そうすれば「Inspectorウインドゥに欄が見えてる=他所のスクリプトからいじれる変数」という簡単な視覚確認ができる。

この方法は最もわかりやすく簡単なので、アクセスするGameObjectを指定する場合などはできればこの方法を使いたい所だが、いかんせん使えない状況がある。自分や対象がゲーム途中で生成されるとか、状況により対象が変わるとかの場合だ。基本的な対象指定ではこれをまず使っておいて、使えない場合に改めて他の方法を考慮すればいいと思う。

 

 

3.Find系

上記2番目のトピックで述べたアサインで指定する方法が使えない場合は、Find系で相手を探すことを考える。オブジェクト名、タグ、型のいずかでシーン内や対象内を検索する感じになる。該当が見つからない場合はnullを返す。また、基本的にアクティブ状態(「SetActive(true)」の状態)のゲームオブジェクトでないと検索できない。

ここからの説明で階層構造を表すのに子孫や祖先という言い方を使うけど、これは文字の通り子供の子供…や親の親…といった何世代先であっても階層の下流(あるいは上流)にあれば全て範囲内ということです。ただし親の別の子供みたいな直接の上流でも下流でもないものは普通に範囲外。

あとFind系共通仕様として、複数の検索該当が存在するのにひとつを探した場合、該当のうちのどれを参照するかはシステム内部で最初に引っかかったものになるので確実性がないことに注意。例えば同じオブジェクト名のものが2つある時にFindするとどっちに対して参照するかは不確実。ためしにPlayしてみたら欲しいほうのオブジェクトを取ってくれたのでこれでいいやとか適当なことをやると、あとで何のきっかけで参照先がぶれてしまうかわからない。なので、複数が対象になり得る場合は独自のオブジェクト名をつけて確実に対象を1つにするとかの区別が必要になってくる。

また、Find系は重い処理なので、Update()内で使って毎フレーム走らせたりするのは避けて、一度Find系で相手を見つけたら変数に入れておき再利用する。ではFind系3つのタイプを見ていく。

 

Find()

ゲームオブジェクト名でシーンや対象内を検索。

この例はCubeという名前のオブジェクトを見つけて変数hogeに取っている。これで今後このCubeに何かしたい場合は
hoge.transform.position = new Vector3 (0, 0, 0);
とかhogeに対して何かすれば再び探さなくてよい。

フォルダのように階層構造になっている入れ子の子要素にあたるオブジェクトに限定する場合はtransformコンポーネントを通してアクセスすることができる。

この例の場合は相手指定をしていないので自分の子のCubeを取っている。直下の子供だけが対象で孫以下のものは無視される。同名別固体なCubeオブジェクトが他所にあっても自身の子供のCubeに限定することができるが、自身の子供にCubeが2つある場合は参照先が確定しないので注意。末尾の.gameObject の部分は、transformを介してアクセスした場合は参照する情報もtransformなので、ここでゲームオブジェクトを取るようにしている。
hoge.transform.Find(“Cube”).gameObject;
この例のようにすれば、変数hogeに取っておいたオブジェクトの子供の中からCubeを探すことになる。
あと、このtransform.Find()ではFind系の特例的に相手が非アクティブ状態(「SetActive(false)」の状態)でも見つけることができる。非アクティブにする前にFindして変数に取ってから非アクティブにできるなら必要ないけど。

さらにFind()では階層構造から探す事もできる。

この例だとそのシーンでCubeの子になっているChildというオブジェクトを探す。Box1/Box2/Cube/Child のような奥に入り込んだ構造でも見つけてくれるが、Cube/Box1/Child とかだと条件に合っていないので当然引っかからない。

transformで対象の子供からに絞って探すことももちろんできる。

この例のようにすれば対象の(この場合は相手指定をしていないので自分の)子供がCubeで孫がChildの構造になっているChildを見つけるわけだが、ひとつ注意。この場合自身がCubeで、直下にChildを持っているとしてもそのChildは引っかからない。transformが言わばそいつ自身のことで、Findでその中身から”Cube/Child”に合うものを検索していると考えればわかりやすいだろうか。

あと、他Find系2種にはFindWithTagならFindGameObjectsWithTag、FindObjectOfTypeならFindObjectsOfTypeという複数一括検索用のスクリプトがあるのだが、このFind()には複数を一括で探すバージョンはないので、シーン内のCubeという名前のオブジェクトを全て取りたいといった場合は自前でfor文を回したりすることになる。ただこれは、公式的にはFind()よりFindWithTag()のほうが軽いようなので、複数探すならうまくタグ分けして比較的軽いFindGameObjectsWithTag()を使えということだと思う。その点から言っても、Find()で見つけたいものは独自の名前、いわゆるユニークネームをつけておく必要がある。

 

FindWithTag() (複数ならFindGameObjectsWithTag())

タグでシーンや対象内を検索し、そのタグがついているゲームオブジェクトを取得する。公式的には重い処理のFind系3種の中ではこれが一番軽いようだ。ただ軽いからと1体にしか使わないタグをやたらめったら作るのはナンセンスなので、FindGameObjectsWithTag()で複数を一度に取るほうが使い道としては多いかな。

 この例ではEnesという配列にEnemyタグの付いたオブジェクトを全部取っている。もちろんオブジェクト郡にはタグを付けておかねばならないし、配列がよく分かっていないうちは配列も調べる必要がある。場合によっては配列でなくListが必要かも。更にこのあと大概forかforeachで配列に何か処理を行うとかすることになるので、派生の調べ事が増えがちで訳が分からなくなりやすいかもしれない。「同じタグが付いてるオブジェクト郡に一斉に何かしたい」「同じタグが付いているオブジェクト郡の中から更に特定の何かを探し出したい」というような場合に使うものだと覚えておいて、そのシステムがゲーム構築に必要な場合に徐々に調べていくほうがいいかな。色々詰め込むと本筋の話が薄くなるので、ここでは主目的のFindGameObjectsWithTag()の話だけで説明は留めます。最初の頃に使うといったら「ボムでEnemyを一斉にDestroyする」とかかな。

このくらいの処理なら配列やforの使い方があやふやでもGoogle先生の力を借りればさくっと作れるはず。

 

FindObjectOfType() (複数ならFindObjectsOfType())

型でシーンや対象内を検索。型はRegidbodyやCollider、スクリプトなどInspectorに並ぶ項目の事と思っておけばOK。コンポーネントとも言う。つまりこれは他の2種とちょっと違ってゲームオブジェクトじゃなくて持っているコンポーネントを探す。ただしこの関数は遅いFind系の中でも特に動作が遅いうえ、大抵他のもっとましな方法があるので基本的に使わない。こういう方法もあると一応覚えておく程度でいい。

 

 

以上Find系3種。何度も言うように重いので一度探したら変数に保持しておいて、使いまわすようにするのが基本。そして変数に取ったオブジェクトに対して以下残り2つのSendMessage系やGetComponent系を使って、処理をさせたりあっちの変数を参照したりといったやり取りをする感じになる。

 

 

4.SendMessage系

対象のオブジェクトにメッセージを飛ばして指定のメソッドを実行させるイメージ。普通はpublicにしないと外からメソッドを直接実行させる事はできないけど、SendMessageだとpublicにする必要なく行える。また後述のGetComponent系を使っての実行と違ってどのコンポーネントとか詳しく指定しないでよく、相手のオブジェクトがどこかにそのメソッドを持っていれば実行してくれる。やや遅めの処理だけど扱いが簡単なので、頻繁でない処理を手軽にやりたい場合に良く使う。引数が一つしか送れず、戻り値も取れないのでそもそも簡単な一方通行の伝達での使用を想定していると思う。

ちなみに引数というのは処理の伝達の際に何か値を一緒に送る場合のその値のことで、戻り値と言うのは処理後に命令を発した元のところに結果を返す事。砕いて言えば荷物を1つしか持っていけないし、行ったきり帰ってこないので送った荷物を加工して返してもらうような処理は(これだけでは)できない。要するに複雑なやり取りには向かない。あとC#,JavaScript間みたいな言語が違う間のやりとりも普通にできちゃうのも特徴だけど、これはそもそもゲーム内にベース言語が混在してる状態があまりよくないのでオマケ程度。

これも3種類あるけど3種のやることは同じで、単体宛て、入れ子の子孫宛て、入れ子の祖先宛てという送り先が違う分類になる。ではSendMessage系の3種を見ていく。

 

SendMessage()

単体のゲームオブジェクトに指定したメソッドを実行させる。例えば

この場合だと、対象指定してないので自分自身のどこかのスクリプトに

というメソッドを持っていればそれを実行する。もちろん
hoge.SendMessage(“Damage”);
とやれば変数hogeに保持しておいたオブジェクトが対象になる。

引数を持つ場合は

のようにする。この場合メソッド側も引数を受け取るようにするわけだけどこれは話が逸れるし調べればすぐわかるので割愛。

 

BroadcastMessage()

対象相手の入れ子の子孫全てに指定したメソッドを実行させる。対象相手自身も実行に含む。もちろん引数もひとつなら持たせられる。何かのコピーの一群を空のオブジェクト内にまとめて入れておいて一斉に何かを起こすとかできるけど、複数一斉なぶんより重くなるので注意。

 

SendMessageUpwards()

対象相手の入れ子の祖先全てに指定したメソッドを実行させる。対象相手自身も実行に含む。祖先全部に何か一斉に処理っていう状況があまりなさそうなので他のSendMessage系より使用頻度は低いかもしれない。

 

 

以上SendMessage系3種。ちなみに相手が指定のメソッドを持っていない場合にはエラーが出てスペルミスとかがわかるようになってる。状況に応じてメソッドがあったりなかったりするだけでミスではないのでエラーがうっとうしい、みたいな場合は

のように書くとエラーチェックをしなくなる。

 

 

5.GetComponent系

コンポーネントの事はRegidbodyやCollider、スクリプトなどInspectorに並ぶ項目の事と思っておけばよいと上で述べたが、GetComponent系は名前の通りそのコンポーネントを参照する。例えば相手オブジェクトのRegidbodyを参照しとあるタイミングでUseGravityをOffにして重力から切り離すとか、スクリプトも取れるので外から変数を変更したりメソッドを実行させたりできる。SendMessageと違って複数の引数も扱えるし戻り値も受け取れる。自由度が高くそれなりに速いので、複雑なやり取りや頻繁なやり取りの場合に使う。

これも3種あって、InChildrenやらInParentと名称の末尾についているのからわかるとおり、対象のオブジェクト単体、子孫全ての中から、祖先の全ての中からの3つの分類。さらにその3つがひとつのコンポーネントだけを参照する単体用と、当てはまるコンポーネント全てを取る複数用どちらのタイプもある。

 

GetComponent<T>() (複数ならGetComponents<T>())

単体のゲームオブジェクトの持つ、指定したコンポーネントを参照する。

 この例は自身のRegidbodyを参照して変数exGCに取り、IsKinematicをオンしている。もちろんこれも
hoge.GetComponent<Rigidbody>();
とすれば変数hogeに取っておいたオブジェクトのRegidbodyを参照する。

あと、スクリプトを参照する場合の変数の型宣言がちょっとわかりづらいのでそれも書いておこう。小数点ありの数字を入れる変数の型はfloatだし、Regidbodyを入れる変数の型は当然Rigidbodyだけど、スクリプトを入れる変数の型はどうすれば?

このようにスクリプト名が型になる。

そしてここまでのトピックで話に出たが、参照先のスクリプトの変数やメソッドはpublicにしておかないと外から参照できないので注意。

こんな感じに変数exGCに取ったスクリプトの中でpublicになっていれば
exGC.HealthPoints=3;
とやって外から変数を変えたり
exGC.Attack();
とやってメソッドを実行させたりできる。GetComponentさえしちゃえば、参照したスクリプトの中だけなら一番最初の方法で出したpublic staticと似たような感じで使えるって事だね。

あと複数形のGetComponents<T>()は、単体のゲームオブジェクト内にある同じ種類のコンポーネントを全部取るわけだけど、ひとつのオブジェクトに同じ種類のコンポーネントをいくつもつけておいてどうこうっていうのはあまりやらないと思うので、一応存在を覚えておく程度でいい。

 

GetComponentInChildren<T>() (複数ならGetComponentsInChildren<T>())

 対象相手の入れ子の子孫全ての中から指定のコンポーネントを探して参照する。 対象相手自身も検索先に含む。GetConponentInChildren<T>()の方はひとつのコンポーネントを対象にするわけだけど、これも今まで同様複数の該当先があった場合はどれを参照するか不確定なので注意。単純な例では子供が3人いて3人ともRegidbodyを持ってる場合にどいつのを参照するかは不確実ってことね。

GetConponentsInChildren<T>()なら対象自身とその3人の子供全員のRegidbody全部を配列に取るような形になる。

この例ではRegidbodyを全て配列exGCに入れて、foreach文で処理を回して全員のIsKinematicをオンにしている。

 

GetComponentInParent<T>() (複数ならGetComponentsInParent<T>())

 こちらは対象相手の祖先全ての中から指定のコンポーネントを探して参照する。検索に対象相手自身も含む。子孫か祖先かの違い以外は上と変わらない。

 

 

 

以上で冒頭で述べた5系統をすべてみてきたわけだけど、総じて何かアクセスしたいオブジェクトなりコンポーネントなりを変数に保持しておいて使いまわすのが基本になる。ゲーム進行中に何かを探すというのが重めの処理で避けたいからだ。だから最初から変数に入るInspectorウインドゥ上でアサインする方法か、Start()内やAwake()内で探して変数に入れてしまうのがよい。あるいは、prefabからInstantiateで生成したときに変数に突っ込んでしまうようにすれば後々探す手間とコストを省くことができる。

その場面で軽くて効率のいい処理にするにはどのアプローチがいいのか、というのはゲームを組み立てるときのひとつの物差しになる。投げ出してしまうよりはとりあえず動けばいいという前傾姿勢も必要だけど、自分なりに納得のいく処理アプローチを組めたときにプログラムの楽しさに気づいたりする。もちろんスマホ用みたいな軽さが重要なプラットフォームの場合は避けては通れない。そのためにも様々な手順の情報が必要。

そして数ヵ月後に見直したときに「いや、こう書けば3行ですむし…昔の私なにしとん…」みたいになるのが嗜みである。