Asynchronously Instantiate Objects with InstantiateAsync In Unity

Posted by : on

Category : Unity

Introduction

From Unity version 2023.3 which is still in beta at the time of writing, we have the option to use the InstantiateAsync method to instantiate game objects. This can be a game changer for many projects, as we now have the opportunity to improve performance by asynchronously instantiating a game object, or letting Unity do all the necessary performance heavy operations in a different thread except the last stage, and when we want to instantiate our game object, allow Unity to perform the last stage in the main thread.

The InstantiateAsync method, returns an AsyncInstantiateOperation that contains useful information about the progress of the instantiation and gives us control over the instantiation process. It is an asynchronous operation in all but the last stage, which involves the integration phase and the calls to any Awake methods the object has.

Let’s start with simple examples and move on to more technical ones, that involve controlling the asynchronous operation and explanations over the different overloads of the InstantiateAsync method.

Simple Usage

Let’s start by a simple comparison between the Instantiate method and the InstantiateAsync method.

Suppose that we have the following simple script in one game object or prefab that we want to instantiate:

public class ToSpawn : MonoBehaviour
{
    private void Awake() => Debug.Log("ToSpawn Script Awake frame: " + Time.frameCount);
}

Then in an object in our scene, we have the following script that uses the old Instantiate method

private void Start()
{
    Instantiate(objectToSpawn);
    Debug.Log("After the call to Instantiate");
}

When we run our game we will see the following in the console

ToSpawn Script Awake frame: 1
After the call to Instantiate

The operation is synchronous. Unity instantiates the object and then calls the Debug.Log method. Now, let’s replace the Instantiate with InstantiateAsync

private void Start()
{
    InstantiateAsync(objectToSpawn);
    Debug.Log("After the call to InstantiateAsync");
}

The console now looks like this

After the call to InstantiateAsync
ToSpawn Script Awake frame: 2

We can see here that our Debug.Log method executed before the instantiation of our game object. The operation is asynchronous, After the call of InstantiateAsync Unity will continue executing without blocking the main thread, and when the object is ready to instantiate, only then the main thread will execute the final stage of instantiation.

The first four overloads of the InstantiateAsync method, have the same parameters that the Instantiate method has:

public static AsyncInstantiateOperation<T> InstantiateAsync(T original);

public static AsyncInstantiateOperation<T> InstantiateAsync(T original, Transform parent);

public static AsyncInstantiateOperation<T> InstantiateAsync(T original, Vector3 position, Quaternion rotation);

public static AsyncInstantiateOperation<T> InstantiateAsync(T original, Transform parent, Vector3 position, Quaternion rotation);

The difference here is the return type, which is AsyncInstantiateOperation<T>

The AsyncInstantiateOperation Type

The AsyncInstantiateOperation type, has properties and methods that allow us to get information or control the asynchronous operation. The most important of those is the Result property.

The AsyncInstantiateOperation<T> is a generic, the Result property will return an array of T. This is an important difference from the classic Instantiate method, that returns a single object. The reason for this, is that with the InstantiateAsync method, we can now instantiate a number of objects with one statement. We just have to pass a number in the count parameter. The following four overloads of the InstantiateAsync method, are the same as before, only this time they accept a count parameter:

public static AsyncInstantiateOperation<T> InstantiateAsync(T original, int count);

public static AsyncInstantiateOperation<T> InstantiateAsync(T original, int count, Transform parent);

public static AsyncInstantiateOperation<T> InstantiateAsync(T original, int count, Vector3 position, Quaternion rotation);

public static AsyncInstantiateOperation<T> InstantiateAsync(T original, int count, Transform parent, Vector3 position, Quaternion rotation);

Here is an example of their usage, that also uses the Result property:

[SerializeField] private ToSpawn objectToSpawn;
private ToSpawn[] _spawnedObjects;

private void Start()
{
    _spawnedObjects = InstantiateAsync(objectToSpawn, 10).Result;
}

Here the _spawnedObjects is an array that holds our 10 newly created objects.

Inherited Members from the AsyncOperation

AsyncInstantiateOperation inherits from AsyncOperation the progress, isDone, allowSceneActivation and priority members as well as the completed event.

The progress member is a float that shows the progress of the instantiation and can have values between 0 and 1. When the progress becomes 1 the isDone boolean returns true.

The allowSceneActivation, if set to false, stops the instantiation when the progress is at 0.9. This delays the final step of integration of the objects and the execution of their awake methods until the allowSceneActivation is set to true. The reason for this, is that the final step is executed on the main thread. By setting the allowSceneActivation to false, we can delay the instantiation of our objects to a point in time when a delay in the game won’t be noticeable.

Here is an example of how they work:

   [SerializeField] private ToSpawn objectToSpawn;
   private ToSpawn[] _spawnedObjects;
   private AsyncInstantiateOperation<ToSpawn> result;

   private void Start() => Spawn();

   private void Update()
   {
      if(!result.isDone)
         Debug.Log("Press space to instantiate objects, current progress: " + result.progress);
      
      if (Keyboard.current.spaceKey.wasPressedThisFrame)
         result.allowSceneActivation = true;
   }

   private async void Spawn()
   {
      _spawnedObjects = await AsyncInstantiation();
      Debug.Log("Spawn method ended!");
   }
   
   private AsyncInstantiateOperation<ToSpawn> AsyncInstantiation()
   {
      result = InstantiateAsync(objectToSpawn, 10000);
      result.allowSceneActivation = false;
      result.completed += Message;
      return result;
   }
   
   private void Message(AsyncOperation _) => Debug.Log("All objects Instantiated! ");

The AsyncInstantiation method, asynchronously will do all the necessary steps that are needed by Unity to instantiate 10000 objects, except from the last step. This happens because of the result.allowSceneActivation = false statement. Then we register to the completed event the Message method.

The Spawn method is called from the Start method, is async and awaits the AsyncInstantiation method.

The Update method checks the isDone and as long it is false, prints the progress of the asynchronous instantiation. The progress will stay to 0.9 because of the result.allowSceneActivation = false statement. When the space key is pressed, the last step of the instantiation will happen, the ToSpawn game objects will be created on the scene and their Awake methods will be called.

After that, the Spawn method ended! message will be printed from the Spawn method and the All objects Instantiated! message from the event.

The Priority property

The priority property, allows us to define the order the asynchronous operations will be executed. This might sound counterintuitive, after all they are asynchronous, what does order have to do with asynchronous code?

Here’s the catch with Unity’s asynchronous operations: They execute asynchronously with the main thread, but not with each other. Unity’s asynchronous operations, like the InstantiateAsync or the LoadSceneAsync, are added to a queue. Until one completes the others don’t execute.

In a sense, these operations are “almost” asynchronous, they are executed asynchronously from the main thread, but in order between them. That’s where the priority property is useful, to allow us to define the order these operations will be executed.

Other Methods of the AsyncInstantiateOperation

The AsyncInstantiateOperation has three more methods, the Cancel, IsWaitingForSceneActivation and WaitForCompletion.

The Cancel method, cancels the instantiation as long as the isDone property is false. The isDone property is false as long as the final stage of instantiation has not been completed. All the work up to that point will be cleaned up from Unity asynchronously.

The IsWaitingForSceneActivation becomes true, when the allowSceneActivation is false and all the steps for instantiation (except of course from the last step) have been completed. It is the equivalent of the progress being equal to 0.9 when the allowSceneActivation is false.

The WaitForCompletion method, blocks the current thread until the instantiation completes. For example, if we had called it before the return statement in the AsyncInstantiation method from the previous example, we would only see a message from the Update method when the progress would be 0.9, because until then everything else wouldn’t execute.

The Span Overloads Of The InstantiateAsync Method

Finally, the InstantiateAsync method has two more overloads:

public static AsyncInstantiateOperation<T> InstantiateAsync(T original, int count, ReadOnlySpan<Vector3> positions, ReadOnlySpan<Quaternion> rotations);

public static AsyncInstantiateOperation<T> InstantiateAsync(T original, int count, Transform parent, ReadOnlySpan<Vector3> positions, ReadOnlySpan<Quaternion> rotations);

These overloads accept positions and rotations as spans. Because we can instantiate multiple game objects with the InstantiateAsync method, having to manually move and/or rotate after instantiation each one would be tedious. With the Span parameters, we can instantiate them in the place and with the rotation we want. Here’s an example using the previous AsyncInstantiation method:

private AsyncInstantiateOperation<ToSpawn> AsyncInstantiation()
{
    Span<Vector3> positions = stackalloc Vector3[3];
    Span<Quaternion> rotations = stackalloc Quaternion[3];

    positions[0] = new Vector2(1, 1);
    positions[1] = new Vector2(2, 1);
    positions[2] = new Vector2(1, 3);
      
    rotations.Fill(Quaternion.identity);
      
    result = InstantiateAsync(objectToSpawn, 3, positions, rotations);
    result.allowSceneActivation = false;
    result.completed += Message;
    return result;
}

The Span paradox

To someone used to .NET code, this example might seem strange. We can await the AsyncInstantiation method and that means it will be executed asynchronously. If we had tried to declare a Span inside an async method, the compiler would show an error.

This is because Spans are ref structs, and are stored in the stack. The stack of a thread is not available to other threads. If the AsyncInstantiation method, returns an AsyncInstantiateOperation object that can be awaited, how is it possible for the data in the Span to be accessible?

This is because Unity is a C++ engine. Besides the stack and the managed heap, Unity also has the native memory space. Here is where it can store the data it needs for the C++ part of the engine.

The Spans act effectively as a temporary container. Their data is copied to the native memory space and used from there. Access to the stack of the thread is not needed after the copy, that would be impossible. So feel free to use code like the above without fear.

If I had to guess, why Unity decided to use Spans instead of Memory which is stored on the heap, I think because of the performance impact of the copy is less than the performance impact of the garbage collector, plus the problem Unity would have to solve of keeping where in memory of the heap would this data be at any point in time, as it is moving to full compatibility with the .NET and that means having to deal with a moving garbage collector instead of the Boehm one.

Conclusion

This is it about the InstantiateAsync method, for asynchronously instantiating objects. It is still in beta as of this writing, but is here to fill a void between instantiating disabled objects at start, if they are known and then enabling them when they are needed, and object pooling.

Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.


Follow me: