Chapters

Hide chapters

Unity Apprentice

First Edition · Unity 2020.3.x LTS Release · C# · Unity

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

11. Asynchronous Functions, Coroutines & Object Pooling
Written by Matt Larson

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

while (transform.position != someTargetPosition)
{
    MoveTowards(someTargetPosition);
}

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.

Soem Ewolefeuq LuaNuvuabote xaojl rapidx VruykBekaeduca(PaeMaceukuwa)

IEnumerator DieCoroutine()
{
    // 1. Delay for 5 seconds.
    yield return new WaitForSeconds(5);
    // 2. Destroy the GameObject
    Destroy(gameObject);
}
StartCoroutine(DieCoroutine());

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.

Huex Opolofuah BoiUxcwb otiad iqxbz Zaxh ab virbcujah!

public async void DieAsync()
{
    // 1.
    await Task.Delay(2000);
    // 2.
    Destroy(gameObject);
}
projectile.GetComponent<ProjectileBehaviour>().DieAsync();

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).

public interface IPoolable
{
    // 1.
    void Reset();

    // 2.
    void Deactivate();

    // 3.
    void SetPool(ObjectPool pool);
}
public class ProjectileBehaviour : MonoBehaviour, IPoolable

Complete the ObjectPool

First, create the methods that will provide a reusable object pool for either type of GameObject.

private Queue<GameObject> pool = new Queue<GameObject>();
// 1.
IPoolable poolable = anObject.GetComponent<IPoolable>();
if (poolable != null)
{
    // 2.
    pool.Enqueue(anObject);
    poolable.SetPool(this);
}
// 1.
if (pool.Count > 0)
{
    // 2.
    GameObject toReturn = pool.Dequeue();
    toReturn.GetComponent<IPoolable>().Reset();
    return toReturn;
}
// 1.
IPoolable poolable = anObject.GetComponent<IPoolable>();
if (poolable != null)
{
    // 2.
    poolable.Deactivate();
    Add(anObject);
}
// 1.
for (int i = 0; i < PoolSize; i++)
{
    // 2.
    GameObject poolMember = Instantiate(Prefabs[i % Prefabs.Length],
                                        transform);
    // 3.
    poolMember.SetActive(false);
    Add(poolMember);
}

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.

if (ProjectilePool)
{
    ProjectilePool.Return(gameObject);
}
// 1.
gameObject.SetActive(true);
gameObject.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
// 2.
DieAsync();
gameObject.SetActive(false);

Updating the projectile launch

The next change needed for ProjectilePool is to pull GameObjects from the ProjectilePool instead of instantiating them.

public ObjectPool ProjectilePool;
GameObject projectile = ProjectilePool.Get();

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.

if (EnemyPool)
{
    EnemyPool.Return(gameObject);
}
state = States.Ready;
characterAnimator.SetBool("Death", false);
agent.isStopped = true;
gameObject.SetActive(false);
gameObject.GetComponent<NavMeshAgent>().enabled = false;

public ObjectPool EnemyPool;
GameObject enemy = EnemyPool.Get();

Key points

  1. Coroutines and async/await are two ways you can implement asynchronous operations for timed delays to actions.
  2. 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.
  3. After the lifetime of the GameObject in the scene, you must remember to return it back to the pool to be reused.
  4. 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.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now