Poolers (Part 2 of 4)

In some scenarios, the collection of “Poolable” items itself isn’t relevant to how you are using them. If you have no need of iterating over and applying logic to the entire group then you might be satisfied with using a simple “Set” to contain them. For example, an explosion might be pooled after its animation has completed and a bullet might be pooled after it hits an object or leaves a game zone. Scenario’s with event-based pooling might be a perfect fit for our first Pooler subclass.

Set Pooler

The first concrete subclass of “BasePooler.cs” is the “SetPooler.cs” script. You can open and review the completed script from the project, but I will point out and comment on several code snippets here.

public HashSet<Poolable> Collection = new HashSet<Poolable>();

This pooler is implemented using a generic “HashSet” to manage the collection of “Poolable” items. Initially I was tempted to make the “Collection” itself “private”. A “public” collection can be edited manually (adding and removing elements, etc) but it is my ideal intention that the collection is only modified through the “Enqueue” and “Dequeue” methods. Ultimately I decided to leave the field “public” because there are a lot of other methods and properties available to a “HashSet” which might be convenient such as obtaining the current count of items. In this case I felt that greater flexibility and convenience was more important than forced safety.

public override void Enqueue (Poolable item)
{
	base.Enqueue(item);
	if (Collection.Contains(item))
		Collection.Remove(item);
}

The “Enqueue” method was overriden so that I could add additional logic. The original logic is still utilized by calling the “base” implementation. However, I also make sure that the Collection stays in sync by removing the item if it was currently referenced.

public override Poolable Dequeue ()
{
	Poolable item = base.Dequeue();
	Collection.Add(item);
	return item;
}

The “Dequeue” method is similarly overriden. The original logic is still utilized by calling the “base” implementation. However, I also make sure that the “Collection” stays in sync by adding the newly obtained obejct.

public override void EnqueueAll ()
{
	foreach (Poolable item in Collection)
		base.Enqueue(item);
	Collection.Clear();
}

Now we finally have an implementation for “EnqueueAll”. Here I am using a fast enumerator to iterate over our Collection. For each item in the set, we call the “base” class’s “Enqueue” method. Note that we do NOT call the “Enqueue” method of the current class, because that would cause the set to be modified while we were iterating over it. It is only once the loop is complete that we update the “Collection” to be in sync by calling “Clear”.

In the past Unity had a memory leak when using a fast enumerator like this. It is possible that it still does, but this method is unlikely to be invoked frequently and the leak would be small if it hasn’t already been addressed, so I am unconcerned. If it causes you trouble, you can always copy the Collection to a temporary array and use a normal “for” loop instead.

Demo

Open and run the “CubePooler” demo scene. There are two menu buttons on the left. Every time you click the “Add” button you should see a new “Cube” appear somewhere on screen. If there were cubes available in the pool, they will be reused, otherwise they will be instantiated. If you click “Clear”, all of the cubes will be sent back to the pool for reuse.

This demo is meant to show what could happen for event-based pooling. In this case the event will be a click. Any cube that you click on will be sent back to the pool.

Now let’s see the script which is the “consumer” of the “Pooler” – the script which makes this demo work. The script is called “CubePoolerDemo.cs”. You can open and review the completed script from the project, but I will point out and comment on several code snippets here.

[SerializeField] SetPooler pooler;

First I need a reference to the pooler. I used the “SerializeField” tag so that I could connect the reference in the inspector even though the field is “private” in scope.

void OnEnable ()
{
	pooler.willEnqueue = OnEnqueue;
	pooler.didDequeue = OnDequeue;
}

void OnDisable ()
{
	pooler.willEnqueue = null;
	pooler.didDequeue = null;
}

I use methods which are automatically invoked (because the script is a MonoBehaviour) to handle subscribing and unsubscribing to the “Actions” that the pooler will send.

void OnDequeue (Poolable item)
{
	float xPos = UnityEngine.Random.Range(-6f, 6f);
	float yPos = UnityEngine.Random.Range(-4f, 4f);
	float zPos = UnityEngine.Random.Range(-5f, 5f);
	item.transform.localPosition = new Vector3( xPos, yPos, zPos );
	item.gameObject.SetActive(true);

	Button button = item.GetComponent<Button>();
	button.onClick.AddListener( ()=>{ pooler.Enqueue(item); } );
}

Whenever I get an object from the pool (whether newly instantiated or reused), this Action handler method is triggered. I move the instance to a random position, and make sure to enable the GameObject (pooled objects are stored in a disabled state). Then I grab a reference to a Button component on the instance. I attach an anonymous delegate as a listener to the onClick event, and this delegate causes any button which is clicked on to be returned to the pool.

void OnEnqueue (Poolable item)
{
	Button button = item.GetComponent<Button>();
	button.onClick.RemoveAllListeners();
}

It is important to be responsible and clean up after yourself. Since I added a listener to the instance, I will also remove the listener when the object is about to be sent back to the pool. If I forgot to do this step, then there could potentially be many listeners registered to the click, and the item would then try to be returned multiple times from a single click.

public void OnAddButton ()
{
	pooler.Dequeue();
}

This method is the target of the menu button – “Add”. It simply calls the pooler’s dequeue method. For the “Clear” button I was able to reference the pooler’s “EnqueueAll” method directly because the signature was compatible – it accepts no parameters and has a return type of “void”. The “Dequeue” method does not have a “void” return type – it returns a “Poolable” instance, so I had to make a new wrapper method.

Summary

In Part 2 (of 4) we introduced one of the new concrete subclasses for our Pooler. This version used a “HashSet” to track the collection of poolable items it was responsible for. We reviewed the code for the Pooler as well as interacted with a Demo scene which implemented it. Finally we reviewed the code for a controller script which consumed the pooler.

Leave a Reply

Your email address will not be published. Required fields are marked *