Java-Gaming.org
Java4K - to go         Javadoc:
Featured games (67)
games approved by the League of Dukes
Games in Showcase (∞)
games submitted by our members



News: Read the Java Gaming Resources, peek at the official java tutorials or join us at irc #jgo.
 
    Home     Help   Search   Login   Register   
Pages: [1]
  Print  
  Keep screen aspect ratio with different resolutions using libGDX  (Read 169 times)
0 Members and 1 Guest are viewing this topic.
Offline yuma

JGO n00b
*

Posts: 40
Medals: 1


Monkeys are listening


« on: 2012-02-05 17:03:58 »

Originally, this was a post in my blog ( Pointing 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).

(My) Solution

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.

1  
2  
3  
4  
5  
6  
7  
8  
9  
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).

1  
2  
3  
4  
5  
6  
    @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.

1  
2  
3  
4  
5  
6  
7  
    @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.

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
        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.

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
    @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:


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:


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:


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.

Offline Nate

JGO Neuromancer
****

Posts: 1033
Medals: 26


mooooo


« Reply #1 on: 2012-02-06 11:40:27 »

You can simplify your code using this:
1  
2  
3  
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.

Offline yuma

JGO n00b
*

Posts: 40
Medals: 1


Monkeys are listening


« Reply #2 on: 2012-02-06 14:32:03 »

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
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
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  Lips Sealed

Sometime this night I'll modify the "tutorial" to include your comments. Thanks again!

Pages: [1]
  Print  
 
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.16 | SMF © 2011, Simple Machines Valid XHTML 1.0! Valid CSS!
Page created in 0.163 seconds with 23 queries.