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

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,344


    Init(args) is a toolset that makes it possible to pass arguments to Objects from the outside during initialization.

    The set of tools have been seamlessly integrated into Unity using extension methods and custom inspector code, so that they are as intuitive to learn and use as possible.


    Did you ever wish you could just call AddComponent with arguments like this:
    Code (CSharp):
    1. Player player = gameObject.AddComponent<Player, IInputManager>(inputManager);
    Or maybe you’ve sometimes wished you could Instantiate with arguments like so:
    Code (CSharp):
    1. Player player = playerPrefab.Instantiate(inputManager);
    Init(args) enables you to do just this: inject upto twelve arguments to any Object during their initialization - with a remarkable amount of flexibility on how to do it!

    Main Features
    • Add Component with arguments.
    • Instantiate with arguments.
    • Create new GameObject with multiple components and arguments.
    • Create ScriptableObject with arguments.
    • Specify arguments in the Inspector - with full interface support and automatic null validation.
    • Service system - a cleaner and more flexible replacement for singletons.
    • Attach plain old C# objects to GameObjects via simple wrapper components.
    • Assign to read-only fields and properties.
    • Type safety thanks to use of generics.
    • Dependencies injected using pure interface and constructor injection: no costly reflection necessary!
    • Make all your code easily unit testable by default.
    • Write loosely coupled, reusable code that follows SOLID principles.

    Invert Your Dependencies

    To fully appreciate how Init(args) can help to significantly improve your code architecture, one needs to understand a key principle in software engineering: inversion of control.

    Your scripts often rely on some other scripts to work properly; these are called their dependencies.
    A common and straight-forward way to retrieve references to those other scripts in Unity is using the singleton pattern or similar approaches. For example consider the following script:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class Player : MonoBehaviour
    4. {
    5.    private void Update()
    6.    {
    7.       if(InputManager.Instance.Input.y > 0f)
    8.       {
    9.          float speed = 0.2f;
    10.          float distance = Time.deltaTime * speed;
    11.          transform.Translate(Camera.main.transform.forward * distance);
    12.       }
    13.    }
    14. }
    While using this method does accomplish the job of retrieving the instance, it also comes bundled with some severe negative side effects that may end up hurting you in the long run - especially so in larger projects.
    • It can cause the dependencies of a class to be hidden, scattered around the body of the class, instead of all of them being neatly defined in one centralized place and tied to the creation of the object. This can leave you guessing about what prerequisites need to be met before all the methods of a class can be safely called. So while you can always use GameObject.AddComponent<Player>() to create an instance of a Player, it’s not apparent that an InputManager component and a main camera might also need to exist somewhere in the scene for things to work. This hidden web of dependencies can result in order of execution related bugs popping up as your project increases in size.
    • It tends to make it close to impossible to write good unit tests; if the Player class depends on the InputManager class in specific, you can’t swap it with a simpler mock implementation that you can control during testing.
    • Tight coupling with specific classes can make it a major pain to refactor your code later. For example let’s say you wanted to switch all classes in your code base from using the old InputManager to a different one; you would need to modify all classes that referenced the old class, which could potentially mean changing code in hundreds of classes.
    • Tight coupling with specific classes also means less potential for modularity. For example you can’t as easily swap all your classes to using MobileInputManager on mobile platforms and PCInputManager on PC platforms. This limitation can lead to having bulky classes that handle a bunch of stuff instead of having lean modular classes that you can swap to fit the current situation.
    • Tight coupling with specific classes leads to less flexibility. If your code uses Logger.Instance to log messages to the Console, you can't easily swap a single instance to use DebugLogger instead for logging more detailed information to the Console.
    • Tight coupling can also make it impossible to move classes from one project to another. Let’s say you start working on a new game and want to copy over the Camera system you spent many a month perfecting in your previous project. Well if your CameraController class references three other specific classes, and they all reference three other specific classes and so forth, that might leave you with no choice but to start over from scratch.
    This is where inversion of control comes in: instead of your scripts resolving their dependencies with specific implementations on their own, they will work with whatever objects are provided to them from the outside.

    Now let's take a look at this code that achieves inversion of control with the help of Init(args):

    Code (CSharp):
    1. using UnityEngine;
    2. using Sisus.Init;
    3.  
    4. public class Player : MonoBehaviour<IInputManager, Camera>
    5. {
    6.     private IInputManager inputManager;
    7.     private Camera camera;
    8.  
    9.     protected override void Init(IInputManager inputManager, Camera camera)
    10.     {
    11.         this.inputManager = inputManager;
    12.         this.camera = camera;
    13.     }
    14.  
    15.     private void Update()
    16.     {
    17.         if(inputManager.Input.y > 0f)
    18.         {
    19.             float speed = 0.2f;
    20.             float distance = Time.deltaTime * speed;
    21.             transform.Translate(camera.transform.forward * distance);
    22.         }
    23.     }
    24. }
    This small shift in how you architect your scripts does away with all of the aforementioned down sides with the previous example:
    • The Init function makes it very clear what other objects are needed for the Player class to function: an IInputManager and a Camera. Even if the Player class eventually grew to be 1000+ lines long, you could still find all the objects it depends on just by taking a look at the Init function, instead of having to read through the entire script.
    • The class no longer references the InputManager class directly, but instead communicates through the IInputManager interface. This makes it very easy to swap to using a different IInputManager implementation (for example during unit testing).
    • If you should go and modify the Player class in the future and introduce a new dependency to it, you will instantly get compile errors from all existing classes that are trying to create an instance of the Player class without providing this necessary dependency - this makes it really easy to find all the places you need to go modify to ensure the Player object gets set up properly in all places in your code base. If you were using the regular AddComponent without any arguments you would not have this safety net.
    But then if you're not using a singleton, how exactly do you determine what instance should be provided to the Player object?

    An easy way to do this in code is to add the [Service] attribute to the class you want to use.

    Code (CSharp):
    1. [Service(typeof(IInputManager), FindFromScene = true)]
    2. public class InputManager : MonoBehaviour, IInputManager
    3. {
    This let's Init(args) know that all components which require an IInputManager should be provided a reference to a single shared instance of InputManager from the scene.

    All services are marked with a Service tag in the Inspector to make it easier for developers to understand how dependencies are wired together by looking at either the code or the Inspector.

    service-tag.png

    A matching tag will also be shown in Init sections of components that accept an IInputManager argument, to indicate that the instance will be automatically provided, and there's no need to manually drag-and-drop it in.

    init-arguments.png

    The Service tag can also be clicked to highlight the object that defines the service; in this case the InputManager script that contains the Service attribute. This further helps developers keep track of how dependencies are wired together.

    ping-service-definition.png

    Whenever the need arises, you can easily swap a particular component to use some other Object besides the shared service, simply by dragging and dropping another Object into the argument field.

    override-service.png

    Replacing a service with a mock during unit tests is also very easy.
    Code (CSharp):
    1. using UnityEngine;
    2. using NUnit.Framework;
    3. using Sisus.Init;
    4. using Sisus.Init.Testing;
    5.  
    6. public class TestPlayer
    7. {
    8.     private class MockInputManager : IInputManager
    9.     {
    10.         public Vector2 Input { get; set; }
    11.     }
    12.  
    13.     private MockInputManager inputManager;
    14.     private Player player;
    15.     private Testable testable;
    16.  
    17.     [SetUp]
    18.     public void Setup()
    19.     {
    20.         inputManager = new MockInputManager();
    21.         player = new GameObject<Player, Camera>().Init1(inputManager as IInputManager, Second.Component);
    22.         testable = new Testable(player.gameObject);
    23.     }
    24.  
    25.     [TearDown]
    26.     public void TearDown() => testable.Destroy();
    27.  
    28.     [Test]
    29.     public void LeftInput_Moves_Player_Left()
    30.     {
    31.         var startPosition = player.transform.position;
    32.         inputManager.Input = Vector2.left;
    33.         testable.Update();
    34.         var endPosition = player.transform.position;
    35.         Assert.AreEqual(startPosition.y, endPosition.y);
    36.         Assert.Less(endPosition.x, startPosition.x);
    37.     }
    38. }
    You can also turn any component into a service without needing to make any code changes - a feature which can be particularly useful when you don't have access to the component's source code.

    Simply select Make Service Of Type... from a component's context menu and pick the service type (class or interface type which determines the fields into which the service should be injected).

    make-service-of-type.png


    For more information about Init(args) head on over to the Asset Store page, refer to the Documentation or check out the Scripting Reference.
     
    Last edited: Apr 3, 2024
    TeagansDad likes this.
  2. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Version 1.0.1 of Init(args) has been submitted to the Asset Store (should be live within a couple of days).

    New features in the update:

    ConstructorBehaviour<T...>

    A new base class that can be used instead of MonoBehaviour<T...> with support for assigning received arguments into read-only fields and get-only properties without the need for any reflection to be used!

    To do this derive from a ConstructorBehaviour<T...> base class and define a parameterless constructor which receives the initialization arguments from the constructor in the base class via its out parameters.
    Code (CSharp):
    1. using UnityEngine;
    2. using Sisus.Init;
    3.  
    4. public class Player : ConstructorBehaviour<IInputManager, Camera>
    5. {
    6.     private readonly IInputManager inputManager;
    7.  
    8.     public Camera Camera { get; }
    9.  
    10.     public Player() : base(out IInputManager inputManager, out Camera camera)
    11.     {
    12.         this.inputManager = inputManager;
    13.         Camera = camera;
    14.     }
    15. }
    While with MonoBehaviour<T...> providing arguments during initialization is optional, with ConstructorBehaviour<T...> this is mandatory; an exception will be thrown if an instance is instantiated without the arguments it requires. The idea behind this is to mimic the way constructors work with plain old C# objects, allowing you to restrict object creation to only be allowed when all the required dependencies are provided.
    Code (CSharp):
    1. // This raises an exception:
    2. var player = gameObject.AddComponent<Player>();
    Code (CSharp):
    1. // This works:
    2. var inputManager = Service<IInputManager>.Instance;
    3. var camera = Camera.main;
    4. var player = gameObject.AddComponent<Player, IInputManager, Camera>(inputManager, camera);
    This new base class is mainly intended to be used for objects that are dynamically instantiated at runtime using AddComponent(args) or Instantiate(args), in which case the created instance is able to receive the arguments passed to the method in its constructor.

    It is also possible to add ConstructorBehaviours directly into a scene in the Editor and provide arguments for them using Initializer components, just like you can with MonoBehaviour<T...>. However, in this situation the arguments can not be assigned through the constructor and have to instead be passed in through the Init method and assigned to the target fields using reflection. This means that performance isn't as optimal as it would be when no reflection is involved. However this whole process happens automatically behind the scenes, and unless you have a very large number of ConstructorBehaviours in your scenes this probably isn't a big deal.

    Another thing to be aware of with ConstructorBehaviours is that the constructor gets called before Unity's deserialization process takes place. This means that if you instantiate a ConstructorBehaviour from a prefab and assign any arguments into serialized fields, those will get overridden when the deserialization takes place. Because of this it is recommended to only assign arguments into non-serialized fields or properties in the constructor.
    That notwithstanding, assigning arguments into serialized fields in ConstructorBehaviours does still work; the ConstructorBehaviour base class will detect when this happens and automatically handle re-injecting the arguments into the fields once the deserialization process has finished. However this injection also utilizes reflection, so it's still better to avoid it when you can.

    Null / NullOrInactive

    Init(args) has been designed with inversion of control in mind and aims to make it as easy as possible to work with interfaces instead of specific classes in Unity.

    One pain point when using interfaces in Unity is that checking them for null can be problematic. This is because in addition to being actually null, Objects in Unity can also be destroyed. You usually don't even notice the difference when working with specific classes directly, because the Object class has a custom == operator which internally also checks whether the Object has been destroyed. However when an Object has been assigned to an interface type variable and this is compared against null, the overloaded == operator does not get used. This can result in null reference exceptions when trying to access the members of a destroyed Object.
    Code (CSharp):
    1. Player player = FindObjectOfType<Player>();
    2. IPlayer iplayer = player as IPlayer;
    3.  
    4. Destroy(player);
    5.  
    6. Debug.Log(player == null);  // Prints true
    7. Debug.Log(iplayer == null); // Prints false
    To help with this problem in version 1.0.1 a new property has been added into every base class in Init(args): Null. When an interface type variable is compared against this Null property it functions just like the overloaded == operator in the Object class and returns true when the instance has been destroyed.
    Code (CSharp):
    1. Player player = FindObjectOfType<Player>();
    2. IPlayer iplayer = player as IPlayer;
    3.  
    4. Destroy(player);
    5.  
    6. Debug.Log(player == null);  // Prints true
    7. Debug.Log(iplayer == null); // Prints false
    8. Debug.Log(iplayer == Null); // Prints true
    It is also possible to utilize this safe null checking inside classes that don't derive from any of the base classes in Init(args) by importing the static members of the NullExtensions class.
    Code (CSharp):
    1. using UnityEngine;
    2. using static Sisus.NullExtensions;
    3.  
    4. public static class PlainOldClass
    5. {
    6.     public static void Example()
    7.     {
    8.         Player player = Object.FindObjectOfType<Player>();
    9.         IPlayer iplayer = player as IPlayer;
    10.  
    11.         Object.Destroy(player);
    12.  
    13.         Debug.Log(player == null);  // Prints true
    14.         Debug.Log(iplayer == null); // Prints false
    15.         Debug.Log(iplayer == Null); // Prints true
    16.     }
    17. }
    In addition to the Null property there's also a new NullOrInactive property. This functions identically to the Null property but also checks if the Object is a component on an inactive GameObject.

    Code (CSharp):
    1. Player player = FindObjectOfType<Player>();
    2. IPlayer iplayer = player as IPlayer;
    3.  
    4. player.gameObject.SetActive(false);
    5.  
    6. Debug.Log(player.gameObject.activeInHierarchy); // Prints true
    7. Debug.Log(iplayer == NullOrInactive); // Prints true
    This functionality can be particularly useful when utilizing Object Pooling and an inactive GameObject is meant to represent an object which doesn't exist.
     
    Last edited: Dec 27, 2021
  3. BagarraoEduardo

    BagarraoEduardo

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

    Hope you're well. I've sent this question on the support form, but I'll sent here as I can show more precisely the problem with prints.

    I'm also using Corgi Game engine(and other plugins), and Corgi relies a lot on singleton MonoBehaviors to add to hierarchy. I've been having problems adding the arguments to the classes, as it appears the following warnings(I send in the image). I've used the demo scene as reference, but I can't add the dependencies the way they're added in the scene idk why

    But my main question is, besides adding a singleton dependency, how can I add them to a script from hierarchy? Because I need the settings that are set up on hierarchy and I would like to drag them to the dependencies that way.

    Thank you in advance!

    Eduardo Bagarrão
     

    Attached Files:

  4. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Hey @BagarraoEduardo

    The warning message seems to have happened from trying to drag the Script asset of the LevelManager to the Initializer field, when it is expecting a Component instance from the Hierarchy instead.

    If the LevelManager component is only created at runtime you can create an IValueProvider component for the Object and drag that to the Initializer field instance:

    Code (CSharp):
    1. using Sisus.Init;
    2. using UnityEngine;
    3. using MoreMountains.CorgiEngine;
    4.  
    5. public class LevelManagerWeakReference : MonoBehaviour, IValueProvider<LevelManager>
    6. {
    7.     public LevelManager Value => LevelManager.Instance;
    8. }
    You could even combine multiple IValueProvider implementations in just one component if you'd prefer that:

    Code (CSharp):
    1. using Sisus.Init;
    2. using UnityEngine;
    3. using MoreMountains.CorgiEngine;
    4. public class CorgiServiceProvider : MonoBehaviour, IValueProvider<GameManager>, IValueProvider<LevelManager>, IValueProvider<GUIManager>, IValueProvider<InputManager>
    5. {
    6.     GameManager IValueProvider<GameManager>.Value => GameManager.Instance;
    7.     LevelManager IValueProvider<LevelManager>.Value => LevelManager.Instance;
    8.     GUIManager IValueProvider<GUIManager>.Value => GUIManager.Instance;
    9.     InputManager IValueProvider<InputManager>.Value => InputManager.Instance;
    10. }
    If the components already exist in the same scene in edit mode you should be able to just drag-and-drop them directly to the Object field.

    UPDATE: I tested it, and it seems like dragging a component into an Initializers from another GameObject doesn't currently work as expected. It tries to assign the dragged GameObject itself to the field instead of its components, and because of this thinks it's of incompatible type. This is probably what is causing your issues. I will improve this in the next update.

    Until the next update the only way to drag a Component from another GameObject is by opening two inspector windows, locking one of them to display the Initializer and the other one to display the Level Manager and then dragging the Level Manager component from one Inspector to the other.
     
    Last edited: Dec 26, 2021
    BagarraoEduardo likes this.
  5. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Init(args) version 1.0.2 is now live in the Asset Store.

    Bug fixes in the update:
    • Fixed issue with Initializers / Any<T> not being assigned a component value from a GameObject which was drag-and-dropped to them using the Inspector, even if the GameObject contained a component that was of an assignable type.
    • Fixed issue with Initializers / Any<T> not accepting UnityEngine.Object references that implement IValueProvider<T> when they were drag-and-dropped to them using the Inspector.

    New features in the update:


    Inspector Runtime State

    All base classes in Init(args), such as MonoBehaviour<T...>, now have a Custom Editor that visualizes all non-serialized fields of the Object in the Inspector in Play Mode.

    This makes it possible to see the state of read-only fields and properties assigned to in the constructor of a ConstructorBehaviour<T...>.

    It also makes it possible to see the fields of a plain old class object that is wrapped by a Wrapper component even in cases where the wrapped object doesn't have the Serializable attribute.

    runtime-state.png
     
    Last edited: Dec 27, 2021
  6. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38

    Confirmed that the update solved the issue that I was having. Thank you for the help! ;)
     
    SisusCo likes this.
  7. Slashbot64

    Slashbot64

    Joined:
    Jun 15, 2020
    Posts:
    344
    Bit of impulse buy here maybe more examples tutorials to get into this method of doing things

    anyway Windows Build errors...

    Code (CSharp):
    1. Assets\Sisus\Init(args)\Scripts\Initializer\InitializerT1.cs(6,15): error CS0246: The type or namespace name 'EditorOnly' could not be found (are you missing a using directive or an assembly reference?)
    2.  
    3. Assets\Sisus\Init(args)\Scripts\Initializer\InitializerUtility.cs(17,15): error CS0246: The type or namespace name 'EditorOnly' could not be found (are you missing a using directive or an assembly reference?)
    4.  
    5. Assets\Sisus\Init(args)\Scripts\Initializer\WrapperInitializerT1.cs(8,15): error CS0246: The type or namespace name 'EditorOnly' could not be found (are you missing a using directive or an assembly reference?)
    6.  
    7. Assets\Sisus\Init(args)\Scripts\Utility\InitArgs.cs(11,15): error CS0246: The type or namespace name 'EditorOnly' could not be found (are you missing a using directive or an assembly reference?)
    8. UnityEditor.BuildPlayerWindow+BuildMethodException: 13 errors
    9.   at UnityEditor.BuildPlayerWindow+DefaultBuildMethods.BuildPlayer (UnityEditor.BuildPlayerOptions options) [0x002ca] in <2cd7ebc9c2ef4276a8edbc7de85c89ce>:0
    10.  
    11.   at UnityEditor.BuildPlayerWindow.CallBuildMethods (System.Boolean askForBuildLocation, UnityEditor.BuildOptions defaultBuildOptions) [0x00080] in <2cd7ebc9c2ef4276a8edbc7de85c89ce>:0
    12. UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
    13.  
    14.  
    15.  
     
  8. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    @Slashbot64 Thank you for letting me know about the build errors, I'll fix these and submit an update asap!

    I recommend taking a look at the included Demo scene to get a better feel about how one can use Init(args).
    But adding some more written examples is also a good idea :)
     
    Last edited: Jan 3, 2022
  9. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    An update that fixes the build errors has been submitted to the asset store for review.

    You can contact me if you need to get the fixed package before it has gone through the asset store review process (this typically takes a day or a few).


    UPDATE: The update is now live.
     
    Last edited: Jan 4, 2022
  10. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Version 1.0.3 of Init(args) is now out. Improvements in this update include:
    • Added new Services component. These can be attached to any Scene object or prefab and used to define a collection of services straight through the Inspector. Clients that can use the services can be filtered based on where they are in the scene hierarchy relative to the Services component. This component can be useful for defining localized services (e.g. only available inside a certain scene) as well as dynamic services (e.g. only available when a menu is open).
    • Services are now automatically initialized in such and order that services that are needed by other services get initialized before the services that need them! This can help avoid execution order related NullReferenceExceptions when trying to access services inside other services.
    • Added ability to control Initializer null argument validation in a more granular way, and improved edit mode validation in general to get rid of potentially false warnings.
    • Improved Initializer editor Type popup fields. They now have a search box for filtering the listed types and can display any number of types, which means things like System.Object fields are much better supported.
    • Added ability to automatically generate Initializers and Wrappers for classes from the MonoScript context menu.
     
  11. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Let me give a practical example of how the new Services component can be used to provide Init arguments to your components and help completely decouple all your components from each other.

    In this example we will create a system consisting of the following components:
    1. UIManager
      component which can be asked to show a Dialog to the user.
    2. Dialog
      component which handles passing the dialog text to the UI and invoking a callback if its Confirm method gets called.
    3. DialogButton
      component which handles calling Confirm on the Dialog if the user clicks a button.
    To start lets define the interfaces which our components can use to communicate with each other without knowing anything about each others' concrete classes:
    Code (CSharp):
    1. using System;
    2.  
    3. public interface IUIManager
    4. {
    5.     bool IsDialogOpen { get; }
    6.     void ShowDialog(string text, Action onConfirmed);
    7. }
    8.  
    9. public interface IDialog
    10. {
    11.     bool IsOpen { get; }
    12.  
    13.     void Show(string text, Action onConfirmed);
    14.     void Confirm();
    15.     void Cancel();
    16. }
    Then let's write the actual code for the components.

    We will make use of the MonoBehaviour<T...> base class in Init(args) to allow them to receive arguments during initialization.

    Code (CSharp):
    1. using Sisus.Init;
    2. using System;
    3.  
    4. public class UIManager : MonoBehaviour<IDialog>, IUIManager
    5. {
    6.     private IDialog dialog;
    7.  
    8.     public bool IsDialogOpen => dialog != Null && dialog.IsOpen;
    9.  
    10.     protected override void Init(IDialog dialog) => this.dialog = dialog;
    11.  
    12.     protected override void OnAwake() => CloseDialog();
    13.  
    14.     public void ShowDialog(string text, Action onConfirmed) => dialog.Show(text, onConfirmed);
    15.  
    16.     public bool CloseDialog()
    17.     {
    18.         if(IsDialogOpen)
    19.         {
    20.             dialog.Cancel();
    21.             return true;
    22.         }
    23.  
    24.         return false;
    25.     }
    26. }
    Code (CSharp):
    1. using Sisus.Init;
    2. using System;
    3.  
    4. public class Dialog : MonoBehaviour<IText>, IDialog
    5. {
    6.     private IText text;
    7.     private Action onConfirmed;
    8.  
    9.     public bool IsOpen => gameObject.activeInHierarchy;
    10.  
    11.     protected override void Init(IText text) => this.text = text;
    12.  
    13.     public void Show(string text, Action onConfirmed)
    14.     {
    15.         this.onConfirmed = onConfirmed;
    16.         this.text.Content = text;
    17.         gameObject.SetActive(true);
    18.     }
    19.  
    20.     public void Confirm()
    21.     {
    22.         onConfirmed?.Invoke();
    23.         gameObject.SetActive(false);
    24.     }
    25.  
    26.     public void Cancel() => gameObject.SetActive(false);
    27. }
    28.  
    Code (CSharp):
    1. using UnityEngine.UI;
    2. using Sisus.Init;
    3. using UnityEngine;
    4.  
    5. public class DialogButton : MonoBehaviour<IDialog, Button, DialogButtonType>
    6. {
    7.     private IDialog dialog;
    8.     private Button button;
    9.     private DialogButtonType type;
    10.  
    11.     protected override void Init(IDialog dialog, Button button, DialogButtonType type)
    12.     {
    13.         this.type = type;
    14.         this.dialog = dialog;
    15.         this.button = button;
    16.     }
    17.  
    18.     private void OnEnable() => button.onClick.AddListener(OnClicked);
    19.     private void OnDisable() => button.onClick.RemoveListener(OnClicked);
    20.  
    21.     private void OnClicked()
    22.     {
    23.         switch(type)
    24.         {
    25.             case DialogButtonType.Cancel:
    26.                 dialog.Cancel();
    27.                 return;
    28.             case DialogButtonType.Confirm:
    29.                 dialog.Confirm();
    30.                 return;
    31.             default:
    32.                 Debug.LogError($"Unrecognized ButtonType: {type}.", this);
    33.                 return;
    34.         }
    35.     }
    36. }
    We will also need to create an Initializer to pass all Init arguments for the DialogButton. It will not receive all its arguments automatically without an Initializer because it needs two arguments which we will not be setting up as services:
    Button
    and
    DialogButtonType
    .

    Code (CSharp):
    1. using Sisus.Init;
    2. using UnityEngine.UI;
    3.  
    4. public sealed class DialogButtonInitializer : Initializer<DialogButton, IDialog, Button, DialogButtonType> { }
    5.  
    Now we can build a menu system out of these components.

    We will use the Services component to turn the UIManager component into a service of type IUIManager.
    We will configure the Services component to allow all GameObjects nested under the UIManager to access the service.

    services-uimanager.png


    On the GameObject containing the Dialog component will we add two Services components.

    The first one we use turn the Dialog into a service which all GameObjects nested underneath the root UI GameObject can access via the IDialog interface.

    The second one we only use to provide the Init argument for the Dialog component: the IText component. This is just a simple wrapper for a component responsible for showing the text on the UI, such as UnityEngine.Text or TextMeshProUGUI.
    We configure
    For Clients
    to
    In Game Object
    so that the IText service is only access components in this same GameObject.
    (Note that it would probably actually make more sense to just use an Initializer to provide the IText argument to the Dialog component, but I wanted to demonstrate how the Services component can be used to provide services confined within a single GameObject)

    services-dialog.png

    And finally we will add the DialogButton as a child of the Dialog and use the Initializer we created earlier to specify the Button and DialogButtonType Init arguments that will be used to initialize the component.
    The Initializer automatically detects that IDialog is a service we can use and lets us know we don't need to specify it manually by just displaying Service in the Inspector next to that parameter's name.

    services-button.png

    Now we have a fully functioning UI system in place that can be used to display dialogs containing any text to the user and performing any actions when the user gives their confirmation!


    Here is a simple example component that uses this system to display a dialog to the user and invoke an UnityEvent if the user clicks the Confirm button:

    Code (CSharp):
    1. using Sisus.Init;
    2. using UnityEngine;
    3. using UnityEngine.Events;
    4.  
    5. public class ShowDialog : MonoBehaviour<IUIManager>
    6. {
    7.     [SerializeField]
    8.     private string text = "";
    9.  
    10.     [SerializeField]
    11.     private UnityEvent onConfirmed = new UnityEvent();
    12.  
    13.     private IUIManager uiManager;
    14.  
    15.     protected override void Init(IUIManager uiManager) => this.uiManager = uiManager;
    16.  
    17.     public void Show() => uiManager.ShowDialog(text, onConfirmed.Invoke);
    18. }
    Here's our menu system in use:
    services-result.gif
     
    Last edited: Jan 16, 2022
  12. SisusCo

    SisusCo

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

    New features added:

    Lazy Initialization
    Support was added for lazy initialization of classes decorated with the ServiceAttribute.

    If you set
    LazyInit = true
    then an instance of the service will not be created when the game starts like usually, but this will be delayed to only take place when the first client requests an instance.

    This makes it possible to use the ServiceAttribute with FindFromScene load method, even in cases where the service component doesn't exist in the first scene of the game when the game is loaded.

    In games with lots of services, lazy initialization can also be used to reduce the initial loading time of the game by spreading loading to happen over a longer period of time.

    Lazy initialization was implemented using the static constructor feature in C# which means it is guaranteed to only happen once per service type and to be fully thread safe. It also means that subsequently clients can retrieve instances of a service through a simple auto-implemented property without any additional overhead being introduced with reference fetching always happening through something like Lazy<T>.Value.

    Wrapped Services
    Additionally it is now possible to add the ServiceAttribute to a Wrapper component or asset, and set the service type to the type of the wrapped object (or any interface it implements), instead of the wrapper itself.

    When using ServiceAttribute with load methods such as FindFromScene or ResourcePath the service initialization system is now smart enough to recognize this pattern and can retrieve the service instance through the Wrapper.WrappedObject property of the wrapper.

    This makes it possible to have plain old class object services while still allowing designers to easily configure their values through the Inspector.

    It also makes it possible to register plain old class objects as services while keeping them decoupled from the ServiceAttribute.

    Initializer Services
    Similarly it is now also possible to add the ServiceAttribute to an Initializer, and set the service type to the type of the initialized Object (or any interface it implements), instead of the Initializer itself.

    When using ServiceAttribute with the load method FindFromScene, the service initialization system is now smart enough to handle this scenario. It can find the Initializer for the service from the scene, trigger the Initializer to immediately initialize the object, and then register the initialized Object as a service.

    This method or registering Services can be used to decouple your MonoBehaviour services from the ServiceAttribute.
     
  13. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Hey Sisus, hope you're doing fine!

    I'm using this plugin on a project that is running on Unity 2020.3.26f1, but it gives the following error when building the game:
    Captura de ecrã 2022-01-28, às 00.46.54.png

    However, when I remove the plugin the project builds without problem. Do you know what it can be? I'm using Mac OS and it's set to IL2CPP build with .NET Standard 2.0.

    Thank you so much in advance!

    Best Regards,
    Eduardo Bagarrão
     
  14. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Hey @BagarraoEduardo , thank you for letting me know about this!

    It looks like some changes I added in the last update are probably missing a vital using directive outside of the editor platform.

    I'll investigate and get this fixed today.
     
  15. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Hello @SisusCo,

    Thank you for the fast answer! Looking forward for the upcoming fix ;)

    Best regards,
    Eduardo Bagarrão
     
  16. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    @BagarraoEduardo Builds errors are fixed now; I sent you a PM with the updated package.

    I'll also submit the fix to asset store for review during the weekend, so it should become available for download from there some time next week.
     
  17. BagarraoEduardo

    BagarraoEduardo

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

    It's working perfectly now! Thank you so much once again!

    Best Regards,
    Eduardo Bagarrão
     
    SisusCo likes this.
  18. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Version 1.0.5 is now out.

    The main improvement in this version is a major overhaul of the inspector UX of Initializers.

    Initializers can be used to specify the arguments that are passed to the Init function of a component.
    Using Initializers to set up components instead of just exposing serialized fields can offer a bunch of benefits:
    • Initializers can expose and serialize interface arguments (even if the implementing class derives from UnityEngine.Object).
    • Initializers can warn you about missing references at runtime.
    • Initializers can warn you about missing references in edit mode.
    • Initializers allow you to use properties and read-only fields on the client component since Unity doesn't need to be able to serialize them.
    • Initializers retrieve all Service dependencies for you automatically, so you don't need to waste time dragging in the same references or worry about missing references if you make changes to your manager components.
    • You can add the InitOnReset attribute on the Initializer to automate the component setup process even further without having to clutter the client component with these implementation details.
    • You can see all dependencies of a component just by looking at its Init function (both on the code side and the Inspector side), instead of having to locate all its serialized fields.
    With the latest update working with Initializers is now a much smoother experience.

    generate-initializer.gif

    You will now see a collapsible Init section at the top of any components that accept initialization arguments.

    You no longer have to write any code to create an initializer for your components; simply press the + button inside the Init section and select "Generate Initializer", and the class will be generated for you. This means you can now get all the benefits of using Initializers with basically zero extra work.

    If an Initializer class already exists for the component, then the + button can be used to add an Initializer for the component instead.

    Once you have added an Initializer for a component you will see a new Null Argument Guard icon on the right side of the Init section. You can customize which null guards are active for the component by clicking the Null Argument Guard icon. By default you will get warnings in your Console if any null arguments are detected on your components in edit mode and at runtime at the moment the initialization takes place.

    The Null Argument Guard can be in four different states that are communicated through its icon:
    1. Grey icon with a line crossed over it means that null guards are not active.
    2. Yellow icon with an exclamation mark means that the component contains null arguments.
    3. Blue icon with a check mark means the component contains no null arguments and only some null guards are enabled (for example, edit mode warnings are enabled but runtime exceptions are disabled).
    4. Green icon with a check mark means the component contains no null arguments and all null guards are enabled.
     
  19. BagarraoEduardo

    BagarraoEduardo

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

    Hope you're doing well. I've updated to the v1.0.5 and now there are appearing some errors on the project:

    Captura de ecrã 2022-02-10, às 23.07.07.png

    Do you know that it can be?
    • MacOS 12.2
    • Unity 2020.3.26f1
    • Metal Graphics API
    Thank you in advance!

    Best Regards,
    Eduardo Bagarrão
     
  20. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Hey @BagarraoEduardo

    Reinstalling the demo using the 'Demo Installer' package should fix the issue.

    I had to make a few small changes to the IInitializer interface in order to be able to pull off all improvements I wanted to add in version 1.0.5 and I forgot that the demo scene had a class that manually implemented the interface instead of deriving from one of the base classes.

    Sorry for the inconvenience!
     
    BagarraoEduardo likes this.
  21. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    It solved the problems! Thank you for the answer ;)

    Best Regards,
    Eduardo Bagarrão
     
    SisusCo likes this.
  22. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Version 1.0.6 of Init(args) has been submitted to the asset store for approval.

    Further improvements have been made to the custom editor that all components with an Init function use.

    Let's say you have a component like this:

    Code (CSharp):
    1. using Sisus.Init;
    2.  
    3. public class TestInitializable : MonoBehaviour<int, string>
    4. {
    5.     public int Level { get; private set; }
    6.     public string Description { get; private set; }
    7.  
    8.     protected override void Init(int level, string description)
    9.     {
    10.         Level = level;
    11.         Description = description;
    12.     }
    13. }
    Since the last update you've been able to easily auto-generate an Initializer for the component just by clicking the + button inside the Init section in the Inspector.

    The generated class would look something like this:

    Code (CSharp):
    1. using Sisus.Init;
    2.  
    3. public sealed class TestInitializableInitializer : Initializer<TestInitializable, int, string> { }
    It's pretty neat how succintly an Initializer can be created for a component - however one limitation with this system was that you couldn't add any attributes to the argument fields, since they are all defined in the base class.
    This meant that useful attributes like Range or Tooltip couldn't be added to the Init arguments.

    After spending a lot of time testing out different ways to allow for this possibility, I came up with the following solution:

    Code (CSharp):
    1. using Sisus.Init;
    2. using UnityEngine;
    3.  
    4. public sealed class TestInitializableInitializer : Initializer<TestInitializable, int, string>
    5. {
    6.     private class Init
    7.     {
    8.         [Range(0, 100), Tooltip("Level between 0 to 100.")]
    9.         public int Level;
    10.  
    11.         [TextArea, Tooltip("Please provide a description for this object.")]
    12.         public string Description;
    13.     }
    14. }
    You can now define a nested Init class inside an Initializer class, and then define fields with types matching those of its Init arguments inside of it.
    Any property attributes you then add to these fields will get used when drawing the Init arguments they represent in the Inspector.

    initializable.png
     
  23. BagarraoEduardo

    BagarraoEduardo

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

    Hope you're doing well!

    Sorry for always bothering with questions, but I think that this newer update made the v1.0.5 initializers disappear(the ones that you've just needed to press the '+' button to generate the initializer and set it). However they're here, because I can't add another initializer:

    Captura de ecrã 2022-02-21, às 20.22.44.png

    It says it's already added and in the target it says "None"

    Captura de ecrã 2022-02-21, às 20.23.44.png

    I've deleted the plugin and reinstalled, but the problem remained. The only workaround that I found was to delete the components and remove the initializers from the project, create them again in the old way(As I'm reimplementing the structure of my player controller it was only 2 scripts :D) and add them together with the components that I want to initialize.

    Do you know what can be happening here?

    Thank you so much in advance!

    Best Regards,
    Eduardo Bagarrão
     
  24. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Hmm... I did make some pretty substantial changes to how the custom editors for initializable components are handled in 1.0.6, so it's very possible that this has introduced some sort of bug causing your Initializers to not get drawn in the Inspector like they should.

    However I wasn't able to figure out how to replicate this issue yet (testing with a fresh install of Init(args) 1.0.6 on Unity 2020.3.26f1).

    initializers.png

    I'll need to do some more investigation to try and figure out what could be causing this.

    Are you using Wrapper initializers or MonoBehaviour<T...> initializers by the way?
     
  25. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Thanks for the answer!

    I'm using the MonoBehaviour<T...> ones. After quitting the project yesterday and opening it today, it seems the initializers are hidden again(even after added them in the "old way" like I did yesterday). I've confirmed it because I've added today the initializers and set the target component and it said that the target component shouldn't had multiple initializers attached.

    Update #1: This behavior doesn't seem linear though(doesn't do this everytime), I'll prefer to see this with more attention this evening.

    Update #2: So after seeing this out with more attention:

    - Adding an initializer stays hidden in the inspector(If the associated class already is added on the GameObject);

    - It only shows the initializer if I add the initializers to the gameObject before the MonoBehaviour to be initialized. All the initializers disappears after press the play button in the editor;

    - The associated initializers are removed after removing the respective classes.

    I'm available to give any more info that could help solving this question ;)

    Thank you once again in advance!
     
    Last edited: Feb 22, 2022
  26. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    @BagarraoEduardo Thank you for providing additional details!

    Unfortunately I still wasn't able to get the initializers to disappear in my testing despite entering and existing play mode.
    I tried enabling Enter Play Mode Options as well but it didn't have any effect.

    Just to clarify and make sure we are on the same page:
    The separate Initializer components are now actually supposed to disappear from the Inspector.
    However the Initializer components that disappear are supposed by be replaced by these Init sections that appear embedded right inside the editor of their target component:

    Init.png

    So in your screenshot JaneController should first contain an empty Init section, and when you drag JaneController to the JaneControllerInitializer's Target field it should in fact disappear, and then its state should become visible inside the JaneController's Init section (similar to the screenshot above).

    But for some reason it seems like the Init section is not appearing for you in the screenshot at all, as if the custom editor for MonoBehaviour<T1, T2> that handles drawing the Init section wasn't being used.
     
  27. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    I just now realized from your screenshot that you seem to have Odin installed as well. That is likely somehow conflicting with the MonoBehaviour<T...> custom editor.

    As a temporary work around for the problem, if you disable Odin's custom editor for MonoBehaviour<T...> types in Odin's preferences, I suspect that the Init sections will start working.

    I'll investigate how I to get this conflict resolved so you can hopefully restore Odin for these types soon.
     
  28. BagarraoEduardo

    BagarraoEduardo

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

    Thank you so much for the answer!

    Oh I misunderstood, I thought that would be another option and that we could still use the separate initializers.

    Exactly, even in that way isn't appearing on my project. After spending some time I've found the problem!

    I've tested some other things but without success. After, I've tested on a new project of the same Unity version. The inspector appeared without any problem! However, after installing the same plugins that I'm using on my project one-by-one, I've found that after installing the Odin inspector the Init inspector disappears.

    Don't know how to solve this and if it's possible to have interoperability between the two plugins.

    Thank you so much in advance!

    Best Regards,
    Eduardo Bagarrão
     
  29. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38

    Didn't refreshed this while writing my answer, but it's exactly this ;)


    Unfortunately I can't find the MonoBehaviour<T...> to disable on Odin(doesn't appear either on plugin types nor user types in the Sisus namespace), but I'm able to temporarily disable the odin inspector to my game's namespace.


    Looking forward for the final solution. Once again thank you for all the help!


    Best regards,

    Eduardo
     
  30. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    @BagarraoEduardo
    Okay I managed to fix the compatibility issues with Odin. I had to wait until Odin has injected its own Editors before injecting my own for the MonoBehaviour<T...> classes, and now everything looks to be in working condition.

    I'll send you the fixed package in a PM and then submit and update to the asset store.
     
    BagarraoEduardo likes this.
  31. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Thank you so much for the help, it's working perfectly!
     
    SisusCo likes this.
  32. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Init(args) version 1.1.0 is now out!

    A Service tag can now be seen in the Inspector in the header of all service components.
    This small tag packs in a lot of functionality into it.

    For one, it is possible to right-click the service tag and select "Find Clients In Scenes" to select all objects in the loaded scenes that depend on the service.

    find-clients-in-scene.gif

    Additionally, selecting the context menu item "Find Defining Object" highlights the object that defines the service.

    If the service has been defined using the ServiceAttribute, the script that contains the attribute is highlighted in the Project view.

    If the service has been defined using a Services component, the GameObject with the component is highlighted in the Hierarchy view.

    find-defining-object.gif

    In addition to these two ways to define services, a third more minimalistic way has also been introduced in the new version.

    You can now right-click any component and select "Make Service Of Type...". This pops open a menu that lists the type of the component and its derived types and interfaces. Simply select one of the types on the list and this component will become available to any clients that need it through the type that was selected.

    make-service-of-type.gif

    The service tag can now also be seen inside Initializers to identify all service arguments that you don't need to manually assign through the inspector.

    Clicking this service tag highlights the service in the Hierarchy view.

    If the service is defined using the ServiceAttribute and doesn't exist in the hierarchy in edit mode, then the script that defines the service is highlighted instead.

    click-to-ping-service.gif
     
    Last edited: Jan 7, 2023
  33. BagarraoEduardo

    BagarraoEduardo

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

    Hope you're doing fine! I'm having a doubt here on an initializer where I've needed to change to receive a tuple. However now I can't assign values to the tuple objects.

    Captura de ecrã 2022-05-03, às 00.28.28.png

    Can you help me to solve this?

    Thank you so much in advance :)

    Best Regards,
    Eduardo Bagarrão
     
  34. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Hey @BagarraoEduardo ,

    The Initializer relies on the client's Init argument types being serializable by Unity through fields that have the SerializeReference attribute. Unfortunately this does not support generic types, so Unity can't serialize nor display the ValueTuple<T1...> type argument. You should even see an error message about it in the Console:

    2022-05-03 20_11_59-Greenshot.png

    There's a sort of work around for handling values that Unity can't serialize, using the IValueProvider system in Init(args).

    Code (CSharp):
    1. using Sisus.Init;
    2. using System;
    3. using UnityEngine;
    4.  
    5. namespace MyNamespace
    6. {
    7.     public sealed class AnimatorRigidbodyValueTupleProvider : MonoBehaviour, IValueProvider<ValueTuple<Animator, Rigidbody>>
    8.     {
    9.         [SerializeField]
    10.         private Animator animator;
    11.  
    12.         [SerializeField]
    13.         private Rigidbody rigidBody;
    14.  
    15.         public (Animator, Rigidbody) Value => (animator, rigidBody);
    16.         object IValueProvider.Value => Value;
    17.     }
    18. }
    2022-05-03 20_11_08-Greenshot.png

    While this works perfectly well, it doesn't get rid of the generic type error message in the Console, so it's not really a practical solution in this case...

    So currently the only real solution is to create a custom non-generic and serializable struct to hold the values instead of using ValueTuple<T1,T2>.

    That or create a custom Initializer for JaneAnimator from scratch. All it would really need to do is call target.Init with the value tuple argumeent in the Awake event function, and have a really early script execution order.
     
    BagarraoEduardo likes this.
  35. BagarraoEduardo

    BagarraoEduardo

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

    Thank you so much for the detailing! And sorry for the late reply.

    I've opted for using a struct to encapsulate all the needed objects! Thank you once again for all the help!

    Best Regards,
    Eduardo Bagarrão
     
    SisusCo likes this.
  36. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    @BagarraoEduardo In the latest version of Init(args) it's now also possible to create Initializers with ValueTuples and other generic arguments :)

    This can be done by deriving from the new InitializerBase base class and implementing the properties for all the Init arguments.

    Unlike when using Initializer as the base class, the responsibility of serializing the arguments is on you, giving you more freedom in how to do it. For example, using ISerializationCallbackReceiver to generate a Dictionary is now possible.

    Code (CSharp):
    1. using UnityEngine;
    2. using Sisus.Init;
    3. /// <summary>
    4. /// <see cref="Initializer{,}"/> for the <see cref="JaneAnimator"/> component.
    5. /// </summary>
    6. public sealed class JaneAnimatorInitializer : InitializerBase<JaneAnimator, (Animator animator, Rigidbody rigidbody)>
    7. {
    8.     [SerializeField]
    9.     private Animator _animator;
    10.  
    11.     [SerializeField]
    12.     private Rigidbody _rigidbody;
    13.  
    14.     protected override (Animator animator, Rigidbody rigidbody) Argument
    15.     {
    16.         get => (_animator, _rigidbody);
    17.         set
    18.         {
    19.             _animator = value.animator;
    20.             _rigidbody = value.rigidbody;
    21.         }
    22.     }
    23. }
    JaneAnimator.png
     
    BagarraoEduardo likes this.
  37. BagarraoEduardo

    BagarraoEduardo

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

    Sorry for the late answer, covid got me and I haven't been on my best days :D

    This suits perfectly my needs, thank you so much for this update!

    Best Regards,
    Eduardo
     
  38. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Oh man, I hope you'll get well soon - take care!
     
    BagarraoEduardo likes this.
  39. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Thank you so much!
     
  40. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Hey @SisusCo,

    Hope you're doing well. I've came across an idea while working on a task for my game. I was doing a custom StateMachineBehaviour and I saw that isn't possible drag any stuff of the own GameObject where I'm working on to that kind of script.

    I don't know Unity's script lifecycle and if this is possible, but in case you need some ideas to extend this tool, maybe this could be a cool one! I've tried to add IInitializable<T> interface but it didn't work tho.

    Best Regards,
    Eduardo
     
  41. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    @BagarraoEduardo That's a very interesting idea!

    It should be possible to create an AnimatorInitializer component that would get all StateMachineBehaviours of a given type from an Animator and initialize them with arguments. Something like this:

    Code (CSharp):
    1. public class MyAnimatorInitializer : MonoBehaviour
    2. {
    3.     [SerializeField]
    4.     private Animator target;
    5.    
    6.     [SerializeField]
    7.     private MyComponent myComponent;
    8.    
    9.     private void Awake()
    10.     {
    11.         foreach(var behaviour in target.GetBehaviours<StateMachineBehaviour>())
    12.         {
    13.             if(behaviour is IInitializable<MyComponent> initializable)
    14.             {      
    15.                 initializable.Init(myComponent);
    16.             }
    17.         }
    18.     }
    19. }
    Probably Awake and OnEnable events would get called for the StateMachineBehaviours before the arguments are initialized though, but I'll need to test to check this... I'll investigate if creating a generic system for easily creating AnimatorInitializers would maybe make sense.
     
    BagarraoEduardo likes this.
  42. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    @SisusCo Thank you so much, this really suited what I was doing! I've added an initializer to it after, like this(Don't know if it makes sense, but as I want to inject GameObject level services - IJaneAnimatorWrapper, I thought it could make more sense this way for my project)

    Code (CSharp):
    1. public class JaneStateMachineBehavioursInitializer : MonoBehaviour<Animator, IJaneAnimatorWrapper, IJaneLogic>
    2.     {
    3.         private Animator _animator;
    4.         private IJaneAnimatorWrapper _animatorWrapper;
    5.         private IJaneLogic _janeLogic;
    6.  
    7.         protected override void Init(
    8.             Animator animator,
    9.             IJaneAnimatorWrapper wrapper,
    10.             IJaneLogic janeLogic
    11.             )
    12.         {
    13.             _animator = animator;
    14.             _animatorWrapper = wrapper;
    15.             _janeLogic = janeLogic;
    16.         }
    17.  
    18.         private void Awake()
    19.         {
    20.             foreach (var behaviour in _animator.GetBehaviours<StateMachineBehaviour>())
    21.             {
    22.                 if (behaviour is IInitializable<IJaneAnimatorWrapper, IJaneLogic> initializable)
    23.                 {
    24.                     initializable.Init(_animatorWrapper, _janeLogic);
    25.                 }
    26.             }
    27.         }
    28.     }
     
  43. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    That's a great solution, makes perfect sense :)

    Remember to use
    protected override void OnAwake()
    in MonoBehaviour<T...>-derived classes, though. The base class uses the Awake function to retrieve the initialization arguments and passes them to the Init function before calling OnAwake.
     
    BagarraoEduardo likes this.
  44. BagarraoEduardo

    BagarraoEduardo

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

    Sorry for the late answer, I didn't notice the notification on my email. Thank you so much, I've corrected it ;)

    Best Regards,
    Eduardo
     
    SisusCo likes this.
  45. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Init(args) 1.1.3 is out now and includes:
    • New StateMachineBehaviour<T...> base classes for state machine behaviours that can receive upto six arguments during initialization.
    • Initializer support for state machine behaviours. These initializers can be generated and configured using the animator inspector for all state machine behaviours found inside the animator controller. The arguments are passed to the state machine behaviours' Init functions when the GameObject that contains the animator is loaded.
    state-machine-behaviour-initializer.png
     
    Last edited: Jun 22, 2022
    BagarraoEduardo likes this.
  46. BagarraoEduardo

    BagarraoEduardo

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

    Hope you're doing fine. I have here a question. I have here this class

    Code (CSharp):
    1. using System;
    2. using CodeStage.AntiCheat.ObscuredTypes;
    3. using LonelyJane.Characters.Common.Interfaces;
    4. using LonelyJane.Core.Interfaces.FSM;
    5. using Sisus.Init;
    6.  
    7. namespace LonelyJane.Core.FSM
    8. {
    9.     [Serializable]
    10.     public class State : IState, IUpdate, IFixedUpdate
    11.     {
    12.         protected readonly IStateMachine _stateMachine;
    13.         protected readonly IEntity _entity;
    14.         protected readonly IAnimatorWrapper _animatorWrapper;
    15.         protected readonly ObscuredString _animationName;
    16.  
    17.         public State(
    18.             IStateMachine stateMachine,
    19.             IEntity entity,
    20.             IAnimatorWrapper animatorWrapper,
    21.             ObscuredString animationName)
    22.         {
    23.             _stateMachine = stateMachine;
    24.             _entity = entity;
    25.             _animatorWrapper = animatorWrapper;
    26.             _animationName = animationName;
    27.         }
    28.  
    29.         public virtual void OnEnterState()
    30.         {
    31.             _animatorWrapper.SetBool(_animationName, true);
    32.         }
    33.  
    34.         public virtual void OnExitState()
    35.         {
    36.             _animatorWrapper.SetBool(_animationName, false);
    37.         }
    38.  
    39.         public virtual void Update(float deltaTime) { }
    40.  
    41.         public virtual void FixedUpdate(float deltaTime) { }
    42.     }
    43. }
    And I've created a Wrapper class so I can use IUpdate and IFixedUpdate callbacks:

    Code (CSharp):
    1. using LonelyJane.Core.FSM;
    2. using Sisus.Init;
    3.  
    4. namespace LonelyJane.Characters.Jane.Wrappers
    5. {
    6.     public class StateComponent : Wrapper<State> { }
    7. }
    8.  
    And my question is if on a Wrapper(StateComponent) class, all the values that the host class(Player) receives on the constructor should be derived from MonoBehaviour? It's because it appears this on the Wrapper class ("Value") and I wanted to initialize the State instances with the common constructor.

    Captura de ecrã 2022-06-29, às 23.34.05.png

    How I want to initialize state instances => In a IInitializable<T> Init's method:

    Code (CSharp):
    1. public class JaneEntity : Entity, IInitializable<IJaneInputWrapper, IJanePhysicsWrapper, IJaneAnimatorWrapper, IJaneParameters>, IJaneEntity
    2.     {
    3.         public IState IdleState { get; private set; }
    4.         public IState MovementState { get; private set; }
    5.  
    6.         public IJaneInputWrapper InputWrapper { get; private set; }
    7.         public IJanePhysicsWrapper PhysicsWrapper { get; private set; }
    8.         public new IJaneAnimatorWrapper AnimatorWrapper{ get; private set; }
    9.         public IJaneParameters Parameters { get; private set; }
    10.  
    11.         public void Init(
    12.             IJaneInputWrapper inputWrapper,
    13.             IJanePhysicsWrapper physicsWrapper,
    14.             IJaneAnimatorWrapper animatorWrapper,
    15.             IJaneParameters parameters)
    16.         {
    17.             base.Init(animatorWrapper);
    18.  
    19.             InputWrapper = inputWrapper;
    20.             PhysicsWrapper = physicsWrapper;
    21.             AnimatorWrapper = animatorWrapper;
    22.             Parameters = parameters;
    23.  
    24.             IdleState = new JaneIdleState(StateMachine, this);
    25.             MovementState = new JaneMovementState(StateMachine, this);
    26.         }
    27.     }

    Thank you so much in advance!

    Best Regards,
    Eduardo
     
  47. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    Hey!

    You can create a WrapperInitializer instead of a normal initializer, and Init(args) should then be able to allow configuring the arguments through the inspector.

    Code (CSharp):
    1. using Sisus.Init;
    2. using LonelyJane.Core.FSM;
    3.  
    4. namespace LonelyJane.Characters.Jane.Wrappers
    5. {
    6.     /// <summary>
    7.     /// <see cref="Initializer{,}"/> for the <see cref="StateComponent"/> component.
    8.     /// </summary>
    9.     public sealed class StateComponentInitializer : WrapperInitializer<StateComponent, State, IStateMachine, IEntity, IAnimatorWrapper, ObscuredString>
    10.     {
    11.         protected override State CreateWrappedObject(IStateMachine stateMachine, IEntity entity, IAnimatorWrapper animatorWrapper, ObscuredString obscuredString)
    12.         {
    13.             return new State(stateMachine, entity, animatorWrapper, obscuredString);
    14.         }
    15.  
    16.         #if UNITY_EDITOR
    17.         /// <summary>
    18.         /// This section can be used to customize how the Init arguments will be drawn in the Inspector.
    19.         /// <para>
    20.         /// The Init argument names shown in the Inspector will match the names of members defined inside this section.
    21.         /// </para>
    22.         /// <para>
    23.         /// Any PropertyAttributes attached to these members will also affect the Init arguments in the Inspector.
    24.         /// </para>
    25.         /// </summary>
    26.         private class Init
    27.         {
    28.             public IStateMachine stateMachine;
    29.             public IEntity entity;
    30.             public IAnimatorWrapper animatorWrapper;
    31.             public ObscuredString obscuredString;
    32.         }
    33.         #endif
    34.     }
    35. }
    2022-06-30 08_40_48-Greenshot.png

    This also makes it possible to use constructor injection in the wrapped object if you want, so you can use get-only properties etc.

    And you can remove the Serializable attribute from the wrapped object, as all the initialization arguments are serialized individually in the wrapper initializer (unless you need it for custom serialization of some sort).
     
  48. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    Thank you so much for the answer! I completely have forgotten of the Wrapper initializer, I've changed it now! But now I've realized that the Wrapper Initializer has a similar issue that some time ago I reported here (in a project with Odin Inspector),

    however after disabling the custom editor it still doesn't appear:

    Captura de ecrã 2022-06-30, às 19.35.21.png


    My Wrapper Initializer right now(I've removed an unnecessary class):


    Code (CSharp):
    1.     /// <summary>
    2.     /// <see cref="Initializer{,}"/> for the <see cref="StateComponent"/> component.
    3.     /// </summary>
    4.     public sealed class StateComponentInitializer : WrapperInitializer<StateComponent, State, IStateMachine, IEntity, ObscuredString>
    5.     {
    6.         protected override State CreateWrappedObject(IStateMachine stateMachine, IEntity entity, ObscuredString animationName)
    7.         {
    8.             return new State(stateMachine, entity, animationName);
    9.         }
    10.  
    11. #if UNITY_EDITOR
    12.         /// <summary>
    13.         /// This section can be used to customize how the Init arguments will be drawn in the Inspector.
    14.         /// <para>
    15.         /// The Init argument names shown in the Inspector will match the names of members defined inside this section.
    16.         /// </para>
    17.         /// <para>
    18.         /// Any PropertyAttributes attached to these members will also affect the Init arguments in the Inspector.
    19.         /// </para>
    20.         /// </summary>
    21.         private class Init
    22.         {
    23.             public IStateMachine StateMachine;
    24.             public IEntity Entity;
    25.             public ObscuredString AnimationName;
    26.         }
    27.         #endif
    28.     }
    Thank you so much in advance ;)

    Best Regards,
    Eduardo
     
  49. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,344
    @BagarraoEduardo Hmm, surprising that even disabling Odin's inspector didn't help. I'll look into this, thanks for letting me know!
     
    BagarraoEduardo likes this.
  50. BagarraoEduardo

    BagarraoEduardo

    Joined:
    Sep 20, 2017
    Posts:
    38
    No problem ;)