After quite a few years of not only using Unity, but also helping out people on a wide spectrum of skills, I’ve come to identify some of the more important topics that people come across when they’re learning to code with Unity. And among these topics, one that separates beginners from pros is being able to set up communication between objects.
A common occurrence seems to be that people randomly come across one of many ways to have object A interact with object B, learn it and consider it the way to go. That way is usually easy to understand for programming beginners, but often not very good in the long run. The bigger problem however is that there’s no one good way to go about this. Learning multiple techniques and knowing when to use which is essential to becoming a good programmer that can handle even projects of greater complexity.
This article will not be a definitive guide or an exhausting list. But it is supposed to show a few solutions to specific problems, and – more importantly – explain a bit about the thoughts that should influence your decisions in this matter. The intention is to give you a better understanding about this topic, which hopefully will allow you to analyze problems and their possible solutions from a new perspective.
Dependency Injection
Dependency Injection (DI) is a term that seems to sound a bit advanced or even overwhelming to some. What people often miss is that everyone who has used Unity for more than a day has already used Dependency Injection. And it’s one of the good ways to go about object communication.
The term describes the presence of some sort of system that introduces objects to one another. Unity and its editor form a DI system that allows you to drag an object into the inspector of another.
The code of the component that you dragged the object into will then be able to use the object that you specified this way. In the background, Unity will have injected a reference (or dependency) into your object, basically telling the button which door to open. Neither the button’s nor the door’s code needs any kind of knowledge about this. This makes this approach different from, say, using GameObject.Find, where one of the objects has to have code that has to find the other one.
There are many DI systems out there, and some are even meant for use in Unity. But to use DI, you don’t need to go further than Unity itself.
To reiterate on how to use it, even though it’s one of the basics that probably everyone already knows, define a “serialized field” with the type GameObject
or the type of the component you want to use (like Door
). In fact, all UnityEngine.Object
s allow this, which includes most assets.
[SerializeField] private Door doorToOpen = default; public void PushButton() { doorToOpen.Open(); }
When to use this?
This technique comes in handy whenever you, for example, want something defined by level design. Which button opens which door? What cutscene does this trigger start? Which item will the player receive when opening this chest?
You always need to ask yourself: “Which part of my game will define which object will be used here?” And if the answer is, for example, “my level design”, you’re probably smart to use this simple Dependency Injection option, where you drag one component into another.
It’s less smart to use this in many other cases. Take, for example, a camera prefab that always follows the player. You could make a serialized field in the camera’s “follow component” and then drag the player into it; but that means that you will have to do that over and over again each time you set up a new scene. You could slap your player and the camera into one prefab, but that’s usually going to cause other problems.
A case where this approach is to be judged on a case-by-case basis is in-prefab references. Imagine a tank prefab where a component on the root GameObject references a component in a child object, like the cannon, because it has some degree of control over it. If the component is never going to have any variance regarding which object it’ll reference, you’ll have a field in your inspector that’s not supposed to ever have any other value than the one intended by the script. This means that the inspector contains unnecessary clutter; and each field that can accidentally get a different value assigned than the intended one is an unnecessary source of potential bugs.
However, depending on your game design, that very same component might work in a different, but intended way if there’s another – or no – object assigned. Maybe you want to use the same script for a tank that still has a cannon, but that cannon is used by an AI rather than the player. Choosing to not assign the cannon in the inspector allows you to define such cases without writing extra code.
As you can see – there’s no generally right way to do this. Even in two cases that are exactly the same, one approach might be good or bad depending on the game design context.
Static fields
I’ve seen my fair share of people getting to know the static
keyword for the first time and then thinking of it as a magic word that allows us to make a thing visible to other objects so they can use it. Coming after that is a thread from a confused beginner who wonders how one would have a second door to open with a second button.
The static
keyword is used to unbind a field or method from object instances. In other words: When you have two light sources, one can be blue and the other can be red. That is because the color property is not static and therefore, each light object has its own value for it. If the color property were static, there would be a single, program-wide color value that would apply for all light sources.
In non-static contexts, code needs to be told which object they’re supposed to be working with. Is the button opening door A or door B? It makes sense that the program cannot magically decide that for you, so you need to answer that question before opening your door. However, in a static context, the question is superfluous. You don’t need to define any “which one” when there’s only ever one. Since all light sources have the same color with a static color property, you just need to change “the” light color, and you’d never have to define which light source you’re talking about.
When to use this?
I hope that it’s obvious by now that all this means that you cannot just throw static
on a field in order to avoid the step of defining which object you’re talking about. Thus, using a static field is a bad idea when it comes to level design questions, like which button opens which door. It’s useful when, and only when you can be sure that you’ll ever only have one thing of a kind.
Examples could be the player’s health or the current score. Imagine making a simple platformer or endless runner. Whenever you collect a coin, you add 1 to the static coin count field. Maybe you have a static Game Over event that will be invoked under certain conditions. Having your input being handled to some degree in a static context makes sense – why should there be multiple input systems?
Again, some of these cases are not safe bets for static
. What if, at some point, your game gets multiple players for local multiplayer? You’d have to revisit the code and add more hp fields, more coin fields and whatever else each player has to have. Your UI text object that displays the number of coins would have to be duplicated, and suddenly, you are back at the initial question: How do you define which text displays which coin count? This is not an answer that cannot be answered, but as a general rule: If your solution creates a new problem, which has a solution that creates another problem… you should reconsider the path you’ve taken.
One of the things you can do with the static
keyword is using a certain pattern that is often referred to as “singleton”. If you’re interested in reading more about this, here’s an article for you.
Intermediaries
In many cases, there are good reasons to avoid direct contact between objects.
Imagine implementing a Game Over. Some things can trigger it (like losing all health points or running out of time) and some things happen as a result (like the screen fading to black, a Game Over scene being loaded or the player respawning at the last checkpoint).
Even though the timer eventually triggers the player respawn, these two systems don’t have an inherent semantic connection. If you ever want to change what happens on a Game Over, it would be a bad thing to be forced to revisit the timer script. Timer and respawn should be anonymous for each other. And on top of that, if the player health system can also trigger a Game Over, it would be extra bad if you had to revisit that code, too. In this case, and a multitude of other cases, there are two objects that need to interact, but shouldn’t know each other. That’s where you can use intermediaries.
How such an intermediary is implemented is another question in itself, as there are – again – multiple viable options. In the case of our Game Over event, a static event might do the trick.
public static class GameOver { public static event Action onGameOver; public static void Trigger() { onGameOver?.Invoke(); } }
This class and its event are our intermediary. Objects can sign up to respond to the event:
private void OnEnable() { GameOver.onGameOver += RespawnPlayer; } private void OnDisable() { GameOver.onGameOver -= RespawnPlayer; } private void RespawnPlayer() { // TODO Respawn the player }
And other objects can trigger the event, and with it, all the currently registered responses:
GameOver.Trigger();
As you can see, the class that triggers the Game Over and the class that responds to it don’t even know that the other one exists. But through the intermediary, one can trigger the other object’s response. In addition, any number of triggers and responders can be added around this event, and none of the existing classes would need to be updated.
If you extrapolate this, you might end up with a proper event system, which would be a system that allows for any number of events without having to write a new class for each. Event systems can come in different sizes and shapes, too. Possible options range from a static class that manages all the events (in a dictionary, for example) to using ScriptableObjects, which brings us back to Dependency Injection, but for intermediaries. And events are not the only things you can put between objects for communication, so take this section only as a brief introduction.
Hardcoded object searches
Let’s get back to the example about a tank prefab that has multiple components distributed throughout its GameObject hierarchy.
Let’s assume that we are in a situation where we can be sure that component A requires component B to properly function. If a prefab was set up so that there was no component B available, the nature of what component A does doesn’t allow us to just ignore that and keep running.
If both components were on the same GameObject, we could use the [RequireComponent]
attribute (as described in this article) to have Unity making sure that a component B exists for us. But sometimes, it makes sense to have components on different GameObjects even when they depend on each other. In these cases, it’s definitely possible to just make a serialized field and draw B into A’s field. But, as stated before, requiring this kind of mandatory setup in the inspector might be an unnecessary source of potential bugs and means useless clutter in the inspector. If you know that your code will require a very specific thing, why fulfill that requirement outside of that code itself?
In cases like these, a handful of Unity’s methods come to mind. For example, Transform.Find
would allow to specify a GameObject somewhere down the hierarchy through names. We could use it to find a component on a clearly defined child object. However, this means that we depend on the hierarchy being set up perfectly in the way that the code expects it. If someone on the team who uses the editor happens to forget that this restriction exists, it’s almost inevitable that bugs are introduced. Nothing will keep that team member from correcting a spelling error or renaming an object because a more speaking name was found. The resulting bug is guaranteed to be relatively hard to find, thus causing headaches for night after night.
To avoid names and other strings (like tags), GetComponentInParent
and GetComponentInChildren
are better options. They’re bound to be somewhat slower than a reference set through Dependency Injection in cases with complex GameObject hierarchies, but as usual… don’t avoid a solution for performance reasons unless you’ve noticed performance issues.
Using these methods allows you to get a reference to a component of a specific type in your GameObject hierarchy, specifying nothing but the type. In cases where you might have multiple components of that type in the hierarchy, but you only want to have one, you’re back at the old “which one?” question. It would need to be answered trough different means, and that alone could mean that going back to Dependency Injection is worth considering.
It happens that this approach also works when you have a component that doesn’t require the other component. If the method returns null
, your component can just keep working as long as that makes sense. In cases where the other component is actually required, we still have a similar issue as with Transform.Find
: What will keep someone who doesn’t have the entire codebase in mind from doing something in the editor that results in the code not working as intended? How can we make sure that required objects exist for our component to find? It turns out that Unity doesn’t really have an answer for that, which is why I wrote a package called “Validation“. It allows you to extend your component to contain a test case that checks things like this. Check out the description on the project page for more infos.
Overall, hardcoded object searches are never the perfect option to go with. Since we’re usually talking about objects that are created in the editor, we are writing code that has to make assumptions about objects that are created in another world (the Editor), possibly by other people or yourself in the future, who forgot the specifications written in the code. The Validation package helps with that, but it doesn’t remove the initially invisible restrictions that are imposed on the editor user by the code. In comparison, if someone adds a component on a GameObject and sees that there’s a few fields in the inspector that look like they’d really like some values assigned… that helps making the intended way to set up the component more obvious.
Conclusion
This article might be a bit underwhelming for you if you expected some solid howtos and guidelines to follow. But if you read carefully, the intention is that you noticed how defining those guidelines is virtually impossible. The entire context of your component plays into what eventually becomes a list of pros and cons that you have to consider. And which of these pros and cons are more important depends on the complexity of your project.
You’re participating in a game jam? Don’t even waste energy on these things.
You want to make a more complex project that you’ll have to develop and maintain for months or even years? Better spend time on these issues early or they’ll come back to bite you.
All the available options, all their pros and cons and all the factors to consider add up to the very complex topic of inter-object communication.
There’s enough to explore and definitely enough to practice for many years.
This is why I consider this topic to be one that separates beginners from pros.