Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

how would be the dependency injection and reactive programming in ECS?

Discussion in 'Entity Component System' started by akbar74, Feb 27, 2019.

  1. akbar74

    akbar74

    Joined:
    Nov 16, 2017
    Posts:
    21
    hello
    i use packages like unirx and zenject in my projects for reactive programming and dependency injection.
    in ecs we will need this packages again? or it will be different?
     
    untether likes this.
  2. psuong

    psuong

    Joined:
    Jun 11, 2014
    Posts:
    118
    untether, andreiagmu and akbar74 like this.
  3. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,655
    Both DI frameworks feels fine in ECS, afaik @5argon can bright light to unirx use case in ecs (for UI if memory serves me)
     
    psuong and akbar74 like this.
  4. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,647
    Yeah I use zenject to setup my systems. I really like it.

    I can see a use for UniRx for UI as well. ECS is not a great pattern for UI in general anyway.
     
    andreiagmu and akbar74 like this.
  5. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,554
    I am using only the UniRx.Async part in my ECS game. (Nothing related to reactive programming, and not related to ECS also) It great being able to do `await` and have it resume backed by PlayerLoop system. It's like you can do coroutine anytime where it doesn't even start with IEnumerator, by telling the player loop to enumerate for you. Critical for Addressable Asset System and writing unit/integration test looks a lot better.
     
    andreiagmu and akbar74 like this.
  6. Chiv

    Chiv

    Joined:
    Mar 6, 2014
    Posts:
    10
    Is UniRx.Async different from the C# async/await (as Unity does now support it with the C# 7.3 support in 2018.3, does it not?)?

    Could you post an example for that? Your architecture is always well-thought and nicely structured, so I would very much like to see how you actually do it if it is not too much of a hassle to post of course.
     
  7. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,647
    I just have a custom world initialization that mimics Unitys except for a few custom app specifics and fixes. Instead of initializing on the spot I just bind them and initialize them at once when everything is bound.

    Binds on 149

    Code (CSharp):
    1. // <copyright file="WorldInitialization.cs" company="BovineLabs">
    2. //     Copyright (c) BovineLabs. All rights reserved.
    3. // </copyright>
    4.  
    5. namespace BovineLabs
    6. {
    7.     using System;
    8.     using System.Collections.Generic;
    9.     using System.Linq;
    10.     using JetBrains.Annotations;
    11.     using Unity.Entities;
    12.     using UnityEngine;
    13.     using Zenject;
    14.  
    15.     /// <summary>
    16.     /// The WorldInitialization.
    17.     /// </summary>
    18.     public static class WorldInitialization
    19.     {
    20.         public static World Initialize(DiContainer container, string worldName, bool editorWorld)
    21.         {
    22.             var world = new World(worldName);
    23.             World.Active = world;
    24.  
    25.             // Register hybrid injection hooks
    26.             var injectionAssembly = typeof(GameObjectArray).Assembly;
    27.             RegisterHook(injectionAssembly.GetType("Unity.Entities.GameObjectArrayInjectionHook"));
    28.             RegisterHook(injectionAssembly.GetType("Unity.Entities.TransformAccessArrayInjectionHook"));
    29.             RegisterHook(injectionAssembly.GetType("Unity.Entities.ComponentArrayInjectionHook"));
    30.  
    31.             PlayerLoopManager.RegisterDomainUnload(DomainUnloadShutdown, 10000);
    32.  
    33.             var entityManager = world.GetOrCreateManager<EntityManager>();
    34.             container.Bind<World>().FromInstance(world).AsSingle();
    35.             container.Bind<EntityManager>().FromResolveGetter<Unity.Entities.World>(w => entityManager).AsSingle();
    36.  
    37.             // Setup the World PlayerLoop
    38.             container.Bind<IInitializable>().To<PlayerLoopUpdate>().AsSingle();
    39.  
    40.             foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    41.             {
    42. #if UNITY_EDITOR
    43.                 // We don't really want to load unit test systems. not sure if this is actually an issue.
    44.                 if (assembly.FullName.Contains("Tests"))
    45.                 {
    46.                     continue;
    47.                 }
    48. #endif
    49.  
    50.                 if (!TypeManager.IsAssemblyReferencingEntities(assembly))
    51.                 {
    52.                     continue;
    53.                 }
    54.  
    55.                 try
    56.                 {
    57.                     var allTypes = assembly.GetTypes();
    58.                     CreateBehaviourManagersForMatchingTypes(container, editorWorld, allTypes, world);
    59.                 }
    60.                 catch
    61.                 {
    62.                     Debug.LogWarning($"DefaultWorldInitialization failed loading assembly: {(assembly.IsDynamic ? assembly.ToString() : assembly.Location)}");
    63.                 }
    64.             }
    65.  
    66.             return world;
    67.  
    68.             void RegisterHook(Type hook)
    69.             {
    70.                 InjectionHookSupport.RegisterHook((InjectionHook)Activator.CreateInstance(hook));
    71.             }
    72.         }
    73.  
    74.         public static void DefaultLazyEditModeInitialize()
    75.         {
    76. #if UNITY_EDITOR
    77.             if (World.Active == null)
    78.             {
    79.                 if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
    80.                 {
    81.                     // We are just gonna ignore this enter playmode reload.
    82.                     // Can't see a situation where it would be useful to create something inbetween.
    83.                     // But we really need to solve this at the root. The execution order is kind if crazy.
    84.                     if (UnityEditor.EditorApplication.isPlaying)
    85.                     {
    86.                         Debug.LogError("Loading GameObjectEntity in Playmode but there is no active World");
    87.                     }
    88.                 }
    89.                 else
    90.                 {
    91. #if UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP
    92.                     var container = new DiContainer(new[] { StaticContext.Container });
    93.                     ZenjectManagersInstaller.Install(container);
    94.  
    95.                     Initialize(container, "Editor World", true);
    96.  
    97.                     container.Resolve<InitializableManager>().Initialize();
    98. #endif
    99.                 }
    100.             }
    101. #endif
    102.         }
    103.  
    104.         private static void DomainUnloadShutdown()
    105.         {
    106.             World.DisposeAllWorlds();
    107.  
    108.             WordStorage.Instance.Dispose();
    109.             WordStorage.Instance = null;
    110.  
    111.             ScriptBehaviourUpdateOrder.UpdatePlayerLoop();
    112.         }
    113.  
    114.         private static void CreateBehaviourManagersForMatchingTypes(DiContainer container, bool editorWorld, IEnumerable<Type> allTypes, World world)
    115.         {
    116.             var systemTypes = allTypes.Where(t =>
    117.                 t.IsSubclassOf(typeof(ComponentSystemBase)) &&
    118.                 !t.IsAbstract &&
    119.                 !t.ContainsGenericParameters &&
    120.                 t.GetCustomAttributes(typeof(DisableAutoCreationAttribute), true).Length == 0 &&
    121.                 t.GetCustomAttributes(typeof(GameObjectToEntityConversionAttribute), true).Length == 0);
    122.  
    123.             foreach (var type in systemTypes)
    124.             {
    125.                 // TODO temporary, using replacements to fix a bug in p24
    126.                 if (type == typeof(Unity.Transforms.CopyTransformToGameObjectSystem) ||
    127.                     type == typeof(Unity.Transforms.CopyTransformFromGameObjectSystem) ||
    128.                     type == typeof(Unity.Transforms.CopyInitialTransformFromGameObjectSystem))
    129.                 {
    130.                     continue;
    131.                 }
    132.  
    133.                 if (editorWorld)
    134.                 {
    135.                     if (Attribute.IsDefined(type, typeof(ExecuteInEditMode)))
    136.                     {
    137.                         Debug.LogError(
    138.                             $"{type} is decorated with {typeof(ExecuteInEditMode)}. Support for this attribute will be deprecated. Please use {typeof(ExecuteAlways)} instead.");
    139.                     }
    140.                     else if (!Attribute.IsDefined(type, typeof(ExecuteAlways)))
    141.                     {
    142.                         continue;
    143.                     }
    144.                 }
    145.  
    146.                 try
    147.                 {
    148.                     // If we don't have a default constructor, we have dependencies we need to inject.
    149.                     if (type.GetConstructor(Type.EmptyTypes) == null)
    150.                     {
    151.                         container.Bind(type)
    152.                             .AsSingle()
    153.                             .OnInstantiated<ScriptBehaviourManager>((i, manager) => world.AddManager(manager))
    154.                             .NonLazy();
    155.                     }
    156.                     else
    157.                     {
    158.                         var system = world.GetOrCreateManager(type);
    159.  
    160.                         container.Bind(type)
    161.                             .FromInstance(system)
    162.                             .AsSingle()
    163.                             .NonLazy();
    164.                     }
    165.                 }
    166.                 catch (Exception e)
    167.                 {
    168.                     Debug.LogException(e);
    169.                 }
    170.             }
    171.         }
    172.  
    173.         /// <summary>
    174.         /// Update the player loop after setup and dispose worlds.
    175.         /// </summary>
    176.         [UsedImplicitly]
    177.         private class PlayerLoopUpdate : IInitializable
    178.         {
    179.             private readonly World world;
    180.  
    181.             /// <summary>
    182.             /// Initializes a new instance of the <see cref="PlayerLoopUpdate"/> class.
    183.             /// </summary>
    184.             /// <param name="world">The world to manage.</param>
    185.             public PlayerLoopUpdate(World world)
    186.             {
    187.                 this.world = world;
    188.             }
    189.  
    190.             /// <inheritdoc/>
    191.             void IInitializable.Initialize()
    192.             {
    193.                 ScriptBehaviourUpdateOrder.UpdatePlayerLoop(this.world);
    194.             }
    195.         }
    196.     }
    197. }
    And my actual installer is just a collection of installers that bind and setup various settings

    Code (CSharp):
    1.      
    2.     public class ApplicationInstaller : ScriptableObjectInstaller
    3.     {
    4.         // ...
    5.  
    6.         /// <inheritdoc/>
    7.         public override void InstallBindings()
    8.         {
    9.             var world = WorldInitialization.Initialize(this.Container, "Default World", false);
    10.             this.LoadSceneData(world);
    11.  
    12.             this.Container.Bind<Camera>()
    13.                 .FromComponentInNewPrefab(this.mainCameraPrefab)
    14.                 .UnderTransformGroup("[Cameras]")
    15.                 .AsSingle()
    16.                 .NonLazy();
    17.  
    18.             this.InstallInstallers();
    19.         }
    20.  
    21.         private void InstallInstallers()
    22.         {
    23.             EntityInstaller.Install(this.Container, this.blueprints);
    24.             TerrainInstaller.Install(this.Container, this.terrain);
    25.             FogOfWarInstaller.Install(this.Container, this.fogOfWar);
    26.             // ... etc
     
    untether, Chiv and Micz84 like this.
  8. Chiv

    Chiv

    Joined:
    Mar 6, 2014
    Posts:
    10
    Thank you for posting!
    So if I understand it correctly, you use this for systems which request (via the constructor) dependencies for functionality outside of ECS (or else you would use
    GetOrCreateManager<Type>()
    , if e.g. a system would like to get another)?
    Could you provide some examples for what kinda dependencies Systems might request?
     
  9. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,647
    In bed about to sleep but I can post some examples tomorrow, but mostly I just inject settings, factories, etc. None of my systems reference another system as I try to keep them decoupled.

    I just prefer to use injection to initialize. Makes it very easy to change settings, for instance I could inject different settings per world. Also let's me easily share settings between similar systems without being coupled to a static class. Finally makes unit testing extremely easy.

    You could just initialize the systems yourself with createsystem but I prefer the automation of a di library.
     
    Chiv likes this.
  10. Chiv

    Chiv

    Joined:
    Mar 6, 2014
    Posts:
    10
    Ha, Time Zones. I am on lunch break here.
    Thanks for your insights even so late in the night!
     
  11. kork

    kork

    Joined:
    Jul 14, 2009
    Posts:
    280
    I wrote a very simple DI layer for this which uses constructor injection. All my systems declare their dependencies in the constructor like this:

    Code (CSharp):
    1.  
    2.     [DisableAutoCreation] // System will not be auto-created by Unity
    3.     [DependencyComponent]
    4.     [UpdateAfter(typeof(DetachVisualSystem))]
    5.     public class AttachVisualSystem : JobComponentSystem
    6.     {
    7.         private readonly Prefabs _prefabs;
    8.         private readonly EndFrameBarrier _barrier;
    9.  
    10.         public AttachVisualSystem(Prefabs prefabs, EndFrameBarrier barrier) // dependencies
    11.         {
    12.             _prefabs = prefabs;
    13.             _barrier = barrier;
    14.         }
    15. ...
    16. }
    This helps with unit testing because you cannot forget to give a dependency into your system. All systems are annotated with
    DisableAutoCreation
    so Unity will not auto-create them. At startup I scan the assembly for things annotated with
    DependencyComponent
    and use the constructor arguments to find the dependencies. Then everything is resolved automatically. Under the hood the systems are created using this:

    Code (CSharp):
    1.  
    2.        public object CreateSystem(Type type, object[] constructorParams)
    3.         {
    4.             return _world.CreateManager(type, constructorParams);
    5.         }
    6.  
    The DI layer provides the constructor parameters. Finally I call
    ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World.Active)
    to make sure the systems are in the correct order.

    This way i can add new systems and dependencies with minimal effort - just write a constuctor and everything will be given to you. This also allows me to inject other systems (like EndOfFrameBarrier) into systems. The bootstrap is very simple then, create a dependency context and scan for components.

    Code (CSharp):
    1.      _dependencyContext = new DependencyContext();
    2.          
    3.             // Factories allow you to modify the way how dependencies are created,
    4.             // by default it's simply calling the constructor, but you can also declare a factory
    5.             // for certain types.
    6.             // This is a special factory that uses the World.CreateManager to build the
    7.             // actual object
    8.             _dependencyContext.DeclareFactory<ScriptBehaviourManager>(new SystemFactory(World.Active).CreateSystem);
    9.        
    10.             // Another factory that picks up objects from the current scene
    11.             // instead of creating them with "new".
    12.             _dependencyContext.DeclareFactory<MonoBehaviour>(new MonoBehaviourFactory().LoadMonoBehaviourFromScene);
    13.  
    14.             // with Declare<Whatever> you can manually declare dependencies
    15.             // provide the entity manager to non-system classes
    16.             _dependencyContext.Declare(World.Active.GetOrCreateManager<EntityManager>());
    17.        
    18.             // provide the built-in end frame barrier
    19.             _dependencyContext.Declare(World.Active.GetOrCreateManager<EndFrameBarrier>());
    20. y);
    21.  
    22.              // this finds anything annotated with [DependencyComponent]
    23.              // and auto-adds it to the context.
    24.             _dependencyContext.ScanForComponents(_ => true);
    25.              // and this does the work of resolving every dependency
    26.              // if a dependency is unsatisfied this will throw an exception
    27.             _dependencyContext.Resolve();
    28.  
    I try to keep dependencies to a minimum as it keeps stuff decoupled. Typical objects I inject are the barrier for
    JobComponentSystems
    , several registry-like objects that contain settings, access to prefabs and resources and various caches that I need for faster lookup of entities (e.g. spatial lookup tables).
     
    Last edited: Feb 28, 2019
    untether and learc83 like this.