Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. 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,363
    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:
    2,140
    Have you tried preserving the whole assembly?
     
  6. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    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,363
    @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,363
    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,363
    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,363
    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:
    2,027
    @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
  17. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    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
  18. Voxel-Busters

    Voxel-Busters

    Joined:
    Feb 25, 2015
    Posts:
    2,027
    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,363
    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:
    2,027
    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
    SisusCo likes this.
  21. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    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.
     
    PanthenEye, Njordy and Voxel-Busters like this.
  22. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    Version 1.3.0 of Init(args) has been submitted to the Asset Store for review!

    New in this update:

    CustomInitializer<T...>

    It is now possible to generate an Initializer for any component from its context menu - even if it does not derive from MonoBehaviour<T...> or implement the IInitializable<T...> interface.

    Code (CSharp):
    1. class AudioSourceInitializer : CustomInitializer<AudioSource, AudioClip>
    2. {
    3.     protected override void InitTarget(AudioSource target, AudioClip clip) => target.clip = clip;
    4. }
    custom-initializers.gif

    ValueProviderMenuAttribute

    The ValueProviderMenuAttribute can be added to a scriptable object value provider, and configured to appear as an option in a dropdown menu next to Init arguments of specified types.

    Code (CSharp):
    1. [ValueProviderMenu("Localized String", typeof(string), Order = 1)]
    2. internal sealed class LocalizedString : ScriptableObject, IValueProvider<string>, INullGuard
    3. {
    4.     [SerializeField] UnityEngine.Localization.LocalizedString value = new();
    5.  
    6.     public string Value => value.IsEmpty ? null : value.GetLocalizedString();
    7.  
    8.     object IValueProvider.Value => Value;
    9.     bool INullGuard.HasValue(Component client) => !value.IsEmpty;
    10.     private void OnDestroy() => ((IDisposable)value).Dispose();
    11. }
    This menu item allows quickly assigning the value provider in question to any matching fields, and modifying it directly within the Init section, without having to spend time manually creating a new scriptable object asset and dragging-and-dropping it to the field.

    value-provider-menu-attribute.gif

    IValueByTypeProvider

    Added IValueByTypeProvider interface for value providers that can be asked for a value of any type - unlike IValueProvider<T>, which can only return one particular type of value. This can make it possible to create more reusable value providers. For example, one
    GetComponentInChildren
    value provider can search for any type of component in the children of the client.

    Code (CSharp):
    1. [ValueProviderMenu("Get Component In Children", typeof(Component), Order = 2)]
    2. internal sealed class GetComponentInChildren : ScriptableObject, IValueByTypeProvider
    3. {
    4.     public bool CanProvideValue<TValue>(Component client) => client != null && typeof(Object).IsAssignableFrom(typeof(TValue));
    5.  
    6.     public bool TryGetFor<TValue>(Component client, out TValue value)
    7.     {
    8.         value = client != null ? client.GetComponentInChildren<TValue>() : default;
    9.         return value != null;
    10.     }
    11. }
    IValueProviderAsync<T>

    All initializers now have improved asynchronous initialization capabilities. They can load values from providers that implement
    IValueProviderAsync<T>
    or
    IValueByTypeProviderAsync
    asynchronously, and only initialize the client once all its initialization arguments are ready.

    A simple example use case would be loading a addressable Sprite asset asynchronously, and only once the loading has finished, injecting it into an Image component.

    Example IValueProviderAsync:
    Code (CSharp):
    1. [ValueProviderMenu("Load Addressable Sprite", typeof(Sprite), Order = 10)]
    2. internal sealed class LoadAddressableSprite : ScriptableObject, IValueProviderAsync<Sprite>, IValueReleaser<Sprite>
    3. {
    4.     [SerializeField] AssetReferenceSprite addressableAsset;
    5.     readonly HashSet<Component> clients = new HashSet<Component>();
    6.  
    7.     async ValueTask<object> IValueProviderAsync.GetForAsync(Component client) => await GetForAsync(client);
    8.  
    9.     public async ValueTask<Sprite> GetForAsync(Component client)
    10.     {
    11.         if(clients.Add(client) && clients.Count == 1)
    12.         {
    13.             return await addressableAsset.LoadAssetAsync<Sprite>().Task;
    14.         }
    15.  
    16.        if(!addressableAsset.IsDone)
    17.        {
    18.               await addressableAsset.OperationHandle.Task;
    19.        }
    20.  
    21.         return addressableAsset.Asset as Sprite;
    22.     }
    23.  
    24.     public void Release(Component client, Sprite sprite)
    25.     {
    26.         if(clients.Remove(client) && clients.Count == 0)
    27.         {
    28.             addressableAsset.ReleaseAsset();
    29.         }
    30.     }
    31. }
    value-provider-async.gif

    InitInEditModeAttribute

    Similar to the InitOnResetAttribute, this attribute causes instances of the component class to which it is attached to get initialized in edit mode. However, while the InitOnResetAttribute only causes the components to be initialized when they are first attached to a game object, the InitInEditModeAttribute also causes them to be re-initialized every time that any object is modified in the same scene or prefab to which they belong.

    This can be useful for situations where it is desired for one component to update its state whenever another component that it depends on changes. E.g.
    [InitInEditModeAttribute(From.GameObject)]
    could be added to an OnClick component that depends on a Button component being on the same game object, to make sure that the reference is updated when a Button is attached to the game object, the OnClick component is dragged to another game object etc.

    Other Improvements
    • Using Medium or High Managed Stripping Level is now supported.
    • Custom editor improvements for a better Inspector experience.
    • Initializer script generation improvements.
    • New add-on package for uGUI, containing initializers for components like Image and Text. These can be useful for adding localization or addressable reference support to these components.
    • Value providers can now implement
      INullGuard
      , which the Inspector can use to know if they'll be able to provide a non-null value or not at runtime, and if not display a warning in if null guard is enabled.
    • Value providers can now implement the
      IValueReleaser
      interface, to get informed when game object to which they are attached to gets destroyed. This can be used, for example, to release loaded addressable assets, or other unmanaged memory, once it is no longer needed. It is basically the value provider equivalent of IDisposable, with the additional benefit that it gets passed a reference to the client component as well as the value that had been provided to the client.
     
    Last edited: Dec 19, 2023
    PanthenEye likes this.
  23. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    I've created a short video tutorial to help with getting started using Init(args). I hope it's useful!

     
    theforgot3n1 and CodeRonnie like this.
  24. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Hello Sisus!
    I am looking into using dependency injection as have a large project I am working on.
    All is going well and I am enjoying the simplicity of it, and being able to see things in the inspector easier. However, I have a few questions

    1. All my services are using FindFromScene = true. It works great, except that the ServicesDebugger and Service Window does not pick them up. Is this intended?
    2. I cannot inject more than 6 arguments. We have a commercial product with up to 12 dependencies at times. Is there any way of injecting more than 6 arguments?
    3. What happens exactly when you Destroy a Service that has FindFromScene? Will any client depending on that service now receive a null ref? What happens if you re-create the same Service, will everything work again?

    Great tool, could be of big use for us!
     
    SisusCo likes this.
  25. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    I also want to mention a potential bug.
    I changed most of my services (all of them located in a "GlobalServices" prefab and an "EncounterServices" prefab) to be [Service(FindFromScene = true)] as well as MonoBehaviour<...> that need some Init stuff.

    All of them, as far as I can tell, are able to init properly, getting the right services.
    Except 1.
    I have no idea why that specific MonoBehaviour on the "EncounterServices" prefab is not getting its Init called. It just doesnt get called. If I add an Initializer, however, it does get called.
    Any ideas what it might be about?
     
  26. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    This seems like a bug. I'll look into it, thanks for letting me know!

    No elegant solution at the moment unfortunately. I'd like to increase the dependency cap in the future, but it's a lot of work - especially so for something like 12 dependencies.

    One option to work around the limitation is to only expose up to six services in the inspector with an Initializer, and then retrieve the rest manually inside Init or OnAwake:
    Code (CSharp):
    1. public abstract class CoreBehaviour<T1, T2, T3, T4, T5, T6>
    2. : MonoBehaviour<T1, T2, T3, T4, T5, T6>, // additional services that only some derived types receive
    3.   IArgs<A, B, C, D, E, F> // core services that all derived types receive
    4. {
    5.     protected sealed override void Init(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6)
    6.     {
    7.         if(!InitArgs.TryGet(Context.MainThread, this, out A a, out B b, out C c, out D d, out E e, out F f))
    8.         {
    9.             Debug.LogError("Core services not found for client {GetType().Name}!", this);
    10.             return;
    11.         }
    12.  
    13.         Init(a, b, c, d, e, f, t1, t2, t3, t4, t5, t6);
    14.     }
    15.  
    16.     protected abstract void Init(A a, B b, C c, D d, E e, F f, T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6);
    17. }
    18.  
    19. public class Player : CoreBehaviour<G, H, I, J, K, L>
    20. {
    21.     protected override void Init(A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l)
    22.     {
    23.         // assign to members
    24.     }
    25. }
    The downside with this would be that the Init(args)'s systems like the initializer would not know anything about these services that you retrieve manually in code, so those would not get automatically validated, you couldn't ping them via the Inspector, you'd need to manually await them if they're asynchronously loaded, etc. I would still probably go with this approach myself if possible, due to its simplicity (less maintenance overhead).


    Another approach would be to use an initializer that takes a single value tuple (or custom struct) argument, wrapping all the 12 dependencies.

    In order to flatten the tuple, to serialize all the members and expose them in the Inspector properly, you'd need to derive from InitializerBase<TClient, TArgument> instead of Initializer<TClient, TArgument>. InitializerBase leaves the responsibility of serializing/locating all the arguments to the derived class. We can then implement the class similar to Initializer1.cs, except with handling for unwrapping all the arguments:
    Code (CSharp):
    1. public sealed class PlayerInitializer : InitializerBase<Player, (A a, B b, C c, D d, E e, F f, G g)>
    2. {
    3.     // Any<T> is a value wrapper that adds interface and IValueProvider<T> support
    4.     [SerializeField] Any<A> a;
    5.     [SerializeField] Any<B> b;
    6.     [SerializeField] Any<C> c;
    7.     [SerializeField] Any<D> d;
    8.     [SerializeField] Any<E> e;
    9.     [SerializeField] Any<F> f;
    10.     [SerializeField] Any<G> g;
    11.  
    12.     protected override (A a, B b, C c, D d, E e, F f, G g) Argument
    13.     {
    14.         get => (a.GetValue(this), b.GetValue(this), c.GetValue(this), d.GetValue(this), e.GetValue(this), f.GetValue(this), g.GetValue(this));
    15.  
    16.         set
    17.         {
    18.             a = value.a;
    19.             b = value.b;
    20.             c = value.c;
    21.             d = value.d;
    22.             e = value.e;
    23.             f = value.f;
    24.             g = value.g;
    25.         }
    26.     }
    27. }
    28.  
    29. public sealed class Player : MonoBehaviour<(A a, B b, C c, D d, E e, F f, G g)>
    30. {
    31.     protected override void Init((A a, B b, C c, D d, E e, F f, G g) args)
    32.     {
    33.         // assign to members
    34.     }
    35. }
    This is just a minimal implementation though. Classes like the one found in InitializerT6.cs have additional code for handing more advanced situations, like automatically disposing arguments that implement IValueReleaser during the initializer's OnDestroy event, awaiting for asynchronous value providers to provide their value and null argument validation.

    The downside with this approach is that if things like asynchronous value providers unexpectedly do not work in these particular initializers, it could result in bugs that are hard to track down in the future, if care is not taken to document the limitations clearly, or detect them and log warnings in OnAwake/OnValidate or something to that effect.

    Or on the flip side, if support for these more advanced features was copy-pasted from InitializerT6.cs, then that could increase maintenance overhead quite a bit.

    Instances from classes that have the ServiceAttribute are created only once automatically, just before Awake is called for any objects in the initial scene. So you can think of them as analogous to singletons with DontDestroyOnLoad.

    If you want to support services that are tied to the lifetime of a particular scene or prefab instance, the easiest way is to make the service into a component, and attach a Service Tag to it.
    service-tag.gif

    If you want to do this with a scriptable object instead, you can use a Services component, which allows you to drag-and-drop one or more services from the scene or the project hierarchy to the list of services.

    If you want to do this with a plain-old C# object, you can create an asset from a scriptable object that implements IValueProvider<MyService>, and drag-and-drop that to a Services component.

    If you want to manually register service objects based on some other variables, you can use
    Service.SetInstance
    .

    If an existing service is swapped with another one at runtime, clients which already have received their dependencies via Init are not automatically re-initialized again on-the-fly with the new service.

    If you need to add this functionality, you can use
    Service.AddInstanceChangedListener<MyService>(OnServiceChanged)
    and
    Service.RemoveInstanceChangedListener<MyService>(OnServiceChanged)
    in your client class, and handle updating event subscriptions on the old and new clients etc. accordingly.

    If an existing service is destroyed at runtime, clients will not be notified of this automatically. The references they have will continue pointing to the same instance, which is now destroyed. This means that it'll be possible for clients to continue calling custom members on those instances, as if they were still alive, but a MissingReferenceException will get thrown by Unity automatically if you try to access most of the internal methods.

    To avoid calling members on destroyed services that have been assigned to interface type fields, you can manually check if they are null or destroyed by comparing them against the
    Null
    property with an
    ==
    or
    !=
    operator.
    Code (CSharp):
    1. IMyDestroyableService service;
    2.  
    3. void OnDisable()
    4. {
    5.     if(service != Null) // <- 'Null', not 'null', to properly handle interface type field
    6.     {
    7.         service.Unregister(this);
    8.     }
    9. }
    Or you could make the class that has the ServiceAttribute into a decorator, which elegantly handles situations where the wrapped objected has been destroyed.
    Code (CSharp):
    1. [Service(typeof(IMyService))]
    2. public sealed class MyServiceReference : ScriptableObject, IMyService
    3. {
    4.     MyService wrappedService => MyService.Instance;
    5.  
    6.     public void Unregister(IMyClient client)
    7.     {
    8.         if(wrappedService == null)
    9.         {
    10.             // log warning, do nothing (Null Object Pattern), queue request etc.
    11.             return;
    12.         }
    13.  
    14.         wrappedService.Unregister(client);
    15.     }
    16. }
     
    Last edited: Feb 11, 2024
  27. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    Maybe in that particular client class, there is a
    void Awake()
    method definition, which hides the Awake function in the base class, preventing the auto-initialization code from running?

    If so, then changing the method definition to
    protected override void OnAwake()
    should fix the issue.

    If you are using the latest version of Init(args), there should be a warning in the Console about this.
    I also want to add a warning to the Inspector about this in a future update, since it's pretty easy to make this mistake - especially when refactoring existing code.
     
    theforgot3n1 likes this.
  28. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    I haven't been able to reproduce this yet.

    I did notice that the Service Debugger view might not refresh its contents until it's mouseovered, but that is all.

    Can you check with the Package Manager that you have the latest updates installed? Just to make sure that this is not a bug that has already been fixed as a side effect of some other changes.
     
  29. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Thank you for the quick feedback, haven't had the time to check things out yet :)
    Will do so today after 5pm.

    A few remarks meanwhile:
    1. I might've gotten confused about how to tag a MonoBehaviour as a service. I have done both the ServiceTag and the ServiceAttribute approach. If only one is supposed to be done, perhaps there can be some logerror/warning to indicate this (perhaps I missed this)? Going off of your reply, it seems like the ServiceTag is the right approach because its lifecycle is clearly tied to the monobehaviour.
    2. The approach I've done so far for more than 6 dependencies is to do Service<MyService>.GetFor(this), resulting in this code


    Code (CSharp):
    1. protected override void Init(GlobalEventManager firstArgument, EncounterQueryManager secondArgument,
    2.             EncounterSelectionManager thirdArgument, EncounterMapManager fourthArgument,
    3.             EncounterPlayerManager fifthArgument,
    4.             EncounterPathfindingManager sixthArgument)
    5.         {
    6.             this[nameof(this.gem)] = firstArgument;
    7.             this[nameof(this.eqm)] = secondArgument;
    8.             this[nameof(this.esm)] = thirdArgument;
    9.             this[nameof(this.emm)] = fourthArgument;
    10.             this[nameof(this.epm)] = fifthArgument;
    11.             this[nameof(this.pfm)] = sixthArgument;
    12.             this[nameof(this.tmi)] = Service.GetFor<TargetingModeIndicatorsManager>(this);
    13.             this[nameof(this.trm)] = Service.GetFor<TurnManager>(this);
    14.             this[nameof(this.zcm)] = Service.GetFor<ZoneOfControlManager>(this);
    15.             this[nameof(this.elm)] = Service.GetFor<EventLoopManager>(this);
    16.             this[nameof(this.atm)] = Service.GetFor<AbilityTargetingManager>(this);
    17.  
    18.             this.gem.StartListening(EventName.UnitAfterEndTurn, this.gameObject, this.TransitionToIdle);
    19.             this.gem.StartListening(EventName.EncounterStateChanged, this.gameObject, this.Refresh);
    20.         }
    Thoughts on that?

    I will consider using Tuples as well. But I dont have a convenient set of dependencies that are always present, so I might end up getting a lot of different tuples for different managers.
     
  30. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    I have doublechecked my version, it is the latest, 1.3.1.

    It looks like my debugger is still not showing the correct services, I only have some default ones.

    upload_2024-2-13_17-39-3.png

    If you would like to, I can provide more information on the setup of my unity project (perhaps share screen if it helps). It is heavily based on prefabs, so that might affect things.
     
  31. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Also small "bug" fix; when you are inside of a prefab and you make a script into a service via "Make Service of Type...", it does not dirty the scene. I think most cases you do want that action to dirty the scene!
     
  32. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    Great idea, I'll definitely add that in.

    ServiceAttribute can be used to register components from the initial scene using
    [Service(FindFromScene = true)]
    , or from arbitrary scenes using addressables/resources/add component. These services are globally available to all clients and are expected to exist for the whole lifetime of the application. When this is the case, I do recommend using ServiceAttribute, because it's easier to detect the attribute's existence in editor code, so automated null argument guarding and Inspector breadcrumbing can be more effective.

    For component services that have a shorter lifespan, or should only be accessible to clients locally within a particular prefab or scene hierarchy, ServiceTag is the way to go.

    I think that's a simple and valid approach.

    You could also group the services inside small modules, if you want to make it a bit clearer what dependencies clients have, what services are available to clients, and to make automated initialization order optimizations more reliable:
    Code (CSharp):
    1. [Service]
    2. public sealed class Managers
    3. {
    4.     public readonly TargetingModeIndicatorsManager TargetingModeIndicatorsManager;
    5.         public readonly TurnManager TurnManager;
    6.         public readonly ZoneOfControlManager ZoneOfControlManager;
    7.         public readonly EventLoopManager EventLoopManager;
    8.         public readonly AbilityTargetingManager AbilityTargetingManager;
    9.  
    10.     public Managers(TargetingModeIndicatorsManager targetingModeIndicatorsManager, TurnManager turnManager, ZoneOfControlManager zoneOfControlManager, EventLoopManager eventLoopManager, EventLoopManager eventLoopManager, AbilityTargetingManager abilityTargetingManager)
    11.     {
    12.         TargetingModeIndicatorsManager = targetingModeIndicatorsManager;
    13.             TurnManager = turnManager;
    14.         ZoneOfControlManager = zoneOfControlManager;
    15.         EventLoopManager = eventLoopManager;
    16.         AbilityTargetingManager = abilityTargetingManager;
    17.     }
    18. }
    However, you'd run into the same 6 argument limit on this front as well, so the modules couldn't contain dozen+ services - unless you were to retrieve them manually using Service.Get.

    BUT I'm now thinking that this 6 argument limit is too restrictive in practice for larger projects. I'm going to bite the bullet and increase it to 12 in the next update.
     
    theforgot3n1 likes this.
  33. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    If you are able to share the code for one of the services that isn't showing up, or the exact way the service is configured using a ServiceAttribute / ServiceTag in particular, that could help.
     
  34. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    I did eventually do a setup where I didnt use the ServiceAttribute anywhere but only ServiceTag, but didnt quite get it to work. Still got some Init() functions not being called, despite their dependencies having ServiceTags.

    I got a lot of those AmbiguousMatchWarning warnings (was called something similar) when running and destroying/instantiating scenes.

    > BUT I'm now thinking that this 6 argument limit is too restrictive in practice for larger projects. I'm going to bite the bullet and increase it to 12 in the next update.

    I dont have any number of dependencies more than 12, I think the largest I have is 11 so far, the one I posted earlier. So if you did manage to get 12 to work, that should really be enough for any enterprise project. More than 12 is a bit nuts.
     
  35. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    That sounds like it could very well be the source of the issue! If you can send me the stack trace for those, I may be able to get it fixed.

    Perfect. 12 is doable, I'm already making good headway on the implementation.
     
  36. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    Version 1.3.2 of Init(args) has been submitted to the Asset Store for review.

    New in this update:

    MonoBehaviour<T1, T2, ..., T11, T12>
    The maximum number of arguments that can be injected to components that derive from a MonoBehaviour<T...> base class has been increased to twelve, up from the previous limit of six.

    12-services.png

    Even though it's easy to say that ideally classes shouldn't depend on that many other classes, in practice there can always be exceptions - and Init(args) should be flexible enough to support such situations.

    Service Debugger
    The Service Debugger Window saw some improvements:
    • Services are now sorted alphabetically within the view.
    • Services that are destroyed or inactive are now greyed out in the list.
    • All services registered during runtime via Service Tags, Services components, and manually in code, are now also listed in the Service Debugger.
    • The view now also specifies which clients each service is accessible to.
    service-debugger.gif

    UX Improvements
    Init(args) is now able to detect some additional user errors, and potential user errors, and communicate these to the user. This is meant to lower the learning curve, and to help users avoid some potential pitfalls during usage.

    If a type has been registered as a service type, using both a ServiceAttribute, as well as a Service Tag or a Services component, then an info box will be shown in the Inspector for the component that has the Service Tag / the Services component, informing the user about what will happen at runtime.

    Info-box.png


    The Services component will now also detect if a user tries to drag-and-drop a script asset into the list of services to register, discard the value, and log a warning to the Console to let the user that this is not supported, and what they could do instead or register a plain old C# object as a service (use a wrapper or a value provider).

    Services-warning.png
     
    Last edited: Feb 18, 2024
    theforgot3n1 and CodeRonnie like this.
  37. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Thank you for the recent UX improvements and adding 6 more arguments to the monobehaviour when needed. I have a few of those (thankfully not too many) and it helps to have a consistent way also of dealing with the outliers.

    Services Debugger window is great. It's nice to have it in a window instead of in the `ServicesDebugger` component in `DontDestroyOnLoad` (maybe there always was a window?).

    I have a bug though. In the game I described in a previous post, I am running into a scenario where an inactive gameobject with a component that requires a global script, made servicable to Everywhere by a `ServiceTag`, is not getting that script. Here is a screen shot of the runtime state.

    upload_2024-3-9_15-11-59.png

    When I enable the `SkillsLevelUpContainer`, it works. It then looks like this.

    upload_2024-3-9_15-13-25.png

    One difference is that the `IAbilityTooltipPick3Variant` is, by a script, moved to `DontDestroyOnLoad` because of some optimizations, but it's still the case that the script should have had its runtime state correct from the getgo!

    Because `GlobalQueryManager` is null, I have a null ref error.

    upload_2024-3-9_15-16-36.png

    `GlobalQueryManager` looks like this and is in DontDestroyOnLoad.

    Any thoughts on what might be causing this? :)
     
  38. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Another bug of note, this one perhaps more serious!

    Occasionally, me and the game designer get stuck in a very long loop where, it seems, the ServiceTag is calling OnValidateDelayed dozens of times. Analyzing it a bit it likely has to do with how ServiceTag interacts with prefab setups, probably nested prefabs. When the loop happens, a bunch of changes in git showed up for a few nested prefabs.

    in this piece of code in ServiceTag


    Code (CSharp):
    1. if(service == null)
    2.             {
    3.                 // Handle service that is Missing due to being dragged to another GameObject.
    4.                 if(!(service is null) && service.GetHashCode() != 0 && definingType.Value != null)
    5.                 {
    6.                     var instancesMatchingDefiningType = Find.All(definingType.Value, true);
    7.                     if(instancesMatchingDefiningType.Length == 1 && instancesMatchingDefiningType[0] is Component instance)
    8.                     {
    9.                         bool instanceHasOtherTags = false;
    10.                         foreach(var tag in instance.GetComponents<ServiceTag>())
    11.                         {
    12.                             if(tag.service == service)
    13.                             {
    14.                                 instanceHasOtherTags = true;
    15.                                 break;
    16.                             }
    17.                         }
    18.  
    19.                         if(!instanceHasOtherTags)
    20.                         {
    21.                             #if DEV_MODE
    22.                             Debug.Log($"Moving Service tag {definingType.Value.Name} of {instance.GetType().Name} from {name} to {instance.gameObject.name}...", instance.gameObject);
    23.                             #endif
    24.                             service = instance;
    25.                             ComponentUtility.CopyComponent(this);
    26.                             Undo.DestroyObjectImmediate(this); // Can this handle prefab assets? prefab instances? Test!
    27.                             ComponentUtility.PasteComponentAsNew(instance.gameObject);
    28.                             return;
    29.                         }
    30.                     }
    31.                 }
    32.  
    33.                 #if DEV_MODE
    34.                 Debug.Log($"ServiceTag on GameObject \"{name}\" is missing its target. Removing it from the GameObject.", gameObject);
    35.                 #endif
    36.                 Undo.DestroyObjectImmediate(this); // Can this handle prefab assets? prefab instances? Test!
    37.                 return;
    38.             }
    It kept hitting the final return.
    Occasionally it got down to the `OnValidServiceTagLoaded`.

    It did this for about 2 minutes. I'll keep you posted if there is more I figure out.

    EDIT: I had some errors after this! Figured out what had happened! All the ServiceTags in a prefab named `EncounterServices` were removed during this OnValidateDelayed loop. I discarded the changes in git and now it works.
     
    Last edited: Mar 9, 2024
  39. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    Components on inactive GameObjects do not receive services; services are retrieved by the base class at the beginning of the Awake event, which only triggers once the GameObject becomes active.

    You might be able to get around this by manually retrieving the service during the OnAfterDeserialize event:
    Code (CSharp):
    1. public class Example : MonoBehaviour<GlobalQueryManager>, ISerializationCallbackReceiver
    2. {
    3.     void ISerializationCallbackReceiver.OnBeforeSerialize() { }
    4.  
    5.     void ISerializationCallbackReceiver.OnAfterDeserialize()
    6.     {
    7.         if(InitArgs.TryGet(Context.OnAfterDeserialize, this, out GlobalQueryManager gqm))
    8.         {
    9.             Init(gqm);
    10.         }
    11.     }
    12. }
    This will only work though if the service has already been registered before the deserialization process occurs for the client. If both the client and service are located in the same game object / prefab, this might not be the case.
     
  40. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    So, somehow the ServiceTags got destroyed during OnValidate when they shouldn't have?

    I wonder if there was a compile error in the assembly that contained the defining type of the service, and this caused the ServiceTag's defining type to appear to be null during OnValidate, causing the ServiceTag to think that it had invalid state and removing itself.

    If so, then I can add a check to see that there are no compile errors before performing clean up to fix this issue.
     
  41. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Ahh. I thought the `Init` method was called regardless of whether the gameobject is active. I must have misinterpreted the documentation on Init, it says `Note that Init gets called immediately following object creation even if the GameObject to which the component was added is inactive unlike some other initialization event functions such as Awake and OnEnable.`

    I assume this is something you wouldn't want to implement? I prefer to avoid the OnAfterDeserialize since I would rather have a singular initialization flow, like with Init.

    Part of the appeal of Init to me was to be 100% certain that the MonoBehaviour in question gets its dependencies, even when inactive. Basically, have the level of guarantee of a constructor. Without using the ComponentBehaviour flow, since most of my gameobjects are instantiated on the fly.

    This might be the case. I'll let you know if it happens again.
     
  42. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    That documentation comment is indeed very misleading, I'll need to update it!

    That statement is true when an instance is created using
    Instantiate(service1, service2...)
    or
    AddComponent(service1, service2...)
    , but not for clients that exist in scenes or prefabs that are loaded using traditional means.

    I'd rather not implement that logic for all MonoBehaviour<T...> instances for multiple reasons:
    1. It would introduce some overhead for all clients, while only being useful in very rare edge cases (the client is inactive, and an external object tries to access its members before it becomes active).
    2. It's not possible to use GetComponent during OnAfterDeserialize, which would make it complicated to determine if the client has an initializer attached to it or not. Services configured using an Initializer for a specific instance should take precedence over the default services that have been registered using things like ServiceAttribute and ServiceTag.
    3. Awake is always executed on the main thread, while OnAfterDeserialize is not, so it's safer to only call Init from the Awake method whenever possible.
    That being said, I think I should be able to add a special Initializer that can easily be attached to any client, which can retrieve services during its OnAfterDeserialize event and inject them to the client, even if they are attached to an inactive game object.

    I could then show a help box in the editor of any clients attached to inactive game objects, that explains that they won't receive services until they become active, unless this Initializer is attached to them.
     
    Last edited: Mar 15, 2024
    theforgot3n1 likes this.
  43. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    First off, thank you for being so thorough in your answers :)

    In my project, I would benefit from having the possibility to Init something that is inactive. My use-case is in the UI. I have some UI code, where, for performance reasons, some parts of the UI are inactive. Sometimes the whole of the UI refreshes, e.g. when opening the character panel showing all character stats. When that is done, I want the whole UI to refresh its views, even the parts that are inactive. I've had to retrieve dependencies in those inactive UI scripts through FindObjectOfType previously (now I use Service.GetFor), but ideally I'd want those critical dependencies to be guaranteed.

    It makes total sense not to implement it for all MonoBehaviours! It is indeed an edge case, most of my stuff is happy to initialize on Awake. Having a special initializer (and the help box as a warning for inactive ones), would be more than enough. :)
     
    SisusCo likes this.
  44. hakastein

    hakastein

    Joined:
    Sep 14, 2023
    Posts:
    6
    Does this library support the latest versions of FishNetwork?
     
  45. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    @headcrabogon Yes. If you install the add-on found at
    Sisus/Init(args)/Add-Ons/FishNet.unitypackage
    you'll be able to derive from
    NetworkBehaviour<T...>
    instead of
    NetworkBehaviour
    to receive services.

    FishNet.png

    Example:
    Code (CSharp):
    1. using Sisus.Init;
    2.  
    3. public class ExampleClient : NetworkBehaviour<ExampleService>
    4. {
    5.     protected override void Init(ExampleService service)
    6.     {
    7.         ...
    8.     }
    9. }
     
  46. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    Version 1.3.3 of Init(args) is out now.

    New in this update:


    Inactive Initializer
    A help box will now be shown in the Inspector for client components that are attached to inactive game objects, that lets the user choose whether to initialize them during Awake when the game object becomes active, or during OnAfterDeserialize before they become active.

    inactive-initializer.gif

    Other improvements:
    • It is now possible to customize default Null Argument Guard flags for initializable components even if they don't have an Initializer attached. In this case the flags are serialized in the component's .meta file.
    • When a value provider's Null Argument Guard has failed, mouseovering its tag in the Inspector now displays a tooltip with additional information.
    • Added ability to right-click the Null Argument Guard icon and select 'Debug' to open the Service Debugger or 'Help' to open a documentation page.
    • Added a Requires<T...> base class that can be derived from to get services automatically injected to plain C# objects.
    • Added CustomEditorDecoratorAttribute that can be used to register custom editors responsible for drawing the Init section for targets of particular types.
    • Added the ability to easily generate initializers for StateMachineBehaviours using the Animator Editor's .
    • Improved Any<T> custom property drawer to support collections and inlining of value provider editors even outside of Init sections.
    • Increased maximum dependency count of NetworkBehaviour<T...> SerializedMonoBehaviour<T...> from six to twelve.
    • Added AudioSourceInitializer to the UnityEngine add-on package.
    • Improved undo support.
    • Simplified implementation of the IValueProvider<T> interface, by adding a default implementation for IValueProvider.Value.
    • Bug fixes.
     

    Attached Files:

    theforgot3n1 likes this.
  47. hakastein

    hakastein

    Joined:
    Sep 14, 2023
    Posts:
    6
    Hello, after update i got error
    Assets\Sisus\Init(args)\Scripts\Initializer\InitializerBaseInternal.cs(213,97): error CS1061: 'MonoBehaviour' does not contain a definition for 'didAwake' and no accessible extension method 'didAwake' accepting a first argument of type 'MonoBehaviour' could be found (are you missing a using directive or an assembly reference?


    It's looks like you use wrong tag in this piece of code

    Code (CSharp):
    1. #if DEBUG && UNITY_2022_2_OR_NEWER
    2. if(target is MonoBehaviour targetMonoBehaviour && targetMonoBehaviour && targetMonoBehaviour.didAwake)
    3. {
    4.     Debug.LogWarning($"{GetType().Name}.Awake was called after target {target.GetType().Name}.Awake has already executed! You can add the [InitAfter(typeof({target.GetType().Name}))] attribute to the {GetType().Name} class to make sure it is initialized before its client.");
    5. }
    6. #endif
    becouse didAwake available only for 2023.1

    https://unity.com/releases/editor/whats-new/2023.1.0

    • Scripting: Added: Expose didAwake and didStart for MonoBehaviours.
     
  48. hakastein

    hakastein

    Joined:
    Sep 14, 2023
    Posts:
    6
    Also got
    Assets\Sisus\Init(args)\Add-Ons\FishNet\NetworkBehaviourT1.cs:239 Cannot resolve symbol 'EditorOnly'
    on entire fishnet addon files

    And

    The keyword 'new' is required on 'Reset' because it hides event function 'void FishNet.Object.NetworkBehaviour.Reset()


    Warning
     
  49. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    Yes indeed, I should have used UNITY_2023_2_OR_NEWER there instead. I've submitted a fix for this to the Asset Store for review, but since they don't do reviews during the weekends, it'll only go live on Monday (at the earliest).
    But I can PM you the fixed package right now, so you don't need to necessarily wait that long.
     
  50. hakastein

    hakastein

    Joined:
    Sep 14, 2023
    Posts:
    6
    I am already fixed it for myself :)
     
    SisusCo likes this.