Engine Code Breakdown - Object Pooling in ImpactJS
A downloadable book
What is Entity Pooling?
Entity Pooling allows Impact to re-use Entities that have been killed in the game already, instead of creating completely new ones.
When Pooling is enabled for an Entity Type and an instance of that Entity is removed from the game world (through .kill()), this instance is put into a Pool. The Pool is essentially just an array that collects these killed Entities.
Now, when you want to create a new Entity, Impact first looks into the Pool to see if there already is an Entity of the same Type available and ready for reuse. If an Entity is available in the Pool, some of the properties are reset and the Entity is removed from the Pool and put back into the game. If there's no Entity in the Pool, Impact constructs a new one, as usual.
Why Pooling?
Sometimes you have an Entity Type that has a short lifetime and is spawned and killed very rapidly. Some examples include all sorts of particles, projectiles from guns or even some types of enemies.
Creating and releasing an Entity can be a costly operation, especially on lower-end hardware. Each time an Entity is killed and not re-used, it becomes garbage and will eventually be collected by the JavaScript Engine's Garbage Collector. Garbage Collection takes time and can cause some intermittent pauses in your game, so you should try to create as little garbage as possible. Pooling helps with that.
Pooling only makes sense for Entities that are frequently created and removed again. For instance, enabling Pooling for your Player Entity probably won't make any difference at all, because there's only one instance around at all times, and Player death doesn't happen frequently (in most kinds of games anyway).
On the other hand, if you have a Bullet Hell Shooter, where hundreds of bullets are spawned and removed again, by all means use Pooling.
More about object pooling best practices
Object Pooling is a great way to optimize your projects and lower the burden that is placed on the CPU when having to rapidly create and destroy GameObjects. It is a good practice and design pattern to keep in mind to help relieve the processing power of the CPU to handle more important tasks and not become inundated by repetitive create and destroy calls. In this tutorial, you will learn to use Object Pooling to optimize your projects.
Using object pools can effectively reduce the overhead of object creation and avoid frequent garbage collection, thereby optimizing the fluency of the game.
Object pool optimization is a very common optimization method in game development.
A lot of objects such as bullets, NPC and special effects. in the game are constantly being created and removed which is very costly and may cause memory fragmentation.
In HTML5 games, the javascript garbage collection will use lots of CPU and is likely to cause a stuttering or laggy issue upon garbage collecting.
The object pool technology can solve this problem well. When the object A is removed from the game scene, it is recycled to the object pool, then when the same type of object is needed, A is directly taken out of the object pool for reuse.
The advantage is that it reduces the overhead when instantiating the object, and allows the object to be reused repeatedly, reducing the chance of new memory allocation and the garbage collector running.
When the game starts, it creates the entire collection of objects up front (usually in a single contiguous allocation) and initializes them all to the “not in use” state.
When you want a new object, ask the pool for one. It finds an available object, initializes it to “in use”, and returns it. When the object is no longer needed, it is set back to the “not in use” state. This way, objects can be freely created and destroyed without needing to allocate memory or other resources.
To the memory manager, we’re just allocating one big hunk of memory up front and not freeing it while the game is playing. In other words, the object pools grabs a big chunk of memory when the game starts, and don’t free it until the game ends. This way, the curse of memory fragmentation won't happen.
When to Use object pool
This pattern is used widely in games for obvious things like game entities and visual effects, but it is also used for less visible data structures such as currently playing sounds. Use Object Pool when:
- You need to frequently create and destroy objects.
- Objects are similar in size.
- Allocating objects on the heap is slow or could lead to memory fragmentation.
- Each object encapsulates a resource such as a database or network connection that is expensive to acquire and could be reused.
Be careful though
The pool may waste memory on unneeded objects.
The size of an object pool needs to be tuned for the game’s needs. When tuning, it’s usually obvious when the pool is too small (there’s nothing like a crash to get your attention). But also take care that the pool isn’t too big. A smaller pool frees up memory that could be used for other fun stuff.
Using ig.EntityPool in your project
Impact (since 1.23) comes with the easy to use ig.EntityPool. To enable Pooling, all you have to do is to require 'impact.entity-pool, add a .reset() method that is called when the entity is revived from the Pool and call ig.EntityPool.enableFor() for your Entity Class.
For instance, if you have a Bullet entity for which you want to enable Pooling, it would work like this:
ig.module(
'game.entities.bullet'
)
.requires(
'impact.entity',
'impact.entity-pool'
)
.defines(function(){
EntityBullet = ig.Entity.extend({
animSheet: new ig.AnimationSheet( 'media/bullet.png', 8, 8 ),
shootSound: new ig.Sound( 'media/sounds/bullet.*' ),
// (...)
init: function( x, y, settings ) {
this.parent( x, y, settings );
this.addAnim( 'idle', 1, [0,1,3] );
this.shootSound.play();
},
reset: function( x, y, settings ) {
// This function is called when an instance of this class is resurrected from the entity pool.
// The parent implementation of reset() will reset the .pos to the given x, y and will reset the .vel, .accel, .health and some other properties.
this.parent( x, y, settings );
// Play the shoot sound again. Remember, init() is never called when the entity is revived from the pool.
this.shootSound.play();
},
// (...)
});
// Enable Pooling!
ig.EntityPool.enableFor( EntityBullet );
});
With Pooling enabled, you can just spawn your Entity like usual and Impact will figure out the rest. E.g.:
ig.game.spawnEntity(EntityBullet, x, y, settings);
Dive into ig.EntityPool class
ig.EntityPool allows you to easily enable pooling for frequently used types of Entities.
The EntityPool is global, you don't have to create it, but just enable it for your Entities. When reviving an Entity from the Pool, Impact will call the Entity's .reset() method, instead of init().
public APIs
.enableFor( EntityClass )
Augments the given EntityClass with pooling functionality. This should only be called once for each Entity Class for which you want to enable pooling.
.drainPool( classId )
Removes all Entities with the given .classId from the Pool.
Example:
ig.EntityPool.drainPool( EntityBullet.classId );
.drainAllPools()
Removes all Entities from the Pool.
This function is automatically called by the Game's .loadLevel() method, before the new level is loaded.
Take a look at the implementation of .enableFor( EntityClass )
enableFor: function( Class ) {
Class.inject(this.mixin); // inject the two object pooling related functions defined in mixin to the Class.
},
mixin: {
staticInstantiate: function( x, y, settings ) {
return ig.EntityPool.getFromPool( this.classId, x, y, settings );
},
erase: function() {
ig.EntityPool.putInPool( this );
}
},
staticInstantiate and erase function corresponding to the pool's fetching and recycling operations.
About Class.inject
.inject() works similar to .extend() but does not create a new Class - instead, it changes the Class in place. This is useful if you want to change the behavior of one of Impacts classes without changing the engine's source code, e.g. for plugins.
// Overwrite ig.Image's .resize method to provide your
// own scaling algorithm
ig.Image.inject({
resize: function( scale ) {
if( scale == 2 ) {
this.data = awesome2XScalingAlgorithm( this.data );
}
else {
// Call ig.Image's resize function if scale is not 2
this.parent( scale );
}
}
});
// The new resize method will also be used in subclasses of
// ig.Image (e.g. ig.Font)
private functions
putInPool: function( instance ) {
if( !this.pools[instance.classId] ) {
this.pools[instance.classId] = [instance];
}
else {
this.pools[instance.classId].push(instance);
}
},
getFromPool: function( classId, x, y, settings ) {
var pool = this.pools[classId];
if( !pool || !pool.length ) { return null; }
var instance = pool.pop();
instance.reset(x, y, settings); // Note here reset is called
return instance;
},
Leave a comment
Log in with itch.io to leave a comment.