Building Unity UI that scales for a real game - Prefabs/Scenes?

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey what's up I'm Jason and today we're gonna talk about an issue that I think most unity developers run into at some point in their career it's something that I struggled with and I get questions about all the time and I just want you to listen to the scenario see if it matches with what you've run into and if so then follow along in the video and I'm gonna show you some really good techniques and tricks to simplify things and if not then just hit the watch later button and save that off hit the like button and then come back to it when it actually makes sense to you when you've run into the problem so here's the scenario you've started building a game and everything's going fine right you're getting your level set up you've got one level you've got a UI that maybe control some things in the level or lets you select different stuff or maybe shows just data about your objects in the level whatever it is right maybe it's just showing your score or something else about your NPC's so you build it up everything looks good you're ready to move on to level 2 and suddenly you go oh wait a minute what do I do about my interface because right now my interface is all in one level so I've got this one scene that's got a user interface it's got all of my objects in it and I need to make this user interface reusable the easiest thing to do and what I think a lot of people do to start is just copy that UI stuff right over to the other level with unity now you can open up multiple levels you can literally just duplicate it and copy it over or another option that people will sometimes move to you is making their user interface into a prefab but then they suddenly find that references aren't working right objects aren't hooking up right and the the entire process starts to fall apart so they end up having to constantly modify the user interface in multiple levels or go back and rework things every time they want to make a small change if you have one or two levels it's probably not a big deal once you start building up level after level after level it becomes a nightmare and that's what I'm gonna show you how to avoid I'm going to show you how to set up a user interface that works across any number of levels how to load multiple levels kind of switch back and forth between them and have the interface bind up to whatever you have active at the time and make it so that it's easy to manage easy to extend and clean to use before we get started though I wanted to briefly show off the different assets that I'm gonna use in this project and remind you that you can go download the code for it below also don't forget to hit the like button the subscribe button and a little notify Bell all that other stuff really helps or even better just share the video anywhere you like whatever places you like to share things just go share it there and it helps more than anything else you can do before we get into solutions I want to show the problem a little bit better so here I've gotten my level set up and this is called help I'm stuck because this is pretty much the feeling that I had when I ran into this scenario the first time and ended I've got two different entities I've got this skeleton king and a micro dragon and if you look they both have a stats component on them and they have an entity component on them we'll talk a lot more about this as we go further we're gonna start with the entity doing some health stuff and then dive all the way into managing NPC stats showing all that on screen and it's gonna get lots of fun and I get her be lots of fun and be pretty interesting I think so we've got these two things in here and then I've got a UI system UI system original which has a UI controller script on it and then it's got a reference to a stats list and a health panel that are children of it this is how a lot of UI is that I see end up being built so I've got a stats panel underneath it let's look at the health canvas though instead or the health panel we've got a health panel here and then this is going to update when we take some damage so let's play see what that looks like and then start worrying about what happens when we want to add a new level so I click on my NPC and my UI all shows up if I hit the number two on my keyboard he takes some damage and if I select this other guy you can see he's back at 100% and this guy is at 60% but again I want this to work for level two will help I'm stuck too or help I'm stuck three and I don't want to have to recreate this every single time I don't want to have to drop in this UI system into every single level that I have and if I again if I make changes I don't want to have to redo those changes so what's the solution that you can think of that we could do for this again one default and kind of obvious one is to just make this into a prefab and then reuse that prefab across multiple levels and that works relatively well except you have to go in and remember to set the prefab up in there then you also have to check for any prefab overrides and if people are working in the scene sometimes they'll make changes to that prefab and then break things and you end up with quite a few issues so there's another solution the solution that I like to use is to put my UI into a completely separate scene so I'm gonna go into my scenes folder now and just open up level 1 I'm not gonna as always save it why not so in level 1 you'll see that I've got my main camera I've got the environment here which is pretty much exactly the same it's just that background from this kit that I showed earlier and then we've got the two entities in them in here oh we actually have this extra stat adder script in here this is just for debugging I'm gonna actually remove it but we've got an entity and a stats on each one of these and then our main camera has this little UI loader script I'm gonna open up the UI loader script show what it does and then we're gonna show it in action so the UI loader script is very simple one just for me to be able to load and unload you eyes at runtime in a demonstration kind of scenario and also be able to switch between different levels the main part of the code is right here and it's very very simple and straightforward so what we're gonna do I'm gonna go in and press f1 and that's gonna look to see if we have a scene named UI that's loaded if that's false so if we don't have one we're going to load the UI scene additively if we do have one loaded we're just going to unload it all the rest of this code is just for loading and unloading different levels we'll dive into that later for now it's just f1 to toggle the UI on and off so I'm gonna go in here hit play once we get to that game view look if I click on these NPC's nothing happens but if I hit f1 suddenly my UI appears let me do that again without being in maximized mode though so I've got in game view I want to turn off maximize on play I'll just put the game view side by side with the scene view and just watch what happens in the hierarchy so I'll hit play again everything loads up there's no UI I can go select things I'm not gonna select anything this time instead I'm just gonna hit up one and load the UI notice that nothing showed up because I don't have anything selected so there's nothing to show health for yet if I go select an NPC there we go I've selected the dragon you'll see that his health shows up and some of the stats show up and if I hit to his health goes down every time and I can go select this other guy and his health is back at 100 but I'll hit 2 he goes down to 90 go back to that dragon who's at 50 and you can see that it's binding up and showing me the stats and the health for the different entity that I had even though the scene or the UI part wasn't loaded at the time it's getting loaded later and all bound up so let's take a peek at what that actually looks like so I've stopped playing and it's time for us to take a look at some of the code let's check out the click select controller we've already looked at that UI loader and this click select controller sitting on the main camera as well it also has a reference to our main camera we'll talk about that in a moment though let's open up the script and see what it's got so there are a couple things going on here first is this reference to that camera or to the main camera it's just a serialized field to give us a permanent reference to our camera so that we can do a ray cast into the scene we could alternatively use something like camera dot main but I always recommend avoiding using the camera by that camera dot main tag because there are some performance issues with it and just other problems that I have with tags I don't really like to use them or recommend them so instead we have a serialized field of this private camera so we can assign it in the editor and use it for our ray cast which you'll see very shortly I guess that's write down we are on line 15 we'll talk about that when we get there though there are a couple things going on here let's zoom in a little bit after the camera we have a public static event if you're not used to events don't worry you're gonna get used to them as we go through this video because they're a core part of any data mind binding and UI work so we've got an static event action and it's of type entity what this means is that we have event that will fire off and things can listen to it and whenever they listen to it whenever we say hey this thing has happened which is on select identity change so whenever we tell something that the Select identity has changed we're going to pass in an entity as a parameter so we're gonna give them the entity that it actually changed to now this doesn't necessarily mean that we had to give them the right entity we could do it wrong and pass in any entity just means that an entity will be the parameter of course if we're doing things right and we're not trying to confuse ourselves we'll probably pass in the selected entity the other thing that we have here is a public static entity field named select identity with a private setter the reason for this is that we don't want other code changing our select identity we always want to fire off this selected entity changed event whenever we switch entities and we don't again we don't want anything else changing that up so we'll be able to set the entity tell everything that cares about our entity changing or our selected entity changing that we've changed it and then anything that wants to just see what entity we have selected can also read it right here from this property let's take a look at update now and see how that actually works how we assign these entities how we do that callback and then how it's all registered and hooked up to the UI afterward so the first thing what we're doing is in our update method just looking to see if we've pressed the fire one button which is just a default for left click so I'm really just checking to see if I've left clicked on something then we do a screen point to recall on our camera what this is doing when we pass in input Mouse position it's taking our camera and it's creating a ray from the point that we've clicked into the world so whatever we're clicking on basically whatever we see underneath our mouse cursor is where our Ray is going to shoot and the Ray is just going to tell us whether or not we've collided with an entity that's really what we're looking for realistically it tells us if we've collided with anything but we're gonna ignore anything that's not an entity so we get our Ray here which is just a type Ray I have far here it could be ray ray or VAR ray either way we're getting a ray and then on the next line line 16 we're just doing a debug draw ray where we give it the origin we give it the direction and a distance if you don't give it a distance or multiply it by a number here our ray is only gonna be one meter long coming out of the camera it's gonna be a tiny short little line then we give it a color and a duration let's go see what that looks like real quick so if I go back in here and hit play if you watch in the scene view when I click around you see that red debug ray appearing and if I click on this entity right here see that it's going right through them and if I click on this guy you can kind of see if I get a little bit closer you can see that it's going right through this little dragon so that's what we're using to do selection checks so we we draw that ray just so we can see it as a debugger and then we do a ray cast so we say if physics dot ray cast and we pass in our Ray and then we give it an output parameter of hit info which is a ray cast hit which is just gonna have information about the thing that our ray cast hit or the first thing that our ray cast hit so if that hits anything at all this part will return true if this doesn't hit anything it's gonna return false and none of this code in here is gonna run or this code right here it's gonna run but if it does hit something the hit info will get filled out it'll have an object in there and then this code in here is gonna run the next thing that we do is we check this hit info we look at the collider on there so if we go back here and look at our dragon or something it's got a capsule Collider so we get the collider from the hit info and then we say get component of type entity so we're gonna look to see if the thing that we had a Collider on that we clicked on has an entity and we do that right here on line 19 and we just cache that in this entity variable then we set selected entity to that entity and fire off this event so a couple interesting things can happen here one is that we could click on something that has a Collider and no entity so I clicked on a building or the ground it doesn't have an entity selected entity is going to be set to null and our on selected entity changed is going to be set to no or it's gonna get fired off with a no let's give it a try real quick so what I'm gonna do now is do by attaching I'm using writer but you can do this just fine in Visual Studio as well so I've added a breakpoint here just by clicking turning it on putting a little red dot there and then I hit f5 and what we're gonna do is run through this I'm gonna click and then the debugger is gonna break right here it's going to show us the entity that we've selected or the non entity that we've selected and just kind of let us follow the flow of things so here we go I'll go click on somewhere in the background and oh I didn't hit anything I haven't actually hit a Collider there's no colliders on any of these things let me click on the dragon now that I've clicked on the dragon we actually have a Collider here and if I put my mouse over it you see that it's the capsule Collider on that micro dragon our entity was found and selected entity was set or is about to be set to this entity so if I hit f10 it's gonna run that line of code selected entity got set and now our on selected entity changed will get invoked but if you look at it when I put my mouse over it it's no there's nothing for it to call nothing has registered for this on selected entity change so if I hit f11 which would normally step into the code and run whatever this is calling it just doesn't do anything because there's nothing nothing registered at all and look down here I also have this little section where if I hit escape it sets my selected entity to no and calls the on selected entity changed with no so that's allowing me to unselect something let's try that I'm gonna add a breakpoint there I hit f5 which is gonna let it resume playing and then I'll go back into the editor and I'm just gonna hit escape now you see it selected entity is set to a value it's gonna get set to no in our on selected entity change to still null so when I hit f11 nothing really happens it just kind of skips over it and the reason that it skips over it by the way is this little operator here this question mark dot it's the oh I always forget the name of it now but it's the little Elvis operator what it does is it checks to see if this is null and if it is no it doesn't run the code after the question mark if it's not null then it will run the code after the question mark so another way that you could write this let's just stop debugging real quick is if on selected entity let's see if I can copy and paste that better if that is not equal to no then you could do that so this is just an alternative way to do it oh no propagation that's the term for it so here I just go and hit alt enter and writer just kind of automatically cleans that up turns it into a one-liner that if I understand what that question mark means makes sense and it's a little bit easier to read takes up a little bit less space okay so this is what it does without a UI if there's no UI there's really not much happening we select an entity but we don't do anything UI related we don't bind anything up we don't show a health bar and if I take damage well take damage is gonna fire off it might take some damage but I'm never gonna know it because I can't see it on my entity so how can we fix this what can we do well let's load in our UI so I'll hit f1 remember that's pulling in the UI from that loader script I'm gonna go right back to it just for one second the UI loader script we're just looking to see if we have that UIC and loaded if not we're loading it so that's what I've done here is load this UI scene and there's an important set of scripts or an important script right here on this UI controller object it's the same one that I had in the single version but it's really set up so that it can run against multiple levels so if we look at it it just has a reference to two other panels or scripts underneath it and some code in this UI controller let's open it up so let's zoom or let's zoom out real quick give you a real brief look at everything that's in there and then we'll go through and talk about the different parts so at the beginning up top we have a reference to us that's list that's that object that you were seeing that had all of the stats on there with the little buttons to add and then we have another reference to a health panel the important part though is in our awake so for our awake method we're calling click select controller dot on selected entity changed and this is the important part plus equals click selected controller click select controller on selected entity changed so we could even let's let's rename this I'm going to change this to be handle selected entity changed because I think that giving it a shorter name might make it a little bit easier to read and that's just control RR or f2 to do a rename in write or Visual Studio so just massive for batch rename them and I think it looks a little bit easier to read so what we're doing here let's even split that is registering for this on select identity changed event so if I go back to it and here we go you see that remember it's an event and events can be registered for by any number of things that's why we don't do an equals this we do plus equals we're adding it on to the list of callbacks to fire off whenever on select identity changed is invoked so when this is invoked right here what we're saying is call our handle select identity changed method and notice that our handle select identity changed method has an entity as a parameter remember I mentioned go back to it that we're gonna be passing in an entity here because it's an action of type entity that's why we need to have the entity as a parameter on the method that we're calling because it's gonna get passed in by the way it is worth noting you can put commas here and have multiple parameters you don't have to have a single parameter we're just using one right here because it makes sense so we can have multiple entities integer strings whatever type of object you want to pass around you just have to make sure that you handle it when you're calling it with the invoke and passing it in and that you're handling it on the other side and otherwise you're not gonna be able to compile okay so let's take another peek of this we register for on select identity changed here that plus equals means we register for don't worry about the - to talk about that in a moment and then when we call this invoke our handle select identity changed method is gonna get called it's going to pass in the entity and we're gonna skip past the stats part first because it's a little bit more complicated just talk about what happens with the health panel first so if the health panel is not null which we know it's not know because we've assigned it here but maybe somebody it's working on the scene and they've added a new object or they want to disable the health panel they deleted it whatever we don't want the health panel show up it could be no so we want to make sure that we check for it if the health panel is not null though then we call this method named bind and pass in the entity let's talk really briefly about binding though and that means so what we're doing here is setting up some simple data binding and if you look at the definition of data binding on Wikipedia it says that it's a general technique that binds data sources from a provider and consumer and synchronizes them I don't know that that really simplifies it but the idea is that we give an object like our UI element a thing that it needs to hook up to and then it does all of the work in that binding to manage updating itself when something has changed now this could be done a bunch of different ways we're gonna use events and we're gonna dive right back into that but I wanted to just point out that data binding is a very common thing it's something that you'll see across all different types of programming and pretty much all different projects everybody does it in slightly different ways although in some systems that are very well defined practices for how you do data binding in unity not so much but ours is going to be relatively simple and I found that most of the time when you want to bind things in unity to your UI there's not much more that you have to do beyond what we're gonna be doing right now so let's jump back into it so we've got our bind method let's open up that code again and let's go take a look inside this bind method of the health panel and see what it does and also check out the health panel so our health panel has a couple things on it it's got a health bar a health text and a panel root serialized field then it has a private entity that is the bound entity let's zoom that in a bit and we're going to skip past the ondestroy for just a moment we'll go back to D registering and what this minus equals means shortly after we go through the actual registration process let's look at the bind method here so our bind method does a couple things first we check to see if we're already bound to something and if we are we do this D registering again talk about that in a moment but next thing we do is set our bound entity to the entity so imagine the first time we call this our bound entity is empty or null and we're saying hey bound entity you are now the entity that got passed in so when our UI controller has the handle selected entity changed called so we've clicked on something it invoked it came all the way through here and we say hey health panel bind to that select identity then we say if the bound entity is not Noah's remember we could say bind to null or no entity so if it's not null we turn the panel root on we register for another event on this bound entity this on health changed event and then we call handle health change so we're doing two different things here and we did this in the UI controller let's go back to that for a moment so in the UI controller in awake not only do we register for handle selected entity changed we actually call it right after we register for it the reason for this and the reason that we're doing it in the health panel as well is that when we first call this awake our UI or our level that are with our click select controller could already have a selected entity so the entity has been selected they already clicked on it they did whatever they needed to do it which in our case is really just clicking on it and then they loaded the UI and what we want to happen is for our UI to still update and bind so when it awakes we just say hey assume that or pretend that somebody just clicked on whatever the selected entity is and handle it just like you would in that case so we want to be able to make sure that we're setting up ourselves by default when we when we initialize and I do that either right before or right after registering for the event let's go back into that bind of the health panel though in the health panels bind we're doing exactly that we're saying hey on health changed call this handle health changed event so whenever our bound entity that's only the one that we've selected so it'd be like the dragon or the skeleton guy right now whenever that things health changes call handle health changed also call it right after and we'll pass in the health amount because handle health changed look at that it takes an integer for the health amount and if we look at this event on Hell on health change I'm gonna hit f12 and go to it you see that it actually has a parameter type of int and that's the amount of health that we're having order that we have after we take damage in fact if you look at it right here on line 13 of our take damage method which is how we're taking damage we're actually just calling on health change down in Boca and passing in our health amount so let's go back and take a look at handle health change handle health change doesn't do much it does the normal UI stuff so this is where we get into doing what a UI would normally do without all of the binding right so we we could have like on our bound entity go find the health panel or have a reference to the health panel somewhere and go say hey change over to you know set your health to this but then we also have to manage which one of these things is supposed to do that if we had that on every entity like hey whenever our health changes go find the health UI and then set it then we'd also have to do some determination of are we the right thing are we the thing that's supposed to be updating that UI element and that's kind of what we're getting here with the bound entity set up we are binding to a very specific one so that whenever it's health changes we can update the UI and our entity doesn't even need to know that there's a UI around our entity is fine if we add in a UI later on in the game we don't have any UI or we strip it out or we add in three different ways to show health in different spots or whatever it is our bound entity or our entity itself doesn't have to change so back into our handle health changed I got a little excited there we set the text for the amount of health and then we set the fill amount on our fill bar let's go check that out let's see what it looks like I want to run through the process and then I want to actually step through it so you can see the full flow of how this is all working so I go select the entity there and let's do it with the no UI on I select the dragon and then I turn a UI on again our speed shows up our damage shows up we're kind of ignoring that but our health shows up as one hundred ninety eighty let's get him down to 70 hit escape reselect them hit escape let's unload the UI reload the UI select them now I'm going to unload the UI and we're gonna add in a breakpoint so we're gonna go to our UI controller or go right up to the top I'm going to put a breakpoint in here and hit f5 give it just a moment to attach and then I'm gonna turn on the UI and we're gonna step through and see what happens now I'm gonna make sure I have an entity selected first I'll just click on this guy yep hit my breakpoint to select the king the skeleton king then I'll go back in and I'm going to enable my UI hit f1 and here we go so the first thing I want to know is that our on select identity changed event is no nothing is registered for it yet but if I hit f10 we should see that it now has a system that action of type entity and if I expand it out I can even see the method is that handle selected entity changed so that's now registered as a callback whenever we call on selected entity changed our handle selected entity change method should get called but we're also calling it again right after the registration in our awake and our selected entity is that skeleton king so if I step in hit f11 I can step over these staffs list parts for now it's again it's a little bit more complicated so just hit f10 we'll go into the health panel window so I'm gonna hit F Ken on this check then f11 to go into the binding first thing I noticed that my bound entity for my health panel is no I don't have anything bound to it yet which makes sense I just loaded this up and then we'll see that we're setting our binding on line 24 so I hit f10 and now our bound entity is valid now we check that it's not no we know it's not no because we just kind of passed it in but I'm gonna step into it we'll turn on the panel route which let's go take a peek at that in just a moment but it's really turning on the panel that our health interface is on and then we set up a callback for on health changed to call our handle health changed method which is right down here let's go put a breakpoint there I'm gonna put a breakpoint right here on line 39 so we can play with that a little bit then we'll hit f10 and I'm in hit f10 and watch it's gonna stop at my breakpoint cuz it went into handle health changed and we're gonna set the health to our current amount which is just that health is a hundred so it's gonna set everything to 100 and we're good by the way our fill percent right now is kind of set to capper or be exactly around 100 health we have to convert it to a percent if we want to do a different amount but I just stuck with 100 because I thought it was pretty easy to use okay so there we go we called that handle health change we step out step out and step out and we're kind of at the end of it that's the end of our awake lifecycle and if we go look in here we've got our health showing up now remember if I hit to our entity is gonna take some damage so let's see what it looks like when he takes damage let's go into our entity I'm gonna add a breakpoint and to take damage method and then I'm gonna go in here and hit - I didn't select anything notice I just kind of clicked off in the air but I hit - and our health is going to change by an amount which is just 10 and that's just because if I go up to call stack here this is the call stack window and writer by the way see that our key code - if I hit alpha - it just calls selected enter view that take damage and passes in an amount of 10 assuming that we have a selected entity okay so we reduce our health by 10 goes down to 90 nothing's happened yet our on health changed event however has something registered for it so if I hit f11 you'll see that it's calling into our handle health changed and if you look here then this is why because you told it whenever health changes call our handle health changed and here we passed in the 90 so now it's gonna know how you go set the health to 90 okay that's pretty interesting and pretty simple but what happens if we switch npcs so i go select this dragon how is it all changing so I've clicked on him let's take a peek so here we do that ray cast the same thing we did before for our client or click select controller if I can remember the name of that we found an entity and that entity is the micro dragon guy so we set our selected entity and then on selected entity changed it's not know anymore so if I hit f11 I can step into it and see what it's doing oh it's calling our handle selected entity changed because we registered for it in a wake and then we step over step past the stats thing again for a moment go into the health panel thing it's the same exact health panel but now we're telling it to bind to this new entity so let's step into that and see what it looks like so I hit f11 and then I hit f10 and now our bound entity is not null so we're binding to an entity but it's gonna be a different entity so I'm gonna hit f10 again and talk a little bit about what's going on here why this exists and what this is for if I register for an event when I no longer want that thing to fire off or that thing to be true so that I don't want the event to call this code anymore I have to deregister it and we do that with the - to remove an event registration if I didn't do this then whenever the health of the skeleton King changed we would still be calling a handle health changed so right now it's not it's kind of hard to reproduce because if I hit two it just kill it hurts my selected thing but imagine these things are just taking damage all over the place from AES and fighting each other and stuff when I've changed the selection I don't want to keep calling handle health changed on because the old thing that I had selected has its health change right I only want it to work for the currently bound entity not the previously bound ones so the minus equals is very important to prevent that from getting called again it's also very possible to accidentally register the event multiple times and not read the register and ever like if if we bound it every time in on and Abel or you can kind of see here we do an own on destroy if we just kept turning the thing on and off it'd be very easy to leave these event registrations around so let's talk about that for a moment too so on destroy of our health panel why is it doing this why is it D registering so imagine our UI elements go away like we were in there and we've unloaded our UI but we still have this bound entity set up on our health panel what's gonna happen well our bound entity is gonna go away because it's no longer bound but the on health changed event registration that we have that is not going away so our entity like the skeleton or the dragon that's sitting there it's still gonna try to call this handle health changed method except it's gonna be doing it on a health panel that doesn't exist that's been destroyed so when we destroy the object that the thing or the callback is going to which is our handle health changed on this health panel we need to deregister anything that's calling into it in this case it's just removing that on health changed part so we D register it there and we also do register it whenever we change the entity basically whenever an entity becomes unbound we want to deregister it and you see that we do that also up a level in our UI controller okay so let's step through some more we're setting we're removing the health changed event registration so if I hit f10 again you see that's gone back to null now on our bound entity and our new entity is gonna become our bound in B which still doesn't have a health change registration then we'll say hey bound entity's not null so set the panel to active register for health change like it's registered again and then handle health changed in and remember if I hit f10 I hit this next breakpoint and we go in and we see the health is a hundred for this guy and the fill amounts gonna be out a hundred percent now I want you to remember if you're not super comfortable with events or you just feel a little bit confused don't worry don't get too overwhelmed events are very important part of c-sharp programming in general and just I'd say in general game development or any programming really and there's something that you need to master but they take a little bit of time to kind of get used to and get I guess really familiar with especially the whole concept of having to unregister them so if you're not sure just remember that whenever you register for an event you almost always want to unregister for it somewhere and you want to avoid situations where you register for an event multiple times for the same thing like I wouldn't want to accidentally call this twice let's let's say I did let's say I copied this and I pasted it and I called it twice what's actually gonna happen here is whenever the health changes it's gonna call handle health changed two times in this case it probably wouldn't matter you meant it might not even be able to notice but if we were doing something other than just updating a UI element we would almost definitely notice for writing something out into a log we'd have multiple log entries if we're saving some data off we'd be taking twice as long to save the data or send the data or whatever it is so we don't want to double register for events and we want to make sure that we definitely clean up our event registrations with a minus equals let's go take a quick peek at that UI controller as well because here on the in the ondestroy whoops what have I done let's go back you see that I do the same thing where we unregister for the event in ondestroy and this is a pretty common practice to just in ondestroy unregister for anything that i've registered for in start or away just to make sure that it's cleaned up and it's all gone so let's talk about some more advanced scenarios now like let's say we want to load up another level is this gonna work how is this all going to work is everything going to break let's try it out so I told you before in our UI loader script let's open it up we have an option here to load up a different level so f3 will find a scene 2 or level 2 and unload it and load level 1 let's look at f4 because that's the one that loads level 2 so and if I hit f4 we find level 1 and we just say hey unloaded if it exists and then we say hey if we don't have level 2 loaded add it so load it additively so this is that scene manager low-level async loaded additively and then when it completes this is kind of an important weird that'll callback here we're actually registering for an event so if you look at the tooltip you can probably see the little tiny word event there but what we're doing is we're registering for the completed event on load scene acing so whenever this is finished it's gonna fire off the completed event and it's gonna run our code afterwards now there is another way to do this right now we're using a lambda expression if I cut this I could say handle level to load completed and I could hit alt enter and create a method for it and see it's gonna give me this asynchronous operation or async operation I could paste in my code right here let's just delete that all down get it nice and small and we could do it just like this so here let's move I'm gonna put this on to the next line just know that this is just so that it fits all on the screen so I moved the dot completed down it'll work fine either way but what's happening here is that we're just calling this this method now the alternative way or the way that it was written is just doing an event callback and shoving it all into a little delegate that makes it so I don't have to make another method here and you can see that right here on this line 23 and 24 we just say hey give me the operation which is that parameter from the event so remember if I look mouse over it it says it's an action of async operation operation is that parameter just that same parameter that's getting put into here but here we can do it in a lambda statement and we don't even use it so you notice there's no references to it you just call scene manage or not set active scene the reason that I'm doing this by the way is just to set whatever seeing that we've loaded to be the active scene so that the lighting works all right so I've reattached and let's just load the levels back and forth so I'm gonna load it into level one load into level two and I'll click again and let's just watch what happened again so the reason this is still working and the reason that our UI still hooks up and just kind of gives us access to the selected entities is again that our click selective controller on selected entity changed event is static if I go back to it remember it's a static event so it's getting called by any click select controller it doesn't matter what click select controller is loaded if it's the one in level one or the one in level two they're both calling into this same static event they're not calling into an event on themselves and they're calling that event to just say hey something has changed or our selected entity has changed and then our registration of course just stays working with our UI controller so as long as we've loaded it in a UI controller and we haven't destroyed it which unregister is it here then we're registered for the event callback and we're gonna get the health panel binding and everything's just gonna work so that's how it essentially works but now I want to talk about that stats panel because I think that when we start off doing something simple like a health panel might be relatively easy oh look at that hit a breakpoint let's get out of that f5 f5 get rid of the breakpoints but doing something like a health panel isn't too hard what if we wanted to do something that's a little bit more complex like some stats like I wanted to be able to select this guy let's hit f5 and stop with all the debugging and I want to go give him some speed or whatever now normally I probably wouldn't just have buttons where you select the character and then add a stat to him but you might like go pick up a powerup or you've you know done something to the NPC to change his stats in some way and you might want to be able to show that in your UI and also just reference it around it in your game because remember these concepts for eventing don't just apply to you eye stuff they can apply all across your game architecture but you think you eyes are just a really easy way to show how to take advantage of it and how much time it can save so here we go I can set all of these stats and just keep modifying them and click off click back on and see all of the stats appear let's see what that looks like why does that stat system work how does it work how are we showing the different amounts of stats and even if I go in and like select one of these dragons yep do I have a yeah I got my special stat at our script I can even add in a new stat that they don't have like max health and then hit what is it spaced I've set it up to you space BAM now he's got a new max health stat and it's added to his stats and then it also shows up in the UI let's take a peek now so you may wonder like what this UI scene looks like let's just take a peek at it we have a couple things in here we have that UI a controller I've talked a lot about and we have the health panel if I go look at this health canvas it's actually a separate canvas here with a panel underneath it and then underneath that we have a fill for our it's just a fill image for that bar right there and then a tex-mex t' for the text in here the reason that they're in separate canvas is by the way I there's a whole talk about the benefits of splitting up canvases and some of the performance characteristics but generally you want your canvases to be grouped together so that they change together so that if something is changing on a canvas when things change if they all change together put them on in one canvas if they change independently or different times they should usually be on separate canvases primarily for performance but also find that it makes it easier to manage these things a little bit so let's look at the other parts of this stats canvas because this is where I think the more interesting stuff is happening and where there's a nice easy pattern that you can use to kind of follow along so I'm gonna switch to my scene to 2d mode zoom out that's just my scene view and let's take a peek here so we've got the stats canvas selected and I expand it out underneath it because on the route notice there's really nothing it's just the standard canvas underneath it I have a empty rect transform that has a stats List script underneath that we have a stats grid and if I scroll this down you see that it's just using a grid layout group where I can kind of just define the size and the width of the columns and the height of them so I've set it to 300 and I said them to us there's 50 there there are 40 Oh for 50 I mean or 40 you can see this is just a standard grid layout group for setting up the size I like to use these instead of horizontal and vertical layout groups a lot of times find them a little bit easier to work with okay underneath there we have a couple different things well we have a stat holder and then we have a bunch of stat holder clones let's stop playing and see what this looks like outside of plate mode so stop playing mode and I'll jump to my scenes and open up that UI in fact I'm gonna add it just drop it in additively so we still have a nice pretty background now let's expand out the stats list one more time and under the stats grid we have a stats holder and that is this object here with the red background the text another text and a button and a line so if I disable it you can see that's this object here this is kind of the template or prefab for all of the stats that I'm using except it's not in a separate prefab or anything it's actually just a child of this so that I can see it in here and see what it looks like and the code is just going to find it and disable it automatically so that I don't have to worry about it and I don't have to drop one out every time I want to see what it looks like and play around with it so my stat holder underneath it or actually let's look at the script it has a single script called stat holder which has a label and a value and then that just links up to the label here which is the name where it says strength and a value which is that 140 we also have a button on here let's take a look at the button the button has an on click event calling to the stat holder which is just its parent and it's calling add stat on it so let's go look at that stat holder script the stat holder script has oh not much to it really we have those two fields the label and the value they're both text meshed pro text objects then we have a stat data and a stats private object that we're binding to so you see that he's in a similar pattern except we're not binding to an entity instead we're binding to the stats that an entity has and a piece of stat data and then we have this ad stat method that it says it's never used but remember if we go back in here and look the button our button is set up bound to call ad stats so when we click on the button it calls ad stat on this parent what does that do what it says hey give us our stat type figure out what that is and we call stats dot modify and we pass in the stat type and the amount so we're just increasing it by one let's go take a look at what stats that modify does real quick and here it is so what we're actually doing in this modify is grabbing a stat from our runtime stat values which is actually just a list of stats we're getting it by the stat type so that's the strength speed damage whatever it was in fact if we hit let's the f12 and just go look at stat type you see there we go get an old version of a controller in there basically we have it's just an enumeration of different types with strength speed damage and Max health in there let's go back so we're finding the first run time stat that has the matching stat type there should only ever be one that matches we could also use a dictionary or something keyed to make this lookup a little bit faster but it's small enough list that it didn't make sense to overcomplicate it so we're using the first or default to find the one that matches the write stat type if it's equal to null so if it's a new stat that we don't have on our character yet then we go add in a new stat data so we instantiate a new stat data set the stat type and then we add that to our list then we take whatever value we had and increment it by the amount remember right now we're just passing in a 1 so we just click and add 1 so if it's a new stat it's actually gonna get created added to the list and then the value is gonna get set from 0 to 1 if it's an existing one this won't be no and we'll just be incrementing the value by 1 let's look at how run time stat values exists though what that means and then talk about the on stat change event invocation because that's when things get a little bit more complicated but if you don't know what this runtime stat stuff is it might not make any so let's grow up to the top of the stats class a couple important things we have that on stat changed which we're going to talk about in a moment we also have this private serialized stat data's field this is so that we can give our character some default stats when we're setting them up so that's why when I go in here and I select one of these guys I see some default stat values and if I go pick one of them let's go select them select the micro dragon I've got the stat data as expanded out so here we've collapsed and expanded and underneath it it just has some elements for different stats that I've given them as a default now in a real project I'd make a custom editor for this so that I don't have to do this weird element editing stuff but it seems kind of outside of this video so right now we're just expanding it out and I can just add in more stats I could add in a third stat just by changing that to a three go pick hey he also has 22 max health then if I hit play load my UI I should be able to select them and watch it let's see I select them there we go I've got my max health at 22 the other thing that's important to note though is this run time stat value so I told you where the stat data's come from that's our defaults the run time ones are our actual like live stat values so right now my damage is at 21 if it you look here if I hit plus the run time value is going up the definition data here this 21 here is not changing and that's mostly just so that I don't accidentally misuse things and change my stat data on a prefab or some other place object instead of the runtime version of it I want to make sure that I'm not accidentally modifying serialized fields so I created a new set of that data that's the runtime modified once and I expect to be modifying stats constantly depending on the type of game we could have all kinds of different things modifying the stats and having that base value there can be really helpful sometimes I might just want to recalculate and just know what that base value is and like all of the items or something else and then add that all up and put it into run time stats whenever they change there are a lot of different ways to go about doing it but having them in a separate list of stat data's helps a lot to simplify things okay so we've got that figured out we now have our run time list of that's and we have this on stat change that's getting modified or getting invoked so let's see how do we find out how this works a quick and easy way to do this is in writer or visual studio go select the event and just hit shift f12 and this is gonna actually show us all of the places that the event is referenced so this could be where it's registered for D registered for invoked or anything else so if I look here you'll see it has my usages of on stat changed and in modify if I just double click on it this is the invocation so the read access is actually technically the invocation the write access is where we're going to be doing the binding and unbinding so if i look at my stats list remember i have the bind method and i have the on destroy both of these the on destroy and this first binding one here have the minus because they're d registering for the event the one we're actually registering is this one right here I just double click on it and I can go to it and here you'll see that it's in this stats list script so it's a kind of big script I'm gonna zoom it out a bit and we'll talk briefly about how it works so the stats lists I guess let's go up to the top has that stats holder prefab which if we look at it it's stopped playing real quick and go take a peek so we look at our stats list if we select the stats holder prefab remember it's just the child in that grid it's not actually a prefab like in a normal prefab way in fact I should probably rename it call it like template or something other than the word prefab but we've got this object here that we're going to instantiate multiple of and then we have the route for where those go and if I select it again staff list click on the route the route is just the grid okay so in our awake we set the object or the stat holder prefab the one that we're cloning or making copies of to not active that's where I said we're just gonna hide the existing one since we're in a grid the layouts gonna automatically adjust for that and we'll just have a hidden object there that shows up again when we're not editing or we're not playing when we're in edit mode and on destroy we look for any bound stats that we might have because we're not binding to an entity we're binding the stats and we D register for that event Deon's that's changed if we happen to have one of it so next we have the bind method and remember this is the one that we glossed over or I skipped over multiple times in UI controller so in UI controller when the selected entity changed we would bind the health panel to the entity we'd also bind the stats list to the entity's stats so let's go back into our stats list when we do that binding what we're doing is saying hey if the bound stats is the same as the existing one then set bound oh if it's no hide the object so for rebinding to nothing just hide our route object so that we don't have that background but then just bail out so this is just saying hey if they're the same return but also if they're the same in there no just make sure that your route object isn't active so that we don't have that empty stats panel then we say hey if bound stats is not equal to null so we've kind of gotten past this if it's not null then register four or deregister for the on state changed event or stat changed I said state stat so on stat changed event and then set our new bound stats this should look a lot like the entity binding right and then we go through and despawn all of the holders so we get the bound stats we set it up so that it's bound to this stats then we loop through all of our existing object or stat holders and destroy all of the game objects now I want to be really clear here in a bigger scenario or in a scenario where you care about memory management which is like in a real game you probably don't want to just destroy these and recreate them it's much better to hold them around pull them and reuse them but again I didn't want to over complicate this too much and dive into pooling and events and UIs and everything else so just remember that if you're doing this in a bigger project once you get a little bit further along remove the destroying part and switch to pooling these instead so keep them around deactivate them and reuse them as much as possible instead of recreating them now let's just say that you're doing that though and we're gonna pretend that you're gonna do that later anyway what would you after that clear out the holders list so this is just a list of all of these stat holders that we've created and we just destroyed them so we need to clear that list then we say if the bound stats is not equal to no tell register for that stat changed events call our bound stats on the name for that is is very bad let's call this handle stat changed always good to rename things when they don't make sense either or oh and you just can't read them out loud so we bind for our stats on stat to call handle stat changed which we'll look at in just a moment then since it's not no we set our game object to active so that shows up and then we loop through all of our runtime stat values and create a stat holder it's like it creates that holder real quick creates that holder just instantiates based off of that prefab which again I mentioned is really like a template not a prefab and puts it into the holder as its position so that's just the parent transform that we're giving it then we call stat holder dot bind and we bind it to the bound stats and the stat data we set it to active so the stat holder shows up and then we add it to our list of holders mostly Oh primarily so that we can clear it out and destroy it later but also because when a stat changes we want to be able to update it so the last thing that we did here it kind of glossed over us when we register for a bound stats on stats let's go take a look at that that's this method in our stats so whenever we modified a stat remember we were calling on stat change so we hit the plus our on stat changed is getting called on our stats which is that object on the character on the entity it's not the individual stat it's like are all of our stats so when that gets invoked we call handle stat changed and here we just look at our holders list oh and holders actually is a dictionary I think I said it was a list it's a dictionary and that's mostly so that we can look it up and use it keyed so we try to find the holder that has our stat type on it so let's go look at holders really holders is a dictionary of stat type and stat holder so this is a quick look up by a stat type so we can give it a stat type like health or max health and say hey give me the holder for max health if you have one we say hey give me the holder for speed it'll only allow one of each type to be added in there and each one of those will have a single stat holder although this data could technically be null we're not ever gonna have that situation in our case so we're creating a style holders in our create stat holder and adding them and when we add them by the way I should have met notice this right when I saw the code but we add it with stat type as the key and stat holder is the value so when a stat changes we try to find the stat holder value here by using the holders try get value and on a dictionary what this will do is it'll look for an entry that has the first parameter as a key so it's gonna look for an entry that has our stat type as the key and then it's going to if it's true so if it's able to find it that's what the try gate value does that's going to return true if it's able to get it false if it's not if it is able to get it it's gonna put the value or the stat data into this existing stat value parameter it's an output parameter and then we'll call existing stat data bind and we'll bind it up to these stats and that's that data entry otherwise we go through and create the new one so if it didn't exist we create a new one which is then gonna add it and then of course do the binding so we either bind here or there one of the two spots depending on how the stat has changed and actually after looking at this again I think that the name bind here is a little bit misleading because we're not actually binding the objects in this case we're really just setting the data here so I'm gonna rename this bind method with control shift our are or just patrol our are and I'll call this set data because I think it can be a little bit confusing to misuse the word there we're not actually binding it we're just setting the data which is also why we're doing that in handles that changed if we were doing a binding every time I stat changed something might be a little bit off I don't think that that would make sense but setting the data is exactly what we want so we're doing our actual binding when we do the event registration here this is our bind method the other one was kind of I think miss named or just mislabeled so I wanted to clarify that and the last thing I wanted to take a quick peek at too was the stat data class because I realized I may have kind of glossed over it the stat data class is just a public class that's marked serializable so that it shows up as expandable children inside the inspector and it has a public stat type on it and a value it's just a data structure that I can use to hold the existing runtime stats and the I guess design time stats for our NPCs or our entities there so this system again it works relatively well and it's pretty simple and pretty straightforward and I tried to keep it very uh opinionated it's very easy to build up an entire framework around something like this where you have your own base classes and interfaces that you're reusing along the way but I wanted to show the core part of how this thing works how to set up the separation and have your UI be a totally separate scene or even have multiple UIs be able to swap out different UIs at different times or on different devices and have everything just kind of work and that's really the goal of this if this is the kind of thing you're interested in though please let me know if you made it all the way to the end definitely hit the like share and alert buttons and all that stuff and just leave a comment and let me know I'd love to do more of these longer more in-depth videos where I focus on problems that I think a lot of us struggle with and again yeah if you like this kind of thing make sure that you just let me know so I'll keep doing more of them also as always a special thanks out to everybody on patreon really appreciate it you guys are awesome and I think I'm out of things to say let's go back in and play one more time one of these times I need to make one of these into just like a real interesting fun game too as a demo sometime soon all right anyway thanks again bye you
Info
Channel: Jason Weimann
Views: 89,739
Rating: undefined out of 5
Keywords: unity tutorial, unity technologies, game development, game dev, unity UI, unity user interface, unity prefab, unity scene, unity hud, unity tutorial 3d beginner, unity3d, unity, tutorial, prefab, scene, ui, unity ui builder, unity hud canvas, unity hud text, unity scene change, unity 3d user interface, unity3d rpg
Id: 6ztY9-IX3Qg
Channel Id: undefined
Length: 59min 52sec (3592 seconds)
Published: Wed Jan 22 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.