When developing software, performance is one of the most important facets, especially if targeting a platform like web/mobile.
Creating and Destroying objects requires a lot of memory and processing power relative to our other game actions, but we can reduce the impact of Instantiation in Unity by simply reusing them.
In Unity, we can do this by Instantiating all of the objects first, then storing references to them.
We will explore this concept in an example open source game I created ‘slashdot’, which also contains shaders from the last two posts.
We will begin creating the class which will actually handle our pooled objects. When working with pooled GameObjects vs simply Instantiating and Destroying them, we want to be careful of a few key concepts. Firstly, we want to disable most properties for reuse later as opposed to destructing them. Rarely you will need to create and destroy components on initialization, but the vast majority of components or the GameObject itself can be disabled and enabled.
public GameObject enemyPrefab;
public Queue<Enemy> PooledEnemies;
public List<Enemy> TrackedActiveEnemies;
Assign an enemy through the inspector. Next we will create our pools.
Creating the Objects
Call the setup function in the Awake of the class to setup the pool.
void SetupPools()
{
for (int i = 0; i < 100; i++)
{
var enemy = Instantiate(enemyPrefab, Vector3.zero, Quaternion.identity);
PooledEnemies.Add(enemy.GetComponent<Enemy>());
enemy.SetActive(false);
}
}
This will Instantiate all of the objects and keep a reference for us.
Using the Objects
Now, when we want to use a GameObject we can simply call our function in our class from our instance to return a GameObject for us to manipulate.
A super simple implementation might look something like the below.
If only using the <Queue> type and planning for one enemy. However, we want to use multiple enemy types. We can make our pooled enemies a list to have more flexibility. An example implementation for this logic would be an EnemyType enum that the GetEnemy function checks, like so.
public List<Enemy> PooledEnemies = new List<Enemy>();
public GameObject GetEnemy(Enemy.EnemyType enemyType)
{
foreach (var enemy in PooledEnemies)
{
if (enemy.CurrentEnemyType == enemyType)
{
PooledEnemies.Remove(enemy);
return enemy.gameObject;
}
}
}
Now we can simply use this as we would an instantiated object.
randomEnemyType = Random.Range(0, 3) == 0 ? 1 : 0;
var enemy = GetEnemy((Enemy.EnemyType)randomEnemyType);
enemy.transform.position = new Vector3(Random.Range(0,100), Random.Range(0,100), enemy.transform.position.y, 0f);
enemy.SetActive(true);
var enemyComponent = enemy.GetComponent<Enemy>();
enemyComponent.Init();
TrackedActiveEnemies.Add(enemyComponent);
Returning the Object to the Pool
We can use a function like the one below to return a used object to the pool after we are done with it.
public void RemoveEnemy(Enemy enemy)
{
enemy.gameObject.SetActive(false);
TrackedActiveEnemies.Remove(enemy);
PooledEnemies.Add(enemy);
}
Simply call RemovePooledEnemy() wherever needed.
Manager.Instance.RemoveEnemy(this);
Re-using Objects
Most of the quirks that you’ll encounter from pooling GameObjects like this stem from figuring out how to reset everything nicely. Unity doesn’t run most code on disabled objects; it’s usually preferable to reset things on Init to avoid unexpected behavior.
I recently saw these UI effects in a game called Cult of the Lamb and they were very satisfying to watch. Let’s learn how to create our own types of effects like these.
We want to begin with a base shader to manipulate, so let’s start by displaying a sprite.
Our shader must expose it to the editor in order to set our texture. Add a line under our properties defining a main texture.
_MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}
And the variable under SubShader.
sampler2D _MainTex;
float4 _MainTex_ST;
The _ST value will contain the tiling and offset fields for the material texture properties. This information is passed into our shader in the format we specified.
We want to add some movement and distortion to our sprite. Begin with movement.
How can we manipulate our shader pixels? Let’s show an example by modifying our main texture. We’ll simply change the position. To do so, we can do something simple like shifting the texture coordinate down and to the left.
If you examine your sprite at this point, you may notice some odd distortion as it moves.
Set your sprite’s import settings correctly! Mesh Type: Full Rect Wrap Mode: Repeat
Once you ensure your sprite has the correct import settings, it’s time to introduce our final 2d sprite we want to manipulate with the shader to achieve our effect.
This image will greatly change the shader appearance, and you should try different gradients and patterns. Here’s my image scaled up:
But I recommend using the smallest resolution that looks good for your project due to memory and performance.
We also need a seamless noise texture, for the distortion.
Let’s add another variable for it.
_NoiseTex ("Base (RGB) Trans (A)", 2D) = "white" {}
Once we’ve assigned our noise texture, it’s time to start moving it.