Init(args)
Asset Store | Documentation | Scripting Reference | Video Tutorial
Init(args) is a toolset that makes it possible to pass arguments to Objects from the outside during initialization, with a great amount of flexibility.
Whether you want to pass arguments in code during instantiation, or drag-and-drop Object references into interface type fields across scenes in the Inspector, Init(args) will be there to help make it easy and painless.
The set of tools have been seamlessly integrated into Unity using inspector tooling and familiar-feeling extension methods, 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:
Player player = gameObject.AddComponent<Player, IInputManager>(inputManager);
Or maybe youâve sometimes wished you could Instantiate with arguments like so:
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:
using UnityEngine;
public class Player : MonoBehaviour
{
private void Update()
{
if(InputManager.Instance.Input.y > 0f)
{
float speed = 0.2f;
float distance = Time.deltaTime * speed;
transform.Translate(Camera.main.transform.forward * distance);
}
}
}
While using this method does accomplish the job of retrieving the instance, it also comes bundled with some 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() 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):
using UnityEngine;
using Sisus.Init;
public class Player : MonoBehaviour<IInputManager, Camera>
{
private IInputManager inputManager;
private Camera camera;
protected override void Init(IInputManager inputManager, Camera camera)
{
this.inputManager = inputManager;
this.camera = camera;
}
private void Update()
{
if(inputManager.Input.y > 0f)
{
float speed = 0.2f;
float distance = Time.deltaTime * speed;
transform.Translate(camera.transform.forward * distance);
}
}
}
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.
[Service(typeof(IInputManager), FindFromScene = true)]
public class InputManager : MonoBehaviour, IInputManager
{
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.
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.
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.
Whenever the need arises, you can easily swap a particular component to use some other Object instead of the shared service, simply by dragging and dropping another Object in.
Replacing a service with a mock during unit tests is also very easy.
using UnityEngine;
using NUnit.Framework;
using Sisus.Init;
using Sisus.Init.Testing;
public class TestPlayer
{
private class FakeInputManager : IInputManager
{
public Vector2 Input { get; set; }
}
private FakeInputManager inputManager;
private Player player;
private Testable testable;
[SetUp]
public void Setup()
{
inputManager = new FakeInputManager();
player = new GameObject<Player, Camera>().Init1(inputManager as IInputManager, Second.Component);
testable = new Testable(player.gameObject);
}
[TearDown]
public void TearDown() => testable.Destroy();
[Test]
public void LeftInput_Moves_Player_Left()
{
var startPosition = player.transform.position;
inputManager.Input = Vector2.left;
testable.Update();
var endPosition = player.transform.position;
Assert.AreEqual(startPosition.y, endPosition.y);
Assert.Less(endPosition.x, startPosition.x);
}
}
You can also turn any component into a service without writing any code - 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).
For more information about Init(args) head on over to the Asset Store page, refer to the Documentation or check out the Scripting Reference.