Важно! Засега този урок е достъпен само на английски език!
Yet another tutorial on progress bars in Unity. Helpful? Decide for yourself. I want to kick off the dev series in this blog with something relatively simple: loading screens. Keep in mind this article is aimed at not so advanced Unity enthusiasts.
I remember the days when I participated in online tournaments in Warcraft 3, one of the biggest and greatest RTS games of all time. 10 years ago this game was the deal! Any major e-sport tournament at that time had Counter Strike and Warcraft 3 as its biggest prize pool disciplines. Names such as Insomnia, Grubby, SpiritMoon (or just Moon), MadFrog and Sky left their mark on e-sport history. Even today in the EU and the States the scene remains active with the help of pioneers such as Back2Warcraft. China and South Korea are still going strong.
One of the most controversial moments I kept experiencing over and over again was taking place while I was waiting my one-versus-one match to begin. The timer in the lobby would count 5…, 4…, 3…, 2…, 1…, and the beautiful loading screen of the game appeared. It always managed to make me wait patiently and yet it was giving me time to clean my mind of all the stress, anticipation and adrenaline of the upcoming battle. At that same time, behind the curtains, the game would prepare the battleground, spawn the units and fill my bank with resources…
The idea for a progress bar is to distract your users while they wait for your beautiful game to load. Imagine a breath-taking 3D-rendered dungeon with thousands of bloodthirsty orcs spawned at its entrance. Loading this on your powerful gaming PC might take a few seconds or so but on your mobile phone it might take a minute.
In Unity, you segregate your game in sequences of Scenes. Your 3D dungeon would be one predefined Scene while your main menu would be another. By attaching a click event on a ‘Load Level’ button from your menu, your script would load the desired Scene.
Let’s see how you could switch between Scenes in Unity:
public class Loader : MonoBehaviour
{
public void ChangeScene(int sceneIndex)
{
SceneManager.LoadScene(sceneIndex);
}
}
SceneManager
provides you with the nifty LoadScene()
method which accepts the Scene’s index or name. However, this can cause your game to ‘hang’ for a bit, while Unity tries to finish loading the next Scene and jump-start you there.
The idea is to have this process in the background, asynchronously, while dazzling the player with a good-looking UI.
public class Loader : MonoBehaviour
{
public void LoadLevel(int sceneIndex)
{
StartCoroutine(LoadAsync(sceneIndex));
}
IEnumerator LoadAsync(int sceneIndex)
{
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneIndex);
while (!operation.isDone)
{
Debug.Log(operation.progress);
yield return null;
}
}
}
For this purpose SceneManager
also has a method called LoadSceneAsync()
. This starts an asynchronous operation from which you could obtain the exact
data on the loading progression.
I remember the noob years when dirty hacks were the only tools that could get our job done in time (Oh, the shame…) We would make progress bars purely based on animation, meaning we would fool you with a fake progress bar, calculating roughly the time slower machines would take to complete the setup. Nasty!
Our operation variable contains the progress attribute. Its values increment from 0 to 0.9. This is due to how Unity handles the transition.
The loading happens from 0 to 0.9 and the actual activation from 0.9 to 1. The first activity is what is time consuming and costly.
If you’ve noticed, receiving the updated values on the progression is iterative, thus the implementation for that is by starting a Coroutine.
In C#.NET when you iterate with foreach, it ‘secretly’ hides the complexity of the IEnumerator
interface but I won’t get into that, nor will I
elaborate on Coroutines in this blog.
However, I want to show my players the progress in percentage and it must be from 0 to 100. I quickly adjust the code by clamping my values in the desired range:
public class Loader : MonoBehaviour
{
private float _progress = 0f;
public void OnLoadLevelClick(int sceneIndex)
{
StartCoroutine(LoadAsync(sceneIndex));
}
IEnumerator LoadAsync(int sceneIndex)
{
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneIndex);
operation.allowSceneActivation = false;
while (_progress < 1f)
{
_progress = Mathf.Clamp01(operation.progress / 0.9f);
Debug.Log("Loading... " + (int)(_progress * 100f) + "%");
yield return null;
}
operation.allowSceneActivation = true;
}
}
The other significant difference is the toggle of operation.allowSceneActivation
between false and true.
The idea here is to prevent Unity from automatically activating the Scene once it’s loaded. Instead, we will tell it when exactly
to do it by setting its value back to true.
The artist has provided me with some beautiful art assets for the UI. He and I have determined the desired behavior to be the following: Progress bar fills up, a text with the percentage is shown to the user (i.e. Loading… 53%) and once the activation is to happen, we require input from the player with the appropriate text (i.e. “Click anywhere on the screen to start”); when the click event happens, we let Unity do its job and activate our badass level.
The progress bar in this tutorial is made with the assets I already have. They were created for Full HD (1920x1080) so this is the referenced resolution I will work with. If you don’t have any images at your disposal, you can find wonderful UI elements on the Unity Store or just use the default styling and colors.
Important note! This tutorial's author implies you are already familiar with the basics of UI in Unity. In case you are not, pay a visit to this nice introduction article and come back here when ready!
Create a couple of Scenes. Name them MenuScene and LevelScene. You’ll work mostly with the first one.
The structure of the Scene is simple. Start with creating a UI button in the Hierarchy tab to make Unity automatically add predefined Canvas and EventSystem game objects.
In the Canvas game object adjust the UI Scale Mode to ‘Scale With Screen Size’. After that, set the Reference Resolution to X: 1920 and Y: 1080 (or your desired one). Put the ‘Match’ slider at the middle of the Width/Height scale (value of 0.5).
Add a simple Background game object with Image component. Set its image to native size and set it to scale with the Canvas (RectTransform: stretch both width and height). Position the button at the center of the screen.
Add an empty Game Object and place it at the top, right after your Main Camera. Rename it to Loader.
So far, you should end up with something similar:
Most of the articles I’ve seen showcase the use of the slider UI component with some simplification to serve the purpose of a loading bar. I’ll show you something else, simpler and focusing on the aesthetics of what the artist has painted.
Add a UI panel inside the Canvas and place it at the bottom. Set its anchor and position at the bottom (hold alt + shift and click the bottom center box inside the RectTransform). Set its height to what fits your assets best. In my case it was 175. Rename it to LoadingScreen. Set its color to black and keep the alpha opacity.
Inside the UI panel create an Image UI game object. I will add my loading bar background as its Source Image and set its native size. Rename it to LoadingBar.
Inside the LoadingBar game object create another UI Image. Add the fill image of your bar and rename it to Fill. From the Image component change its Image Type to Filled. Your options will refresh and display the Fill settings. Change the Fill Method to Horizontal. Fill Origin should be Left and Fill Amount at 0.
When changing the Fill Amount slider value, did you notice that the rectangle of your fill image changed as well? That way the gradient color we have remains perfect and the image is without any distortion. No resizing is happening, only masking, which keeps things nice and clean.
Add a text game object inside your LoadingScreen object and position it right above your loading bar. Give it some generic placeholder like “Loading…” and use an appropriate color and font size to make it easy to spot. Rename it to LoadingText.
Ah, almost forgot about our button! Add an image or leave it blank. Rename it to LoadLevelButton. Its text value should be something of your choice (i.e. New Game, Load Level 1, etc.)
To transition between Scenes, you must place them inside the project’s build settings. Do so in File -> Build Settings. Drag & drop your Scene files inside the window that pops up. That’s enough! Just mind which scene has which index number (0 and 1 in case it’s a new Unity project with no other scenes yet).
Let’s add some C# goodness to the mix! Remember our code snippet from earlier? Time to put it in use.
Go to your empty Loader game object. Click Add Component and type Loader. Click New Script and hit create and add.
Open your newly created class, delete everything and paste the following code:
public class Loader : MonoBehaviour
{
private float _progress = 0f;
public void OnLoadLevelClick(int sceneIndex)
{
StartCoroutine(LoadAsync(sceneIndex));
}
IEnumerator LoadAsync(int sceneIndex)
{
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneIndex);
operation.allowSceneActivation = false;
while (_progress < 1f)
{
_progress = Mathf.Clamp01(operation.progress / 0.9f);
Debug.Log("Loading... " + (int)(_progress * 100f) + "%");
yield return null;
}
operation.allowSceneActivation = true;
}
}
You’ll have to include some dependencies at the top of the file:
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
Go to your button and find the On Click()
panel. Add your script by dragging your Loader game object from the
Scene Hierarchy to the field where it says None (Object). Once you do that, the select box on the right should become active.
Choose Loader
-> OnLoadLevelClick
. This is the method we’ve created in our class.
Add as a parameter the Scene Index of the LevelScene. In my case, it’s 1.
Run your scene and test what happens. When pressing your button, it should pretty much instantly load the new level. In the Console tab, it should print Loading… 0%, Loading… 100%. If that’s the case, perfect! It works fine but it’s super-fast. Let’s connect the rest of the UI with the code.
In your Loader
class add these public properties:
public GameObject loadingScreen;
public Image fill;
public Text text;
public Button loadLevelButton;
This will raise some unresolved dependencies which are easily solved when you add the UnityEngine.UI at the top of the file.
using UnityEngine.UI;
Hit ‘Save’. In the Unity Editor, your Loader game object should refresh itself and show the public fields visible. Drag and drop the objects from the Scene Hierarchy on their respective places: your panel over the loadingScreen property, the fill image over the fill property and so on.
Instead of Debug.Logging our progress we will update our text value and our exact fill amount inside the while loop. Let’s do it like this:
while (_progress < 1f)
{
_progress = Mathf.Clamp01(operation.progress / 0.9f);
fill.fillAmount = _progress;
text.text = "Loading... " + (int)(_progress * 100f) + "%";
yield return null;
}
Right after the loop has finished we want to change the text from “Loading… 100%” to “Click anywhere to start.”
text.text = "Click anywhere to start.";
Add a bool flag property at the top of your class and set its default value to false:
private bool _isClicked = false;
Now we want to check if we have registered a mouse left-click and prevent our Coroutine from stopping
if we haven’t. Right before operation.allowSceneActivation = true;
add the following:
while (!_isClicked)
{
yield return null;
}
Then from the ‘quick and dirty’ bag of tricks I’ll pull this one:
private void Update()
{
if(Input.GetMouseButtonUp(0) && _progress == 1f)
{
_isClicked = true;
}
}
Once every frame we will check for mouse left clicks AND if our progress data has reached 1 (remember 0 to 1?), meaning the end of our rendezvous. If the condition is true, we update our flag. At the next frame update, when the Coroutine continues its next iteration, BAM!, our check fails and we tell Unity to go on and activate all it wants.
So far, our class should look like this:
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class Loader : MonoBehaviour
{
public GameObject loadingScreen;
public Image fill;
public Text text;
public Button loadLevelButton;
private float _progress = 0f;
private bool _isClicked = false;
public void OnLoadLevelClick(int sceneIndex)
{
loadingScreen.SetActive(true);
StartCoroutine(LoadAsync(sceneIndex));
}
IEnumerator LoadAsync(int sceneIndex)
{
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneIndex);
operation.allowSceneActivation = false;
while (_progress < 1f)
{
_progress = Mathf.Clamp01(operation.progress / 0.9f);
fill.fillAmount = _progress;
text.text = "Loading... " + (int)(_progress * 100f) + "%";
yield return null;
}
text.text = "Click anywhere to start.";
while (!_isClicked)
{
yield return null;
}
operation.allowSceneActivation = true;
}
private void Update()
{
if (Input.GetMouseButtonUp(0) && _progress == 1f)
{
_isClicked = true;
}
}
}
Testing the Scene right now should work!
Hide your loading screen! Uncheck the GameObject checkbox in the RectTransform box from the Unity Editor on your LoadingScreen GameObject. We don’t want to see it by default!
Disable your button to prevent double clicks! If you are fast with the mouse clicks, then you’d instantly discover a bug. Sanitize your input and deactivate the button once pressed.
public void OnLoadLevelClick(int sceneIndex)
{
loadingScreen.SetActive(true);
loadLevelButton.interactable = false;
StartCoroutine(LoadAsync(sceneIndex));
}
That’s it! You have your loading screen. Adding some animations would make things look smoother and more professional. I’d personally add some Fade In, Fade Out and Pulse effects on the loading bar and the text to achieve more polished feel.
When I was beginning my journey in the world of Unity it bugged me a lot that I couldn’t find how to do this exactly the way I wanted. I hope I was helpful to you with this simple tutorial!
Source: http://www.alanzucconi.com/2016/03/30/loading-bar-in-unity/
Important! You can download the complete files from the tutorial from GitHub: https://github.com/primegames/unity-loading-bar
Коментари