C# is an Object-Oriented programming language and Unity extends this paradigm for video game programming. This means game objects are responsible for their own actions. Other game objects may trigger these actions, but in general, it should be left to each game object to determine the details of how that action is carried out. What this means from a programming point of view is game objects need to communicate with each other in some way.
We’ve already seen how objects can declare both variables (properties) and methods as public or private. Public variables and methods can be executed or accessed by any other game object. There are ways to limit access to only certain game objects, but we’ll leave that discussion for another day.
There is a twist in the story though. You can’t just say Enemy.Destroy()
because while there may be only one Enemy class, each time we instantiate it, we create a new Enemy object. So, we have to know exactly which Enemy needs destroyed.
In a previous post, we showed how to destroy an enemy when it collides with the player. So, let’s build on that. Let’s destroy the enemy when it collides with a laser bolt also. We’ve already seen how to do that, and it is easy enough to add. However, we need to destroy the laser bolt also. As it turns out, this task is not much harder. We’ve already seen that a reference to the other object’s collider is passed in. So, we can simply pass other.gameObject
to the Destroy()
method also. Now, I am not a huge fan of this. I feel like the Enemy object is taking matters into its own hands. But it is simple enough we’ll let it sit for now.
using UnityEngine;
public class Enemy : MonoBehaviour
{
[SerializeField] float mySpeed = 4f;
...
private void OnTriggerEnter(Collider other)
{
if (other.tag == "Player")
{
Destroy(this.gameObject);
}
if (other.tag == "Laser")
{
Destroy(other.gameObject);
Destroy(this.gameObject);
}
}
...
}
This is one way objects can interact with each other. That is, by passing a reference or pointer to itself to a calling method. In this case, when two objects collide and the OnTriggerMethod()
is called, a reference to the colliding object is passed. That means, variables and methods made public by that object can be directly accessed.
Unfortunately, not all components are so easily accessible. This is because most of those components are optional. So we have to go find them. Unity gives us a variety of options for doing this. One such is the GetComponent<T>
method. Note: the GetComponent<T>
uses a Generic Type which is kinda complicated, so we’re going to skip past that until I understand it better.
using UnityEngine;
public class Enemy : MonoBehaviour
{
[SerializeField] float mySpeed = 4f;
...
private void OnTriggerEnter(Collider other)
{
if (other.tag == "Player")
{
other.GetComponent<Player>().TakeDamage();
Destroy(this.gameObject);
}
if (other.tag == "Laser")
{
Destroy(other.gameObject);
Destroy(this.gameObject);
}
}
...
}
What the GetComponent<Player>
call does is search through the game object looking for a component called ‘Player,’ which is the name of the script attached to the player game object.
Once it finds the component, Unity then invokes it like a method/object GetComponent<Player>()
. Once it does that, we are then able to access any of the public variables and methods in our script, including the TakeDamage()
method.
Now, you might have seen some examples that cause you a bit of confusion. For instance, it turns out, the following four lines of code are functionally equivalent.
private void OnTriggerEnter(Collider other)
{
...
other.GetComponent<Player>().TakeDamage();
other.transform.GetComponent<Player>().TakeDamage();
other.gameObject.GetComponent<Player>().TakeDamage();
other.gameObject.transform.GetComponent<Player>().TakeDamage();
...
}
As you can see from our method arguments, ‘other’ is of the type Component.Collider. This might seem contradictory according to other things you might have been taught. First, we know the object dot notation usually represents a parent.child relationship. This might lead you to believe, from the first line, that our Script is a child or component of Collider component. But we can see visually from the Inspector this is not true. You might also have been told the Transform is the same as the GameObject. So what’s with the other three lines?
First, let’s look at “Transform is the same as GameObject.” Transform, Collider and our Player script are all Components and are attached to the GameObject. So that means GameObject is actually the parent of all three. GetComponent<>
then searches all the components attached to the game object. It just so happens Transform is the one component every game object must have, and it can only have one. So, in this sense, they are synonymous. Because the other components are optional, we must use the GetComponent<>
method to find and access them.
That still does not answer why our dot notation seems to be so wishy-washy and back-and-forthy. Why is there more than one road to our TakeDamage()
method? In other words, shouldn’t gameObject.GetComponent<Player>().TakeDamage();
be they right path? And how do we get there from ‘other’ in the first place when other is a child of gameObject, not the parent?
Well, this goes back to the explanation of our own function call. Game objects, components, etc. can pass references to themselves around. So, other.GameObject
can be visualized as being a bit circular. Not only does our game object have a reference to its collider (represented as the variable ‘other’ above), but the collider also has a reference to the GameObject. In essence, other.GameObject
is allowing us to travel up the hierarchy, so we can then travel back down to the component we want.
The same is true for our Transform component. And in fact, since both are generally considered synonymous, we can even do something silly like this.
other.gameObject.transform.gameObject.transform.GetComponent<Player>().TakeDamage();
Finally, the GetComponent<Player>()
method. We’ve already determined the Player script is not a child of the Collider. So while we can understand why calling GetComponent<Player>()
on GameObject and Transform will work, why does it work on the Collider? Simply stated, the GetComponent<T>()
method is provided as a convenience. Since components must be attached to a GameObject and Components have a reference to their GameObject, the GetComponent<>()
method can be provided as a convenience and make the assumption we are looking for Components attached to the GameObject and not the Component itself. Because of that, all of our other...GetComponent<Player>().TakeDamage();
calls all work functionally the same.
So remember, as in any relationship, communication is the key!