Changing scene/level smoothly in Unity 5 (5.5+) #unitytips

Changing scenes in Unity is harder than I expected. Since version 2 of Unity, we’ve had a deceptively simple and easy to use API call:

Application.LoadLevel() // Easy to use, but wrong name, wrong parameter, no status

It’s easy, but the player-experience was horrible. Unity Pro users had access to a better API. Then, in Unity 5.3, they added the new SceneManager class, and deprecated all the old methods.

So … what’s the best practice for loading a level / changing a scene in Unity 5?

Background, unavoidable problems

Use cases

There are two situations that really matter:

  1. Human Players waiting for a level (or area) to load / re-load / restart
  2. Developers (artist, coder, tester) trying to test every 5 mins during development

Developers: what you need

Speed. You really, really, need speed. Developers cannot wait for loading screens. If it takes you 3 seconds to change a piece of code, and 30 seconds waiting for the loading screen so you can test it in-game, your development speed is slowed down by 1000% (no typo).

Players can wait for loading, so long as it’s not too long, and it’s presented nicely. But everyone benefits if loading times are short.

Why does Unity require you to pre-choose which Scenes are in the build? Why is there even a Build step for Unity games? Why is there no “Scene” class in Unity (they recently added a fake Scene struct – we’ll come back to that – but it doesn’t count)? Why can’t Scenes be made procedurally, at runtime? It (probably) comes down to speed.

GameEngines have traditionally been written in C++. “Loading levels rapidly” is one of the advantages C++ had over other languages for many years. It’s interesting to look at why.

In C++ (and C), all memory access is “raw”, low-level. This means you can do some great tricks. For level-loading, a common approach for many years was:

  1. Write the game
  2. Create the level in an editor
  3. Save the level
  4. Build the game, including the level
  5. When the game is running, copy the exact bytes from memory … straight to disk. This creates a disk-image of the level.
  6. Ship the game with all those “imaged to disk” levels pre-installed.
  7. When the player runs the game, do a fast-copy straight from disk back into memory. OSs, CPUs, RAM, and hard-disks are very very fast at this kind of copy.

Players: what you need

The worst thing for the player is a game that locks-up while they’re playing. The very worst case is being in the middle of an action – e.g. running forwards, or firing a gun – and the game freezes, all input is locked, animations stop running, etc.

The best thing for the player is a game where all loading is instantaneous (less than 1 frame). The second best thing is a game where all loading is amortized: i.e. the “cost” (CPU time etc) of loading is spread over multiple frames.

If we spread the cost over enough frames, the player won’t even notice the drop in frame rate. e.g. if your game runs at 65-85 FPS normally, and during loading it drops to 45-65 FPS, the player will be very happy. It’s not perfect – but if we can’t get loading time down to less than one frame, this is still a very good equivalent.

Unity’s scene-loading speed

…is slow. I suspect it’s the Unity Serializer (my nemesis!). Unity staff have implied that Unity2017 or 2018 might replace it. Well, fingers crossed!

Until then, we can assume that Unity will always be slow at this. But we can split into several parts that are known to be slow, and some of them you can speed up yourself:

  • Compiling shaders.
  • Loading textures.
  • Enabling GameObjects
  • Calling “Start()” and “Awake()”

Shader compilation is a pity. It can be VERY slow (many seconds if you’re unlucky) but it’s the price we pay for Unity’s very convenient ultra-dynamic self-compiling multi-shader system. You can do things like pre-compile certain shaders (lots of google hits on this).

BUG: there have been some major bugs with Substance-based materials in Unity 5. If you’re using a lot of sbar materials, this may be adding seconds to your load time. Try latest 5.6 builds too – might be fixed now

Texture loading is always slow – it takes time to send things to the GPU. However, Unity generally is fast at loading textures on most platforms; this is one of the most-optimized parts of game-dev.

BUG: Unity 5 was surprisingly bad at re-using textures. Some 5.x versions unload a texture only to reload it. Also check out your use of AssetBundles: major bugs exist there that make this worse (look at texture-duplication across bundles).

Enabling gameobjects. This one is absurdly slow and there’s nothing you can do about it.

BUG: it shouldn’t be slow. Really. But it’s all internal Unity code, so you can’t fix it.

FIXED: Until Unity 5, physics objects (e.g. all Colliders) were very very slow at being SetActive(true), or enabled=true. This is now fixed! It saves a huge amount of time.

Calling Start/Awake. Unity gives you no direct control over it, you have to rewrite your game if you want to change the performance here. Worst case, you can manually re-write all your game scripts so that they don’t use Start and don’t use Awake.

BUG: most Unity tutorials tell you to use Start and Awake heavily; when you make a big game you quickly discover that the official advice was wrong all along. Professionals avoid doing this.

WORKAROUND: if you’re comfortable with coroutines, Start can be re-implemented as a CoRoutine. Worth a try…

Making it work…

Additive scene-load

The most control I’ve managed to get over Scene Loading is to use Additive loads:

SceneManager.LoadSceneAsync( "My Scene Name", LoadSceneMode.Additive );

This does several things:

  1. The load happens in the background, asynchronously. Your game keeps running (does not lock up!)
  2. The new scene is loaded in parallel with current scene. When it’s loaded, your game is still running. You get to do the performance-intensive part first, then you have control of when to do the final (fast) switch from old to new.
  3. All data currently in memory remains in memory until YOU decide to remove/unload it.

However, the API is buggy, the documentation is mostly missing, and a lot of the methods and classes have the wrong names.

Using SceneManager: how to

Bugs I’ve hit (June 2017)

A quick list, so you’re prepared:

  • LoadSceneAsync hangs Unity’s internal CoRoutine system: all coroutines will be blocked
  • LoadSceneAsync corrupts some core Unity APIs (e.g. Camera, RenderSettings, etc – I believ: all Unity APIs with static accessors, the new scene gets the data from the old scene, which should be impossible)
  • Showing loading progress is difficult. It usually goes: “0% … 0% … [hang a few seconds] … 90% … 90% … 90% … [hang for many seconds]”
  • Scene does not represent Scenes (if the API were written correctly, it would be called “SceneSummary” or similar)
  • LoadSceneAsync is partially single-threaded, and not fully asynchronous (yes, really. This is the biggest single problem!)
  • LoadSceneAsync can be run multi-threaded, but “By design” it hangs at 90% completion (because the last 10% is fully synchronous / single-threaded)
  • LoadSceneAsync won’t work correctly if you have Cameras in both scenes (yes, really!)
  • LoadSceneAsync won’t accept a Scene as parameter. WTF?

How to use LoadSceneAsync without hanging the game

Here’s the order I’ve worked out by trial and error, and reading forum posts by various Unity staff:

*  NB: this is a copy/paste from my last commit. There may
*     be errors. Any improvements I find I'll update this code

// you MUST start ALL coroutines before the loadasync call
StartCoroutine( ChangeScene( "New Scene Name" );

public IEnumerator ChangeScene( string sceneName )
// Display progress bar

// MUST save a ref to old scene - needed later!
Scene oldScene = SceneManager.GetActiveScene();
// NB: Unity has major bugs in the "by name" call; they recommend you use "by path"
//  .. which is a workaround to their bugs, but it makes your game hardcoded: if you
//  .. change your folder structure, your game stops working. It's a nasty hack.
//  .. Thanks, Unity!
Scene newScene = SceneManager.GetSceneByName( sceneName );

// You MUST de-tag your Main-Camera, or Unity's own code crashes
// ..e.g: nasty bug in Unity's FPS controller: it permanently breaks mouselook!
// ... if you disable it, your screen will go black / flicker, so detag instead
Camera oldMainCamera = Camera.main; // we need this later
Camera.main.tag = "Untagged";

// Start the async load
// NOTE: Unity does not allow you to pass-in the scene to load as a parameter. WTF?
AsyncOperation asyncOperation = SceneManager.LoadSceneAsync( sceneName, LoadSceneMode.Additive );
while( ! asyncOperation.isDone ) // note: this will crash at 0.9 if you're not careful
   progressBar.value = asyncOperation.progress;

   // 0.9f is a hardcoded magic number inside the SceneManager API
   // ... this is OFFICIAL! Not a hack!
   if( asyncOperation.progress == 0.9f )
      // ...unless you do several things here.

      // You MUST disable all AudioListeners, or you will get spammed with errors
      ...e.g: FindObjectOfType<AudioListener>().enabled = false;

      // Because we're at 0.9f, and scene is about to flip-over,
      //  you MUST disable your live cameras
      ...e.g.: oldMainCamera.gameObject.SetActive( false );

      // MUST either delete the old scene,
      //   OR disable every root gameobject
      foreach( var go in oldScene.GetRootGameObjects() )
         go.SetActive( false );
      ... or:
         Destroy( go );

      yield return null;

   // Scene has now loaded; but you MUST manually "activate" it
   SceneManager.SetActiveScene( newScene );


Other tips

If the final 10% is very slow, you can try pre-disabling all GameObjects in the new scene, so that “calling Start and Awake” happens much faster (because there’s less to start/awake).

In my tests, even with a scene with everything fully disabled, load times were still long and hangs still occurred, so it’s at best an optimization, not a solution.


With the above setup, my experience is:

  1. I can animate a progress bar appearing on screen (with fade etc), because the CoRoutine does this before the async-load is started
  2. Async-load correctly loads the new scene
  3. The game continues to run while loading goes from 0% to 90%
  4. At 90% to 100% the whole of Unity hangs
    • This is a known (major!) bug in Unity
    • As far as we know, there is no fix or workaround.
  5. With the above code, the hang went down to 1 second even on 5-year-old desktops.
  6. There is sometimes a 1-frame flash of “no camera in scene”
    • Unity’s bugs require disabling the camera at 90%, or 3rd party code accesses the wrong Camera in startup
    • Unity’s bugs in SetActive mean that enabling/disabling can arrive in wrong frames, but never more than 1 frame apart, as I understand it
  7. In some cases, in the Editor, the hang went down to less than 0.1 seconds: Unity 5 has a broken scene-caching system which is randomly working / not working for me.

Final thoughts

The API is a step forward compared to Unity 2’s “Application.LoadLevel” but it’s still a mess.

Most important for me: Compared to Application.LoadLevel(), the async code above gives me much faster in-editor loading times (0.5-1.5 seconds vs 2-4 seconds). This has made it possible to test game features that go back-and-forth between scenes, without wasting hours every week WAITING for scene loads.

However … what should have been one or at most 2-3 lines of code in your game becomes a monster (as you can see above) if you have a normal game and you don’t want players blocked/hung. And best of all: almost none of it is documented.

Poor docs, lots of bugs (many edge-cases appear poorly tested – especially for such a core feature). Poorly named structures … Scene? Ugh. IT’S NOT A SCENE! Note: Unity does have an implicit internal Scene object which contains various things like ScriptableObjects, dynamic Materials, etc. I’m hopeful that SceneManager will eventually get rewritten to expose this internal object (which is probably only abstract right now – probably it’s a bunch of C++ data at the moment, and needs some work to wrap it into a C# class).

Leave a Reply

Your email address will not be published. Required fields are marked *