Strings can be found in many places throughout the Unity Engine. Your first steps with Unity are very likely to involve using some strings at some point, because that’s what many resources tell you to do. Unity’s vanilla soon-to-be-replaced input system uses strings. And even though it’s gotten better over the years, one of the first official Unity tutorials, the Roll-a-ball tutorial, recommends the use of tags, which are strings.
In my projects, I completely ignore Unity’s tags feature, I don’t use GameObject.Find
, Transform.Find
, SendMessage
or whatever else involves the use of strings… unless it’s about input or output.
This article explains why.
Why strings are bad
As a simple example, let’s take a look at the GetComponent
method. More specifically, these two overloads of it:
GetComponent<T>() GetComponent(string type)
The first version is what you’re supposed to use in pretty much all cases. You write the type of component you want to get into the pointy brackets. The second version is more of a relic from the time where Unity would recommend using Unity’s JavaScript variant for scripting, which had generics (aka the pointy brackets) tacked on top.
Both overloads do the exact same thing: You specify which type of component you want and get back either a component of that type on the GameObject
in question, or you get back null if none can be found. Still, you should use the first variant and not the second, and the reason is that the second uses a string.
The reasoning behind the string being problematic here is rather simple: Nobody can check whether you mistyped before you hit play. Even when you hit play, any spelling error in your string will only create an exception when the GetComponent
method is actually called in your line.
In his book Why Programs Fail, Andreas Zeller describes what can be seen as multiple stages of bugs. Very simply put:
- A defect is a piece of code that is not as it should be. They are created as mistakes made by the programmer.
- An infection is an erroneous piece of program state created by a defect, or another infection. For example, a defect in your formula can give a person a negative age without this making sense for the program.
- A failure is an infection that has become visible to the user. Maybe the negative age is displayed, the enemy is suddenly in another place for no apparent reason, or the game crashed.
A typo in a string is a defect. If the misspelled type in your string doesn’t exist, you get an error message (we’ll count that as a failure here), but in the worst case, that type (or that tag) actually exists and we get an infection that isn’t even visible right away. Then in some completely different part of your code, you get a failure – but the defect is somewhere else.
Furthermore, even if you don’t make a mistake in your string, you might at some point rename your component. As a result, all your strings with the old component name in them instantly become defects, and it’s hard to be sure that you have found and changed them all.
Why other things are better
The single reason why the first GetComponent
overload is better: The computer knows what you’re talking about and sees defects. While it has to accept any string as a valid value, a type, as put in the pointy brackets, is a thing where the compiler understands what it is.
So if you create a defect like this, like by writing the name of a type that doesn’t exist, the compiler will support you by pointing you to it; you can’t even start play mode. The same goes when you change the class name at some point, but any decent enough IDE will even help you avoid the issue in the first place by offering you a renaming option that will automatically update all the occurrences of that name.
So you should always avoid using strings when there’s an alternative that the compiler and the IDE can understand and help you with. But there’s not always an alternative… right?
There’s always alternatives…
Choosing between one GetComponent
overload and another is simple. But what about tags? What about GameObject.Find
?
Let’s start with Tags. Tags can be used in two ways:
- To check whether a specific object has a tag. This is done using
CompareTag
. - To find one or more objects with a specific tag. This is done using methods like
GameObject.FindWithTag
.
In the first case, we have found an object (for example through collision) and want to know whether it’s a specific object (like the player) or belongs to a specific group of objects (like enemies). The tag-less solution here is basically right in front of you: A component.
Instead of setting a tag, add an (empty) component to your GameObject
, and instead of using CompareTag
:
if (collision.gameObject.CompareTag("Player"))
…use GetComponent
and see if it returns an object reference or null:
if (collision.gameObject.GetComponent<Player>())
“Wait”, you say, “this can’t be good for performance!”
I have multiple answers to that.
- I prefer clean quality code over performance. If i can sacrifice a bit of performance to get more readable or robust code, I’ll do it.
- People go crazy over performance and I have no idea why. Most of the time, we’re not talking about the difference between 30 and 60 fps, we’re talking about very few nanoseconds per operation. You’re not programming for a Gameboy here!
- I’ve benchmarked this.
GetComponent
is faster thanCompareTag
.
Note for advanced Unity users regarding 3.: Yes, there should be some overhead for deserialization and then for garbage collection. But if you worry about this, please refer to answers 1. and 2. 🙂
Lastly, I’ve experienced that in a majority of cases, the empty component doesn’t stay empty for long. Most of the time, you want to check an object’s tag because you want to interact with it in a certain way if it has a specific tag:
if (collision.gameObject.CompareTag("Player")) { collision.gameObject.SendMessage("AddCoin"); }
Whenever this is the case, you can just check whether the GameObject
has the component you’re trying to work with, and if it has it, work with it:
var purse = collision.gameObject.GetComponent<Purse>(); if (purse) { purse.AddCoin(); }
or of course with the fancy TryGetComponent
method:
if (collision.gameObject.TryGetComponent(out Purse purse)) { purse.AddCoin(); }
So let’s take a look at the other case, where you use a Tag to find an object. It’s basically the same case as GameObject.Find
: String in, object out.
Which alternative to use here depends on the situation. I believe that knowing the right solution for any context here is part of true Unity mastery.
For example, you could have a button and a door in your scene. You want the button to know which door it opens. You could make the Button use GameObject.Find
to get its door. But instead, it’s better to make a serialized field that references the door, and to drag the door GameObject
into that field. A possibly even better solution would be to use a UnityEvent
, so your button can do anything instead of being limited to opening doors.
Another example could be a camera following the player. The camera could find the player using the “Player” tag. But instead, you could create a static field which the player object registers itself to, and all objects can find the player through it. In Unity circles, this solution is mostly-accurately known as a Singleton. An often also very good solution would be a clever use of ScriptableObjects.
…except when there’s not.
There’s one big exception to the no-strings rule: Input and output.
Input means anything from reading a file, over the player entering something in a text field, to a web server response. Output means text displayed to the player, a URL used for a web request or a savegame being stored on a hard drive. In short: Any place where your program is in contact with something that is not part of itself.
Here, strings are the universal layer that everyone understands. It’s sequences of bytes, usually formatted in a specific way. When we read a file, we always need to write some exception handling for the case where the file’s content is not formatted in the expected way. This extra step is necessary because we can’t avoid using strings here; but it’s also the exact thing we can luckily avoid while we’re within our own program – by not using strings.
Conclusion
In summary, using strings can be avoided pretty much all the time while we’re creating the architecture for our game’s code. And since strings rather surely reduce the code’s quality, it’s a good idea to do so. With the exception of program input and output, the rule of thumb is:
You can create your game entirely without Unity’s tags, GameObject names, and similar. If you want to use tags or a method like GameObject.Find
, try to find an alternative way. There always is one.