LibGDX Tutorial - Implement resolution and aspect ratio independence, viewports, camera (Understanding 2)
It is time to sum up everything we have done until now to achieve resolution and aspect ratio independence. It is essential that you read the Common Problems 1 and Common Problems 2 tutorials to understand the logic behind our actions. We have already used libGDX's camera and viewports, but lets go one step further.
" So, what have we done until now? "
Achieving aspect ratio independence
As a base aspect ratio we used 16:9 and that's exactly the reason why our camera's dimensions are;
- 1280 x 720 for the splash screen and the menu (the division width/height equals 16:9).
- 16 x 9 for the game world.
With the usage of the camera we basically define a virtual resolution. I always prefer to use the so called banana units for the user interfaces (close to the real resolution of my base device in which i debug my games) and small units for my game world (a resolution that gives the same aspect ratio as the aspect ratio i want to choose as base for my game world).
" Why did you choose a 16:9 aspect ratio? "
First of all, devices with an aspect ratio of 16:9 are the most used devices. The first time i had to make this decision for Black Dodger, i also considered the fact that i will only have to deal with devices with lower aspect ratios, because back then there were no devices with 18:9 aspect ratio.
It seems that devices with aspect ratio 18:9 or more (for example there are some devices with an aspect ratio of 18.5:9 and some with a resolution of 2436 x 1125, the division of which is bigger than an 18.5:9 aspect ratio) are here to stay. Even though i previously said that if those devices would become a trend i would definitely choose their aspect ratio as a base for my game worlds, i reconsidered.
" Why? "
To answer the above question lets think what viewports we have used until now.
" Why? "
To answer the above question lets think what viewports we have used until now.
- Fit viewport for the splash screen and the menu screen (black bars technique, maintain the aspect ratio).
- Extend viewport for the game world (show more game world to the player, maintain the aspect ratio).
I always tend to not choose a fit viewport because black bars are ugly, but this time we could afford it because the backgrounds for both the splash and the menu screens were black.
The extend viewport is cool because we just show a little bit more of the game world to the player. If we always choose the biggest possible aspect ratio, then we will only need to expand the world to one direction and this is ultra cool. Having to always manage 1 direction instead of 2 seems better.
But, there is a trade of! Suppose that we use the biggest possible aspect ratio as a base for our game world. The bigger the difference is between our base aspect ratio and the aspect ratio of the device that will run the game, the more space the black bars will take or the more game world we will need to show. And this is not good at all. Imagine a screen with big black bars, or with much useless game world, no, it definitely is not good.
With all that stuff being said, i still stick to a 16:9 base aspect ratio because it is somewhere in the middle between the smallest and the biggest aspect ratios.
So, for the splash and the menu screens we are perfectly ok. If a device has a smaller aspect ratio than 16:9, then black bars will be added on top and bottom of the screen and if it has a bigger aspect ratio then the black bars will be added at the right and left sides of the screen.
Nothing special here. We just modified a little bit the original extend viewport for our needs.
Then, open the " GameStage " and the instead of calling the extend viewport, we will call our own viewport.
Below, you can see 2 examples of how the game would like like if we used the extend viewport and how it looks like using our own modified extend viewport.
Finally, lets explain the reasons behind the choices of the position and the size of the background. We need to define the maximum extra world on top and at the sides that we are going to show to our player.
-We have chosen a virtual resolution of 16 x 9, thus a 16:9 aspect ratio.
-The smallest aspect ratio is 4:3 ≃ 1.333. Let's say a device with a resolution 400 x 300
400 ÷ 16 = 25
9 x 25 = 225
(300 - 225) ÷ 25 = 3
So we will show the most, 3 more units at the top of the screen.
-The biggest aspect ratio is 2436:1125 ≃ 2.1653. Let's say a device with a resolution 2436 x 1125.
1125 ÷ 9 = 125
16 x 125 = 2000
(2436 - 2000) ÷ 125 = 3.488 ≃ 3.5
So we will show the most, 1.75 more units at the left side and 1.75 more units at the right side of the screen.
That's why the size of our background is defined to be 16 + 3.5 = 19 width and 9 + 3 = 12 height.
We also place the background at the position (-1.75, 0) because this 1.75 left side of the background is useless and supposed to be seen only by devices with bigger aspect ratios.
" But how big we must make the actual image? "
Problem
There is a major problem for our game world though. Suppose that 2 devices, the first one with a smaller aspect ratio than 16:9 and the second with a bigger, run our game. You also must know, that extend viewport always expands the world to 1 direction, on top if the aspect ratio of the device that runs our game is smaller than our base and at the right side if the aspect ratio of the device that runs our game is bigger than our base.- In the first case we are perfectly ok, we will just show some useless game world on the top of our screen, no harm at all.
- In the second case though, if we show extra game world at the right side then everything will be messed up. The hero wont start from the centre of the screen, we also will not be able to go to the right side of the screen because there will be an invisible wall there!
We could try to fix that by changing the position of the right enemies, of the right wall and of our hero position but then we have a completely new game world, totally unbalanced. We would also have to change the speeds if we wanted to make the world less unbalanced but even then the result would be disappointing.
For that reason we will create our own viewport, which basically is a copy paste of the source code of the extend viewport, with the only difference being the fact that for bigger aspect ratios than our base one, the world will expand equally both at the left and the right sides. That way, we could draw a background with walls or something (in our case i just drew extra world, it would be much better if you drew walls though) and make the game look better and be 100% balanced. Create a " viewport " package and inside create a class with the name " MyViewport ".
package com.getest.game.viewport;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.OrthographicCamera
;import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Scaling;
import com.badlogic.gdx.utils.viewport.Viewport;
public class MyViewport extends Viewport {
private float minWorldWidth, minWorldHeight;
private float maxWorldWidth, maxWorldHeight;
private float WIDTH, HEIGHT, AR, V_WIDTH, V_HEIGHT, V_AR;
public MyViewport (float minWorldWidth, float minWorldHeight) {
this(minWorldWidth, minWorldHeight, 0, 0, new OrthographicCamera());
}
public MyViewport (float minWorldWidth, float minWorldHeight, Camera camera) {
this(minWorldWidth, minWorldHeight, 0, 0, camera);
}
public MyViewport (float minWorldWidth, float minWorldHeight, float maxWorldWidth, float maxWorldHeight) {
this(minWorldWidth, minWorldHeight, maxWorldWidth, maxWorldHeight, new OrthographicCamera());
}
public MyViewport (float minWorldWidth, float minWorldHeight, float maxWorldWidth, float maxWorldHeight, Camera camera) {
this.minWorldWidth = minWorldWidth;
this.minWorldHeight = minWorldHeight;
this.maxWorldWidth = maxWorldWidth;
this.maxWorldHeight = maxWorldHeight;
setCamera(camera);
}
@Override
public void update (int screenWidth, int screenHeight, boolean centerCamera) {
// Fit min size to the screen.
float worldWidth = minWorldWidth;
float worldHeight = minWorldHeight;
WIDTH = Gdx.graphics.getWidth();
HEIGHT = Gdx.graphics.getHeight(); AR = WIDTH/HEIGHT;
V_WIDTH = 16;
V_HEIGHT = 9;
V_AR = V_WIDTH/V_HEIGHT;
Vector2 scaled = Scaling.fit.apply(worldWidth, worldHeight, screenWidth, screenHeight);
// Extend in the short direction.
int viewportWidth = Math.round(scaled.x);
int viewportHeight = Math.round(scaled.y);
if (viewportWidth < screenWidth) {
float toViewportSpace = viewportHeight / worldHeight;
float toWorldSpace = worldHeight / viewportHeight;
float lengthen = (screenWidth - viewportWidth) * toWorldSpace;
if (maxWorldWidth > 0) lengthen = Math.min(lengthen, maxWorldWidth - minWorldWidth);
worldWidth += lengthen;
viewportWidth += Math.round(lengthen * toViewportSpace);
} else if (viewportHeight < screenHeight) {
float toViewportSpace = viewportWidth / worldWidth;
float toWorldSpace = worldWidth / viewportWidth;
float lengthen = (screenHeight - viewportHeight ) * toWorldSpace;
if (maxWorldHeight > 0) lengthen = Math.min(lengthen, maxWorldHeight - minWorldHeight);
worldHeight += lengthen;
viewportHeight += Math.round(lengthen * toViewportSpace);
}
setWorldSize(worldWidth, worldHeight);
setScreenBounds((screenWidth - viewportWidth) / 2, (screenHeight - viewportHeight) / 2, viewportWidth, viewportHeight);
if( V_AR < AR)
apply(false);
else
apply(centerCamera);
}
public float getMinWorldWidth () {
return minWorldWidth;
}
public void setMinWorldWidth (float minWorldWidth) {
this.minWorldWidth = minWorldWidth;
}
public float getMinWorldHeight () {
return minWorldHeight;
}
public void setMinWorldHeight (float minWorldHeight) {
this.minWorldHeight = minWorldHeight;
}
public float getMaxWorldWidth () {
return maxWorldWidth;
}
public void setMaxWorldWidth (float maxWorldWidth) {
this.maxWorldWidth = maxWorldWidth; }
public float getMaxWorldHeight () {
return maxWorldHeight;
}
public void setMaxWorldHeight (float maxWorldHeight) {
this.maxWorldHeight = maxWorldHeight;
}
}
Nothing special here. We just modified a little bit the original extend viewport for our needs.
Then, open the " GameStage " and the instead of calling the extend viewport, we will call our own viewport.
package com.getest.game.stages; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.physics.box2d.Body; import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer; import com.badlogic.gdx.physics.box2d.Contact; import com.badlogic.gdx.physics.box2d.ContactImpulse; import com.badlogic.gdx.physics.box2d.ContactListener; import com.badlogic.gdx.physics.box2d.Manifold; import com.badlogic.gdx.physics.box2d.World; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.ui.Image; import com.badlogic.gdx.scenes.scene2d.ui.ImageButton;i import com.badlogic.gdx.scenes.scene2d.ui.Skin; import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Timer; import com.badlogic.gdx.utils.viewport.ExtendViewport; import com.getest.game.actors.EnemyActor; import com.getest.game.actors.GroundActor; import com.getest.game.actors.HeroActor; import com.getest.game.actors.LeftWallActor; import com.getest.game.actors.RightWallActor; import com.getest.game.camera.AndroidCamera; import com.getest.game.misc.BodyMisc; import com.getest.game.misc.WorldMisc;
public class GameStage extends Stage implements ContactListener{ private float accumulator, TIME_STEP; private Box2DDebugRenderer renderer; private WorldMisc wrl; private World world; private GroundActor ground; private LeftWallActor leftWall; private RightWallActor rightWall; private HeroActor hero; private Boolean right = false, left = false; private Skin leftButtonSkin, rightButtonSkin, jumpButtonSkin, slideButtonSkin; private ImageButton leftButton, rightButton, jumpButton, slideButton; private Texture backGroundTexture; private Image backGroundImage; private Timer timerEnemy = new Timer(); public GameStage(){ super(new MyViewport(16f, 9f, new AndroidCamera(16f, 9f))); accumulator = 0.0F; TIME_STEP = 1/300F; // Recomended by libgdx setupWorld(); } private void setupWorld() { wrl = new WorldMisc(); world = wrl.createWorld(); renderer = new Box2DDebugRenderer(); Gdx.input.setInputProcessor(this); world.setContactListener(this); setupBackGround(); setupGround(); setupLeftWall(); setupRightWall(); setupHero(); setupButtons(); createEnemy(); } private void setupBackGround(){ backGroundTexture = new Texture("inGame/backGround.png"); backGroundTexture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear); backGroundImage = new Image(backGroundTexture); backGroundImage.setSize(19.5F, 12F); backGroundImage.setPosition(-1.75F,0); addActor(backGroundImage); } }
Below, you can see 2 examples of how the game would like like if we used the extend viewport and how it looks like using our own modified extend viewport.
Game with extend viewport |
Game with our modification of extend viewport |
Finally, lets explain the reasons behind the choices of the position and the size of the background. We need to define the maximum extra world on top and at the sides that we are going to show to our player.
-We have chosen a virtual resolution of 16 x 9, thus a 16:9 aspect ratio.
-The smallest aspect ratio is 4:3 ≃ 1.333. Let's say a device with a resolution 400 x 300
400 ÷ 16 = 25
9 x 25 = 225
(300 - 225) ÷ 25 = 3
So we will show the most, 3 more units at the top of the screen.
-The biggest aspect ratio is 2436:1125 ≃ 2.1653. Let's say a device with a resolution 2436 x 1125.
1125 ÷ 9 = 125
16 x 125 = 2000
(2436 - 2000) ÷ 125 = 3.488 ≃ 3.5
So we will show the most, 1.75 more units at the left side and 1.75 more units at the right side of the screen.
That's why the size of our background is defined to be 16 + 3.5 = 19 width and 9 + 3 = 12 height.
We also place the background at the position (-1.75, 0) because this 1.75 left side of the background is useless and supposed to be seen only by devices with bigger aspect ratios.
" But how big we must make the actual image? "
Achieving resolution independence
We have completed the first step of achieving resolution independence because we are using a camera with our own virtual resolution and we filtered all of our assets. We must also create different sets of assets as i have already explained for some specific resolutions. We won't be doing that in these tutorials because it's very time consuming. I will show you though, how to define the actual width and height of your backgrounds.
Until now, we only provide the best graphic experience to devices with resolution close to ours which was 1280 x 720. But because we wanted to make the backgrounds bigger, to show more space to the devices with different aspect ratios, we have to draw a bigger background. How much bigger?
1280 ÷ 16 = 80 and 19.5 x 80 = 1560
720 ÷ 9 = 80 and 12 x 80 = 960
Our background image must be 1560 x 960.
If we also wanted to support devices with resolutions close to 1920 x 1080 then our background would be;
1920 ÷ 16 = 120 and 19.5 x 120 = 2340
1080 ÷ 9 = 120 and 12 x 120 = 1440
Our background image would be 2340 x 1440.
We always choose supported resolutions to have an 16:9 aspect ratio and each time, we check the closest supported resolution for the device that runs our game and we load the assets from that folder. It doesn't matter if the aspect ratio of the device is different, because we have also achieved aspect ratio independence.
Aaand we are almost done! I hope that you now have completely understood everything. What's left is to create the animation for our hero, fix our timestep, learn how to load our assets with the libGDX's asset manager and place some ads.
Feel free to ask any questions in the comments below!
Comments
Post a Comment