Unityで別オブジェクトの情報を取得したり操作する上で、最も簡単な方法はInspectorに露出して参照先をセットすることです。
ただ、このアプローチはシーンを分割する事になると厄介です。UnityのSceneは別のSceneのオブジェクトに対してアクセス出来ないので、別シーン上に存在するオブジェクトはスクリプトから解決する必要が出てきます。
例えば下のような、ステージ・メインキャラクター・キャラクターを追跡するNPCといったシーンに分割した場合、NPC(Yuko)はメインキャラクター(Unitychan)を直接参照出来ないため、スクリプトでメインキャラクターを探す必要が出てきます。
それは正直面倒くさいので、今回は可能な限りスクリプトを利用せず、かつ汎用的に、他のシーンのコンポーネントを取得する方法を考えてみます。
上手く行けば、下のGifアニメのように逐次追加しても参照関係を保持してくれます。
目次
実際にやってみる
アプローチの内容を詳しく紹介する前に、簡単な物を作ってみます。
今回期待する動作は、以下の通り
- カメラがプレイヤーを追跡する
- カメラとプレイヤーは別シーンにある
カメラはCameraシーン、プレイヤーは2Dシーン
今回はCameraがキャラクターを追跡する機能を考えてみます。
最初の(シーン分割前)はこんな感じのコードを使用しました。
事前準備
事前準備として、以下のコードをプロジェクトに登録します。
シーン内のオブジェクトをシーン外に公開する為の受け皿を用意する
2Dシーン内にあるPlayerオブジェクトを外部に公開するための受け皿となるScriptableObjectを用意します。
今回のアプローチで最もコードを書く部分は多分コレです。
必要なのはPlayerのみなので、PlayerのTransformのみ定義します。フィールドではなくプロパティを利用する点に注意です。
コードは2Dシーン専用なので、2Dシーンのフォルダにでも置いておくと良いです。
後はAssets > Create > Expose Scene Propretyで2Dを選択して、受け皿となるScriptableObjectを作ります。
シーン内オブジェクトをScriptableObjectに登録する
次にScriptableObjectにシーン内のオブジェクトを登録します。
- 2Dシーンに新しいGameObjectを作成し、Registerコンポーネントを登録
- RegisterのOnAwakeイベントを増やして、先ほど作成したScriptableObjectを登録
- イベントはTransform.Playerを呼び出すように設定し、2Dシーン内のPlayerを登録
これで2Dシーンがロードされると、ScriptableObjectのTransform playerの中に2DシーンのPlayerオブジェクトが注入されます。
別シーンのプレイヤーを追跡するカメラを作る
最後にカメラが追跡する対象を、ScriptableObject内にあるTransform playerから取得するようにコードを変更します。
後はCamera側にもExpose 2D SceneのScriptableObjectを登録します。
これで2DとCameraシーンが揃った時、Main CameraのFollow Camera 2Dは別シーンのPlayerオブジェクトを参照した状態になります。
少し別のアイディア
やり方を紹介しましたが、幾つかのアイディアや注意すべき点があります。
ScriptableObjectの登録が面倒くさい場合
呼び出し元のオブジェクトが面倒くさい場合、スクリプトに元々登録しておけば初期値としてセットしてくれます。
AssetBundleに格納する場合の注意
このアイディアはAssetBundleで利用した場合の問題回避です。
今回2DとCameraの二つのシーンを要した訳ですが、この二つを別々のAssetBundleに格納する場合、元々は同じオブジェクトだが違うオブジェクトとして生成されます。
この問題はScriptableObjectをSceneを登録したAssetBundleとは別に登録しておき、SceneのAssetBundleを呼び出す前にロードしておけば解決します。
公開する型を決め打ちして汎用的に使えるようにする
このアイディアは、シーン毎に公開するデータを定義したScriptableObjectを用意するのではなく、汎用的な物を作ってしまえ!というアイディアです。
今回は「2Dシーンの内容を公開する」という形でExpose2DSceneなクラスと、公開するTransform playerプロパティを使用しましたが、これ実際には型だけで作っても良いかもしれません。
例えばTransformを公開するScriptableObjectを沢山用意すれば、少なくともTransformを公開する上で追加のコードは必要ありません。
ScriptableObject側に動きを定義する
このアイディアは、シーン上のオブジェクトは基本的に登録するだけで、動作はScriptableObject側でする…というアイディアです。
上のアイディアは「Sceneのオブジェクトの露出」が前提になりますが、処理も全部含めた場合は「全シーンから参照可能なGameManager」という立ち位置になります。
ScriptableObjectを経由して◯◯◯Managerとかにアクセスするなら、ScriptableObject自体を◯◯◯Managerにしてしまえ!
ただこのアプローチを行う場合、データをストックして処理を決定するScriptableObjectと、オブジェクトを接続するScriptableObjectは別にしておいた方が良いです。
これは設計が面倒云々の話もありますが、破棄のタイミングが主な理由です。
またオブジェクトの登録(優先度100のAwake)よりScriptableObjectのOnEnableの方が先に呼ばれる点には注意が必要です。例えばUniRX等でストリームを云々する場合は、遅延実行するなりシーン上のオブジェクトから呼び出すなり。
参照先のシーンがロード前の場合
参照先のシーンがロード前の場合、ScriptableObjectは参照できますが中身がありません。この場合、ロード完了まで呼び出し元を遅延してしまう事アイディアがあります。
もしGameManagerのように扱う場合、入力内容をキャッシュしておき、接続時に反映する…等も良いかもしれません。
動作の異なるクラスを渡したい場合
ベースクラスを用意します。
これはUnityEventがインターフェースを呼び出せない為です。UnityEventを使わないのならばインターフェースも使用できますが、その場合はオブジェクトを登録するスクリプトを用意する必要があります。
感想
このアプローチの良いと思う部分はFindやStaticと異なり、特別なタグやクラスを必要とせずアクセスを解決出来る点です。またFind系よりも生成順で問題になりにくいのも好きなポイントです。
最終的に全部揃ってれば動く…という緩さが中々。
後は1P 2Pのような接続先を、割と楽に複数持つことが出来る点とか、オブジェクトの登録に特別なコードが要らない点とか。
インターフェースが使えれば完璧だった。
関連する内容
今回の記事は、この内容を前提に書いています。
オブジェクトへの参照を流し込むフレームワークです。
これで「会話シーン」のような物は、会話ウィンドウ用のシーンを呼び出して中身を設定しておけば勝手になんとかしてくれる。