Object Pooling

Introduction

The object pooling system is available for any base object via PoolObject.

note that pooled objects are not in the state's internal list of objects, kill = true will not destroy them, instead the pool object will handle its collection, “dispose it back to the pool”.

An example pool object exists for CitrusSprite : CitrusSpritePool.

Physical objects can be pooled as well, but due to the difference in how nape and box2D work, we had to divide those pool objects into two classes : NapeObjectPool and Box2DObjectPool.

The key difference considered is that nape allows removing a body from its space in the middle of a world simulation, so to remove an object and dispose it to the pool, we can set its body.space to null and it will stop being simulated and be kept in memory.

For box2D physics object, this process of removing the body from the simulation is done using body.setActive(false) but can only be done outside of a world simulation and to make sure that happens, the setActive call is delayed to the next PoolObject update.

Setting up a pool

We will be using the example provided here: https://github.com/alamboley/Citrus-Engine-Examples/tree/master/src/objectpooling

Where we set up a pool of Crate objects with Nape. The exact same can be done by switching Nape for Box2D and NapeObjectPool with Box2DObjectPool (and of course importing the right Crate object from the box2D package).

First, of course, we setup the physics

_nape = new Nape("nape");
_nape.gravity = Vec2.get();
_nape.visible = true;
add(_nape);

Then we create the NapeObjectPool

_napeCrate = new NapeObjectPool(Crate, { width:100, height:100, touchable:false } , 1);

We tell NapeObjectPool that we want to pool Crate objects, and give them default properties such as width, height and the touchable property. Width/height are necessary for the actual shape/body to be created properly.

In those default params, you can only use properties which will be String,Boolean,Numbers,Class… anything that isn't an instance that is passed by reference actually, because that instance won't be cloned and be given to all objects when the pool instanciates, they would share it and that's a bad idea.

As an example for that warning , if you did :

_napeCrate = new NapeObjectPool(Crate, { width:100, height:100,view:new Quad(100,100), touchable:false } , 1);

you create a single Quad and gave it to the default params object. if you want a pool of 50 objects, then cannot all have the same display object as a view so this will fail.

You could, for the view, give it a url string as each created Crates will load up their own view, or a Class that defines a display object, as all created Crates will use that class to instantiate their own view.

The “object pool” needs to be added to the state so CitrusEngine can update it.

addPoolObject(_napeCrate);

if you want to customize how your crates are created, disposed and recycled, attach listeners to the available signals before initializing the pool :

_napeCrate.onCreate.add(_handleCrateCreate);
_napeCrate.onDispose.add(_handleCrateDispose);
_napeCrate.onRecycle.add(_handleCrateRecycle);
 
_napeCrate.initializePool(100);
The terminology we'll be using with the object pooling system is questionable, but make note :
  • Dispose : when you no longer need an object and want to disable it (in broad terms), you dispose of it, sending it back to the pool, passing that object to the onDispose listeners to custom process it on disposal.
  • Recycle : when you need an object from the pool. If no objects are available, an object will be created and passed through the onCreate listeners, then to the recycling process (the onRecycle listeners) then finally made available. If an object was available in the pool for “recycling”, then it will only go through the onRecycle listeners.
  • Create : an object is created once, you cannot predict, unless you keep count, if the object you get from the pool is freshly created or simply re-used. So keep that in mind when writing the listeners for the create/recycle/dispose signals.

onCreate is called when an object is first created - not when it it “added to the state”. Remember this is a pool, objects that you will use, can be newly created objects if there was nothing left in the pool but do not assume they are.

onDispose is called when an object is sent back to the pool, this happens when you kill an object.

onRecycle is when you “get” an object, when you need to display a Crate in game with this pool, you would do _napeCrate.get(); and that crate will be re-used.

In the example we're looking at, we setup listeners for all these signals to show how you can exploit all possibilities.

touchDic is a dictionnary that associates display objects with the citrus objects it represents so that on a touch event, we know what we touched. Its also the simplest thing we could have to show why using the onDispose signal could be used : we don't the object and its view referenced in the dictionary if the object is not on screen (is disposed in the pool) so we use that signal just to remove it.

		private function _handleCrateCreate(c:Crate,params:Object):void
		{
			var q2:Quad = new Quad(size, size, 0xFFFFFF);
			q2.name = "outline";
			var q:Quad = new Quad(size-margin*2, size-margin*2, 0xFF00FF);
			q.x += margin;
			q.y += margin;
			q.name = "main";
			var s:Sprite = new Sprite();
			s.addChild(q2);
			s.addChild(q);
			c.view = s;
		}
 
 
		private function _handleCrateRecycle(c:Crate,params:Object):void
		{
			touchDic[(c.view as Sprite).getChildByName("main")] = c;
			c.velocity = [(Math.random() * 2 - 1) * 60, (Math.random() * 2 - 1) * 60];
		}
 
 
		private function _handleCrateDispose(c:Crate):void
		{
			touchDic[(c.view as Sprite).getChildByName("main")] = null;
			delete touchDic[(c.view as Sprite).getChildByName("main")];
		}

So notice, we create a complex view in handleCrateCreate , That view will be available during the entire life time of the object, wether disposed or not.

when

_napeCrate.initializePool(100);

is called, this will be done a hundred times (views will be created for each objects. note that dispose is also called after an object is first created - to prevent that, add listeners to the onDispose signal AFTER calling initializePool.

Also note, listeners could be defined within your objects (as public functions for example) to clear up the code in your state.

Using a pool

Getting an object from the pool

Now we have a 100 crates available, but nothing on screen, and nothing in our nape space. everything is in memory basically - disposed objects do not update.

We will now put some of these crates on state using get() :

var i:int;
var c:Crate;
 
				for (i = 0; i < objNum; i++)
				{
					//create or recycle a new crate, giving them a new x/y position
					c = (
					_napeCrate.get(
					{ x:stage.stageWidth / 2 +(Math.random() * 300 - 150),
					y:stage.stageHeight / 2 +(Math.random() * 300 - 150) }
					).data 
					as Crate);
					//randomize the color of the crate
					((c.view as Sprite).getChildByName("main") as Quad).color = Color.rgb(122,80,80) * Math.random() + 0x555555;
				}

get() returns a DoublyLinkedList node (as the pool objects are based on linked lists). To access the actual object, you need

c = get().data as Crate;

and get accepts an argument that would be new parameters to apply to the recycled/created object. Here we set new x and y positions randomly for each of them.

Notice that pooled objects are out of the usual CitrusEngine state system.

You should not add() them to the state.

When you get(), it will be there.

Disposing objects back to the pool

If the pooled object is a default Coin object, it will kill itself on contact. when a pooled object is killed, the “object pooler” collects it and disposes of it back to the pool…

So its as easy as setting kill = true as you would with “normal” objects.

you can also disposeAll() straight away.

What if you want to dispose objects back to the pool when a certain condition is met?

The pool objects have methods to loop through all recycled objects or all inactive objects (objects still in the pool) so you can use the former to check if a condition is met and kill the object or not :

_napeCrate.foreachRecycled(function(crate:Crate):Boolean
			{
				if (crate.x < 10)
					crate.kill = true;
				return false;
			});

The code above, kills all “active” crates which x is below 10.

the callback function for foreachRecycled and foreachDisposed need to return a Boolean value. if you're looking for a specific object and found it , return true. the “for each” loop will stop. if not return false.

here we want to check ALL recycled objects so we return false constantly.

Killing pooled objects outside of the camera is now simple !

var art:StarlingArt;
var objectBounds:Rectangle = new Rectangle();
 
napeCrate.foreachRecycled(function(crate:Crate):Boolean
	{
		art = view.getArt(crate) as StarlingArt;
		if (art.content is DisplayObject)
		{
			(art.content as DisplayObject).getBounds( art.parent, objectBounds )
 
			if (!camera.intersectsRect( objectBounds ) )
				crate.kill = true;
		}
		return false;
	});
  citrus/object_pooling.txt · Last modified: 2013/11/25 20:13 by chichilatte
 
Powered by DokuWiki