I recently discovered a sort of unbelievable game development platform by Scirra called Construct 2. Using this tool you can basically create an entire game without any programming whatsoever. It’s pretty darn neat.
Space Blaster is one of their demo games. You can find it on the Chrome Web Store, as well as Facebook. After playing a few rounds, I wanted to try it out on my Nexus 4. I downloaded Construct 2, opened up Space Blaster (which ships with Construct 2 as an example), and exported an HTML5 app. It looked like this:
|
|
SpaceBlaster |-- c2runtime.js |-- index.html |-- jquery-1.7.1.min.js |-- logo.png |-- offline.appcache |-- images |-- media |
I ran:
|
|
$ python -m SimpleHTTPServer |
This is where my troubles began.
Mobile Browsers are for Reading Email
I opened Space Blaster in a browser on my phone and almost immediately started crying. The first thing I noticed was how laggy it was. The frame rate was so low that my bullets were passing right through enemy ships. The second thing I noticed was the absence of sound. Then it crashed.

This bummed me out a little bit because the actual game is super fun. It’s got great sound effects, and the animations are very impressive when they actually play smoothly. Then I got a little mad, because I should know better. Playing a game in a modern mobile browser is like reading the news on a flip phone or sending an email with a beeper.

Any of these can be attempted, but why? The basic reason, I suppose, is that folks insist on doing everything on the go, which means working with the materials at hand, no matter how terrible they are. I find myself in a similar situation, the bearer of an HTML5 canvas game that I yearn to play on my phone. Luckily, I work at a company that makes a native-accelerated OpenGL canvas for running HTML5 games on mobile.
Why Would Anyone Do This?

Let’s step back for a second and really appreciate what Construct 2 has to offer. It enables someone with zero coding experience to create a complete HTML5 game. Is your mind blown yet? It really should be. Think of all the sweet game ideas that at this very moment are bouncing around in people’s heads, and how many of those people aren’t programmers. Five years ago, these ideas would never see the light of day. Nowadays, thanks to frameworks like Construct 2, they really can become games. That’s truly awesome.

So where does Game Closure fit in? It’s quite simple. Essentially, lots of people (including Construct 2 users) are creating HTML5 games that run awesomely in real browsers and terribly in mobile browsers. We’ve already solved this problem with a custom native implementation of the HTML5 canvas. If we can leverage our technology to help bridge this gap, we can bring these games to a much wider audience. If we can streamline the process, folks will be able to make HTML5 games using any framework out there and deploy them to smart phones with ease. This degree of compatibility, after all, is the point of HTML5 and open standards in general.
Step one is porting Space Blaster.
Getting Something on the Screen
Basic Setup
I made a fresh game with basil in devkit/projects, and pulled in the JavaScript and images I figured we’d need from Space Blaster:
|
|
$ basil init scblaster Created a new empty project at scblaster [register] adding scblaster $ cp ~/SpaceBlaster/*js scblaster/src/ $ cp ~/SpaceBlaster/images/* scblaster/resources/images |
My src folder now contained 3 files: the Application.js generated by basil init; c2runtime.js, where the Space Blaster game lived; and jquery-1.7.1.min.js, which Space Blaster seemed to require (and which I renamed to jq.js for ease of importing). I opened up Application.js and manifest.json and started tinkering.
Step one was to get something running in the browser. First I changed the game’s orientation from landscape to portrait. This simply entailed replacing the string landscape with portrait in the supportedOrientations array in manifest.json. Easy peasy.
Next I turned to Application.js, which looked like this:
|
|
import ui.TextView as TextView; exports = Class(GC.Application, function () { this.initUI = function () { var textview = new TextView({ superview: this.view, layout: "box", text: "Hello, world!", color: "white" }); }; this.launchUI = function () {}; }); |
Great. I deleted initUI and replaced the TextView import at the top with a couple imports of our own:
|
|
import src.jq; import src.c2runtime; exports = Class(GC.Application, function () { this.launchUI = function () { // do something here! }; }); |
At this point it ran with no errors and transitioned from a GC splash to a black screen. What fun! Now we just need the actual game.
Digging, Lying, and Cheating
So how do you start this thing? A quick scan of the index.html generated by Construct 2 reveals the magic line to get the show on the road:
|
|
cr_createRuntime("c2canvas"); |
A search for this function in c2runtime.js takes us to this code:
|
|
cr.createRuntime = function (canvasid) { return new Runtime(document.getElementById(canvasid)); }; cr.createDCRuntime = function (w, h) { return new Runtime({ "dc": true, "width": w, "height": h }); }; window["cr_createRuntime"] = cr.createRuntime; window["cr_createDCRuntime"] = cr.createDCRuntime; |
Seems straightforward enough. They’ve got this cr.createRuntime thing, and they attach it to the window object as cr_createRuntime. Fantastic. The only problem is that they expect to pull a canvas out of the DOM using an ID. There’s no DOM on the phone, so this approach won’t work. But wait! There’s that other function, createDCRuntime (attached to window as cr_createDCRuntime), which just takes a width and height. Worth a shot! I added
to my previously-empty launchUI, leaving width and height undefined because the constructor resizes the canvas to the max dimensions available, which is what we want. I ran it again and immediately got an error:
|
|
Cannot call method 'addEventListener' of undefined |
Fair enough. Construct 2 barfed while trying to route input events to their handlers. You guys didn’t expect this to work right off the bat, did you? Anyhow, it seemed that the undefined variable was being set a few lines up, in this code chunk:
|
|
var elem = (this.runtime.fullscreen_mode > 0) ? document : this.runtime.canvas; var elem2 = document; if (this.runtime.isDirectCanvas) elem2 = elem = window["Canvas"]; else if (this.runtime.isCocoonJs) elem2 = elem = window; |
So what’s this.runtime.isDirectCanvas? It gets set in the Runtime constructor, like so:
|
|
this.isDirectCanvas = !!canvas["dc"]; |
And what’s canvas? That’s just the object that gets passed in by cr_createDCRuntime (see above), and all it has on it are width, height, and dc (which equals true). So since we’re using cr_createDCRuntime, isDirectCanvas is definitely true, which means that elem and elem2 get set to window["Canvas"], which, according to the Chrome debugger, isn’t anything. So let’s make it something, and while we’re at it we can map our input events to Construct 2′s handlers.
|
|
// map our input events to Construct 2 handlers var inputMap = { touchstart: "onInputStart", touchmove: "onInputMove", touchend: "onInputSelect" }; window.Canvas = { addEventListener: bind(this, function(name, handler) { this[inputMap[name]] = function(e) { e.changedTouches = [{ pageX: e.pt[1].x, pageY: e.pt[1].y, identifier: 0 }]; handler(e); }; }) }; |
Construct 2 will now add its event listeners directly to what it thinks is a “direct canvas” (which we know to be our shim object). Our shim in turn will create input callbacks that modify our input events (Construct 2 expects a “changedTouches” array) and pass them along to Construct 2′s handlers. Simple dimple!
Run it again and we get a different error, which means we’re making progress. This time, AppMobi is not defined, which makes sense because we’re not AppMobi. The error happens here:
|
|
this.ctx = AppMobi["canvas"]["getContext"]("2d"); |
Construct 2 is trying to get a canvas from AppMobi. Time to lie:
|
|
// pretend to be AppMobi window.AppMobi = { canvas: GC.app.engine.getCanvas() }; |
We’ve given them our canvas instead. The guilt will fade with time.

Refresh for the next error. Now that Space Blaster has successfully gotten its hooks into our Context2D, it expects to find a function called present. Here’s where the game tries to call it:
|
|
Runtime.prototype.draw = function () { this.running_layout.draw(this.ctx); if (this.isDirectCanvas) this.ctx["present"](); }; |
At this point I’m pretty happy that we’ve made it to the draw function. This just might work! So what’s present? Turns out it’s an AppMobi/DirectCanvas thing that gets called every tick to trigger a render. Our canvas knows how to render without this. Let’s cheat!
|
|
// pretend to be AppMobi (lie) // pretend to support context.present (cheat) var canvas = GC.app.engine.getCanvas(); var ctx = canvas.getContext('2d'); ctx.present = function() {}; window.AppMobi = { canvas: canvas }; |
Great! Refresh, next error here:
|
|
if (this.isDirectCanvas) AppMobi["webview"]["execute"]("onGameReady();"); |
Oh man, this direct canvas is so demanding! Let’s just cheat again:
|
|
window.AppMobi = { canvas: canvas, webview: { execute: function() {} } }; |
This is fun.
I Still Can’t See Anything
Ok, now we’re getting an error in ctx.drawImage. This is because we haven’t told Construct 2 where the graphics are hiding. Let’s do it!
As it turns out, the whole game is defined in a JSON object at the bottom of c2runtime.js. In this case, the object is over ten thousand lines long. Anywho, the most recent error indicates that it’s trying to find an image at images/background3-default-000.jpg. The DevKit serves image assets in resources/images, so this is an easy fix. We just have to override cr.getProjectModel, which generates the game definition object. I added this to launchUI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
// import game and modify project model jsio("external src.c2runtime import cr"); var oldGetProjModel = cr.getProjectModel; function deepReplace (obj, search, replace) { if (isArray(obj)) { return obj.map(function (item) { return deepReplace(item, search, replace); }); } else if (typeof obj == 'string') { return obj.replace(search, replace); } else { return obj; } } cr.getProjectModel = function() { var data = oldGetProjModel(); // overwrite image asset paths data = deepReplace(data, /^images\//, "resources/images/"); return data; }; |
That jsio line at the top is some black magic for grabbing a local variable out of a JavaScript file. Since we’re importing c2runtime here, we can remove the import from the top of our Application.js. The rest basically remaps Space Blaster’s image sources to their new home in resources/images. When I run it again I get the same error:
|
|
Uncaught Error: IndexSizeError: DOM Exception 1 |
This basically means it still can’t find the image it’s looking for. Let’s tell the DevKit not to build spritesheets for these assets, as they’ve already been sprited. Create a new file in resources/images/ called metadata.json, and paste in this:
And it works!
I Still Can’t Hear Anything
Right, the original had sound, and lots of it. Let’s grab the audio assets we need.
|
|
$ cp -r ~/SpaceBlaster/media/ scblaster/resources/media |
Now we just need the game to look in the proper place. We’ll solve this the same way we handled image asset paths, by overwriting the project model.
|
|
cr.getProjectModel = function() { var data = oldGetProjModel(); // overwrite asset paths data = deepReplace(data, /^images\//, "resources/images/"); data = deepReplace(data, /^media\//, "resources/media/"); return data; }; |
Refresh, and we’ve got sound!
Wait, What About the Phone?

Remember a million years ago when the whole point of this was to run Space Blaster on a phone? Let’s give it a shot. Plug an android phone into your computer and start typing in a terminal:
|
|
$ basil debug native-android --install |
Great!
A Few More Fibs
You just installed the game on your phone. If you try to run it, you’ll get a gross jQuery error:
|
|
{js} Reporting exception ./src/jq.js line: 2 ,g,h,i,j,k,l,m,n,o,p,q=c.createElement("div"),r=c.documentElement;q.setAttribu TypeError: Cannot call method 'setAttribute' of undefined at ./src/jq.js:2:17334 at ./src/jq.js:2:20984 |
Man, I’ve heard enough out of you, jQuery. You worked fine in the browser, but now you expect a full DOM on the phone, and that’s just not going to happen. What exactly is this game doing with jQuery? I removed the jq import at the top of Application.js to find out.
It turns out that everything runs fine without jQuery, for the most part. The only thing missing is window size detection, which only happens if jQuery is present:
|
|
if (typeof jQuery !== "undefined" && this.fullscreen_mode > 0) this["setSize"](jQuery(window).width(), jQuery(window).height()); |
So let’s fake it. Add this to launchUI:
|
|
window.jQuery = function(w) { return { width: function() { return w.innerWidth || w.screen.width; }, height: function() { return w.innerHeight || w.screen.height; } }; }; |
Looks good in the browser. After rebuilding and reinstalling on the phone, you’ll see this:
|
|
TypeError: Object # has no method 'getElementById', from ./src/c2runtime.js:1861 |
Yup, there’s no DOM, so there’s no document.getElementById. Time for more lies!
|
|
document.getElementById = document.getElementById || function() { return null; }; |
Don’t worry, no one ever has to know. Rebuild, reinstall, and see the next error:
|
|
TypeError: Cannot call method 'loadSound' of undefined, from ./src/c2runtime.js:9760 |
Looks like we’ve got more shimming to do. Update window.AppMobi to look like this:
|
|
window.AppMobi = { canvas: canvas, context: { loadSound: function(src) {}, playSound: function(src) {} }, webview: { execute: function() {} } }; |
That’ll do it. Our loadSound doesn’t actually need to do anything, because our AudioManager can load everything we need up front. You’ll see.
But Where’s the Game?

Rebuild, reinstall, and run. The first and last thing you’ll notice after the GC splash is a black screen. This is because alwaysRepaint defaults to true on the GC engine. When alwaysRepaint is true, the engine draws every tick. Since we’re not doing any of the drawing, this just clears the canvas every tick, obliterating whatever Space Blaster just tried to draw. Let’s turn that off:
|
|
// turn off screen clearing this.engine._opts.alwaysRepaint = false; |
Next thing you know you’re looking at the Space Blaster main menu on your phone. Yay! Before you pop the champaign, tap for the next error.
Creating createPattern
The problem:
|
|
TypeError: Object # has no method 'createPattern', from ./src/c2runtime.js:12059 |
This example demonstrates the basic usage of createPattern:
|
|
var ptrn = ctx.createPattern(img, 'repeat'); ctx.fillStyle = ptrn; ctx.fillRect(0, 0, 150, 150); |
It’s basically an image tiler with a funny API. Very doable. Here’s our 2D Context’s implementation of fillRect:
|
|
this.fillRect = function(x, y, width, height) { this._ctx.fillRect(x, y, width, height, this.fillStyle, this.getCompositeOperationID()); }; |
Great, all we need is a createPattern that returns some kind of object, and a this.fillStyle test of some sort in fillRect, and we’re good to go. This createPattern should do the trick:
|
|
this.createPattern = function(img, repeatPattern) { return { img: img, repeatPattern: repeatPattern }; }; |
Now let’s just handle this case in fillRect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
this.fillRect = function(x, y, width, height) { if (typeof this.fillStyle == 'object') { var img = this.fillStyle.img, w = img.width, h = img.height, wMax, hMax, xx, yy, op = this.getCompositeOperationID(); switch (this.fillStyle.repeatPattern) { case 'repeat': for (xx = 0; xx < width; xx += w) { wMax = Math.min(w, width - xx); for (yy = y; yy < height; yy += h) { hMax = Math.min(h, height - yy); this._ctx.drawImage(img.__gl_name, img._src, 0, 0, wMax, hMax, x + xx, y + yy, wMax, hMax, op); } } break; case 'repeat-x': for (xx = 0; xx < width; xx += w) { wMax = Math.min(w, width - xx); this._ctx.drawImage(img.__gl_name, img._src, 0, 0, wMax, hMax, x + xx, y, wMax, hMax, op); } break; case 'repeat-y': for (yy = 0; yy < height; yy += h) { hMax = Math.min(h, height - yy); this._ctx.drawImage(img.__gl_name, img._src, 0, 0, wMax, hMax, x, y + yy, wMax, hMax, op); } break; case 'no-repeat': default: wMax = Math.min(w, width); hMax = Math.min(h, height); this._ctx.drawImage(img.__gl_name, img._src, 0, 0, wMax, hMax, x, y, wMax, hMax, op); break; } } else { this._ctx.fillRect(x, y, width, height, this.fillStyle, this.getCompositeOperationID()); } }; |


That’s all it takes! Now the DevKit supports createPattern invocations of various kinds: ‘repeat’, ‘repeat-x’, ‘repeat-y’, and the pointless ‘no-repeat’. Here’s a before and after. The createPattern on the left returns the string “red”. The createPattern on the right returns the object discussed above. Just look at all those missiles! Nice work, createPattern.
And now we’ve got a game! Wooo! Unfortunately, our game has the following problems:
- the screen goes black when you touch it
- there’s no sound
- some of the images are having transparency issues
These are hard core dealbreakers. Luckily, they’re easy to fix.
Black Screen on Touch?
This is due to another flag on the GC engine. Remember alwaysRepaint? There’s another engine flag that defaults to true called repaintOnEvent. The idea with this flag is that in cases when you’re not repainting every tick (alwaysRepaint == false), you’ll probably want to repaint when the user taps the screen (which generally results in some visual difference). Since we’re not doing any of the drawing, this behavior will simply result in a black screen whenever the user taps. So let’s turn it off. Add
|
|
this.engine._opts.repaintOnEvent = false; |
to launchUI, and you’re in business! Now take a break to play a few rounds. Yes, this is JavaScript, and yes, it actually works. Let that sink in before moving on to the next issue.
Sound?
Ok, so we copied over the Space Blaster audio assets to our new project. They’re running fine in the browser, but we’ll have to roll our own sound support to get a peep out of the phone. Luckily, the DevKit already did this for us.
We’ve got two copies of each sound: an m4a, which is played in the browser; and an ogg, which is played on the phone. We want file names without any spaces or weird punctuation, because this simplifies our AudioManager configuration. So I made a simple Python script that looks like this:
|
|
from subprocess import call print 'replacing dashes with underscores and sanitizing' call('rename -s - _ resources/media/*ogg -z', shell=True) |
And ran it like this:
|
|
$ python fixAudio.py replacing dashes with underscores and sanitizing |
Great! Now we can just drop in our AudioManager, which looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
// set up our sound manager var sound = new AudioManager({ path: 'resources/media', preload: true, files: { epicarpg: { background: true }, explosion_1: {}, explosion_2: {}, explosion_3: {}, explosion_4: {}, jetloop1: {}, lazer_fire_1: {}, lazer_ricochet: {}, mattoglseby___3: { background: true }, retrolaser1: {}, retrolaser2: {}, squaremotif1: {}, tronblast1: {}, upgrade1: {} } }); |
Remember when we shimmed loadSound with a function that didn’t do anything? That’s ok because of the preload: true here, which loads up all the sound effects for us. Also, I flagged the music files (epicarpg and mattoglseby___3) with background: true. This guarantees two things:
- these songs won’t preload (this would take up memory and time, and is unnecessary, as music files are streamed)
- these songs won’t play at the same time (playing one stops the other)
Don’t forget to import AudioManager at the top of Application.js:
Cool. Now let’s hook up our AudioManager to the AppMobi shim:
|
|
// pretend to be AppMobi window.AppMobi = { canvas: canvas, context: { loadSound: function(src) {}, playSound: function(src) { sound.play(src.slice(16).replace(/ /g, '_').replace(/-/g, '_').replace('.ogg', '')); } }, webview: { execute: function() {} } }; |
And it’s a game! With sound! Woohoo!
What’s with the Black Boxes?
Here’s a funny one. As it turns out, some of Space Blaster’s graphic assets use regular transparency, and others just have a black background. Silly game developers! Here are two explosion spritesheets from the game:
exlosion1-sheet0.png, exlosion2-sheet0.png:


See what I mean? The quickest fix here is an ImageMagick batch conversion, a la this script:
|
|
import os from subprocess import call def process(targets, dirname, fnames): print "replacing black backgrounds with transparency" for target in targets: hits = [os.path.join(dirname, fname) for fname in fnames if fname.startswith(target)] for fpath in hits: print ' - converting', fpath call('convert -transparent black -fuzz 10%% %s png32:%s'%(fpath, fpath), shell=True) os.path.walk('resources/images', process, ['explosion1', 'explosion3', 'playerthrust']) |
It finds all the graphics with names that start with “explosion1″, “explosion3″, and “playerthrust” (the other images use regular transparency), and uses convert (ImageMagick‘s command-line utility) to replace black with transparency. Run it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
$ python fixImages.py replacing black backgrounds with transparency - converting resources/images/explosion1-sheet0.png - converting resources/images/explosion1-sheet1.png - converting resources/images/explosion1-sheet2.png - converting resources/images/explosion1-sheet3.png - converting resources/images/explosion1-sheet4.png - converting resources/images/explosion3-sheet0.png - converting resources/images/explosion3-sheet1.png - converting resources/images/explosion3-sheet10.png - converting resources/images/explosion3-sheet11.png - converting resources/images/explosion3-sheet12.png - converting resources/images/explosion3-sheet13.png - converting resources/images/explosion3-sheet14.png - converting resources/images/explosion3-sheet15.png - converting resources/images/explosion3-sheet2.png - converting resources/images/explosion3-sheet3.png - converting resources/images/explosion3-sheet4.png - converting resources/images/explosion3-sheet5.png - converting resources/images/explosion3-sheet6.png - converting resources/images/explosion3-sheet7.png - converting resources/images/explosion3-sheet8.png - converting resources/images/explosion3-sheet9.png - converting resources/images/playerthrust-sheet0.png |
Great!
Spit and Polish
What’s left?
Icon
I used the GIMP to steal this image from one of the enemy spritesheets and resize as necessary:

Setting the icon in manifest.json is easy:
|
|
"icon": "resources/icons/icon512.png", |
Splash Screen
This game asset seems sufficiently spaceish:

We can make this the splash screen with a few simple lines in manifest.json:
|
|
"splash": { "autoHide": true, "universal": "resources/images/background3-default-000.jpg" }, |
Retrospective
We did it!
What We Accomplished
Here’s the Application.js we ended up with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
|
import AudioManager; exports = Class(GC.Application, function () { this.launchUI = function () { // turn off screen clearing this.engine._opts.alwaysRepaint = false; this.engine._opts.repaintOnEvent = false; // map our input events to Construct 2 handlers var inputMap = { touchstart: "onInputStart", touchmove: "onInputMove", touchend: "onInputSelect" }; window.Canvas = { addEventListener: bind(this, function(name, handler) { this[inputMap[name]] = function(e) { e.changedTouches = [{ pageX: e.pt[1].x, pageY: e.pt[1].y, identifier: 0 }]; handler(e); }; }) }; // set up our sound manager var sound = new AudioManager({ path: 'resources/media', preload: true, files: { epicarpg: { background: true }, explosion_1: {}, explosion_2: {}, explosion_3: {}, explosion_4: {}, jetloop1: {}, lazer_fire_1: {}, lazer_ricochet: {}, mattoglseby___3: { background: true }, retrolaser1: {}, retrolaser2: {}, squaremotif1: {}, tronblast1: {}, upgrade1: {} } }); // pretend to support context.present var canvas = GC.app.engine.getCanvas(); var ctx = canvas.getContext('2d'); ctx.present = function() {}; // pretend to be AppMobi window.AppMobi = { canvas: canvas, context: { loadSound: function(src) {}, playSound: function(src) { sound.play(src.slice(16).replace(/ /g, '_').replace(/-/g, '_').replace('.ogg', '')); } }, webview: { execute: function() {} } }; // import game and modify project model jsio("external src.c2runtime import cr"); var oldGetProjModel = cr.getProjectModel; function deepReplace (obj, search, replace) { if (isArray(obj)) { return obj.map(function (item) { return deepReplace(item, search, replace); }); } else if (typeof obj == 'string') { return obj.replace(search, replace); } else { return obj; } } cr.getProjectModel = function() { var data = oldGetProjModel(); // overwrite asset paths data = deepReplace(data, /^images\//, "resources/images/"); data = deepReplace(data, /^media\//, "resources/media/"); return data; }; // humor Construct 2 on native // - fake getElementById // - fake jQuery document.getElementById = document.getElementById || function() { return null; }; window.jQuery = function(w) { return { width: function() { return w.innerWidth || w.screen.width; }, height: function() { return w.innerHeight || w.screen.height; } }; }; cr_createDCRuntime(); }; }); |
And here’s the APK we ended up with: Space Blaster APK.
The project lives at this github repo. As it stands, you should be able to run any Construct 2 game on a phone by simply:
- 1) exporting your Construct 2 game as an HTML5 app
- 2) cloning this github repo
- 3) replacing
src/c2runtime.js with the c2runtime.js generated by Construct 2 for your game
- 4) replacing everything in
resources/images/ with the image assets from your game
- 5) replacing everything in
resources/media/ with the audio assets from your game
- update the
AudioManager in Application.js accordingly
- 6) running
fixAudio.py and/or fixImages.py if necessary
- if you have to run
fixImages.py, replace the target prefix array on line 12 with whatever prefixes correspond to images with a black background that should actually be transparent
I know that’s six whole steps. I want to simplify this process and probably turn this into some sort of plugin. I’ll keep you guys posted.
Why It Matters
To put this in perspective, consider the general case of porting a game from one platform to another. In fact, look at what this guy has to say about it:
You don’t need to be a 1970s stand-up comedian to know that there’s a large lexicon of monosyllabic, four-letter words for describing something you don’t like – but only PC gamers use the word “port” with such a fervent degree of repulsion.
Basically, he hates it, and so does everyone else, because it’s hard and it usually involves a rewrite. Now, we just took a game that only runs in a desktop browser, wrote about 100 lines of JavaScript, and came out the other end with a game that runs smoothly on Android and iOS. Did we rewrite the entire thing in Java and Objective-C? Did we hire a team of developers and set aside months of dev time? No dude, not even close. It took one developer a couple days to figure this one out, and the next Construct 2 port will take a matter of minutes.
The big takeaway here is that when we use open standards, we get to pool resources. I didn’t write Space Blaster, and Scirra didn’t write the DevKit, but they play nice together because we both bought in to the “HTML5″ idea. And so have lots of other game engines. This means that you should continue to develop your HTML5 games using any framework that tickles your fancy, because when the time comes, you can easily deploy to mobile using our native canvas. Isn’t that great? I think so.