【Unity2018】Entity Component System(ECS)でZenjectを使う

突然ですが、皆さんECSでもDIしたいしRxしたいですよね?? ということでECSでもZenjectを導入してみました。

導入

いつもどおりSceneContextとSceneInstallerを作り、シーンに置きます。 Odinを入れてるのでインスペクタの見た目がちょっと違います!

f:id:notargs:20180920035609p:plain

各ManagerへのInjectとInstall

通常のMonoInstallerではSceneに存在する全てのMonoBehaviourに自動でInjectしてくれますが、ECSにそんなものはないので、手でせこせこInjectしていきます。 World.Active.BehaviourManagersで全てのマネージャーが取得できるので、Container.BindContainer.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);
        }
    }
}

f:id:notargs:20180920035131p:plain

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を生成します。

f:id:notargs:20180920035153p:plain

using UnityEngine;
using Zenject;

namespace Game.System
{   
    public class GameBootstrap : MonoBehaviour
    {
        [SerializeField] SceneContext sceneContext;
        [Inject] PlayerFactory playerFactory;
        
        void Awake()
        {
            sceneContext.Run();
            playerFactory.Create();
        }
    }
}

動きました!

f:id:notargs:20180920035205g:plain

まとめ

ECSでもZenjectを活用することで疎結合に実装を進めていくことが出来そうな雰囲気を感じました。 デメリットはInject属性がコンフリクトしてすっちゃかめっちゃかになることです。 ご活用下さい!