【Unity入門】特定ゲームオブジェクトへスクリプトからアクセスする方法まとめ

Unity入門講座


Unityの本格ゲーム制作講座はこちら
【30日間の全額返金保証付き】

この記事では、Unityプログラミングをしているとよく発生する問題

「別ゲームオブジェクトにスクリプトからアクセスしたい」

に対する解決策をまとめて解説していきます。

通常のUnity機能はもちろん、シングルトンやサービスロケータなどを用いた応用的かつ実践的なテクニックも扱っていきます。

解説の題材として、前回作成した汎用サウンドマネージャ機能を用いていきますね。

前回記事を読んでいない場合は参考にしてみてください。

参考記事:

【Unity入門】汎用サウンドマネージャー(Sound Manager)の作り方 前編
ゲームを作るに当たって、最後に手を入れがちですが、とても重要なのが「サウンド」です。 Unityには標準のサウンド機能(AudioClip,AudioSource,AudioListener)があるのですが、これをそのまま使うのはちょ...
【Unity入門】汎用サウンドマネージャー(Sound Manager)の作り方 後編
あると便利なサウンドマネージャー(SoundManager) 前回は 手軽に効果音が管理・再生できる 再生する効果音の指定にわかりやすい別名がつけられる まで作りました。 前回の記事: 今回は...

【Unity入門の森の最新ゲーム開発講座もお届け】

特定ゲームオブジェクト(Managerオブジェクト)へのアクセス方法まとめ

別記事で作ったSoundManagerに限らずですが、ゲーム全体で何かしらの処理を一括して管理する役割を持つManagerオブジェクトは当然、使用する側が存在します。(別記事でテストで作ったSoundTestなど)。

他オブジェクトから使用する場合に、Managerオブジェクト(ゲームオブジェクト)にどのようにしてアクセスするのか、が問題になります。

色々なケースがありますが、その中でもよく使われる方法を紹介します。

Inspectorから指定

一つは今回作ったSoundTestのように

Inspectorから指定出来るようにする方法ですね。

これが出来るならこれが一番です。

しかし、これは、「最初からSceneに置いてあるオブジェクト」でないと、当然Inspectorから指定は出来ないので、「InstantiateしたPrefabについているスクリプトから音を鳴らしたい」といったケースには使えません。

FindObjectOfType

次に、FindObjectOfTypeを使う方法です。

この方法でしたら、Startメソッド内でManagerオブジェクトを取得できるので、Instantiateなどで生成されたオブジェクトからでもManagerが使用出来るようになります。

ただし、このFindObjectOfTypeという処理はScene上にある全てのオブジェクトが持っているComponentを全て総当りして目的のものを探すイメージに近く、非常に重い処理になります。

そのため、毎フレーム呼ばれるような処理(Updateメソッド内等)で使用するのはもってのほかですし、Startメソッド内で確保する方法にしてもPrefabが一気に100や200作られるようなケースには向きません(動くとは思いますが・・・)

GameObject.Find & GetComponent

FindObjectOfType はFind系メソッドの中でも1・2を争う遅さ(負荷)なので、それを使うならGameObject.Findの名前によるオブジェクトの探索+GetComponentによるComponentの取得の2段階にした方が速いです。

ただしこの方法はSoundManasgerオブジェクトの名前が「SoundManager」であることが条件となります。タイピングミスも許されないのでちょっと不安ですね。

GameObject.FindWithTag & GetComponent

GameObject.FindWithTagという、Tag(タグ)によって探索+GetComponent です。
このGameObject.FindWithTagは名前による探索(GameObject.Find)よりも速い(らしい)です

ただし、下準備が必要です。
まず、Tagを自分で一つ定義する必要があります(Layers→EditLayers→Tagsに追加)

ここでは「SoundManager」というTag名にしました。

こうして、定義したTagを、SoundManagerオブジェクトのTagに指定します。

こうすることで、GameObject.FindWithTagによる高速な探索が出来るようになります。

が、やはり文字列を指定して探索する仕組み上、タイピングミスは怖いですね。


 

ここまでは、Unityの最初からある機能を使っての解決方法でしたが、その他にも自前でスクリプトを組む色々な方法があります。

今回はその中でも特に有力な

  • Singleton
  • サービスロケータ

を紹介したいと思います。

Singletonパターンを使ってUnityでオブジェクトにアクセスする

Singleton(シングルトン)パターンとは、
「そのオブジェクトが一つであることを保証し、オブジェクトへグローバルにアクセス出来るようにする」
という、デザインパターンの一つです。

実のところ今までに紹介したFind系の方法もオブジェクトが一つである前提の処理でした。

これを「保証」させます。(ついでにどこからでもアクセス出来るようにします)

実装方法としてよく使われるのは、自分自身以外のマネージャー(ここではSoundManager)が既に居るなら自身をDestroyさせる、という方法です。

なお、「既にあるかどうか」を管理するためにstatic で宣言された プロパティを使い、さながら椅子取りゲームの最後の一つの椅子のように、速いもの勝ちで取り合う事になります。

最低限実装すると以下のようなコードになります。

Awake内で、誰も椅子に座っていないなら(Instance == null)、自分が座り(Instance = this)、既に自分以外の誰かが座っているなら(Instance != this)、自分は座れない=>死(Destroy)  としています。(座れない=死とは、とんだ椅子取りゲームですね)

この、Singleton化したSoundManagerを使う場合は

このように今までのFind系メソッド使用の例と同じくメンバ変数へと取得する事もありますが、今までのFind系メソッド等に比べると十二分に高速なため、

このようにUpdate処理の中でもSingletonインスタンスを取得しつつ使用、という方法もよく取られます。

なお、マネージャー系(基本一つしかなく、どこからでもアクセスさせたい)=Singleton という風潮があります。

と、いうのもこのSingletonを組み込んだジェネリックなMonoBehaviourとして「SingletonMonobehaviour<T>クラス」を作っておくことで、継承するだけで非常に簡単にグローバルアクセスが出来るようになるためです。

具体的にはこのようなクラスです。

※大分簡略化しています。 「SingletonMonobehaviour」で検索するともっと多機能なものも出てくると思います

このような基底クラスを用意しておくと、
「よし、作った**ManagerをSingletonにしたい!」
といった場合に

今まで

だったところを

SingletonMonoBehaviour継承に変更し(<>の中にはSingleton化したいクラス名を入れます)

既にマネージャー内でAwakeメソッドが宣言されている場合は、親クラスのAwakeメソッドが呼ばれなくなってしまうので、明示的にbase.Awake(); を実行して親クラスのAwakeメソッドを呼んであげます。

この2ステップだけでSingleton化され、どこからでもアクセス出来るようになります。
この手っ取り早さもウケている要因の一つだと思います。

ただ、本来「一つである」ということと「どこからでもアクセス出来る」ということは別で考えるべき事のはずなのですが、一緒くたになってしまっているところや、テストがしづらい等で物議を醸しがちなのもSingletonの特徴です。(もちろん、無理の無い範囲で使用する分にはまったく問題ありません)

サービスロケータパターンでUnityでオブジェクトにアクセスする

Singletonパターンの対抗馬としてにわかにささやかれているのがサービスロケータパターンです。

Singletonパターンは、マネージャーオブジェクトそのものの形を変異させてしまう(Singletonにしてしまう)のに対し、サービスロケータは、作ったマネージャーの形はそのままに「サービス(ゲームのどこからでも必要とされるもの)」として捉え、管理する方法になります。

下準備

まず、準備として今回のSoundManagerのサウンド再生部分をサービス化するにあたり、外から使用される機能だけをインタフェースとして抽出します。

今回のSoundManagerは「Playメソッドによって再生が出来る」 という機能しかありませんので、Playメソッドをインタフェースとして宣言しておきます。

そしてSoundManagerは、このISoundServiceを実装したことにします。

SoundManagerスクリプトの修正は以上になります。最終的に以下のようになりました。

サービスロケータクラス

サービスロケータも色々な書き方があるのですが、ものすごくシンプルに書くとこのようなスクリプトになります(以下で<T>の使い方がわからない方向けの参考記事:https://blog.websandbag.com/entry/2018/08/11/230001)。

サービスの保持用のプロパティ:Instance

サービスの登録・開放用のメソッド:Bind,UnBind
という非常にシンプルな構成です。

使用する時はSingletonパターンと同じく

メンバ変数へ取得してから使っても良いですし、

このようにUpdate処理の中でサービスロケータからサービスを取得しつつ使用、という方法でも構いません。

Singletonパターンに似ていますね。
しかし、実はサービスロケータを使う場合はこのままでは動きません。

サービスロケータにサービスを登録する必要があります。

サービスの登録

サービスを登録するためのオブジェクトをHierarchyに作成し、サービス登録のためのスクリプトを用意します。 例ではどちらも名前は「ServiceInstaller」としました。

ServiceInstallerスクリプトでは、SoundManagerオブジェクトInspectorで受け取れるようにしておき、Awakeメソッド内でサービスの登録、Destroy時にはサービスの開放を呼ぶようにしています。

スクリプトを保存したら、忘れずにUnityEditorのInspectorでSoundManagerを指定するようにします。

これで、サービスロケータにSoundManagerが登録され、他スクリプトからサービスロケータ越しにISoundServiceを取得して音が鳴らせるようになりました。

「なんだSingletonより手間が多いじゃないか」となってしまいがちですが、このサービスの登録を自分で行う。というのがむしろ利点になります。

例えば、
「テスト実行中は、どの効果音を再生しようとしたかログを出したい。が、製品版にはログはでてほしくない」
と言った場合、このようなクラスが役立ちます

SoundManagerと同じく、ISoundServiceを実装した、LoggedWrapSoundServiceクラスです。

中身は非常にシンプルで、コンストラクタで別のISoundServiceを受け取り、Playメソッド内ではDebug.Logによるログ出力をしてから、コンストラクタで受け取ったISoundServiceのPlayメソッドを呼ぶようにしています。

では、ServiceInstallerスクリプトをちょっと修正します。

まず、ログ機能のON/OFFをInspectorで指定できるようにメンバ変数 isSoundLogEnable を追加し

この変数のtrue/falseで、Awakeメソッドでのサービスロケータへの登録を分岐させています。

true=ログが有効 な場合は サービスロケータへの登録は先程作った LoggedWrapSoundService にしていますね(コンストラクタにsoundManagerを渡しているので、音もちゃんと鳴ります)
また、falseの場合は、今まで通り、SoundManagerをそのまま登録しています。

ではスクリプトを保存してUnityEditorで挙動を確認してみましょう。

IsSoundLogEnableにチェックが入っていない状態では今まで通り、ログには何も出力されませんが

IsSoundLogEnableにチェックを入れて実行をすると、ログが出力されるようになっています。

このように、サービスの登録処理を分岐させることによって、デバッグ用の処理と・リリース用の処理を分けたりできますし、場合によってはログ出力だけするISoundServiceの実装クラスを作成して、
「まだサウンドマネージャは完成してないけれど、Soundを再生する処理は書きたい。」
と言った要望にも答えることが出来ます。便利ですね。

このサービスロケータは発展させていくと、DI(Dependency Injection:依存性注入)や、そのDIのフレームワークであるZenject(Extenject)の理解等にも一役買う技術です。
覚えておいて損は無いと思いますよ。


【Unity入門の森の最新ゲーム開発講座もお届け】

コメント

タイトルとURLをコピーしました