Bundling your phaser.js game with esbuild

kevin_potschien_01d9fc71a

Kevin Potschien

Posted on September 16, 2024

Bundling your phaser.js game with esbuild

It's been two years since I wrote an article about how to bundle phaser projects using parcel. Eventually I needed to find a solution that fit my needs better and ended up trying all the big players in bundling right now. After a couple of weeks with sticking to esbuild, here's a final write up on my current workflow, that reliably works for every new project I work on.

Setting up the project

Skip this part if you're familiar with how to set up a basic project in your IDE and continue with Adding esbuild

The Basics

Using mkdir create an empty folder - but don't worry, you can simply create a folder as you normally would with your system.

After opening this folder with our IDE, initialize a new project. In my case, I use yarn init in the terminal of VS Code, using the latest stable version of Yarn.

Once you're done, add Phaser as a dependency:
yarn add phaser and a couple of dev dependencies using yarn add -D esbuild esbuild-plugin-copy @types/node

Also, add "type": "module" to your package.json file, as we want to take advantage of EcmaScript Modules.

Structure

Start off by creating a src folder, that will hold all of our essential game files as well as a folder called scripts, also located in our root directory. In there, create a folder called assets to hold our music, spritesheets, and more. We also want a file called app.ts that comes with a simple scene.

// app.ts
import Phaser from 'phaser';

class GameScene extends Phaser.Scene {
    preload() {
        this.load.image('coin', './assets/coin.png');
    }

    create() {
        this.add
            .text(this.sys.canvas.width / 2, 300, 'Hello World')
            .setOrigin(0.5, 0.5);

        this.add.image(this.sys.canvas.width / 2, 250, 'coin');
    }
}

const game = new Phaser.Game({
    type: Phaser.AUTO,
    scale: {
        mode: Phaser.Scale.ScaleModes.FIT,
        autoCenter: Phaser.Scale.Center.CENTER_BOTH,
    },
    width: 800,
    height: 600,
    parent: 'game',
    scene: GameScene,
});
Enter fullscreen mode Exit fullscreen mode

and an index.html as our entry point:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Phaser-Esbuild-Template</title>
        <style>
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }
        </style>
    </head>
    <body>
        <div id="game"></div>
        <script src="./app.js" type="module"></script>
    </body>
</html>

Enter fullscreen mode Exit fullscreen mode

Adding esbuild

Esbuild's customization gives us different ways of handling the bundled output: We will focus on creating a dist folder that contains a index.html file as our main entry point. There is a way to only output your game as a single, minified js file. In this case, handling assets gets slightly more complex. I will leave an example as a branch in the final repo.

Watch Mode

In order to work on our game locally, we need a script that watches for changes and reloads our browser.

Add a script to the package.json that will run a file located in the scripts folder:

    "scripts": {
        "start": "DEBUG=true node scripts/esbuild.start.js"
    }
Enter fullscreen mode Exit fullscreen mode

esbuild.start.js contains the following:

import esbuild from 'esbuild';
import { copy } from 'esbuild-plugin-copy';

const context = await esbuild.context({
    logLevel: 'info',
    entryPoints: ['src/app.ts', 'src/index.html'],
    bundle: true,
    outdir: 'dist',
    sourcemap: true,
    platform: 'browser',
    loader: {
        '.html': 'copy',
    },
    format: 'esm',
    define: {
        'process.env.DEBUG': `"${process.env.DEBUG}"`,
    },
    plugins: [
        copy({
            assets: {
                from: ['./src/assets/**/*'],
                to: ['./assets'],
            },
            watch: true,
        }),
    ],
});

const result = await context.rebuild();

await context.watch();

await context.serve({ servedir: './dist' });
Enter fullscreen mode Exit fullscreen mode

At this point, running the yarn start command should give you a live preview of your game running. If it's not, make sure you read the console's output carefully or leave a comment here and I'll try to help you out.

Build

Just like with the Watch Mode, we add an additional script to the package.json:

    "scripts": {
        "start": "DEBUG=true node scripts/esbuild.start.js",
        "build": "rm -rf ./dist && node scripts/esbuild.config.js"
"
    }
Enter fullscreen mode Exit fullscreen mode

this will clean up our dist directory before every new build and make sure no extra files slip into the dist directory.

add the esbuild.config.js file with this content:

// esbuild.config.js
import esbuild from 'esbuild';
import { copy } from 'esbuild-plugin-copy';

esbuild.build({
    logLevel: 'info',
    entryPoints: [
        { out: 'app', in: 'src/app.ts' },
        { out: 'index', in: 'src/index.html' },
    ],
    bundle: true,
    outdir: 'dist',

    sourcemap: false,
    minify: true,
    legalComments: 'none',
    loader: {
        '.html': 'copy',
    },
    define: {
        'process.env.DEBUG': `"${process.env.DEBUG}"`,
    },
    plugins: [
        copy({
            assets: {
                from: ['./src/assets/**/*'],
                to: ['./assets'],
            },
            watch: true,
        }),
    ],
});
Enter fullscreen mode Exit fullscreen mode

Most of these settings should be self-explanatory, but make sure to consult esbuild's documentation if you want adjust everything to your needs.

The final repo is available on my GitHub.

Let me know in the comments if there's any hickups or problems!

💖 💪 🙅 🚩
kevin_potschien_01d9fc71a
Kevin Potschien

Posted on September 16, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related