There are a lot of ways to communicate between gameobjects. So many that sometimes it might get confusing to decide how to do that. Here I’ll go one by one and generally describe use cases for each so that you can have a better time deciding on your specific use case
Drag and Drops
A quickfire way to get some information from another object is to have a public reference of the other object and access their fields. Pretty straightforward but an important thing to note here is to not give write access to the fields unless it’s absolutely necessary. Generally, a public property of the field with a get access should do the trick
Mostly it works well. But when you need to create prefabs out of your objects, and the dependency is out of the current prefab, it will break. Another hassle is when you create a new player, you will need to manually drag and drop all of its dependencies.
Use case: Use it if the dependency is within a prefab like the example above. Use it when the project is small, and you don’t even need to create prefabs
From scripts
A common way to do this is to get the reference of the other object using get calls. For example, our favorite get call: GetComponent<Behaviour>. It works really well for most cases. I have used this for a long time and even do sometimes. It solves the problems of the Drag and Drops
It adds dependency resolution-related code to your script which generally crowds it with unnecessary data. It takes the focus away from the core of the script even for a bit. Mostly it breaks the Dependency Inversion principle. In short, it means that it’s not an object’s responsibility to resolve its dependencies.
There’s another issue it can create. Imagine you have a GameManager class, and you need to access its variable from 10 different classes. From every class Awake/Start you will need to do something like this
FindObjectOfType call is costly. Doing this in 10 different classes can affect the loading time of your scene. Moreover, it means, you are repeating yourself 10 times, which is inefficient
Use case: Use it to resolve dependencies on the same object, children, or parent
Singleton
Now, this is an interesting topic. A lot of devs love singletons. There are a lot of opinions as to why you should and should not use singletons. I’ll give you my take
Generally yes, Singletons solve a lot of problems. In the previous section, we had a GameManager class that was needed in 10 different classes. If we use a singleton, we won’t need to find it in every class. The dependency is resolved by GameManager using a static reference.
I don’t like it for a few reasons.
- What if I need another GameManager in my scene? Singleton by definition is single. There cannot be multiple instances of this. This limited static instance then breaks our whole structure. I don’t like to put limitations like this that can break the code
- If you go into Project Settings -> Editor -> Enable Play Mode Settings, you will see an option that says Enter Play Mode Options. More about it here. What it mainly does is that it skips certain steps when reloading a scene to load it super fast. It drastically improves iteration speed. But for that to work, it skips recreating C# states, which means that it does not reset the static variables. It can break the game and create issues with singletons.
- Finally, for longer projects frequent singleton uses can create a habit of using many singletons in one project, even if something is not necessarily single.
Use case: For truly single instances, small projects, and if you are not using Play Mode Options
Zenject Dependency injection
With a singleton, one big problem we were trying to solve was to find the reference of a class and cache it, so that every other call
To review, GameManager was setting the static reference in the Awake call, thus resolving the dependency. We could have SoundManager, LevelManager, etc other “single” classes with the same code as Singleton. But what if we can have one setup class where we can set up all of these in one place? That would mean, that the dependencies of the managers won’t be resolved by the managers. The dependencies will be resolved externally by someone else. That’s where Zenject comes in.
Container.Bind<GameManager>().FromComponentInHierarchy().AsSingle();
Here’s an example. Zenject has installers that are starting points of execution. Inside the installers, you can write code like this to resolve the GameManager dependency. In the code, you are asking the Container (for simplicity image it as the scene) to Bind GameManager, searching for it in the hierarchy. AsSingle means that there should be one instance of this and give everyone that one instance.
[Inject] private GameManager _gameManager;
You can now get the bound GameManager from any script like this.
This is cool! It’s not static. It takes care of the GameManager and other manager dependencies. That means the dependencies are inverted to Zenject rather than the objects solving their own dependencies.
You can also use Zenject to solve GetComponent and other dependencies. Thus, inverting the dependency of everything to the context. This makes code cleaner and much more organized. Here’s a sample installer
Of course, this is a simple representation of how Zenject works. Refer to the Zenject Documentation to learn more and use it your own way. Find more here
Use case: You can use this to solve most dependencies
Conclusion and My Take
To summarize
Attribute | Drag and Drop | From scripts | Singleton | Zenject |
---|---|---|---|---|
Flexibility | Bad. Not so flexible when you try to make prefabs out of objects that are connected. | Good | Good | Good |
Extensibility | Bad. When adding a new object, you need to drag and drop all of its dependencies again | Good | First-time setup | Good |
First-time setup | Simple | Requires searching for components through the script. So requires code for the setup | Bad, Whenever we need more objects of the same type it breaks down | Complex |
Code Cleanliness | Good | Dependency resolution code inside the object | Bad. Can get messy when used too much | Good |
So, depending on our use case we can follow a combination of the above approaches. Personally, I like having Zenject for most of my dependencies that span across prefabs. For dependencies within a prefab, I use a combination of Zenject and Drag and Drop whichever makes the more sense depending on the situation. The important thing is that I try to resolve the dependencies outside of my classes
I hope it was informative for you. For the next part, we will focus on the communication using events. We will check out C# events, Unity events, Scriptable object-based event systems, and Zenject events (called Signals). We will see how each option compares to the other. Till then, good day!