Loading new Phaser assets without writing code

April 23, 2021

I used to write some lines of code each time i created a new asset for a Phaser game. This was a little bit annoying. But with this Webpack trick, i don't have to think about it anymore.

The goal of this tutorial is that we just have to place our assets in folder and Webpack will pick them up and then our Phaser game will know what to do with them.

Let's start by creating a folder structure in our src folder, where Webpack will look for the assets. We will not be putting our assets directly in the dist, pub or public folder. Webpack will put them there for us, randomize the name, but give us a reference we can pass to Phaser. The folder structure looks like this:

▾ src/
  ▾ assets/
    ▸ audiosprites/
    ▸ images/
    ▸ music/
    ▾ sprites/
      ▸ 13x11/
      ▸ 16x16/
      ▸ 32x32/
      ▸ 48x48/
      ▸ 4x4/
    ▸ tilemaps/

Depending on the folder we will use a different Phaser load function.

But we have to make sure our Webpack config is ok first. I am using Webpack 5, but it can be done with other versions as well, but you may need extra loaders. In the case of Webpack 5, we don't need any extra dependencies.

Clean your distribution folder (mine is public) . In webpack.config.js set clean in output to true:

output: {
  path: path.resolve(__dirname, 'public'),
  publicPath: '',
  clean: true
},

Next we need to add a rule to module.rules also in the webpack.config.js

module: {
    rules: [
        // other rules
        // ...
        {
            test: /\.(png|svg|jpg|jpeg|gif|mp3)$/i,
            type: 'asset/resource'
        }
    ]
},

Next we will use Webpack's require.context method to load everything and add it to Phaser. We will add the code in the preload function of a scene. The this refers to a Phaser scene.

Images

let importAllImages = (r) => {
    r.keys().forEach((k) => {
        let name = k.replace('./', '').replace('.png', '');
        this.load.image(name, r(k));
    });
};
importAllImages(require.context('../assets/images/', false, /\.(png)$/));

That was the easy one to understand.

Music

let importAllMusic = (r) => {
    r.keys().forEach((k) => {
        let name = k.replace('./', '').replace('.mp3', '');
        this.load.audio(name, r(k));
    });
};
importAllMusic(require.context('../assets/music/', false, /\.(mp3)$/));

Very similar like the first one.

Sprites

let importAllSprites = (r) => {
    r.keys().forEach((k) => {
        let parts = k.split('/');
        let sizes = parts[1].split('x');
        let w = Number(sizes[0]);
        let h = Number(sizes[1]);
        let name = parts[2].replace('.png', '');
        this.load.spritesheet(name, r(k), { frameWidth: w, frameHeight: h });
    });
};
importAllSprites(require.context('../assets/sprites/', true, /\.(png)$/));

Sprites will have a subfolder for the sizes of their frames. A sprite that is 32 pixels wide and 64 pixels high will be in the 32x64 folder.

Audiosprites

let importAllAudioSprites = (r) => {
    r.keys().forEach((k) => {
        let name = k.replace('./', '').replace('.mp3', '');
        this.load.audio(name, r(k));
    });
};
importAllAudioSprites(require.context('../assets/audiosprites/', false, /\.(mp3)$/));

let importAllAudioSpriteData = (r) => {
    r.keys().forEach((k) => {
        let name = k.replace('./', '').replace('.json', '');
        this.cache.json.add(name, r(k));
    });
};
importAllAudioSpriteData(require.context('../assets/audiosprites/', false, /\.(json)$/));

Two things to note here: The json file and the mp3 file need to have the same name and the json files will be compiled in your bundle.

Tilemaps

let importAllTilemaps = (r) => {
    r.keys().forEach((k) => {
        let name = k.replace('./', '').replace('.json', '');
        let data = r(k);
        this.cache.tilemap.add(name, {format: 1, data: data});

        // extra: make a minimap image
        let canvas = document.createElement('canvas');
        let ctx = canvas.getContext('2d');
        let map = this.make.tilemap({ key: name });
        canvas.width = map.width;
        canvas.height = map.height;
        let layer = map.getLayer(0);
        layer.data.forEach((row) => {
            row.forEach((tile) => {
                if (tile.properties.collideAll) {
                    ctx.fillStyle = "#8e918b";
                    ctx.fillRect(tile.x, tile.y, 1, 1);
                } else if (tile.properties.swimable) {
                    ctx.fillStyle = "#9aadb3";
                    ctx.fillRect(tile.x, tile.y, 1, 1);
                } else if (tile.properties.climbable) {
                    ctx.fillStyle = "#9ab3a1";
                    ctx.fillRect(tile.x, tile.y, 1, 1);
                } else if (tile.properties.collideUp || tile.properties.collideSupport) {
                    ctx.fillStyle = "#b2b4b0";
                    ctx.fillRect(tile.x, tile.y, 1, 1);
                } else {
                    ctx.fillStyle = "#dddddc";
                    ctx.fillRect(tile.x, tile.y, 1, 1);
                }
            });
        });
        this.textures.addBase64('map ' + name, canvas.toDataURL());
        // end extra

    });
};
importAllTilemaps(require.context('../assets/tilemaps/', false, /\.(json)$/));

Note: The json files will also be compiled into the main bundle js.

Final note

The assets can now be used in your scenes by the filename. If your filename is background.png you can use it as

this.add.image(0, 0, 'background');

That's it... Hope you find it useful.

Comments

No comments yet. Be the first.

New comment