Wednesday, July 31, 2013

Object Pooling: Resource Recycling!

Alright, so we're 2-for-2 regarding posts about Spacer; I guess I've made quite a lot of progress on it lately. The concept of object pooling isn't new to programming, but it's new to me.  As an added bonus, this concept is very useful to programming things other than just video games; I could easily see it being used for particle physics or computational chemistry simulations (or, I suppose hundreds of business-type applications).  Therefore, that will be the topic of this post.  Object pooling is essentially digital recycling.

I guess Randall Munroe's stick figure comics are influencing me.  Also the fact that this comment is in the alt text.


The basic idea is to avoid constantly creating and destroying objects, which is very processor-expensive when you're dealing with potentially hundreds of complex objects. To accomplish this, we create a bunch of "empty" instances of the class we need, activate them as needed, and deactivate them for reuse when we're done. That is, instead of "new-ing up" a bullet every time an in-game gun fires, turn to your "bullet pool" and activate the next inactive bullet.

There are a couple keys to making object pooling work:

  1. All objects being pooled should have an empty constructor
  2. The pooled objects should all have some "initialize" or "activate" method
  3. Be sure to clean up the object after you are done using it... you don't want any residual properties left over from the previous usage. 
Without further adieu, here's the code sample for my generic pooling:
class ObjectPool<t> where T : IPoolableObject
{
 #region Private Fields

 private List<t> activeItems;
 private Queue<t> inactiveItems;
 private int resizeAmount;

 #endregion

 #region Public Properties

 public T this[int index] { get { return this.activeItems[index]; } }
 public int Count { get { return activeItems.Count; } }
 public List<t> InactiveObjects { get { return inactiveItems.ToList(); } }

 #endregion

 public ObjectPool(int Capacity, int ResizeAmount = 1)
 {
  if (Capacity < 1)
   throw new ArgumentOutOfRangeException("The capacity must be 1 or greater");
  if (ResizeAmount < 1)
   throw new ArgumentOutOfRangeException("The resize amount must be 1 or greater");

  activeItems = new List<t>();
  inactiveItems = new Queue<t>();
  resizeAmount = ResizeAmount;

  //Fill up the inactive items list
  System.Reflection.ConstructorInfo constructor = typeof(T).GetConstructor(new Type[0]);
  for (int i = 0; i < Capacity; i++)
  {
   inactiveItems.Enqueue((T)constructor.Invoke((object[])null));
  }
 }

 public T ActivateItem(string ObjectModelName, Vector2 StartingPosition, float StartingOrientation, Vector2? StartingVelocity = null, float StartingAngularVelocity = 0)
 {
  if (inactiveItems.Count == 0) //enlarge the queue if we've run out of objects! 
  {
   System.Reflection.ConstructorInfo constructor = typeof(T).GetConstructor(new Type[0]);
   for (int i = 0; i < resizeAmount; i++)
   {
    inactiveItems.Enqueue((T)constructor.Invoke((object[])null));
   }
  }

  T newItem = inactiveItems.Dequeue();
  activeItems.Add(newItem);
  newItem.ActivateObject(ObjectModelName, StartingPosition, StartingOrientation, StartingVelocity, StartingAngularVelocity);
  return newItem;
 }

 public void DeactivateItem(T item)
 {
item.DeactivateObject();
inactiveItems.Enqueue(item);
  activeItems.Remove(item);
 }

 // The stuff below just makes the ObjectPool fancy, allowing you to call a "foreach" loop on the pool or simply get a list of all the active objects:
 public IEnumerator<t> GetEnumerator()
 {
  return (IEnumerator<t>)this.activeItems.GetEnumerator();
 }

 public List<t> ToList()
 {
  return activeItems;
 }
}

IPoolableObject, here is a really simple interface inherited by your pooled objects. It's everything that's needed for a "new" bullet and everything it needs to clean up after itself (dirty object..).  The ObjectModelName is used as a lookup value for both a pre-loaded texture and the statistics library entry (ex: health, speed, armor, ect..., remember? See the previous post if you don't). StartingPosition, StartingOrientation, StartingVelocity, and StartingAngularVelocity are all relavent quantities for the physics engine.  As for cleanup, everything of relevance gets overwritten during the ActivateObject call, so there really isn't anything to the DeactivateObject method.  Your objects don't have to have these parameters (or lack-there-of), I'm just throwing them out there as an example.

interface IPoolableObject
{
 void ActivateObject(string ObjectModelName, Vector2 StartingPosition, float StartingOrientation, Vector2? StartingVelocity = null, float StartingAngularVelocity = 0);
void DeactivateObject();
}

Also, the bit about the "ResizeAmount" isn't really necessary. I just wanted something to fall back on if the initial capacity guess was too small and isn't the app barfing by throwing a "queue empty" exception.  You could just have the pool return null; make sure your code knows how to interpret a null object, though.

As for how it's implemented in the larger game, it's super effective!

// Create and construct the pool:
public ObjectPool<bullet> BulletPool;
BulletPool = new ObjectPool<bullet>(100);

// Activate a new bullet:
Bullet newBullet = bulletPool.ActivateItem(BulletTypeFired, StartingLocation, StartingOrientation, StartingVelocity);

// Insert a bunch of code about how your app uses the newBullet

// and finally, deactivate it when you are finished:
BulletPool.DeactivateItem(newBullet);

So there you have it.  I'd like to give some credit to a couple other sources that I kinda picked and chose my favorite parts from: Jason Mitchell, Thomas Aylesworth, and the XNA Wiki: Generic Resource Pool.

No comments:

Post a Comment