The game development landscape has changed significantly over the last few years (understatement of the century, but whatever). One of the most significant of these changes , especially for game developers - though it’s one that often goes unnoticed - is the rise of screen resolution and aspect ratio fragmentation.
Go back a few years…let’s call it 2010, shall we? The Nintendo DS was on the way out and the iPhone 4 was, sadly, the current pinnacle of handheld gaming. TV screens and monitors had a resolution that varied a bit (though largely stuck at the awfully termed “high definition”, but were mostly either 16:9 or 4:3. Android was kicking about, but wasn’t really being taken seriously as a viable gaming platform just yet, and there weren’t an overwhelmingly huge number of screen sizes out there.
In short - game developers, by and large, knew what screen size they were targeting when doing all the graphics-y bits of the game. Unless you’re currently one of the indie developers heavily immersed in 3DS development (hint: you’re probably not), this is something that really does need to be taken into consideration. And it’s not just Android now (yeah, I heard you, jeering Apple fanboys) - there are now several different screen resolutions of iPhone and iPad, as well as a couple of different aspect ratios.
One solution would be to make a very slightly different version of the game for each viable screen ratio and just scale the resolution. Does this sound elegant? Didn’t think so. Ideally, we want to write one game that will look at least acceptable on even the strangest screen ratios and resolutions - we can probably stipulate “it’s landscape”, but not a huge amount more. We’re going to run into a few issues we’ll need to think about:
- What happens if the screen is really small but with a really high resolution, or the opposite?
- How do we handle a change in aspect ratio - do we stretch the viewport or cut bits off/add bits on?
- What happens if we’re using something like a physics engine and need to translate coordinates?
(I’m thinking mainly about 2D games here. 3D devs, you do your thing, you gods among men.)
For me, one of the best things to change in the ol’ mind-brain is to stop thinking in terms of pixels!
Okay, so that might be a bit harsh - your artists will be drawing in terms of pixels, textures and the like are probably measured in pixels, and you will need pixels when working out how to render those bloody fonts. In terms of the placement and size of your in-game entities, though, it’s a unit of measurement you should stay well clear of.
I’m going to outline one solution I used recently, focusing mainly on those last two bullet points. I’m using LibGDX (which is brilliant) as my graphics library, and the excellent Box2D for physics, but with a bit of luck this should be at least slightly helpful right across the (2D, as always) board. Bear in mind the usual disclaimer: this isn’t the solution, nor is it necessarily the best solution, but it’s one I found easy to implement, and, well, it works for me. Cool? Cool.
One crucial issue I ran into using these two libraries in tandem is that LibGDX likes to draw in terms of pixels, as it wraps OpenGL. Box2D, being a physics library, relies on real-world measurements like metres and kilograms; yards and pounds don’t get a look-in. Praise be to Erin Catto.
So - we don’t want to think in terms of pixels, so what should we use instead? Well, the metric units are right there thanks to Box2D - it makes sense to use those. This means your in-game entities will define their dimensions and positions in terms of metres. You’ll also define the size of the viewport itself in metres.
I’ve decided that I prefer the approach of resizing the viewport in an intelligent way, as opposed to stretching it. This is at the cost of losing a little bit of screen, but this can be adjusted afterwards anyway, by moving the camera. If you prefer the stretching approach, hopefully the differences in implementation should be easy enough to work out.
Our first step is to pick a default resolution. This isn’t hugely important, but provides a baseline we can do our scaling from. I primarily work with Android, so I reckon 1280*720 is a good initial target - it’s used by quite a few popular phones. You’ll probably want to choose your own - just pick whatever you think best fits your use case.
Next up, we’ve got to pick a default pixel-to-unit ratio, letting us define how big objects will appear on the screen, as well as the size of the viewport in our chosen units (metres, in my case).
I’ve been tempted in the past to use a scaling of 1px/m, just to save time and so that there’s a one-to-one mapping. Don’t do this. Imagine you have a square block, 400px to a side. That’ll look fine on your screen, not taking up a huge amount of room - but to your physics engine, that’s 160 square kilometres to deal with. You’re moving around an object the size of Liechtenstein. Your viewport is therefore going to be comparatively enormous just to fit it on screen, and because of how the orthographic perspective works, it’ll assume it’s very far away - meaning a lot of your physical simulations, particularly those involving gravity, are going to appear very slow indeed. Unless you scale all your forces up to compensate, but surely that’s more work?
I’ve selected 128px/m - it leaves me with a neat 10m width viewport, perfect for the sort of game I’m making, and is also a power of two, meaning it might be helpful with memory management of textures - depending on implementation and OpenGL version. Now that we’ve got this, we can build a little ‘Scaling’ class to handle the translation for us. Here’s mine.
public class Scaling {
public static final int TARGET_WIDTH = 1280;
public static final int TARGET_HEIGHT = 720;
public static float PIXELS_PER_METER = 128;
public static final int DEFAULT_TILE_WIDTH = 64;
public static float pixelsToMeters(float pixels) {
return pixels / PIXELS_PER_METER;
}
public static float metersToPixels(float meters) {
return meters * PIXELS_PER_METER;
}
public static float getInitialWidth() {
return pixelsToMeters(TARGET_WIDTH); //The initial width in metres.
}
public static float getInitialHeight() {
return pixelsToMeters(TARGET_HEIGHT); //As above, for height.
}
}
You might not need all these methods. Note that the three integers declared at the top are marked final, but the float PIXELS_PER_METER is not - that’s because we’ll be modifying this as our viewport changes size.
Next, we need to define what happens when the screen resizes. LibGDX has a method for this that you can override in the appropriate place. Mine looks like this (I’m also calling it as part of my initial setup).
@Override
public void resize(int width, int height) {
if (height < width) {
Scaling.PIXELS_PER_METER = width / w;
h = height / Scaling.PIXELS_PER_METER;
} else {
Scaling.PIXELS_PER_METER = height / h;
w = width / Scaling.PIXELS_PER_METER;
}
camera.setToOrtho(false, w, h);
camera.update();
}
The variables h and w are fields inside my Screen object - they keep track of the size of the viewport, in metres in this case. There’s not much point keeping track of the size in pixels, as we can just do Gdx.graphics.getWidth(), and so on. Here’s what this code’s doing:
- If the new height is less than the new width (i.e., it’s a landscape aspect ratio):
- Set the new px/m ratio to be the new width in pixels, divided by the current width in metres.
- Scale the height according to the new scaling ratio.
- Our width in metres remains the same!
- Note: if you’re stretching the viewport instead, you’ll want a different px/m ratio for each axis.
- If the screen has a portrait aspect ratio, do the opposite.
- Set our viewport, camera, to have the new dimensions.
One of our screen dimensions in metres will stay the same - this means objects stay a reasonable size on the screen, and making the viewport larger doesn’t simply equal ‘zooming out’. Note that we set our camera’s dimensions using metres rather than pixels - this is fine! LibGDX doesn’t care, it’ll draw everything at the appropriate size on screen, provided the dimensions you give to your rendering and drawing methods - SpriteBatch and the like - are in metres too.
Now that we’ve done this, we should be able to resize our windows so that our game is still playable(ish), even at frankly silly aspect ratios.
{: .center-image}
Cool, huh? Hopefully it should be obvious enough how to adjust this effect. You may have noticed that the drawing isn’t quite pixel-perfect, particularly on the ‘Dino-Bar’, due to the scaling. A fix for this would be to have several different sizes of asset available, and pick the closest one according to what size it’s being drawn on-screen - we could use the Scaling method metersToPixels for this.
I someone finds this article useful - if you have any questions, please don’t hesitate to leave a comment below. Ta ta for now - and remember, pixels are the devil.