Instance pooling is a design pattern which can potentially help performance in games where you are creating and destroying a lot of instances.
If your program is spawning and destroying objects very frequently, calling the Create and Destroy Events many times per step, it’s potentially a lot of extra processing. Creating instances and destroying others constantly seems wasteful. Why destroy the instances that need to be destroyed when instead you can re-use them, and avoid having to call the Create event on a New instance every time you need one?
The Code
You can download my demo code here:
The GMS2 demo is an import of the 1.4 demo, cleaned up to remove the Compatibility scripts.
Looking at the project code, you will see that I have used inheritance to make the Control and Instance Pool test implementations as close to identical as possible. I have included telemetry code, which tracks the FPS of the game while it runs, so that performance is quantifiable. In the GMS1.4 version of the project, all of the telemetry code is neatly separated from the demo code into its own Execute Code action. This makes it easy to modify the project for your own use, and get rid of the telemetry code entirely if you don’t need to quantify runtime performance, and only want the code that actually implements the Instance Pool pattern.
How it works
The basic concept of the Instance Pool pattern is simple. The goal is to minimize the amount of times we create new instances of some object in large numbers during the game. A good candidate for objects to use Instance Pooling with would be bullets, bonuses, and enemies. Instance Pooling is particularly beneficial when the object in the pool is somewhat “heavy” — that is when the Create or Destroy events cause a lot of processing to happen.
The way we achieve this goal is by re-using already-existing instances of the object. This is done by deactivating the instance rather than destroying it. We add the id
of all deactivated instances to our Pool. Then, when a new instance is needed, we first check the pool to see if an already-created but inactive instance exists, and if so, we remove it from the pool, activate it, and re-use it.
The most efficient way to create and manage our pool is with a stack. Stacks are elementary data structures in computer science. Imagine a stack of pancakes. The stack gets bigger as you add more pancakes to it. If someone orders a pancake, you take the top one off of the stack, and give it to them.
This is what’s known as Last In, First Out, or LIFO data access. This happens to be just what we need. The stack gives us an easy way to keep track of the id of all of our deactivated instances, and when we need to activate one, we just pop it off the top of the stack. This is very simple to do, and therefore very fast. In GameMaker, stacks are created by ds_stack functions. If you’ve never used them before, take a moment to read up on them. It will help you understand how the demo works.
The code at the heart of this is quite simple. It looks like this:
//First check the pool to see if it has any deactivated instances in it that we can use
if ds_stack_empty(pool)
{
//If not, we need to create a new instance. No performance gain.
instance_create(x,y,oInstancePoolBullet);
}
else
{
//If the pool has an instance we can use, take it out and activate it.
var bullet = ds_stack_pop(pool);
instance_activate_object(bullet);
//Reset the state of the instance.
//**If** this is cheaper than a new instance, we gain performance.
bullet.x = x;
bullet.y = y;
bullet.direction = direction + random_range(-20,20);
bullet.alarm[0] = bullet.TTL;
}
There’s only a little bit of other code that you need. You need to create the ds_stack, most likely this is best done in the Create Event, using ds_stack_create()
. And when the pool is no longer needed, you need to destroy the ds_stack using ds_stack_destroy()
, most likely in the Destroy Event.
Of course, we also need to push instances to the stack when they are no longer needed. To do that, we do this whenever we would ordinarily destroy the instance:
ds_stack_push(oInstancePoolPlayer.pool, id);
instance_deactivate_object(id);
So, don’t destroy instances that you want to pool; deactivate them instead. With our bullet demo, I do this by giving bullets a “time to live” and then setting an timer. When the alarm goes off, the instance’s id
is added to the stack, and the instance is deactivated. Then it just waits for the game to need it. In a full game, you would need to look at other occasions where you need to deactivate the bullet, such as when it goes outside the room, off the screen, or when it hits an object.
That’s basically it. But you may need to think about some additional concerns, which I will cover in the Findings section of this tutorial.
Running The Demo
Open the project with GameMaker Studio, compile, and run.
The Demo consists of an Instance Pool demo and a Create-Destroy demo for comparison.
Press the Space bar to spawn bullets. Press Tab to switch from one demo to the other.
The FPS is displayed at the top-left of the screen. The first number is the minimum fps, the middle is the average fps over the last 30 steps, and the last number is the maximum fps. The minimum and maximum fps reset once per second. The average is a rolling average over the last second.
In the Instance Pool demo room, hold the space bar down until the first bullets to spawn reach the end of their life. At this point, the instance pool is filled, and no more instances will spawn; instead, deactivated instances will be re-used. At this point, you should notice the FPS dramatically shoots up and stays consistent.
Findings
To my surprise, I did not find that there was much benefit to creating an Instance Pool for bullets. Create and Destroy is about as fast as Instance Pooling with simple bullet objects in GameMaker. I expect the reason for this is that the GameMaker runtime engine is not particularly fast, but even when I did a YYC build, I still saw no real advantage to using an Instance Pool, at least in terms of fps.
Over a long game session, it may be that there is a reduction in memory fragmentation since we are updating values in RAM that has already been allocated, rather than rapidly allocating/de-allocating memory when we create new instances and destroy them constantly. But I haven’t tested that, so it’s merely speculation for now. Update: This benefit of reducing memory fragmentation is confirmed by Mike Dailly of YoYoGames.
In order to show a benefit to Instance Pooling, I had to weigh down the Create Event for my bullets. To do this, I ran a simple repeat
loop to increment a variable. This is obviously inefficient and pretty pointless, but it serves the purpose of simulating a complicated object with an init script that takes some time to run. If that init script needs to be run only once, and not every time the instance is re-used, then there is a benefit and using the Instance Pool pattern will help.
When does it make sense to use the Instance Pool pattern? When there is a performance issue, and the Create or Destroy Events are more expensive than instance_activate_object() and instance_deactivate_object().
Setting up and managing the instance pool is pretty simple, but it does take some work, and incurs some overhead. It’s not worth it to incur this overhead unless there’s a tangible benefit in the form of noticeably improved performance.
Run the code profiler, and see where your code is spending the most time. If it’s heavy on Create and Destroy events, an instance pool might be just what the doctor ordered. Otherwise, you may want to look elsewhere for your low framerate fix.
Additional Considerations
As I mentioned, there are a few other considerations that you should think about when implementing an Instance Pool, which I will discuss here.
Should we Prime the pool?
In my demo, we only create new instances when there are none in the stack. This means that until we’ve deactivated our first instance, the stack is empty, and there is no performance gained by having it.
In fact, since we’re checking the stack before we create each new instance, there’s actually an infinitesimal hit to performance. When you run the Instance Pool room, we see pretty much the same fps performance as we did in the Create-Destroy room, until the first bullets start to be recycled. Only then do we see fps shoot up.
Well, what if we want the performance to be high from the very first bullet? We can do that, by priming the stack, by creating a bunch of bullets, and then immediately deactivating them and pop their id’s onto the stack.
We could create a special object that we might call oPoolPrimer, which lives for several steps, creates the instances we need and deactivates them, adds them to the Instance Pool stack, and then destroys itself when its work is done.
The best time to do this is when the performance hit won’t be noticed, such as when the game is in a menu screen. Alternately, we can create the instances gradually, over a longer time than would introduce a noticeable performance penalty.
To be effective, you will need to have a good way of calculating maximum the number of instances you need to have active in your game at any given time. Prime your pool with that many instances. That way, even when you have all of them active, you aren’t creating new ones, and your fps will stay high.
Trim the pool?
One of the downsides of the Instance Pool is that all those deactivated instances take up resources. Even though the CPU may not be spending as much time with those instances, they do incur some processing overhead to manage even when disabled, and as well they use some RAM.
Depending on your game, you might have a moment when you need a huge number of instances active, but in other parts of the game the action isn’t as intense, and you won’t need so many. For example, in my demo, I spawn 100 bullets per step, for 20 steps, resulting in some 2000 instances max if you hold the space bar down the entire time. But if you don’t need to shoot all the time, all those disabled bullets end up in the stack. Do you really need 2000 disabled instances? If there isn’t going to be as much shooting going on, you might not. We can free up resources by trimming the stack every so often.
One way to do this would be to set a recurring alarm so that every few seconds, the stack is trimmed if it goes over a certain size.
///Alarm0 Event:
while ds_stack_size(pool) > trim_limit
{
//remove the top instance from the stack and destroy it
with ds_stack_pop(pool) {instance_destroy();}
}
alarm[0] = reset;
This will keep the stack small when it doesn’t need to be so big.
Delete the pool?
If you delete the stack, you should realize that any disabled instances that we were tracking in the stack still exist. They will continue to do so until the game ends, or you change rooms. If you don’t change rooms right away for some reason, you may want to run through the stack before you destroy it, popping all the instances out of them so that they can be destroyed as well. Otherwise, those inactive instances will continue to take up resources.
while !ds_stack_empty(pool)
{
//remove the top instance from the stack and destroy it
with ds_stack_pop(pool) {instance_destroy();}
}
ds_stack_destroy(pool);
Benefits in network multiplayer games
This comes from GMC Forums user JuJu: There’s a nice implicit benefit of instance pooling: It makes handling multiplayer netcode for complicated scenes easier if you can refer to instances by a common index across all machines.
Hello, I am a GMS2 user from China. Can I translate and repost this article to my blog? There is almost no Chinese tutorial on the GMS2 object pool design on the Internet. Looking forward to your reply:)
Hypn0s
If you look over on the right side of the site, there’s a Google Translate control which you can use to select whichever language you would like to have the site machine-translated into.
I hope this is helpful, but if the automatic translation isn’t good enough, feel free to translate this or any other article you wish. That goes for anyone else, too.
If you publish a translation of any article, please attribute me as the original author, and provide a link to the original article. Thanks!
Chris