The Architecture of spaceflipper
Spaceflipper is an adaption of a pinball game I recently developed for a client. I would like to share my experience with developing the game and show you some insights to the technical architecture of Spaceflipper.
Tools and Frameworks
First of all Spaceflipper is a game entirely written in HTML5 Canvas. After some research I decided to use the following frameworks to help me develop the application:
EaselJS – Canvas
EaselJS is an outstanding javascript canvas library. It’s pretty fast, super easy to use and extend (especially for former flash developers): http://www.easeljs.com
Besides that it has support for touch events out of the box which makes it even more awesome. If you’re interested in how to extend EaselJS for multitouch gestures, you can read my post Multitouch gesture transformations with EaselJS.
Actually apart from EaselJS I also used PreloadJS for preloading all the needed assets in advance and SoundJS for handling sound output on specific events.
So pretty much the complete CreateJS suite except from TweenJS, because I personally prefer:
TweenLite – Animation
TweenLite was immensly popular in the actionscript community. It is very small and an extremely fast animation engine.
A very nice feature of the framework is that you can animate an object along a defined curve. That was used for the pinball ramp in the upper-left corner. But more on that later.
Box2D – Physics
I’m pretty sure you are familiar with games like angry birds. That one and a whole lot of others use the popular Box2D physics engine. Reason enough to give the framework a chance: http://box2d.org
As Box2D was originally written by Erin Catto in C++ (thank you by the way) and was later on ported to other languages like flash, python and javascript there are no official repositories for those languages.
Especially for javascript there are like 10 different ports and It’s pretty hard to decide on which one to go. I found the following to be the most up-to-date and accurate, so I went with this one: https://code.google.com/p/box2dweb
The only thing I didn’t like about the port was that every class was bound to its original package, so you would have to write something like „new Box2D.Dynamics.b2Body()“ every time you need to create a new Box2D object.
Don’t get me wrong: I’m a huge fan of namespacing in general. But if your application relies heavily on a framework and all its classes it kinda makes more sense to have them easily accessible. Here’s a little snippet i wrote to extract all definitions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var global = this; (function(){ var extract = function(pkg, startsWith){ for (var k in pkg) { if (typeof pkg[k] === 'object') { extract(pkg[k], startsWith); } else { if (k.indexOf(startsWith) >= 0) { global[k] = pkg[k]; } } } }; extract(Box2D, 'b2'); })(); |
Afterwards creating a new Box2D object is written as „new b2Body()“. Straight forward and makes development a little faster.
FizzX – Box2D Editor
Coding Box2D shapes by hand can be a pain in the ass. Most of all if it’s not about simple shapes as a circle or a rectangle but rather complex polygon-shapes. Luckily there are several editors out there to help you draw shapes over a referenced background image. I decided to go with: http://allanbishop.com/fizzx. It’s clean, simple and exports paths as expected.
I have to admit that I didn’t even try RUBE, which seems like one of the most popular editors out there, so I can’t tell you anything about that one, but it is most probably worth a look: https://www.iforce2d.net/rube. Though iforce2d was a very helpful resource while learning Box2D.
Project Structure
This is basically the project structure. I like to have a minified app.js version at the root that get’s compiled via a simple pack.php script. debug.php sets up all the debug modes I need (for example Box2D’s debug draw) and stats.php is exactly the same as the final generated index.html except that it shows me the applications‘ FPS via stats.js.
The folder js is for external vendor scripts and frameworks, includes contains php classes and partials. images and css should be self explanatory. com contains my own EaselJS-Bootstrap framework: https://github.com/firsara/createjs-bootstrap (note to self: I should move this folder’s location). assets contains images and sound effects that get loaded at runtime and are needed by the application (i.e. the flippers, bumpers, etc.).
app is where all the javascript classes are and where the game logic gets developed.
I like the following sub-structure for my applications:
- display: for visible objects (i.e. Bumper.js, Ball.js, etc.)
- model: for internal data storage and calculations (i.e. Player.js to store points, lifes, etc.)
- shapes: contains all the exported shape definitions from FizzX
- utils: for helper classes (i.e. a custom Box2dUtils.js class, a FPS.js handler, etc.)
- views: for visually combining display objects into single views (i.e. GameView, WinView, MenuView, etc.)
- config.js: configures my application
- imports.js: extracts needed definitions to the global namespace (see snippet above)
- Main.js: my main application class which kicks off everything
- Reality.js: sets up the Box2D world and draws the debug view if needed
- run.js: gets basically loaded at the very last to create the main class instance
The structure could need a controller-subset, but I’m actually fine with the current setup.
Problems and solutions
I won’t go in depth down to actual code but I would like to show you some obstacles I came up with during development and how I solved them.
Box2D + EaselJS
At the beginning I had some problems using Box2D and EaselJS in combination but with a little trick it’s damn easy.
First let’s have a look at the initial setup and getting Box2D and EaselJS to update and render every frame at the same time (that’s what we want right?).
Even though I use abstract classes to help me with all the stuff a possible implementation could look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | var FPS = 60; var gravity = new b2Vec2(0, 15); var world = new b2World(gravity, true); var stage = new createjs.Stage('stage'); createjs.Ticker.setFPS(FPS); createjs.Ticker.addEventListener('tick', update); function update(e){ _this.world.Step( 1 / FPS //frame-rate , 10 //velocity iterations , 10 //position iterations ); _this.world.DrawDebugData(); _this.world.ClearForces(); stage.update(e); } |
This way we have Box2D and EaselJS in sync on every frame. It’s important to update Box2D’s world first otherwise you will notice a lag when you turn debugDraw on.
With that being said, my next problem was to have the Ball (the EaselJS object) visually positioned in the right spot on each frame (i.e. moving along its associated Box2D object). The basic idea is to have Box2D caluclate every movement and EaselJS just render images correctly positione to the canvas.
Let’s assume you’ve created a subclass of EaselJS‘ Container like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | this.createjs = this.createjs||{}; (function() { var Box2DContainer = function() { this.initialize(); }; var p = Box2DContainer.prototype = new createjs.Container(); // constructor: p.Container_initialize = p.initialize; p.initialize = function() { this.Container_initialize(); }; createjs.Box2DContainer = Box2DContainer; }()); |
Then, inside the initialize function, after creating a Box2D body and adding it to the world, you simply update the associated EaselJS object values on every tick, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var fixtureDef = new b2FixtureDef(); [...] var bodyDef = new b2BodyDef(); bodyDef.type = b2Body.b2_dynamicBody; var body = Reality.instance.world.CreateBody(bodyDef); this.addEventListener('tick', update); function update(){ var position = body.GetPosition(); // CONF.SCALE = box2d world scale // to translate box2d-internal values to pixels and vice versa this.x = position.x * CONF.SCALE; this.y = position.y * CONF.SCALE; } |
I’m thinking about writing a clean Box2DContainer.js class for EaselJS to get rid of the overhead in every class that needs to keep track of positions and rotations…
The ramp
The ramp in the top left was one of the things that i had some trouble figuring out how to best implement it. My first approach was to basically let the ball just roll into the ramp and let Box2D behave the way it does, which most likely leads to the following problems:
- If the ball is fast enough to enter the ramp but still too slow it might hang in the bottom of the 360°-curve. A possible solution would be to apply some Box2D-Force when the ball is detected to be inside the ramp.
- As Box2D does not contain any logic for a three-dimensional z-axis and we’re talking about a 360°-turn which will cross its path the ball would be blocked by one of the outlines. Inevitable. The only solution would be to dynamically add and remove the outer borders where the ramp crosses its path.
After some thoughtful time I came up with the following simple solution:
See the little dot right in front of the ramp? I created a Box2D object that does not collide with the ball but rather acts like a trigger.
When a collision is detected and the ball is fast enough I temporarily deactivate the ball from collision detection and simply run a predefined animation (see above: TweenLite – you can animate an object along a defined curve).
At a certain point a small ramp-overlay at the crossing section needed to be removed and later on re-added to simulate the ball rolling upwards, but after figuring out how to generally animate along a curve that’s easy!
The triggers
Speaking of triggers. There were used many others in the game. One for detecting the start position, one to keep the launch area closed after the ball is rolling into the game, two to add extra bumpers, a multiball-detector, etc.
As the Box2D documentation is a little messy this took me a long while to figure out, event though it’s as simple as:
1 2 | var fixtureDef = new b2FixtureDef(); fixtureDef.isSensor = true; |
adding isSensor to a fixtureDefinition will make it respond to contacts with other Box2D objects using event handlers but they won’t collide and jump off of each other.
The scaling
Since Spaceflipper scales perfectly fine to every device size (and that was a must-have in the first place) I had to come up with an idea for scaling and repositioning the content when resizing. My solution was to first calculate a scaling based on the window-size and the actual game-background-size and an x and y offset to have the view later on be repositioned to the center of the screen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | var canvasRatio = Main.instance.canvas.width / Main.instance.canvas.height; var viewRatio = GameView.size.originalWidth / GameView.size.originalHeight; if (canvasRatio > viewRatio) { var scale = Main.instance.canvas.height / GameView.size.originalHeight; GameView.scale = scale; GameView.height = Main.instance.canvas.height; GameView.width = GameView.size.originalWidth * scale; } else { var scale = Main.instance.canvas.width / GameView.size.originalWidth; GameView.scale = scale; GameView.width = Main.instance.canvas.width; GameView.height = GameView.size.originalHeight * scale; } GameView.xOffset = Math.round((Main.instance.canvas.width - GameView.width) / 2); GameView.yOffset = Math.round((Main.instance.canvas.height - GameView.height) / 2); // update box2d world scaling CONF.SCALE = CONF.ORIGINAL_SCALE * GameView.scale; |
With those values calculated I needed to apply the scaling to every visual asset created, like the ball or a bumper and then reposition by the scaling factor and moving by the x and y offset.
1 2 3 4 5 6 7 8 9 10 | bitmap = new createjs.Bitmap('ball.png'); bitmap.scaleX = GameView.scale; bitmap.scaleY = GameView.scale; var startX = 880; var startY = 1090; var ball = new createjs.Container(); ball.x = startX * GameView.scale + GameView.xOffset; ball.y = startY * GameView.scale + GameView.yOffset; |
This also applies to all the Box2D objects and shapes. You get the idea…
Result
Take a look at the final game: http://fabianirsara.com/examples/flipper/. It’s playable on a desktop computer using your keyboard or on an iPad or iPhone. It should run smoothly at 60 FPS even with a whole lot more balls used than with the current multiball implementation. Enjoy! 🙂