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

Init(args) - The practical DI framework

Discussion in 'Assets and Asset Store' started by SisusCo, Dec 21, 2021.

  1. AlanHero

    AlanHero

    Joined:
    Feb 7, 2021
    Posts:
    4
    Hi @SisusCo I am facing a problem that I want to create global services from MonoBenaviour from my mainScene. I don't understand what approach should be used here. In addition, I do not understand why dependencies are not injected via IInitializable to other services. What I'm doing wrong?
     
  2. AlanHero

    AlanHero

    Joined:
    Feb 7, 2021
    Posts:
    4
    I have already tested, create Service A using Service attribute, and implemented IInitializable with dependency of Service B, The Service B is defined in scene using tag or services component. But it didnt't work: In Service Debugger, there are not Service B, and Service A didn't received the instance of service B
     
  3. AlanHero

    AlanHero

    Joined:
    Feb 7, 2021
    Posts:
    4
    The main reason that I want to define service from scene MonoBehaviour is that convinient to get other dependencies as we do as usual MonoBehaviour component.
     
  4. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    Hey @AlanHero ! I'll try to clarify how exactly this can be achieved with Init(args).

    Firstly, to register scene object A as a global service before the first scene is loaded, you need these two things:
    1. The service component must be added to the first scene in build settings. When testing in the editor, you also need to start the game from this initial scene.
    2. The component class must have the Service attribute with FindFromScene set to true.
    Code (CSharp):
    1. using Sisus.Init;
    2. using UnityEngine;
    3.  
    4. [Service(FindFromScene = true)]
    5. public class A : MonoBehaviour { }
    And then, to have component B receive an instance of service B, you can do one of the following:


    Option 1: Also add the Service attribute to B with FindFromScene set to true.
    Code (CSharp):
    1. using Sisus.Init;
    2. using UnityEngine;
    3.  
    4. [Service(FindFromScene = true)]
    5. public class B : MonoBehaviour, IInitializable<A>
    6. {
    7.     public void Init(A a) => Debug.Log($"B received {a.GetType().Name}");
    8. }
    When you do this, Init(args) will automatically initialize service A first, and only then service B, because it knows that B depends on A. Both services will be initialized and ready to be used by any clients before the first scene is loaded.


    Option 2: Attach an Initializer component to B
    Generate Initializer.png
    When you do this, then the Initializer will handle fetching the service A and passing it to B, before B's Awake function runs.


    Option 3: Make B derive from MonoBehaviour<A>
    Code (CSharp):
    1. using Sisus.Init;
    2. using UnityEngine;
    3.  
    4. public class B : MonoBehaviour<A>
    5. {
    6.     protected override void Init(A a) => Debug.Log($"B received {a.GetType().Name}");
    7. }
    When you do this, the MonoBehaviour<T> base class will handle fetching the service and injecting it to the Init function at the beginning of the component's Awake event function.


    At the moment just adding the Service tag to a component won't cause it to receive other services automatically. Now that I think about it, it would actually be possible to do it automatically at this point as well... I might perhaps implement support for this in a future update, so that service-to-service injection is guaranteed to occur even if none of the above prerequisites are met.
     
  5. PanthenEye

    PanthenEye

    Joined:
    Oct 14, 2013
    Posts:
    1,790
    Have you tried preserving the whole assembly?
     
  6. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    I did try adding that as well to the main assemblies, to no avail - unfortunately that only helps to preserve all types in the assembly, but not the members within them.

    I might spend some more time looking into this some thing later; it'd be interesting to figure out what kinds of things Unity is omitting when those higher stripping levels are used. I don't think I've ever been able to use the highest values on any of the projects I've tried it on in the past, something has always broken.
     
    PanthenEye likes this.
  7. picpic1979

    picpic1979

    Joined:
    Feb 11, 2015
    Posts:
    6
    Hello, i just test initargs for check if it is suit for my project, i already could make somes working tests, but in fact i have error when i try to create an initializer for a very simple wrapped object. i have this error in console.

    1
    Assets\Scripts\MyTestWapperObjectComponentInitializer.cs(8,24): error CS0311: The type 'MyGame.MyTestWapperObjectComponent' cannot be used as type parameter 'TWrapper' in the generic type or method 'WrapperInitializer<TWrapper, TWrapped, TArgument>'. There is no implicit reference conversion from 'MyGame.MyTestWapperObjectComponent' to 'Sisus.Init.Wrapper<MyGame.MyTestWapperObjectComponent>'.

    2
    Assets\Scripts\MyTestWapperObjectComponentInitializer.cs(8,24): error CS0534: 'MyTestWapperObjectComponentInitializer' does not implement inherited abstract member 'WrapperInitializerBase<MyTestWapperObjectComponent, MyTestWapperObjectComponent, MyTestWapperObject>.CreateWrappedObject(MyTestWapperObject)'


    this is my object
    Code (CSharp):
    1. using UnityEngine;
    2. using Sisus.Init;
    3.  
    4. namespace MyGame
    5. {
    6.     public sealed class MyTestWapperObject : IUpdate
    7.     {
    8.  
    9.         public MyTestWapperObject()
    10.         {
    11.  
    12.         }
    13.  
    14.         public void Update(float deltaTime)
    15.         {
    16.             Debug.Log("hello");
    17.         }
    18.     }
    19. }
    the wrapper part
    Code (CSharp):
    1. using UnityEngine;
    2. using Sisus.Init;
    3.  
    4. namespace MyGame
    5. {
    6.     /// <summary>
    7.     /// <see cref="Wrapper{}"/> for the <see cref="MyTestWapperObject"/> object.
    8.     /// </summary>
    9.     [AddComponentMenu("Wrapper/My Test Wapper Object")]
    10.     internal sealed class MyTestWapperObjectComponent : Wrapper<MyTestWapperObject> { }
    11. }
    12.  
    finally the error in the initializer missing the implementation
    Code (CSharp):
    1. using Sisus.Init;
    2.  
    3. namespace MyGame
    4. {
    5.     /// <summary>
    6.     /// Initializer for the <see cref="MyTestWapperObjectComponent"/> wrapped object.
    7.     /// </summary>
    8.     internal sealed class MyTestWapperObjectComponentInitializer : WrapperInitializer<MyTestWapperObjectComponent, MyTestWapperObjectComponent, MyTestWapperObject>
    9.     {
    10.         #if UNITY_EDITOR
    11.         /// <summary>
    12.         /// This section can be used to customize how the Init arguments will be drawn in the Inspector.
    13.         /// <para>
    14.         /// The Init argument names shown in the Inspector will match the names of members defined inside this section.
    15.         /// </para>
    16.         /// <para>
    17.         /// Any PropertyAttributes attached to these members will also affect the Init arguments in the Inspector.
    18.         /// </para>
    19.         /// </summary>
    20.         private sealed class Init
    21.         {
    22.             #pragma warning disable CS0649
    23.             public MyTestWapperObject MyTestWapperObject;
    24.             #pragma warning restore CS0649
    25.         }
    26.         #endif
    27.     }
    28. }
    29.  
    I should do something wrong but what ? Thanks
     
  8. picpic1979

    picpic1979

    Joined:
    Feb 11, 2015
    Posts:
    6
    i just rerereread the docs and i think i couldn't use the automatic initializer for a wrapped object and that is the error...
    I have to write it by hand and make my class serializable that right ?
     
  9. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    @picpic1979
    Oh, it looks like the script generation template for the wrapper initializer is a bit off o_O

    The first generic type is supposed to be the wrapper component, the second the wrapped class, and the rest the types of the constructor parameters on the wrapped class.

    Like this:
    Code (CSharp):
    1. public class Greeter : IUpdate
    2. {
    3.     private readonly string text;
    4.  
    5.     public Greeter(string text) => this.text = text;
    6.  
    7.     public void Update(float deltaTime) => Debug.Log(text);
    8. }
    9.  
    10. class GreeterComponent : Wrapper<Greeter> { }
    11.  
    12. class GreeterInitializer : WrapperInitializer<GreeterComponent, Greeter, string>
    13. {
    14.     protected override Greeter CreateWrappedObject(string text) => new Greeter(text);
    15. }
    You are still supposed to manually implement the CreateWrappedObject method. The script generator isn't smart enough at this time to examine the constructors on the wrapped class and generate the code for passing the initialization arguments to one of them, or anything like that. Adding support for that could be pretty nice though - at least when there's only one constructor with parameters on the class.

    In any case, I'll fix the issue with the script generation getting the generic arguments wrong for the next release at least.
     
    Last edited: Aug 31, 2023
  10. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    If you use an initializer, then the wrapped class does not need to be serializable. In this case the initializer will hold all the initialization arguments and handle serializing them for you. The wrapped instance will be created at runtime inside the Initializer's CreateWrappedObject method, where you can also pass it all of its dependencies. When using this method you can make use of constructor injection, read-only fields and properties and all that good stuff :)

    If you do not use an initializer, then the class does need to be serializable though, or otherwise Unity's serializer won't automatically create the instance for you for any components existing in scenes or prefabs.
    Code (CSharp):
    1. [Serializable]
    2. public class Greeter : IUpdate
    3. {
    4.     [SerializeField]
    5.     private string text;
    6.  
    7.     public Greeter(string text) => this.text = text;
    8.     public void Update(float deltaTime) => Debug.Log(text);
    9. }
    If you use
    gameObject.AddComponent<GreeterComponent, Greeter>(new Greeter("Hello, World!"));
    then the class doesn't need to be serializable either, as you'll be handling the creation of the instance and passing it to the AddComponent method.
     
  11. picpic1979

    picpic1979

    Joined:
    Feb 11, 2015
    Posts:
    6
    Thanks it IS more clear i will check it tomorow....
     
    SisusCo likes this.
  12. trueh

    trueh

    Joined:
    Nov 14, 2013
    Posts:
    74
    Hi!

    Nice asset. I'm integrating it in my game and I have noticed that Init() is not called in MonoBehaviours before OnValidate() which is causing me some headaches. Would it be possible to have a way of doing it just like it's done with OnReset()? I mean, implementing OnValidate() in MonoBehaviour<> and let it call Init() if an InitOnValidate attribute is present in the class.

    The reason why I'm needing it is because I have some components updating the UI controls in the editor when some of their values are changed.

    Thanks in advance.
     
  13. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    Hi @trueh !

    That's an interesting idea. Unfortunately it is not as easily achievable. The problem is that OnValidate is not guaranteed to be called on the main thread, which means that it is not at all safe to use methods like GetComponent during the event.

    In theory it would be possible to get an effect like this though, if the dependency resolution was delayed to happen after a delay on the main thread. Something like this:
    Code (CSharp):
    1. private void OnValidate()
    2. {
    3.     if(GetType().GetCustomAttribute<InitOnValidateAttribute>() is null || !Thread.CurrentThread.IsBackground)
    4.     {
    5.         OnValidated();
    6.         return;
    7.     }
    8.  
    9.     // wait to execute on the main thread
    10.     EditorApplication.delayCall += ()=>
    11.     {
    12.         if(this == null)
    13.         {
    14.             return;
    15.         }
    16.  
    17.         if(InitArgs.TryGet(Context.OnValidate, this, out TArgument argument))
    18.         {
    19.             Init(argument);
    20.         }
    21.  
    22.         OnValidated();
    23.     };
    24. }
    25.  
    26. protected virtual void OnValidated() { }
     
  14. trueh

    trueh

    Joined:
    Nov 14, 2013
    Posts:
    74
    Thank you. That's very similar to the code I have without using InitArgs. I always update the controls on a delayCall while in OnValidate(). Still, it wouldn't work with InitArgs as the code is not considering Context.OnValidate in TryGet<>(). To make it work I had to update all TryGet<>() methods in this way:
    Code (CSharp):
    1.  
    2. #if UNITY_EDITOR
    3. if((context == Context.Reset [b]|| context == Context.OnValidate)[/b] && TryPrepareArgumentsForAutoInit<TClient, TFirstArgument, TSecondArgument, TThirdArgument>(client))
    4. {
    5.     firstArgument = GetAutoInitArgument<TClient, TFirstArgument>(client, 0);
    6.     secondArgument = GetAutoInitArgument<TClient, TSecondArgument>(client, 1);
    7.     thirdArgument = GetAutoInitArgument<TClient, TThirdArgument>(client, 2);
    8.     return true;
    9. }
    10. #endif
    11.  
    Honestly, this issue is not going to prevent me from using InitArgs, but it is a little bit annoying to change a text in a component and don't see it updated in the editor. Probably this is critical for UI developers only. I can try to avoid using InitArgs in these components.

    Regards.
     
  15. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    Oh yeah, Context.MainThread should actually have been used there instead of Context.OnValidate, since the execution has been delayed to happen on the main thread. When using Context.OnValidate calling of non-thread safe Unity API methods is avoided.

    Ah, so the desired optimal behaviour would be that if component A is modified, then all clients that depend on A would have their Init method executed again. That does make sense. Especially when A is initially attached to a game object, it would be convenient if all clients that depend on it would get initialized.

    It should be possible to put something like this together using ObjectChangeEvents.changesPublished. I can do some investigating into this.
     
    trueh likes this.
  16. Voxel-Busters

    Voxel-Busters

    Joined:
    Feb 25, 2015
    Posts:
    1,858
    @SisusCo Hey! The product looks very nice and usable for my current project.
    Do you have any limitations/issues currently that I need to be aware?

    I have few quick questions now
    1. I personally hate singletons. What if I want to use more instances of the same service?
    2. Any configuration/settings inspector to define/see what services exist/active in the current context (across all opened scenes)?

    Regarding #1, here is an example
    Consider I have MovementInputManager which is a service. I want to create two MovementInputManager services one for the Player and another for Player Companion. These two services are instantiated (init) with two different key mappings (Scriptable object) (one with ASDW and other Arrow keys for movement). How to do this?
     
    Last edited: Nov 11, 2023 at 5:16 AM
  17. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    Hey @Voxel-Busters ! One known limitation with the project at the moment is that if Managed Stripping Level is raised to Medium or High, then the [Service] attribute based global service registration and automatic injection to clients will stop working. It's something I'm still investigating, so hopefully I'll be able to resolve this in a future update.

    Init(args) doesn't have the concept of singletons, in the sense that it would restrict there to only ever be a single instance of a class. Instead, it allows you to specify that a particular instance should be used by clients globally by default, while still always allowing you override that default service on a case-by-case basis.

    To override a service for an individual component, you can just drag-and-drop another instance via the Inspector.

    To override a service for all components within a particular scene or transform hierarchy, you can use the Services component, or register a component as a service from its context menu.

    hierarchy-specific-services.gif

    There is a simple Service Debugger window, that can be opened under
    Analysis > Service Debugger
    , which lists all currently active global level services.

    You can also see non-serialized fields of clients visualized in play mode, helping you verify that all the expected services were received in practice at runtime.
    service-debugger.png

    You can also click the
    Service
    tag on initializers to locate the service that will be injected at runtime.
    click-to-ping-service.gif

    And you can also do the opposite, and locate all the clients of a service within the open scenes.
    locate-clients.gif
     
    Last edited: Nov 11, 2023 at 8:06 AM
  18. Voxel-Busters

    Voxel-Busters

    Joined:
    Feb 25, 2015
    Posts:
    1,858
    Perfect!
    Do you already generate link.xml for classes using reflection to avoid the stripping issue?
     
  19. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    That is exactly what I'm trying out atm. In the latest release I've just used
    [assembly: Preserve]
    , which didn't seem to be enough.
     
  20. Voxel-Busters

    Voxel-Busters

    Joined:
    Feb 25, 2015
    Posts:
    1,858
    I see. We never had any issues generating it in our plugins.
    Anyways, let me know if you need any help on it.

    I'm going to buy this plugin after one question. This makes code super neat :)

    When to use Init Args over Zenject?
     
    Last edited: Nov 11, 2023 at 10:07 AM
    SisusCo likes this.
  21. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    I'd say Zenject's approach is closer to that of a traditional C# DI Framework, where you use pure code to configure basically all services, and the Inspector is touched as little as possible. It's almost like a replacement for the Inspector, moving all the configuration that usually happens using it away into installers in code.

    This is a really flexible approach, but can come with some downsides:
    • It is so different from the usual Unity workflow, that there can be a pretty steep learning curve.
    • It can prevent designers from making quick adjustments to services easily via the Inspector.
    • It can get complicated to try and bridge the gap between the code and scenes and prefabs.
    • It can get difficult to decipher what service should be getting injected to a particular client and how in more complicated setups. You might need to follow a hard-to-track trail of breadcrumbs across multiple installers and nested containers.

    To contrast, Init(args) has been designed to more closely resemble the traditional workflow in Unity, and focuses on just getting rid of all the major limitations and pain points that one usually has to contend with in Unity.
    • You can still use the Inspector to configure everything, but it now supports interfaces, value providers (for localization, addressables etc.) and cross-scene references.
    • You can still use AddComponent and Instantiate in code, but they just now also support passing arguments.
    • When the service of a client has been defined in code, it is easy to determine what the service is and where in code it is configured using just the Inspector.
    • Creating and using shared services is as easy as with the singleton pattern, while avoiding all the usual downsides of singletons. Learning curve is minimal.
    I'd say that the more code-driven your project is, the more easily you'll be able to integrate Zenject into it. Also the more comfortable that your team is with using traditional DI frameworks, the easier it will be to onboard everybody to use it.

    If you want to lean more into the strengths of the Unity editor, and want to keep your project simple and designer-friendly, then Init(args) could be the better choice.
     
    Voxel-Busters likes this.