Bundling your phaser.js game with esbuild
Kevin Potschien
Posted on September 16, 2024
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,
});
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>
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"
}
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' });
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"
"
}
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,
}),
],
});
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!
Posted on September 16, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.