Resolution independent games.

According to Wikipedia (List of common resolutions) there are several tens of different resolution sizes around.
When we design and build games, we’d like this resolution hell to be handled in the best and less painful way possible, and there’s two ways of doing this:

  • Scale the Canvas that holds the game.
  • Make your game resolution independent.

Scaling the canvas means to scale the final game generated frame. Which is good when you don’t want to honor retina devices and there’s a direct mapping between a screen point and an in-game unit. For example, your canvas is 600×400 pixels, and your game is 600×400 units.

Resolution independence comes at a higher cost. But on the other hand, perfectly honors Retina-display with HD assets, and allows to define your game for example in meters, and fit the content in your 600×400 pixels canvas. For example this HTML5 game (iBasket html5) defines a 10 by 5 meters game area, which is mapped to whatever the browser window size is. It is retina display enabled, so if you get it from the iTunes store, and play it in an iPad air, you’ll see the difference.
How this is achieved, is the purpose of this post.

Points vs Game units

In modern devices, a pixel is not a point anymore. There’s a devicePixelRatio property that tells the ratio between a point and the corresponding pixels for that point.
What we’d like to do, is exactly the same, but for the content of our canvas. That is, scale the Canvas content, not the Canvas object itself. Pretty much like defining a meta-viewport’s width for our page, we must define before-hand, our game units. Since we want to be retina enabled, for the sake of the example it sounds more reasonable to define our in-game internal units as 2048×1536 than otherwise. This will give us a perfect mapping between our game internals and a retina display. Then, these 2048×1536 units will be mapped to our canvas size, lets say 800×600, 1136×960 or 720×480 pixels, we, simply don’t care.

The desired result would be something like the image below:
retina or not

The left side of the image is drawn with an HD image whereas the right side is not. Both images come from a 4096×4096 and 2048×2048 pixels image. The blurry effect happens as a result of scaling the image.
Two things must be noted about the result:

      Both images are drawn on the same canvas, which is 1024×768 pixels. So both images are stretched.
      The left side image looks sharp.

Internally the game is using 2048×1536 units, which are down-scaled to 1024×768 pixels. Both Images are stretched from their real pixels size, to the virtual internal units size, and then to the screen. Thought It may seem many operations, this happens transparently and w/o performance penalty. But the result is what we expected: full retina support and virtual in-game units.

A bit of (1st grade) maths

How this virtual units to pixel thing works is pretty straightforward.
In the upcoming Cocos2d-html5 v4 preview, you can play with a demo of this in (Cocos2d-html5 Units demo).

Cocos2d-html5 is a scene graph, and as such, Nodes can have other nodes up to an undefined nesting level. The trick for all this units-pixels tradeof is to set up a scale transform in the root hierarchy node. Simply calculate the least scaling value based on the current orientation that honors the Canvas aspect ratio. In our case, we want to fit a 2048×1536 units game into a Canvas of size, lets say 800×600 pixels. Proportionality calculations give us:

// why min ?. Because we want to fit keeping aspect ratio.
// surface is the canvas
// units, is the size of our game units.
var scale= Math.min( 
                 this._surface.width / this._units.width,
                 this._surface.height / this._units.height );
this._unitsMatrix.setScale( scale, scale );

This matrix will up or down scale all Nodes to proportionally be measured in units, and not pixels. How many units are 300 pixels ? Save the scale value for later usage. Is that all ? yup, where are mostly done.

All our calculations have actually been very convenient. Game units and canvas size have the same aspect ratio. But what happens when they don’t ?

Letterboxing

Letterboxing, is when the target and destination aspect ratio don’t match, and thus, some (generally) black bands appear on left-right or top-bottom. There´re two ways to handle this situation.
The most obvious one, is to do nothing, and position the canvas either in the center or the middle of the screen depending on the orientation. In some extreme screen aspect ratios, you can end with a lot of empty (black) space surrounding the canvas. Apart from that, it would be somehow complicated to position Nodes on the letterbox area since it is out of the Scene itself. This would require to manually convert between pixels and units, and that’s exactly what you want to automatically happen.

Cocos2d-html5’s response to this is to resize the Scene objects. (Scene objects are top containers and are considered independent animation units. Since the root node contains scenes, the previously calculated scale operation applies to scenes too). To prevent the letterboxing, the engine calculates an ideal scene size for the current Units and Canvas size as follows:

var d:cc.math.Dimension= new cc.math.Dimension();

// units is a Dimension object with the game units size.
var units= this.getScaleManager()._units;

var ratio= Math.min( window.innerWidth / units.width,
                     window.innerHeight / units.height );

d.width= window.innerWidth / ratio;
d.height= window.innerHeight / ratio;

// now, preferred units is the expected size based on the game units size, and the Canvas size aspect ratio.
this._preferredUnits.set( d.width, d.height );

Scenes will be resized to the preferred Game Units size. The result would be something like the image:
proportions

With this Canvas size, we get that our 2048×1536 game units should actually be 2726×1536 to fit perfectly after applying the root scale operation, pretty far from our original expected dimension, and worst of all, our 4/3 aspect ratio game would have huge letterboxes around it unless we set the expected dimension to 2726×1536.

The trick, consists in keeping all the game in a single container of original game units size (2048×1536), and center it in the scene after setting it to the preferred units size for the choice of Canvas size. Now you can safely position elements out of the original game units, like the ‘connect to facebook’ button, the game logo, etc.
It is very reasonable to clear the scene background with an stretched image that covers the whole area, and have some art instead of letterbox black bands.

Almost there…

Calculating the scale, and fitting the content in the scene is easy. 1st grade maths to the rescue. There’s a final thing to note, and is how we should call the CanvasRenderingContext2D drawImage function.
In its most basic form, we would call: ctx.drawImage( image, x, y );
But, this won’t get the expected result. Remember there’s a scale operation on the root node which propagates down all across the scene graph nodes. In fact, that draw call will be converted to: ctx.drawImage( image, x,y, image.width, image.height ). Image size is in pixels. And our game is in virtual units. 2048×1536 in the example. So we simply can’t call drawImage with x and y parameters. We must specify, what to draw in Game Units, not Image pixels. So if in our game, the Sprite took 200×200 units, a call of the form:

ctx.drawImage( image, x, y, 200, 200 ) must be performed, in oppsotion to ctx.drawImage( image, x, y ) which will give wrong results.

Final considerations:

  • Orientation. Device orientation change will not only bring a resolution change, which is not just inverted. You also will for sure need to relayout all your in-game elements.
  • Screen resize. In desktops, a browser window resize operation will fire resize events that must be tracked.
  • Physics engines, like Box2D, work in meters. With this method, you can safely define your game units to be 10×7 meters, and let the magic calculations happen for you.
  • This result can be also achieved by building a Canvas object of the size of the Units, and scaling it to fit the screen with css attributes applied to the canvas. The outcome will be, for our example, a 2048×1536 canvas, where probably there will be not enough Javascript power for such a gigantic Canvas.

Cocos2d-html5 v4 API


// Calculate the pixel-units scale matrix.
// units size and canvas size.
renderer.setScaleContent( UW, UH, W, H );

// when the canvas is resized, make the canvas take over the whole screen, 
// honor aspect ratio, calculate preferred units size for the Canvas size,
// and, in this case, make the Scenes be the preferred units size.
renderer.adjustContentToFullScreen( 
    cc.render.ScaleContentSceneHint.STRETCH );

// if the orientation changes, resolution is not just inverted.
// recalculate all info regarding scale content scale.
renderer.forceOrientation(
    cc.render.OrientationStrategy.BOTH,
    function orientationValid( orientation ) {
        if ( orientation===cc.render.OrientationStrategy.LANDSCAPE ) {
            renderer.setScaleContent(UW, UH, W, H);
        } else {
            renderer.setScaleContent(UH, UW, H, W);
        }
    },
    function orientationInvalid() {
    });

At this point, our game is totally resolution independent. Will gracefully up/down scale content and honor retina display graphics regardless the html viewport.

Leave a comment