Saturday, April 26, 2014

Custom Loading Screens in Unity3d

Since releasing my game, No Girls Allowed, I've had some people ask me how I created the custom loading screens in it. If you've played the game, you've probably noticed that whenever you start a level, you come across a different loading/tip screen. Instead of having to keep explaining it to people, I've decided to just create a little tutorial about it that I can simply point everyone to. As an added bonus, I'm also going to (very briefly) touch on coroutines and scaling the GUI to make content look the same across different screen sizes and resolutions.

Keep in mind that I'm not claiming to be an expert on the subject in any way. I've just had some people ask me about this, and decided to share what I did in my game. Hopefully this can help save some of you time so you can spend it on more fun stuff in your own games. In fact, if you want to save a bunch of time and take the easy way out, I've zipped up the scripts and linked to them at the bottom of this tutorial. So with all that in mind, let's get going.

Setting Stuff Up

To get started, here are a couple of loading images you can use if you'd like:

This is going to be a pretty quick and easy tutorial, so these will be the only image assets we need. Go ahead and place them wherever you keep your textures - for me, that's right in the Resources folder. The first thing we're going to do inside of Unity is create a couple of scenes. I just called mine "Scene1" and "Scene2" to keep it simple. Next, add a couple of scripts, and let's name them "Scene1_Script" and "Scene2_Script". These will be attached to the main camera in each respective scene. Also, don't forget to add both scenes to the build settings - otherwise you won't be able to build. Alrighty, now it's time to get to the code.

Scene 1 Script

The high-level goal of Scene1_Script is to pick a random load screen, save it in our PlayerPrefs, slide it in, and load the next scene. To begin, let's set up the variables we'll be using.
Texture loadScreen; //Main Texture used for load screen
float randomLoadScreen; //Random float to determine which texture to use
float loadY = -720.0f; //Load screen Y Position
bool slidingLoadScreenIn; //Whether it's sliding in
Alright, now let's do something with this stuff. We'll start by creating our method to randomize the loading screen. Let's call it "PickRandomScreen". In here, we'll pick a random float between 0 and 1, and set our loading screen to Loading1 if it's below 0.5, and Loading2 if it's above it. We'll also need to save the results to PlayerPrefs. It should look like this:
void PickRandomScreen(){
 randomLoadScreen = Random.Range (0,1.0f);
 if(randomLoadScreen <= 0.5f){
  loadScreen = (Texture)Resources.Load ("Loading1");
  PlayerPrefs.SetString("LoadingImage", "Loading1");
 }
 else{
  loadScreen = (Texture)Resources.Load ("Loading2");
  PlayerPrefs.SetString("LoadingImage", "Loading2");
 }
}
Alright, since this is the first thing we want to do, let's go ahead and call it in our Start method.
void Start () {
 PickRandomScreen();
}
Now that we've selected our loading image, we want to use it. Since this is just a tutorial, we'll just load Scene2 by clicking a button. Let's add this to the OnGUI method:
if(GUI.Button(new Rect(20, 20, 200, 50), "Load Next Scene")){
 slidingLoadScreenIn = true;
}
Alright, now we need to draw the actual loading screen to the OnGUI method and position it. It's going to start just off the screen, and when slidingLoadScreenIn is active it will slide in to cover it. It should look like this:
GUI.DrawTexture(new Rect(0, loadY, 1280.0f, 720.0f), loadScreen, ScaleMode.ScaleToFit, true, 1.78f);

if(slidingLoadScreenIn){
 if(loadY < 0){
  loadY += 10.0f; //Increment up to slide load screen in
 }else{ //All done - load screen is in place
  slidingLoadScreenIn = false; //Stop lowering screen
  Application.LoadLevel ("Scene2"); //Load next scene
 }
}
If you build it, you should be able to click the button and see the load screen slide down, now. However, you'll see that it might not cover the whole screen. To fix this, we need to adjust the Matrix to format everything to fit in our native size. I made the images 1280x720, so that's what we'll use.

Declare these along with the rest of your variables in the beginning of the file:
float native_width = 1280.0f;
float native_height = 720.0f;
float rx;
float ry;
Throw this in the top of your OnGUI method:
rx = Screen.width / native_width;
ry = Screen.height / native_height;
GUI.matrix = Matrix4x4.TRS(new Vector3(0,0,0), Quaternion.identity, new Vector3(rx,ry,1));
Alrighty, now everything should be scaled properly and the loading screen should come on down when we click the button. That should be all we need for Scene1_Script. Good job, everyone! Now go take a food or potty break and be back in 10.

Scene 2 Script

Back already? K, well, let's get going on the rest of our project. The next thing we need to do is set up Scene2_Script to get the loading screen, simulate pulling in some resources, and then dismiss the loading screen. Here are the variables for this script:
//Setting up values for Matrix
float native_width = 1280.0f;
float native_height = 720.0f;
float rx;
float ry;

Texture loadScreen;
float loadY = 0.0f;
bool slidingLoadScreenOut;
bool allDone;
The first thing we need Scene2_Script to do is get the loading screen that was randomly picked in the other script. If you remember, we saved it to PlayerPrefs after we set it. Now you know why. We're going to create this method and call it in our Start method:
void GetLoadScreen(){
 loadScreen = (Texture)Resources.Load (PlayerPrefs.GetString("LoadingImage"));
}
So, the whole reason for loading screens is in the name - to load stuff for the level. Since this is a really simple little project with nothing else to load, we're going to just set up a coroutine to wait for a few seconds to simulate the loading of assets. Deal? Deal. After the time has elapsed we set the slidingLoadScreenOut boolean to true to trigger the sliding out. We'll write out a couple things to the console to make sure it's doing what we want it to.
IEnumerator DoStuff1(){
 Debug.Log ("Waiting 3 seconds to simulate loading...");
 yield return new WaitForSeconds(3.0f); //Wait for 3 seconds
 Debug.Log ("K, that's good.");
 slidingLoadScreenOut = true; //Send the loading screen back up
}
To start this coroutine, we'll place this in the Start method:
StartCoroutine(DoStuff1());
Alright, now before this will do anything, just like in the first script, we'll need to actually draw the loading screen in the GUI. We'll start with setting up the Matrix, just like we did earlier, then draw the texture, and slide it out based on the slidingLoadScreenOut boolean. The last thing we'll be adding here is a little label that will appear when we're all done and the loading texture is out of the picture.
//Matrix Stuff
rx = Screen.width / native_width;
ry = Screen.height / native_height;
GUI.matrix = Matrix4x4.TRS(new Vector3(0,0,0), Quaternion.identity, new Vector3(rx,ry,1));
  
GUI.DrawTexture(new Rect(0, loadY, 1280.0f, 720.0f), loadScreen, ScaleMode.ScaleToFit, true, 1.78f);

if(slidingLoadScreenOut){
 if(loadY > -720.0f){
  loadY -= 10.0f;
 }else{
  slidingLoadScreenOut = false;
  allDone = true;
 }
}
if(allDone){
 GUI.Label (new Rect(500, 300, 200, 50), "Hey, we did it!");
}
*One thing to note: You will always want to make the loading screen the last object in the OnGUI method. This is because the method works by layering the later objects on top of the previous ones.

As promised, here are the zipped up scripts for your shortcut-taking pleasure.

Well, that's about all I've got for you guys. If you have any questions or suggestions, feel free to let me know in the comments. Also, be sure to follow my blog if you're interested in more of these little tidbits, because I'll try to add more here and there as I come across them. Thanks for indulging me, everyone!