Unity is a good game development tool and honestly I like most of its features. However the code framework is awkward to use on big projects where code maintainability is of fundamental importance. As you may have guessed, the following considerations in this article do not apply to simple projects or projects developed by one or two coders. For these specific cases, Unity is good enough. The arguments in this article apply to relatively complex projects developed by a medium-big team.
After years of analysing the intrinsic issues of the Unity framework, I realised that they all share the same root: the way Unity injects dependencies inside entities, or better saying, how Unity makes this natural process very difficult.
A dependency can be defined as an object needed by another object to execute its code. If a class A needs the instance of a class B to execute its code, B is a dependency for A. When B is passed into A, we say that the dependency B is injected into the object A.
Unity is based on the concept of “Entity Framework”. An Entity Framework brings many benefits: pushes the users to favour composition over inheritance, keeps the classes small and clean focusing on single responsibilities, promotes modularity. This is in a perfect world.
In Unity, the Entites are called GameObjects and the Components are based on MonoBehaviour implementations. Components add “behaviours” to Entities and theoretically they should be perfectly encapsulated. The Entity Component structure relies on the fact that components define the whole entities logic. In theory:
- MonoBehaviours must operate within the GameObjects they are attached to. This is needed to ensure that encapsulation is not broken, as it shouldn’t be needed to. Components, by design, must handle only the Entity they are attached to.
- MonoBehaviours are modular and single responsibility classes reusable between GameObjects.
- The behaviour of a GameObject should be extended adding MonoBehaviours instead of adding methods to a single MonoBehaviour. One component must add one behaviour only to encourage modularity.
- GameObjects should not be created without a view such as a mesh or a collider or something that is really an entity in the game.
- MonoBehaviours can know other components on the same GameObject using GetComponent, however they cannot know MonoBehaviours from other GameObjects. This is still part of the framework design.
The first three points are obvious, even to who has never heard about Entity Component frameworks. The Components design allows to add logic to an entity without using inheritance. We can say that instead to specialize the behaviour of an Entity vertically (usually specializing classes through polymorphism), Components allow to expand behaviours horizontally. This is not the place to talk about it, but in case you didn’t know, composition over inheritance is a well known pattern which benefits have been proved extensively. The great thing about an Entity Component framework is then that composition comes naturally, without any particular effort. Smaller is the component, easier will be to reuse it, that’s why giving a single responsibility to each component will promote modularity.
The fourth point may seem an arbitrary rule, but Unity entities by default implement the transform class, implying that the object must be placed in the world. What’s the point to have a Transform by default if then it’s not used? Having a complex hierarchy in Unity doesn’t come for free, all the transforms must be computed hierarchically, therefore an extra GameObject is an extra matrix multiplication to execute every frame.
Last point starts to uncover the real issue. MonoBehaviours are meant to add behaviours on the GameObject that they are attached to and consequentially Unity can’t provide a clean way to let GameObects know each other without resorting to awkward solutions. The other problem is that Unity asks to the user to use MonoBehaviours even when the logic to code is not related to an Entity at all. For example: let’s say there is an object called MonsterManager in the game world which must know the monsters that are still alive. When a monster dies, how can this object know it?
Currently there are 2 simple solutions to this problem:
- MonsterManager is a Monobehaviour created inside a GameObject that has no view. All the monsters will look for the MonsterManager class using the GameObject.Find or Object.FindObjectOfType function inside the Start or Awake methods and add themselves into the MonsterManager. MonsterManager could potentially listen to a Monster event to know when it will die.
- The only other alternative is that MonsterManager is a Singleton and it’s used directly inside the Monster Monobehaviour. When a monster dies, a public MonsterManager function is called.
both cases are needed to solve the same problem: Inject dependencies. In the case A, MonsterManager is a dependency for the Monster objects. The Monster objects cannot add themselves in the MonsterManager if they don’t know the MonsterManager object. In the second case, the dependency is solved through the use a global variable. You may have heard that global variables and singletons are very bad to use and you may not have fully understood why yet. For the moment, let’s focus on the practical aspects of these solutions:
What’s wrong with GameObject.Find
Beside being quite a slow function, GameObject.Find is one example of how awkward the Unity Framework is for big projects development. What happens if someone in the team decides to rename the GameObject? Should GameObjects renaming or deletion been forbidden? GameObject.Find can lead to several run-time errors that cannot be caught in compiling time. This scenario could be really hard to manage when dozens of GameObjects are searched through this function. GameObject.Find and similar functions should be definitively abolished by the Unity framework.
What’s wrong with the Object.FindObjectOfType
How should the MonsterManager object be injected inside the Monster class then? There is actually another solution: calling the function Object.FindObjectOfType. However, what is FindObjectOfType? Object.FindObjectOfType could be seen as a wrong implementation of the Service Locator Pattern, with the difference that it is not possible to abstract the implementation of the service from its interface. This is another problem of Unity framework, which I will now briefly hint at, but that would need another entire article, Unity discourages the use of interfaces. The interface is the most powerful concept at the core of every well designed code. Instead to push the coders to use interfaces, Unity pushes the coder to uses Monobehaviour, even when the use of Monobehaviour is not necessary.
What’s wrong with the Singleton
Singleton is a controversial argument since the pattern has been invented. I am personally against the use of Singleton; every single Singleton class I have encountered so far, led only to design problems hard to fix through refactoring. However if you ask me why I am against the Singleton, I will not answer you with the usual answers (they break encapsulation, hide dependencies, make impossible to write proper unit tests, bind the code to the implementation of a service instead of its abstraction, make refactoring really awkward), but with the hindsight of the practice: your code will become unmanageable after a short term. This because the use of Singletons come without any design constriction that, while apparently makes the coder life easier, will make it a hell later on, when the code turn in to an incomprehensible blob without a structured flow. Projects developed by one of two coders wouldn’t show the problem at first or ever (for example, if the project doesn’t need to be maintained over time), but mid-size projects will pay the consequences of wild design when it’s probably too late for refactoring. Think about it, you can use Singletons everywhere, without limits, without rules. What would keep a coder from making a total mess out of it? Common sense? Let’s not fool ourselves, common sense doesn’t apply when release deadlines approach fast.
To be honest, this is not enough to dismiss Singletons. What is a Singleton if not just another way to inject dependencies? When you don’t have any other choice than singletons, the real problem becomes how to manage the states of these classes when encapsulation is broken. Singletons usually and normally rely on public functions, public functions that normally change states inside the Singleton itself. When the singleton is used recklessly in several parts of the code, it becomes very hard to track when and how these states change. Refactoring becomes tremendously risky because changing the order of the call of the public functions of the same singleton could mean finding the singleton itself in an unexpected state. The biggest problem about Singleton is then the fact that they cannot be designed with Inversion of Control in mind. You are always forced to write functions like IsSingletonReady() or DisableThisSpecificBehaviour() or AccessToThisGlobalData(), leaving the control of the internal states to external entities there are hard to track, especially due to the Singleton global variable-like behaviour.
Let’s be clear, the main reason why dependencies are injected is to let objects communicate between each other. Communication is impossible without any form of injection. Therefore while is some rare case, the use of a Singleton could be acceptable, you can’t work with a framework that won’t let you use any other kind of injection. While it’s feasible, it’s totally unreasonable to solve all the kind of communication problems through singletons.
At last, being essentially static classes, singletons usually become the most common source of memory leaks in a project.
There are 3 ways to resolve dependencies: using the Service Locator pattern, injecting dependencies manually through constructors/setters, exploiting reflection capabilities through an IoC container and, obviously, using Singletons. Disregarding Singletons, I do not like the Service Locator Pattern because the SLP itself is a singleton (or a static class) and this could lead to some severe limitations compared to the IoC container solution. Since Unity doesn’t have a starting place where dependencies can be created and injected, manual injections become quite hard and basically impossible without a good understanding of the Unity limitations.
Therefore a good solution available (but not the only one as we will see later on in this article series!) is to use IoC containers. Another article is needed to explain what an IoC container is, therefore I will give a first simple definition: An IoC container is a…container that creates and contains the dependencies that must be injected inside the application objects. It is called Inversion Of Control because the design is thought in such a way that the objects are never created by the user, but they are lazily created by the container when they are needed.
There are several IoC containers out there, a lot written in c# as well, but practically no one works in Unity and most of all, they are damn complicated*. For these reasons I decided to create an IoC container for Unity trying to keep it as simple as possible. Actually creating a basic IoC container is pretty straightforward and everybody can do it, my implementation has just a few tweaks that makes it simple to use with Unity projects.
Conclusion (for this article)
The second part of this article (including source code) is available here: http://blog.sebaslab.com/ioc-container-for-unity3d-part-2
I strongly suggest to read all my articles on the topic:
*Since when I first wrote this article, new IoC containers have been developed. The best ones out there are StrangeIoC, ZenInject and Adic. Google them, it’s worth it.