Dear friendy reader,

nice to see you around here. I publish these articles for free and it would be very kind of you to support my blog. You can start by disabling your AdBlocker. That would mean a lot to me and will help to finance this blog.

Best regards, J. Wiese

Phaser 3 - Loading screen & asset organization

Phaser 3 - Loading screen & asset organization

The first real thing a user faces is the loading screen, therefore showing a progress bar is essential. The approach of loading the assets and displaying the progress is not much different from Phaser 2. The structure with the two scenes “boot” and “preload” works also for Phaser 3, but the underlying API of the file interaction has been changed a lot. In the following, I’ll show an example, how to organize the assets and to display a simple progress bar.

You start the game with a boot scene, which only loads the assets needed for the loading screen. Afterwards, the preload scene starts and will trigger all downloads and will wait for them to finish. It’s good practice to download all or most of the assets within the loading screen to minimize the loading times during the game. For this reason, the game needs to know all assets it may make use of. For this example, this list will be stored in a JSON file located at assets/json/assets.json. The boot scene is just a blank screen. Therefore, you don’t want it to last long. Just make sure the game only loads small assets and don’t forget to load the assets.json, which I mentioned before. After the caching has been finished, you will start the next scene within the create function. The boot scene can look like the following.

export default class BootScene extends Phaser.Scene
{
    constructor ()
    {
        super({ key: 'boot' });
    }

    preload ()
    {
        // load all files necessary for the loading screen
        this.load.json('assets', 'assets/json/assets.json');
        this.load.image('logo', 'assets/image/logo.png');
    }

    create ()
    {
        this.scene.start('preload');
    }
}

Preload scene

If you don’t want to create an extra scene for this purpose, you can pass the files within the scene configuration object. This will force the engine to download the files, before the scene starts. This means you will receive no feedback of the download status and you can not draw anything onto the canvas. After the downloads have been finished, the scene will start with the call of preload(). Just define the files along with the scene key within the scene config object like the following.

constructor() {
    super({
        key: "preload",
        files: [
                { type: 'image', key: 'logo', url: 'assets/images/logo.png'},
                { type: 'json', key: 'assets', url: 'assets/json/assets.json'}
            ] 
     });
}

Now the assets and logo keys are available at the cache. The next step is to parse the content of the assets.json and trigger the downloads. Additionally, you should display a progress bar to the user to provide some visual feedback. This can easily be done by listening to the events of the LoadManager.

preload ()
    {
        this.loadAssets(this.cache.json.get('assets'));
        this.add.image(this.centerX(), this.centerY(), 'logo');
        this.createProgressbar(this.centerX(), this.centerY() + 200);
    }

    centerX ()
    {
        return this.sys.game.config.width / 2;
    }
    centerY ()
    {
        return this.sys.game.config.height / 2;
    }

Parsing the assets

The API call for downloading an asset can be different for each asset type. Because each device the game is running on is different and not all file formats are supported consistent (Remember we are in the wild of the web.), you should consider providing the assets in multiple formats. Just define all variants in the assets.json and let the loadAssets() function parse it. For example, I declare an audio key and add multiple files for the different formats to it.

{
    "image": {
        
        "main": "main.jpg",
        "beta": "beta.jpg"
    },
    "svg": {
        "test" : "svg/test.svg"
    },
    "audio": {
        "music":{
            "opus" : "audio/music.opus",
            "webm" : "audio/music.webm",     
            "ogg" : "audio/music.ogg"       
        } 
    },
    "atlas": {
        "megaset" : ["atlas/megaset-0.png", "atlas/megaset-0.json"]
    },

    "spreadsheet" : {
        "one" : ["spreadsheets/one.png", { "frameWidth": 32, "frameHeight": 48 }]
    },
    "xml" : {
        "two" : "xml/two.xml"
    }
}

This JSON object is passed to the function loadAssets(), which is responsible for decoding the keys and their belonging asset URLs and telling Phaser to download them. Some file types have different API calls than other. So, the function should consider this and should call the correct API with the correct parameters. In this example the JSON file also provides the parameters for these file types, like the tile size of a spreadsheet or the view port of an HTML file.

loadAssets (json)
    {
        Object.keys(json).forEach(function (group)
        {
            Object.keys(json[group]).forEach(function (key)
            {
                let value = json[group][key];

                if (group === 'atlas' ||
                    group === 'unityAtlas' ||
                    group === 'bitmapFont' ||
                    group === 'spritesheet' ||
                    group === 'multiatlas')
                {

                    // atlas:ƒ       (key, textureURL,  atlasURL,  textureXhrSettings, atlasXhrSettings)
                    // unityAtlas:ƒ  (key, textureURL,  atlasURL,  textureXhrSettings, atlasXhrSettings)
                    // bitmapFont ƒ  (key, textureURL,  xmlURL,    textureXhrSettings, xmlXhrSettings)
                    // spritesheet:ƒ (key, url,         config,    xhrSettings)
                    // multiatlas:ƒ  (key, textureURLs, atlasURLs, textureXhrSettings, atlasXhrSettings)
                    this.load[group](key, value[0], value[1]);

                }
                else if (group === 'audio')
                {

                    // do not add mp3 unless, you bought a license 😉 
                    // opus, webm and ogg are way better than mp3
                    if (value.hasOwnPorperty('opus') && this.sys.game.device.audio.opus)
                    {
                        this.load[group](key, value['opus']);

                    }
                    else if (value.hasOwnPorperty('webm') && this.sys.game.device.audio.webm)
                    {
                        this.load[group](key, value['webm']);

                    }
                    else if (value.hasOwnPorperty('ogg') && this.sys.game.device.audio.ogg)
                    {
                        this.load[group](key, value['ogg']);

                    }
                    else if (value.hasOwnPorperty('wav') && this.sys.game.device.audio.wav)
                    {
                        this.load[group](key, value['wav']);
                    }
                }
                else if (group === 'html')
                {
                    // html:ƒ (key, url, width, height, xhrSettings)
                    this.load[group](key, value[0], value[1], value[2]);

                }
                else
                {
                    // animation:ƒ (key, url, xhrSettings)
                    // binary:ƒ (key, url, xhrSettings)
                    // glsl:ƒ (key, url, xhrSettings)
                    // image:ƒ (key, url, xhrSettings)
                    // image:ƒ (key, [url, normal-url], xhrSettings)
                    // json:ƒ (key, url, xhrSettings)
                    // plugin:ƒ (key, url, xhrSettings)
                    // script:ƒ (key, url, xhrSettings)
                    // svg:ƒ (key, url, xhrSettings)
                    // text:ƒ (key, url, xhrSettings)
                    // tilemapCSV:ƒ (key, url, xhrSettings)
                    // tilemapTiledJSON:ƒ (key, url, xhrSettings)
                    // tilemapWeltmeister:ƒ (key, url, xhrSettings)
                    // xml:ƒ (key, url, xhrSettings)
                    this.load[group](key, value);
                }
            }, this);
        }, this);
}

Voila, all the files are available at the cache!

Displaying the progress bar

The LoaderPlugin provides us the exact percentage of the download progress. To display the percentage with a nice loading bar, I make use of two Phaser.GameObjects.Graphics game objects. One of them is the actual progress bar and the other one is the border.

The LoaderPlugin provides a list of events to inform about the current status, but we need only the two progress and complete. Feel free to use the other ones, especially if you decide to download assets during the game in the background, these events will make it possible.

Event nameParametersDescription
fileprogress{Phaser.Loader.File}file, {Number} percentageOfFileIs called for each file, after the DOM ProgressEvent has been called by the browser.
progress{Number} percentageReports the overall progress in percentage (0…1)
load{Phaser.Loader.File} fileThe download of the file has been completed.
loaderror{Phaser.Loader.File} fileFailed to load a file.
complete{Phaser.Loader.LoaderPlugin} loader,{Number} newStorageSize,{Number} failedSizeSignals, that the download queue is now empty and all files are completely downloaded.
createProgressbar (x, y)
    {
        // size & position
        let width = 400;
        let height = 20;
        let xStart = x - width / 2;
        let yStart = y - height / 2;

        // border size
        let borderOffset = 2;

        let borderRect = new Phaser.Geom.Rectangle(
            xStart - borderOffset,
            yStart - borderOffset,
            width + borderOffset * 2,
            height + borderOffset * 2);

        let border = this.add.graphics({
            lineStyle: {
                width: 5,
                color: 0xaaaaaa
            }
        });
        border.strokeRectShape(borderRect);

        let progressbar = this.add.graphics();

        /**
         * Updates the progress bar.
         * 
         * @param {number} percentage 
         */
        let updateProgressbar = function (percentage)
        {
            progressbar.clear();
            progressbar.fillStyle(0xffffff, 1);
            progressbar.fillRect(xStart, yStart, percentage * width, height);
        };

        this.load.on('progress', updateProgressbar);

        this.load.once('complete', function ()
        {

            this.load.off('progress', updateProgressbar);
            this.scene.start('title');

        }, this);
}

Going further

  1. You also can show information about the single files which are currently downloaded or the download speed by gathering the file size changes reported by the fileprogress event.
  2. One step further the asstes.json can be load the with Webpack, using the file loader plugin. Then, with the correct configuration, you can replace the URLs with import statements. This enables Webpack to manipulate the files while building the game, such as optimizing the file size or more. Maybe I will add an example later here.

Download

You can find the full example at the GitHub repository or download it directly.