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);
	}
	
}

Thursday, September 12, 2013

Unity3D - Modify Textures Procedurally

Procedurally created textures are very useful tools within Unity. Not only can they be used to create textures that the players will see, but they are also helpful when you need to visualize the results of some algorithms.

I'll get you started by showing you an example. Our goal is to write a function to visualize how texture coordinates are organized on a Unity plane object.

private void SetTexture (int width, int height) {
 Texture2D texture = new Texture2D(width, height);
 Color[] pix = new Color[texture.width * texture.height];
 renderer.material.mainTexture = texture;
 
 
 for (int y = 0; y < texture.height; y++) {
  for (int x = 0; x < texture.width; x++) {
   pix[y*texture.width + x] = new Color((float)x/texture.width,0f,(float)y/texture.height);
  }
 }
 
 texture.SetPixels(pix);
 texture.Apply();
}
This function first creates a new Texture2D and an array of Color objects that will hold the color values for the pixels in our new texture. Then it sets the main texture of the renderer to our newly created Texture2D object.

The for loops iterate through each pixel in the texture. Take note of how the correct pixel index is referenced in the pix array. Since we are using a 1-dimensional array to represent a 2-dimensional array of data, we have to do something a little special here. In the pix array, all the color values for the first row of pixels will be stored in indices 0 through n-1 where n is the the width of the texture (stored in texture.width in our case). The second row of pixels will be stored in indices n through 2n-1. This pattern continues throughout however many rows of pixels there are. Therefore, we can find the index to the pixel located at (x,y) by multiplying y by the width of the texture and then adding x.

We want to color each pixel in such a way that we can get the information we want just by looking at it. In this example, we set the red value of each pixel to be x/texture.width, the green value to 0, and the blue value to be y/texture.height. This will mean that pixels at higher x values will be more red, and pixels at higher y values will be more blue.

After setting the color of all the pixels, we have to set the pix array as the array of pixels that our texture uses. We do this with texture.SetPixels(pix). Lastly we must call texture.Apply().

Applying this algorithm to a plane in unity gives us this result:


For the pixel at (0,0), all three of the red/green/blue values of the color should be 0, giving us black. Looking at our plane, we can now deduce that this particular pixel is in the top right corner. Note that the way I have the plane oriented in the picture, its local axes line up with the global axes. We can also deduce that the last pixel is located at the bottom left since at that location, both red and blue are at maximum, giving us purple.

By changing the texture on the plane procedurally, we are able to learn that on the primitive unity plane, the x coordinate of the texture goes from the positive x direction to the negative x direction, and the y coordinate goes from the positive z direction to the negative z direction.

This technique can be used in many different applications to help you to quickly visualize something that might otherwise be more difficult. For example, I have used it to visualize the distribution of randomly generated islands in an ocean.