post1

Multiple resolutions problems

This is an old post from our website but since we transferred to new web technology we could not save it so we need to re publish it

Multiple resolutions problems

Developing for android devices is not an easy task. There is a variety of problems that need to be solved, and one of the most common ones is dealing with multiple screen sizes on different devices, which means different resolutions and aspect ratios.

In this blog post we want to share our solution to the problem when making our upcoming game for Android (for which we, by the way, still don’t have a name… who would tell that thinking of a name is more difficult than developing a whole game). Note that our solution is not THE solution. There are other ways to handle this problem that work just fine, so if you are reading this trying to solve the problem, we highly suggest searching for other solutions before choosing one.

In the next sections we are going to show some common ways of handling multiple screen problems, and then our way together with a libGDX code example. So without further ado, let’s get started!

Stretching the screen

The easiest way to handle this problem is to simply stretch (or squash) the content of your screen until it perfectly fits the device screen.

stretch
The effects of stretching can be seen on this red square.

Now, this method is quick and dirty… and it is visually unacceptable (I guess there are some games that can go away with it, ours is not one of them)

Black Bars

Another solution is to keep the aspect ratio and fit the content on screen by padding out the dead space on screen. This keeps the aspect ratio of the content but it creates black bars.

Black Bars
The square kept its aspect ratio, but now we have black bars on the sides of the divice.

These black bars can be filled with custom made borders, but in some extreme cases it can look bad or it can feel like the game was not made for that device.

Our solution – Bleeding

The solution we went for is to create a bleed area of content and adapting what is shown on the device. First we define a virtual resolution for our game, in our case that is 480×320. Once we set the virtual resolution we will be using, we will set our orthogonal camera in such a way to match the aspect ratio of the device while keeping at least height or width of our virtual resolution.

bleed final
With bleed you can see that the square kept its aspect ratio, and the playing field is also visible (green). The yellow area is the bleed area (it will be used for graphics, but never for gameplay)

Next we look at the aspect ratio range we want to support, and after some searching the web we find these two resolutions that stand at the opposite sides of the aspect ratio spectrum, 1280×800 and 768×1280. So the aspect ratio varies from 1.6 to 0.6, so to create a bleed area that will cover everything we need to make it 820×820 (this number might change as more devices come out).

bleed whole
The whole playing field looks something like this. The yellow bleed area surrounds the green playing field

So let’s look at how this works on an example. Let’s say that our target device has 800×600 resolution, that is an aspect ratio of 1.333. Our virtual resolution is set to 480×320, so the aspect ratio is 1.5. To make everything fit nicely on screen we need to create a camera that has the aspect ratio of 1.333, but it’s height and width don’t get smaller than 480 and 320 respectively. To achieve this we see that we have to create a camera with the resolution of 480×360. Once created this camera will show the bleed area so it keeps the aspect ratio of our virtual resolution, and scale the rest of the scene so it matches the screen

bleed keeping the aspect ratio
Here we can see how the camera is set up (black rectangle). The camera has the same aspect ratio as the device so it captures a portion of the bleed area we set up.

The main problem with this method is that we use a fixed virtual resolution, so if a device has much higher resolution than our virtual one the game might look blurry. One way to deal with this is to set up multiple virtual resolutions and chose the one best fitted for the situation, but that is out of scope of this post

Code:

When creating the camera for our scene, we want to create it in such a way so it keeps the aspect ratio of the device. So in our render method we create/set the camera like this.

public class GameRenderer {
 
    private OrthographicCamera cam;
    private ShapeRenderer shapeRenderer;
    private SpriteBatch batcher;   
 
    private int virtualH = 320;
    private int virtualW = 480;
    private int realH;
    private int realW;
...

Alright, so first we declare Orthographic camera, shape renderer and sprite batch, and we define our virtual height and width. Nothing special here.

public GameRenderer(GameWorld world)
{
    cam = new OrthographicCamera();
    shapeRenderer = new ShapeRenderer();
    batcher = new SpriteBatch();
         
    int realH = Gdx.graphics.getHeight();
    int realW = Gdx.graphics.getWidth();
     
    resize(realW, realH);
     
    shapeRenderer.setProjectionMatrix(cam.combined);
    batcher.setProjectionMatrix(cam.combined); 
}

Next we want to create a new Orthographic camera, shape renderer and sprite batch inside the class constructor and get the real width and height of the device screen. Once we have that we call the resize method that will do the magic for us. After the resize method sets the camera to the correct aspect ratio we also need to set the same projection matrix to ShapeRenderer and SpriteBatch. Both ShapeRenderer and SpriteBatch use their own transformation matrices for rendering on screen, so in order for everything to have the same coordinate system we need to make their projection matrices same as the camera one (otherwise 0,0 of the camera and 0,0 of the sprite batch would not point at the same space on the device screen).

public void resize (int width, int height)
{
    float aspectRatio = (float) width / (float) height;
    if(aspectRatio >= (float) virtualW / (float) virtualH){
        cam.setToOrtho(true, virtualH * aspectRatio, virtualH );
        int transX = Math.abs((int)(((float)virtualW - (float)virtualH * aspectRatio) / 2f)) ;
        cam.translate( -transX , 0 );
        cam.update();
    }
    else{
        cam.setToOrtho(true,virtualW , virtualW * aspectRatio);
        int transY = Math.abs((int)(((float)virtualH - (float)virtualW * aspectRatio) / 2f)) ;
        cam.translate( 0 , -transY );
        cam.update();
    }
         
}

Here is where the magic happens, and it is the most simple vanilla solution :). First we need to find the real aspect ratio of the device, as we passed the real screen size as parameters we just divide the two and get our aspectRatio. Now we need to check if the aspectRatio is greater or lesser than our virtual aspect ratio that we get by dividing the virtual width with virtual height. Here two things can happen. One the aspect ratio is 1.5 or greater, that means that we need to increase our width and keep our height, so we set our camera accordingly (note that we use the true value when we set our camera, so the y axis becomes inverted). Just setting the camera like this will create a bleed area just to the right, we want our camera centered so we need to move it to the left. The amount we have to move the camera is half of the difference between the virtual width and the new width we just calculated. Once we translated the camera we ensure that the bleed area to the left and right will be the same and that our play area will remain centered on the middle of the screen. In case when the aspect ratio is less than 1.5 we do a similar thing, but keep the width, ajust the height and translate the camera on the y axis.

public void render()
{
    cam.update();
       cam.apply(Gdx.gl10);
       batcher.getProjectionMatrix().set(cam.combined);
       shapeRenderer.setProjectionMatrix(cam.combined);
         
    Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
 
       batcher.begin();
         
       //ADD DRAW CALLS HERE
 
       batcher.end();
}

Finaly in our render method we update the camera, reset the projection matrices (just in case we meddle with our camera somewhere else in code), and call our draw methods. With this setup the playable area will always be rendered between 0,0 and 320,480 in the virtual space (which can take the whole real screen, or just a portion depending on the bleed)

Final comments:

-Usualy the GUI uses different space from game elements, so when drawing GUI elements we use a separate projection matrix that has 0,0 at the device top left corner and 1,1 at the device bottom right corner

-If you chose to invert the y axis of the camera you will also have to invert the sprites if you want to use SpriteBatch to draw them. SpriteBatch has y axis pointing upwards and has 0,0 at the bottom left edge so giving it the projection matrix of the camera will invert its axis and sprites together with it. Nothing too scary though as you can always just use flip method on your loaded texture regions

spriteExample.flip(false, true);
It's only fair to share...
Share on Reddit0Digg thisShare on Facebook0Share on Google+0Share on LinkedIn0Pin on Pinterest0Share on StumbleUpon0Share on TumblrTweet about this on Twitter0Buffer this page
Posted in LibGDX, Tutorials.

2 Comments

  1. When you talk about the black bars you mention that “black bars can be filled with custom made borders.” How can I achieve that? Can you give a code example? I would really appreciate it.

    Thanks

    • Here is one way to do it. Do everything same as with bleeding (difference being that you don’t need the bleed area for your background images as now you will put custom borders in)
      Now you need to find the points where the borders begin and end (I will assume that the device is wider than the virtual aspect we use).
      Those points should be 0,0 and virtualH,0. They are both transX wide (meaning there is transX space between the edge of the screen to the beginning of the playable area)

      with this you have all the info needed to make the custom borders placement. For the right you place it at virtualH,0, and for the left you place it on 0-borderSpriteWidth,0
      (so the end of the border aligns with the beginning of the playing field)
      You draw these borders as any other sprite in your game, just draw them last so they overlap everything else.

      For border images you could either use a big image that won’t fit on screen or try to resize it when drawn based on transX
      Hope it helped a bit :3

Comments are closed.