Tuesday, September 17, 2013

Unity 3D - Recycling Bins: An Object Pooling Solution

Object pooling in this article refers to the reuse of game objects that would otherwise be frequently destroyed and re-created. Destroying game objects in Unity leads to quicker memory fragmentation and more work for the garbage collector (see the article by whydoidoit here for more detailed information).

My solution to this was to create two classes: RecycleBin and RecycleBinManager. The RecycleBin class is in charge of keeping up with all of the instances of a certain prefab, and manages their reuse. The RecycleBinManager is responsible for keeping track of the recycling bins, and giving out the reference to the correct one when needed (a single recycling bin instance only deals with objects of a single type of prefab).

To use the sysem, you first request a reference to the correct recycle bin from the RecycleBinManager, then use that reference whenever you would normally instantiate or destroy instances of the associated prefab. In essence, the bin just disables objects that you tell it to remove, and then relocates and re-enables them later, avoiding some of the potential performance problems caused by using Destroy() and Instantiate().

RecycleBin Class

Here is the code for the RecycleBin class:

public class RecycleBin {
	public GameObject prefab;
	Queue<gameobject> _safeToMove = new Queue<gameobject>();
	public Transform folder;
	
	
	public RecycleBin (GameObject prefab) {	
		this.prefab = prefab;
		folder = new GameObject(prefab.name + " Recycling Folder").transform;
	}
	
	public GameObject Add (Vector3 position, Quaternion rotation) {
		GameObject objReference;
		
		// If we don't have any disabled GameObjects that we can reuse, make a new one
		if (_safeToMove.Count == 0) {
			objReference = GameObject.Instantiate(prefab, position, Quaternion.identity) as GameObject;
			objReference.transform.parent = folder;
			objReference.transform.rotation = rotation;
			return objReference;
		}
		
		// If the _safeToMove queue has something in it, relocate and enable it
		else {
			objReference = _safeToMove.Dequeue();
			objReference.transform.position = position;
			objReference.transform.rotation = rotation;
			objReference.SetActive(true);
			return objReference;
		}
	}
	
	public void Remove (GameObject go) {
		_safeToMove.Enqueue(go);
		go.SetActive(false);
	}
	
}


RecycleBin contains two methods:
  • Add (Vector3 position, Quaternion rotation) - Call this function whenever you would normally instantiate an instance of the prefab. It returns a reference to the GameObject that was just placed.
  • Remove (GameObject go) - Call this function whenever you would normally destroy one of the instantiated prefabs. Give it a GameObject reference that was returned by the Add() function to remove that object.
The constructor requires that you specify the prefab that the bin will be responsible for. It also creates an empty GameObject that will be used as a folder. All objects created through the recycling bin will have their parent set as that empty GameObject to keep the hierarchy window tidy.

RecycleBinManager Class

Here is the code for the RecycleBinManger class:
public class RecycleBinManager : MonoBehaviour {
	
	private static List<recyclebin> _objectCollections = new List<recyclebin>();
	private static Transform _recyclingBinFolder = new GameObject("Recycle Bins").transform;
	
	public static RecycleBin GetRecycleBin (GameObject prefab) {
		foreach (RecycleBin objCollection in _objectCollections) {
			if (objCollection.prefab == prefab) {
				return objCollection;	
			}
		}
		// If we didn't find one that matched, it dosn't exist yet, so create a new one.
		RecycleBin newObjectCollection = new RecycleBin(prefab);
		newObjectCollection.folder.parent = _recyclingBinFolder;
		_objectCollections.Add(newObjectCollection);
		return newObjectCollection;
	}
		
}


The manager keeps up with all of the different recycling bins. It's only method is a static function that retrieves or creates the recycling bin for a given prefab. It also creates an empty GameObject that is used as a folder for all of the recycling bin folders to keep the hierarchy in the inspector tidy.

Note that you will need to include
using UnityEngine;
using System.Collections.Generic

Example

Here is a quick example on how to use the recycling bins. Create a c# script called RecyclingTest.cs, and place in it the following code:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class RecyclingTest : MonoBehaviour {
	
	public GameObject prefab;
	private RecycleBin _bin;
	private Queue<gameobject> _gameObjectQueue = new Queue<gameobject>();
	private float _xLoc;

	void Start () {
		_bin = RecycleBinManager.GetRecycleBin(prefab);		
	}

	
	void OnGUI () {
		if (GUI.Button(new Rect(10, 10, 100, 50), "Add Object")) {
			Vector3 newLocation = new Vector3(_xLoc, 0f, 0f) + transform.position;
			_xLoc += 2f;
			GameObject newGameObject = _bin.Add(newLocation, Quaternion.identity);
			_gameObjectQueue.Enqueue(newGameObject);	
		}
		if (GUI.Button(new Rect(10, 100, 100, 50), "Remove Object")) {
			if (_gameObjectQueue.Count &gt; 0) {
				_bin.Remove(_gameObjectQueue.Dequeue());	
			}
		}
		
	}
}

You probably also want to add a directional light so you can see a bit better.

Create a new empty game object, name it Recycle Test, and attach the RecyclingTest script to it. Then add a prefab to the prefab variable on the script in the inspector. Any prefab will do. I created a new one that was just a cube.

Hit the play button. There will be two buttons. When you click the Add Object button, an instance of your prefab will appear. If you don't see the cubes, you may need to re-position the main camera. Hit the button a few more times to make more instances of the prefab. If you look in the hierarchy window, you'll notice a new object there called "Recycling Bin." Expand it and you will find a recycling folder for your prefab. Inside there will be the instances of the prefab.



Leave the hierarchy of the recycling bin expanded and watch what happens when you hit the remove object button. An object will disappear from the game window, and one of the items in the recycling bin folder will be grayed out, meaning that it is now disabled.

Now hit the add button again. Another object will appear in the game window, and one of the items from the hierarchy that was previously grayed out is now enabled again. This is because the recycling bin reused one of the old objects by relocating it to a new position and re-enabling it.

Take a look at the code in RecyclingTest.cs. In the start function, we are getting the recycling bin instance for the prefab, and caching it in the _bin variable. In the OnGUI() function, when the Add Object button is pressed, we first determine the location of the next object, and then make a call to bin.Add(), passing in the position and rotation that we want the new object to have. Also, we keep up with a reference to the game object by placing it in a queue.

When the Remove Object button is pressed, we check to see if there are any objects to remove by checking if the queue has anything in it. If so, we get a reference to an object from the queue, and tell the bin to remove it.

You can follow the same general method outlined in this example to reuse objects of any number of different types of prefabs in your project. If we were to create another instance of RecyclingTest and gave it a different prefab, second RecycleBin would be created to hold those instances, and you would see another folder in the hierarchy.

Conclusion

By reusing old objects when available, but still creating new ones as needed, this solution is both efficient and flexible. Optionally, Instead of letting the bins instantiate new objects as the game goes, a sufficient number of objects can be "pre-loaded" at the start of a scene by adding and then removing them so that most or all future adds are re-uses.

As a real-world example, a project I am working on involves a number of different islands with similar trees (not unity terrain trees). As the player goes from island to island, the same tree objects are re-used using this system.

Full Code for RecycleBinManager.cs

using UnityEngine;
using System.Collections.Generic;

public class RecycleBinManager : MonoBehaviour {
	
	private static List<RecycleBin> _objectCollections = new List<RecycleBin>();
	private static Transform _recyclingBinFolder = new GameObject("Recycle Bins").transform;
	
	public static RecycleBin GetRecycleBin (GameObject prefab) {
		foreach (RecycleBin objCollection in _objectCollections) {
			if (objCollection.prefab == prefab) {
				return objCollection;	
			}
		}
		// If we didn't find one that matched, it dosn't exist yet, so create a new one.
		RecycleBin newObjectCollection = new RecycleBin(prefab);
		newObjectCollection.folder.parent = _recyclingBinFolder;
		_objectCollections.Add(newObjectCollection);
		return newObjectCollection;
	}
		
}

public class RecycleBin {
	public GameObject prefab;
	Queue<GameObject> _safeToMove = new Queue<GameObject>();
	public Transform folder;
	
	
	public RecycleBin (GameObject prefab) {	
		this.prefab = prefab;
		folder = new GameObject(prefab.name + " Recycling Folder").transform;
	}
	
	public GameObject Add (Vector3 position, Quaternion rotation) {
		GameObject objReference;
		
		// If we don't have any disabled GameObjects that we can reuse, make a new one
		if (_safeToMove.Count == 0) {
			objReference = GameObject.Instantiate(prefab, position, Quaternion.identity) as GameObject;
			objReference.transform.parent = folder;
			objReference.transform.rotation = rotation;
			return objReference;
		}
		
		// If the _safeToMove queue has something in it, relocate and enable it
		else {
			objReference = _safeToMove.Dequeue();
			objReference.transform.position = position;
			objReference.transform.rotation = rotation;
			objReference.SetActive(true);
			return objReference;
		}
	}
	
	public void Remove (GameObject go) {
		_safeToMove.Enqueue(go);
		go.SetActive(false);
	}
	
}

3 comments:

  1. Looking interesting... how this will work with huge amount of bullets prefabs?
    can i expect performance improvement with this?

    ReplyDelete
    Replies
    1. It would work well with bullet prefabs. It requires less overhead than repeatedly instantiating and destroying the bullets as you avoid unneeded calls to Instantiate(), and it will avoid all the garbage collection from if you would have destroyed the bullets normally.

      Delete
  2. well, in that case i will give it a try. thanks for sharing!

    ReplyDelete