11 Asynchronous Functions, Coroutines & Object Pooling¶
Introduction¶
The veggie gladiators are pouring through the gates! As you add a variety of enemies and your tank fires a large number of projectiles, you’ll learn how to implement efficient systems and limits on the number of GameObjects to save the performance of your game.
Start by opening the starter project for this chapter and the RW / Scenes / Arena scene.
Play the game and fire several projectiles. You’ll see the accumulation of GameObjects in the Hierarchy.
Timed destruction of GameObjects¶
You need mechanisms to clean up the Hierarchy from the projectiles and enemies after they expire. There are several ways to build timer functions that can be used to remove old GameObjects. You’ll explore two patterns for writing asynchronous timer functions: coroutines and async/await.
Synchronous and asynchronous functions¶
When you write code, you’re giving your game a set of instructions to run. It will complete those instructions in the order they’re written. This is called synchronous programming. It works well because you can determine what your game will do next.
However, it’s not so good when you want something to happen over a period of time or frames. For example, if you were to write something like this:
while (transform.position != someTargetPosition)
{
MoveTowards(someTargetPosition);
}
The code in the while
loop would be called multiple times in the same frame — or “tick” — until your object had moved to where you wanted it to be. But to the user, it would appear as if it happened instantly.
Asynchronous methods are run on their own independent paths — so they don’t block the main flow of your game. You can even run multiple asynchronous tasks at the same time.
Understanding coroutines¶
In Unity, you can write coroutine methods for your asynchronous code. A coroutine must be called using StartCoroutine(MyCoroutine);
, and the coroutine needs to be an IEnumerator
method. At the end of each frame, Unity will return back to the MonoBehaviour
and continue execution at the statement immediately after the statement beginning with yield return
.
It’s time to write your first coroutine method to remove an enemy GameObject whenever an enemy is killed. Open the EnemyController script from the RW / Scripts folder. Within the class, add the following method:
IEnumerator DieCoroutine()
{
// 1. Delay for 5 seconds.
yield return new WaitForSeconds(5);
// 2. Destroy the GameObject
Destroy(gameObject);
}
Here’s what you’re doing:
- When you use a
yield
instruction, you’re saying to theIEnumerator
, “OK, we’re done for now. Come back later.” You can specify when to return using built-in methods such asWaitForSeconds
orWaitForEndOfFrame
. - In this case, after the wait provided by
WaitForSeconds(5)
has occurred, execution is returned back to the next instruction after theyield
, where you destroy the GameObject.
Calling this coroutine when an enemy is defeated will therefore allow a delay for the enemy death animation before the enemy GameObject is destroyed.
To do that, find the method OnTriggerEnter
, and at the start of the block if (state != States.Dead)
, add the following:
StartCoroutine(DieCoroutine());
When a character is killed by a projectile or tank, the coroutine will now be called and enemies will be removed from the scene after a delay of 5 seconds after their death. The coroutine provides the asynchronous behavior of handling the timing while the gameplay can proceed in parallel.
Save your changes, head back to the Unity Editor and play the scene.
A new alternative: Async/Await¶
In recent versions of Unity, there’s a new mechanism to provide asynchronous behavior: the pattern called Async/Await. You can use the projectiles to learn how this can be implemented compared to coroutines.
With the Async/Await pattern, you create an async
method that includes an instruction — Task.Yield()
. These are quite similar to the IEnumerator
and yield return
statements from the coroutine pattern.
The keyword async
defines a method that can run asynchronously. Put the async
between the access modifier (public, private, etc.) and the return type.
Open the RW / Scripts / ProjectileBehaviour script and add the following method to the class:
public async void DieAsync()
{
// 1.
await Task.Delay(2000);
// 2.
Destroy(gameObject);
}
Compare this with the coroutine function above:
- Instead of the
yield
, you have anawait
instruction that acts on aTask
, which in this case provides a delay for 2000 milliseconds (2 seconds). TheTask
is a class that represents a single operation that can run asynchronously. - After the
Task
asynchronously completes, execution continues to theDestroy
call to remove this projectile GameObject.
You need to start your async function after the projectile is instantiated, so open the RW / Scripts / PlayerController script and find the OnFire()
method. Towards the bottom of this method, add the following to the bottom of the if (projectile)
block:
projectile.GetComponent<ProjectileBehaviour>().DieAsync();
Save all your changes in your scripts and run the scene. Fire some projectiles and after 2 seconds, the timed calls to Destroy
clean up the used projectiles.
Note: Which pattern should you use? If you prefer the style of the coroutine, feel free to use it, but often the async/await pattern is a better option. In many ways, coroutines were a precursor to async/await. The biggest advantage of async/await is its overall simplicity and common approach to asynchronous code in C# outside of Unity. Either mechanism in Unity will asynchronously run your code on the main thread of the game avoiding conflicts that could occur with asynchronous multithreading. One of the advantages of async/await is that it allows you to return a value — which you can’t do with an
IEnumerator
method. If you need to stop your asynchronous methods from outside of the method, there are built-in functions for coroutines such asStopCoroutine
, however, there aren’t any easy built-in methods for async/await. To read more about coroutines, check out the Unity Official Documentation. And, to learn more about Asynchronous Programming in Unity, check out this great tutorial on raywenderlich.com.
Object pooling¶
The asynchronous timer methods help to clean up the scene, but there’s a performance impact when you create and destroy GameObjects. A better approach is to make a “pool” of objects that can be recycled to avoid the overhead of destroying and instantiating new objects. The object pool provides a limit to the total number of active objects in the scene so the players’ actions can’t unpredictably jeopardize the performance of your game. Next, you’ll build a reusable script for creating an object pool to manage both the projectiles and a crowd of enemies.
How to implement object pooling¶
You can construct an object pool by completing the RW / Scripts / ObjectPool script. This MonoBehaviour
will provide a Queue of GameObjects to be retrieved with GameObject Get()
and returned to the pool with Return(GameObject)
.
Before you can create a generic ObjectPool
class, there are certain rules that any poolable GameObject must consider. If you open the RW / Scripts / IPoolable interface, you see it defines the methods any poolable GameObject should implement in a component script:
public interface IPoolable
{
// 1.
void Reset();
// 2.
void Deactivate();
// 3.
void SetPool(ObjectPool pool);
}
A C# interface like this sets the basic rules that scripts must follow to:
- Implement a method that returns it back to a ready state in
Reset()
. - Implement a method that defines what happens when it goes back to the pool in
Deactivate()
. - Provide a helper method to track what pool it belongs to.
To use the IPoolable
interface, your class must inherit it. For example, if you look at ProjectileBehviour
, you’ll see that it does just that:
public class ProjectileBehaviour : MonoBehaviour, IPoolable
Once you state this, the class must implement the interface’s methods. So in this case, it needs to have methods called Reset()
, Deactivate()
, and SetPool(ObjectPool pool)
defined. Stubs for these methods have already been added to ProjectileBehaviour
and EnemyController
.
Later, you’ll update these methods to follow the rules above so that the ProjectileBehaviour
and EnemyController
are ready for action.
Complete the ObjectPool¶
First, create the methods that will provide a reusable object pool for either type of GameObject.
Open the RW / Scripts / ObjectPool. The class has pre-defined variable called pool
:
private Queue<GameObject> pool = new Queue<GameObject>();
This is a Queue
of GameObjects that contains a “first-in, first-out” queue of any reusable GameObject.
To add objects to the pool
queue, add the following to the Add(GameObject anObject)
method:
// 1.
IPoolable poolable = anObject.GetComponent<IPoolable>();
if (poolable != null)
{
// 2.
pool.Enqueue(anObject);
poolable.SetPool(this);
}
Going through this:
- First check if the supplied GameObject has the necessary
IPoolable
component script. - If it does, put it into the
pool
queue to be used later.
Now to request a poolable GameObject from the queue. Add the following to the Get()
method stub, above the return null;
statement:
// 1.
if (pool.Count > 0)
{
// 2.
GameObject toReturn = pool.Dequeue();
toReturn.GetComponent<IPoolable>().Reset();
return toReturn;
}
- Check if the pool has any members.
- If so, reset the state of the pool member so it’s ready to be used, and return it.
You now need a way to put a GameObject back in the pool, Add the following to Return(GameObject anObject)
:
// 1.
IPoolable poolable = anObject.GetComponent<IPoolable>();
if (poolable != null)
{
// 2.
poolable.Deactivate();
Add(anObject);
}
- First check if this the supplied object is an
IPoolable
member. - If so, deactivate the poolable object, then add it back to the pool.
The last method you need to write is a helper to construct a pool from a set of prefabs. You’ll use this to create a pool of projectiles or various enemies. Add the following to the Awake()
method:
// 1.
for (int i = 0; i < PoolSize; i++)
{
// 2.
GameObject poolMember = Instantiate(Prefabs[i % Prefabs.Length],
transform);
// 3.
poolMember.SetActive(false);
Add(poolMember);
}
This method will build a pool by instantiating one or more example prefabs provided by the array of Prefabs
.
Going through this:
- The number of objects in the pool will be determined by the
PoolSize
variable. So loop around for that number of times. - Instantiate the next prefab from the
Prefabs
array. This uses modular arithmetic; just as you can count 24 hours on a clock face without using a number beyond 12, this allows you to count the full pool size without extending beyond the length of yourPrefabs
array. - Make the pool member inactive in the scene before adding it to the pool.
Save your changes and head back to the Unity editor.
Creating an object pool for projectiles¶
Find and select the empty Actors / Projectiles GameObject in the Hierarchy and add the component script Object Pool. Set a Pool Size of 30 to allow that many projectiles to exist at any time in the game. Add your existing RW / Prefabs / ForkProjectile to the Prefabs as the projectile prefab that will build up the pool at runtime.
The ForkProjectile already has a component ProjectileBehaviour, but you need to define what happens when a projectile is reset and when it’s deactivated upon returning to a pool.
Open the RW / Scripts / ProjectileBehaviour script.
In DieAsync()
, replace Destroy(gameObject);
with:
if (ProjectilePool)
{
ProjectilePool.Return(gameObject);
}
This will now return the GameObject to the pool instead of destroying it.
The Reset()
method needs to provide a ready-to-use projectile and is called automatically when requesting a GameObject with ObjectPool.Get()
. This method needs to clear the velocity of the projectile and keep it active for a couple of seconds before it’s returned to the pool automatically.
Add the following to the Reset()
method:
// 1.
gameObject.SetActive(true);
gameObject.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
// 2.
DieAsync();
Going through this:
- To reactivate a projectile, make the GameObject active and clear its velocity.
- Start the async method to return it back to the pool.
Finally, you need to implement the Deactivate()
method that will be called by ObjectPool.Return(GameObject)
when returning the projectile to its pool:
Add the following to Deactivate()
:
gameObject.SetActive(false);
This simply sets the GameObject inactive to hide it from view and stop all motion.
Save your changes. You’ve now completed the major logic for the projectile pool — ready now to reuse, reduce and recycle those projectiles!
Updating the projectile launch¶
The next change needed for ProjectilePool is to pull GameObjects from the ProjectilePoolinstead of instantiating them.
Open the RW / Scripts / PlayerController script and replace the line public GameObject Projectile;
with the following:
public ObjectPool ProjectilePool;
Next, in OnFire()
replace GameObject projectile = GameObject.Instantiate(Projectile);
with:
GameObject projectile = ProjectilePool.Get();
This retrieves a ready-to-use ForkProjectile that can fire from the tank.
Since the projectile now starts the DieAsync()
coroutine as part of the Reset()
pool method, you can also remove projectile.GetComponent<ProjectileBehaviour>().DieAsync();
from the if (projectile)
block of OnFire()
.
Save your changes and head back to the Unity editor. Select Actors / Tank in the Hierarchy, and in the Player Controller component in the Inspector, assign the Projectiles GameObject to the Projectile Pool field:
Save your scene. Play it and observe that ForkProjectiles now fill the Projectiles GameObject and become activated and inactivated as they’re fired by the tank and returned back to the pool.
Creating an object pool for the enemies¶
Creating a pool of different varieties of enemy gladiators is even easier now that the ObjectPool
is defined.
Open the RW / Scripts / EnemyController script and in DieCoroutine()
, change Destroy(gameObject)
with:
if (EnemyPool)
{
EnemyPool.Return(gameObject);
}
This is the same as what you did for the projectiles earlier.
It’s critical to reset the enemy state when obtaining an enemy from the pool when calling ObjectPool.Get()
, so add the following to the Reset()
method:
state = States.Ready;
characterAnimator.SetBool("Death", false);
This resets the enemy to be ready for battle.
Now, add the following to Deactivate()
:
agent.isStopped = true;
gameObject.SetActive(false);
gameObject.GetComponent<NavMeshAgent>().enabled = false;
Remember that when a GameObject is returned to the pool with ObjectPool.Return(GameObject)
the Deactivate()
method is called. So this is where you hide the enemy by inactivating the character and disable its navigation.
Save the script and go back to the Unity editor.
Find the empty Actors / Enemies GameObject in the Hierarchy and add the Object Poolcomponent to it. Set a Pool Size of 50, and assign the Warrior_Carrot Variant, Warrior_Pepper Variant and Warrior_Potato Variant prefabs to the pool by dragging them from the RW / Prefabs folder.
Lastly, open the RW / Scripts / GateSpawner script. Here, you again need to replace the GameObject variable with an ObjectPool
variable.
At the top of GateSpawner
, replace public GameObject Enemy;
with:
public ObjectPool EnemyPool;
Now, in SpawnEnemies()
, use your new variable by replacing GameObject enemy = Instantiate(Enemy, Gate.transform.parent);
with:
GameObject enemy = EnemyPool.Get();
Save your changes, and in the Unity editor, select all four of the GateWall VariantGameObjects in the Hierarchy, find their Gate Spawner component, and assign all to have the Enemies GameObject in the Hierarchy to the Enemy Pool field in the component.
Now, play the scene and you’ll see a varied attack force of slow potatoes, carrots and spicy peppers!
Within the Enemies container, the same enemies are being reactivated and recycled to use in the scene.
Congratulations! You’ve made the game more complex with new enemies, and you’ve mastered efficiency with asynchronous operations and object pooling!
Key points¶
- Coroutines and async/await are two ways you can implement asynchronous operations for timed delays to actions.
- Object Pools are a reusable mechanism for managing large numbers of GameObjects in a scene — such as Projectiles, Enemies and other spawnable elements. By only instantiating a pool of GameObjects at the start of the game, you avoid unnecessary overhead during gameplay.
- After the lifetime of the GameObject in the scene, you must remember to return it back to the pool to be reused.
- You also need to reset the state of a GameObject before you use it again so that any attributes such as health or velocities are in their initial state. Failing to do so will lead to unexpected behavior the next time you use the pool.
Continue to the next chapter to see what becomes of all the defeated veggie gladiators and what’s cooking down below!