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.