Unity is a good game development tool and honestly, I like most of its features. However, the programming 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 object A needs object 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 object A.
Unity code framework is designed around Bilas’ Data-Driven GameObject System1. A component-based entity framework brings many benefits: pushes the users to favour composition over inheritance, keeps the classes small, clean and focused on a single responsibility, and promotes modularity. This is in a perfect world.
In Unity, the Entities 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 entity’s logic. In order to use a Gameobjects framework properly, the following should be followed:
- 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 by adding MonoBehaviours instead of adding code to an existing MonoBehaviour. One component must add only one behaviour to encourage modularity.
- GameObjects should not be created without a view such as a mesh or a collider or anything that is actually an entity in the game (i.e.: Monobehaviours shouldn’t be managers)
- MonoBehaviours can know other components on the same GameObject using GetComponent, however, they shouldn’t know MonoBehaviours from other GameObjects.
The first three points are obvious, even to those who have never heard about Entity Component frameworks. The Components design allows adding 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 expanding 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 practice of which benefits have been proved extensively. The great thing about an Entity Component framework is then that composition comes naturally, without any particular effort. The smaller the component, the 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 of having a Transform by default if it’s not used?
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 the user to use MonoBehaviours even when the logic to write 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.
- 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 to the MonsterManager if they don’t know the MonsterManager object. In the second case, the dependency is solved through the use of 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
Aside from 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 be forbidden? GameObject.Find can lead to several run-time errors that cannot be caught at 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 (in favour of better alternatives).
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 interface of the service from its implementation. This is another problem of the 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 among the most powerful concepts at the core of every well-designed code. Instead of promoting the usage of interfaces and therefore abstraction, Unity pushes the coder to use Monobehaviour, even when the use of Monobehaviour is not necessary.
What’s wrong with the Singleton Pattern
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 Singletons, I will not answer you with the usual answers (they break encapsulation, hide dependencies, make it impossible to write proper unit tests, bind the code to the implementation of service instead of its abstraction, make refactoring really awkward, usually create memory leaks), but with the hindsight of the practice: your code will become unmantainable. This is because the use of Singletons comes without any design constriction that, while apparently makes the coder life easier, will make it a hell later on, when the code turns into an incomprehensible blob without a structured flow (spaghetti code).
Projects developed by one of two coders wouldn’t experience the problem at first or ever (for example, if the project doesn’t need to be maintained over time), but mid-size project developers will pay the consequences of wild designs when they realise that refactoring became a very costly operation. 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.
If this is not enough to dismiss Singletons think of this: what is a Singleton if not just another way to inject dependencies? When you don’t have any other choice than singletons to inject dependencies, you can be sure that your design smells!
Furthermore, singletons must rely on public functions, which may 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 changed. Refactoring becomes tremendously risky because changing the order of the calls of the public functions of a 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 being fundamentally a globally accessible variable.
Communication between objects is the hardest OOP problem to solve and the main reason why dependencies are injected is to let objects communicate with each other. Communication is impossible without any form of injection. Therefore while in some rare cases, 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 kinds of communication problems through singletons.
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. Forgetting Singletons, I use the Service Locator Pattern ONLY when it’s used to inject actual stateless services. Manual Injection would then be the preferable solution, but since Unity doesn’t have an entry point where dependencies can be created and injected, manual injections become quite hard to achieve without a good understanding of the Unity limitations.
The last 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 make it simple to use with Unity projects.
The second part of this article (including source code) is available here: http://blog.sebaslab.com/ioc-container-for-unity3d-part-2
2022 Update: Note that I abandoned the use of Inversion of Control containers in 2015 and I do not recommend their use anymore (more on this in the next articles). Manual dependency injection is far superior.
I strongly suggest to read all my articles on the topic: