Setting up TypeScript, Phaser, Webpack, … Argh!

Write a game that runs in the browser using TypeScript and Phaser 3

Kieran Mathieson
17 min readMay 30, 2019
Photo by JESHOOTS.COM on Unsplash

You want to write a game that runs in a browser. You decide on TypeScript, and Phaser 3. Your code will be modular, broken into many files, with well-defined links between them. For production, you’ll pack your JS into one file, and minify it.

Your IDE will be VS Code. You’ll tell VS Code about Phaser’s symbol definitions, so IntelliSense can help you. You’ll use a linter. There’ll be a GitHub repo for your project.

To the keyboard! The Interweb tells you to install an endless stream of dev tools. You copy and paste an import statement into your TypeScript, but it’s red and squiggly. Oh, but that statement isn’t in other examples. Should you remove it? The build tools complain about something, but you don’t know what. Argh!

I feel your pain. Trying to copy and paste my way into a good dev environment didn’t work. I had to step away from the keyboard, understand what the dev tools do, how they’re configured, and how they relate to each other.

This article explains what I’ve learned so far. Maybe it will help you, too.

It explains what I think is happening, anyway. I’m still shaky on the deets. If there are errors, or ways to improve, please comment.

Most writers start by telling you how to install the dev tools. That comes last in this article. The idea is to build a mental model of the dev tools and procedures first. Then, once we know what we’re trying to do, we’ll set it up.

Let’s start by setting some goals. Then we’ll look at ways of getting there.

Goals

There are goals for:

  • Project organization
  • Build process
  • Development environment

Goals for project organization

Modularity helps people think about complex things. We’ll break the code up into many files, so we can work on the pieces individually. Some pieces will be Phaser abstractions, like scenes. Other pieces will be our own abstractions, like game state.

We’ll put our app’s files into a folder, to keep them together. We’ll put related pieces into their own subfolders. We’ll also have a subfolder for libraries like phaser.min.js, a subfolder for image assets, etc.

The build process and dev environment should adapt to the way we want to organize our project. No matter how we organize the subfolders, the tools should still work.

One of our JS files will be the app’s entry point. It’s the code that starts everything running. We’ll name the file app.ts, though you can call it whatever you like.

Goals for the build process

Browsers don’t understand TypeScript. We’ll have to convert our TypeScript code into JavaScript. Which JS version? We’ll target ES5, since every browser (more or less) understands it. The TypeScript compiler, TSC, will do the translation. TSC is a program that translates a TypeScript file into a JavaScript file. TypeScript in, JavaScript out.

Modularity

Recall that we’ll achieve modularity by splitting the code up into files. The program will be easier to think about if we limit how much code chunks know about each other.

You can use TypeScript’s export and import statements to link code chunks. For example, the code to model a dog might be in dog.ts:

Say the code in playscene.ts uses the dog code:

import { Dog } from "./dog";
let oscar: Dog = new Dog();
console.log(oscar.bark());

Someone can write playscene.ts without knowing how dogs bark. All they need to know is that there is a Dog class, that has a bark() method. They import Dog from dog.ts, and off they go.

Recall that TSC converts TypeScript into JS. TypeScript in, JS out. Most of the time, TSC can do its job without help. For example, it can convert let x: string; to var x;.

import and export are more difficult for TSC. There are no easy substitutes in ES5. People created various ways of adding modularization to ES5. They created standards like CommonJS, AMD, and others. Jurgen Van de Moere wrote an introduction — Thanks, Jurgen.

Suppose we pick CommonJS, since it’s… er, common. Two things have to happen. First, a JS file with the code for the CommonJS library has to be added to our codebase. Maybe we use the file common.js from the Interwebs. Second, TSC has to know that it should use the CommonJS API to do the equivalent of export and import. Later, we’ll see how we tell TSC that it should use the CommonJS API.

Packing

We have a bunch of TypeScript files we wrote. TSC will convert each one into JavaScript, so now we have a bunch of JavaScript files. We’ll smoosh the JavaScript files together into one file, for users’ browsers to download. That smooshed file is the distributable. Let’s call it bundle.js. You can call yours anything you want.

How to use the distributable? Here’s what the HTML for your game might look like:

Minifying

Once we’ve smooshed everything, we’ll want to minify bundle.js. That means to make it smaller, by removing spaces, using shorter variable names, and applying other tricks. For example, suppose we had this JavaScript:

var dogSize = "big";
if (dogSize === "big") {
console.log("A big dog!");
}

This is equivalent:

var d="big";if(d==="big"){console.log("A big dog!")}

It does the same thing, but would be a smaller download.

There’s a difference between a distributable you use for production (that is, send to users), and one you use for development (for testing and debugging). The production distributable should be minified, to reduce users’ download time. However, in development, minification is less of an issue.

It would be nice if our build process could run in either production mode (minifying), or development mode (no minifying).

Build process goals summary

We want to put all of our TypeScript files in one end. Get one ES5 file (bundle.js) out of the other end. Minified for production.

Where are we?

  • Goals for project organization — Done
  • Goals for the build process — Done
  • Goals for the development environment — Up next

Goals for the development environment

Let’s use VS Code. It’s a good thing. One of its best features is IntelliSense. VS Code has TypeScript IntelliSense built-in. Start typing, and VS Code will show you TypeScript keywords that fit:

TypeScript IntelliSense

If VS Code knows about Phaser’s classes, constants, and such, it can help us write Phaser code:

Intellisense for Phaser

VS Code can do IntelliSense on our own code as well. For example, it can learn about our Dog class:

Intellisense for our own code

Let’s use linting, too. A linter is a program that checks code against a set of rules. The linter should run inside VSCode, so we can see its output as we work. For example:

Linter reporting issues

There’s a default linter rule set, but you can tweak it.

So, we want VS Code to have IntelliSense for all the things, and linting. One more VSCode bit: let’s make it easy to run our game from VS Code. Maybe right-click index.html, and run it on a local server.

Run index.html on a local server

We can do that.

Source maps

We’re writing TypeScript, organized into many different files. When we’re debugging in a browser, we’d like to debug as if the browser was running our TypeScript. But, of course, the browser is running bundle.js, the compiled-and-smooshed-and-maybe-minified result of the build process. That makes debugging difficult, to say the least.

Fortunately, there are source map files. They have names like bundle.js.map. They tell browser debugging tools how the original source files map to the compiled code. You can work with what looks like the original files, setting breakpoints, look at variables, and so on.

Source maps should be part of our development environment.

Meeting the Goals

Now we know the three types of goals:

  • Project organization: many files that refer to each other.
  • Build process: many TypeScript files in — one JavaScript distributable out.
  • Development environment: IntelliSense for everything. Linting. Run index.html on a local server. Source maps for debugging.

Meeting project organization goals

Let’s put all the files for the project into one folder. Its subfolders might be:

  • src: our TypeScript source code will go here. The game’s entry point will be src/app.ts.
  • lib: Phaser 3 and other libraries will go here.
  • dist: the distribution ES5 file, bundle.js, will go here.
  • assets: sprites, sound files, etc., will go here.

index.html will go into the project root.

That will meet the first goal:

  • Project organization — Done
  • Building the distributable — Up next
  • Development environment

Onward!

Meeting build process goals

Remember the goals of the build process:

  • Put all of our TS files in one end.
  • Get one minified ES5 file (bundle.js) out of the other end (for production).
  • Get one ES5 file (bundle.js) out of the other end (for development).

We’ll use Webpack to do the work. Webpack takes a bunch of files, and packs them together. It can also control TSC, the TypeScript compiler, so we don’t have to run it separately. That is, Webpack tells TSC to translate our TypeScript files into ES5 (and optionally make the source map files), before it packages them. The way we’re using it, Webpack is both a task runner (to control TSC for us), and a packer (to make the distributable). TypeScript files in — distributable out.

We’ll get to how to install Webpack later, after we’ve figured out all the pieces we need. Let’s focus on building a good mental model for now.

Apart from packing and minifying, we’ll also use Webpack to meet a goal for the development environment: making source maps.

We control Webpack with a file called webpack.config.js. Notice that this is a JS file, not a JSON file. Different tools are controlled by different types of files. Here’s a sample for this project:

The first thing Webpack has to do is compile our TS files to JS. It doesn’t know how to do that by itself. We tell Webpack to use the ts-loader plugin, which knows how to invoke TSC (the TypeScript compiler, remember) to compile TypeScript files. The test key gives a regular expression identifying the files to compile. In this case, every file with .ts at the end.

The output and filename keys tell Webpack where to put the packaged code. The devtool key says we want a source map.

So far, we have a config file for Webpack. However, remember that Webpack uses TSC. TSC needs its own config file, tsconfig.json. This works for me:

The target key means the compilation target is ES5. How to do the module import/export in ES5? Using CommonJS. Generate source maps. Allow pure JS files. The next four are TypeScript options. The lib array includes some extra libraries, and helps Phaser IntelliSense work (more later).

With these two config files in place, we should be able to run Webpack, and end up with a distributable. Once everything is installed.

We’ve done the first two things now:

  • Project organization — Done
  • Building bundle.js, the code distributable — Done
  • Development environment — Up next

Onward! Ad victorium!

Meeting development environment goals

For development, we want to:

  • Get IntelliSense to work for TypeScript, Phaser, and our code.
  • Linting.
  • Easily run index.html on a local server.
  • Make source maps available to the browser, for debugging.

The last one we’ve already done, by configuring TSC and Webpack.

VS Code has IntelliSense for TypeScript built in. No worries! It will also analyse our code for us, and add it to IntelliSense. More later.

Now for IntelliSense for Phaser. Most popular JS libraries have typing files, that IntelliSense can use to give its hints. Typing files have the extension .d.ts. The site DefinitelyTyped has a large collection. They’re easy to install from there.

At the time of writing, Phaser 3 has a typings file, but it has not been contributed to DefinitelyTyped. We’ll install it manually, later.

For linting, there’s TSLint, yet another dev tool. We need to install it, and hook it up to VS Code. No worries. There’s a VS Code extension for that. And an extension for opening a file in a local server, too.

That’s it! We now know what we want to achieve:

  • Project organization. A nicely organized folder tree, with TypeScript code broken across files, linked by export and import.
  • Building process. A smooshed ES5 package, minified for production.
  • Development environment. VSCode with mondo IntelliSense, linting, source maps, and open-in-a-server.

Installing Everything (Finally!)

Time to hit the keyboard.

  • Implementing project organization
  • Implementing the build process
  • Implementing the development environment

Implementing project organization

Let’s put all the files for the project into one folder, called doggame. Here are some ways to do it.

Windows

Start a command line shell. Win+R, type cmd, and press Enter.

Mine looks like this, after I set some colors to make it easier on my eyes.

I put my projects on the D: drive, so I type d:<Enter>.

Switching the default drive

If you don’t have a D: drive, maybe type cd \ to switch to the root of the C:\ drive.

Create a folder for the project, and change into it:

Project folder created

Next, make the folders for different parts of the project.

Folders for different things

Linux

Pretty much the same thing.

cd ~
mkdir doggame
cd doggame
mkdir src
mkdir lib
mkdir dist
mkdir assets

Remember that the folders are:

  • src: our TypeScript source code.
  • lib: Phaser 3 and other libraries go here.
  • dist: the distribution ES5 file, bundle.js, goes here.
  • assets: sprites, sound files, etc.

index.html will go into the project root, that is, in D:\doggame , C:\doggame, ~/doggame, or whatever.

BTW, as far as I know right now, Windows and Linux are equally suited to Phaser 3 development.

While you’re here, download Phaser, and put it in the lib folder. What I did is go to the Phaser download page, right-click on min.js, and save:

Downloading Phaser

It’s also a good idea to grab the Phaser source code, from the zip file on the Phaser download page, or from GitHub. I’ve found myself referring to the source code sometimes, to figure something out.

Cool! What’s next?

  • Install project organization stuff — Done
  • Install build process stuff — Up next
  • Install development environment stuff

Installing build process tools

We have a bunch of different dev tools to install. It’s easier if we use another tool to keep track of everything: NPM.

Install NPM

NPM is a dependency manager. It can install other software (like Webpack and TSC). NPM can track versions, and update software for you, without you having to download anything yourself.

NPM is part of the Node.js ecosystem. Node.js is a program that runs JavaScript in an OS, like any other language. For example, if you put PHP code in dog.php, you could tell your PHP interpreter to run it, like this:

php dog.php

If you put JS code in dog.js, you could tell Node to run it, like this:

node dog.js

With Node, JS is just another language you can work with, like C#, PHP, or Python. Of course, JS has a trick that the others don’t have: JS also runs in browsers.

The people who wrote tools to manage JS, like Webpack, know JS, of course. So they wrote their tools in JS. How to run JS? Node! That’s why Node appears so often in JS developer land. Even if you never write a Node app yourself, the tools you use need Node to run.

NPM is distributed with Node. Actually, NPM stands for “Node Package Manager.” Install node, and you get NPM.

Node.js installation procedures vary across OS. For Windows, download and run an installation file, usually with a .msi extension.

For Linux, something like:

sudo apt-get install node

Your command may vary, depending on your distro.

The Node installer will modify the system path, to make it easier to run NPM on the command line.

Now that NPM is installed, let’s set it up. We could just start using it, but it goes a little better with a configuration file. NPM will create one for you. Type:

npm init

NPM will ask you some questions. You can type answers, or keep hitting Enter to take the defaults. You’ll get the file package.json. What is package.json for? It’s where NPM tracks what it has downloaded.

You can edit package.json, if you want. I usually do. The default package.json file has this:

"main": "index.js",

That’s not the entry point I want, so I just delete the line.

Install TSC

Tell NPM to install TSC (the TypeScript compiler) by typing this:

npm install --save-dev typescript

Check out your project folder, and you can see what NPM did. First, it created a folder called node_modules. That’s where NPM puts stuff-it downloads for you. Inside that, you’ll see a folder called typescript. TSC itself is in typescript/lib/tsc.js. Yes, the TSC compiler is written in JS. It needs Node to run. That’s why it’s packaged as a Node module.

NPM also modified its configuration file, package.json. Remember that package.json is where NPM tracks what it has downloaded. It just grabbed TypeScript, so there should be an entry for that. Sure enough, open package.json , and you’ll see something like:

"devDependencies": {
"typescript": "^3.4.5"
}

It’s a dev dependency, because you typed --save-dev when you installed TSC.

Create a TSC config file

Put this in tsconfig.json:

Install Webpack

You’ll install two things here. First, ts-loader. Remember that we want Webpack to control TSC. ts-loader is a Webpack plugin that lets that happen. Type:

npm install --save-dev ts-loader

When that’s done:

npm install --save-dev webpack

This one takes a while. NPM downloads a shipload of stuff. Check in the node_modules folder.

Create a Webpack config file

Make the file webpack.config.js. Try this:

Let’s try it so far

Before we get to VS Code, let’s try what we have so far. Let’s make an app with some code you saw earlier.

Make index.html in your project root, with this in it:

Now make src/dog.ts, with this:

One more file, the app entry point, src/app.ts:

import { Dog } from "./dog";
let oscar: Dog = new Dog();
console.log(oscar.bark());

Now for the big test. Run Webpack in the root of your project, where index.html is. If necessary, use the cd command to make that the current folder. For example, if the current folder is src, type cd .. :

Up the parent folder

.. means “parent.”

Run this:

webpack --mode development

(The first time I ran this, it failed, because I used the file name app.js, instead of app.ts. The second time, it failed because I had named the TS config file tsconf.json, instead of tsconfig.json. So… things can go wrong.)

Webpack should run TSC, telling it to compile all of the TS files into JS files. You should get source maps as well. Then Webpack should smoosh the JS files together, into bundle.js. Because you asked for a dev build, bundle.js should not be minified.

You should see something like:

Webpack created bundle.js, combining code translated from app.ts, and dog.ts. Opening up bundle.js, I see:

It’s a smooshed file, but not minified. The code you see is where TSC sets up CommonJS to do export/import.

Open index.html in a browser, and see:

As expected!

Now try:

webpack --mode production

Check out bundle.js now:

!function(e){var t={};function r(n){

Minified! Hooray!

Now, how about the source map? Did that work? Looking in the dist folder, I see the file bundle.js.map. That looks promising. But will it work with the browser?

Here’s what I see in Chrome’s dev tools:

Dev tools in Chome

Hey! dog.ts shows up, even though the browser didn’t really load it (dog.ts was compiled into dog.js, and that was smooshed into bundle.js; bundle.js is what the browser actually loaded). I can set a breakpoint in the TypeScript code, and inspect a property of the Dog class.

One thing left to do.

Set up VS Code

Recall that we want VS Code, with:

  • IntelliSense for all the things
  • TypeScript linting
  • Starting index.html in a server
  • Source maps

The build process gives us the source maps. Time to do the rest.

Download VS Code for your platform, and install it. That will give you IntelliSense for TypeScript, and your own code. The latter happens automatically when you open your project folder in VS Code. You don’t need to do anything.

Download the typing file phaser.d.ts. If you cloned the Phaser repo, the file will be there too. Create a folder called types in your project folder. Copy phaser.d.ts there. Restart VS Code if it’s running. It should find the Phaser typing file itself.

At the command prompt in your project folder (that is, cd projectfolderpath), install the linter like this:

npm install --save-dev tslint

NPM will download TSLint into node_modules.

Like everything else, TSLint needs a config file, tslint.json. Create it like this:

node_modules\.bin\tslint --init

Now you need to tell VS Code about the linter. Install the TSLint extension:

Install VSCode’s TSLint extension

You might need to restart VS Code to get it working.

We want to be able to see a local file in a server, like this:

Install the Live Server extension.

Dog Game

Let’s see how everything works, by making a game. What will the game be? Click on a background image, and a dog will jump there.

A dull game. Art by Teagan Mathieson

OK, not exciting, but it’s just an example.

Here are the files for the project:

Files for the project

The files we make ourselves are highlighted. The other files we download, or they’re created by the tools we’ve installed.

We need one HTML and two TypeScript files. Here’s the code for index.html:

The script tags load in Phaser, and a file with minified, bundled JS translations of our TypeScript files. Webpack makes bundle.js.

Here’s the code for src/app.ts:

Notice const rather than var for declaring variables. If you use var, the linter will complain. Also note that all variables are typed. Nice!

The linter complains about two things in this program:

Linter complaints

The linter says that keys should be sorted alphabetically. I’ve chosen to ignore this rule, since keeping semantically related keys together makes the code easier for humans to follow. See the Quick Fix at the bottom of the dialog? One of the options is to ignore the rule. VS Code will insert an annotation to that effect.

Annotation to ignore key sorting rule

You can also disable the rule in the linter’s configuration file.

Here’s the last code file, src/rungame.ts:

Notice that all of the variables are typed. Also, no linter warnings in this file. After putting the art assets in place, and writing the code, I created the distributable like this:

webpack --mode production

You can run the result at https://doggame.kieranmathieson.com/. There’s a GitHub repo, too.

Done!

Sometimes, cutting and pasting commands from the Interweb is an exercise in frustration. You need to dig a little, and understand the tools you’re installing. Hopefully, this article will help you get ready to make great Phaser 3 games.

Thanks to Teagan Mathieson and Jeremy Sprigler for their help with this article.

Resources

VS Code. The free IDE for JavaScripters.

Webpack. Smoosh JS to reduce download time, and save users’ minutes.

TypeScript. Use this instead of writing plain JS. Your blood pressure will thank you.

Phaser. A JS game library. Good stuff.

Node.js. JS outside a web browser. NPM and other things need it.

TSLint. Nags you as you type your TypeScript. In a good way.

--

--