Javascript game development with Node.js, Grunt and Texture Packer

I have used MAMP for javascript game development for a few years, and it has worked fine except for a few issues I find cumbersome, like updating MAMP. I wanted to see if I would benefit from switching to Node.js and use Grunt to automate some of the more tedious tasks I usually do manually. It turned out I was right.

Grunt is a task runner that enables you to automate tasks and define your own workflows. You write your tasks in javascript and use Node.js to run them. For instance you could have a task that packs your sprites into a sprite sheet, or concatenates your javascript files into a single file and then minify it. There is a huge amount of tasks that have already been made so it's very easy to get started.

As for defining workflows, you can tie the tasks together in series and have different sets of task run for different cases. For example a development task can pack your code but not minify it and include debug libraries, whereas for a publish task you can have the code minified, exclude the debug libraries and push all of your code to a remote server. All of this is easily handled in the terminal by running commands like 'grunt development' and 'grunt publish'.

If you want to know more about Grunt, head over to gruntjs.com and check out the documentation.

Jesse Freeman has written a couple of articles, Phaser Project Template and Automating TypeScript with Node and Grunt, that could help you gain more knowledge on using Node.js and Grunt. He also uses TypeScript, which I don't.

I use OS X and all terminal commands I have written here work on OS X. I have no idea how well these translates to Windows.

Before we start

I have created a sample project that you can use for testing. You can clone the project from https://github.com/mandarinx/GameDevTemplateJS. This contains the Grunt file, package.json (I will explain this file later), the Texture Packer task and some sample graphics from http://kenney.nl/. You will have to install Node.js, Grunt and Texture Packer to be able to use the example project. Whenever I mention certain tasks and properties of the Grunt configuration, I refer to the Grunt file found in the example project.

If you don't want to clone the example project I recommend you to download the zip file and extract the Grunt file and package.json. It will make it easier for you to follow this article if you can see the code.

One of the tasks in my Grunt file uses Texture Packer to pack images into atlases. You will need to download the application and install the command line tools. Texture Packer has a trial version. Check out Texture Packer's documentation for how to install the command line tools.

Install Node.js

Node.js is really easy to install. Just download it, run the installer and follow any optional instructions given during the installation. Download Node.js from nodejs.org.

Install Grunt

Open a terminal window and execute this line:

npm install -g grunt-cli

When you add the -g option to npm install, you install the requested Node module in a global space. You will be able to use Grunt's command line interface (CLI) from anywhere and in any of your projects. Without the -g option you will install the module in the current directory.

package.json

package.json is a file that resides in your project's root folder and contains information about the configuration of your project. This file is very useful when sharing your project between co-workers, because your co-workers can simply run npm install in the project root directory, and Node will then use package.json to install any dependencies. You can always use the package.json file in the example project as a starting point for your own, but I recommend learning how to create one from scratch.

package.json is documented at https://npmjs.org/doc/json.html.

Create package.json

Create package.json by executing npm with the init option. Open your terminal window, go to your project's root directory and type the following:

npm init

I skipped the questions about entry point and test command. Just press enter when asked. Finish the wizard and save the file. Now that you have a package.json file, we need to add the modules which the Grunt task depends on. It's tedious to add them one by one, but I recommend you learn how to do it.

You can either install a node module and manually update package.json, or let Node.js handle it all for you. This is how you install a Node module and update package.json:

npm install package_name --save-dev

The last option, --save-dev, looks a bit funny. Why not just --save? It turns out there are multiple save options. As far as I know, they all have to do with certain aspects of distribution of your project. I haven't had any issues with sticking to --save-dev only, but if you are going to have multiple contributors on your project I suggest you read the section on dependencies in Node.js documentation.

Make sure you execute the function in the project's root folder. These are the dependencies my Grunt task depends on:

grunt-contrib-clean
grunt-contrib-concat
grunt-contrib-connect
grunt-contrib-copy
grunt-contrib-uglify
grunt-contrib-watch
grunt-open
grunt-exec
grunt-replace
grunt-mkdir

Install them one by one like this: npm install grunt-contrib-clean --save-dev, npm install grunt-contrib-concat --save-dev, etc. If Node complains during the installation (you'll see red text in the terminal window), you might have to execute npm install using sudo, like this:

sudo npm install package_name --save-dev

When you are done installing the modules you will see that Node has added a section to package.json called devDependencies and listed all modules and version number there. If you hadn't included the --save-dev option, Node wouldn't have update package.json. For any future module you will use, install it like you installed these and you won't have to worry about updating package.json.

You will probably notice that Node has created a sub directory in your project's root folder, called node_modules. This is because we have installed all dependencies in the local space (without using the -g option). These modules will only be accessible in this project.

I have added a custom property to my package.json. I'm not sure if it's kosher to add custom stuff to that file, but I haven't run into any problems yet. After the name property, I added a namePretty where I write the project name with spaces. E.g.: name: "MyNextGame", namePretty: "My Next Game". The prettified name is used in the title tag in index.html.

My package.json looks like this:

{
  "name": "GameDevTemplateJS",
  "namePretty": "Game Development Template for Javascript games",
  "version": "0.0.1",
  "description": "A starting point for learning how to use Node.js and Grunt for developing Javascript games.",
  "repository": {
    "type": "git",
    "url": "https://github.com/mandarinx/GameDevTemplateJS"
  },
  "author": "Thomas Viktil",
  "license": "GPL v2",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-copy": "~0.4.1",
    "grunt-contrib-watch": "~0.3.1",
    "grunt-contrib-connect": "~0.3.0",
    "grunt-exec": "0.4.2",
    "grunt-contrib-clean": "~0.5.0",
    "grunt-contrib-uglify": "~0.2.4",
    "grunt-open": "~0.2.0",
    "grunt-replace": "~0.5.1",
    "grunt-mkdir": "~0.1.1"
  }
}

Using package.json

Now that you have a package.json others can use it to download all of the dependencies by simply running:

sudo npm install

When running npm install, Node will look for a package.json file in the current directory and install all modules listed as dependencies. I would push package.json to my repository and ignore the node_modules directory.

Create a directory structure for your project

I haven't used this structure in more than two projects so it isn't exactly battle tested yet. I consider the setup a beta version because it works, but it has potential for improvements.

You can easily create all of the necessary directories by running the init task. Open your terminal window, go your project's root directory and run:

grunt init

If you copied the Grunt file out of the example project, you will see that Node outputted an error on the line after grunt init. It complained about not finding the ./tasks directory. Grunt tries to load all modules before running the task, and since the tasks directory hasn't been created yet, it outputs an error.

This is the directory structure you should have after running the init task:

./assets
./assets/atlas
./assets/audio
./assets/maps
./deploy
./resources
./src
./src/css
./src/js
./src/lib
./tasks

And this is how I use the directory structure:

Assets
Contains asset like audio, texture atlases, tile maps, etc. I use Photoshop's script 'Export layers to files' to save each layer out as separate files. Grunt will use Texture Packer to pack these files into atlases, and then save the atlases in the deploy directory.
Deploy
The directory you can upload to your server. It's initially empty but will be filled with files and directories by Grunt.
Resources
For Photoshop files, documents and other source files that don't belong in any of the other directories.
Src
Where I put source files. I write my code in src/js, put css files and web fonts in src/css, and finally any libraries that my game depends on goes into src/lib. At the root of the src directory you'll find an index.html. This will be copied to the root of the deploy directory and will be the main entry point for your game.
Tasks
Where I put all my custom tasks. Create one file per task to make it easier for you to maintain the code.

Grunt

There are plenty of articles that will help you understand Grunt files, so I won't go too in-depth on how this Grunt file works. I'll just outline the main functionality and explain some core concepts. You should keep the GruntFile.js from the example project open while you read this. It will be easier for you to follow.

At line 3 we pass an object to grunt.initConfig() containing settings for each task. Some of the objects are task definitions, and some are variables. For instance the pkg object refers to package.json so that we can access properties like the name of the project, version number, etc. The dir object is a list of directories in the project folder. I find it better to define paths to the directories like this so that I can change them in one place.

Objects like clean, copy, replace, concat, etc. are all task definitions and refer to various Node modules or custom tasks. For instance, clean will be used by grunt-contrib-clean. If the clean object is not present, or misspelled, Grunt will not be able to use the correct module. Each module has it's own way of configuring itself. All of the tasks I have found are usually documented on GitHub. Here are the documentation for the one's I'm using:

At the bottom of the file we define task aliases, which are tasks that execute other tasks in a specified order. For instance by running the task called default, Grunt will first run the clean task defined in the object we passed to grunt.initConfig(). When clean is done, it will continue with texturepacker and then run the build task. The build task is an alias defined a few lines above the default task. Once build is done, default continues with the rest of its sequence.

Close to the bottom of the file, we have a bunch of lines that look like this: grunt.loadNpmTasks('...');. These lines loads the Node modules we downloaded earlier. Loading of the modules was moved to the bottom of the file because it caused some errors with the texturepacker task. I can't remember exactly what caused the errors, but it works like this and it's not inconvenient either.

How the Grunt task works

The heart of the task is the default task. When you run the task Grunt will do the following, in this order:

  1. Clean. Grunt deletes all files in the deploy directory.
  2. TexturePacker. All atlases are packed into the deploy/assets directory.
  3. Concat. Javascript files in src/js are concatenated into one big file and put in deploy/js.
  4. Uglify. A copy of the concatenated file is minified (or uglified) into deploy/js.
  5. Replace. Variables such as the project name are added to index.html, and the modified file is saved to deploy.
  6. Copy. Various directories such as assets/audio, assets/maps, src/lib and others are copied to the deploy directory.
  7. Connect. Grunt starts a web server at port 80.
  8. Open. localhost/index.html is opened in Google Chrome.
  9. Watch. Grunt watches a few directories for changes and runs various tasks when certain files have changed.

Watching

When changes occur in the directories being watched, Grunt will run tasks so that the deploy directory always stays up to date. The directories being watched are:

  • src/js. Whenever you make a change to an existing file, create a new one or delete an old one, the task alias called updatejs is run. Updatejs will run Concat and Uglify.
  • assets/maps. I use Tiled for my tile maps. When I hit the save button in Tiled, Grunt will run copy:assets which refers to the assets target of the copy task. This will copy the assets from assets/maps to deploy/assets/maps.
  • assets/atlas. Grunt looks for changes in any file that resides under assets/atlas. The reason why I haven't restricted this to only png-files is that I want to detect changes in tps-files. When changes occur, Grunt runs the texturepacker task.
  • src/index.html. When you make changes to src/index.html, Grunt runs the replace task that replaces the occurrences of variables such as @@GameNamePretty with the prettified name specified in package.json.

Custom tasks

I recommend looking into creating custom Grunt tasks, because they can speed up your workflow a lot. I have created a so called multi task called texturepacker. This task is loaded by calling grunt.loadTasks('./tasks');. You'll find that line next to all the loadNpmTask-lines at the bottom-ish of the Grunt file. This line makes Grunt look for tasks in the tasks directory. Any correctly defined tasks will be loaded. You can easily dump your own tasks into this directory, and Grunt will automatically load them. You still need to configure your task in GruntFile.js.

You use and configure the texturepacker task by defining a texturepacker property to the object you pass into grunt.initConfig(). The configuration options are:

  • targetdir for specifying the directory where TexturePacker should put the packed atlas file(s).
  • tps for specifying an optional TexturePacker project file containing options for how to pack the atlas file. If you don't specify a tps file, the texturepacker task will use a set of default options defined in the task file.
  • dirs is used to specify a list of directories that will all use the same optional tps file and be outputted to the same target directory.

These options are bundled into targets so that you can define a different set of options for different directories. Targets are defined as sub objects to the texturepacker object. In the example file you will only find one target called misc.

Test run

Let's do a test run. Go to your terminal window, go to the project's root directory and type the following:

sudo grunt

You have to use sudo because some of the tasks requires it. For instance, the web server is set to use port 80 to make it easier to type the address in the browser.

The first thing you should have noticed is that Google Chrome opened a new tab pointing at http://localhost/index.html. The background color of the page should be light grey.

If you are using the example project, you will also see that Grunt has filled the deploy directory with files. Deploy should look like this:

./deploy/assets/atlas/boxes0.json
./deploy/assets/atlas/boxes0.png
./deploy/assets/atlas/hud0.json
./deploy/assets/atlas/hud0.png
./deploy/css/game.css
./deploy/index.html
./deploy/js/GameDevTemplateJS.js
./deploy/js/GameDevTemplateJS.min.js

GameDevTemplateJS.js should be 0 bytes, and GameDevTemplateJS.min.js should be 57 bytes, containing only the banner text defined in the uglify task.

Looking at the terminal window, you should see that Grunt is now watching your directories and waiting for something to do. You'll see that the last line says 'Waiting...'. Let's give Grunt something to work on.

Note: to quit the Grunt task press Ctrl + C.

Rebuild the atlas

Move assets/atlas/boxes/box.png out of the atlas directory. Don't delete it, you'll need it afterwards. Go back to the terminal window. Not far from the bottom of the window you should see a line that says >> File "assets/atlas/boxes/box.png" deleted. Grunt noticed that box.png was removed from the atlas directory and processed the new atlas. Let's look at the rest of the output.

1: >> File "assets/atlas/boxes/box.png" deleted.
2: Running "texturepacker:misc" (texturepacker) task

3: Running "exec:boxes" (exec) task
4: Resulting sprite sheet is 256x512
5: Writing sprite sheet to deploy/assets/atlas/boxes0.png
6: Writing deploy/assets/atlas/boxes0.json

7: Running "exec:hud" (exec) task
8: Output files are up-to-date, nothing to do

9: Done, without errors.

I have added line numbers just to make it easier to explain.

Line 1
Grunt notices a change and what the change was.
Line 2
Grunt runs the misc target of the texturepacker task. Take a look at tasks/texturepacker.js if you want to see how I made the texturepacker task.
Line 3
Exec is a task found in the texturepacker task, and all it does is call the command line interface tool for Texture Packer. Boxes is a target of the exec task and refers to the boxes directory under assets/atlas.
Line 4, 5, 6
This it output from Texture Packer. The files are written to deploy/assets/atlas/boxes0.png and boxes0.json.
Line 7, 8
In the texturepacker task you'll see that the misc target has two source directories. That's why Grunt also runs exec:hud. But Texture Packer sees that nothing has changed in the hud atlas and aborts it.

Look at the boxes atlas in the deploy directory and you'll see that box.png was removed. Try moving box.png back to assets/atlas/boxes/ and see what's happening.

Change index.html

The last test you can do is to open src/index.html in a text editor and make a change. Add a line of text on line 13 saying 'Hello!'. When you save the file, Grunt will update index.html in the deploy directory. Go to your browser and refresh the page to see the change. Looking at the terminal window, you should see these lines as the last ones:

>> File "src/index.html" changed.

Running "replace:index" (replace) task
Replace src/index.html -> deploy/index.html

Done, without errors.

Grunt noticed a change in src/index.html, ran the index target of the replace task and finished without any errors.

Add your own files

Now you should be ready to start adding your own files, such as a game library, art assets, tile maps and such.

Just to help you a little along the way, I prefer to use Phaser.js and would therefore recommend you to check it out. Go to https://github.com/photonstorm/phaser and download Phaser.js (use the button on the right that says "Download ZIP"). Save the zip-file anywhere you like, extract it when done downloading and copy the phaser/build/phaser.min.js to src/lib of your project.

Now you can open src/index.html and change the script tag that looks like this:

<!--script src="js/gamelibrary.min.js"></script-->

To this:

<!--script src="js/phaser.min.js"></script-->

In the deploy directory all javascript files are found in the js sub directory, even the one's from src/lib. When you refresh the browser tab, phaser.min.js will be loaded and you are ready to make a game.

Tips

You'll notice that the deploy/js directory has two copies of your game's javascript file, one that's minified and one that's not (GameDevTemplateJS.js and GameDevTemplateJS.min.js). The index.html refers to the non-minified version. I like to use that during development because it makes it easier for me to debug my own code. Before uploading to a server you should change it so the minified version is used, and exclude the non-minified from the upload.

Conclusion

I don't think I will ever go back to MAMP. I really like that I can write Javascript on the back-end, front-end and to automate tasks. I am very impressed by how easy it is to use Grunt, and hos fast it is. Compared to ImpactJS, where you have to run a php script to pack your code, Grunt packs my code every time I save a file. I would recommend Node.js and Grunt to anyone!

Future improvements

TexturePacker task

  • Add overrides to the tps file. You could use a tps for general settings, and in the task's target object add overrides for certain settings. The overrides object could also be used to override the default settings. Overrides could for instance be to adjust padding between sprites, choosing method for scaling or change output file format.
  • Create a separate repository with a proper README if anyone but me finds this task useful.

Directory structure

  • I'm thinking of reserving some names for various types of assets. If Grunt finds a directory called assets/atlas it will use the texturepacker task on that directory, while any other sub directories of assets are simply copied into deploy/assets.

HTML files

  • At some point it might be necessary to add more html files. For instance, I might want to have one for the game, one for the high score list, credits, contact info and so on. It will be messy to put them all in src/. I could put them in src/html and extend the replace task to run the replace task on all html files. The replace task accepts a list of files, but you have to add them manually. By creating a custom task I can do this automatically.

Task variables

  • More clarity in dir properties. It's now a mixture of having directories with and without a '/' ending. Some of the properties contains file patterns, such as 'assets/maps/*/.json'. I should propably create a separate object for patterns.

Deployment task

  • A deployment task could make the index.html refer to the minified version of the game file and upload the contents of the deploy directory to a server.
  • It could be handy to add an option for sending en email to certain addresses whenever the server is updated. Just like TestFlight does.

Example project

  • Make a proper README.

Latest articles