Originally, this was a post in my blog ( :point: visit it!). But I think here it will be more useful (or destructive in case it is not accurate). I took the liberty of posting it without asking. In case this is not the place or doesn’t meet the minimum requirements, just tell me. I’ll remove it from here, print it on paper and burn it to ashes!
Keep screen aspect ratio with different resolutions using libGDX
There’s something in that “Screen Resolution” game menu that possesses me. I’ve always fancied making a game with different screen resolutions, but the task is far from trivial. These notes are the result of a weekend spent looking for the solution (with help from the JGO community). You can download the source code from here.
Problem
Imagine you are developing a game and start supporting the 480×320 resolution because it fits nice in your smartphone. You align the menus, place the sprites, and do some nasty hacks (that we all have done sometimes) to make your game look pretty. In the end, you have a game that has been developed, literally, for your own phone! (or phone screen resolution). It will look distorted in other phones with different screen resolutions
What do you want to is to support multiple screen resolutions without hardcoding all the layout for every single screen resolution that exists (there are lots of them).
b Solution[/b]
The solution I’ve found it’s not TEH solution, but it works good enough for me.
I’m working with libGDX. This library has a OrthographicCamera class that fits nice for 2D games. This class is responsible to 1) define the volume of the game scene (which in OpenGL argot is called frustum) and 2) to project it orthographically into a plane: the scene image. In addition, libGDX also provides a wrapper to the OpenGL function glViewport(), which transforms the scene image obtained with the camera class into the device screen.
The plan is the following:
- Define a virtual resolution to work with (align menus, place sprites, etc.).
- Set the camera to use the virtual resolution.
- Use glViewport() to adjust our scene image to the physical resolution of the device screen (keeping the aspect ratio of course).
To define the virtual resolution, it is fine to define static final fields in your AplicationListener game class (I’m using libGDX argot). The camera, a Rectangle defining our viewport, and the SpriteBatch, which all of them we will be using later, are also (non-static) fields of the class.
public class MyAwesomeGame implements ApplicationListener
{
private static final int VIRTUAL_WIDTH = 480;
private static final int VIRTUAL_HEIGHT = 320;
private static final float ASPECT_RATIO = (float)VIRTUAL_WIDTH/(float)VIRTUAL_HEIGHT;
private Camera camera;
private Rectangle viewport;
private SpriteBatch sb;
When our game starts, it will first execute the method create() and then resize(int, int) with the width and height of the window as input parameters. In create() we should initialize all the fields required further. In particular, we will initialize the camera and the SpriteBatch (canvas of each frame).
@Override
public void create()
{
sb = new SpriteBatch();
camera = new OrthographicCamera(VIRTUAL_WIDTH, VIRTUAL_HEIGHT);
}
In resize() we should setup the Rectangle that we will be using later to set the viewport. And here it is the trick. Let’s see this function slowly. First we declare and initialize some local variables.
@Override
public void resize(int width, int height)
{
// calculate new viewport
float aspectRatio = (float)width/(float)height;
float scale = 1f;
Vector2 crop = new Vector2(0f, 0f);
They are quite intuitive, for instance, aspectRatio holds the ratio width/height of the device screen (physical resolution), scale is the factor to which scale our scene image, and crop (do not confuse with crap) is the amount of pixels to be cropped from the viewport in order to keep the aspect ratio of the scene image.
Now, if aspectRatio is greater than the virtual aspect ratio it is because the physical resolution is wider (proportionally) than the virtual resolution. Therefore, we should match the height of both resolutions (virtual and physical) and crop in the X direction since our virtual scene image wont fill the whole screen. Conversely, if aspectRatio is lesser than ASPECT_RATIO then we should match the width of both resolutions and crop in the Y direction.
if(aspectRatio > ASPECT_RATIO)
{
scale = (float)height/(float)VIRTUAL_HEIGHT;
crop.x = (width - VIRTUAL_WIDTH*scale)/2f;
}
else if(aspectRatio < ASPECT_RATIO)
{
scale = (float)width/(float)VIRTUAL_WIDTH;
crop.y = (height - VIRTUAL_HEIGHT*scale)/2f;
}
else
{
scale = (float)width/(float)VIRTUAL_WIDTH;
}
float w = (float)VIRTUAL_WIDTH*scale;
float h = (float)VIRTUAL_HEIGHT*scale;
viewport = new Rectangle(crop.x, crop.y, w, h);
}
Finally, we just have to modify the render() method (which is used to render our scene, of course) to update the camera, set the viewport, and draw our objects/entities.
@Override
public void render()
{
// update camera
camera.update();
camera.apply(Gdx.gl10);
// set viewport
Gdx.gl.glViewport((int) viewport.x, (int) viewport.y,
(int) viewport.width, (int) viewport.height);
// clear previous frame
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
// DRAW EVERYTHING
}
And that’s it. Let’s see it in action.
Some images
To illustrate this tips I’m rendering a scene that consists on two rectangles. One green that fills all the scene (just to know where exactly our scene image is), and one square red just to detect visually aspect ratio violations. We use 480×320 as our virtual resolution, as our smartphone uses it natively. Therefore, in our phone we should see everything and without distortion, just as this screenshot I just took:
http://cparcial.files.wordpress.com/2012/02/screenresolution-smarphone.png
Now imagine I send this awesome game to my friend @notch (any similarity with real characters/persons is fictional) which is really rich and has a smartphone with greater resolution. He will see this flawed game:
http://cparcial.files.wordpress.com/2012/02/screenresolution-smartphone2.png
Notice that the square has been distorted into another rectangle (non-squared). My friend is loosing part of the feeling of my game! And most important, the artist that is making such awesome graphics is really pissed off…
Using the method of this tutorial he will just get the right game:
http://cparcial.files.wordpress.com/2012/02/screenresolution-smartphone2fixed.png
Ok, it is true. He’s not using his whole smartphone screen (btw, who told him to spend that much money in a fancy new smartphone in the first place) but at least the aspect ratio is correct and the game graphics artist is happy again.
Further approximation to perfection
I have discovered nothing new, but at least I won’t doubt again how to perform this tedious but mandatory task. You must know that there are, for sure, better approaches to solve the resolution problem. For instance, I just came up with the idea of having two/three versions of the game with different aspects ratios (say 4:3, 16:9, and 16:10). Then, you viewport the layout corresponding to the aspect ratio that is closer to the physical aspect ratio, and hence, minimizing the ugly black bands.
If you have any comment/suggestion/praise/curse, do not hesitate to leave a comment here or say something in Twitter.