Organise large projects in Unity

Marcel Bloemendaal
we are YipYip

--

Unity is a fantastic platform for game development. It is largely free (unless you start making a lot of money with it), easy to work with and it can yield impressive results quite quickly. When using unity for somewhat larger projects, it can however be hard to maintain a good technical structure. On most platforms it is common to use design patterns like Model View Controller to keep everything organised and flexible. But since on Unity almost everything is automatically a MonoBehaviour, it is easy to end up with a lot of components that operate separately when your project really needs a shared central intelligence. On top of that Unity’s asset management isn’t always as transparent as you’d like it to be and especially dynamic asset loading can be a tricky thing to get right. After having made a few more serious projects myself, I thought I’d share my own findings.

General structure

Singletons

Unity’s component system is a really powerful way of adding and reusing functionality on game objects. But in most projects you quickly feel the need for your components to communicate with something more than other GameObjects or MonoBehaviours. At first this may seem impossible, but fortunately it isn’t. In fact, it can easily be achieved using singletons.

What is a singleton? A singleton is a class that is not meant to be constructed anywhere. In fact in clean singleton implementations, the constructor is even private or protected, so it cannot be constructed except by itself. Instead, the class contains a static function or property returning an instance of the class. This function is usually called something like ‘getInstance()’. Singleton classes usually have a private static variable containing a unique instance of itself. At the start of the project this variable contains null. When getInstance() is called, it checks if the variable (still) contains null. If so, it creates an instance of itself (as it can access its own private constructor) and stores that in the static variable. Then in any case it returns the instance in the variable. This way there is always only 1 instance of this class.

Although many classes in a Unity project will be MonoBehaviours, it is still very possible to use other types. Singletons can be accessed from anywhere, so they are perfect to use as controllers or managers as they can be reached from any component.

Single access point

Be careful though, not to create a whole bunch of singletons. If at some point there are something like 15 singletons in a project, it will become a bit of a guessing game for which things there is a singleton and what it’s called. If you do have the need for many objects to be accessible from anywhere, it is best to use 1 singleton, and give it a lot of named member variables. This way you always have a common starting point, and you can easily find which members you can access from there. I tend to use a ‘Dependencies’ singleton for this. This way you could access the current level with something like:

Dependencies.shared.LevelManager.CurrentLevel

If at some point there are something like 15 singletons in a project, it will become a bit of a guessing game.

Technical design

By having some managing entities, separate from your scene hierarchy, you can achieve a lot more structure in your project. Now that you have a way to do that, take the time to think about how you are going to organise your project technically. You don’t have to go as far as as using Model View Controller, but it does help a lot to have a general idea of how your project is structured and what is allowed to talk to what.

In my experience, it works very well to have sort of one way control between the code outside the scene hierarchy and the code inside it. I usually keep the managers to be somewhat more ‘model’ like. They mostly only control the underlying data of the game and dispatch any changes to this data as events. The Unity GameObjects and Components just listen to this and adapt accordingly. They also set themselves to the appropriate state in their Start methods, so the managers have to know as little as possible about the Unity side of the project. This means you’re really free in the way you display the data. You could even display it in multiple ways at the same time. Think of a minimap: the actual game character updates it’s position based on the data, but at the same time, it’s dot on the minimap also updates it’s position based on the same data. You could add this minimap without ever changing the code of your managers.

Take the time to think about how you are going to organize your project

Should it stay or should it go?

Apart from accessibility, there is another huge benefit to these non-MonoBehaviour singletons: they won’t go anywhere when the scene is unloaded. This is a feature you’d sometimes like to see in your Unity components as well. Luckily Unity offers you a way to do this. You can call

DontDestroyOnLoad(this.gameObject)

from any component, or an any GameObject. From the moment you call this, the specified GameObject and all of its children will no longer be destroyed when you unload a scene or switch to another. This is especially useful for things that need to remember their state when you switch to a different environment, like a HUD or certain kinds of overlays.

The downside is that although this command may prevent the object from being destroyed when you unload the scene, it does not prevent it from being created again when it is reloaded. After switching to a different environment a couple of times, you could end up with a lot of HUD instances on top of each other, possibly without ever noticing it. To solve this, you’ll have to implement a singleton behaviour for these components yourself. Here’s my code for doing this:

using UnityEngine;public class SomeBehaviour : MonoBehaviour {    protected static UnityHelper sharedInstance = null;    void Awake() {
if (UnityHelper.sharedInstance == null) {
UnityHelper.sharedInstance = this;
DontDestroyOnLoad(this.gameObject);
} else {
Destroy(this.gameObject);
}
}
}

When using objects that are not destroyed when switching scenes, it is probably a good idea to bundle them into a single (prefab?) GameObject, that you can reuse in any scene. Another benefit would be that you would only have to prevent the root GameObject from being destroyed. Its children will automatically be kept as well.

I do believe that this is something that should be used sparsely. While it is very convenient to preserve some objects and their state across scenes, you’ll create a more flexible project if everything is as state independent as possible. If something can properly adapt to the data state when it is initialised, it can be used more freely. So whenever you create a DontDestroyOnLoad object, make sure there is a good reason for it.

You’ll create a more flexible project if everything is as state independent as possible

Dynamic resources

Unity has a system folder in the Assets folder you can use for any resources that need to be accessed dynamically. In theory this sounds really convenient. The problem with this folder is that Unity specifically advises you not to use it. It appears to mainly be meant for rapid prototyping, as it is very easy to use. When used improperly (read: frequently) it can slow down start up time and build time severely. It also causes problems for Unity’s memory management. So it seems it is indeed to be used with caution.

The problem with the resources folder is that Unity specifically advises you not to use it.

Factories

Still, you’ll often have to load content dynamically, so how to do that? My solution is to use factory components.

A factory is a class who’s only purpose is to instantiate a number of other classes that usually are of similar types. If you have a game with a lot of enemy types, it is best if the code creating your level doesn’t have to know which types of enemies there are, to be able to construct them. If you would add an enemy type, you would have to change all classes that need to construct enemies. To solve this, you can use a factory. The factory class contains a function like ‘createEnemy(string enemyType)’ that returns the appropriate enemy. When an enemy type is added, all you have to change is the factory class.

Suppose you need to instantiate enemy objects, you would create an EnemyFactory. This component would have a public variable of type GameObject for all enemyTypes, in which you store the corresponding prefab. The code would look something like this:

using UnityEngine;public class EnemyFactory : MonoBehaviour {    public GameObject MeleeAttackEnemyPrefab;
public GameObject RangedAttackEnemyPrefab;
public GameObject HeavyAttackEnemyPrefab;
public GameObject CreateEnemy(string type, GameObject parentObject) { GameObject prefab = null;
GameObject enemy = null;
switch (type) {
case "MeleeAttackEnemy":
prefab = this.MeleeAttackEnemyPrefab;
break;
case "RangedAttackEnemy":
prefab = this.RangedAttackEnemyPrefab;
break;
case "MeleeAttackEnemy":
prefab = this.HeavyAttackEnemyPrefab;
break;
}
if (prefab != null) {
GameObject skin = (GameObject)GameObject.Instantiate(prefab);
}
if (enemy != null) {
transform.SetParent(parentObject.transform, false);
}
return enemy;
}
}

The public prefab variables in the factory have to be set from the Unity editor. This means that it will have to live somewhere in your scene hierarchy. Although this also enables you to assign the factory to member variables of other components, that is not a very clean and flexible way to access it. What I tend to do to improve this, is have all factories that need to be accessed a lot, store themselves in an instance var of a dependency object in their Awake() methods. That way you can instantiate enemies (or whatever type it is that your factory is for) from anywhere.

Exceptions to the rule

In most projects, I still had some files in the Resources folder. Mostly they were just JSON files that contained things like level descriptors and other definitions that had to be edited by designers. You could probably keep those in factories as well, but to date I have not found any drawbacks of having a few of those in Resources. It may not be the best way technically, but it does make editing some things much easier and more accessible for designers or other less technical team members.

Threading

Unity does not actually support threading. Still you can achieve something similar by using Unity’s coroutines. In larger projects using a data model that is outside the component hierarchy, as described above, I’ve often had the need to use coroutines in objects that are not MonoBehaviours or GameObjects.

The solution is yet another object in my Dependencies object, that I’ve called the ‘UnityHelper’. This helper object is nothing more than a MonoBehaviour that prevents its destruction when a scene is (un)loaded, and that can be accessed from anywhere. Whenever you have the need to run a coroutine, you can simply use this shared component to run it with. This can also be useful when some non-MonoBehaviour objects need to do something at a moment the Update or FixedUpdate methods are ran. The helper object could dispatch events in either of these methods, to notify your model objects they are being ran.

Whenever you have the need to run a coroutine, you can simply use this shared component to run it with.

Conclusion

Unity is a great tool for quickly setting up a game and produces results quickly. When working with it in bigger projects, you may need to find your way a bit more, but when you do I believe Unity is still very suitable for this. I hope the above will help some people sort out how to set up their projects nicely and enable them to keep enjoying Unity.

--

--