📜 ⬆️ ⬇️

Features of dependency injection in Unity3D

Hi, Habr!

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

Constructor injection


Constructors are fine, but smoothly until you start using MonoBehaviour or ScriptableObject, instances of which can be created only 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. There is no successor to MonoBehaviour, apart from a specific game object. For him, injection of dependence through the constructor or its analog is impossible. What we, naturally, do not want to do. So, here we will introduce dependencies in other ways.
')
For ScriptableObject, the factory is the method ScriptableObject.CreateInstance (T), where T is, again, the type of the object. But the situation is not fundamentally different from MonoBehaviour - you cannot 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 to bypass the ScriptableObject constructor, an initialization method is used, to which all necessary parameters are passed. For convenience, the CreateInstance method is overridden for each successor of a ScriptableObject with indication of the parameters of the constructor in order not to knock on each instance in Init. But, as you and I understand, this greatly complicates the implementation and use of the class: it requires checking (wherever possible) for initialization, and in-out there is a risk that somewhere an instance will be created without initialization, through the standard CreateInstance . For these two reasons, I try to avoid this use of the Init method.

Property Injection


But injection of dependencies through the public properties of an object is already in the 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 links to objects all less clear.

Link to prefab or own component


A small disclaimer: by "own", I mean a component hung on the same game object as the script in which we inject the dependency.

The peculiarity of this option is that the reference to your own component or prefab can be saved to the prefab of an object and easily used in the future. For example, we can create N generators (spawners) of monsters, specifying each monster that it must generate, and then save these generators to prefabs and reuse as many times as needed without any difficulty whatsoever. Changing the dependency (i.e. the selected monster) is easy - in any generator used on the stage, we replace the reference to the monster object, save the prefab and everything, at all levels it is changed. Here they are, the delights of dependency injection!

External component reference


Here the difficulty lies in the fact that in the prefab of an object it is impossible to 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 solutions to this problem:


Manual implementation

As in the case with the creation of prefabs, we simply point out the dependencies of each object in the inspector. This option works if we have few such objects with dependencies. But when there are many (or a large number of scenes), then this option is unlikely to suit us. And when changing dependencies, we’ll have to redo everything anew, so it’s rather an anti-pattern, against which even a singleton looks even more beautiful.

"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 (small addition - all component fields that have been modified in the editor in this way should be marked dirty (blank?) Using the EditorUtility.SetDirty method, otherwise they will be reset to their original value when the game starts) .

Global access

For global access, I think it’s not necessary to paint in detail (we all did that when we started programming) - the object used as a dependency is accessed via a public static property or method to get a reference to an object instance. Call it the proud words of "addiction" can be a stretch, but sometimes, in practice, it is necessary to use anti-patterns, is not it?

Generating object

If our objects are the result of the work of a certain generating entity (and one way or another 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 you can transfer the dependencies of all the "children". That is, the dependency is indicated in the public field of the generating object and is transmitted to the generated entities. The only catch is that if he creates different objects, then this dependency must either be specified in the abstract class of generated objects, or, if the dependency is individual, one will have to additionally describe the initialization rules for one or another object.

Global object creation event

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

To implement this method of dependency injection, we need to select a static event raised in the Awake method and to which a certain listener will subscribe to the objects it implements. And then ask these objects certain parameters. This method is very, very controversial (in general, like any globally accessible objects), since the programmer will have to monitor the presence of the listener. Fortunately, Unity3D informs the developer about the empty public property of MonoBehaviour via a message in the console, but you will not have to rely on the IDE here. Yes, and follow all these initializers will be difficult. Writes this item to dirty anti-patterns.

IoC containers

In my practice, this method is in all languages ​​known to me, perhaps the most popular.

What is a container? This is a list of matches between the interface and the specific object that implements this interface. However, since we work with objects created by the platform itself, and not by us, in the container configurator you will have to implement a search for specific components on the scene or, again, specify them through the inspector (that is, the container must be a component of some object on the scene). Further, in scripts where dependency injection is used, a reference to the required object is pulled out of the container.

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

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

Afterword


The implementation of dependencies in Unity3D is somewhat non-standard due to the peculiarities of the platform. 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 it is worth finding a free moment in the near future and be confused with writing a small library for the same IoC containers with a full configurator adapted for Unity3D, 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.

Thanks for attention :)

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

Source: https://habr.com/ru/post/245589/


All Articles