Why Dependencies are Bad and How To Avoid Them In Unreal Engine | UE5

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
imagine you have all these enemies that want to attack the player but you want to limit the number that can attack at the same time and once one of the attacker dies you want another one to take its place and if the player dies you want all the attackers to go back to their original location so how do you implement this without having all of the enemies having to coordinate with each other keep track of who's attacking and who's waiting communicate with the player to get their current states and avoid all this mess of dependencies now the answer is the mediator pattern welcome everyone to the second part of this series on software design patterns in Unreal Engine in the last video we discussed the Observer pattern and how we can use it to communicate between different objects of our game without coupling their implementation if you haven't se that one I'm going to link it somewhere on the screen now and in this tutorial we will be discussing the mediator pattern we'll be covering two use cases in this video first I'll show you how how to use the mediator pattern to implement a combat manager and this combat manager will handle the combat scenario I showed you in the intro of this video then the second use case I will show you how to utilize a mediator pattern to fix some of the drawbacks of the Observer pattern that we implemented in the last video by creating an event manager so let's get started so what is the mediator pattern and what problem does it solve well this pattern aims to answer the question how to coordinate communication between different actors in my game without having direct references or dependencies on each other if we take the example in the beginning of the video how do the enemies know how many attackers are currently attacking the uh player and if they can also attack now or not so the mistake that most people make here is have direct communication between the enemy and the player and the enemies and each other not only does this create hard references which is bad for memory usage but also creates a dependency between the different actors in the game which means that they both need each other to function correctly it's always good to aim for what is called Loosely coupled code which means that separate parts of your game function independently of each other and if you want to make a change later down the line it's going to be a lot easier and your code is even going to be reusable in other projects so how does this mediator pattern work exactly well the mediator pattern says that to make certain actors independent of each other you should have them communicate only through a mediator that sits in the middle and never directly to each other the mediator then redirects the actions needed to the correct actor that way your actors only depend on a single mediator class and don't care what happens on the other end imagine the real life use case of uh airplanes approaching uh an airport and they want to land now the airplanes don't communicate directly with each other and coordinate you take Runway one I take Runway two no that would cause a lot of accidents they communicate through air traffic control which is a tower that sits in the middle and coordinates The Landing efforts of all the different aircrafts that's coming and that's a real world's uh usage of the mediator pattern so let's jump right into our first practical use case and build the combat manager now the problem here is you have all these enemies that want to attack the player but you want to limit the number that can attack at the same time and once one of the dies you want the other one to take its place and if the player dies you want all attackers to go back to their original location so instead of putting all of this logic in the player and the enemy class we're going to create a combat manager as our mediator to handle all of this combat logic and keep the player and enemy logic decoupled to do that we'll first create a new actor called combat manager and this actor is going to be placed in the world now this combat manager will need to keep track of a few things to achieve the behavior we want it will have a variable called attack Target which is just an actor reference then it will have a list or array of attackers which is an array of actors and finally it will have an array of waiting attackers these are the attackers that want to attack but the Max has already been reached and note that they are all actor references I didn't say that the attack Target is of type player or the attacker is of type enemy and this is going to be very important in a second but first how should the actors communicate with this mediator now when the attacker joins the fight they will ask the mediator if they can attack then the mediator will check how many attackers are already attacking the Target and then tell the attacker either yes you can attack or no you should wait this means that the attacker should expose an attack function and a weight function to the mediator but leave the implementation up to the attacker themselves and this is exactly what interfaces are for so let's create an interface for the attacker and I'll show you how it's used so in our combat manager folder let's create a new interface and call it BPI attacker this will contain the definition of all the functions the mediator needs from the attacker so first an attack function that takes as input the attack tar Target as an actor second is a weight function that also takes his input an attack Target and lastly a retreat function this will be called when the attack Target dies to tell the attacker they can now Retreat great now these are all the functions the mediator will use to communicate with the attacker now let's create the interface for the attack Target let's call it BPI attack Target and the mediator needs to know only one thing from the attack Target Target and that's how many attackers can attack you at the same time so we'll create a function called get Max attackers count and this returns an integer okay now before we Implement these functions let's see how the combat manager will use them so back in our combat manager on begin play Let's get the max attacker count from our attack Target and promote that to a variable next we said we need a way for the attacker to ask the mediator if they can attack so let's create the function for that and call it handle attack request now this function takes as input an actor reference of the attacker make sure the attacker is also not the attack Target then check if the number of current attackers is less than the maximum defined by the attack Target if true then add new attacker to the array of attackers and if false will add them to the array of waiting attackers but we still need to actually tell them to attack or wait so we'll call the attack function on the true branch and call the weight function on the false Branch but notice that I didn't have to cast from actor to BP enemy because attack and weight are interface functions that can be called on any object and won't do anything if the object doesn't implement the interface now this function is ready to use all we need to do is Implement our interfaces in the enemy and player and call this function so starting with the player let's go to the class settings and under interfaces We'll add the BPI attack Target interface and this will give us uh a function here to implement on the left so let's double click it and just add a static number here like three attackers and that will be our Max then we go to our enemy and same thing class settings add interface BPI attack Target then we implement the three attacker functions it's good to mention here that I have a very simple Behavior tree for my enemy with four states a passive weight attack and dead State and these states are all controlled by a state variable uh in the AI controller and this is what we're going to be doing when we tell the enemy to attack or Retreat or wait we'll just be changing this uh variable through a function in our AI controller so back in our enemy let's doubleclick all of our interface functions and for each one we will just get our AI controller and switch the state to the correct one and the final step is to actually call the handle request attack function from our combat combat manager in our enemy and back in our combat manager we need to tell it who the attack Target is so we're going to make this attack Target variable instance editable and also in the begin play check if it is not valid so if we didn't set an attack Target from the level we just default to the player and now let's test it all out all right so right now only three are attack attacking me and the rests are waiting nicely done and if I go to my character and change this three to something like five now we have five attacking me and only one waiting perfect now let's handle the case that when our attack Target dies all of the attackers should go back to their uh original spawn location to do that let's go back to our combat manager and add a function called handle death now this function will take as input the attacker that uh sorry the actor that is calling it and it will check is this actor the attack Target and if so then we want to tell all the attackers and waiting attackers to retreat we'll do that by combining both of the attackers and waiting attackers array using an append function then Loop over the combined result and call Retreat on each actor which is an interface function then when the loop is complete we empty both the attacker and waiting attacker's array so that we start the encounter uh from scratch the next time now all we need to do is call this function from our player when we die so in our player class let's get a reference to our combat manager and promote that to a variable and down here I fake death when I press the Z key so after that will'll just add a call to handle death function from our combat manager all right now let's test it out so if I press Z I'm going to die nice and they all go back to their original location awesome now the last case we need to handle is when an attacker dies we want one of the waiting attackers to take their place cuz right now they just stand and wait all right to do that we need a function in our combat manager let's call it engage waiting attacker now this function gets the first waiting attacker in the array make sure that it's valid because maybe there are no attackers waiting and if it is valid then add them to the attackers array and remove them from the waiting attacker array and finally call their attack function now we just need to call this function when the attacker dies so we'll handle that in the handle death function so off of the false Branch so if it's not the attack Target we check now is this actor in our array of attackers is it one of the attackers so if it's true that means an attacker died so we remove them from the array of attackers and call engage waiting attacker function finally in the false Branch let's handle the case that when a waiting attacker dies so then we just remove them from the array of waiting attackers and that's it and now finally in our enemy when the enemy dies all we need to do is get our combat manager and call the handle death and pass our self as a reference all right now let's test everything out in action so we have three attackers attacking me I kill one of them awaiting attacker takes their place and so on now all waiting attackers are attacking me and if I die they all go back awesome now I can even make one of these enemies or really any actor the attack Target so just by clicking your combat manager in the level and select the attack Target here let's say to be this guy now if I play you'll see that they're all waiting and none of them are attacking because this attacker doesn't or this enemy doesn't implement the attacker uh the attack Target interface so that means that the max number of attackers is defaulting to zero so if we go to our enemy we say we want our enemy to also be an attack Target so they can either attack or be attacked so we just tell them to implement the attack Target interface and the max number of attackers we can make it a variable Max attackers and we can make that even Exposed on spawn so we say this enemy has a max number of attackers of three but this enemy has a max number of attackers of five so now in my combat manager let me select this enemy to be the attack Target and we have three attackers attacking him now but if we select this guy who I said should have five so combat manager pink guy has five so you'll see that five are attacking him nice and if I kill him they all go back because their attack Target just died I can even make this poor Cube the attack Target now our combat manager is complete let's jump into our Second Use case which combines the mediator pattern with the Observer pattern from last tutorial to create an event manager in the previous video we wanted our enemy count in the widget to update when an enemy dies and we wanted the door to open when all enemies are dead so following the Observer pattern we dispatched or published an event when the enemy died and we bind or subscribe to this event in the widget and the level now this solution prevented the enemy from communicating directly with the widget or the door but there's still a problem we still need a reference to the enemy to bind to their on death event in both the widget and the door this means that in any part of your game if you want to know when an enemy dies you first have to get a reference to all your enemies in your level which is not ideal because as a subscriber to an event I don't care who's publishing it I just want to be notified when it happens and this is where the mediator pattern comes in instead of having the widget or the level bind directly to the event from the enemy we'll have an event manager which will be our mediator handle all the published calls and the subscribed calls now to create the event manager we will first need to create a custom game State object because the game state is an object that is globally accessible and is replicated on both server and the client so it will be the perfect place to hold the implementation of our event manager now this game State object will be our event manager so it will be responsible for dispatching all of the events in our game so first we need to go to our enemy and delete our previous uh on death event dispatcher and go back to our game State and add a new event dispatcher here we'll call it on enemy death then we go back to our enemy and now instead we will call the dispatcher from our custom game State object and I will also show you later on how you can remove the need to cast to the game State now let's go to our widget and get a reference to our game State we don't need to get a reference to the enemies anymore because we can now bind to the event being dispatched from the event manager and we'll go to our level blueprint and do the same thing excellent so now when all the enemies are dead the door should open widget is updating correctly and the door is open Perfect all right so now we still want to get rid of this initial uh reference where Loop to get all of the enemies to get their initial count because we get their initial count and start subtracting it when it reaches zero we open the door and also in the widget we do the same we get the initial enemies count so that we show remaining enemies six and then we start subtracting so how do we get rid of this using the same pattern and avoid having a hard reference to our enemy well what we can do is over here in our game state or our event manager we can add another event and call it on enemy spawned and now we can dispatch this event in our enemy whenever we spawn and then count all the enemies spawned instead of just start with a default number so in our enemy here uh after the begin play we're just going to get our game State and call enemy spawned now be careful when uh dispatching events in the begin play the actors that are subscribing to the event might not have initialized yet so if you want to dispatch an event and begin play always good to add a uh delay until next tick to ensure that all of the actors are uh initialized so back in our widget right now we Loop over all of the enemies to get their length and that will be our initial enemies count now we don't want to do this anymore I'm going to delete this instead we want to get our game state which I should probably make a variable and we'll just call it my my game State and use it here as well so now uh I can bind to the on enemy spawn event let me move that a bit and what I can do is I can increase the enemy count on spawned so I have this enemy count widget there already integer that starts at zero now anytime the enemy spawns I can just get this number and say increment which means add one to it oh let's move this a bit here and after adding one we need to update the text with our new enemy count all right so let's test this out so I play and it starts with enemies remaining six and as I kill them it updates correctly now I can even spawn new enemies when I press X here and enemies spawn and you can see that the widget count is updating now enemies are 11 perfect now we need to do the same thing for the door because the door still starts with six enemies and it opens when six enemies are dead so we're going to do this remove the enemies count uh the length and loop here as well and we are going to get our game States uh let's also promote it to a variable here call it my game State and we are going to bind to the enemy spawned and do an increment enemy count now I know I'm duplicating this part here a lot of people have commented why are you duplicating you already have it in the widget well this is to have the widget and the door independent if I reuse it I'm coupling the door and the widget and this is not something that we want to do it's actually what we're trying to avoid so this part is duplicated and I'm going to increment the enemy count again all right so now we start at six let me Spawn some more we have eight I'm going to kill them perfect door still hasn't opened door still hasn't opened and only now does the door open excellent so now what we've done is we combined The Observer pattern with the mediator pattern to create an event manager in our game State and this removed all references from our widget and our um uh and our level blueprint to our enemy so now we no longer have any references to the enemy we just have a single dependency which is our game State now I can quickly show you something that uh you might want to do as well which is create a interface so if you search for inter interface to create some an interface a blueprint interface for your um event manager so I'm going to call this event manager now right now I'm just dispatching the events here and that means that the uh the enemy needs to know that there exists an event called on enemy death and we actually need to cast our game state to my game State before being able to use it if you want to avoid doing this cast uh an interface will help you with that so if I go to my interface and I Define a function called publish uh enemy death and we have another one called publish enemy spawned let's compile that so now in our enemy I don't need to cast anymore off of the game State itself uh I can show you here so instead of using my game state I'm just going to use the regular get game State and say uh publish enemy death so now I'm calling this function on the interface and I actually don't need this anymore don't need this and I can just say here get my game State and publish enemy spawned now the only downside of this is that in your event manager uh in your game State first of all we have to impl ment this interface so go to class settings under uh implemented interface search for your event manager and when you compile you'll see that you have publish enemy spawned and publish enemy death now you have to implement these and the implementation is basically calling their dispatcher so publish enemy death will call on enemy death and publish enemy spawned will call on enemy spawned so that's the only downside is you'll have to sort of do this overhead part where the event dispatches uh the event dispatcher but it makes your publisher um cleaner for the subscriber you still have to uh cast to my game State uh because you need a reference to the actual published event and you can't add that to an interface in blueprints at least great now let's quickly summarize everything we've done the mediator pattern can be used to reduce or completely remove chaotic dependencies between different actors or objects in your game we showcase that by creating an event manager in our game state that allows any actor to subscribe to an event without referencing the publisher of that event we also created a combat manager as a mediator between multiple enemies and their attack Target to coordinate their attacks and communicate with each other that way all the logic of handling coordination of the attack is taken care of by the mediator and the attack targets can just worry about implementing their attack functions their weight functions and just the implementation that is only relevant to them now this gives us a Loosely coupled architecture with excellent separation of concerns that makes our game logic much easier to scale and reuse I demonstrate that by changing the attack Target to be one of the enemies or even a cube and everything still worked fine now if you're still watching and you're still wondering why do all of this what's the real benefit of implementing proper software design patterns let me leave you with this contrary to what Hollywood movies will have you believe programmers don't spend the majority of their time furiously typing on a keyboard actually as a programmer the majority of your time is spent reading and reasoning about your code and no matter how smart you are your brain can only fit a certain amount of information at the same time that's why skilled programmers are those that can write software that is easy to read and reason about making them much faster more efficient able to process more information and build much scalable products knowing design patterns and how to use them is how you'll be able to do that as well now we've reached the end of the video there will be more design patterns coming soon more Unreal Engine tutorials just a lot more stuff here on the channel so if you're interested and you found this content useful please consider giving a like And subscribe and as always thank you for watching and I'll see you in the next one [Music] a
Info
Channel: Ali Elzoheiry
Views: 12,188
Rating: undefined out of 5
Keywords: Beginner mistake, Beginner tutorial, Beginner tutorial unreal engine, Design patterns in unreal, Observer pattern, Software design patterns, Ue5, clean code, common software design patterns, design pattern, design patterns, game dev, game dev motivation, game dev tips, game dev tips and tricks, game developer, game development, indie game dev, indie game development, ue5, unreal, unreal engine, unreal engine 4, unreal engine 5, mediator pattern, mediator design pattern
Id: y4fE2JdFdvY
Channel Id: undefined
Length: 26min 39sec (1599 seconds)
Published: Mon Mar 04 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.