ObjectPool - Grisgram/gml-raptor GitHub Wiki
Object pooling is a very important technique to save precious memory and time.
Imagine a shooting game, where hundreds of bullets are created, swoooshing over the screen for a fraction of a second and then explode or disappear. This would mean hundreds and hundreds of "Create" and "Destroy" events, permanent memory allocation and could throw your garbage collector into a serious depressive crisis. Don't do that.
Even with smaller amounts of instances, something like little critters that run around in your level to make it look more "alive" and disappear after some time or any imaginable "recurring" type of objects: Why should you always create new instances? Wouldn't it be better, to just "take them out of the scene until you need them again"? Without having the full overhead of creation and destruction every time?
This is, where object pools come in. Imagine them as a kind of storage box, where you put your objects in, and you take some out if you need them, and put them back in, when you do not need them now. You put them back in, you do not throw them into your trashcan. That's the difference.
Now, the object pools in raptor
are super easy to use and cause no overhead for you as developer. You just use something else, instead of instance_create_layer
.
GameMaker helps us here natively with its mechanic to "activate/deactivate" objects. This is, what the object pools use. Deactivated objects do not cost rendering time (in contrast to "parking" unneeded objects outside of the visible area). Deactivated objects receive no events, are not in the render loop, they keep existing and don't get deallocated. That's all. And that's good.
There are two main methods, which will simply replace every instance_create_layer
and instance_destroy
calls in your game. That's all you need to do. Just replace the methods used and your objects are pooled.
This is the replacement for instance_create_layer
and instance_create_depth
.
/// @func pool_get_instance(pool_name, object, layer_name_or_depth_if_new, _init_struct = undefined)
/// @desc Gets (or creates) an instance for the specified pool.
/// NOTE: To return an instance later to a pool, it must have been created with this function!
/// In the rare case, you need to manually assign an already existing instance
/// to a pool, use the function pool_assign_instance(...)
/// The optional _struct will be sent as argument to onPoolActivate (which gets called ALWAYS,
/// no matter if this is a fresh instance or resurrected from a pool)
/// @returns {instance}
function pool_get_instance(pool_name, object, layer_name_or_depth_if_new, _struct = undefined) {
You supply a pool name (more on that below), specify the object to take from the pool, and in case, a new instance is needed (no more objects of this type in the pool), you specify either a layer name or a number as depth as the third argument. If you specify a number here, instance_create_depth
will be used internally, otherwise the object is created through instance_create_layer
.
The final argument, the _init_struct
is a struct which will be forwarded to the onPoolActivate
callback the object will receive.
We can not use the instance creation feature of GameMaker here, as the instance does already exist, when it is taken out of the pool. Instead, this struct is always sent to onPoolActivate, even if the pool created a new instance. Read more about this callback below in the callbacks section.
Here's an example:
var next_bullet = pool_get_instance("Bullets", obj_bullet, "bullet_layer");
Instead of destroying your instance, you send it back to the pool with this function. It knows, from which pool it has been taken and will be returned to the very same pool.
This function also supports sending any struct to the onPoolDeactivate
callback into the object before it gets deactivated.
pool_return_instance(instance = self, [_struct})
Here's an example for the bullet above. Let's assume, we are in the "leaves room" or any other event, where the bullets shall stop rendering.
/// @func pool_return_instance(instance = self, _struct = undefined)
/// @desc Returns a previously fetched instance back into its pool
/// An optional _struct may be supplied as parameter to the onPoolDeactivate callback.
That's it for the most simple usage of object pooling.
There are some additional helper functions available which might help here and there in some specific situations.
/// @func pool_return_or_destroy(instance = self, _struct = undefined)
/// @desc In highly dynamic games, it may occur, that you don't know whether a specific
/// instance has been aquired from a pool or not. In this case, this function is very handy,
/// because it checks, if it's possible to return it to its pool, or just destroy it.
function pool_return_or_destroy(instance = self, _struct = undefined) {
/// @func pool_is_assigned(instance = self)
/// @desc Checks, whether the instance has a pool assignment (i.e. "pool_return_instance" may be used)
function pool_is_assigned(instance = self) {
/// @func pool_assign_instance(pool_name, instance)
/// @desc Assign an instance to a pool so it can be returned to it.
function pool_assign_instance(pool_name, instance) {
/// @func pool_get_size(pool_name)
/// @desc Gets current size of the pool
function pool_get_size(pool_name) {
/// @func pool_clear(pool_name)
/// @desc Clears a named pool and destroys all instances contained
/// @param {string} pool_name
function pool_clear(pool_name) {
/// @func pool_clear_all()
/// @desc Clear all pools.
/// NOTE: The ROOMCONTROLLER automatically does this for you in the RoomEnd event
function pool_clear_all() {
/// @func pool_dump_all()
/// @desc Dumps the names and sizes of all registered pools to the log
function pool_dump_all() {
But of course, there's more. Because at the moment, we have taken one very important step out of sight with these pools: The Code in the create event! What, if we need to set up things every time a bullet appears? If the create event does never run again, how can we do that?
The answer is quite simple: The object pools of raptor
utilize two callback functions: onPoolActivate
and onPoolDeactivate
which you implement in your create event of the object.
Callback | Replaces | Invoked when |
---|---|---|
onPoolActivate(_data) |
Create |
Object is created OR reactivated from pool. The _data struct is the struct you supplied through pool_get_instance . |
onPoolDeactivate(_data) |
Destroy |
Object is removed from the scene. The _data struct is the struct you supplied through pool_return_instance . |
This callback is invoked every time, an object is taken out of a pool, even when a new object is created.
So, for pooled object, you put your initialization code in this function, and all is good.
The exact timing of this callback is "just after it has been activated or created", it is already in the scene when this runs.
These values are applied to the object by default, before onPoolActivate
gets invoked:
The x
and y
position of the object requesting this object from the pool. So, if you player object requests a new bullet from the pool it will by default be placed at the player object's position. That's why you may want to adjust that in your onPoolActivate
callback.
If the requesting object can't be determined (self
does not point to an object instance), no coordinates get applied.
Important
onPoolActive
also gets called, when a new instance is created!
This makes the function a complete replacement of everything, you would have done in the Create
event of non-pooled objects.
Just do your "Create" in this function.
Here's an example, which also demonstrates the usage of the _data
parameter:
onPoolActivate = function(_data) {
PLAYER_OBJ.ammo--;
x = _data.target.x - 30;
y = _data.target.y - 30;
image_alpha = 0.5;
image_blend = _data.overlay_color;
// ...and a thousand other init things you may want to do
}
This callback occurs, whenever an object is put back into a pool. The exact timing is "just before the object gets deactivated", so it is still in the scene when this callback runs.
onPoolDeactivate = function(_data) {
global.achievements.bullets_fired++;
image_alpha = 0;
}
REMEMBER: Both of these methods should be placed in the Create
event of your object!
In theory, you can put all your pooled objects into a single pool. In reality, you might not want that. I suggest, that you create one pool (i.e. use a unique pool name) for each type of pooled object, like "Bullets", "Critters", "Sparks", ... etc.
The more different types of objects you put into a single pool, the longer it takes to find an object of the requested type. If all your pools only contain one single type of object, you run the pools at maximum performance, as every request can always take the first available object out of the pool, it never has to search through the pool to find an object of the requested type.
I can not recommend enough to use those object pools! They make a significant difference in the performance of your game.