Developing Cobalt Dungeon using Phaser and Cordova

Multithreaded JavaScript has been published with O'Reilly!
Visit the original version of this article on html5gamedevs.com.

I recently released a mobile game called Cobalt Dungeon. This game is based on Phaser 2 and uses a fullscreen canvas and basically only touches the DOM during initialization. The application itself is wrapped in Cordova 8 and runs on iPhone and Android. It's currently available for both platforms in their respective stores:

The game took me four months to create, from start to finish. I had created games before using HTML5 / canvas, and had released one before on the app stores (I later open-sourced it as Mobile Game Skeleton, if you're curious), so the process was familiar. This game is very Indie, like most of the games on this forum, with me being the only dedicated contributor. I made the music and sound effects, sourced /remixed existing graphics, etc. One thing that might make this game a little special is that 99% of the development was done on Linux, almost entirely using open source tools, though I do have a Macbook Air I use solely for iOS builds and I did use FL Studio for music. I'd like to talk a bit about the process I went through, hopefully someone else will find it useful as well!

I have been producing a series of game development videos recently and will make a high-level one soon based on this post! I also have a blog with hundreds of development posts, if you are into the whole reading thing.

Development Workflow

Almost all of the day-to-day game development, e.g. the actual core gameplay of moving around the dungeon and attacking enemies and picking up items, is designed and debugged using the responsive view of the web debugger in Google Chrome. Personally I prefer Firefox for web browsing but there's no beating Chrome when it comes to dev tooling. This tool is great because once you tell it to pretend to be a particular device (e.g. an Android Nexus 5x or an iPhone 6s) it'll do everything from setting the user-agent to reconfiguring the pointer to send touch events. Being able to interact, alter game state, and read console messages in real time is a total must. Very little on-device development happened for the first three months (mostly occasional performance testing).

Of course, testing locally is fine for a while, but you definitely do need to test on a real device. My daily driver is a Nexus 5x which is a little over 2 years old. I used this device for most of my testing. I figure the average phone is about as powerful as this one so I would constantly make sure the game ran at 60 FPS on this device. My other test device is an iPhone 5. This phone is like 5 years old, so it's mostly safe to say that any game that'll run on this phone will run on any modern iOS device. (One caveat is the new iPhone X notch, which I still need to fix. Resolution differences are also important to consider)

You may find weird edge cases which only present themselves when your game runs on a physical device. A common example is related to touch inputs. When debugging these issues you can actually use a Chrome feature called Remote Debugging, which will allow you to use Chrome installed on your computer to interact with Chrome on your Android device (I'm not sure if such a feature exists with iOS Safari, but I didn't need such a feature). Normally Chrome will even show duplicate the view of the phone on your computer screen but it doesn't seem to work with Canvas.

Artwork/Aesthetics

The game uses 16×16 tiles and a palette of 24 colors. If you find yourself making a pixel art game, you must pick a palette and adhere to it! I found that having such a restriction really helped with creativity, as well as getting an overall cohesive feel. The basic terrain is based on free artwork I found on OpenGameArt.org. That website is amazing for getting assets and I highly recommend you check it out (here's a bunch of music I contributed if you're into that). The players / enemies are based on another art pack which I purchased a license for (around $30 at the time, I think). I would then touch up this artwork to get everything following the same palette. I would also have to stitch the graphics together to build spritesheets and get animations going.

Any time I would create new artwork I then do my best to fir the same art style. For example, the first boss Shroomzilla I put togehter. It uses the same color palette, though the visual style is definitely more complex than the simpler graphics used throughout the rest of the game. The graphics for the Ice, Moss, and Fire worlds are altered from the base terrain, but still follow the same palette. The main menu graphic of a stairway going into the dungeon is also something I had to draw based on googled reference material (also with the same palette).

The graphics I use do use 1×1 pixels in their source PNG files, despite being rendered on device as some arbitrary pixel size, e.g. 6×7. I do the stretching by dynamically scaling the viewport when the game first loads (I'll make a video explaining this process at some point). This is cool because it's impossible for me to address a sub pixel. You'll see this issue in a lot of pixel-art games where there's partial pixel overlap. Rendering on Android is done with Web GL, and on iOS with software. This was necessary to get the viewport scaling to work, prevent blurry pixels on iOS, and prevent slow performance. E.g., scaling the viewport when rendering Web GL on iOS is very slow and blurry and slow, and works perfectly on Android.

Music/Sound Effects

The audio was all custom made for this game, both the sound effects (SFX) and the background music (BGM), though the process for making both is very different.

SFX: The Sound Effects were mostly made using the wonderful as3sfxr tool. This tool, despite being super old and written in Flash, is one of the best (if not _the_ best) tools for making 8-bit (-esque?) sound effects. The workflow I use is to first figure out what part of the game needs a sound effect, then click the category which sounds most similar, and then start randomizing the sound until it sounds similar to what I'm looking for. If a randomization goes in the wrong direction then undo and try again. Once a sound is pretty I then modify individual parameters until it sounds correct. Once it's done I generate a WAV file and load it into FL Studio. The goal with FL Studio is to create a single MP3 file with all the sound effects, e.g. the first at 0 seconds, the next at 3 seconds, etc. Once this is done I specify the sound locations using JSON. Unfortunately I kept having issues with Phaser's audio library so I chose to use a library called Howler to play the audio. The code for using Howler looks something like this:

const TIME = 2999;
const SFX = {
  damage: [0, TIME],
  explode: [3000, TIME],
  door: [6000, TIME],
  wait: [9000, TIME],
  upgrade: [12000, TIME],
};

const sfx = new Howl({
  src: './audio/sfx.mp3',
  sprite: SFX,
  autoplay: false,
  volume: 1.0,
  onload: finishPhaserPreloading
});

sfx.play('damage');

BGM: The music was entirely composed using FL Studio (overview video, some notes on how I made the music). One of my goals was for the game to feel familiar to the generation of 8-bit / 16-bit gamers. However, making actual 8-bit / chiptune music was not a requirement by any means. So what I chose to do was keep every song simple; most have only 2 or 3 instruments. With the exception of percussion/drums, the music is entirely synthesized. Whenever possible I would use simple waveforms and simple filters (e.g. for the ice levels the lead is a sine-wave with a touch of delay and reverb). Sometimes I would use more complex instruments, like the string instruments in the main menu / fire levels. With the exception of the main menu music, each song follows the same structure, which you can download as an FLP file here. The tempo does change which is why the songs are of different lengths.

Overall I like the way the music turned out. I frequently listen to it while commuting. There's a few things that annoy me, like the bridge in the ice music or the repetitiveness of the moss/jungle music, but overall it's not too bad. The music is the largest part of the application, consuming about 18MB of the overall ~24MB binary. Since most people are listening to it via crummy mobile phone speakers I've compressed the audio at 96kbps in the game (higher bitrates are available for download on my Patreon).

Libraries / Code

As I mentioned, this game is built using Phaser 2 (I might upgrade to Phaser 3 soon, now that the children/group feature is being added), as well as the Howler library. Code is written in mostly ES2015 syntax. I use Browserify to combine my code, traversing import/export statements. Once browserify is done I then pipe the output through Google Closure Compiler to get a single JavaScript file without any comments or whitespace, and being minified as much as possible (e.g. dead code paths are removed). I don't, however, run any of the libraries through closure compiler. So in my final HTML file I'm loading four libraries: phaser.js, howler.js, cordova.js, and my games bundle.js. Many people seem to enjoy many weeks configuring webpack and getting bleeding-edge versions of the language transpiling but I try to avoid that as much as possible.

The game is wrapped in Cordova 8. This requires a whole bunch of JDK and Java build tooling be installed, as well as Xcode on my Macbook. Configuring all that tooling is a nightmare! I also make use of the following four Cordova plugins:

<plugin name="cordova-plugin-vibration" spec="^3.0.1" />
<plugin name="cordova-plugin-media" spec="^5.0.2" />
<plugin name="cordova-plugin-admobpro" spec="^2.31.1" />

Vibration is required to get vibration working on iOS (it's not required for Android, it just works out of the box). The media plugin is interesting. At first I would play all the music using Howler. This means the browser itself load the audio into memory. Unfortunately I found that the browser is incapable of destroying the music, even when the appropriate methods are called to unload the audio! Using the Cordova media plugin is necessary if you want to be able to play more than a few songs and not have a mobile browser segfault without a stacktrace in sight. The admobpro plugin is used display ads in the game. Unfortunately the author skims a few percent of your proceeds off the top unless you buy a license. I'm also trying to use an IAP module but am currently having compatibility issues with that and Cordova 8. Now that the game has released I'll try to get the plugin working as I'd like to offer players the ability to give me $2 and to have ads disabled (ads also hurt performance).

I do load two JavaScript libraries installed via NPM into the compiled bundle.js by way of Browserify. These are two libraries I also made and open-sourced. Neither has any dependencies so that the output bundle is as straight-forward as possible. The first one is roguelike. This library has a ton of features! All the level and room generation is done using this library. I also use it for a lot of math / random / dice roll calculations as well. The second library is autotile. I use it for taking a 2D array of booleans (representing if the ground is a floor or a hole) and converting it into a format to represent the actual spritesheet offsets. This is very handy so that you don't need to perform the calculations yourself. This allows me to represent a floor using a simple array of booleans instead of tightly coupling it with spritesheet offsets.

Tools

As I mentioned, I use FL Studio to do the audio work. I bought the Producer version for $200 and have been really happy with it. Since I'm running on Linux that means I need to wrap it in Wine. This experience is a little iffy, e.g. if I attempt to scroll anywhere in the UI the app completely freaks out. Other than that it's been a pretty solid experience, especially since my songs only have a few instruments. For most music, however, this won't be true. The more instruments and effects running, the higher the CPU cost. Running FL Studio directly on Windows will be much more efficient than with Wine and Linux. There are of course free alternatives, especially native Linux tools, but I had used it years ago and was comfortable with it.

All coding was done using VIM. Once you get used to those keybindings you'll be trapped using this editor forever.

While most of the rooms are procedurally generated, I did want to create a bunch by hand. For example, all of the tutorials and challenges are done by hand. For those I used a tool called Tiled. (At first I hand-generated JSON. This would never scale and prevented creativity). Tiled allows me to edit a visual map, with different layers, using the actual spritesheets used in my game. Tiled will output an XML file format which can even be imported into Phaser! However, passing around a file format representing rooms which is so tightly coupled to the graphics is not a route I wanted to take. So I wrote a converter tool to convert the XML files created by Tiled into a very simple JSON representation which contains only exactly what I need to represent a level. As an example, enemies can be represented as an array of objects with an X/Y coordinate, an Enemy ID, and their Phase. The Tiled representation would contain lookup information for the enemies coordinate in the spritesheet, the layer then graphic is on, and wouldn't have the metadata I need. Here's a video of my Tiled workflow if you'd like a better idea of how I make rooms.

For creating the Bitmap font format used by Phaser I used a tool called BM Font. This allows me to take a TTF font and create the XML/PNG files needed for rendering on the web. The process for doing this can be super complex so I even made a video on font conversion for Phaser.

Graphic editing was done using GIMP, a free image editing app originally made for Linux but is available for all platforms. Coming from a Photoshop background it can take a while to get used to the keyboard shortcuts and the weird choices GIMP made (e.g. layers have different dimensions, what's up with that?!) I created a palette using the 24 game colors and that really helped my efficiency. I also tweaked the UI to be in single window mode (ala Photoshop) and also reconfigured the tools (mostly disabling anti-aliasing, enabling a 16×16 grid). Once you dig through the menus it's possible to save the tool configuration permanently, which really helps with efficiency.

Hardware

I did the drawing of the main menu graphic using an old Wacom Tablet Bamboo, it's older but probably cost $100. The music was made using an Akai MPK Mini (two octave MIDI controller), also for $100. Neither of these tools were required to produce this game, but they sure make it a fun process. All development was done using a Lenovo Thinkpad Carbon X1 5th generation, by far the best laptop I've ever owned! Worth the $1600 price tag.

Live Ops

Once a game is live you don't want to have to release a new version of the app for every little update. This is when having a CMS is really helpful. I built a Node-based CMS specifically for game development called Grille. My workflow for this is that I edit a Google Sheet, change values (for example enemy attributes, shop costs, game text, etc). Once I'm satisfied with the result I use Grille to generate a JSON file which I can upload to a CDN.

Of course, it's useful to know what in the game actually needs changing! For example, do most players get to level 7 and then stop playing because they get killed by a Mage and get frustrated? For that I use a service called Mixpanel. Throughout the codebase I make analytic calls with useful data. For example, when a player dies, goes to a new floor, buys a shop item, views and ad, etc. I can then use the Mixpanel UI to view a “funnel” of users as they progress through the game.

Mixpanel Level Progression Funnel
Mixpanel Level Progression Funnel

Monetization

I make money with ads thanks to Admob. There are two types of ads; the first one is an ad shown when the player switches between rooms. This ad is displayed at most once every five minutes. The second ad is an ad the player can choose to view. When the player dies they can choose to view an ad to respawn in the room they died in. Otherwise they can choose to respawn at an older save point.

Thomas Hunter II Avatar

Thomas has contributed to dozens of enterprise Node.js services and has worked for a company dedicated to securing Node.js. He has spoken at several conferences on Node.js and JavaScript and is an O'Reilly published author.