はじめに
この記事は CyberAgent Developers #2 Advent Calendar 2018 の19日目の記事です。
昨日は med-orange さんの C# 7.2: in パラメータ修飾子 でした。
どうも!株式会社サムザップで Unity/C# エンジニアをしている二宮です。
リンクスリングスのバトル部分で、サーバとクライアントの両方を担当しています。
リンクスリングスでは、Zenjectという、Unity用に設計されたDIフレームワークを利用しています。
このZenjectの挙動の中でどうやって実現しているのか気になるところがあったので、今回はそこを掘り下げていこうと思います。
Zenjectでは何ができるのか
まず、軽くZenjectの機能に触れておきますね。
Zenjectは、
- コンテナにBindして、
- それをフィールドやメソッド、コンストラクタなどにInjectすることができる
シンプルなフレームワークになります。
例えば、下記のFooInstallerとFooTestの両方をシーンに配置して、UnityをPlayすると、FooTestのAwake時には、_foo変数へのInjectが済んでいる状態になります。
class FooInstaller : MonoInstaller<FooInstaller>
{
public override void InstallBindings()
{
Container.Bind<Foo>().AsSingle();
}
}
class FooTest : MonoBehaviour
{
[Inject] Foo _foo;
// Awake時にはFooが代入されている状態になる
}
さらに、シーン配置オブジェクトだけにとどまらず、Prefabからの動的生成時も可能です。
GameObject gameObject = Container.InstantiatePrefab(MyPrefab);
疑問
Attributeつけるだけで、事前に設定したインスタンスをInjectしてくれるなんて、なんて便利なんだろう、、!
しかもAwake時点でInjectされた変数を使えるとか素敵、、、!
ん???
「あれ、AwakeってInstantiate時に呼ばれていなかったっけ、、、?この代入処理っていつしてんの、、、?」
var instance = UnityEngine.Object.Instantiate(_prefab);
// この時点でAwakeが呼ばれていたような、、、?
というわけで、題名の通り、なんでAwakeより先にInjectできるのかが気になりました。
調べてみる
さて、調べてみました。
知ってしまえばすごく単純です。
最初に紹介した2つのパターンは別々の方法で実現されています。
シーン配置オブジェクトのAwakeが呼ばれる前にInjectできる理由
これは、イメージ通りというか、すごく単純でした。
UnityのScript Execution Order設定を利用します。
同時に生成されたコンポーネントの実行順を定める設定ですね。
そうするとシーンロードやEditorPlay時には、こんな流れで実行されます。
- Script Execution Orderにより、MonoInstallerのAwakeが最速で呼ばれる
- (Awake内処理) Bind処理を実行
- (Awake内処理) 全てのコンポーネントを取得し、Inject Attributeを探索
- (Awake内処理) Inject対象にインスタンスをInject
- Inject済みコンポーネントのAwakeが呼ばれる
これで、Awakeよりも先にInjectが済んでいるんですね、なるほどなるほど。
Prefabから動的に生成したObjectのAwakeが呼ばれる前にInjectできる理由
さて、Prefabから生成する場合にはどうでしょう。
当然、ExecutionOrderのパターンは使えません。
何しろ、
UnityEngine.Object.Instantiate(_prefab)
という関数を通って次の行に処理が移るときには既にAwakeが呼ばれてしまっています。
さて、Zenjectではどうやって解決しているでしょうか。
で、コードを読んでいったところ、AwakeはgameObjectが非アクティブであれば呼ばれないという性質を利用しているようでした。
つまり、とっても簡略化するとContainer.InstantiatePrefab(MyPrefab)
はこんな感じ。
var isActive = prefab.gameObject.activeSelf; // prefabのActive状態を保持
prefab.gameObject.SetActive(false); // prefabを非アクティブ化
var instance = UnityEngine.Object.Instantiate(prefab); // 非アクティブなインスタンスを生成
// ここでInject処理
instance.SetActive(isActive); // アクティブ状態をprefabの状態に戻す
return instance;
終わりに
最終的に、Unityコールバッククイズみたいになってしまいましたが、面白かったでしょうか。
「gameObjectが非アクティブだとAwakeを待てる」というのは、覚えておくと結構いろんな処理で使えるかもしれないですね。