Saturday, November 21, 2015

Armoured Engines Dev Blog - Loading Screen

This post first appeared on the Bounder Games development blog at boundergames.com.

I recently had a fellow indie dev on Twitter asking how we made the Armoured Engines loading screen, so I wanted to share the process with all of you! This process can be used to make an animated loading screen using Unity 5.




The SceneManager


First of all, I have a group of "manager" game objects which all have the DontDestroyOnLoad() function called, so these objects persist throughout the game. One of these is the SceneManager, a singleton object that can be easily accessed from any script in the project. It does a few different things such as abstracting level, town, and map loading - but most importantly, it handles the scene transition.

Here is the function where all the magic happens:

public IEnumerator LoadScene(string sceneName, string music)
{
// Fade to black
yield return StartCoroutine(m_blackness.FadeInAsync());

// Load loading screen
yield return Application.LoadLevelAsync("LoadingScreen");

// !!! unload old screen (automatic)

// Fade to loading screen
yield return StartCoroutine(m_blackness.FadeOutAsync());

float endTime = Time.time + m_minDuration;

// Load level async
yield return Application.LoadLevelAdditiveAsync(sceneName);

if (Time.time < endTime)
yield return new WaitForSeconds(endTime - Time.time);

// Load appropriate zone's music based on zone data
MusicManager.PlayMusic(music);

// Fade to black
yield return StartCoroutine(m_blackness.FadeInAsync());

// !!! unload loading screen
LoadingSceneManager.UnloadLoadingScene();

// Fade to new screen
yield return StartCoroutine(m_blackness.FadeOutAsync());
}

As you can see, this function is a coroutine. I won't be going into the details of coroutines but they are basically awesome so I definitely suggest reading up on them. Without a coroutine this function would have to be handled using Update() and would be much more complicated!

Load the Loading Screen


We don't just want the loading screen to appear abruptly over our current screen - in game dev, you very seldom want anything to just appear. Instead, we will fade to black (since for our scene the loading screen is black), then load the loading screen, then fade the black away.

To do this we use three asynchronous methods. First we use a simple coroutine I wrote which is attached to a simple black sprite covering the scene: FadeInAsync(). This simple change the sprite's alpha from 0 to 1.0 over a set number of seconds.

// Fade to black
yield return StartCoroutine(m_blackness.FadeInAsync());

Once that coroutine returns, the screen is black and ready for our loading screen to be loaded. Here I use Application.LoadLevelAsync(), a built in Unity function. This unloads our current scene (aside from things marked DontDestroyOnLoad() such as our SceneManager and its black sprite) and loads our new scene.


// Load loading screen
yield return Application.LoadLevelAsync("LoadingScreen");

// !!! unload old screen (automatic)

Once LoadLevelAsync() returns, it's time to fade out our black using FadeOutAsync(), the reverse of our previous FadeInAsync().


// Fade to loading screen
yield return StartCoroutine(m_blackness.FadeOutAsync());


Loading the New Scene


Loading the next scene is a bit more complicated. I use Application.LoadLevelAdditiveAsync() to load in our new scene. This loads the new scene but does not destroy anything in our loading scene. This means that is going to have to happen manually! Don't forget this or you will end up with both your new scene and the loading scene active when the process is done.

float endTime = Time.time + m_minDuration;

// Load level async
yield return Application.LoadLevelAdditiveAsync(sceneName);

Another thing to note is that you will need to make sure your loading scene is on a higher layer than everything else in your new scene, or the new scene has any renderers turned off when loaded. Otherwise the new scene elements will draw on top of your loading scene.

Similarly, make sure any logic in your new scene is paused until your loading scene is completely gone - otherwise your character may die before the scene is loaded!

At this point, we chose to set a minimum amount of time for the loading scene to run, in order for it not to look jerky for very short load times. To do this, we simply wait for the remaining seconds that have not yet elapsed. This is completely optional, but if you use it make sure this time is quite short.

if (Time.time < endTime)
yield return new WaitForSeconds(endTime - Time.time);

This is also the time at which we chose to start our next scene's music, but that may be different for your project. We have a music manager which handles fading out old music and in new music using the PlayMusic() function.

// Load appropriate zone's music based on zone data
MusicManager.PlayMusic(music);


Unload the Loading Screen


Once the new scene is loaded in the background, it is time to get rid of our loading screen. Again, we don't want it to just instantly disappear, We face back in the black background first, again using FadeInAsync().


// Fade to black
yield return StartCoroutine(m_blackness.FadeInAsync());

Once the black background is faded in, we can get rid of the loading screen. However, there is no built in method to do this since the loading screen and new scene are now merged into the active scene. To get rid of the loading screen, we've created a separate singleton that lives on the root object of the loading screen called LoadingSceneManager. This singleton's sole responsibility is deleting it's object, though in the future we may add more functionality such as a loading bar or percentage display. For now we call a simple function UnloadLoadingScene() which simply destroys the loading scene's root object.

// !!! unload loading screen
LoadingSceneManager.UnloadLoadingScene();

At this point, if you have turned off drawing for your new scene, you should turn it back on before fading the black screen cover away.

With the loading screen destroyed we are free to fade away the black using FadeOutAsync(). At this point you may want to signal to your in game scene that the new level is ready to start, so game logic can be turned back on.

// Fade to new screen
yield return StartCoroutine(m_blackness.FadeOutAsync());


Potential Issues


When implementing this, we had several issues. First, the cameras in our title screen and in game level had different orthographic sizes, so when the new scene finished loading, the scene appeared to jump to a new size. For us this was simple as we hadn't actually intended for the cameras to be different sizes, so we simply fixed that error and things were fine, but if you do intend to have different sizes you should make sure you load your new camera during one of the black sections rather than during the loading screen itself.

We also had a problem with our UI from our new scene showing on top of our loading screen and black backgrounds. This is because our UI was set to use screen space overlay and could not have a rendering layer set. We solved this by tying the UI in each scene to it's camera, and settings a render layer below that of the loading screen. This may not work for everyone, so if you need your UI in screen space overlay you can may your black screen cover a UI object rather than a sprite and make sure it draws on top of your UI. You will also need to turn off the drawing of your UI until the black screen cover has faded in.

Hopefully this will help someone else make an animated loading screen! Feel free to ask any questions in the comments or contact me on Twitter @Jiyambi!

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hi Sarah Herzog! Greate content here!

    But I have problems when I want to call the 'LoadScene()' method through another class! You could take a look at the question I asked in the forum of Unity? Please.

    http://answers.unity3d.com/questions/1220217/startcoroutine-by-another-class-coroutine-inside-c.html

    Thanks in advance!

    ReplyDelete
    Replies
    1. Looks like you got an answer before I could get to it :) Yeah, coroutines can be a bit of a pain because they are tied to the object that calls "StartCoroutine" on them, but are still soooooo useful, I love them! Thanks for your kind words, and glad to be of assistance!

      Delete