Preloaders and Progress Bars!

Unity WebGL Custom Progress Bar

When making a browser-based game, you’re asking players to be generous with their time and wait patiently while your content downloads. This means how you indicate download progress in your preloader is super important. A Unity webGL custom progress bar is essential.

The default Unity webGL progress bar is a awful.

  • It doesn’t update live, instead jumping in big chunks.
  • Doesn’t give any idea as to how large the download is.
  • When the download completes it just hangs for up to two minutes without any indication as to what’s going on!

A terrible user experience, meaning most potential players will drop off before your game even loads. Let’s look at how we can improve this.

Here’s an example of a Unity webGL game with a preloader that fixes these issues. This is what we’re going to make.

It’s best to start by basing a new template off the default one (I wrote a guide on how to set up templates here).

By default, the code that affects how our preloader works is located in TemplateData/unityProgress.js. Looking through this file you’ll see that it’s adding all the html elements to display the progress bar programmatically. This is a bit difficult to work with (and not really the way Javascript is intended to be used), so the first thing to do is move all the html into our index.html file instead.

Setting Up a Basic Unity WebGL Custom Progress Bar

Strip all the display code from unity progress.js until you’re left with:

function UnityProgress (dom) {
  this.progress = 0.0;
  this.message = "";
  this.dom = dom;

  var parent = dom.parentNode;

  this.SetProgress = function (progress) { 
    if (this.progress < progress)
      this.progress = progress;

    this.Update();
  }

  this.SetMessage = function (message) { 
    this.message = message; 
    this.Update();
  }

  this.Clear = function() {

  }

  this.Update = function() {

  }

  this.Update ();
}

Next we make the html + css progress display. The things I find pretty necessary are:

  • A Loading Bar
  • A Loading bar background
  • Progress update text (“X/Y downloaded” etc)

All wrapped in its own div in index.html, just after the canvas:

<div id="loadingBox">
  <div id="bgBar"></div>
  <div id="progressBar"></div>
  <p id="loadingInfo">Loading...</p>
</div>

Add some styling (inline or in the header or linked in a separate file). I’ll go with a style that fits into a full browser window template. First we centre the whole box:

div#loadingBox {
  width: 100%;
  height: 20px;
  position: absolute;
  top: 50%;
  margin-top: -10px;
  text-align: center;
}

Then sort out the sizing, colouring and positioning of the loading bars:

div#bgBar {
  position: absolute;
  width: 200px;
  margin-left: -100px;
  left: 50%;
  height: 2px;
  display: block;
  background-color: #333;
}

div#progressBar {
  left: 50%;
  position: absolute;
  margin-left: -100px;
  width: 0px;
  height: 2px;
  background-color: white;
  border-radius: 2px;
}

div#bgBar {
  border-radius: 2px;
}

And just style up the loading progress text a tiny bit too:

p#loadingInfo {
  color: #666;
  letter-spacing: 1px;
  position: absolute;
  width: 100%;
  font-family: "Monaco", sans-serif;
  text-transform: uppercase;
  text-align: center;
  font-size: 8px;
  margin-top: 10px;
}

Once the loading information displays the way you want it to, we just need to connect it up to the loading data in unity progress.js.

Hide everything when loading is complete in SetProgress() and Clear(), and connect up the progress to your CSS and loading message in Update():

function UnityProgress (dom) {
  this.progress = 0.0;
  this.message = "";
  this.dom = dom;

  var parent = dom.parentNode;
  
  this.SetProgress = function (progress) { 
    if (this.progress < progress)
      this.progress = progress; 

    if (progress == 1) {
      this.SetMessage("Preparing...");
      document.getElementById("bgBar").style.display = "none";
      document.getElementById("progressBar").style.display = "none";
    } 
    this.Update();
  }

  this.SetMessage = function (message) { 
    this.message = message; 
    this.Update();
  }

  this.Clear = function() {
    document.getElementById("loadingBox").style.display = "none";
  }

  this.Update = function() {
    var length = 200 * Math.min(this.progress, 1);
    bar = document.getElementById("progressBar")
    bar.style.width = length + "px";
    document.getElementById("loadingInfo").innerHTML = this.message;
  }

  this.Update ();
}

Advanced Preloader Features

Now we’ve got our own tidy progress bar and update text, but it still moves in big chunks instead of smoothly growing, and doesn’t feel like it’s making progress after the download completes. Let’s fix the jumps using TweenJS, and we’ll display a spinner gif after the download reaches 100% to keep some motion on screen.

Download the TweenJS package and copy over the CSSPlugin.js, then attach it to your index.html:

<script type="text/javascript" src="https://code.createjs.com/tweenjs-0.6.0.min.js"></script>
<script type="text/javascript" src="TemplateData/CSSPlugin.js"></script>

Then make or download a gif spinner, and add it to your loader html:

<img id="spinner" src="TemplateData/spinner.gif" style="display: none; margin: 0 auto" />

Now just connect these two parts:

function init() {
    
}

function UnityProgress (dom) {
  this.progress = 0.0;
  this.message = "";
  this.dom = dom;
  
  createjs.CSSPlugin.install(createjs.Tween);
  createjs.Ticker.setFPS(60);
  

  var parent = dom.parentNode;
  
  this.SetProgress = function (progress) { 
    if (this.progress < progress)
      this.progress = progress; 

    if (progress == 1) {
      this.SetMessage("Preparing...");
      document.getElementById("spinner").style.display = "inherit";
      document.getElementById("bgBar").style.display = "none";
      document.getElementById("progressBar").style.display = "none";
    } 
    this.Update();
  }

  this.SetMessage = function (message) { 
    this.message = message; 
    this.Update();
  }

  this.Clear = function() {
    document.getElementById("loadingBox").style.display = "none";
  }

  this.Update = function() {
    var length = 200 * Math.min(this.progress, 1);
    bar = document.getElementById("progressBar")
    createjs.Tween.removeTweens(bar);
    createjs.Tween.get(bar).to({width: length}, 500, createjs.Ease.sineOut);
    document.getElementById("loadingInfo").innerHTML = this.message;
  }

  this.Update ();
}

Done! Now you have an elegant preloader with much nicer UX that keeps your player up to date with exactly how close they are to playing the game.

Tips and Pitfalls

  • For your loading text, use a monospaced font. As progress grows, the visual size of the load should grow. Using a non-monospaced font will cause the visual size to jitter instead.

Run into any problems? Need my help on making some lovely UX  in browser games? Get in touch.

45 thoughts on “Unity WebGL Custom Progress Bar”

  1. Good day.
    Thank you for provided information, but unfortunately, I cant get that working.. Seems there is a trouble with that Tween library pathes.
    Could you please provide an example of that loader, (index.html + TempalteData fodler) ?

  2. Whoops! Thanks for catching this, I was missing adding the TweenJS script link to your index.html, I’ve updated the guide just under “Advanced Preloader Features”

    And yep, I’ll look into posting a full package of this template.

  3. Hey Alexander, Excellent Tutorial. I was exactly looking for this. I fallowed your steps but unfortunately it does not get past the “Preparing…”. Any ideas?

  4. Hmm, the first step would be to open the developer console in the browser and see if anything’s being logged that might help get to the bottom of this.

    Seeing “Preparing…” would suggest that the files have successfully downloaded, so that’s working at least. I’ve found it can take up to a two minutes before the preloader moves on from that step, but if it doesn’t, then in my experience it means something might be breaking in the actual asset files Unity is building.

    Either way, taking a look in the console will help solve this.

  5. still cant connect that JS library… In downloaded pack some different hierarchy, and I cant understand where to put that CSSPlugin.js
    At least can you share a screen how your project hierarchy looks like?

  6. May be that will be useful for someone – I thought that need to add some more additional js files to template, because CSSPlugin.js got references to others js. But everything is easier – need to put only single CSSPlugin.js and everything will works fine.
    One more addition – if you use spinner.gif, dont forget to put that line in Clear() function in UnityProgress.js
    document.getElementById(“spinner”).style.display = “none”;

  7. guys, i have built my UnityWebGl project and upload it to a web server.
    There is a problem when i access it remotely,
    if i use the 3G network (from smartphone hotspot) , it runs well.
    but when i use home wifi network, it doesnt show the progress bar.
    Anyone have idea about this?
    What could be the problem?

  8. Hi Simon, that sounds like it might be a caching issue, make sure the cache is cleared or try using a different browser.
    If that’s not the problem, maybe there’s something in your home network setup that is blocking the transfer of particular files?

  9. Hello, Alex.

    Does this code works in Unity 5.3?
    I have built my WebGl project with custom UnityProgress.js. It works fine, except downloading progress text (it is always “0.0/1” instead of “X/Y downloaded”).

  10. Hey Roman, it should work, I applied the same technique to a 5.3 project just the other day. That problem sounds like an issue with the file loader, not the template side of things. Check the console for any errors.

  11. Hello Alex.
    I have the same problem as in Roman.
    Download progress text is always “0.0/1″, prepeare text big chunks jumping. And I do not understand why.
    On yours Web site its working, on my not.
    You can see how it works on test dot cc3 dot ru. Also you can download scripts.zip with all progress bar files from site root.
    Can you check them ? Thank you.

  12. Hey Maksim, I’ve had a quick look, and it looks to me like the problem you’re running into is that your project is failing to load because the main WebGL file isn’t being found, and your server is replacing it with a 404 html file. This causes the progress text to be 0.0/1 because it doesn’t have anything to load.
    Unity did change its WebGL structure recently making all builds gzipped by default, and this might be causing your problem, so my recommendation would be to investigate your server configuration. Also check to make sure you’ve got the invisible .htaccess in the Release folder uploaded and working.

  13. Hi Alex– this seems like a really great tutorial, but pretty limited to people who already have a grasp of html and javascript. It would be very helpful to newbies like me if you periodically showed what the full code for index.html and unityprogress.js should look like at each major step. I’m eager to improve that awful WebGL template, but I’m a little lost in the weeds here… thanks!

  14. First of all, thanks a lot for your Tutorial, it was very helpfull. I also have the mentioned issue that the progress bar just jumps from 0.0 of 1.0 to just pending. Here is the Link : http://ply.com/catalog/product/view/id/84302

    I also used Unity 5.3 and i think your right that the problem has something to do with the zipped js files. It also occures using the unity standard template. Do you have any solution to that ?

    Thanks

  15. Since Untiy 5.3 all the files get packed into jsgz files and the progress bars doesn’t work anymore, just moves in one big chunck from 0.0 of 1.0 to preparing.

    Do you have any idea how to fix that ? The Progress (comming from UnityLoader.js -> setStatus) used in the UnityProgress.js seems to fire just twice, once before the donwload started and when its finished.

    Any help would be much appreciated !

  16. Lorenz: I just did a quick test, and this bug where progress is not reported seems to also occur in the default Unity template. My recommendation would be to file a bug report with Unity.

  17. Hi,
    First of all, thank you for this article and sharing all that great stuff. It was very helpfull and it works nicely.

    I searched for a way to customize the loading message. For now it shows something like : “DOWNLOADING DATA…(10254321/24754398)”
    It seems that this are octets, so if i would like to show the info in Kilo octets, i divide by 1000 to obtain something like that:
    “DOWNLOADING DATA… ( 1 281 ko / 3 094 ko)”
    To customize this message i found one solution, it is to change the the way Release/UnityLoader.js is generated, by modifying this file : Unity\Editor\Data\PlaybackEngines\WebGLSupport\BuildTools\UnityConfig\decompress.js at line 178 :
    if (Module[‘setStatus’]) Module[‘setStatus’](‘Downloading data… (‘ + Math.ceil(loaded/1000) + ‘ ko / ‘ + Math.ceil(total/1000) + ‘ ko)’);

    The problem… as it is not part of the template, but part of Unity files, on next update, the changes i made will be overriden…

  18. Hi Jeff, It’s super cool that you figured out that much! It would be good if Unity gave us more control over the loading messages – I suppose you could put in a feature request to speed the process along.
    In the meantime, a way to keep a similar effect within a template would be to use a string find/replace or regex to extract and modify the loading message in javascript.

  19. My workaround for the big jump between downloading 0.0 of 1.0 to preparing was to have the spinner appear when downloading and to have the progress bar appear when it’s preparing or running. When I print to console, the message runs through all the numbers when preparing (e.g. 1/9, 2/9, … , 8/9) but for some reason I only see progress 8/9. Is this because it loads so quickly that the message isn’t able to keep pace with the updates?

    For those who struggled, like I did, to get everything working, I put my code up on github so you can see the full code and directory tree: https://github.com/flipexchange/flipexchange.github.io

  20. Is there any way how to change “DOWNLOADING DATA…(10254321/24754398)” to percentages ? Someting like: “DOWNLOADING DATA…(19%)”

  21. Hi Lukas. Last time I checked, the way the status string was generated wasn’t exposed anywhere, so the best method would be to split the string using (, / and ), then get the numbers and generate a percentage string manually.

  22. moue,
    I also have the issue of it just showing only one spot and not actually loading through or changing.

  23. Hi Alexander,
    first of all thank-you for your tutorial.
    I don’t have knowledge of html and Javascript, so I followed step by step your tutorial, but I get this error message:

    Uncaught ReferenceError: createjs is not defined.

    It seem to refer to line 10 of UnityProgress.js
    “createjs.CSSPlugin.install(createjs.Tween);”

    I’ve copied CSSPlugin.js in TemplateData folder, so I don’t know which could be the problem.
    Do you have any idea?

    Many thanks!

    I’ve copied

  24. Hi Daniele, that suggests the base createjs package hasn’t loaded. Make sure the script is attached via the tag in your html file. If the it’s still not working, try to upload it online first, there may be something about your network preventing access to the createjs CDN.
    Good luck!

  25. Hello Alex, great tutorial!
    Is there still no solution for “DOWNLOADING (0.0/1)”? Thank you!

  26. Hi Fabian, yep, I’ve found that tends to happen when the htaccess file is missing or incorrectly configured. Some versions of Unity will fail to generate out a htaccess file altogether, so do make sure it’s present and uploaded with the directory.

  27. Hi Alex, I have the same small issue as Moe, the “preparing” jump directly to 8/9, after waiting some time (I can’t see 1/9, 2/9, etc). Do you have any advice about it?
    Many thanks!

  28. Hey Daniele, jumping through the preparing stage is pretty normal, but if you’re not getting a progressive load in the downloading stage before it, make sure you have the htaccess file correctly generated and uploaded.

  29. Nice to meet you
    It is a matter that does not proceed from DOWNLOADING (0.0 / 1), but like htaccess is not generated from unity5.4.
    I unzip the Release / .memgz.
    A result, has been successfully displayed.
    please refer.

  30. Thanks for this awesome tutorial.

    Regarding download progress percentage:
    I just unzipped all build archives and create .htaccess to serve these files as compressed over HTTP. After that, I got smooth download bar with progress in bytes, and then starts the magic 😀 I’m not web coder and know nearly nothing about JS, but with a bit of Google got nice download percentage. So, feel free to use this SetMessage function:

    this.SetMessage = function (message) {
    if (message.toLowerCase().indexOf(“down”) !== -1) {
    // incoming: “Downloading Data… (XXXX/YYYYY)”

    // seperate numbers from all string
    var splitted1 = message.split(“(“);
    var lastPart = splitted1[splitted1.length-1].slice(0,-1);
    var sizesArray = lastPart.split(“/”);

    // calculate percentage
    var percentage = Math.round((sizesArray[0] / sizesArray[1]) * 100);

    // set custom message
    this.message = “Downloading Data… ” + percentage + “%”;
    } else {
    this.message = message;
    }
    this.Update();
    }

  31. Thanks Alex for your excellent tuto !

    And many thanks to Poisins, your solution to show the percentage instead of octets is wonderful.

    In addition, you can put this following code instead of the “this.message = “Downloading Data… ” + percentage + “%”;” :

    this.message = !isNaN(percentage) ? “Downloading Data… ” + percentage + “%” : “Downloading Data… 0%” ;

    To check the beginning Nan value of the percentage and don’t show it 🙂

  32. Poisins

    Could you please explain how did you unzipped all build archives, which files? And what did you do with .htaccess.

  33. Hi, it’s really useful article. First thanks for this. It works well, but the data caching is not working. Every time i reload the game it starts downloading entire game content again and again. So What should i have to do to use the Cache data?

  34. Andrew

    I used this .htaccess:

    SetOutputFilter DEFLATE

    SetOutputFilter DEFLATE

    SetOutputFilter DEFLATE

    Just placed this file in templates folder, and Windows builds this file automatically. OSX ignores this file as it’s hidden one (dot file). Haven’t thought about workaround for OSX as I mainly work with Windows OS (because of Visual Studio 😀 ).

    For unzipping – I have created editor’s script for that 🙂

    An now – created Github repo with all files needed for custom loader and build unzipping. https://github.com/poisins/WebGLCustomTemplate_U3D

  35. I am new to Unity 3D but I have managed to create a Web GL
    and post it on itch.io.
    Like you said the default Unity webGL progress bar is a awful.
    Will your code work on OS X and Unity 5.6?
    Beautiful job on your preloader
    Thank you

  36. @ George

    Yes, it will work on OSX and newer Unity versions, as it’s more related to webserver configuration and HTML/CSS/JS than Unity.

  37. Edit: This doesn’t work on Unity 5.6+ as there are template changes. Compression and other server-side configuration will still work, but HTML/JS/CSS should be changed

  38. Can you please explain how one could put an image behind the loading bar? The progress bar and everything works great i just want to use an image in the background like you show at the top of the page..

  39. The progress bar has been changed in Unity 2020.x
    Now the code is insider the index.html, handling return of promise

    var script = document.createElement(“script”);
    script.src = loaderUrl;
    script.onload = () => {
    createUnityInstance(canvas, config, (progress) => {
    progressBarFull.style.width = 100 * progress + “%”;
    }).then((unityInstance) => {
    loadingBar.style.display = “none”;
    fullscreenButton.onclick = () => {
    unityInstance.SetFullscreen(1);
    };
    }).catch((message) => {
    alert(message);
    });
    };

Leave a Reply

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