The RIGHT way to Pool in Unity - Save your Game Performance with the new generic Object Pool

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in this video i'm going to give you the new solution to the biggest performance issue you'll run into with unity this video should apply to new developers if you've not done very much unity development get ready because this is going to teach you what to look out for and get you a little bit ahead of the curve and if you're already an experienced unity developer then just hang on a second because after i go through the explanation of why this is important you're going to be surprised to see some new stuff that you might not have known got added into unity lately that makes it a whole lot easier to deal with so what's that issue the number one thing that i've seen every developer run into including myself well it's garbage collection or specifically garbage allocation or memory allocation that needs to be collected later remember when we use c sharp it manages memory for us automatically using a garbage collection system so we can instantiate or create new objects have them go into the ram of whatever the system is that we're on a desktop a laptop a phone or whatever the device is and then it will clear that back up as soon as it's no longer used as soon as that little block of memory is no longer referenced by anything it'll clear it up but it's not actually as soon as because that process of finding out that there's nothing referencing it is a little bit slow so it delays that and then there's a garbage collection process and if you work on mobile if you've done much mobile work at all i'm sure you've run into the issue of garbage collection causing big freezes in your game now if you work on high-end pc systems you might not notice it as much but your lower end players will start to see these problems as well your game will just keep allocating memory and then eventually dropping back down if you ever watch in your task manager as you're playing a game you might see this exact thing happening with some of the games that you play where the allocations go up up up up up and then it drops down and up up up up up and then drops back down it's doing essentially the same thing but again when we get to a mobile device or a low end system this can be a really big issue so how do we address it and what is the new awesome thing that i really wanted to share the answer to that is object pooling now if you're an experienced developer you're probably thinking i already know about object pooling i do that all the time you've already done videos about this jason what are you talking about well the reason i'm doing another video about it is because unity now has their own object pool classes there's a variety of them that allow you to do a couple of different cool little tasks but really the biggest part of it is that they allow you to have pools without writing any code i no longer have to go back to my old libraries and pull in a pooling class or look at any of my old documentation or my old videos to see how did i do it that one time instead i can just use the built-in classes so today i'm going to show you the performance benefits that i get from this and exactly how you can implement it into your own project it's extremely easy really you can do it with as little as two lines of code but i'm going to show you some more advanced examples as well let's get started with performance why does garbage collection actually matter how can you tell if you're causing garbage collection and how can you figure out where you need to fix it to show that i've set up a quick sample scene that'll allow us to watch garbage allocation and then watch it disappear when we implement pooling here i've got a ball spawner and the ball spawner spawns a ball prefab the ball prefab is a simple sphere with a rigid body and a sphere collider on it and a ball script we'll take a look at the code for that in just a minute but first i want to point out the ball settings if we click on it you'll see that it's a scriptable object just sitting in my root with a single field on it for the life after landing this is how long this ball will stay alive after it lands on the ground let's take a look at what that's actually doing so that the code makes more sense once we dive into it we'll go back to the scene now let's hit play and watch these balls drop and disappear and cause some real issues so here you can see i've got a bunch of balls falling down and after just a short amount of time i've got it at 1.5 seconds they disappear and new balls just keep spawning they spawn from this ball spawner or more specifically from the drop point that i've assigned to that spawner you can see it right there and they just kind of fall down and bounce around if you're curious how they're bouncing if you click on one of these balls or click on the ball prefab you see that the mesh renderer not the mesh renderer the sphere collider has a physics material assigned the bouncy physics material the bouncy physics material just has a bounciness at 2.8 and you can create a physics material just by right clicking in the project view and choosing create and then finding physics material that's just off screen so how is this working and why is this a problem well it's working by instantiating a ball every single frame inside of our ball spawner let's take a look at that now the ball spawner has a ball prefab on it and it has a used pulling option that's turned off so let's see what happens when we don't use that pooling option we can ignore all of these other settings for now we'll open up the ball spawner and inside of our update if we don't use pooling we instantiate the ball prefab and we just set its position to a random position which if you want to see the code for is not very complicated at all we just pick an offset position within five meters in each side from the drop point and then use that so we spawn a new ball and then that ball somehow cleans itself up later let's take a look at that ball script now the ball script is what's doing the cleanup so i'll go find my ball script and we'll open it up in here you see that it has an ontrigger enter now it says water in water trigger is equal to true whenever we enter a trigger and i'm going to show you why it says water in just a moment but first i want to show you that on disabled disables that flag or sets the in water trigger to false and then in the update method if we're not marked as in water if we've never gone into the water then we just early exit out and we don't do anything so what does the update method do for the ball how does it clear out the object if we're using a pool versus not using a pool the first thing i do is just add and add up some time for the amount of time that we're inside of this trigger now we could use something like an on trigger stay but that's going to allocate some memory and i wanted to really get this down to zero allocations so once we go into a trigger we just say hey we're in the trigger and every every frame after that we add a little bit of time then i scale my objects up you may have noticed that those balls were growing and growing over time once they get to their full double size this code right here on line 26 will be true and then if we're in a pool we do the release which we haven't talked about that yet but we're going to in just a moment and in this case we do a destroy destroyer removes that game object and tells the allocation system hey go ahead and reallocate this thing later so why is this a problem why does this need to change and why should you care about that other little bit of pool code let's take a look at the profiler now i have my profiler window already open but if you don't have yours and you don't know where to find it it's under window analysis and profiler inside the profiler window you've got a couple of different options i'm going to expand my window out make it a little bit bigger and show some of those sections we have a cpu usage section which has a lot of stuff going on rendering that we're going to not really pay attention to today and then memory where i actually care so to get the memory view that i have right now and click on let's just click on rendering then back on memory and click on any old frame and then hit the clear button and play that should get this updating constantly and you can see that i'm allocating 136 bytes per frame and making three allocations this is actually pretty bad because eventually it's going to add up and lead to some relatively big collections and here i'm spawning little tiny spears with nothing on them it's really important to note that if i was spawning rockets that have a big model and some materials and stuff these allocation sizes are going to be much much bigger so just because my number is tiny here it's really just so i can spawn thousands and thousands of these things and have fun with it but remember if you have bigger objects this is going to matter even more so we've got 136 bytes there and if i scroll up here i can actually see that in my cpu usage as well i'll click on cpu usage and just click anywhere on one of these frames scroll up to the top of this bottom overview area and i'm just going to collapse down player loop in all of these other sections here if we sort by gc alok it might be sorted by self or total by default but if i sort by gc alok i can see that i've got 136 bytes allocated in the player loop and i can expand that out see that it's inside my script run behavior update and that means that it's inside one of my actual scripts updates or it could be one of the packages that i've pulled in script updates but it's inside of an update method of a mono behavior and i can expand this out some more and see that it's inside my ball spawners update and it's the instantiate method instantiating an object always allocates and of course is part of the big problem that we have here so let's jump on to the solution how do we fix this well i'm going to show you first what it looks like when we fix it i'm going to check the use pulling option on my ball spawner and let's take a look at our allocations we'll go to the memory section and look at those balls start to come in nice and different colored that's how you can tell that we're using the pooling we'll go select memory i'll go click on another frame in here somewhere choose that clear option unpause again and look at our allocations we are now at zero now i want to show you something else though i'm going to stop playing i'm going to check the use pulling option and watch those allocations because they're not going to start at 0. what you're going to see is that we're going to allocate for a while as we spawn in new balls new objects in here see them popping up and then eventually we stop and notice this inactive count as soon as that inactive count got above one and we started reusing things from our pool which is how a pool works then we stopped allocating memory now check this out i've got a bunch of balls there let's minimize our profiler view just a little bit and let's get some more balls spawning so i've got one ball spawner let's go with four ball spawners i'll turn on use pulling for them and enable all of them and check it out check out all those balls dropping down we're going to get some allocations for a moment and then the allocations end so once we're done with allocating we're we're done now what happens if i need to have some more balls spawn let's say i maybe make my balls live a little bit longer i'll go to my assets and go find my ball settings and i'll just crank up this life after landing instead of being at 1.5 seconds let's say i'm like hey i want these things to roll around for 15 seconds i'm going to get rid of that decimal point there let's go look at our profiler again i should see that i start allocating a whole bunch more and if i go look at one of my ball spawners and see that my inactive count keeps dropping down to around zero and then filling back up so as it gets down to zero it starts uh recreating or instantiating new balls and then when it gets some new balls in there and has some available it stops doing those instantiations we're back down to zero allocations all right time to look at the code how is this all working how does this object pool work and how can you use it without really writing much code of your own let's stop playing and open up the ball spawner script so inside of my ball spawner script i had that option for use pulling for not using pooling we just did the normal old instantiate let's just delete that and simplify this down so we have less code to read so here we now have an update that only uses the pooling system to use the pooling system we need a reference to an object pool or something that implements the i object pool interface here you can see that i have a object pool of type ball on line 15. this could be an i object pool by the way but that gives us no access to the count of active objects and i wanted to show that in the inspector so that's why i've changed this from i object pool to object pool now object pool if we hit f12 you'll see is inside of the unity engine pool namespace and it's got a couple of pieces to it it has a stack that it uses internally so if you've ever built a pooling system you know that this one by default is using a stack to create its allocations or to hold its objects that are being allocated and then it's got its constructor down here that we're going to take a look at in just a moment because there's quite a bit to it and then it has a get method to get an object and a get where we can put an object in with an out reference and finally a release and clear option the release object or release method returns an object to the pool that's essentially a return to pool method and then clear just clears out all of the objects and destroys them or calls their destroy action so let's take a look at what i mean by a destroy action and the constructor for our object pool so you see here on line 29 in our awake method we have an expression body method that just initializes the pool it sets it to a new object pool and then uses three of the parameters that we can use for the constructor let's just delete one of these commas re-add it and see if we can get the intellisense to pop up and tell us what these parameters are the first one here is the create function and this is the method that's going to be responsible for creating an object when i create a ball or spawn a ball it could be as simple as just instantiating a copy of the prefab in that case it could just be a method that we reference that instantiates a ball and returns a ball in our case though let's hit f12 and see what our create ball method does it instantiates a ball it sets the pool and returns a ball so pretty close to the same thing let's go take a look at the set pull method now set pool passes in our existing pool and it just assigns the pool right here on line 13 which is a property on line 9 and we only use this pool when we want to release the object here on line 29 so instead of destroying an object if we have a pool so if we are actually a pulled object because i'm allowing this toggle back and forth we just release ourselves by returning ourselves back to the pool so what does that do how does that destroy our object or remove our object from the game so that it's not sitting there rolling around anymore let's go back to that constructor in the constructor we have an action on git and an action on release the action on git is the method that's called when we take an object from the pool so that we can run some code on that object let's take a look at what that code does here ontake ball from pool sets the game object of that ball to active so you might have a clue of what we're doing when we return the object to the pool it also sets up a color timer so we have a color timer here that's doing that gradient and the reason for that was mostly so that i would have some interesting code to put in here and make it a little bit prettier but here we have a float of the color timer value which is just a value from zero to one or it's going to be used from zero to one we increment it by the amount of time past times the speed variable so i can adjust how fast those uh those colors change and then we set the timer back down to zero if it goes to one then on line 39 i use gradient dot evaluate to get a color between those colors so i'll show you the gradients in a moment and you can see exactly how that works jump over to unity look at the ball spawner you see i've got my gradient here that starts off green to red and then on my other ball spawners i just picked some other gradient colors so i could get a variety of balls dropping down showing just a nice smooth transition throughout the colors let's jump back into that ball spawner script and see the next part though so after we pick a color just for fun so we have something to do we get the renderer set that color and then we call reset timer and scale reset timer and scale just resets the amount of time it's been in the trigger and then resets the local scale to one i want to note here that i did have to cache this transform because calling the transform from this callback that's getting hit by reset timer and scale actually used git component which allocated 40 bytes of memory didn't make sense in a tutorial where we're trying to minimize allocation so one case where i actually needed to cache the transform it was a little bit odd but if you happen to know exactly why that happened by the way drop a comment down below i'm kind of curious to see if somebody can really explain in detail why that is so let's continue on we can take a ball from a pool and run that code but what happens when we return a ball to the pool well the first thing we do is check to see if this particle pool exists now what's the particle pool hold on a second i'm going to show you some more pooling i didn't want to keep this too simple but after we check this particle pool and do whatever this thing is doing we set our game object to not active so to answer the question of how do we disable ourselves well on return to ball gets our on return ball pool gets called and it sets our object to not active again that is assigned right here as the action on release let's take a look at some of the other parameters and then we'll take a look at those particles next so if i hit comma here you see that we've got a couple more choices i can also have an action on destroy so something that gets called when this object is actually destroyed instead of being returned to a pool which can happen we've got a collection check option which checks to see if something has already been removed from an object if you enable the collection check which is on by default it's slightly slower but will prevent some errors so how to leave it on until you decide that you definitely don't need it then we have a default capacity default capacity is going to specify how many objects to spawn on default or at default or at the startup time so if you already know hey i need at least 100 of these all the time you can set that capacity to 100 and have it pre-instantiate and pre-allocate so that all that memory is all done at once in a line and which tiny performance increase there but really so that you're not just allocating along the way you want to minimize those allocations make them all happen at once then there's finally a max size option and this one i think is important to talk about a little bit because max size doesn't prevent the pool from creating objects what it does is prevent the pull from saving objects so if you have a pool that should cap out at around a thousand objects but every now and then maybe it goes a little bit higher you could set that max size to a thousand and then if it goes a little bit higher than that it will just destroy those other objects and never recreate them i struggle really hard to find a good reason to use this thing though unless you maybe have some scenario where you've got some number of objects maybe the normal use cases there's a hundred and most of the time there are only a hundred of them but there's a weird scenario maybe it's like explosions and there's a giant explosion thing going on and you've got a hundred thousand or a thousand explosions happening in this one scene and you don't want to pull all of those so max size would make it so that the 900 extra of those get destroyed and the 100 that might get reused of those particles get pulled so i guess i can understand what it's there for it's just a little bit confusing and important to remember to wrap your head around what it does and what it does not do because it does not prevent it from creating and instantiating new objects it will however prevent it from pulling them and possibly give you issues if you set that max size too low now let's take a look at that particle pull code so on returning a ball to a pool i actually have some code in here to place some particles if it has a particle pole assigned and my particle pull object is enabled then we call particlepool.pool.get and we give it the transform position or assign the transform position to the ball's transform position right before we disactivate or deactivate our object so we're essentially here saying hey particle system give me an object from your pool and set that position let's take a look at this particle pool script real quick and then see what it's doing in action so if i hit f12 and hit f12 on particle pull you see this is actually just a straight copy of the particle system from the example code that unity provided i wanted to take this and just pull it in so you could see how you can use that code in your own system so here we've got their pooling system the particle pool with the pool type and the stacked and linked list options which you can check out the memory allocation differences on their site they kind of talk about it a little bit in detail or check out reuben game dev guru site i'll link that down below has a very in-depth detailed description or i guess discussion of how this all works internally but let's see what it's doing so our pool here is actually just a reference to an eye object pool of type particle system and here you'll see that in in its getter for this property they actually just instantiate either an object pool or a linked pool depending on which option you're choosing in the linked pool just a different way of storing the memory instead of having a list you have a linked list and then they assign the different callbacks so they have their on take from pool on return from pool on destroy pool object in case you want to do something there and then they've got their other options available and then if we look down here at the create pooled item you see that it instantiates a particle prefab from their from the prefab that's assigned and then it gets the particle system component which it actually by the way was creating a particle system component so i did make a small change here to actually use an existing particle system instead of trying to create an ugly one from code then it stops the particle sets up the timer for it and you could adjust this if you want i just left it as as they had it and then finally it adds in a return to pool component the return to pull component is one that they've also given the code for and i'll show that in just a moment find the next thing it does is assign that pool just by doing a direct assigning you'll see that in the code that i wrote i used a setpool method i just prefer that over assigning it but this is doing essentially the same thing and then they return back the particle system the return to pull just deactivates take from pull activates and on destroy just destroys the object relatively simple stuff here there's not a whole lot going on but it all just is going to magically work let's jump into unity i'm going to re-enable that particle pool here and then i'll enable my two or three other ball spawners and turn on use pooling and let's see what happens when i start to get thousands of balls dropping all over the ground bouncing around and see if i'm still getting those allocations or if we've maybe solved the problem just using this built-in object pulling system there we go we've got lots of allocations as things start to go up and up and up and then we should watch as that inactive count starts to get above zero our allocation should just disappear any second now and by the way if you try to reproduce this on your own system make sure that you actually look through the profiler and look at what's causing the allocations there we go so you saw as everything started exploding and hitting its timer all of the allocations hit and we started getting huge huge performance pause that's why you pre-allocate so that doesn't happen you get these objects already done and ready to go if that is your use case though and you're going to instantiate a ton of objects at once and you need to pre-allocate them i want to make sure that it's clear that you can't just crank up that default capacity option that you see there that's actually just going to set the size of the stack it'll help a little bit but it doesn't pre-allocate or pre-instantiate the objects you'll still need to do that you'll need to do that by either creating a bunch of objects instantiating them by getting them out of the pool and then returning them to the pool or perhaps just overriding the object pool with your own custom object pool class that has an option to pre-instantiate so that when it gets created it just instantiates some number of objects and adds them to its own pool that's an easy thing to write and i'd leave that as a quick challenge for anybody watching to add down below as a link if you write that up add a link to the gist and share it with everybody i'm sure people would like to see it and maybe compare the different solutions that you guys find before i wrap this up i want to show you one more pool type that they've added though the object pool is great and there are some really interesting pool types like a list pool and dictionary that i'm not quite sure how i'm going to use but i think i'm going to come up with something soon but there's one other pool that i think everybody is going to want to use and could make things just really easy to implement and this is the one that allows you to do things with just one or maybe two lines of code and that's the generic pool if you use the generic pool class it's actually a completely static class and you might run into the issue of accidentally adding some parentheses seeing an issue just make sure that you get rid of those extra parentheses the way that you use the generic pool is you call generic pool of type and give it any class type that you have and then call the get or release method these don't need to be mono behaviors they just have to be a class that can be instantiated so you could pool any type of object in memory whether it's a monobehavior or something else and then pull it right back out now you don't have access to a creation method and i don't think there's a really easy way to use this with prefabs either but you can use this for things that you need to instantiate or spawn throughout your game that are just running code if you've got a game object or a script that you need to attach to objects and spawn around the world maybe their sensors with colliders or other random things this could do it that other script can be responsible for setting itself up when it spawns adding its necessary components if required and then this would be your entire code for the pooling system so i think that the generic pool is going to come in really handy a lot of the time and i'm probably going to use it more and more often you'll probably see this showing up in some of my future videos the other pulling classes will most definitely be showing up in some future videos that i've got planned like the upcoming fps game that i want to do if you're interested in that stuff by the way please make sure that you've already subscribed hit the like button or just drop the comment to let me know and feel free to check out my courses and other things down below as well anyway i want to thank you for watching this video i hope that it was helpful and i hope that you have a better idea of how to use pooling in your games and how it can hopefully help you save a lot of pain trust me it was a lot of trouble with performance when it came down to just garbage collection and mobile stuff i remember spending weeks and weeks and weeks trying to change things and fix things and pull things once i decide discovered that i needed pooling trying to fix all of the different issues that come up with allocation so if you can address them easily and early especially with a built-in solution like this i highly recommend you jump into it alright thanks again for joining me thanks for watching again please hit all those buttons and special thanks to everybody who's joined the new youtube membership and the patreon system i almost forgot about those freaking awesome thank you so much i really really appreciate it all right goodbye everybody i'll see you in the next one
Info
Channel: Jason Weimann
Views: 24,220
Rating: undefined out of 5
Keywords: unity, unity3d, game development, objectpool, pooling, unity pool, game dev, object pooling, unity 3d, c#, brackeys, programming, game development unity, learn to code, object pool
Id: mRh2DA1Uzo8
Channel Id: undefined
Length: 26min 50sec (1610 seconds)
Published: Wed Nov 03 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.