Keep screen aspect ratio with different resolutions using libGDX

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.

You can simplify your code using this:


import com.badlogic.gdx.utils.Scaling;
...
crop.set(Scaling.fit.apply(VIRTUAL_WIDTH, VIRTUAL_HEIGHT, width, height));

For some apps I do similar and scale, but for many I use camera coordinates that map to screen pixels and layouts that work with all resolutions, eg by specifying positions in percentages or in pixels relative to a side of the screen. Usually I do this using scene2d and Table.

Thanks for the Scaling tip. I didn’t know about it. Right now it’s only on nighties of libGDX and I’m using 0.9.2, but I think I’ll update soon.

Also, notice that crop is a vector to center the game scene in the device screen. The variable name is not helping in this case, sorry. Looking into the Scaling enum code I think the correct way to use it is to

import com.badlogic.gdx.utils.Scaling;
...
Vector2 newVirtualRes= new Vector2(0f, 0f);
Vector2 crop = new Vector2(width, height);

// get new screen size conserving the aspect ratio
newVirtualRes.set(Scaling.fit.apply((float)VIRTUAL_WIDTH, (float)VIRTUAL_HEIGHT, (float)width, (float)height));

// ensure our scene is centered in screen
crop.sub(newVirtualRes);
crop.mul(.5f);

// build the viewport for further application
viewport = new Rectangle(crop.x, crop.y, newVirtualRes.x, newVirtualRes.y);

(I haven’t actually tried that code, but it should work).

I have to check scene2D out. It’s the second time I’ve seen it mentioned regarding this topic (I think both times were you) and I haven’t test it yet :-X

Sometime this night I’ll modify the “tutorial” to include your comments. Thanks again!

Correct me if I’m wrong (I have never seen iPhone nor Android device in my life, just evaluating the data of visitors to my websites) but the resolution of iPhone is always 320x480 and for Android the minimum is 320x458. That’s just 22 pixels difference, so stretched won’t look as bad as in your example. Not sure how it works with other resolutions for Android, but if there is a way to “downgrade” all Android to 320x480, I would try simply designing it for 320x480 then force the phone to select the most similar resolution and display it stretched.

You argument can be turned against yourself. If 22 px is small, then why not just put some 11px black bars to the sides? Is not a big deal and allows preserving the aspect ratio.

I wanted to preserve stretch (no resizes) and aspect ratio in the beginning. But this meant to 1) force all devices to a screen resolution, or 2) make a configuration for every screen resolution. I didn’t want either of those solutions, so I picked to loose my restrictions. Now I want to preserve aspect ratio, but allow a symmetric stretch (same stretch in x and y coordinates).

Though the stretch is bad (it could make your fonts look bad, for instance) at least is symmetric, which allows your “squares” to be kept as squares.

iPhone is 320x480 and 640x960. Android can be almost any resolution and aspect ratio, 320x480, 480x854, 720x1280, etc.

Top 10 Android resolutions (my data, not necessarily representative; the percentage are for total visitors (mostly PC), so tread these only as relative):

  1. 320x458 Android 578 2.39%
  2. 821x1274 Android 198 0.82%
  3. 947x411 Android 95 0.39%
  4. 821x1029 Android 60 0.25%
  5. 821x345 Android 60 0.25%
  6. 800x336 Android 53 0.22%
  7. 821x346 Android 45 0.19%
  8. 947x397 Android 44 0.18%
  9. 821x1160 Android 40 0.17%
  10. 821x344 Android 33 0.14%
    (also, among 343 combinations of resolution/OS there was no instance of Android with 320x480 (it seems that only Apple products use it).

As for iPhone I have not even one visitor with 640x960, all of them were on 320x480.

I would use this strategy: Design it for 320x458 since the most Android devices use it, for the most popular iPhone (320x480) display it as non stretched (leave 22 pixels blank). That would be the 2 special cases, for the rest do symmetric stretch (with normal stretch it would get ugly on 821x345 and similar, so it is not an option).

None of those resolutions are full screen, I assume, since no devices have any of those resolutions. 320x480 stretched to 720x1280 or 540x960 is going to look terrible. Stretching is not a high quality way to solve the resolution problem.

If you data is really from < 1000 samples, it isn’t representative. 480x800 is the most popular Android resolution.