We're gonna talk about how to structure your code with UnityEvents so it won't become a huge spaghetti. First, we'll look into the disadvantages of using GameObject.Find and GetComponent function calls. Then I'll explain how Events work and how they make the code more flexible and cleaner. We'll cover topics like code dependency and tightly coupled code.
Gamedev channels mentioned:
Blackthornprod: https://www.youtube.com/channel/UC9Z1XWw1kmnvOOFsj6Bzy2g
Brackeys: https://www.youtube.com/channel/UCYbK_tjZ2OrIZFBvU6CCMiA
Dani: https://www.youtube.com/channel/UCIabPXjvT5BVTxRDPCBBOOQ
Jonas Tyroller: https://www.youtube.com/channel/UC_p_9arduPuxM8DHTGIuSOg
Sebastian Lague: https://www.youtube.com/channel/UCmtyQOKKmrMVaKuRXz02jbQ
Sykoo: https://www.youtube.com/channel/UCNJvwJ6daLmw4_gUKTw4cSg
ThinMatrix: https://www.youtube.com/channel/UCUkRj4qoT1bsWpE_C8lZYoQ
Thomas Brush: https://www.youtube.com/channel/UCuHVjteDW9tCb8QqMrtGvwQ
Are you interested in making games with Unity?
I upload frequently, so stay tuned and consider subscribing!
Hi, I'm CouchFerret and follow me on my journey of indie game development. You will find in depth tutorials about developing games in Unity. We will cover topics from programming to art and game design. Whether you are an expert or beginner, you will definitely find new tips and tricks about the whole game development process. These how to videos help you learn unity to develop 2d and 3d games on your own.
● Sprites and Project Files on Patreon: https://www.patreon.com/CouchFerret
● Join my Discord Server: https://discord.gg/dac7sr2
● Tweet me on Twitter: https://twitter.com/couchferret
● Follow me on Instagram: https://www.instagram.com/couchferret
● Like me on Facebook: https://www.facebook.com/couchferret/
#Unity #UnityTutorial #gamedev
What's up game developers? CouchFerret
here! Welcome back to yet another video of making awesome games in Unity. Today
we will talk about how to structure your code in Unity so it won't become a huge
spaghetti. We'll do it by replacing the GameObject.Find() and GetComponent()
function calls with UnityEvents. And I'll explain how a tightly coupled
code looks aka spaghetti code and why is it bad to have one. Roll the intro! First, huge thanks to all the awesome patreon supporters. Thank you, guys.
With your help
I can dedicate more and more time to make these videos and also my game. Okay, let's jump back into Unity I'm gonna recap how the code looks now and what
components we have. In the scripts folder we have the GameLogic which sits on the
GameLogic object and this component contains all the game related logic so
for example this contains the respawn logic or in the future this could
contain the score logic everything related to the game and the game modes. And we
have the PlayerCont
roller which sits on the archers. This component handles
the archers movement shooting and aiming. And when we shoot with the archer it
creates an arrow GameObject and an Arrow component on it and that's the
last component we have. This component handles the arrows movement and its hit
detection. Let's open it up! When it hits an archer it destroyes the arrow itself
and with the GameObject.Find() function it searches for the GameLogic
GameObject and it gets its GameLogic component with the GetCo
mponent() function
and then it calls its SpawnArcher() function. Which will respawn the archer who
got hit. And this line is a strong dependency which makes the code tightly
coupled. And later in development when our game is more complex the tightly
coupled aspect of it will make it very rigid and hard to change. So for example
if one day we want to create a new version of the game or a new level and
we want to start from an empty scene and slowly drag in our existing components
or game objects
we quickly realize that our components
strongly depend on the rest of the components
we have to include them as well to avoid any errors. And we'll just end up with
the same scene or very similar to it. But there's another problem with this
tightly coupled system that if we change a given component there is a very big
chance that we have to change the rest of the components that depend on it to
avoid any errors. But there are many many problems with a tightly coupled system.
If this was confusin
g don't worry I'm gonna show you an analogy from real life.
Let me get just some paper. Let's say that we have a bunch of cool game dev
youtubers from real life and in Unity they represent the components. Channel
links are in the description, check them out! Okay in Unity all of them have a
specific purpose and they handle certain mechanics. For example, Dani handles the
respawning Brackeys handles the score system and Jonas handles arrows
and so on. And our code's flexibility really depends on
how we handle the
communication between the components and in our analogy between these awesome
games of youtubers. With GameObject.Find() and GetComponent() function calls
they directly talk to each other like in a private conversation. So everybody has
to know what the others are responsible for and how to address them. And this is
how strong dependencies look like. There are multiple disadvantages of doing this.
Let's say that in our game one of the archers has shot another and we'd like to
a
ward the archer with points and respawn the other. Let's say that Dani handles
the respawning Brackeys handles the score system and Jonas handles the arrows. When an arrow hits Jonas has to talk directly to Brackeys to award points and speak
directly to Dani to make the other archer respond. That's two and very exact
communications. And in the long run with a lot of new mechanics and systems this
arrow script can turn into a bunch of direct communications to several other
components. However, th
is is just a minor issue compared to the following one. What
if Dani is just Dani and he leaves for a bit to buy a few pints of milk meaning
that his GameObject and his component both go missing and currently not in the
scene. So when Jonas tries to reach Dani's GameObject he won't find
him and the game will probably end up throwing a lot of NullReferenceExceptions. And if we want to solve the absence of Dani by substituting him
with somebody then we have to tell this change to Jonas, in this ca
se, change the
code where Jonas searches for Dani's GameObject. We'll have to change the
GameObject's name in the Find() function probably the component's name in the GetComponent() function and maybe the function call itself as well. And this
rigidness in the code will eventually turn into a huge burden when we want to
change something. Okay, let's see how we can solve this with an event-based system.
Let's talk about what an event is. An event is an action or occurrence of
changes in the game.
For example, an arrow hitting an archer could be one but picking
up a powerup could be another one. In this case the event is about an arrow
hitting an archer. This is the thing that our code will react to with awarding
points and respawning the archer. An event happens when something invokes it. And to react to an event we have to listen or subscribe to it so we can do
something when it happens. In our real-life analogy an event would be a
shout about something. In this case, Jonas would be sh
outing about the arrow
hitting the archer instead of talking one by one directly to the others. He
just shouts and he doesn't care about who are listening to it. In Unity, it would
look something like this archerHit.Invoke(shooter, victim, arrow). This fires an archerHit event with all the necessary information in it.
It's like if Jonas shouted "Sykoo just got hit by Thomas with that arrow".
Having this event invoked by Jonas we can eliminate all the direct
communications that he manages to Brac
keys and Dani. But to have Brackeys react
to this we have to make him listen to this specific event. And in Unity it
would look like archerHit.AddListener(SomeFunction). With this line
Brackeys will carefully listen to archerHit events but it doesn't matter where
the actual event is coming from. It could have come from Sykoo as well. So
Brackeys will be able to award points based on the information in
the event. And also we have to make Dani to listen to the archerHit event so he
can respawn the
poor archer who got hit. With this system
nobody has direct communication to the others and nobody has to know what the
others are responsible for and how to address them. Because of this when Dani goes for a barrel of milk there won't be any NullReferenceExceptions but
there won't be any respawning as well. But somebody can easily take over his
part by listening to this event and handling the respawning. And the funny
thing is that nobody will notice this take over. We won't have to make chang
es
in the existing codes of others. And we also be able to start a new scene from
scratch and add a few components into it without getting any errors we'll just
have a few events that nobody listens to. And in the future if you want to add
cool sound or visual effects we can just add a component into the scene and make
it listen to the archerHit event. No changes will be necessary in the
existing code. And that's the beauty of this event based system at least on
paper. Okay, that was way too muc
h talk let's jump
into Unity. We'll use UnityEvents for events to replace the existing direct function calls. But first let's create the archerHit
event. I'm gonna have this event and all the future events in a static class so
we can globally reach them from anywhere. Okay, let's create a new folder and a new
script in it. I'm gonna call this BowHandEvents. BowHand is the name of the game. In this script I'm gonna define a static class and we don't have to
inherit from MonoBehaviour. Let's delet
e all these functions and I'm gonna define
a public UnityEvent archerHit and assign a value to it by creating a new
UnityEvent. However, we have to use the UnityEngine.Events to make the errors disappear. And also don't forget to make the
variable static as well. Okay, static classes are special classes that cannot
be instantiated and can only contain static variables or static functions. So
we'll have to use the static keyword all the time to avoid any errors. Static
variables are bound to the
static class itself and not the instance of the class
so we'll have to use the static class's name when we referencing the variables.
For example, I'd like to put all the events into this static class because
this way we can reference them just by typing the class name BowHandEvents and
static variable's name and that's it. And this referencing will work from anywhere.
You might have heard about Singletons or the Singleton pattern itself, now
static classes are kind of similar to them but a bit
simpler and a bit less
flexible however they are still an awesome tool in our toolbox. One
important thing is that static classes are initialized when the application
starts and their variables keep their values between scene changes. Okay, our
static archerHit event is defined so we can invoke it in the arrow script when
the arrow hits the archer. We can easily do that by typing the static class's
name BowHandEvents and the static variable's name archerHit and just
call the invoke function of t
his event. We did that next to the GameObject.Find() and GetComponent() call. Okay, this will only invoke the event and
won't make the other components listen to it. So we need to modify the GameLogic script to make it listen to the archer hit event. Let's define the Awake()
function and in the Awake() function let's reference the event. And we can make the
GameLogic component listen to the archerHit event by using the AddListener() function. In the function's argument we have to specify a funct
ion
that the event will call when it happens. So let's just create a simple
function with Debug.Log() it. I'm gonna call it ArcherHit() and I'm gonna
print out an Archer HIT! message. Now we can use this function in the AddListener() function call. This way when the archerHit event is invoked it will
call the ArcherHit() function of the GameLogic component which will print
out a debug message. One very important thing is that in the AddListener's
function call the ArcherHit() function doesn't ha
ve its parentheses. This is
because here we don't actually call the function we just pass a reference to it.
Okay, let's try it out if we hit one of the archers we should see a debug line.
And here it is. Awesome! One question you may have is that how the ArcherHit()
function will know which archer should it respawn. Well currently it doesn't
know. And if we try to create a parameter in the ArcherHit() function it will
throw an error at the AddListener() function call. This is because the archer
hit event doesn't have arguments. So let's go back to the BowHandEvents
script. One of the awesome thing about UnityEvent is that it has generic
versions. By subclassing of a generic version of the UnityEvent we can create
new types of events that has certain arguments. First let's try it out by
creating a new UnityEvent with one integer in it. We need to define a new
class let's call it HitEvent and make it inherit from the UnityEvent.
Okay, with this line we've created a new event type called
HitEvent which is a
UnityEvent which contains an integer. If we change the archerHit event's type to
this new HitEvent and if we also change the right side as well, then our archerHit event will contain an integer. Okay, let's go back to the GameLogic script.
Here we'll have an error because the archerHit event's arguments don't match up the
ArcherHit() function's arguments. So let's define a new integer parameter in the
ArcherHit() function. Let's call it data and print it out in the Debug.Log
()
so we can see if it works actually. And because we've changed the event we have
to fix the line in the arrow script that invokes it. So let's go back to the arrow
script and in the Invoke() function's parentheses let's just type 42 so we can
see if the value itself propagates correctly through the event. Let's check
it out. Now if we hit play and shoot the other archer we should see a 42 in the
console window. And there it is. However, we don't want to just send an integer
through the event,
we want to send the archer who shot the arrow the victim who
got shot and the arrow itself as well. And to achieve this I'm gonna define a
new custom class that holds these three together and use that in our custom UnityEvent instead of the integer. So let's go back to the BowHandEvents script and
define a public class let's call it HitEventData. And in this case, we don't
have to subclass from anything specific because this class will be just about
holding these three datas together. So let's d
efine those three variables the
shooter the victim and the arrow. To make it easier to create new instances of
this class I'm gonna create a constructor. This constructor function
will be executed when we use the new keyword to create a new instance of this
class. With these three parameters and with
these three lines will be able to easily assign values to the instance variables.
Okay, now we only need to change this integer into HitEventData and our new
event is done. Let's go back to the Arro
w script first to fix the invoke call and
here let's create a new HitEventData and in this parenthesis we have to
specify the three things the shooter the victim and the arrow. In this case, the
shooter will be the archer, the victim will be the other, and the arrow will be
the gameObject. Okay, now we only need to go back to the GameLogic script and
modify the archerHit's parameter's type from int to HitEventData and we can
try out by printing out the victim, for example. Okay, now if I hit pla
y and shoot
the other archer we should see the archer who got hit in the console window.
And it's Archer2. Yeah, it works. Okay, now let's use the SpawnArcher() function
instead of the ArcherHit function that we created for debugging. First, we have
to make it listen to the archerHit event with the same line that we used
for the archer hit function. So let's just replace the function name. Now we
need to modify the SpawnArcher() function's parameter to make it match
with the HitEvent's argument.
So let's change the GameObject archer to HitEventData data and let's replace the archer this is the archer who got hit
with data that victim and that's it. Now, let's go back to the arrow script and
let's remove the old function call that made the archer respond. Now if I shoot the other Archer it
should respawn. However, this time it doesn't use a direct function call from
the Arrow script into the GameLogic script instead the Arrow script invokes
an archerHit event and the GameLogic script li
stens to it and makes the
archer respawn. And with all this work we decouple the code so that the Arrow
script doesn't depend on the GameLogic script. Now in the game logic script we
have the ArcherHit() function and the SpawnArcher() function and both of them
are listening to the archerHit event. So both of them will be called when the
archerHit event is invoked. However, the ArcherHit() function is just
only printing out the victim archer's GameObject and the SpawnArcher() function actually re
spawns the archer. I'm leaving this ArcherHit() function
here because in the future we'll be adding all kinds of stuff like sound
effects and visual effects and we'll put it into the ArcherHit() function. That's
it for today folks. Next time we'll talk about how to use ScriptableObjects to
handle game state in a decoupled way. Stay tuned, be sure to subscribe, and say hello to us on Discord. Huge thanks again to all the
awesome Patreon supporters. Thank you guys! See you next time!
Comments