Main

Unity How to structure your code with UnityEvents - Tutorial

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

CouchFerret makes Games

4 years ago

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