In order to ship an application on the Quest Store, the app needs to maintain a solid 72 frames per second. This is not an easy feat, but by following
documentation and best practices to optimize performance, it should be readily possible. However, games are getting bigger and bigger, and it’s becoming harder and harder to keep everything in memory at once. There needs to be a way to load the parts of the game you need, and unload parts that you don’t, and it has to be done without the player noticing. The solution is streaming, which in Unity is usually achieved using asynchronous scene loading.
Streaming is a pretty well defined problem, and the techniques to achieve it are fairly well
documented. However, there are a few gotchas that can really bite you if you aren’t aware of them, and make what should be a smooth transition into a hitchy mess. Here’s what you need to know to avoid that, and some tips and tricks to help you along the way.
Note: All suggestions below were based on behavior in Unity 2020.3.8f1. Performance results may be different in other versions of Unity.
Why is my ‘Async’ load still causing frame drops?Loading a scene from disk can take a long time, especially with a number of meshes and textures. It’s a lot of data to read. That’s why Unity provides a method to load a scene asynchronously, SceneManager.LoadSceneAsync(). This is great, in that you don’t have to display a loading screen while waiting for that data to be loaded, however, you may have noticed that when you set the allowSceneActivation flag to true, you get one very long frame. “What gives, I thought this was supposed to be asynchronous?” you may ask yourself. Well here’s the deal with Unity:
All objects in Unity obey the Unity lifecycle (e.g., Awake, OnEnable, Start, Update, OnDisable, OnDestroy).
All lifecycle methods execute on the main thread.
Therefore, when you tell your scene to activate, Unity has to run the first step of the Unity lifecycle for every single object in your scene.
“But I don’t have any scripts attached to my objects” you may say. That may be true, but did you know that GameObjects and Transforms have their own Unity-internal version of “Awake”? Yes, every GameObject in your scene, active or inactive, will add to the time it takes for the scene to activate. This leads us to our first tip:
Tip #1: Reduce the Number of GameObjects in your SceneI ran a test where I asynchronously loaded scenes with various numbers of game objects, and measured the length of the frame where I set allowSceneActivation to true. These are the results from running on Quest 2:

As you can see, the time it takes for the scene to activate is linearly dependent on the number of GameObjects in the scene. There is no difference between active and inactive GameObjects in this case.
How do I reduce the number of GameObjects in my scene, you may ask? Well a good solution to reduce the number of game objects is also a good optimization to improve performance: batch as much static geometry as you can into as few objects as possible. Second, since even unused objects take time to load, delete all unnecessary/placeholder objects from your scenes before building for release.
Tip #2: Keep the GameObjects in your Scene Disabled“But you just said that it doesn’t matter whether GameObjects are disabled or enabled in my scene?” That is true for empty GameObjects, however it’s not true for everything! Every component attached to your GameObjects will take some amount of time; however, the time is not evenly distributed. For example, a ParticleSystem component takes longer to activate than a MeshFilter and MeshRenderer, and a TextMeshPro component takes significantly longer than both, as it has to construct its mesh at activation time:

With the GameObjects disabled, you delay much of the very expensive setup these different components need to perform before they can be used. See what happens when all of the GameObjects in these same scenes are disabled:

Of course, to get the benefit of your components, you will need to activate them, but disabling them for load gives you the luxury of enabling them on your schedule, which means you can roll the activation time over multiple frames, avoiding any ugly frame hitches.
Tip #3: Precompile All Shaders (that you need)Something that can cause a big hitch when loading a scene, and is pretty easily avoidable, is Shader compilation. The first time a shader variant is referenced by an active object, Unity will try to compile it, which can take quite a long time, especially if multiple shaders need to be compiled simultaneously. It’s best to make sure all your shaders are precompiled and ready to go during your initial load (which can be masked by a static loading screen). One way to do this is to create a scene with every material referenced in the game, and load that first behind your splash screen. Be sure to cover all the different variant conditions that are in your game that would change Unity’s shader keywords, such as reflection probes, light probes, direction light, etc. Avoid using Shader.WarmupAll, as it will compile every possible variant of your shaders, when you’re unlikely to ever run into most of them in game.
Tip #4: Watch out for Unexpected Mesh PitfallsWhile most meshes are perfectly happy to load asynchronously with your scene, there are a few conditions that will make Mesh loading cause a hitch when activating your scene. If the mesh needs to be readable by the CPU, it will have a steep activation time. There are a few things that will cause the Mesh to be CPU readable, the most obvious of which is leaving ReadWrite enabled. However, there are a few less obvious ways this can happen as well. If the mesh is skinnable (e.g., has bones or blendshapes), it will also need to be CPU readable. It can be difficult to avoid loading skinned meshes all together, but there may be some tricks to avoid the biggest hitches. For example, some modular character systems include all the possible parts in one large FBX file, which at runtime will cause a huge load. Instead, it’s probably better to break all the pieces into individual meshes, and construct your modular character at runtime. In the worst case, it might just be best to remove your skinned meshes from your async scenes entirely, and instead load them behind your initial loading screen.
Tip #5: Nest GameObjects under as few ‘root’ GameObjects as possible (but only while loading!)Interestingly, you will see an improvement in activation time if all of the GameObjects are nested under a ‘root’ GameObject:

It appears that Unity has to do some extra amount of processing for every object at the root of the scene. To avoid it, just put everything under a single, or a small set of ‘root’ objects, it will speed up your scene activation.
However, there is also a downside to nested objects at runtime. Unity will update transforms on multiple threads, so having a single root object will actually decrease performance because it will limit what could be a multi-threaded operation to a single thread. Therefore, it might be better to ‘unpack’ your nested objects to a flat hierarchy at runtime, or to have a small number of root objects to keep the multithreaded runtime performance up.
Tip #6: Recycle your GameObjectsWith this tip, we’ve moved past the easy tweaks you can make to your scenes, this is where you have to start building game architecture to suit the new nature of your streaming game. One way to do this is to start pooling and reusing objects. Most Unity developers are already familiar with this technique, as it’s useful for things like weapon projectiles and effects. Essentially, the idea is to create a pool of common reused objects, and move them around the game world as the player moves through it. For instance, if the player sees barrels in different places, you’d have a barrel pool, and as the player walks past one barrel and moves into another area, behind the scenes you move the same barrel to the new location, and the player is none-the wiser. While this sounds simple enough, there are some complicated pieces.
For one, the designer needs to place the barrel in the location, so you likely need a ‘BarrelProxy’ script that is placed in the scene, so the pool knows where to put it. Second, if you’re using baked lighting (which you probably should!) you have to consider the lightmap influence of your barrels. If your barrels are destructible, they wouldn’t need to cast shadows in your lightmap, but they might still receive shadows. You’d need to instantiate all your barrels before backing lightmaps, record the lightmap ID and Rect, and include that in your proxy component. Then at runtime when you pull the barrel from the pool you should just need to apply that lightmap data to the renderer.
In all likelihood, this will require building custom build steps to handle proxy object instantiation/removal, which we can use to take one more step: remove the proxies from the scenes themselves, and build a manifest of the proxy locations. You can then put this manifest as part of a component in each scene, or as a simple text file included somewhere in the game.
Tip #7: Get CreativeIf the previous 6 tips aren’t enough to make your scene transitions seamless, it’s time to get creative. This is where you decide what makes the most sense for your product, and create the solution that works best for you. The less scenes contain, the faster they load, so maybe the logical conclusion is to reduce the use of scenes even further. Maybe you have a build step that splits each scene into a number of subscenes, which can each load without hitching. Perhaps the solution is only to use scenes for static geometry, everything else will be loaded at runtime as prefabs. If you do come up with a creative solution, we’d love to hear about it.
If you have any questions or feedback, please let us know on the
developer forum.