Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Init(args) - The practical DI framework

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

  1. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Hey again @BagarraoEduardo

    I'm not sure what the issue with StateComponentInitializer above is. I wasn't able to reproduce the issue with Odin installed and its custom editor disabled. The likely explanation is that some custom editor is still taking precedence over the custom initializable drawer in Init(args) in your project.

    Regardless, I think I've figured out a solution to the issue. Instead of drawing the Init section with the help of a custom editor, I'm now injecting it into the Inspector as a separate UI element above the component editor in the next version. This way the Init section should always appear, even if a component has a custom editor.

    I'll send you the updated package in a PM and will submit it to the asset store for review, and hopefully this update resolves your issue :)
     
    BagarraoEduardo likes this.
  2. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38

    Hello Sisus,

    Awesome! I'll test it and I'll give feedback ;) Thank you so much for the help!!

    Best Regards,
    Eduardo
     
  3. AldeRoberge

    AldeRoberge

    Joined:
    Jun 23, 2017
    Posts:
    60
    Can't build, I get this error :

    upload_2022-7-27_16-28-27.png

    Assets\Sisus\Init(args)\Scripts\CrossSceneReference\CrossSceneReference.cs(46,4): error CS0103: The name 'target' does not exist in the current context
     
  4. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    @AldeRoberge Thanks for letting me know! I've submitted a fix to the asset store.
     
    AldeRoberge likes this.
  5. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    Hi @SisusCo I'm also experiencing an issue with Odin. It seems that Odin attributes (i.e. Button) stops working for components inheriting from MonoBehaviour<T>.

    I'm using latest versions of Odin and Init(Args). Unity 2022.2b3
     
  6. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Hey @Singtaa , thanks for informing me! I was able to reproduce this and figured out how to fix it - I'm going to submit an update to the asset store now that resolves the issue.
     
    Singtaa likes this.
  7. DavidLe360

    DavidLe360

    Joined:
    Dec 24, 2018
    Posts:
    127
    Hi, any idea about an error:
    Editor type 'UnityEngine.MonoBehaviour' does not derive from UnityEditor.Editor
    ?

    I suspect it related to my Assembly Definition structure. It happens when on a code like this:


    using Sisus.Init;

    public class Test : MonoBehaviour<ITable_ofText>
    {

    ITable_ofText _ITableToSearchOn;

    protected override void Init(ITable_ofText iTable_ofText)
    {
    _ITableToSearchOn = iTable_ofText;
    }
    }


    The error:

    Code (CSharp):
    1. [Exception] ArgumentException: Editor type 'UnityEngine.MonoBehaviour' does not derive from UnityEditor.Editor
    2. Parameter name: editorType
    3. Editor.CreateEditorWithContext() at <5f40cdb07bd44d76a23dad985a4ec283>:0
    4.  
    5. Editor.CreateCachedEditorWithContext() at <5f40cdb07bd44d76a23dad985a4ec283>:0
    6.  
    7. Editor.CreateCachedEditor() at <5f40cdb07bd44d76a23dad985a4ec283>:0
    8.  
    9. InitializableEditor.CreateInspectorGUI() at /Project/External/Sisus/Init(args)/Scripts/Editor/Inspector/Editors/InitializableEditor.cs:252
    10. 250:   {
    11. 251:       var editorType = InitializableEditorInjector.GetCustomEditorType(target.GetType(), targets.Length > 0);
    12. -->252:       CreateCachedEditor(targets, editorType, ref internalEditor);
    13. 253:       internalEditorIsGenericInspector = string.Equals(internalEditor.GetType().FullName, "UnityEditor.GenericInspector");
    14. 254:   }
    15.  
    16. InspectorElement.CreateInspectorElementFromEditor() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    17.  
    18. InspectorElement.Reset() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    19.  
    20. InspectorElement.ExecuteDefaultActionAtTarget() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    21.  
    22. CallbackEventHandler.HandleEvent() at <e262c2d839014c8090373617ef295bab>:0
    23.  
    24. CallbackEventHandler.HandleEventAtTargetPhase() at <e262c2d839014c8090373617ef295bab>:0
    25.  
    26. SerializedObjectBindingContext.SendBindingEvent[TEventType]() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    27.  
    28. SerializedObjectBindingContext.BindTree() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    29.  
    30. SerializedObjectBindingContext.ContinueBinding() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    31.  
    32. SerializedObjectBindingContext.Bind() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    33.  
    34. DefaultSerializedObjectBindingImplementation.Bind() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    35.  
    36. BindingExtensions.Bind() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    37.  
    38. UnityEditor.UIElements.InspectorElement..ctor() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    39.  
    40. EditorElement.Init() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    41.  
    42. UnityEditor.UIElements.EditorElement..ctor() at <77cfaa957c26445e8d2fa87bf3ff3fa6>:0
    43.  
    44. EditorUIServiceImpl.CreateEditorElement() at <e0f1eb9a819a4bc9b5fd96c1ffd9ade9>:0
    45.  
    46. PropertyEditor.DrawEditors() at <5f40cdb07bd44d76a23dad985a4ec283>:0
    47.  
    48. PropertyEditor.RebuildContentsContainers() at <5f40cdb07bd44d76a23dad985a4ec283>:0
    49.  
    50. InspectorWindow.OnSelectionChanged() at <5f40cdb07bd44d76a23dad985a4ec283>:0
    51.  
    52. Selection.Internal_CallSelectionChanged() at <5f40cdb07bd44d76a23dad985a4ec283>:0
     
  8. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    @DavidLe360 That's my bad - I seem to have introduced a bug during recent refactoring/tweaking of InitializableEditorInjector.GetCustomEditorType and its returning the wrong type on a particular code execution path. I'll submit a fix to the asset store asap. Thanks for informing me!
     
    DavidLe360 likes this.
  9. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Hello Timo!

    Hope you're doing fine! I've been implementing a state machine in my project where the states and the state machines are POCO classes, and today I've started to migrating the states to Wrappers, but I believe it's causing a problem with GC.

    exceptions.png

    Basically now when I try to change my character from idle to movement state it the StateMachine's CurrentState attribute returns null(I would show in debug but I can only post 5 images. I'll post it on a next comment):

    Null property.png

    However, I've debugged the code and I'm sure that this field was correctly initialized in the class that stores the state machine and the states:

    Current State not null.png

    Do you have any idea what can be happening here(If it has something to do with Init(args) Wrappers)? If I migrate all the states to Wrappers can solve this problem?

    Here's how the inspector looks:

    Inspector.png

    Probably I'm also not initializing the class the correct way(I have no experience with the Wrappers). I add the wrapped component and then I get the wrapped object and assign it to the class attribute, like this:

    Captura de ecrã 2022-08-25, às 00.48.17.png

    Thank you so much in advance!

    Best Regards,
    Eduardo Bagarrão
     
  10. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Here's the image that was missing(the CurrentState variable null when changing from idle to movement): transition.png
     
  11. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Hey Eduardo!

    This feels like an order of execution issue. The Awake function of JaneEntity's initializer probably executes before the Awake function of JaneIdleStateComponent's initializer, so no value has yet been injected to the JaneIdleStateComponent.WrappedObject property by the initializer.

    One way to resolve the issue would be by adding the DefaultExecutionOrder attribute to the relevant initializers. The default execution order for Initializers is -29000, so I think adding something lower like [DefaultExecutionOrder(-29500)] to JaneIdleStateComponent should help.
     
    BagarraoEduardo likes this.
  12. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    I'll also do some thinking to see if I could come up with some solution on the Init(args) framework side to make these sort of execution order issues impossible.
     
    BagarraoEduardo likes this.
  13. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Thank you so much for the answer and for the help! Unfortunately it still throws the exception :(

    Captura de ecrã 2022-08-25, às 22.51.00.png


    I'll put this development on standby while I await for the fix then. Thank you so much once again ;)

    Best Regards,
    Eduardo
     
  14. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Surprising that tweaking the initialization order didn't help... I'll continue investigating different ways to resolve the issue. I also sent you a PM with one potential fix - hopefully it helps you get back to development!
     
    BagarraoEduardo likes this.
  15. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Init(args) 1.1.8 is now available from the Asset Store! It includes these improvements:

    Initializer Execution Order Improvements

    Initializers execution order is now automatically optimized, so that all components are only initialized after all other components they depend on have been initialized first.

    If there are unresolvable circular dependencies, a warning is logged to the Console to make it easier to understand why dependencies are not getting injected as expected. This can only happen with Wrappers when two wrapped plain old C# objects (POCOs) use constructor injection, and can't be instantiated without the other POCO existing first.
    Code (CSharp):
    1. public class State : IState, IUpdate
    2. {
    3.     private readonly IStateMachine stateMachine;
    4.     public State(IStateMachine stateMachine) => this.stateMachine = stateMachine;
    Code (CSharp):
    1. public class StateMachine : IStateMachine
    2. {
    3.     public IState CurrentState { get; private set; }
    4.  
    5.     public StateMachine(IState initialState) => CurrentState = initialState;
    It is now also possible to work around this impasse by changing at least one of the POCOs to have a parameterless constructor and to implement an IInitializable<T...> interface through which the initialization arguments can be delivered in a deferred manner by the wrapper's Initializer.
    Code (CSharp):
    1. public class StateMachine : IStateMachine, IInitializable<IState>
    2. {
    3.     public IState CurrentState { get; private set; }
    4.  
    5.     public void Init(IState initialState) => CurrentState = initialState;
    Code (CSharp):
    1. public class StateMachineComponentInitializer : WrapperInitializer<StateMachineComponent, IStateMachine, IState>
    2. {
    3.     protected override IStateMachine CreateWrappedObject() => new StateMachine();
    An InitAfter attribute was also added, which can be used to manually force an initializer to execute after some other initializer(s). This should only be needed in rare cases when the automatic execution order optimizer is unable to detect some hidden dependencies.
    Code (CSharp):
    1. [InitAfter(typeof(Level))]
    2. public class CharacterMotorInitializer : Initializer<CharacterMotor, Character>
    3. {
    Also added an InitOrderAttribute, which is like DefaultExecutionOrder except that it takes a category argument in addition to the order argument, to help ensure that all objects belonging to certain categories execute before all objects belonging to some other categories.
    Code (CSharp):
    1. [InitOrder(Category.Initializer, Order.VeryEarly)]
    2. public class LevelInitializer : Initializer<Level, LevelSettings>
    3. {
    Compatibility Additions
     
    Last edited: Sep 14, 2022
    Singtaa and BagarraoEduardo like this.
  16. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    It seems
    Service.Get()
    errors out when Enter Play Mode Options "Reload Scene" is on. I discovered this when testing out the A* Pathfinding Project package which requires "Reload Scene" turned on.

    Unity 2022.1.15f1, Windows 11, latest Init(Args)
     
  17. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Hey @Singtaa
    I was able to reproduce the issue, and am starting to work on a fix. Thanks for letting me know about this!
     
    Singtaa likes this.
  18. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    @Singtaa I got this fixed now, and service injection is working with all different Enter Play Mode Options. I sent you a PM with the updated package.

    I'll submit an update to the asset store as well; that should become available to download some time next week.
     
    Singtaa likes this.
  19. SisusCo

    SisusCo

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

    Service Argument Visibility Toggle

    Introduced in this version is a new toggle in the Init section, that can be used to control the visibility of service arguments.

    toggle-service-argument-visibility.gif

    Since service arguments are provided to components automatically, you might not always be interested in seeing all these dependencies listed, and might prefer a less cluttered Inspector experience instead.

    When you do want to ping to locate a service object, or need to drag-and-drop a different Object reference to swap out the default service value with something else, you can restore visibility of all dependencies with the click of a button.


    The update will probably become available to download on Monday or early next week.
     
  20. AldeRoberge

    AldeRoberge

    Joined:
    Jun 23, 2017
    Posts:
    60
    Am I doing something wrong? Why doesn't it work?

    EntityManager entityManager = gameObject.AddComponent<EntityManager>(_entitySO);



    public class EntityManager : MonoBehaviour<EntitySO> {

    protected override void Init(EntitySO entitySO)
    {
    Debug.Log("Yeah!");
    }
    }


    Doesn't work!
     
  21. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Hi @AldeRoberge !

    When using AddComponent you need to also specify the types of the parameters on the Init function in addition to the type of the added component.
    Code (CSharp):
    1. EntityManager entityManager = gameObject.AddComponent<EntityManager, EntitySO>(_entitySO);
    Alternatively you can use the AddComponent overload with the
    out
    modifier. In this case the compiler can figure out all generic types from the types of the variables you pass to the method.
    Code (CSharp):
    1. gameObject.AddComponent(out EntityManager entityManager, _entitySO);
     
  22. trueh

    trueh

    Joined:
    Nov 14, 2013
    Posts:
    74
    Hi there!

    Speaking about Odin Inspector. I am trying Init(args) in a project which uses it. Everything seems to work fine but no Odin attributes are shown in the components which use Init(args).

    Is there any way to use Oding attributes together with Init(args)?.

    Thank you in advance.
     
  23. trueh

    trueh

    Joined:
    Nov 14, 2013
    Posts:
    74
    Sorry, I forgot to update Odin preferences. If I enable Odin for Init(args) it happens what another user was talking about. The Init section in the component is not shown.

    Regards.
     
  24. SisusCo

    SisusCo

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

    So the compatibility issue is still happening... Thanks for the heads up, I'll look into it again, see if I can replicate it this time around and implement a more robust fix.
     
  25. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Hello Timo,

    Hope you're doing well! I'm having here a problem with the StateMachineBehaviour.


    Code (CSharp):
    1. public class JaneMovementStateMachineBehaviour : StateMachineBehaviour<IJaneAnimatorWrapper, IJaneParameters>
    2.     {
    3.  
    4.         private readonly IJaneParameters _parameters;
    5.         private readonly IJaneAnimatorWrapper _animatorWrapper;
    6.  
    7.         private CoroutineHandle _prepareToShowRunAnimationCoroutine;
    8.  
    9.         protected override void Init(
    10.             IJaneAnimatorWrapper animatorWrapper,
    11.             IJaneParameters parameters)
    12.         {
    13.             this[nameof(_animatorWrapper)] = animatorWrapper;
    14.             this[nameof(_parameters)] = parameters;
    15.  
    16.             _prepareToShowRunAnimationCoroutine = new CoroutineHandle();
    17.         }
    18.  
    19.         public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    20.         {
    21.             try
    22.             {
    23.                 base.OnStateEnter(animator, stateInfo, layerIndex);
    24.  
    25.                 _prepareToShowRunAnimationCoroutine = Timing.RunCoroutine(PrepareToShowRunAnimationCoroutine());
    26.             }
    27.             catch (Exception exception)
    28.             {
    29.                 Debug.LogException(exception);
    30.             }
    31.         }
    32.  
    33.         public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    34.         {
    35.             try
    36.             {
    37.                 base.OnStateExit(animator, stateInfo, layerIndex);
    38.  
    39.                 if (_prepareToShowRunAnimationCoroutine.IsRunning)
    40.                 {
    41.                     Timing.KillCoroutines(_prepareToShowRunAnimationCoroutine);
    42.                 }
    43.  
    44.                 _animatorWrapper.Run(false);
    45.             }
    46.             catch (Exception exception)
    47.             {
    48.                 Debug.LogException(exception);
    49.             }
    50.         }
    51.  
    52.         private IEnumerator<float> PrepareToShowRunAnimationCoroutine()
    53.         {
    54.             yield return Timing.WaitForSeconds(_parameters.TimeToRun);
    55.  
    56.             _animatorWrapper.Run(true);
    57.         }
    I'm having a NullReferenceException when the _parameters.TimeToRun is called on PrepareToShowRunAnimationCoroutine.

    I saw that now the inspector doesn't get drawn well, and I reinstalled Init(Args), and the Animator displayed correcly again(with null arguments on the initializer). After hitting play button it turned again like this:
    snip.png

    Do you know what it can be?

    Thank you so much for the help!

    Best Regards,
    Eduardo bagarrão
     
    Last edited: Nov 17, 2022
  26. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    @BagarraoEduardo Hey!

    Looks like there's probably handling missing for the target being a destroyed instance in the custom editor code for the Animator.

    I'll try to replicate this locally and implement a fix.
     
  27. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Hi Timo,

    Thank you so much!

    Best Regards,
    Eduardo Bagarrão
     
  28. Lucas_Gaspar

    Lucas_Gaspar

    Joined:
    Sep 27, 2012
    Posts:
    5
    It is possible to inject args into pure C# class? How I can do this?
    I have 3 scripts, ServerTimeUI, ServerTimeHandler and TimeProvider.
    ServerTimeUI is a MonoBehaviour and receives the pure C# class ServerTimeHandler.
    ServerTimeHandler requires a reference to the TimeProvider which is also a pure C#.
    This is what I'm missing, how can I inject the TimeProvider to the ServerTimeHandler ?
    Code (CSharp):
    1. using Sisus.Init;
    2. using TMPro;
    3. using UniRx;
    4.  
    5. class ServerTimeUI : MonoBehaviour<IServerTimeHandler, TMP_Text>
    6. {
    7.     private IServerTimeHandler _serverTime;
    8.     private TMP_Text _text;
    9.     private CompositeDisposable _disposables = new CompositeDisposable();
    10.  
    11.     protected override void Init(IServerTimeHandler serverTime, TMP_Text text)
    12.     {
    13.         _serverTime = serverTime;
    14.         _text = text;
    15.     }
    16.  
    17.     private void Start()
    18.     {
    19.         var disposable = _serverTime.GetServerTimeObservable().Subscribe(x =>
    20.         {
    21.             _text.text = x.Value.ToString();
    22.         });
    23.  
    24.         _disposables.Add(disposable);
    25.     }
    26.  
    27.     private void OnDestroy()
    28.     {
    29.         _disposables.Dispose();
    30.     }
    31. }
    Code (CSharp):
    1. using System;
    2. using Cysharp.Threading.Tasks;
    3. using UniRx;
    4.  
    5. [Serializable]
    6. public class ServerTimeHandler : IServerTimeHandler
    7. {
    8.     private ITimeProvider _timeProvider;
    9.  
    10.     public IObservable<DateTime?> GetServerTimeObservable()
    11.     {
    12.         return _timeProvider.GetTimeAsync().ToObservable();
    13.     }
    14. }
    Code (CSharp):
    1. using System;
    2. using Cysharp.Threading.Tasks;
    3. using Newtonsoft.Json;
    4. using UnityEngine;
    5. using UnityEngine.Networking;
    6.  
    7. [Serializable]
    8. public class TimeProvider : ITimeProvider
    9. {
    10.     public class DateTimeServer
    11.     {
    12.         public DateTime dateTime;
    13.     }
    14.  
    15.     public async UniTask<DateTime?> GetTimeAsync()
    16.     {
    17.         using (UnityWebRequest request = UnityWebRequest.Get("https://timeapi.io/api/Time/current/zone?timeZone=UTC"))
    18.         {
    19.             await request.SendWebRequest();
    20.  
    21.             if (request.result != UnityWebRequest.Result.Success)
    22.             {
    23.                 Debug.Log(request.error);
    24.                 return null;
    25.             }
    26.             else
    27.             {
    28.                 return JsonConvert.DeserializeObject<DateTimeServer>(request.downloadHandler.text)?.dateTime;
    29.             }
    30.         }
    31.     }
    32. }
     
    Last edited: Jan 5, 2023
  29. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Hey @Lucas_Gaspar !

    This is certainly possible. There are a couple of different ways to achieve this you can choose from:


    Option 1: Make both ServerTimeHandler and TimeProvider into services

    The most straight-forward way to achieve this is by making both ServerTimeHandler and TimeProvider into global services by adding the [Service] attribute to both.
    Code (CSharp):
    1. [Service(typeof(ITimeProvider))]
    2. public class TimeProvider : ITimeProvider { }
    3.  
    4. [Service(typeof(IServerTimeHandler))]
    5. public class ServerTimeHandler : IServerTimeHandler
    6. {
    7.     private readonly ITimeProvider timeProvider;
    8.  
    9.     public ServerTimeHandler(ITimeProvider timeProvider) => this.timeProvider = timeProvider;
    10. }
    Init(args) will then automatically create instances of both objects during initialization.

    It detects that ServerTimeHandler depends on the TimeProvider, and thus creates TimeProvider first and injects it to ServerTimeHandler.

    After this the fully initialized ServerTimeHandler service gets automatically injected to all
    MonoBehaviour<T...>
    clients that need it.

    [Service].png


    Option 2: Create a ServiceInitializer for ServerTimeHandler

    If you want ServerTimeHandler to be a service, but do not want to make TimeProvider into one, you can create a ServiceInitializer for ServerTimeHandler that handles creating the ServerTimeHandler instance and passing it its dependencies.
    Code (CSharp):
    1. [Service(typeof(IServerTimeHandler))]
    2. public class ServerTimeHandlerInitializer : ServiceInitializer<ServerTimeHandler>
    3. {
    4.     public override ServerTimeHandler InitTarget() => new ServerTimeHandler(new TimeProvider());
    5. }
    6.  
    7. public class TimeProvider : ITimeProvider { }
    8.  
    9. public class ServerTimeHandler : IServerTimeHandler
    10. {
    11.     private readonly ITimeProvider timeProvider;
    12.  
    13.     public ServerTimeHandler(ITimeProvider timeProvider) => this.timeProvider = timeProvider;
    14. }
    This approach requires a bit more code, but also gives you more control over how the service is initialized.

    It also makes it possible to decouple the service itself from the Init(args) framework, since the ServiceAttribute is added to the initializer instead of the service class itself.


    Option 3: Create a Wrapper for the plain old C# object

    A third option is to generate a wrapper component for ServerTimeHandler.
    Code (CSharp):
    1. [AddComponentMenu("Server Time Handler")]
    2. public class ServerTimeHandlerComponent : Wrapper<ServerTimeHandler> { }
    3.  
    4. [Serializable]
    5. public class ServerTimeHandler : IServerTimeHandler
    6. {
    7.     private readonly ITimeProvider timeProvider;
    8.     public ServerTimeHandler(ITimeProvider timeProvider) => this.timeProvider = timeProvider;
    9. }
    10.  
    11. [Serializable]
    12. public class TimeProvider : ITimeProvider { }
    With this it becomes possible to also generate an initializer component for it, which can be used to specify all its Init arguments via the inspector, just as if it was a normal component.
    Code (CSharp):
    1. public class ServerTimeHandlerComponentInitializer : WrapperInitializer<ServerTimeHandlerComponent, ServerTimeHandler, TimeProvider>
    2. {
    3.     protected override ServerTimeHandler CreateWrappedObject(TimeProvider timeProvider) => new ServerTimeHandler(timeProvider);
    4. }
    As the final step, you can use the Value Provider functionality to make it possible to drag-and-drop the ServerTimeHandlerComponent component to the ServerTimeUI component's Init section.

    Just make the wrapper component implement
    IValueProvider<IServerTimeHandler>
    to unlock this ability.
    Code (CSharp):
    1. [AddComponentMenu("Server Time Handler")]
    2. public class ServerTimeHandlerComponent : Wrapper<ServerTimeHandler>, IValueProvider<IServerTimeHandler>
    3. {
    4.     public IServerTimeHandler Value => WrappedObject;
    5. }
    wrapper.png

    This approach can be great if you prefer to see visual representations of your classes in the scene hierarchy and be able to assign references using drag-and-drop in the Inspector. It can be especially useful if you need to pass scene object or asset references to your plain old C# objects during initialization.


    Hope this helps!
     
    Last edited: Jan 6, 2023
  30. Lucas_Gaspar

    Lucas_Gaspar

    Joined:
    Sep 27, 2012
    Posts:
    5
    @SisusCo This was extremely useful, thank you a lot! :D
     
    SisusCo likes this.
  31. Lucas_Gaspar

    Lucas_Gaspar

    Joined:
    Sep 27, 2012
    Posts:
    5
    It is possible to Inject an interface of a pure C# directly into a ScriptableObject?
     
  32. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    @Lucas_Gaspar ScriptableObject support is still pretty underdeveloped at the moment unfortunately.
    I want to implement full initializer support for them at some point, but that's not in yet.


    At the moment I would recommend one of two approaches when it comes to scriptable objects:

    1. Init using an Initializer component

    You could initialize your scriptable object asset in the initializer of a component that utilizes the scriptable object asset.

    Let's say you needed to inject a plain old C# object Logger to a scriptable object Settings, which was used by a component Player, then your initializer could look something like this:
    Code (CSharp):
    1. internal sealed class PlayerInitializer : Initializer<Player, Settings>
    2. {
    3.     protected override Player InitTarget(Settings settings)
    4.     {
    5.         if(!settings.IsInitialized)
    6.         {
    7.             settings.Init(new Logger()); // Inject dependencies here
    8.         }
    9.  
    10.         return base.InitTarget(settings);
    11.     }
    12. }
    A downside with this approach is that if you need to inject the same scriptable object asset into multiple different components, you would need to repeat the same initialization logic in multiple places.
    So I would probably mostly only consider using this pattern if the scriptable object asset clearly relates to just one component in particular.


    2. Init using an IValueProvider

    You could create a second scriptable object asset that implements IValueProvider, and have it handle initializing your scriptable object asset lazily whenever it is first requested by any client.
    Code (CSharp):
    1. [CreateAssetMenu]
    2. public class SettingsAsset : ScriptableObject, IValueProvider<Settings>
    3. {
    4.     [SerializeField]
    5.     private Settings settings;
    6.  
    7.     public Settings Value => GetTargetInitialized();
    8.     object IValueProvider.Value => GetTargetInitialized();
    9.  
    10.     private Settings GetTargetInitialized()
    11.     {
    12.         if(!settings.IsInitialized)
    13.         {
    14.             settings.Init(new Logger()); // Inject dependencies here
    15.         }
    16.  
    17.         return settings;
    18.     }
    19. }
    Then you would drag this IValueProvider to all your components Initializers' instead.

    IValueProvider.png

    You could also have the IValueProvider return a plain old C# object instead of another scriptable object if you wanted. This might be a good idea to avoid the scriptable object asset ever being used anywhere accidentally without having the IValueProvider in the middle.

    IValueProvider for POCO.png
     
  33. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Version 1.2.2 of Init(args) has been submitted to the Asset Store.

    New in this update:

    ScriptableObject Initializers

    It is now also possible to generate initializers for classes that derive from ScriptableObject<T...>.

    Initializers can be generated and attached to scriptable object assets using + button in their Init section, or by selecting Generate Initializer from their context menu.

    scriptable-object-add-initializer.gif

    Just like their component-based cousins, the initializers make it easy to specify what arguments should be passed to the object's Init function during initialization.

    The arguments are passed to the Init function either during the scriptable object's Awake event, or when services become ready - which ever occurs later.

    Note that in the editor ScriptableObjects' Awake event can occur already in edit mode. In such cases, the scriptable objects will be re-initialized every time when entering play mode, right after services become ready, just before the open scenes are loaded.

    Service Injection Support

    Service instances are automatically resolved for scriptable object initializers as well, so you don't need to manually assign them each time.

    scriptable-object-initializer-service.gif

    As always, if the need should ever arise for you to replace a service type Init argument with some other instance, you can do so simply by dragging-and-dropping the other instance on top of the Service tag.

    scriptable-object-initializer-replace-service.gif

    This gives you a nice blend of singleton-esque convenience, combined with the unparalleled flexibility of dependency injection.
     
    Last edited: Feb 6, 2023
    Lucas_Gaspar likes this.
  34. ch1ky3n

    ch1ky3n

    Joined:
    Oct 26, 2017
    Posts:
    57
    It gave 3 errors if you installed Odin Inspector before importing init<arg>, to reproduce just import odin inspector and Init<Arg>, in unity 2022.2.13
    sorry I couldn't copy paste the stack error, since I'm not at home at the moment.
     
  35. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    @ch1ky3n I'll investigate this - thanks for letting me know!
     
  36. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    A fix for the errors in Unity 2022.2 has been submitted to the asset store for review.
     
  37. Kelkhaun

    Kelkhaun

    Joined:
    Dec 25, 2022
    Posts:
    2
    upload_2023-5-17_15-43-39.png

    Build error. Is there any way to fix this?
     
  38. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Hey @Kelkhaun - thank you for the heads up! I've sent you a PM containing a fix for the issue, and I'll also submit an update to the asset store.
     
  39. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,965
    I was experimenting with copies of
    MonoBehaviour<T...>
    that inherited from Odin's
    SerializedMonoBehaviour
    class instead of the base
    MonoBehaviour
    . At a glance these
    SerializedMonoBehaviour<T...>
    classes appear to be working (
    Dictionary<>
    was the test) but having just started with Init(args) I was wondering what else besides these and their editor classes need to be modified or if it that is sufficient to make them work?
     
  40. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Hey @Ryiah !

    Creating the
    SerializedMonoBehaviour<T...>
    classes should be all that you need to do. Everything else should work automatically, including the custom editor handling, since the framework is programmed around the
    IInitializable<T...>
    interface, and not
    MonoBehaviour<T...>
    specifically.

    The
    SerializedMonoBehaviour<T...>
    base classes are a great idea actually... I could perhaps add a unitypackage to Init(args) containing those by default in the next update.
     
    Ryiah likes this.
  41. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,965
    The
    SerializedScriptableObject<T...>
    base classes would be great too.
     
    SisusCo likes this.
  42. Slashbot64

    Slashbot64

    Joined:
    Jun 15, 2020
    Posts:
    313
    Code (CSharp):
    1. Assets\Sisus\Init(args)\Scripts\Editor\InitializableEditorInjector.cs(43,13): error CS0246: The type or namespace name 'InitializerBaseInternal<>' could not be found (are you missing a using directive or an assembly reference?)
    2.  
    3.  
    2023.1.0b17
     
  43. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Thanks for letting me know about the error @Slashbot64 ! I submitted a fix for version 2023.1+ to the asset store.
     
  44. Slashbot64

    Slashbot64

    Joined:
    Jun 15, 2020
    Posts:
    313
    what is the fix? or is it up on the store already?
     
  45. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    @Slashbot64 The update is live in the asset store now.
     
    Slashbot64 likes this.
  46. Kelkhaun

    Kelkhaun

    Joined:
    Dec 25, 2022
    Posts:
    2
    Does asset work in WEB GL?
     
  47. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Yes. I tested creating a WebGL build from the Demo scene, and it looks like everything works as expected when using Disabled or Low stripping level.

    webgl-test.gif

    If stripping level was raised to Medium or High then some things stopped working. I'll need to do some more investigating to determine what's causing this.

    Edit: it appears that this might be unavoidable. The reflection-based initialization of services seems to stop working when using Medium stripping level, even when the [Preserve] attribute has been added to all the relevant classes and class members.
     
    Last edited: Jun 6, 2023
  48. Slashbot64

    Slashbot64

    Joined:
    Jun 15, 2020
    Posts:
    313
    Code (CSharp):
    1. Assets\Sisus\Init(args)\Scripts\Services\ScopedServiceT.cs(92,79): error CS1061: 'ScopedService<TService>.Instance' does not contain a definition for 'registerer' and no accessible extension method 'registerer' accepting a first argument of type 'ScopedService<TService>.Instance' could be found (are you missing a using directive or an assembly reference?)
    When trying to build windows?
     
  49. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    @Slashbot64 I noticed yesterday as well that the 2023.1 support update introduced a new build-time only error. I sent you a PM with the fix and submitted it to the asset store for review.
     
  50. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Hey @Skyswimsky !

    I did indeed find a bug, where services would get subscribed to the Update event twice if they were registered with the ServiceAttribute using an interface as the service type.
    I'll submit a fix for this shortly - thanks for letting me know about it!
     
    Skyswimsky likes this.