Typescript and Webpack setup for an HTML5 game in ExcaliburJS engine
This is an open-source project on game development with a focus on the HTML5 Excalibur game engine. Check out this feature implementation on this commit in our Github repository!
You can read here the list of features we are going to develop for our Excalibur game, such as Twine integration for characters interactive dialogs If you're interested in game development, follow us on Twitter and Instagram to be caught up on our latest news!
Table of Contents
In this tutorial
In this tutorial we set up an npm project with:
- Typescript as programming language
- Webpack as module bundler
We are going to set up two different Webpack configurations, one for development and one for production.
Typescript configuration
Create a folder named sword-adventure
in your workspace. From the terminal run
npm init
and answer the questions which are prompted in the terminal. Here’s the package.json
file that we’ve obtained:
{
"name": "sword-adventure",
"version": "1.0.0",
"description": "A template for developing games with Excalibur engine",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Facondia Games"
}
Install the Typescript dependency by running
npm install typescript
You should see now the Typescript dependency in your package.json
.
Now run tsc --init
to generate the Typescript configuration file, tsconfig.json
. In our file, we've added the exclude
field to exclude the test files from the compilation. Here's our configuration file:
{
"compileOnSave": false,
"compilerOptions": {
"skipLibCheck": true,
"module": "esnext",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"baseUrl": "./src",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"strictPropertyInitialization": false,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
"lib": [
"es2018",
"dom",
"dom.iterable",
"esnext"
],
"esModuleInterop": true,
"allowJs": true
},
"include": [
"src"
],
"exclude": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx"
]
}
This is our project folder structure until now:
sword-adventure
/package.json
/tsconfig.json
The very last step of Typescript configuration is creating the entry point: the index.ts
file. Create in the project folder a directory named src
. We are going to put here all the classes that we need to build our game. In this folder, create the index.ts
file. It can be empty for now.
At this stage, our project structure is:
sword-adventure
/src
/index.ts
/package.json
/tsconfig.json
/webpack.common.js
Webpack configuration
We are going to have two different configurations, one for development and another for production. The two modes will have some configuration in common. To get started, create a webpack.common.js
file in the project folder:
sword-adventure
/src
/index.ts
/package.json
/tsconfig.json
/webpack.common.js
Our webpack.common.js
configuration will be:
module.exports = {
experiments: {
topLevelAwait: true,
},
entry: "./src/index.ts",
target: "web",
output: {
filename: 'sword-adventure.js',
sourceMapFilename: "[file].map",
path: path.resolve(__dirname, "dist"),
clean: true
},
devtool: "source-map",
module: {
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: "asset/resource",
},
{
test: /\.mp3$/,
loader: 'file-loader'
},
{
test: /\.js$/,
use: ["source-map-loader"],
exclude: [path.resolve(__dirname, "node_modules/excalibur"), path.resolve(__dirname, "node_modules/@capacitor-community")],
enforce: "pre",
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: "ts-loader",
options: {
compilerOptions: {
noEmit: false,
}
}
}
]
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
modules: ['node_modules', 'src/**/*'],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebPackPlugin({
title: "Sword Adventure",
template: "src/index.html"
}),
],
};
Let's see step by step this configuration.
This option
entry: "./src/index.ts"
let Webpack knows that the entry point for the compilation is the index.ts
file that we've created in the previous step.
The output
config
output: {
filename: 'sword-adventure.js',
sourceMapFilename: "[file].map",
path: path.resolve(__dirname, "dist"),
clean: true
}
sets the configurations for the output file that Webpack will create. Our file will be called sword-adventure.js
, it will be placed under the dist
directory of the project folder, and it will have a sourceMap
file. Sourcemaps
are very useful when debugging code in the console. Thanks to Sourcemaps
files, it's possible to work with Typescript files in the browser console instead of compiled js output (remember that browsers work with javascript files, not with Typescript ones!) The clean
option is related to the CleanWebpackPlugin
imported at the beginning of Webpack config file:
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
You'll need to add it to the projects devDependencies
by running
npm install --save-dev clean-webpack-plugin
This plugin remove the dist
folder content every time a new build is run. If it wasn't done, the dist
directory would soon be crowded with build files.
Going back to webpack.commmon.js
, the rules
under the module
field are required so that Webpack knows how to manage assets: images, sounds, css, and font files. It also includes a rule to load *.tsx
(React) files with ts-loader
.
The last interesting configuration option is under plugins
: it's the HTMLWebpackPlugin
new HtmlWebPackPlugin({
title: "Sword Adventure",
template: "src/index.html"
}),
The configuration here says to Webpack that we are going to use our own index.html
file. If it wasn't for this plugin, Webpack would build a index.html
file on its own. When building the code, thanks to HtmlWebPackPlugin
, Webpack will add to our index.html
file the script to load the javascript bundle containing our game. We don't have yet an index.html
file. Let's create it in our src
folder:
sword-adventure
/src
/index.ts
/index.html
/package.json
/tsconfig.json
/webpack.common.js
The content of index.html
file will be:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Sword Adventure</title>
<meta content="light dark" name="color-scheme"/>
<meta
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
name="viewport"
/>
<meta content="telephone=no" name="format-detection"/>
<meta content="no" name="msapplication-tap-highlight"/>
<!-- add to homescreen for ios -->
<meta content="yes" name="apple-mobile-web-app-capable"/>
<meta content="Sword Adventure" name="apple-mobile-web-app-title"/>
<meta content="black" name="apple-mobile-web-app-status-bar-style"/>
</head>
<body>
<div class="absolute-bottom full-width flex-row flex--justify-center">
<div class='white-text' id="loader-progress"></div>
</div>
<canvas id="game" style="display: block; margin: auto auto"></canvas>
</body>
</html>
Let's focus on the content of the body:
<body>
<div class="absolute-bottom full-width flex-row flex--justify-center">
<div class='white-text' id="loader-progress"></div>
</div>
<canvas id="game" style="display: block; margin: auto auto"></canvas>
</body>
In the next tutorial, we are going to use the canvas
element to render the Excalibur game. We will also use the div
element with id loader-progress
to start customizing the default options of the built-in Excalibur loader. Before proceeding with Excalibur setup, we just need to finish with Webpack configuration: the development and production ones.
Development configuration for Webpack
We'll start by creating a webpack.development.js
file:
const {merge} = require("webpack-merge");
const common = require("./webpack.common");
module.exports = merge(common, {
mode: "development",
devtool: "inline-source-map",
devServer: {
static: {
directory: "./dist",
},
compress: true,
port: 9000,
},
});
To run this configuration we first need to install the webpack-merge
package by running in the terminal
npm install --save-dev webpack-merge
You should see the new dependency under devDependencies
in package.json
file. This package, as its doc says, provides a merge function that concatenates arrays and merges objects creating a new object. In our case, it will merge the common Webpack configuration with the specific development and production ones.
Your project structure should look now as
sword-adventure
/src
/index.ts
/index.html
/package.json
/tsconfig.json
/webpack.common.js
/webpack.development.js
With this configuration we can add a couple of scripts to our package.json
:
"scripts": {
"serve": "webpack serve --config webpack.development.js",
"build:dev": "webpack --config webpack.development.js",
},
By running npm run serve
we'll start a live development server, available at localhost:9000
. By running npm run build:dev
we'll build the compiled code bundle and we'll find the output under the dist
directory.
Production configuration for Webpack
We'll start by creating a webpack.production.js
file:
const {merge} = require("webpack-merge");
const TerserPlugin = require("terser-webpack-plugin");
const common = require("./webpack.common");
module.exports = merge(common, {
mode: "production",
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
}
});
To run this configuration we first need to install the terser-webpack-plugin
package by running in the terminal
npm install --save-dev terser-webpack-plugin
You should see the new dependency under devDependencies
in package.json
file. As its doc says,
This plugin uses terser to minify/minimize your JavaScript.
This means that the Webpack output under the dist
directory will be minified and will therefore occupy less space.
Your project structure should look now as
sword-adventure
/src
/index.ts
/index.html
/package.json
/tsconfig.json
/webpack.common.js
/webpack.development.js
/webpack.production.js
With this configuration we can add the last scripts to our package.json
:
"scripts": {
"serve": "webpack serve --config webpack.development.js",
"build:dev": "webpack --config webpack.development.js",
"build:prod": "webpack --config webpack.production.js",
},
By running
npm run build:prod
we'll build the compiled code bundle in production mode. We'll find the output under the dist
directory.
Everything is set up!
We are finally ready to add Excalibur to our project! Let's go on by setting up Excalibur!