The GameObject Pooler

posted in ferrous' Journal
Published February 06, 2016
Advertisement

So another fellow was doing a journal and was a little nervous about posting code for review. It got me thinking that I should post some code up as well. (I do have a game update that I could do as well, but I'll save that for later)

Anyway, my game spawns a lot of random levels that are populated with random amounts of game objects, and rather than use GameObject.Instantiate and Destroy repeatedly, I decided to pool the objects and re-use them. I looked around online for free ones, saw someone posted theirs, but... I didn't like it, but it did give me a good base to strip out and make my own:[code=js:0]//This is a heavy modification of the original code found here: http://forum.unity3d.com/threads/simple-reusable-object-pool-help-limit-your-instantiations.76851/using UnityEngine;using System.Collections.Generic;public class GameObjectPooler : MonoBehaviour{ // TODO: I could probably re-work this to get rid of the singleton public static GameObjectPooler Current; //A public static reference to itself (make's it visible to other objects without a reference) public GameObject[] prefabs; //Collection of prefabs to be pooled HashSet prefabSet = new HashSet(); // internally we use this, makes it easier to programmatically add other prefabs to the pooler if we want Dictionary> pooledObjects; // key = prefab, value = list of pooled objects Dictionary instancedPoolObjects; // Key = instance, Value = prefab public int[] amountToBuffer; //The amount to pool of each object. This is optional public int defaultBufferAmount = 10; //Default pooled amount if no amount abaove is supplied GameObject containerObject; //A parent object for pooled objects to be nested under. Keeps the hierarchy clean void Awake() { //Ensure that there is only one object pool if (Current == null) { Current = this; } else { Debug.LogError("Attempting to spawn a second GameObjectPooler!"); Destroy(gameObject); } //Create new container containerObject = new GameObject("ObjectPool"); //Create new list for objects pooledObjects = new Dictionary>(); instancedPoolObjects = new Dictionary(); int index = 0; foreach (GameObject objectPrefab in prefabs) { int bufferAmount; if (index < amountToBuffer.Length) bufferAmount = amountToBuffer[index]; else bufferAmount = defaultBufferAmount; AddPrefab(objectPrefab, bufferAmount); // Go to the next prefab in the collection index++; } } public void AddPrefab(GameObject prefab) { AddPrefab(prefab, defaultBufferAmount); } public void AddPrefab(GameObject prefab, int startPoolSize) { if(!prefabSet.Add(prefab)) { Debug.LogError("Trying to add prefab that already exists in the GameObjectPooler"); } pooledObjects.Add(prefab, new Stack()); for (int i = 0; i < startPoolSize; i++) { CleanPoolObject((GameObject)Instantiate(prefab)); } } public GameObject GetObject(GameObject objectType) { Debug.Assert(pooledObjects.ContainsKey(objectType), "objecttype: " + objectType.ToString() + " not found in pool, did you add it to the GameObjectPooler?", objectType); GameObject returnObj = null; //If there are any left in the pool... if (pooledObjects[objectType].Count > 0) { GameObject pooledObject = pooledObjects[objectType].Pop(); pooledObject.transform.parent = null; returnObj = pooledObject; } else // I am just always allow growing, TODO: Grow by more than 1 at a time. { GameObject obj = Instantiate(objectType) as GameObject; returnObj = obj; } if(!instancedPoolObjects.ContainsKey(returnObj)) { instancedPoolObjects.Add(returnObj, objectType); } returnObj.SetActive(true); return returnObj; } public void PoolObject(GameObject obj) { CleanPoolObject(obj); Debug.Assert(instancedPoolObjects.ContainsKey(obj), "Object not found in pool: " + obj.name, obj); pooledObjects[instancedPoolObjects[obj]].Push(obj); instancedPoolObjects.Remove(obj); // this causes a bit of churn, but is handy for keeping track of all instances over pooled } // Be very careful with this. If anyone is holding any references to a pooled object, things get wonky public void PoolAllObjects() { foreach(var key in instancedPoolObjects.Keys) { CleanPoolObject(key); pooledObjects[instancedPoolObjects[key]].Push(key); } instancedPoolObjects.Clear(); } private void CleanPoolObject(GameObject obj) { //obj.SendMessage("OnClean"); // OnDisable gets called intrinsically obj.SetActive(false); obj.transform.parent = containerObject.transform; }}

Previous Entry Going Mobile
3 likes 4 comments

Comments

Eck
When I first read your code, I thought you should make your class a Generic, something like:



// I just typed this, I didn't run it through a compiler. 
// I leave getting this working as an exercise to the reader...
GameObjectPooler <GeameObjectType> : MonoBehaviour where GameObjectType : MonoBehaviour
{
     List<GameObjectType> pooledObjects;

     public void AddPrefab(GameObjectType prefab)
     {

     }
     // etc. etc.
}
Then you'd have strongly typed values stored and wouldn't have to cast them to their real types. But after thinking about it, I don't know which is better for you.

I'm also not sure you want your Pooler to inherit from MonoBehaviour (or if Unity supports generic MonoBehaviour scripts). Instead the pooler could be a regular class that's contained by a different component.

Just something to think about.
February 07, 2016 05:25 AM
ferrous

Thanks for the feedback! I could implement a generic method for the GetObject<GameObjectType> for those instances where I want to return the specific type. That might be an easy win as far as modifications go, though I could see other games where having separate pool management might be the better way to go.

Good point on the possible removal as a monobehaviour. I could easily stick this in my GameManager class and get rid of one behavior. If I was going for separate pool managers per object spawner, that would make even more sense. (LaserRifle could own the pool of laserbeams) Though I do lose being able to access the pooler directly from the Editor, but I could expose that functionality in the class that holds the pool.

(Though thinking on it, a static pool for a class like LaserRifle could be the way to go, probably depends on the game. Might be a bit more of a nightmare if Unity was multi-threaded, but it's not, so don't have to worry about it)

Honestly, if I was going for more sustainable/reusable code without singletons, I probably should go with a pool<t> for each thing that spawns. It would be a bit more work to do the PoolAll(), but not a huge amount. And that is functionality that most other game types probably wouldn't need.

My tank tactics game, funnily enough, I didn't need an object pooler, at least yet. Tanks only shoot one shell at a time =)

And thanks again for the feedback, as you can see, it got me thinking, which is always good.

February 07, 2016 07:38 PM
tnovelli

Food for thought, yeah. I don't use Unity but it's interesting to see how you're grappling with static vs dynamic types & pooling. These same issues come up everywhere.

February 15, 2016 04:37 PM
ferrous

Yup, everywhere. Random anecdote: Back when I was working on a PS2 game, we had very strict memory budgets for each level. I think it was something like 7MB. Then about halfway through development the programmers bumped it to 9MB. Why? Well someone had declared a static 2MB buffer that no one was actually using.

February 15, 2016 07:23 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement