The 4th part of my series on creating an EaselJS-Game is about adjusting the game to different display-sizes, which is very important when targeting mobile devices. To visualize, what the result is going to look like – I added the following two iframes, to demonstrate how the game adjusts to a different screen-/stage-size.
Open this http://demos.indiegamr.com/jumpy/part4/index_nearestNeighbor.html in a new browser-window and see how it adjusts to your window-size as you reload the page after resizing the window.
As allways: There are many ways to do this, all will pros and cons, I’m going to explain two (fairly simple) ways to do this.
Method I – Linear Scaling: Easy&straight forward
‘Method I’ basically consists of these two steps:
- Calculate the minimum-scale from the width&height based on a default-width & default hight
- Multiply every (scale-relevant) value of the game with that scale(e.g: dimensions of objects, velocities, ect..)
Calculating the scale is just one line: Okay in my case it’s 3, because I added variables for the default-dimensions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/* defined globally */ var BASE_WIDTH = 800, BASE_HEIGHT = 400; // this was new to me here: i tried using 'const' for those values, instead // of 'var' which was working fine until i tried it with ...guess what: IE! /* when initializing the _game() */ var w = getWidth(), h = getHeight(), scale = Math.min(w/BASE_WIDTH,h/BASE_HEIGHT); // to make the scale accessbile from the 'outsite' through // Game.scale, we assign it to the Game-object self.scale = scale; |
So now that we have our scale-value: We are going to multiply every value, relevant to the scale, with it. As this involves quite a few parts of the code, instead of posting the whole updated source-code I’m going to list a few examples, and you can download the full source as always at the end of the page.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// e.g: when setting up the objects or position them hero.x = 50 * scale; hero.y = h/2 + 50 * scale; hero.scaleX = hero.scaleY = scale; // or when resetting attributes like velocity Hero.prototype.reset = function() { this.velocity = {x:10*Game.scale,y:25*Game.scale}; this.onGround = false; this.doubleJump = false; }; // or when updating values this.velocity.y += 1 * Game.scale; |
Adjustments
However – we are not quite done by just multiplying all the values. Since we now (can) have decimal values for the width and height of all objects, we need to watch out for flaws in the collision detection: to prevent decimenal numbers from screwing with our collision detection, I added a parameter <rounded> to the utility-method ‘getBounds()’:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function getBounds(obj,rounded) { /* I left out the calculation of the bounds, this is described in Part2 */ // and now round everything to get integer values if ( rounded ) { bounds.x = (bounds.x + (bounds.x > 0 ? .5 : -.5)) | 0; bounds.y = (bounds.y + (bounds.y > 0 ? .5 : -.5)) | 0; bounds.width = (bounds.width + (bounds.width > 0 ? .5 : -.5)) | 0; bounds.height = (bounds.height + (bounds.height > 0 ? .5 : -.5)) | 0; } return bounds; } |
In case you are wondering, why I’m not using “Math.round()”: Rounding with bitwise operators is 7-9x faster than using Math.round(): jsperf round() vs. bitwise benchmark
You don’t have to fully understand bitwise operations, but as a basic fact you should know, that with any bitwise operation all decimals will be erased. However, for optimizing code it is allways helpfull to know how bitwise works!
So all values from the returned bounds-rectangle will be integer values, to prevent the method from not detecting a collision, but then moving the object for example 0.5px too far, thus creating a collision-situation.
Rounding values like the position or the velocity of an object is in 90% of the cases a good thing to do, as it is basically impossible to spot a difference between rounded- and non-rounded pixel-values while it is (on most platforms) way faster for the canvas to render on pixel instead of subpixel values. So: use integer-values or for EaselJS-Objects: myObject.snapToPixel = true; – it is faster and in most cases you won’t see the difference.
That’s it with Method I of adjusting a canvas-app to the screen-size, you might think: ‘Looks good to me, why should I use anything else?!’ – here’s the reason: The following image shows two scaled versions of Method I and Method II – and since we are building a retro-/pixel-style game, guess which method we are going to use
Method II – Nearest Neighbor Scaling: keep hard pixel-edges
As there is (currently?) no reliable way to turn off anti aliasing, (actually there ARE ways to do this, but they won’t work across all browsers), we are going to use the following algorithm to scale each of our assets after they are done loading:
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 |
function nearestNeighborScale(img, scale) { // to have a good looking scaling // we will snap all values to 0.5-steps // so 1.4 e.g. becomes 1.5 - you can also // set the snapping to 1.0 e.g. // however I would recommend to use only // a multiple of 0.5 - but play around // with it and see the results scale = snapValue(scale,.5); if ( scale <= 0 ) scale = 0.5; // the size of the "pixels" in the new images // will be rounden to integer values, as drawing // a rect with 1.5x1.5 would result in half-transparent // areas var pixelSize = (scale+0.99) | 0; // getting the data-array containing all the pixel-data // from our source-image var src_canvas = document.createElement('canvas'); src_canvas.width = img.width; src_canvas.height = img.height; var src_ctx = src_canvas.getContext('2d'); src_ctx.drawImage(img, 0, 0); var src_data = src_ctx.getImageData(0, 0, img.width, img.height).data; // setting up the new, scaled image var dst_canvas = document.createElement('canvas'); // just to be sure, that no pixel gets lost, when // we scale the image down, we add 1 and floor the // result dst_canvas.width = (img.width * scale+1) | 0; dst_canvas.height = (img.height * scale+1) | 0; var dst_ctx = dst_canvas.getContext('2d'); // reading each pixel-data from the source // and drawing a scaled version of that pixel // to the new canvas var offset = 0; for (var y = 0; y < img.height; ++y) { for (var x = 0; x < img.width; ++x) { var r = src_data[offset++]; var g = src_data[offset++]; var b = src_data[offset++]; var a = src_data[offset++] / 255; // the alpha value needs to be divided dst_ctx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; dst_ctx.fillRect(x * scale, y * scale, pixelSize, pixelSize); } } return dst_canvas; } function snapValue(value,snap) { var roundedSnap = (value/snap + (value > 0 ? .5 : -.5)) | 0; return roundedSnap * snap; } |
So when the game gets initialized, we scale any bitmap loaded:
1 2 3 4 5 |
self.initializeGame = function() { assets[HERO_IMAGE] = nearestNeighborScale(assets[HERO_IMAGE], scale); assets[PLATFORM_IMAGE] = nearestNeighborScale(assets[PLATFORM_IMAGE], scale); // ... // see source for the rest of initializing part(no changes there) |
And then again, like with ‘Method I’ we have to adjust every scale-relevant value, like velocities, positions EXCEPT for any scaleX or scaleY values, because our nearsetNeighborScale-method did that already. Also, just to be on the save side, we also do the collision-detection adjustments here (see bounds-rounding further up in the article).
Compare both methods side by side
Method I
seamless scaling possible +
image gets distorted on small scale -
image gets blurry on big scale -
Method II
+ no distortion, no blurryness
- needs 0.5x values(0.5,1,1.5,2…)
- only if retro-/pixel-effect is wanted
Download the sources: here.
Agenda
- Part 1: User-Input (Keystrokes, MouseClick, Touch) & Movement
- Part 2: Collisions between objects
- Part 3: Movement&More Collision
- Part 4: Adjustments for mobile devices
- Part 5: Polishing up the game with animations & eye candy
Pingback: Retro Style Platform Runner Game for mobile with EaselJS (Part 3) – adding movement & more collision | indiegamr
Pingback: Retro Style Platform Runner Game for mobile with EaselJS (Part 2) | indiegamr
Pingback: Retro Style Platform Runner Game for mobile with EaselJS (Part 1) | indiegamr
Pingback: Retro Style Platform Runner Game for mobile with EaselJS (Part 5) – Animations and polishing | indiegamr