【Unity2018】Entity Component System(ECS)でZenjectを使う
突然ですが、皆さんECSでもDIしたいしRxしたいですよね?? ということでECSでもZenjectを導入してみました。
導入
いつもどおりSceneContextとSceneInstallerを作り、シーンに置きます。 Odinを入れてるのでインスペクタの見た目がちょっと違います!
各ManagerへのInjectとInstall
通常のMonoInstallerではSceneに存在する全てのMonoBehaviourに自動でInjectしてくれますが、ECSにそんなものはないので、手でせこせこInjectしていきます。
World.Active.BehaviourManagers
で全てのマネージャーが取得できるので、Container.Bind
とContainer.Inject
でそれぞれを関連付けてしまいます。
「相互依存が深いのは嫌だ!!」みたいな人はプロジェクトに合わせてよしなに書き換えてあげてください。
using Unity.Entities; using Zenject; namespace Game.System { public class SceneInstaller : MonoInstaller<SceneInstaller> { [Zenject.Inject] RendererSettings rendererSettings; public override void InstallBindings() { var world = World.Active; foreach (var scriptBehaviourManager in world.BehaviourManagers) { Container.Bind(scriptBehaviourManager.GetType()).FromInstance(scriptBehaviourManager); } foreach (var scriptBehaviourManager in world.BehaviourManagers) { Container.Inject(scriptBehaviourManager); } } } }
Playerコンポーネント
サンプルとして、ここではスティックで動かすことのできるPlayerコンポーネントを作ってみます。 生成処理はどこかにArchetypeを持っておいてCreateEntityするのが普通だと思いますが、staticメンバを使いたくないのでFactoryを作ってInjectできるようにしていきます。
using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; namespace Game.Component { [SerializeField] public struct Player : IComponentData { public static void UpdatePosition(ref Position position, float2 stick, float deltaTime) { position.Value += new float3(stick * deltaTime * 10.0f, 0); } } }
using Game.Component; using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; namespace Game.System { public class PlayerSystem : ComponentSystem { struct Data { public readonly int Length; public ComponentDataArray<Player> Player; public ComponentDataArray<Position> Position; } [Inject] Data data; protected override void OnUpdate() { for (var i = 0; i < data.Length; ++i) { var position = data.Position[i]; var stick = new float2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")); Player.UpdatePosition(ref position, stick, Time.deltaTime); data.Position[i] = position; } } } }
MeshInstanceRendererをSettingsInstallerで注入してみる
MeshInstanceRendererを持ったScriptableObjectInstaller
を作り、Container.BindInstancing
で注入します。
Factoryではこれを受け取り、EntityをInstantiateするときに利用します。
using System; using Unity.Rendering; using UnityEngine; namespace Game.System { [Serializable] public class RendererSettings { [SerializeField] MeshInstanceRenderer player; public MeshInstanceRenderer Player => player; } }
using UnityEngine; using Zenject; namespace Game.System { [CreateAssetMenu(fileName = "SettingsInstaller", menuName = "Installers/SettingsInstaller")] public class SettingsInstaller : ScriptableObjectInstaller<SettingsInstaller> { [SerializeField] RendererSettings rendererSettings; public override void InstallBindings() { Container.BindInstance(rendererSettings); } } }
PlayerFactoryを作る
続いて、Playerを生成するPlayerFactoryを作成します。 コンストラクタインジェクションでEntitymanagerとMeshInstancedRendererを受け取り、それを活用してInstanceを生成するCreateメソッドを持ちます。
using Game.Component; using Unity.Entities; using Unity.Rendering; using Unity.Transforms; namespace Game.System { public class PlayerFactory { readonly EntityArchetype archetype; readonly EntityManager entityManager; readonly MeshInstanceRenderer meshInstanceRenderer; PlayerFactory(EntityManager entityManager, MeshInstanceRenderer meshInstanceRenderer) { this.entityManager = entityManager; this.meshInstanceRenderer = meshInstanceRenderer; archetype = entityManager.CreateArchetype(typeof(Player), typeof(Position)); } public Entity Create() { var entity = entityManager.CreateEntity(archetype); entityManager.AddSharedComponentData(entity, meshInstanceRenderer); return entity; } } }
SceneInstallerでバインドする
SceneInstallerにPlayerFactoryをバインドし、WithArgumentsを使ってMeshInstancedRendererを渡してあげます。
using Unity.Entities; using Zenject; namespace Game.System { public class SceneInstaller : MonoInstaller<SceneInstaller> { [Zenject.Inject] RendererSettings rendererSettings; public override void InstallBindings() { var world = World.Active; foreach (var scriptBehaviourManager in world.BehaviourManagers) { Container.Bind(scriptBehaviourManager.GetType()).FromInstance(scriptBehaviourManager); } foreach (var scriptBehaviourManager in world.BehaviourManagers) { Container.Inject(scriptBehaviourManager); } Container.Bind<PlayerFactory>().AsSingle().WithArguments(rendererSettings.Player); } } }
使ってみる
自分はAutoRunのチェックを外しておき、好きなタイミングでInjectするのが好みです。 InjectされたPlayerFactoryを使い、PlayerEntityを生成します。
using UnityEngine; using Zenject; namespace Game.System { public class GameBootstrap : MonoBehaviour { [SerializeField] SceneContext sceneContext; [Inject] PlayerFactory playerFactory; void Awake() { sceneContext.Run(); playerFactory.Create(); } } }
動きました!
まとめ
ECSでもZenjectを活用することで疎結合に実装を進めていくことが出来そうな雰囲気を感じました。 デメリットはInject属性がコンフリクトしてすっちゃかめっちゃかになることです。 ご活用下さい!