Features of dependency injection in Unity3D

Hello, Habr!

When I got acquainted with the development for Unity3D, for me, who came from the world of Java and PHP, the approach to implementing dependencies on this platform became quite unusual. Basically, of course, this is due to the inaccessibility of the MonoBehaviour and ScriptableObject constructors, the creation of objects outside the developer's access, as well as the presence of an editor in which you can configure each object individually or as a whole prefab (while leaving the opportunity to change one of the prefab instances to your discretion in the process create a scene).

Constructor Injection


Constructors are good, but exactly until you start using MonoBehaviour or ScriptableObject, instances of which can only be created through ready-made factories, implying the use of a constructor without parameters.

For the first case, the factory will be the GameObject.AddComponent (T) method, where T is the type of component being created. MonoBehaviour’s heir cannot exist just like that, in isolation from a specific game object. For him, dependency injection through a constructor or its analogue is impossible. What we naturally do not want to do. So, we will introduce dependencies here in other ways.

For ScriptableObject, the factory is the ScriptableObject.CreateInstance (T) method, where T, again, is the type of object. But the situation is no fundamentally different from MonoBehaviour - you won’t be able to use your own constructor.

But, since the heirs of this class are independent entities and quietly exist without being tied to a specific game object, sometimes an initializing method is used to bypass the ScriptableObject constructor, into which all the necessary parameters are passed. For convenience, the CreateInstance method is overridden for each ScriptableObject descendant with the parameters of the pseudo-constructor, so that each time you create an instance, do not knock your hands in Init. But, as we understand, this greatly complicates the implementation and use of the class: inside it requires checking (wherever possible) for initialization, but outside there is a risk that somewhere the instance will be created without initialization, through the standard CreateInstance . For these two reasons, I try to avoid such use of the Init method.

Property injection


But dependency injection through the public properties of an object is already in Unity3D style. This is how the inspector works with setting parameters for prefab components and scene objects. For scalar parameters, everything is quite simple - just write the values ​​of each property of the object in the corresponding field of the inspector form. But with references to objects, everything is less clear.

Link to prefab or native component


A small caveat: by “own”, I mean a component mounted on the same game object as the script into which we inject the dependency.

The peculiarity of this option is that the link to the own component or prefab can be saved in the prefab of the object and is easy to use in the future. For example, we can create N monster generators (spawners), indicating to each monster that it should generate, then store these generators as prefabs and reuse them as many times as you like without any difficulties. Changing the dependency (i.e. the selected monster) is easy - in any generator used on the scene we replace the link to the monster object, save the prefab and that's it, at all levels it is changed. Here they are, the delights of dependency injection!

Link to an external component


The difficulty here is that in the object’s prefab you cannot save a link to an object located on the stage, which is quite logical, since the prefab is an entity independent of the scene. And there are several options for solving this problem:
  • Manual implementation;
  • "Baking" dependencies;
  • Global access;
  • Injection through the parent object;
  • Globally accessible creation event;
  • IoC containers.


Manual implementation

As in the case of creating prefabs, we simply indicate with our hands the dependencies of each object in the inspector. This option works if we don’t have many such objects with dependencies. But when there are a lot of them (or a large number of scenes), then this option is unlikely to suit us. And when you change the dependency, we will have to redo everything all over again, so this is more of an anti-pattern, against which even some singleton looks much prettier.

Baking dependencies

This option, as in the case of manual implementation, works only with the dependencies of objects already created on the scene. We need to add an editor script that will go through all the objects of the scene of a certain type and slip the specified component into the public property. That is, bake dependencies in objects (a small addition - all component fields that were changed in the editor in this way should be marked dirty (using the EditorUtility.SetDirty method, otherwise they will be reset to the original value when the game starts) .

Global access

For global access, I think you won’t have to paint in detail (we all did this when we started programming) - we turn to the object used as a dependency through a public static property or method to get a reference to an object instance. You can call it proud words “addiction” with a big stretch, but sometimes, in practice, you have to use anti-patterns, right?

Originating object

If our objects are the result of the work of some generative entity (and somehow it is present, if game objects are created during the life of the application - you call GameObject.Instantiate somewhere), then it is through this object that the dependencies of all “children” can be transmitted. That is, the dependency is indicated in the public field of the generating object and transferred to the generated entities. The only catch is that if it creates different objects, then this dependence must either be indicated in the abstract class of generated objects, or if the dependence is individual, you will have to additionally describe the rules for initializing this or that object.

Global object creation event

C # is wonderful with such a simple but cool thing as events. Which, in particular, can be made static, that is, displayed in the global scope (the degree of globality can be limited to one namespace using the internal access modifier).

To implement this method of dependency injection, we need to select in the objects that it is injected the static event that is called in the Awake method and to which a listener will subscribe. And then set certain objects to these objects. This method is very, very controversial (in general, like any globally accessible objects), since the programmer will have to follow the presence of a listener. Fortunately, Unity3D notifies the developer of an empty public property at MonoBehaviour by means of a message in the console, but there is no need to rely on the IDE. Yes, and keep track of all these initializers will be difficult. Writes this item in dirty anti-patterns.

IoC containers

In my practice, this method is probably the most popular in all languages ​​I know.

What is a container? This is a list of correspondences of an interface and a specific object implementing this interface. However, since we work with objects created by the platform itself, and not us, in the container’s configurator you will have to search for specific components on the scene or, again, point them through the inspector (that is, the container should be a component of some object on the stage). Further, in scripts where dependency injection is used, a link to the necessary object is pulled from the container.

The main difficulty here is that Unity3D is not friendly with interfaces (in the UnityEngine namespace, for example, there is only one interface for serialization). And he does not know how to look for components by interfaces. Just as it cannot show fields in the inspector whose types are specified by interfaces. Which is understandable, since the descendant of the Component class is expected there, but there is no interface that would imply it. But on the Internet there are many examples of how to search for components by interface, so if we are willing to sacrifice the ability to use the inspector (or are ready to write an extension for it), you can use IoC containers.

A simple but understandable example of an IoC container can be viewed here.(implementation is not mine). Unfortunately, it does not include an example of searching for objects by interfaces.

Afterword


Dependency injection in Unity3D is somewhat non-standard due to platform features. By the way, while writing this article, I monitored the Internet a lot about ready-made solutions on the topic and came to the conclusion that in the near future it’s worth finding a free minute and getting confused with writing a small library for the same IoC containers with a full configurator adapted for Unity3D, so there are not many ready-made solutions. I hope that soon the code will be publicly available on GitHub, and another article from me will appear on Habrahabr.

Thank you for your attention :)

PS: I will be very grateful for pointing out errors in the text in PM.

Also popular now: