After having used my pooling library for awhile I found myself putting similar implementation code all over my scripts. With a bit of thought I realized that I could eliminate a lot of the code by creating another component. Actually I will be creating several, but they all serve the same purpose of helping reduce the amount of code necessary to work with pooled items.
Goals
In order to use the GameObjectPoolController there is a bit of setup needed. You need to maintain a reference to a prefab and then register that prefab with the controller. You can specify a unique key for the pool and can also configure several options such as whether or not you want to prepopulate any instances or cap the number of instances that the pool can hold. All of this can be easily done through code, but it can be very convenient to be able to configure those settings in the inspector, and that will be one of our goals.
Use of the pool is fairly straightforward, because you can easily call methods to Enqueue or Dequeue an instance as needed, but often times when I was “done” with a scene I wanted to quickly return everything. Sometimes I also wanted to clear the entry from the controller completely. This led to methods that looked largely the same across many scripts. When code is frequently the same, we can capture the functionality in one place to make it reusable, and that will be the another goal.
Although the current pooling controller makes it very easy to work with pooled items, I don’t necessarily like having to always grab and return “Poolable” components. It would be nice to support a way to immediately grab any kind of component you might want to work with. Likewise I would like to be able to give back the object while referencing any component on the GameObject or even the GameObject itself. These extra features will be another goal.
Sometimes I want to maintain and work with the “collection” of the poolable items. There are three primary use cases I’ve encountered which justify three kinds of collections: a Set, List, and Dictionary. Being able to work with any of these will be our final goal.
Getting Started
I created a sample project which contains a few scenes, one showing a potential use for each of the three collection types I want to focus on. Grab a copy of the Demo Project Here. Note that this project was created using Unity version 5.3.1f1.
Base Pooler
The first new script I created for this demo is the “BasePooler.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. First, let’s discuss some of the fields:
public string key = string.Empty; public GameObject prefab = null; public int prepopulate = 0; public int maxCount = int.MaxValue;
Each of these fields directly relate to the parameters you would pass to the “GameObjectPoolController” when Adding a new entry. By making them “public” in a “MonoBehaviour” script, they will all automatically be able to be configured through the inspector. That was an easy goal.
Note that “prepopulate” and “maxCount” are initialized to default values which are acceptable for many cases, which means you only need to configure them in special scenarios. Likewise the “key” is not required, and if it is left empty, the component will register the “prefab” using the preab’s name instead. The only field you “must” configure is the reference to a “prefab”.
public bool autoRegister = true; public bool autoClear = true; public bool isRegistered { get; private set; }
These last three fields allow a user the opportunity to control how much of the registration process is automated. Ideally you will let it handle everything, but under special circumstances you might want to manually configure things at run-time.
protected virtual void Awake () { if (autoRegister) Register(); } protected virtual void OnDestroy () { EnqueueAll(); if (autoClear) UnRegister(); } protected virtual void OnApplicationQuit() { EnqueueAll(); }
Here are a few of the methods which are automatically invoked since the script is a “MonoBehaviour” subclass. In the “Awake” method I allow the Registration process to begin as long as the “autoRegister” flag was left at its default value of “true”. Note that if you set that value to “false” that you will need to manually invoke the “Register” method yourself. The “prefab” must be registered before you start trying to “Dequeue” any items.
There is an important difference between “Start” and “Awake”. The “Awake” method fires earlier than “Start” – it fires immediately. The “Awake” method on ALL scripts fire before the “Start” method on any of them. When instantiating a GameObject programmatically, the “Awake” method would be called immediately, even before the very next line of your own code. Thanks to this, I can immediately begin using a pooler’s items after instantiation, or in any other script’s “Start” method.
In the “OnDestroy” method, I make sure to return any items that the pooler script was currently holding a reference to. I also un-register the prefab from the the pool controller unless you have un-checked the “autoClear” flag. You might decide to do this when using the same pooled prefab across multiple scripts.
In the “OnApplicationQuit” method I also return any items that the pooler script was currently holding a reference to. This bit isn’t super important, but I added it because it helped to remove some errors I saw in the Editor when playing and stopping the scene. It was complaining about objects not being cleaned up. I hate seeing ANY warning or error. Plus it’s possible that these errors could also occur at run-time which would cause your app to crash – not good.
public void Register () { if (string.IsNullOrEmpty(key)) key = prefab.name; GameObjectPoolController.AddEntry(key, prefab, prepopulate, maxCount); isRegistered = true; }
The “Register” method first checks to see if the user has configured anything in the inspector for the “key” field. If not, then the “key” is assigned the same value as the prefab’s name. Then we register our prefab using the “AddEntry” method of the pool controller. All of the relevant parameter options for this method are able to be set by the fields we created. Finally, we mark the “isRegistered” property as “true” so that any manual registration processes are easier to track.
public void UnRegister () { GameObjectPoolController.ClearEntry(key); isRegistered = false; }
In the “UnRegister” method I clear the prefab from the pool controller by its “key”. Finally I mark the “isRegistered” property as “false” so that any manual registration processes are easier to track.
public Action<Poolable> willEnqueue; public Action<Poolable> didDequeue;
At the very top of the script I have these two Actions. An action is a special kind of Delegate. In this case I used a generic syntax so that it passes along a parameter of “Poolable” type. I chose to use an Action over an “EventHandler” or “Notification” because I feel like the “Pooler” should ideally be consumed by a single source. Anything else which needs to know about the use of the pooled items could be daisy-chained through another event on the consumer. As an added bonus, using an Action allows the method handler to automatically know the correct argument types whereas an “EventHandler” and “Notification” both pass an “object” which you would then have to cast to the correct data type.
public virtual void Enqueue (Poolable item) { if (willEnqueue != null) willEnqueue(item); GameObjectPoolController.Enqueue(item); }
The “Enqueue” method is a simple wrapper for the method you would normally call on the pool controller. Before returning the item, it calls an appropriate Action allowing the consumer to know what’s happening. These callbacks are very helpful because you might trigger the method from a variety of places in the consumer, or through a variety of methods on the pooler. By having a single callback, all the logic necessary for preparing an object to be reused can be done in a single location.
public virtual void EnqueueObject (GameObject obj) { Poolable item = obj.GetComponent<Poolable>(); if (item != null) Enqueue (item); } public virtual void EnqueueScript (MonoBehaviour script) { Poolable item = script.GetComponent<Poolable>(); if (item != null) Enqueue (item); }
Here are a couple of new methods which make it easier to work with “Poolable” objects while referring to its GameObject, or another component which happened to be on the same GameObject. Both methods attempt to grab the “Poolable” component and then use the normal “Enqueue” method to complete the job.
public virtual Poolable Dequeue () { Poolable item = GameObjectPoolController.Dequeue(key); if (didDequeue != null) didDequeue(item); return item; }
The “Dequeue” method is also a simple wrapper for the method you would normally call on the pool controller. Like before, after grabbing a pooled item, I trigger the Action to let any registered consumer know. This will commonly be used so that newly obtained objects can be configured in some way.
public virtual U DequeueScript<U> () where U : MonoBehaviour { Poolable item = Dequeue(); return item.GetComponent<U>(); }
Most of the time I don’t care about the “Poolable” component. The “GameObjectPoolController” needs it, but in pretty much every other case I want some other component which happens to be attached to the same GameObject. Using this method I can grab the component I want in a single line without ever seeing or caring about the “Poolable” component.
public abstract void EnqueueAll ();
The final method of this script is left as an “abstract” definition. I will have multiple subclasses which implement it, and those subclasses will all handle the job by implementing one of the three Collection types I mentioned earlier: a Set, List, or Dictionary.
Summary
In Part 1 (of 4) we introduced the goals of this mini-series. Ultimately we are trying to make using the Pooling library even easier than it already was. To accomplish this we will be creating several new components. We reviewed the base class for these components in this lesson.