Unity Object Pooling: Bullet Holes (GameDevHQ)

Unity Object Pooling: Bullet Holes (GameDevHQ)

Show Video

Let's talk about object, pooling this. Is our third and final tutorial, to this mini series check, the description to get caught up in unity. We have specific, methods that help us create and destroy objects, specifically. The instantiate, and destroy methods they're useful and necessary methods, during gameplay and each generally, requires, minimal, CPU time however. For, objects, created during gameplay that have a short lifespan such. As our bullet holes and get, destroyed and vast numbers per second the CPU, needs to allocate, considerably. More time, unity. Uses what's called garbage, collection, to deallocate memory that's, no longer in use repeated. Calls to destroy, frequently. Trigger this task and this is the reason you'll experience a framerate drop during the cleanup process of, your bullets especially. On WebGL, builds our mobile, object. Pooling is the process, of pre instantiating, all of your objects you'll need at any specific moment, before gameplay, begins during. Loading in a wake or void start is where you'd initialize, your pool you'd, populate, a set of bullet holes that can be used and recycle. Through that list if we happen to run out then, we can dynamically, create more at runtime in this. Video we're going to approach object pooling and the most simple to understand way using, a list of objects and the set active feature of unity this, is a very simplistic approach perfectly. Fine to be used in the industry and requires minimal setup for, a more modular, approach you can check out the unity c-sharp survival guide which, has more in-depth, knowledge and implementation. Of creating. Modular, object pools let's, get started. I'm. John, with gamedev HQ and we bring you amazing content three times a week dedicated, to mastering game development smash, the like button and give us a subscribe and join our community down, below, here. We are in unity where we left off and currently. What's happening, is I can fire my Gatling gun and, we're basically creating a, ton, of bullet, holes of varying kinds, so. What we want to do is pre. And. Add them to a list that we can recycle over, and over again we're also going to need to define a bullet hole behavior, that's going to clean up our bullet holes by basically turning them back off so that we can reuse them within our list so the first thing that we need to do here is we. Need to create basically, a pool, manager script, that's going to hold our list, of predefined, bullets, so. To get started with that we're going to create an empty object and, we're going to call this our pool manager now that we have a pool manager created, we can create a script, that's going to manage, our objects pool so let's create a new C sharp script. And. Let's call it pool manager and let's attach it to our pool manager open the script in Visual Studio. So. Here we are inside the pool manager and let's, take a look at what we need to do inside. This pool manager we need to know what, object, are we going to pre instantiate, so, here, we can say what, object. To. Instantiate so, we need a game object variable. That's going to store a reference to, whatever object we want to instantiate the. Next thing we need is a list, of objects. That we can use so, we're going to need a list of objects. That can, be used so. We're gonna instantiate. Say 20, bullets and then we're going to store those bullets, inside of this list and that's going to be the object port list that we can use if we have to expand, it will, support that when we get to it so, all we have to do here is let's start filling this in what object to instantiate we have a bullet prefab so, before we get into supporting. Multiple objects, let's just focus on pooling one bullet, so, here we're going to say public, game objects and we're, gonna just create a bullet hole prefab. This. Is the object that we're going to instantiate, several, times and add to a list to be used so, bullet hole prefab and then, here we have a list of objects that can be used this, is going to use a, list of game objects so here game. Objects, and then. Let's call this bullet, hole list. So. This is going to be our bullet hole list that we're actually pulling, the, bullet holes from and this is going to define what those bullet holes are. Let's. Save the script and head back to unity, and, on. Our pool manager we should be able to now add in some, bullet holes, so. Here you'll see here that we have a bullet hole prefab I'm just gonna add the bullet hole and then. Here we have a bullet hole list and this, list is gonna be dynamically, generated, so we can predefined. How many bullet holes we want to spawn now, this gun probably, shoots around 50 to 100 rounds per. Second, if I had to guess so.

What We're going to do is we're gonna probably instantiate, maybe 50, bullets, and we'll see how we are with that so. Let's create a variable to determine, how many we'd like to instantiate so. Here we can say public, int will. Say spawn, count, and. Whatever that value is assigned to in the inspector is how many bullets were going to pre define. And instantiate that's. Actually do that we need to do that inside of our initialization, method or during, our loading so, it can be done in a wake or void start. So. Inside a void start here what's, the logic we need to spawn, we. Need to spawn. Now. Whatever the spawn count is right so if it's 50 we need to say spawn 50. Bullets. Add. Them. To the bullet, hole list that's logic here spawn 50 bullets add them, to the bullet hole list so how do we spawn the bullets we know we have to use the instantiate, method we, can say instantiate, and then, here we can say bullet hole prefab and we. Know that this is actually going to instantiate, the, bullet holes but now how do I instantiate 50, of them well. Instead, of typing out instantiate, over, and over again we, can use a for loop so with. A for loop we can say let's, iterate through this 50 times and perform, a task 50 times so. Here we're gonna say for int I equals 0, which. Is our index counter while I is less, than the spawn count so if it's 50 we're gonna cycle, through 50 times and we're gonna run some tasks. So while I is less than spawn count after we instantiate we, need to say increment, I so that. Eventually I becomes, greater than 50 so. Here, we, are going to say instantiate. This is going to be ran 50, times and we. Want to basically just instantiate our bullet-hole, prefab, now this, right here it's gonna work I'm gonna spawn 50, bullets just like this the problem though is I now need to add the, bullet that we spawn, to, this bullet hole list in. Order. To do that we, need a reference to the game object we instantiate to. Do that we need to store the game object that's created inside. Of a. Variable, of type game object, so I'm gonna create a local temp, variable, of type, game object down here so we're gonna say game object, and we're gonna say bullet. The instantiate, method and that's, going to assign, the, bullet we created, to this reference variable, right here so I can actually change properties, the position, and so forth of that bullet that we spawned the. Next thing I need to do here is I. Need to now take the. Bullet. And add it to the bullet hole list so, let's access that list so. Here we're gonna say bullet hole list. Dot. Add and. We're. Going to add a game object you see here on the print that say it's looking for a game object and the game object we add is our bullet so, we spawned 50 bullets and we add each bullet, to the list let's, save this and we can head back into unity now, as of unity 2018, you no longer need to cast bullets. Or, instantiated. Objects, as game objects, however it is still best practice to, commonly, cast them as a game object or whatever it is you're instantiating, so. Here cast of a game object alternatively. You could say instantiate. As a, game object and this. Is still true if you're using things like transform. Or. Rigidbody. For example when you're instantiate however, as of, 20 18.3. With, the game object keyword you no longer need to provide, your cast but I'll keep it in there for best, practices, so.

Here We have bullet equals instantiate, bullet hole prefab as a game object and then here we're adding that game object to our bullet hole list. Let's I'll run this and let's see how this looks so. Here we are you'll see here that our spawn count is zero so our bullet hole size list is going to be zero now, let's change the spawn count to say 50, let's. Run this and. You'll. See here that inside, of unity it created 50 bullets in the hierarchy as well, as check out our bullet hole list this, is our pool that we're now going to go through and use these values now the, next thing we need to do here is let's clean this up I want, these bullet holes to be a part of the pool manager not, just instantiate blindly, in the hierarchy. To. Do that we can modify the parent, object and say equal, to, this, object so to, do that we add them to the list and then, here we can say bullet got, transformed, out the parent and the parent is, the transform, of that object and I'm gonna say your parent, is now this object that the script is attached to which, is our pool manager so, I'm going to say equals this. Dot, transform, which, is whatever script, or object, this, script is attached to so. By typing bullet transfer not parent every, bullet that is spawned is now going to be a child, of the pool manager so. It would spawn count is 50 and when we run it you'll see now that our pool manager has 50, bullet, holes and you, can further clean this up by adding an empty game object here, for your bullets and then, using that as, the parent object we. Have our pool the, next thing that we need to do is actually turn, off the. Object because the goal here is that when, we have 50 bullets in our scene whichever. Ones are turned off we're going to turn them on so, you can see here that our pool manager they're all enabled, we need to actually disable, them and then spawn, them or I'll give the illusion of spawning them by enabling, them, at the position, we need to so. After, we initialize, them and, we add them to the list and set their parent let's, now turn them off so, here we're going to take the bullets and. We're going to say dot set active and we're gonna set them to false that is going to turn off the, bullets. Let's. Save it and run it again and you'll, see here that our bullet hole are. All turned, off here's all the bullets and they're now off what, we can do now is we can cycle through this list and, say hey is this bullet off then, what we need to do now is turn it on now, we're going to process the logic for actually, creating. These bullet holes so, we have our poor list but, what we need to do now is look back at our script, where we instantiate bullet, holes initially, instead, of instantiating bullet holes what, we now need to do is, iterate. Through. The bullet, hole poor, list so. We need to iterate through the bullet hole pool list find. An object. That's, let's. Say active. Equals, false and then. What do we need to do with it we need to set, it set. It to enable and then. Change, the position, of the object position. Of the object, matches. The hidden foe point, right and then. We also need to adjust rotation, so, adjust rotation to. Surface, normal. So. We have the pseudo code in the logic let's now start it out we're no longer going to instantiate the bullet holes so I'm gonna just comment, this line out instead, we need to use our objects, pool the, first thing we need to do here is we need to get a reference to, our object pool so, in order to do that the, best practice, scenario is to turn this into a singleton, however. We don't have enough time for that and you can check that out in the c-sharp Survival Guide but. What we're going to do here is we're going to create a reference to the pool manager so. Here at the very top we're going to say private, pool manager to create a handle to it so.

Here Private pool manager and let's call this underscore pool and. Then. Inside, of void, starts. Where, we initialize, things we'll get a reference to the pool manager or assign the handle to that object, so here we're gonna say underscore, pool equals. I'm going to find the object so. Game object not find and, alternatively. You can actually use game. Object, dot, find, object. Of type and that's going to search through the entire hierarchy and find an object of type object. Pool or, pool manager. And. That's going to give us a reference to that script let's save this and now we have access to our pool manager, okay. So down, here, what, do we need to do well, we need to iterate through the bullet hole pool list so how do we iterate through we use a for loop so. Here we're gonna save for. I mean. Here we say for int I equals zero. Well, I is less, than what what are we iterating through well, the pools length so, here we're gonna say pool dot. Bullet, hole list, dot. Count so. While I is less than the count of however many bullets are in the pool and, then we're gonna increment I once we perform whatever task we need to the, next part here is to find an object that's, active, is equal to false in this list so how do we do that we need to now check if, the current, bullet, we're looking at is set active to false to, do that we can access the bullet by, saying the pool dot. Bullet hole list and then, we want to access the current bullet we're looking at which is done through the index of I so. That's the current bullet and what, I want to do now is I want to say if this bullet, dot, active. In hierarchy, if it's active, in hierarchy we can't use it but if it's not we can so, I'm gonna say here if active, and hierarchy is set to false if, that's the case then. We need to set it to enabled, so, here we're gonna say underscore pool bullet. Hole list. Sub. I dot. Set. Active, and the, answer is going to be true so that bullet hole is now going to be true let's. Just run this and see what happens here, what's. Gonna happen here is when I raycast. When, we call that shoot method it's going to iterate through our bullet holes and turn them all true, all right so here we are in unity and you'll see here that if I start shooting as, soon as I shot all of them become enabled now that's not what we want instead we need to limit it to just one becoming, enabled, per shot and as, this, method gets called it, will continue to cycle through so.

After, We set one of them true we need to stop the operation we just want to do one out of time and that's, where a break keyword comes into play this is going to allow us to break out of the loop early and move, on so, now we can go ahead and test this out let's save this let's, hop back into unity, and let's add a break. So. Here is our pool if I, shoot you'll see here that only a select few became, enabled because of how many shots, per second if I shoot again more, become enabled and that's, the process of object pooling what, we need to do now is actually display. Them but then also clean. Up Andrey, disable, these previous, holes so, we need to give them kind of like a lifespan so here we're breaking them and let's take a look at what's next to do here, we set it to enable let's position the object at the hiddenville point so now that we actually have, the object pool created, before we break out of the loop let's set the position at the hip point so, to access, it we can say pool. Let's. Access the current bullet we're on, which. Is our eye index, and then, here we're gonna grip the transform, and we're, going to get the position so transform, dot position, and, we. Might want to get this in local space since it's a child object so here we'll say transform, dot local, position, equals. The hidden foe dot. Point and last, but not least we need to adjust the, rotation so, here once again will say pool. Dot. Bullet hole list I and. Then. We need to access the rotation so, dot transform. Dot rotation. Equals, the. Quaternion, dot look, rotation, and. That's. Going to assign, it to the hidden phone normal. And that's going to adjust the rotation for us, now. As an, added challenge to you guys tell. Me how we could clean, this up to where I don't have to type out under scroll pool bullet, holes list I every. Single time if you know the answer show. It off in the comments. So. Now that we have this let's test it out head back into unity, and let's see if we can spray our bullet holes, so. You'll see here that I'm firing and they're, not showing, up as intended. So, let's see here if we go to if. We find this bullet hole you'll see here that it's it. Is plastered, but it's plastic behind, the wall for some reason and that may be because I actually modified. The local position and I shouldn't have we actually want the world position of those guys to be equal. To the hidden vote position, so, that was my mistake let's go back here let's rerun, this and. We should have our object pool system working now here we are in our pool manager let's watch this in the hierarchy and as, I fire we, have bullet holes now. What's gonna happen once I run out though I'm out and there's no more bullet holes to spawn so what we need to do is we need to set a lifespan for bullet holes we also need to create way, more but. Then also we need to support, the ability, to have, more, bullet holes if we need them so. To get started with this let's first create a behavior for, our bullet holes I'm. Going to create a new C sharp script here and let's, call this bullet, behavior. Now. This bullet behavior the only object, or the only function.

The Script is going to have is the ability to turn itself off after say 2 seconds, of being displayed, so. On our actual bullet prefab, we need to add this in order to now add it to the prefab, we need to select the prefab which is like bullet hole and let's open the prefab up in 2018. Dot 3 this is new we need to open the prefab up and then, I can actually add our, script, to it which is going to be our bullet behavior. Inside. Of our bullet behavior, all we're, going to do here is we're just going to disable, the object we're not going to use update and all, we're gonna do here is we're just going to, turn. Off the. Object so I'm gonna turn void start into an ie numerator, so, that we can actually yield, events within start and here. We're gonna say yield, return. New. Let's say wait 4 seconds 2 seconds so wait 4 seconds 2.0. And. Then. We're actually going to want to use on enabled. Because start is only going to be called once but we're gonna reuse this object several. Times so, here we're actually going to do you would return new waitforseconds 2.0. F and, then, let's change this to on an able, now. On enabled, is called. Automatically. By. Unity, before void, start it's whenever you turn off an object if you turn it off on the, say board gets calls if you turn it on on a name what gets called so when we enable, this object we're, gonna say wait 2 seconds and then we're. Just going to turn it off we're gonna say this. Dot transform, dot set. Active or, this, transform game objects, dot, set active off, we're, just gonna turn us off once we become enabled after 2 seconds that's, going to allow us to be recycled, and go back into the pool because, our pool is looking, for. Or. Disabled. Objects, so, now that we have that behavior let's, run, and, we. Should see here on our pool manager that after two seconds our first, initial, fires are going to get, separated now you'll see here that on unable cannot be occurred team and so what that means is we just need to invoke a method here, so let's call will, call this void, disabled. And. We'll. Set this back to void on enabled, and then we're just going to use an invoke, method so here we have an invoke and you, can specify the method to invoke which is on disable. Followed. By a time frame so two point zero F, and then here we're using to use the proper thing invoke. Okay. So that after two seconds we're going to call this on disable method in the same logic applies we're. Gonna say this but, transform, dots game object dot set active is false. We'll. Save it head. Back into unity. Let's. Run this again and you'll. See now that our bullet holes are going, to actually. Recycle. So, here inside of our pool manager we have the first initial five and they, turn off and now we have an initial five again we can constantly reuse, these in its super, optimal, the. Next task here is to dynamically, extend. This, pool so. Now that we have our bullet-holes recycling, let's take a look at how we could expand, it if we needed to inside. Of our pool manager we have logic, here that says for, int I equals zero while I slipped in the spawn count we're spawning all of them now, inside of our look at Mouse script is where we're actually doing, all of the operations, here, is where, we're checking if active. And hierarchy equals false well what happens if there. Is no, active and hierarchy so, here let's say here we iterated through the entire list and if. The pool is active, there's. Not a single one active right, let's, say here that we got through, the list and not a single one was active. What. That means is that, we, need to now dynamically. Create, a bullet-hole add it, to the pool list and allow, it to be recycled in the future. We're. Gonna use this if else block here we're gonna say if we found something active in the hierarchy fantastic. Else. We. Need to know check, and do something now what is that we need to check we need to. ADEs a new, bullet however. We only want, to create a new bullet only. If. We've. Psyche only, if we're, on the last in the list only if we're. On the last item, of the list we. Need to know that you cycle, through the entire list and couldn't find any bullets to reuse if that's, the case we'll, create a new bullet only if, we're the last item on the list so how do we check it for the last item on the list well it's, our bullet count so here, we. Can best check if it's the last item we, can say here if, underscore, pool, dot. Bullet, hole. We. Can say here if the bow hold the current bullet hole that we're on if underscore pulled a bullet hole list we'll, say and actually, what's gonna tell us if we're on the last one here is I so.

Here, We can say if I. Which. Is the indexer, is less. Than or equal to I should say if equal, to so if I is equal to our, bullet, hole or list counts, so. Here we're gonna say our pool. Dot. Bullet, hole list dot count then, what does that mean. Now. It's never going to actually. I think it will actually eventually equal, and that's when it's gonna get out of this for loop and the. Problem with that is that it might not actually run so I need to know for on the last one so count is fifty however a numbers start at zero while I is less than fifty it's gonna run if I equals fifty it's not going to run so we need to actually subtract, one from count, so that it's 49 so, if I is ever equal to the last one, then, that means last. Bullet, and if. We got into this else statement that means that the last bullet was turned off so, what we need to do is now create, a new bullet so. We need to actually instantiate. The object so. We're going to create a game object we're. Gonna say new bullet then. We're going to set it equal to. The. Information. Inside of our pool manager so, new bullet equals instantiate. We're. Going to instantiate the new bullets which, is the bullet prefab so here we're gonna access our bullet hole prefabs. And that, bullet hole prefab is inside of the pool list so here we say underscore pool dot bullet, hole prefab, we're, instantiating that, guy we're. Gonna instantiate, it as a game object and. Now, that we've been sent she ate it as a game object we need to now come down here we need say new bullets dot. Set active is false. We. Need to parent the object as well so. Not only are we turning the object off but we now need before we do that we need to set the parent we need to say new bullet that. Transform, adopt parent, equals, the pool parent so underscore pool dot. Transform, and. Let's. Take a look at our pool manager we also need to set the object to off and, we need to add it to the list that's, the most important part so. Now that we've done that let's add it to the bullet hole list so, here, and set active false we're gonna say. Bullet. Hole prefabs, list. So. Here we're gonna access, the pool dot.

Bullet Hole list and. We're gonna add a new object to it and that's gonna be the new bullet, okay so. That is how we dynamically. Create a bullet on the fly and if. We run this and test it out everything. Should be okay. So. Let unity compile let's check out the pool manager here and, let's. Check and see as our we'll be able to dynamically, see the size of our list increase, here. So. We're at 50 and, you'll, see here as I fire we're increasing. Our bullet count to 97, and we, can't get past 97, because we're recycling, old bullets. So, at most in my game I'll only ever create 97. Bullets in a recycle, thanks. To all of our awesome, plus, and professional members you guys are the reason we create this amazing content every week for you and special. Thanks to Laurie DK 6, lucky. Ducky 10 and OJ, Zack you guys Rock.

2019-01-03 17:58

Show Video

Comments:

Question for you: This mini-series is over. What do you want to learn NEXT?

+Дмитрий Котусев No problem, let us know if you have any other tutorials you want to see!

+GameDevHQ Thanks a lot.

+Дмитрий Котусев Yeap!!! Right here: https://youtu.be/Xr2qQI_GDn4 - Let us know if this is what you needed!

Don't really sure, whether it has already appeared on your channel or not, but I would really appreciate if you tell something about object destruction including partial destruction of objects in unity (is it possible, if it is then how).

Id really like to see an AI state machine, quick question though - would it be easier to use a queue instead of a list when doing this?

Thank you so much for this Tutorial. It is a great help.

No problem, thank YOU for watching! Let us know if there is any other tutorials you want to see!

Thank you sir!!!

You're very welcome! Let us know if there is any other tutorials you want to see us make.

In that cycle on 16:10 you could just declare a variable and make it equal to the element in the list so both element in the list and variable would point on the same exact object in the heap. If your variable has a short name then the whole structure becomes shorter: { GameObject buff = _pool.bulletHoleList[i]; buff.SetActive(true); // And so on } Can't find any simplier and more efficient way to do it. May be just write a special method to return object of that list by index, but it seems too complicated for this situation.

YES!!! Great work.

Thanks for the suggestion! And to answer your question, would it be easier to use a Queue. This comes down to the desired behavior. Does it "matter" if not every item in the list is used? For bullet holes, they're all the same, maybe some vary, but does it matter that 1 bullet hole may get used again before the last one in the list? If that matters, then a Queue would enable you to use EVERY bullet in the list before re-using the same one, and could actually be less optimal on performance in the beginning since, you are refusing to use an old bullet before reaching the end of your queue. Does that make sense?

Thank you for the tutorial on object pooling, very , very helpful. You did a great job explaining everything and that is exactly what I needed.

We're so glad it helped! Let us know if you're looking for any other tutorials, and of course be sure to catch the rest of the series from the beginning! :)

Procedural Generation Water

Thanks for the tutorial, very helpful, as for the abbreviation part when you're calling the element [i] from your list. I guess you can save it as a variable first such as: var bulletHole= _pool.bulletHoleList[i]; then call bulletHole everytime you want to reference your element i in your list. Such that: bulletHole.transform.position = hitInfo.point; bulletHole.transform.rotation = etc etc.

You can change it to Foreach loop so you dont need to know the index

Thank you.

isnt it better to let the "creation" of bullet holes to be managed by the PoolManager?

oh,i mean all the process of enabling the holes... but yeah,with a solid logic it really dosnt matter XD its the same

For when we run out of usable bullets? And we need to create a new one? It could be, but seeing as it makes sense to create one on the spot, as long as we assing it to the pool list, it doesn't matter. The logic flow is solid.

i liked the video because of your efforts , however i believe that your pool system can be better than that , first because of the forLoop : passing throw more than 50 element every single shoot its not a good thing to improve the fps , rather than that you can use the indexing to pass thew all the objects , also the loop manager do nothing except the first instantiation : i think the whole work done inside of the pool manager would be better , and all you have to do in the shoot script is to call a simple function inside of the pool manager (to have clean script) thank you for your video and we will be waiting for more

Great suggestions to improve the system!

Thanks for watching!

Nicely done!

Possible Unity Bug when Instantiating and adding to a pool while using a for loop. Using Insert(0, prefab) seemed to workaround this issue. Here is an example: Commented what causes crash. int count = 0; for (int a = 0; a < _pool.ObjectPoolList.Count; a++) { if(_pool.ObjectPoolList[a].name == SecondaryBomb.name + "(Clone)" && _pool.ObjectPoolList[a].activeInHierarchy == false) { count++; } } if (count < clusterAmount) { for (int b = count; b < clusterAmount; b++) { newBomb = Instantiate(_pool.ObjectPoolPrefabs[0]) as GameObject; newBomb.transform.parent = _pool.transform; newBomb.SetActive(false); //_pool.ObjectPoolList.Add( newBomb); ****************************************************** Causes Unity to Crash :( _pool.ObjectPoolList.Insert(0, newBomb); // Use Insert at Index 0 instead of Add to avoid Crash } } for (int i = 0; i < _pool.ObjectPoolList.Count; i++) { //Debug.Log("List Iteration: " + _pool.ObjectPoolList[i].name); //Debug.Log(_pool.ObjectPoolList[i].name); //Debug.Log(empty.name + "(Clone)"); if (_pool.ObjectPoolList[i].name == SecondaryBomb.name + "(Clone)" && _pool.ObjectPoolList[i].activeInHierarchy == false) { Debug.Log("Cluster Amount " + clusterAmount); Vector3 pos = Random.insideUnitSphere * clusterRadius; // Get a position inside the Sphere Radius _pool.ObjectPoolList[i].SetActive(true); _pool.ObjectPoolList[i].transform.position = new Vector3(contactPos.x + pos.x, contactPos.y + clusterRadius + pos.y, contactPos.z + pos.z); _pool.ObjectPoolList[i].transform.rotation = Random.rotation; } }

Nicely done!! :)

I have to say that object pooling did make a significant performance increase on the script I was working on. I was previously instantiating 30 gameobjects with colliders and 30 explosions to go with it and would get noticeable slowdown from instantiating them and destroying them over and over. Now its rock steady.

That's awesome! Way to go! :)

Other news