kontra
Version:
Kontra HTML5 game development library
189 lines (169 loc) • 6.19 kB
JavaScript
/**
* A fast and memory efficient [object pool](https://gameprogrammingpatterns.com/object-pool.html) for sprite reuse. Perfect for particle systems or SHUMPs. The pool starts out with just one object, but will grow in size to accommodate as many objects as are needed.
*
* <canvas width="600" height="200" id="pool-example"></canvas>
* <script src="assets/js/pool.js"></script>
* @class Pool
*
* @param {Object} properties - Properties of the pool.
* @param {Function} properties.create - Function that returns a new object to be added to the pool when there are no more alive objects.
* @param {Number} [properties.maxSize=1024] - The maximum number of objects allowed in the pool. The pool will never grow beyond this size.
*/
class Pool {
/**
* @docs docs/api_docs/pool.js
*/
constructor({create, maxSize = 1024} = {}) {
// check for the correct structure of the objects added to pools so we know that the
// rest of the pool code will work without errors
// @if DEBUG
let obj;
if (!create ||
( !( obj = create() ) ||
!( obj.update && obj.init &&
obj.isAlive )
)) {
throw Error('Must provide create() function which returns an object with init(), update(), and isAlive() functions');
}
// @endif
// c = create, i = inUse
this._c = create;
this._i = 0;
/**
* All objects currently in the pool, both alive and not alive.
* @memberof Pool
* @property {Object[]} objects
*/
this.objects = [create()]; // start the pool with an object
/**
* The number of alive objects.
* @memberof Pool
* @property {Number} size
*/
this.size = 1;
/**
* The maximum number of objects allowed in the pool. The pool will never grow beyond this size.
* @memberof Pool
* @property {Number} maxSize
*/
this.maxSize = maxSize;
}
/**
* Get and return an object from the pool. The properties parameter will be passed directly to the objects `init()` function. If you're using a kontra.Sprite, you should also pass the `ttl` property to designate how many frames you want the object to be alive for.
*
* If you want to control when the sprite is ready for reuse, pass `Infinity` for `ttl`. You'll need to set the sprites `ttl` to `0` when you're ready for the sprite to be reused.
*
* ```js
* // exclude-tablist
* let sprite = pool.get({
* // the object will get these properties and values
* x: 100,
* y: 200,
* width: 20,
* height: 40,
* color: 'red',
*
* // pass Infinity for ttl to prevent the object from being reused
* // until you set it back to 0
* ttl: Infinity
* });
* ```
* @memberof Pool
* @function get
*
* @param {Object} properties - Properties to pass to the objects `init()` function.
*
* @returns {Object} The newly initialized object.
*/
get(properties = {}) {
// the pool is out of objects if the first object is in use and it can't grow
if (this.objects.length == this._i) {
if (this.size === this.maxSize) {
return;
}
// double the size of the array by filling it with twice as many objects
else {
for (let x = 0; x < this.size && this.objects.length < this.maxSize; x++) {
this.objects.unshift(this._c());
}
this.size = this.objects.length;
}
}
// save off first object in pool to reassign to last object after unshift
let obj = this.objects.shift();
obj.init(properties);
this.objects.push(obj);
this._i++;
return obj
}
/**
* Returns an array of all alive objects. Useful if you need to do special processing on all alive objects outside of the pool, such as add all alive objects to a kontra.Quadtree.
* @memberof Pool
* @function getAliveObjects
*
* @returns {Object[]} An Array of all alive objects.
*/
getAliveObjects() {
return this.objects.slice(this.objects.length - this._i);
}
/**
* Clear the object pool. Removes all objects from the pool and resets its [size](#size) to 1.
* @memberof Pool
* @function clear
*/
clear() {
this._i = this.objects.length = 0;
this.size = 1;
this.objects.push(this._c());
}
/**
* Update all alive objects in the pool by calling the objects `update()` function. This function also manages when each object should be recycled, so it is recommended that you do not call the objects `update()` function outside of this function.
* @memberof Pool
* @function update
*
* @param {Number} [dt] - Time since last update.
*/
update(dt) {
let i = this.size - 1;
let obj;
// If the user kills an object outside of the update cycle, the pool won't know of
// the change until the next update and this._i won't be decremented. If the user then
// gets an object when this._i is the same size as objects.length, this._i will increment
// and this statement will evaluate to -1.
//
// I don't like having to go through the pool to kill an object as it forces you to
// know which object came from which pool. Instead, we'll just prevent the index from
// going below 0 and accept the fact that this._i may be out of sync for a frame.
let index = Math.max(this.objects.length - this._i, 0);
// only iterate over the objects that are alive
while (i >= index) {
obj = this.objects[i];
obj.update(dt);
// if the object is dead, move it to the front of the pool
if (!obj.isAlive()) {
this.objects = this.objects.splice(i, 1).concat(this.objects);
this._i--;
index++;
}
else {
i--;
}
}
}
/**
* Render all alive objects in the pool by calling the objects `render()` function.
* @memberof Pool
* @function render
*/
render() {
let index = Math.max(this.objects.length - this._i, 0);
for (let i = this.size - 1; i >= index; i--) {
this.objects[i].render();
}
}
}
export default function poolFactory(properties) {
return new Pool(properties);
}
poolFactory.prototype = Pool.prototype;
poolFactory.class = Pool;