Poolers (Part 3 of 4)

In many scenarios, the collection of “Poolable” items itself is relevant to their use. If you want to think of the collection as having an “order” then the collection should probably be implemented as a “List”. Follow along and I will show another pooler based on a list along with a demo scene showing how to use it.

Indexed Pooler

The second concrete subclass of “BasePooler.cs” is the “IndexedPooler.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 List<Poolable> Collection = new List<Poolable>();

This pooler is implemented using a generic “List” to manage the collection of “Poolable” items.

public Poolable GetItem (int index)
{
	if (index < 0 || index >= Collection.Count)
		return null;
	return Collection[index];
}

Because you will often want to refer to these items based on their index, this class provides new ways to get references to the objects based on an index. The first example, “GetItem” allows you to get a reference to an item in the list in a safe way. This means that we first make sure that the index is not out-of-bounds before trying to index into the collection. If the index is out-of-bounds we return “null” rather than crash the application.

public U GetScript<U> (int index) where U : MonoBehaviour
{
	Poolable item = GetItem(index);
	if (item != null)
		return item.GetComponent<U>();
	return null;
}

Here we are exposing a generic method so that we can get direct access to a component on a “Poolable” object – one which we actually care about working with. It is just a convenient little method but these kinds of things can really help reduce the overall amount of code in your impelementing classes which make them a lot more readable.

public Action<Poolable, int> willEnqueueAtIndex;
public Action<Poolable, int> didDequeueAtIndex;

For this class I have added two more Actions. These also pass along an index mapping to the position of the item in the collection. This can be very useful when initializing an object for example. Perhaps you want to configure the “Poolable” object based on data in another list – if you have the correct index then you will know which data to use.

public void EnqueueByIndex (int index)
{
	if (index < 0 || index >= Collection.Count)
		return;
	Enqueue(Collection[index]);
}

Since it may be more convenient at times to work with an index rather than the object itself, I have provided a way to “Enqueue” by the index. It makes sure the index is in-bounds, and if so, calls the “Enqueue” method with the correct item.

public override void Enqueue (Poolable item)
{
	base.Enqueue(item);
	int index = Collection.IndexOf(item);
	if (index != -1)
	{
		if (willEnqueueAtIndex != null)
			willEnqueueAtIndex(item, index);
		Collection.RemoveAt(index);
	}
}

Like we did with the “Set” variation, here we have overriden the “Enqueue” method so that we can add functionality. After returning the item, we get the index to the item in our collection. If it is found, then we post the relevant Action and remove the item.

public override Poolable Dequeue ()
{
	Poolable item = base.Dequeue ();
	Collection.Add(item);
	if (didDequeueAtIndex != null)
		didDequeueAtIndex(item, Collection.Count - 1);
	return item;
}

Here again, we override the “Dequeue” method. After obtaining a new item from the pool, we add the item to our collection list. Then we make sure to trigger the relevant Action so that any configuration which relies on the index can begin.

public override void EnqueueAll ()
{
	for (int i = Collection.Count - 1; i >= 0; --i)
		Enqueue(Collection[i]);
}

For the “EnqueueAll” implementation we loop over the list in reverse and Enqueue each item one at a time. Note that looping over the list forwards will not work because when you remove an item from the beginning of the list, all subsequent items shift their index. I could have made this slightly more efficient by not calling the “Enqueue” method since I already know the “index”, but keeping the behaviour in one place helps reduce the amount of code and potential bugs.

Demo

Open and run the “CellPooler” demo scene. There are three menu buttons on the left. Every time you click the “Add” button you should see a new “Cell” appear in a scrollable container on the right side of the screen. When you click the “Colorize” button, all of the cells managed by the pooler will have their background color changed to a new random color. Note that you can also click on any cell and change its color only. If you click “Clear”, all of the cells will be sent back to the pool for reuse.

Now let’s see the script which is the “consumer” of the “Pooler” – the script which makes this demo work. The script is called “CellPoolerDemo.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] IndexedPooler pooler;
[SerializeField] RectTransform content;

I needed two references to make this demo work. First, I need the pooler, and second, I need the RectTransform which is the “Content” of a “ScrollRect” which displays all of our colored cells. Both of these references were assigned in the inspector.

void OnEnable ()
{
	pooler.didDequeueAtIndex = DidDequeueAtIndex;
	pooler.willEnqueueAtIndex = WillEnqueueAtIndex;
}

void OnDisable ()
{
	pooler.didDequeueAtIndex = null;
	pooler.willEnqueueAtIndex = null;
}

The normal “Enqueue” and “Dequeue” actions can still be invoked if something registers for them. In this demo, I only care about the actions which include the Index.

void DidDequeueAtIndex (Poolable item, int index)
{
	Button button = item.GetComponent<Button>();
	button.transform.SetParent(content, false);
	button.gameObject.SetActive(true);
	button.onClick.AddListener( () => {
		Colorize(button.GetComponent<Image>());
	} );
}

When I obtain a new cell from the pool, I must parent it to the “content” so that it can display in the ScrollRect properly. Note that the content’s “GameObject”also has a “Vertical Layout Group” component which automatically handles the position of all children. Each of the cells also has a Button component. Like the first demo, I add an anonymous delegate to the click event, but this time I randomly apply a color to the cell instead of enqueueing it.

void WillEnqueueAtIndex (Poolable item, int index)
{
	Button button = item.GetComponent<Button>();
	button.onClick.RemoveAllListeners();
}

Time to be responsible! Since we added a listener, we need to know that we remove it too.

public void OnColorizeButton ()
{
	for (int i = 0; i < pooler.Collection.Count; ++i)
		Colorize( pooler.GetScript<Image>(i) );
}

This method shows how you are able to loop over the pooler’s collection of items. Here I randomly modify the background color of every cell the pooler is holding a reference to.

void Colorize (Image image)
{
	float r = Random.value;
	float g = Random.value;
	float b = Random.value;
	image.color = new Color(r, g, b);
}

This is the method which actually applies a random color. The color struct takes values in the range of 0-1 which “Random.value” also operates within.

Summary

In Part 3 (of 4) we introduced one of the new concrete subclasses for our Pooler. This version used a “List” 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 *