Ensuring immersive user experience quality
This guide shows you how to ensure that your Unity apps meet the following quality guidelines:
Background
In Unity, app code runs on the main thread. When your app performs a CPU intensive task, such as loading a complicated scene or initializing app state for a large number of game objects, the main thread can become blocked. This can result in full or partial loss of head tracking.
Users experience this as the world appearing to be locked to head movement or as black borders intruding from the edge of the display. These black borders are caused by the asynchronous reprojection feature having to apply a significant translation to a very old image frame generated when the user was previously looking in a different direction.
Unblocking the main thread
There are a number of techniques you can use to help avoid blocking the main thread for a long time and help ensure that your app maintains head tracking.
Identify specific performance issues
Use the Unity CPU Profiler to identify performance issues in your code.
Once you've identified a problematic section in your code, use the techniques below to avoid blocking the main thread.
Defer initialization work
Identify work that it done in Awake()
or OnEnable()
that can be deferred to
a later time, using a Coroutine, or a refactored Start()
method.
Refactor expensive Start()
methods
Replace an expensive void Start() { … }
script block with one that
makes use of the IEnumerator
pattern in order to defer some or all of the
work to subsequent frames. Here is an example:
IEnumerator Start() {
// Lets say we need to create 1,000 game objects, but we've determined that
// we can only create approximately 100 at a time while maintaining framerate.
for (int i=0; i < 10; i++) {
// Wait until the next frame before continuing the loop. By starting
// with a yield statement, we don't start any work until the next frame.
yield return null;
// Set up 100 game objects (1/10th of the total work).
SetupOneHundredGameObjects();
}
}
Refactor expensive game logic
Similarly, you might be able to replace an expensive section in your script with a Coroutine. This lets you perform a small amount of work in each frame before yielding control back to the game engine.
Use a loading scene
If your initial scene takes too long to load, consider adding a loading scene that provides feedback to the user. You can display a VR splash screen and/or provide audio feedback while your main scene loads.
Use asynchronous scene loading
When loading a new scene, start by using LoadSceneAsync and experiment with different values for Application.backgroundLoadingPriority. You can use the returned AsyncOperation to monitor or control the loading of the scene.
If your app still freezes or loses head tracking, use the
Unity CPU Profiler
to find expensive script operations, including any Awake()
or Start()
functions in the new scene and OnDestroy()
methods in the old scene.
There are a few more tips in
this Unity post.
Use a background thread
Move an expensive task off the main thread onto a background thread. However, note that Unity APIs may only be called from the main thread.
Hiding effects of a blocked main thread
Even with other optimizations, you might still find it difficult to maintain head tracking and frame rate at certain points in your app.
You can work around these issues by hiding the effects of a blocked main thread. For example, when transitioning between two levels in a game, you can fade the camera to black, spend a few hundred milliseconds on expensive computational work, and then fade back into the new scene.
Fade to black
To create a fade in and out effect, attach the ScreenFade
script (and the
material and shader by the same name) in
daydream-elements
to the main camera.
See the LevelSelectButton
script for an example of how to use screen fade.
If you fade to black while loading a scene, try using the synchronous
LoadScene()
method, as this allows the main thread to load the new scene slightly faster.Alternatively, you may find that you can start loading the next scene asynchronously using
LoadSceneAsync()
before fading to black and then delay the fade slightly using a Coroutine.This lets your app:
Provide audio feedback
Consider providing audio feedback (music or other sound) while the screen is faded to black to let users know that the app is still running. This is especially important if screen remains black for a long time, for example while loading and initializing a level.