Load Unity WebGL player only after user clicks a button

25 June 2021 6 min read webdev gamedev

Unity allows you to publish WebGL builds of your games, which is great for easily sharing prototypes or publishing complete projects. But these builds can get large (hundreds of megabytes) and you may wish to warn your users before the download initiates. It’s pretty easy to do this by modifying the html page output by the Unity WebGL build.

This post assumes only beginner-level knowledge of HTML, CSS and JavaScript.

As of Unity 2020.3 (the version I’m currently using) the default WebGL template looks like this:

<div id="unity-container" class="unity-desktop">
  <canvas id="unity-canvas" width={{{ WIDTH }}} height={{{ HEIGHT }}}></canvas>
  <div id="unity-loading-bar">
    <div id="unity-logo"></div>
    <div id="unity-progress-bar-empty">
      <div id="unity-progress-bar-full"></div>
    </div>
  </div>
  <div id="unity-mobile-warning">
    WebGL builds are not supported on mobile devices.
  </div>
  <div id="unity-footer">
    <div id="unity-webgl-logo"></div>
    <div id="unity-fullscreen-button"></div>
    <div id="unity-build-title">{{{ PRODUCT_NAME }}}</div>
  </div>
</div>
<script>
  var buildUrl = "Build";
  var loaderUrl = buildUrl + "/{{{ LOADER_FILENAME }}}";
  var config = {
    dataUrl: buildUrl + "/{{{ DATA_FILENAME }}}",
    frameworkUrl: buildUrl + "/{{{ FRAMEWORK_FILENAME }}}",
    codeUrl: buildUrl + "/{{{ CODE_FILENAME }}}",
#if MEMORY_FILENAME
    memoryUrl: buildUrl + "/{{{ MEMORY_FILENAME }}}",
#endif
#if SYMBOLS_FILENAME
    symbolsUrl: buildUrl + "/{{{ SYMBOLS_FILENAME }}}",
#endif
    streamingAssetsUrl: "StreamingAssets",
    companyName: "{{{ COMPANY_NAME }}}",
    productName: "{{{ PRODUCT_NAME }}}",
    productVersion: "{{{ PRODUCT_VERSION }}}",
  };

  var container = document.querySelector("#unity-container");
  var canvas = document.querySelector("#unity-canvas");
  var loadingBar = document.querySelector("#unity-loading-bar");
  var progressBarFull = document.querySelector("#unity-progress-bar-full");
  var fullscreenButton = document.querySelector("#unity-fullscreen-button");
  var mobileWarning = document.querySelector("#unity-mobile-warning");

  // By default Unity keeps WebGL canvas render target size matched with
  // the DOM size of the canvas element (scaled by window.devicePixelRatio)
  // Set this to false if you want to decouple this synchronization from
  // happening inside the engine, and you would instead like to size up
  // the canvas DOM size and WebGL render target sizes yourself.
  // config.matchWebGLToCanvasSize = false;

  if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
    container.className = "unity-mobile";
    // Avoid draining fillrate performance on mobile devices,
    // and default/override low DPI mode on mobile browsers.
    config.devicePixelRatio = 1;
    mobileWarning.style.display = "block";
    setTimeout(() => {
      mobileWarning.style.display = "none";
    }, 5000);
  } else {
    canvas.style.width = "{{{ WIDTH }}}px";
    canvas.style.height = "{{{ HEIGHT }}}px";
  }
#if BACKGROUND_FILENAME
  canvas.style.background = "url('" + buildUrl + "/{{{ BACKGROUND_FILENAME.replace(/'/g, '%27') }}}') center / cover";
#endif
  loadingBar.style.display = "block";

  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);
    });
  };
  document.body.appendChild(script);
</script>

You can find it at <Unity Installation>/PlaybackEngines/WebGLSupport/BuildTools/WebGLTemplates/. Unity uses this when generating the WebGL build.

You can make your own template if you like (see Unity Docs on WebGL templates) but this post assumes you already have a website/page where you want to upload your game, and you just need to tweak a few lines in the HTML file before you upload it.

Or in my case, I made a Hugo page template that just needs the build URL as a parameter to load a WebGL game.

The relevant line which starts the loading automatically is script.onload = () => { createUnityInstance(.... To control this manually we just need to:

  1. Make a button.
  2. Run the createUnityInstance() function when the button is clicked.
  3. Disable the button when clicked (so the user can’t accidentally start the load multiple times)
  4. Hide the button once loading is finished.

1. Make a button

Just add <button id="unity-load-button">start loading</button> somewhere on your page. I put mine after the Unity canvas. Then position it over the canvas using CSS. Here is an example (copying the technique the Unity loading bar uses):

#unity-load-button {
    display: inline-block;
    position: absolute; /* parent element should be relative */
    top: 40%;
    left: 50%;
    transform: translate(-50%,-50%);
    z-index: 10;
}

2. Run createUnityInstance() when the button is clicked.

First get a reference to the button element: var loadButton = document.querySelector("#unity-load-button");. I added that with the other Unity variables.

Then change the createUnityInstance part by wrapping it all around an event triggered by clicking the load button. So it becomes:

loadButton.addEventListener("click", function() {
  createUnityInstance(canvas, config, (progress) => {
    progressBarFull.style.width = 100 * progress + "%";
  }).then((unityInstance) => {
    loadingBar.style.display = "none";
    fullscreenButton.onclick = () => {
      unityInstance.SetFullscreen(1);
    };
  }).catch((message) => {
    alert(message);
  });
});

3. Disable the button when clicked

We need to add another event listener to the button. Somewhere in the script tag just add:

loadButton.addEventListener("click", function() {
    loadButton.disabled = true;
});

4. Hide the button once loading is finished

Finally, we should mimic the behaviour of the loading bar and disappear once loading is finished. Right after the loadingBar.style.display = "none"; line, I just added: loadButton.style.display = "none";


Final code looks like this:
<div id="unity-container" class="unity-desktop">
  <canvas id="unity-canvas" width={{{ WIDTH }}} height={{{ HEIGHT }}}></canvas>
  <button id="unity-load-button">start loading</button>
  <div id="unity-loading-bar">
    <div id="unity-logo"></div>
    <div id="unity-progress-bar-empty">
      <div id="unity-progress-bar-full"></div>
    </div>
  </div>
  <div id="unity-mobile-warning">
    WebGL builds are not supported on mobile devices.
  </div>
  <div id="unity-footer">
    <div id="unity-webgl-logo"></div>
    <div id="unity-fullscreen-button"></div>
    <div id="unity-build-title">{{{ PRODUCT_NAME }}}</div>
  </div>
</div>
<script>
  var buildUrl = "Build";
  var loaderUrl = buildUrl + "/{{{ LOADER_FILENAME }}}";
  var config = {
    dataUrl: buildUrl + "/{{{ DATA_FILENAME }}}",
    frameworkUrl: buildUrl + "/{{{ FRAMEWORK_FILENAME }}}",
    codeUrl: buildUrl + "/{{{ CODE_FILENAME }}}",
#if MEMORY_FILENAME
    memoryUrl: buildUrl + "/{{{ MEMORY_FILENAME }}}",
#endif
#if SYMBOLS_FILENAME
    symbolsUrl: buildUrl + "/{{{ SYMBOLS_FILENAME }}}",
#endif
    streamingAssetsUrl: "StreamingAssets",
    companyName: "{{{ COMPANY_NAME }}}",
    productName: "{{{ PRODUCT_NAME }}}",
    productVersion: "{{{ PRODUCT_VERSION }}}",
  };

  var container = document.querySelector("#unity-container");
  var canvas = document.querySelector("#unity-canvas");
  var loadButton = document.querySelector("#unity-load-button");
  var loadingBar = document.querySelector("#unity-loading-bar");
  var progressBarFull = document.querySelector("#unity-progress-bar-full");
  var fullscreenButton = document.querySelector("#unity-fullscreen-button");
  var mobileWarning = document.querySelector("#unity-mobile-warning");

  // By default Unity keeps WebGL canvas render target size matched with
  // the DOM size of the canvas element (scaled by window.devicePixelRatio)
  // Set this to false if you want to decouple this synchronization from
  // happening inside the engine, and you would instead like to size up
  // the canvas DOM size and WebGL render target sizes yourself.
  // config.matchWebGLToCanvasSize = false;

  if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
    container.className = "unity-mobile";
    // Avoid draining fillrate performance on mobile devices,
    // and default/override low DPI mode on mobile browsers.
    config.devicePixelRatio = 1;
    mobileWarning.style.display = "block";
    setTimeout(() => {
      mobileWarning.style.display = "none";
    }, 5000);
  } else {
    canvas.style.width = "{{{ WIDTH }}}px";
    canvas.style.height = "{{{ HEIGHT }}}px";
  }
#if BACKGROUND_FILENAME
  canvas.style.background = "url('" + buildUrl + "/{{{ BACKGROUND_FILENAME.replace(/'/g, '%27') }}}') center / cover";
#endif
  loadingBar.style.display = "block";

  var script = document.createElement("script");
  script.src = loaderUrl;
  loadButton.addEventListener("click", function() {
    createUnityInstance(canvas, config, (progress) => {
      progressBarFull.style.width = 100 * progress + "%";
    }).then((unityInstance) => {
      loadingBar.style.display = "none";
      loadButton.style.display = "none";
      loadButton.disabled = true;
      fullscreenButton.onclick = () => {
        unityInstance.SetFullscreen(1);
      };
    }).catch((message) => {
      alert(message);
    });
  });
  loadButton.addEventListener("click", function() {
      loadButton.disabled = true;
  });
  document.body.appendChild(script);
</script>

That’s it! Now you wont initiate a potentially huge download without the user’s consent.

comments powered by Disqus