Object Pooling (in depth) - Game Programming Patterns in Unity & C#

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[Music] hey what's up Jason here from unity3d college today we're going to talk about pooling systems and we're gonna go pretty deep a pooling system allows you to reuse your objects which gives a really big performance increase especially on mobile devices and VR platforms you really want to avoid generating garbage which is exactly what this pooling system stuff will allow you to avoid doing so let's get started we're gonna go over a couple different types of pooling systems we'll start with something relatively tightly coupled and then move in a more generic fashion as we kind of go through these things but to begin I want to show a system with no pooling at all so our first sample we have just the spaceship here just a simple spaceship grabbed off the asset store and this has a blaster on it this blaster is just an empty game object with this blaster script and if we go to the scene view and hit F right there with it selected see it's just kind of set up to be right around where this wing is so that our blaster shots will spawn right there so let's hit play watch it and then we'll take a look at the code there we go any second now there we go we've got some shots coming out if you look here with the refire rate you'll see that it's set to 0.4 so we're getting 1 we're about to a second a little bit faster than that though and this is the prefab that it's spawning this blaster shot no pooling which you see down here in the folder so let's open up the script alright this is somewhat important because all of these examples use very similar code for the firing Andry firing part so let's just look at this kind of in detail and see how it works because it's going to be reused all the way across we have first a serialized field for this blaster shot prefab now this is referencing a very specific blaster shot for this example there's four different examples here they each have a slightly different name just so that there are no collisions and there everything's kind of clean so we've got our blaster shot prefab right here we have a refresh or refire rate remember it said 2.4 in the editor but it's set to 2 here as a default serialized field allows us to edit this private variable in the editor and then we have another private float here for fire timer this one is not edited or inspectable or editor editable but it is getting counted up in updates if we look here on line 15 in an update we increment it by Delta time if it passes the refire rate so if it gets too over its point zero four in this actual example then we set the timer back to zero and call fire this is just a very simple system to call fire every whatever time this is set to whatever that interval is so then in our fire method very simple we are calling instantiate so it's just game object at instantiate we were passing in our blaster shot we give it our position and our rotation so this spawns the shot there let's go look at that blaster shot script so this one you see has a move speed just hard-coded a 30 a lifetime a max lifetime set to 5 seconds and in on enable we reset that lifetime to 0 just hey this thing has not been alive it doesn't matter so much for this non pulled one but in the pooled ones it makes more sense and then in update we move this thing forward using transform translate and giving it a vector three dot forward move speed and Delta time it just makes it go along with the framerate of the game and then we increment the lifetime and if it gets to be too old look it's over that five seconds we destroy it nothing complicated at all and you see here that it looks like we have just ten shots but if I select one you see it's actually moving up and eventually getting destroyed and these are just new ones that are getting created and added on so every time we fire a shot we created a new shot every time a shot gets to five seconds old we destroy it of course if we hit something with one of these shots it would normally destroy it as well now before I talk about how to fix the problem I want to show you what the actual problem is so with this still running I'm gonna go to window and then I believe it's moved to analysis and then profiler and things been moving a lot lately but here we go we've got the profiler window open and I'm on the CPU usage part fact if I drag this up your window may look a little bit bigger than that it may be off floating I've just got it docked right down here so we can see the CPU usage is going I have scripts and garbage collector showing only if you click on these you can toggle them on scripts and garbage collector really the only things I'm worried about right now though so I'm gonna hit current to just get current data right there and we should see it right down in here so if I expand this out in our player loop you see this GC alok that is the garbage collection allocations that have happened this is allocations of memory that are going to need to be cleaned up by the garbage collector later and if I sort by this you can see where this is coming from so part of it is in this send Mouse events this is probably some debugging stuff in the editor because we're not doing anything with the mouse and that's one thing to watch out for the profiler and the editor will generate some garbage here so you need to look at the stuff that's specific to your game in our case though it should be well here let's just click on a point and see if we can find a good spot where as a point where a lot of garbage is there it is so we're these 40s and 80s are happening and here I'm just dragging this white bar until I see the bigger spikes again because those other ones are just just the editor but if I expand that the script run behavior update and expand this and expand that you see that the garbage is actually generated in this instantiate call and it's when we create this new object that it's that the memory is allocated garbage is generated and it then has to be cleaned up when the object is destroyed again on a fast computer in Windows or Mac or even on Linux it's not really gonna be an issue it's not gonna make a big difference at all if we're allocating a little bit of RAM here and cleaning it up if we're on mobile though it's gonna make a huge difference and if we're on VR it can even make people sick because you can end up losing frames now if you're playing again a game on a flat screen running at 30 frames a second or 60 frames a second if you drop a frame doesn't matter if you drop ten frames you know your frame rate drops from 60 to 34 a quarter of a second it's probably not gonna be noticeable unless you're in a really twitchy shooter or something so it's a much bigger deal on these other platforms but it is important and you can go overboard you can be generating so much garbage that it does become a problem on a Windows system so just try to minimize this as much as possible and use these pooling systems for that so with all that explanation there and the reason for why you would want to use pooling let's see how we would set this up using a pooling system and said now here we have a simple pooling example again we have a spaceship expand that out we have a blaster the blaster is a little bit different though we don't have a reference to a prefab we just have a refire rate and here I've turned it down to point zero two so this want to shoot a little bit faster and then we also have in our scene this blaster shot pool and I noted that it's hard-coded because let's say this one is relatively hard coded for how it works and this one does take a prefab for a blaster shot which is just this other blaster shot again very similar just a slightly different class name that we're using here so let's uh well let's hit play watch it in action and then we'll dive into how it's actually working so there we go we're playing in here I've also added an animation so this thing I'm just gonna go up and down but watch the shots here so you see the shots and you may notice something there if you're paying close attention so then we're graying out so if I select one right here let's just watch eventually you'll see this is gonna get disabled well it may have gone so fast it didn't even see it there you go disabled and then re-enabled so what's happening let's go to the scene view and hit F and just here I'll zoom out let's watch one of these shots oh there goes the shot it's flying all the way over there and you see it reappeared back over there it's because it's being reused instead of getting destroyed when it hits the end it's actually getting disabled kind of shoved back into a place that places our pool and then pulled back from there when we need to use it so let's stop playing and take a look at the code and see how this all works well first thing I want to do is just open up the blaster we've already looked at the blaster so let's see what this difference is here here we have a refined rate in the fire time all of this is exactly the same same old boring boilerplate fire refresh stuff but then in our fire method we're doing things a little bit different instead of instantiating something we're calling blaster shot pool dot instance dot get and then that's returning back a blaster shot you can see that right here if I mouse over it then we're setting the transform position and rotation to match our blaster and setting gameobjects to true so here instead of instantiating a new shot we're telling this pool and it's singleton instance to give us the shot and then we're putting it where we want and turning it on let's go into our blaster shot pool now all right so this is a very simple basic pooling system we have a monobehaviour here with our pool class name and then here we have a prefab for the prefab that we want to instantiate with our pool again this one's very tightly coupled we'll get into more generic ones soon but this one only takes a blaster shot and we have a queue of blaster shots a queue is stay first-in first-out collection so you put things in one side with in queue and they eventually pop out with DQ whenever the thing you put in first is is the thing that you'll get out for so I put in two things I'll get out the first thing you get the idea I've done the whole video on these though if you're interested just go search for it or I'll I might link it to beyond that we have a very basic singleton setup so this video is not about Singleton's I didn't want to dive too much into it so I just kept it as basic as possible we have a public static blaster pool shot pool so it mashes named instance with a public getter because this is public and a private setter so nothing else can set the instance in a way qui set the instance to this again this would not survive across a multi-level game or something like that so you need to set it up to actually persist or don't destroy onload or something else you need to come up with a slightly better singleton but I didn't want to go too much into that again not the focus of the video this right here is the focus of the video though this get method so this is what we were calling from the blaster pool right here this blaster shot pool that instance that get and that was well I lost my place already let's go right back to it so this is looking at this blaster shots cue and checking to see if it's empty first time through it's actually going to be empty because we're not pre filling this if it is empty we call ad shots and then we eventually DQ so we get whatever this shot is in there so there are no shots in there we and then we pull one out so eventually if maybe for the first whatever this is maybe ten shots that we're shooting we were actually instantiating a new shot each time we're just never destroying them so they're getting reused now our add shots method let's see it just takes account and loops through and instantiates that many shots so we get a blaster shot from the prefab we said it's not active and we in cue it this is how we add it to that cue and I might wonder why we're taking a count here if we're always passing in a one that's because sometimes we want to do things like pre-warm these so we may do something like on an able and just say add shots ken or we do this in the start or something and the reason for that is once we know how many of the objects we generally need and we have a really good idea maybe we know that our game requires about fifty shots max on screen at a time we can pre initialize all of those right at the start up so that they don't have to be created at runtime it's faster a little bit more performant just to avoid some pitching but it does eat up extra memory so we don't really want to do that until we know a good idea of how many we need and then we do want to have the ability to grow the pool beyond that if we guess wrong in this case we're just not guessing cuz we really don't know cuz let's be honest it's not a real game it's a spaceship going up and down shooting off some shots so that's essentially how bad shots works but then there's one more part right down here this return to pool so spawning things from a pool and putting them in the queue and pulling them out of that queue it's really cool and all but it doesn't do anything if we never put them back in when they're done if we just destroy the objects when they finish then it's not gonna work we need to return them to the pool so let's see where this is called from in fact to do that I can just select it and in Visual Studio hit shift f12 to find the reference and that's in our blaster shot script so this was the script that's on this blaster shot prefab that we're spawning let's look at it so the blaster shot again same same exact stuff except here the life time actually matters because on enable we're setting it back to zero so remember we're disabling the object when it gets to dead time and then we're enabling it when we you need to pull it back out so in our in our update here if we reach the max lifetime instead of destroying the game object like we did in the previous one this one right here we just destroyed game object here we just call blaster shot pool that instance dot return to pool that puts it to not active resetting that timer when we re enable it set it to active again and then Rhian queues it so pretty simple but there are some issues here the biggest issue is that this is super tightly coupled this pool only works with blaster shots the blaster shots need to know exactly about that pool pool has to be there and if we need to do it for another thing we basically have to copy and paste this whole thing so if I want to make a missile pool or something like that I'd have to copy this whole thing make a missile pool change this to missile something missile something missile pool instance you get the idea there's a lot of copy and pasting and just kind of messiness that comes along with this now if you're only gonna pull one thing whatever totally fine but once you need to pool a second type of thing it's a lot better to move into one of these more generic options that I'm going to show you this next pooling system is a whole lot more generic in fact it's probably the most generic pooling system I can come up with and let's take a look real quickly you've got a space fighter here and I've got two blasters and if you look here this Blaster is a blaster game object pooled and we have a game object pool that we're assigning and then a point five refire rate cuz we've got to mix it up every time right then down here we have a game object pool just like that and you'll see that we have a reference to a prefab for this blaster shot game object pooled and I'm gonna clear this reference real quick notice that the prefab type here is game object that's why this is a game object pool it takes any game object and will pull that game object so we don't care what type of object it is I just care that it is a game object so we can pull it let's hit play watch it and then take a look at how this thing works there we go so it's firing two shots from this one you see the shots are there and notice that they're kind of disabling in pairs and that's just a side effect of them spawning together to spawn at a time those two happen to die at the same time so they're enabling and disabling if we were hitting targets and stuff along the way the order of the theis are enabling and disabling would be totally different because some would go past somewhat disabled when they bumped into things I think you get the idea though so there we go we got two shots firing they're using this one pool if I select the two blasters you see if I click on these they both go to that same pool so they're both sharing that pool as a resource for blaster shots well let's look at the blaster first the blaster here has the same refire rate stuff the same fire code that's all exactly the same one difference though is the serialized field for the game object pool that was the one that you saw in the inspector there right here this game object pool that's assigned and that is used right down here and fires so when we fire we just say hey game object pool give us the shot and we do exactly the same stuff so here the only real difference that you're noticing in this blaster shot or this blaster versus the other one is that we don't have a hard-coded pool we're now assigning the pool to the object some situations this could work fine some situations maybe not just really depends on how the game is set up in this case though as long as we have a way to set this pool we're good so it may or may not be done through an inspector field like this it could be done some other way we just need a reference to a pool that we can use to create our objects and keep track of them so let's see we've got our pool there let's see how that pool is actually working we'll open this script up and again you see we have a prefab this time it's just named prefab and it's a game object though instead of a specific type and we have the same singleton pattern right there this instance and instance and we have a queue now that's called objects because there could be anything this could be a particle system it could be an empty game object it could just be a primitive whatever the thing is it doesn't necessarily have to be you know a blaster shot or a missile it or something in our get we do the exact same code check to see if there are none if there are none we add 1 + DQ it and then in our return to pool we do yeah basically the same stuff you see there's really no difference all around except for one thing if we're using game objects we need a way to get these things back into the pool right we have our general game object and we want it to go now back to the pool when it gets disabled or when it's going to be destroyed or whatever it is so the way that I like to do this and this is kind of an optional add-on here you could manage it some other way but I think that this system works pretty well is I like to use an interface and I'll add the interface to one of the components on the game object so it could be a specific component just for handling pooling it could be a component specific to the type of thing it is and this could be on a blaster shot in fact in this case it is or it could be you know its own total separate script but I like to use an interface for it and in add objects what you'll see what we're doing is we're calling get component to get this eye game object pulled interface and we're setting the pool to this let's go look at that interface right here you'll see that the interface is actually extremely simple it has a pool on it for game object pool with a getter and setter that's it so I wonder well how the hell are we using this what does it do well let's take a look again at the project so if we go back over here we look at our game object pool and we look at the prefab that we're spawning it's this so if I click on this and let's just hit open prefab we have this blaster shot game object pulled script on it let's open that up and that's the one that we were actually in here the interface is just at the bottom of it normally I put the interfaces in their own files but in a simple thing like this it makes sense just keep it there if we scroll up here to see that we are implementing that interface the eye game object pooled and right here you can see the implementation of that so it could just be a getter and setter it could be as simple as this let's in fact let's comment that out we could do something as simple as public object pool pool we're gonna get in a set that could be it what I have done here instead though was give it a backing field this private one and then just make it so that it can only be set once so in our check in the center here we're saying hey if the pool is null set it to the value otherwise throw an exception because we're doing something wrong it's not being used as its intended the pool should never change once an object is added into a pool it should stay in that pool indefinitely it shouldn't ever like go to a different pool or have its pool value be set to null or something like that this is so this just here as an extra check then in our update when things expire right here we have this pool that we've set right there and we just call return to pool and we pass in our game object and we're done so here again it's a nice simple little way to add the functionality on top of a game object without having to add too much more we can of course get by without this but then we need to come up with another way to return the object to the pool which could be like a manager system that's managing the lifecycle of these things and pushing them back into the pool but I generally prefer a nice simple system like this so things can just kind of disable themselves or expire or whatever they're gonna do in their code and then return themselves so again this system works pretty well it's not the one that I use most of the time though in fact that one's coming up next so let's get to that all right this pooling system is using generics if you haven't used generics don't worry they're not really complicated they seem a lot more complicated than they are it'll be pretty simple though I think once we go through the whole system so here you see we've got the ship shooting three shots at a time now and lots of shots are firing off and they're just getting reused right none of them are getting destroyed it's all reused and pooled fun stuff so let's stop playing take a look at how this thing differs and what the benefits are here so here we've got three blasters these have a blaster with generics on them blaster with generics blaster with generics and then we have a shot pool so our shot pool if you look here takes a prefab for this blaster shot with generic pool object and that's again same thing just a slightly different script on it for the four different folders that we've got going on here so let's take a look the blaster first just like before and our blaster with generics again we have the same refire all exactly the same and here in our fire method they see this kind of looks a little bit more like the first version right we had the blaster shot pool dot instance get that just returned back a blaster shot pool I think okay this is very similar to that one what what the hell like is this the same nope totally different looks like so if I go to definition there you see that this shot pool is actually totally empty there's nothing here it's a generic object pool of type blaster shot generic pool great name right so this is taking advantage of generics with the benefit here is that we're gonna get a lot of the close coupling that you see in the other one without the copy/paste rewriting code and I say it's close coupling I mean it's gonna be easy to tell what's referencing what and we don't have to use so many editor or specific references for stuff now if you're working heavily with designer so we're constantly changing these things maybe not the best solution for you but if you work like I do and it's mostly in code and you prefer a lot of things to be in code where it makes sense in this system it's perfect for me I find it the easiest to figure out exactly what the thing is what the pool is what I'm grabbing so let's stop blabbing though and just kind of get into how it actually works so if I go to the shot pool that was this we need to look at the base class here this generic object pooled and remember we're using the type blaster shot generic pool so here we go first thing to notice this is an abstract class that just means that we cannot instantiate it you cannot create a new generic object pool it has to be a subclass like our shot pool that we just saw we also can't just add this script directly to a component or a game out ejected as a component it won't work because it's abstract and that's intentional because this is not meant to do anything without defining what this T is right here and this is how the generics work so we create a class and then we give it the little less than greater than brackets there and then we put something in here and T is a pretty usual convention just because it's short for type but this could be named pretty much anything I can name this like T to pull pool or T pool or whatever or I can name it J yes my name is Jason it doesn't really matter but T is very much a convention if you start to see multiple things though like maybe you have one that's like T and K or something just means there were you need two different generic types and you have to give them different names don't go with t1 and t2 though if they're if you do run into this case where you need multiple generics just name them properly in this case T is good so maybe wonder what the hell that all means well let's keep going so we have generic object pool of type T and it's a mono behavior and then we have this where t : component so what this is saying is that T whatever this type is here must be a component this for this part right here this where t : component means whatever T is it must be a component we can't do it without it being a component in fact I can show that so if we save this and we go over to the where's our pool which pool was it I'll go back back back there if we go right here and we change this to like int we're gonna have an error let's say hey it cannot be used there is no boxing conversion from int to unity engine dot component because the type here must be a component luckily our blaster shot generic pooled is a mono behavior which is a component so we're good to go there well let's look at this class while we're in here same exact stuff nothing nothing nothing different right here is the only difference we call shot pool that incidents dot return to pool again it feels tightly coupled but it's a lot simpler and a lot cleaner and I just really prefer this over passing in a pool or assigning a pool I know that my shots all care about the shot pool they're not really coming from other pools and this is the case a lot of the time when I'm making these pools they're usually one type of thing in that pool it's not changing I don't need to adjust it at all setting them up like this works and they usually don't end up with too many pools it's another thing to really point out in a game I may have one pool for the whole thing I could have ten you know but I'm never gonna have hundreds of these things that I need to manage at least not in any project that I've worked on there's probably some edge case project where it makes sense but in general there aren't that many things that I want to instantiate a bunch of and remove an ad so let's look some more so we've got our generic object pool and we've got that tee and we know that T is a component so then here our generic object pool instance also needs to have this T because we want to find the instance for this in fact this right here along with the awake method right there that's how you can do a very simple generic singleton so here we can if we just had that in this we'd have a very simple generic singleton that we could use and then call the instance part on it remember if we look at our shot pool our shot pool is called with shot pool dot instance and it's getting it because right here in the base class we know that we have it then we have a queue of type T so whatever the type is that we're using there in this case it's that blaster shot and then in our get method we're returning T so again this is going to return the blaster shot the one that's right here it's gonna return whatever this is so if you think of it like this right here is essentially what you see let's save that what this is going to be like it's gonna be like that when we use this as our type for the generic so then we're able to get it from the cue just DQ it just like normal and we're able to return it to the pool the same way we just use the same T here for the type of object to return since T matches there and there we can return it and then in objects same exact stuff so nothing really different except for the fact that now with this very very basic generic pool you can ignore this other stuff down below there by the way nothing important there but with the others with this system right here we can create these other pools very very easily we just say hey here's the pool name here's the type done right that's it we have a pool it's ready to use it's reusable to a degree I mean it's the code underneath there is reusable because all we need is this little chunk right there anyway thanks for watching don't forget to Like subscribe share with all your friends and that stuff and have a great day you
Info
Channel: Jason Weimann
Views: 49,461
Rating: undefined out of 5
Keywords: game development, object pooling, design patterns, design patterns c#, game programming, unity tutorial, c# tutorial, unity performance, unity garbage collection, unity3d garbage collection, unity3d gc, unity gc, unity3d performance, gameobject pool, generic pool, brackeys, unity3d, unity 2018, unity 2019, unity3d college, game programming patterns, game design patterns, brackeys c#, unity performance optimization, unity object pooling, unity performance issues
Id: uxm4a0QnQ9E
Channel Id: undefined
Length: 29min 55sec (1795 seconds)
Published: Thu Nov 15 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.