Sometimes you want to be able to refer to specific items in your collection by a key. The key could still be an index, but might be a scenario where your collection doesn’t include all of the indices – perhaps it didn’t start at zero or skips items. Alternatively you might want to map an item to something else, like a string of text. These are the kinds of cases where a Dictionary makes more sense than a List. Our final pooler and demo will be based on this use case.
Keyed Pooler
The third and final subclass of “BasePooler.cs” is the “KeyedPooler.cs” script. This class is actually another abstract class. It is also generic so that you can specify the kind of key you wish to use:
public abstract class KeyedPooler<T> : BasePooler
The class definition looks like this. Note how the class name is followed by the generic letter ‘T’. Anywhere you see the ‘T’ you will actually be referring to another data type which will be specified by a concrete subclass. In Unity you can’t add generic classes to a GameObject. I marked the class as abstract to help imply that it is not intended to be used directly, even though the class is “complete” as is.
public class IntKeyedPooler : KeyedPooler<int> public class StringKeyedPooler : KeyedPooler<string>
The project includes two sample subclasses, one where the “key” is an “int” data type and one where the “key” is a “string” data type. Those two will work in most scenarios, but it would be very easy to add more as you need them. The body of each class is completely empty because the KeyedPooler class already has everything it needs.
You can open and review the completed “KeyedPooler.cs” script from the project, but I will point out and comment on several code snippets here.
public Dictionary<T, Poolable> Collection = new Dictionary<T, Poolable>();
Here I have created a generic “Dictionary” to hold our collection of poolable items. Note that the key for the Dictionary is using the same Generic marker as the one on the class. This means that it will automatically be implemented using the same data type.
public bool HasKey (T key) { return Collection.ContainsKey(key); } public Poolable GetItem (T key) { if (Collection.ContainsKey(key)) return Collection[key]; return null; }
Just like we implemented new methods for the “List” based pooler to get objects by “Index”, it makes sense for the “Dictionary” based pooler to get objects by “Key”. I added a convenient “HasKey” method, but the “GetItem” is also safe, returning a “value” where possible and “null” otherwise.
public U GetScript<U> (T key) where U : MonoBehaviour { Poolable item = GetItem(key); if (item != null) return item.GetComponent<U>(); return null; }
Here again we follow the same basic ideals as before. It is nice to be able to get a direct reference to a script on a Poolable item so this generic method allows it to be done conveniently.
public Action<Poolable, T> willEnqueueForKey; public Action<Poolable, T> didDequeueForKey;
Just like we added new Actions to the “Indexed” Pooler so that we could pass along the “Index” as a parameter, now we have added Actions so that we can pass along the “Key” which may be necessary for proper configuration.
public virtual void EnqueueByKey (T key) { Poolable item = GetItem(key); if (item != null) { if (willEnqueueForKey != null) willEnqueueForKey(item, key); Enqueue(item); Collection.Remove(key); } }
In order for the collection to stay synced properly with what you have grabbed from the pool, you should use “EnqueueByKey” and not “Enqueue”. We start out by attempting to get the item specified by the key. If it is found, we can post our relevant Action, Enqueue the object using the base class (note that it uses the base class because the subclass has no overriden version), and then update the collection by removing the key.
public virtual Poolable DequeueByKey (T key) { if (Collection.ContainsKey(key)) return Collection[key]; Poolable item = Dequeue(); Collection.Add(key, item); if (didDequeueForKey != null) didDequeueForKey(item, key); return item; }
This time we also need a way to “Dequeue” by a key. In the event that an item has already been obtained for the specified key, it simply returns the object it already has. Otherwise, it Dequeues a new item, adds it to our collection, and then posts the relevant Action.
public virtual U DequeueScriptByKey<U> (T key) where U : MonoBehaviour { Poolable item = DequeueByKey(key); return item.GetComponent<U>(); }
For convenience, I also allow dequeuing by key to return whatever kind of script you prefer working with.
public override void EnqueueAll () { T[] keys = new T[Collection.Count]; Collection.Keys.CopyTo(keys, 0); for (int i = 0; i < keys.Length; ++i) EnqueueByKey(keys[i]); }
Normally you can only iterate over a Dictionary by “foreach”, but as I mentioned before, you can’t modify the collection while iterating over it. In order to resolve this problem, I first made an array to hold all of the Dictionaries keys. Then I was able to use a normal “for” loop to iterate over my collection. The array wont change even though the actual Collection does, so my loop is safe even though I am looping “forwards”.
Demo
Open and run the “SpherePooler” demo scene. There is an InputField and three menu buttons on the left. Type something into the “InputField” and that will become the “Key” to use for our Pooler. If you click “Add”, a new sphere will be added at a random position in the scene – unless there is already an object for that key (keys must be unique). As with “Add”, the “Remove” button works based on the value in the “InputField”. If there is an object in the scene which is based on the specified key, then it will be sent back to the pool. The “Clear” button sends all items back to the pool regardless of the contents of the “InputField”.
Now let’s see the script which is the “consumer” of the “Pooler” – the script which makes this demo work. The script is called “SpherePoolerDemo.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] StringKeyedPooler pooler; [SerializeField] InputField keyInput;
I will need two references for this demo, both of which are assigned in the Inspector. The pooler is obvious. I need a reference to the InputField so that I can read the value of the text it contains and use it as the “Key” for pooling with. Note that this is the key for the “Dictionary” based “Collection” on the “Pooler”, and not the key which is used to register a prefab with the pool controller.
void OnEnable () { pooler.didDequeueForKey = DidDequeueForKey; } void OnDisable () { pooler.didDequeueForKey = null; }
Again, we use MonoBehaviour methods for observing or to stop observing Actions from the Pooler. In this case we register for another “new” Action – the one which includes a Key.
void DidDequeueForKey (Poolable item, string key) { 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); item.name = key; }
This looks pretty similar to the Cube demo – we randomly position the sphere somewhere in the screen. One difference is that we do something with the “key” parameter. In a more real-world scenario that key would probably be used to load the item with some sort of data, but here I simply change the name of the instance to be the same as the key. This way, if you forgot what names you had used you can simply look in the hierarchy pane (editor only of course).
public void OnAddButton () { if (!string.IsNullOrEmpty(keyInput.text)) pooler.DequeueByKey(keyInput.text); }
Like before, we need a wrapper method around the Add button since dequeueing from the pooler has a return value. In this case I also wanted to force a restriction – I decided that empty strings were invalid keys, so it only dequeue’s when you type something in.
public void OnRemoveButton () { pooler.EnqueueByKey(keyInput.text); }
The last interface method allows us to remove items from the scene. There are no restrictions on what you try to Enqueue – regardless of the “key”. If the pooler doesn’t have the “key” it just ignores the request.
Summary
In Part 4 (of 4) we introduced the last of the new subclasses for our Pooler. This version used a “Dictionary” 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.
Some questions from a noob trying to understand this properly:
If I prepopulate with 10 spheres, I can do GameObject item = pooler.Dequeue() to get an object from the pool, right?
If I want to give that item a key and put it back into the pool, would I do:
pooler.EnqueueByKey(key);
Thanks!
With the Keyed pooler, you shouldn’t be using the “Dequeue” method directly. You use the “DequeueByKey” method and pass whatever key you want to use at that time. This pooler maintains references to the dequeued items according to that key via a Dictionary. Then as long as you have a reference to the pooler you can also get a reference to any dequeued item if you have the key.
When you pre-populate a pooler, the items it creates are enqueued and have no relation to a key yet.
If you haven’t run the demo, make sure to try it out, it might help clarify the intended flow.
Ah, so when I dequeue it then I assign the key. That’s where I was getting a bit confused.
How do I grow or shrink the pool size at runtime?
It will grow automatically as needed to fill the demand.