I am developing game with libgdx and i got stuck with aspect ratio on different devices.
After a lot of thinking i figured that following is best solution for the problem:
I want to have camera always fixed to 16:9 aspect ratio and draw everything using that camera
If a device aspect is for example 4:3 i want to show only part of the view, not to strech it.
it should look something like this
blue is virtual screen(camera viewport) and red is device screen(visible area to 4:3 devices)
If for example device screen is also 16:9 full view should be visible.
Problem is i don't know how to achieve this.
I've done this for portrait screens, leaving some blank spaces at the top and bottom. As you can see in the following picture, the game stays at a 4:3 ratio and leaves whatever is leftover blank.
The content is always centered, and keeps its aspect ratio by not stretching the content unevenly. So here is some sample code Im using to achieve it.
public static final float worldW = 3;
public static final float worldH = 4;
public static OrthographicCamera camera;
...
//CALCULATING THE SCREENSIZE
float tempCalc = Gdx.graphics.getHeight() * GdxGame.worldW / Gdx.graphics.getWidth();
if(tempCalc < GdxGame.worldH){
//Adjust width
camera.viewportHeight = GdxGame.worldH;
camera.viewportWidth = Gdx.graphics.getWidth() * GdxGame.worldH / Gdx.graphics.getHeight();
worldWDiff = camera.viewportWidth - GdxGame.worldW;
}else{
//Adjust Height
camera.viewportHeight = tempCalc;
camera.viewportWidth = GdxGame.worldW;
worldHDiff = camera.viewportHeight - GdxGame.worldH;
}
camera.position.set(camera.viewportWidth/2f - worldWDiff/2f, camera.viewportHeight/2f - worldHDiff/2f, 0f);
camera.zoom = 1f;
camera.update();
I'm sure im not proposing the perfect solution, but you can play with the values on how the camera position and viewports are calculated so you can achieve the desired effect. Better than nothing I guess.
Also, Clash Of The Olympians Developers talk about how to achieve something like it and still make it look good on different devices (it is really interesting, but there is no code though): Our solution to handle multiple screen sizes in Android - Part one
The newer versions of libGDX provides a wrapper for glViewport called Viewport.
Camera camera = new OrthographicCamera();
//the viewport object will handle camera's attributes
//the aspect provided (worldWidth/worldHeight) will be kept
Viewport viewport = new FitViewport(worldWidth, worldHeight, camera);
//your resize method:
public void resize(int width, int height)
{
//notice that the method receives the entire screen size
//the last argument tells the viewport to center the camera in the screen
viewport.update(width, height, true);
}
I have written a function which works with different sizes of screen and images. The image might be zoomed and translated if it's possible.
public void transformAndDrawImage(SpriteBatch spriteBatch, Texture background, float scl, float offsetX, float offseY){
float width;
float height;
float _offsetX;
float _offsetY;
float bh2sh = 1f*background.getHeight()/Gdx.graphics.getHeight();
float bw2sw = 1f*background.getWidth()/Gdx.graphics.getWidth();
float aspectRatio = 1f*background.getHeight()/background.getWidth();
if(bh2sh>bw2sw){
width = background.getWidth() / bw2sw * scl;
height = width * aspectRatio;
}
else{
height = background.getHeight() / bh2sh * scl;
width = height / aspectRatio;
}
_offsetX = (-width+Gdx.graphics.getWidth())/2;
_offsetX += offsetX;
if(_offsetX-Gdx.graphics.getWidth()<=-width) _offsetX=-width+Gdx.graphics.getWidth();
if(_offsetX>0) _offsetX=0;
_offsetY = (-height+Gdx.graphics.getHeight())/2;
_offsetY += offseY;
if(_offsetY-Gdx.graphics.getHeight()<=-height) _offsetY=-height+Gdx.graphics.getHeight();
if(_offsetY>0) _offsetY=0;
spriteBatch.draw(background, _offsetX, _offsetY, width, height);
}
How to use it? It's easy:
#Override
public void render(float delta) {
Gdx.graphics.getGL10().glClearColor( 0.1f, 0.1f, 0.1f, 1 );
Gdx.graphics.getGL10().glClear( GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT );
spriteBatch.setProjectionMatrix(cam.combined);
spriteBatch.begin();
//zoom the image by 20%
float zoom = 1.2f; //Must be not less than 1.0
//translating the image depends on a few parameters such as zooming, aspect ratio of screen and image
offsetX+=0.1f; //offset the image to the left, if it's possible
offsetY+=0.1f; //offset the image to the bottom, if it's possible
transformAndDrawImage(spriteBatch, background, zoom, offsetX, offsetY);
//draw something else...
spriteBatch.end();
}
Related
I've developed an Android game using Unity. And in the end I've faced the next problem:
Each level of my game has strict borders (like a box) along the sides of the screen. I've been developing the game with aspect 9:16 (pic. 1). When I've built it and run on my phone (it's 9:19.5) I found out that leftmost and rightmost parts of the levels are out of the screen (pic. 2).
How can I fix this, so the game fits every aspect ratio?
I've tried different solutions, but nothing satisfied me. I'm so desperate that I'm already agree to just scale it down from 9:16 so that it fits the WIDTH of the screen plus adding black bars at the top and at the bottom (pic. 3). Height is not so important in my game, but width is crucial. Or is there some better solution?
UPDATE 2:
My project's design (kinda grayboxing):
Here's how it looks FINE with different aspects in play mode:
And here's how it looks POORLY with different aspects in play mode:
Unity's camera assumes that games will be played in a landscape orientation. Therefore the amount of the game world that is rendered vertically will always be the same on any screen, regardless of aspect ratio. If you are restricting your game to be played only on mobile platforms, and only in portrait mode, then you will want to force Unity to do the opposite. You want the camera to always render the same width, and show a variable amount of height depending on aspect ratio. (Helpful hint: Develop with the smallest aspect ratio you expect, like 4:5, not 9:16. Configure all of the aspect ratios or resolutions you want to test in the dropdown menu at the top of the Game View. Switch between these for testing, and allow your UI and game view to expand out for taller aspect ratios. Otherwise your UI will probably run together on smaller aspect ratios. See my post on CanvasScaler here.)
Anyway, to change the default camera behavior, you'll need to add a script to your camera. Something like this should do:
Edit: I modified the component to handle both perspective and orthographic cameras. Based on your game it appears you are, or perhaps should be, using an orthographic camera, which my previous answer did not handle. Also, there are no more properties on the component. Simply attach to a camera, then use the camera's properties as normal. Also, it now handles viewports, so it works with cameras that don't occupy the entire screen.
There is one known issue. With an orthographic camera, if you attempt to adjust the viewport to be greater than the entire screen size, something internal to Unity resets the projection matrix and overrides this component's behavior. To fix, simply toggle the component, or reload the scene or editor, anything that triggers the component to do its thing again.
using UnityEngine;
[ExecuteAlways]
[RequireComponent(typeof(Camera))]
public class HorizontallyAlignedCamera : MonoBehaviour
{
private Camera _camera;
private float _aspectRatio;
private float _fieldOfView;
private bool _isOrthographic;
private float _orthographicSize;
private void Awake()
{
_camera = GetComponent<Camera>();
}
private void OnEnable()
{
CachePropertiesAndRecalculate();
}
private void LateUpdate()
{
if(CameraPropertyHasChanged())
CachePropertiesAndRecalculate();
}
private void OnDisable()
{
_camera.ResetProjectionMatrix();
}
private bool CameraPropertyHasChanged()
{
bool hasChanged = (_aspectRatio != _camera.aspect
|| _fieldOfView != _camera.fieldOfView
|| _isOrthographic != _camera.orthographic
|| _orthographicSize != _camera.orthographicSize);
return hasChanged;
}
private void CacheCameraProperties()
{
_aspectRatio = _camera.aspect;
_fieldOfView = _camera.fieldOfView;
_isOrthographic = _camera.orthographic;
_orthographicSize = _camera.orthographicSize;
}
private void CachePropertiesAndRecalculate()
{
CacheCameraProperties();
if(_camera.orthographic)
RecalculateOrthographicMatrix();
else
RecalculatePerspectiveMatrix();
}
private void RecalculatePerspectiveMatrix()
{
float near = _camera.nearClipPlane;
float nearx2 = near * 2.0f;
float far = _camera.farClipPlane;
float halfFovRad = _camera.fieldOfView * 0.5f * Mathf.Deg2Rad;
// This is what aligns the camera horizontally.
float width = nearx2 * Mathf.Tan(halfFovRad);
float height = width / _camera.aspect;
// This is the default behavior.
//float height = nearx2 * Mathf.Tan(halfFovRad);
//float width = height * _camera.aspect;
float a = nearx2 / width;
float b = nearx2 / height;
float c = -(far + near) / (far - near);
float d = -(nearx2 * far) / (far - near);
Matrix4x4 newProjectionMatrix = new Matrix4x4(
new Vector4(a, 0.0f, 0.0f, 0.0f),
new Vector4(0.0f, b, 0.0f, 0.0f),
new Vector4(0.0f, 0.0f, c, -1.0f),
new Vector4(0.0f, 0.0f, d, 0.0f));
_camera.projectionMatrix = newProjectionMatrix;
}
private void RecalculateOrthographicMatrix()
{
// This is what aligns the camera horizontally.
float width = 2.0f * _camera.orthographicSize;
float height = width / _camera.aspect;
// This is the default behavior.
//float height = 2.0f * _camera.orthographicSize;
//float width = height * _camera.aspect;
float near = _camera.nearClipPlane;
float far = _camera.farClipPlane;
float a = 2.0f / width;
float b = 2.0f / height;
float c = -2.0f / (far - near);
float d = -(far + near) / (far - near);
Matrix4x4 newProjectionMatrix = new Matrix4x4(
new Vector4(a, 0.0f, 0.0f, 0.0f),
new Vector4(0.0f, b, 0.0f, 0.0f),
new Vector4(0.0f, 0.0f, c, 0.0f),
new Vector4(0.0f, 0.0f, d, 1.0f));
_camera.projectionMatrix = newProjectionMatrix;
}
}
So, I found a way to achieve the goal.
I added an empty game object to the scene and child'ed to it all the level structure, plus adding two black bars (as simple cubes) above and below the level. With that approach I can scale that parent game object so that all the level structure scales proportionally.
All I have to do then is creating a script for scaling the parent game object according to the width of the screen.
I'm trying to calibrate an OpenGL scene onto a preview camera for an augmented reality app, but the defined OpenGL perspective and the camera preview don't seem to match.
While looking around my field of view is obviously too large (in both vertical/horizontal direction).
Q: Did I misunderstand the idea of calibrating both?
Edit: I did the following check and just can't explain the result to myself.
Using the trueVerticalFOV from below for my projection matrix, the camera preview and OpenGL match perfectly.
It seems getVerticalViewAngle() didn't return the correct value. I've measured the FOV physically also resulting in trueVerticalFOV.
New Question: Why is this so? Any suggestions?
Cam Preview size and GLSurfaceView are also 800x480, cam zoom is zero.
float horizontalFOV =
camera.getParameters().getHorizontalViewAngle(); // = 60.5
float verticalFOV =
camera.getParameters().getVerticalViewAngle(); // = 47.1
float camAspect = hFOV / vFOV; // = 1.2845...
float displayWidth =
getResources().getDisplayMetrics().widthPixels; // = 800
float displayHeight =
getResources().getDisplayMetrics().heightPixels; // = 480
float displayAspect = displayWidth / displayHeight; // = 1.666...
float trueVerticalFOV = horizontalFOV / displayAspect; // = 36.30...
/Edit
Device's camera vertical FOV:
projectionFieldOfView = camera.getParameters().getVerticalViewAngle();
GLSurfaceView's aspect ratio:
public void onSurfaceChanged(GL10 unused, int width, int height) {
...
projectionAspect = (float) width / (float) height;
...
}
OpenGL's projection matrix:
Matrix.perspectiveM(
projectionMatrix, // projection matrix array
0, // array offset
projectionFieldOfView, // FOV [deg] = 47.1 deg
projectionAspect, // aspect ratio = 1.667
projectionNearClipping, // Near clipping plane = 1
projectionFarClipping ); // Far clipping plane = 3000
I am trying to center a 256px X 256px image in LibGDX. When i run the code I'm using it renders the image in the upper right hand corner of the window. For the camera's height and width I use Gdx.graphics.getHeight(); and Gdx.graphcis.getWidth(); . I set the cameras position to the camera's width divided by the two and its height divided by two... this should put it in the middle of the screen right? when I draw the texture, I set it's position to the camera's width and height divided by two -- so it's centered..or so I think. Why doesn't the image draw in the center of the screen, is there something I'm not understanding?
Thanks!
It sounds as if your camera is ok.
If you set the textures position, you set the position of the lower left corner of that texture. It is not centered. Therefore if you set it to the coordinates of the center of the screen, its extends will cover the space to the right and the top of that point. To center it, you need to subtract half of the textures width from the x, and half of the textures height from the y coordinate. Something along these lines:
image.setPosition(Gdx.graphics.getWidth()/2 - image.getWidth()/2,
Gdx.graphics.getHeight()/2 - image.getHeight()/2);
You should draw your texture at the camera position - half the dimensions of the texture...
For example:
class PartialGame extends Game {
int w = 0;
int h = 0;
int tw = 0;
int th = 0;
OrthographicCamera camera = null;
Texture texture = null;
SpriteBatch batch = null;
public void create() {
w = Gdx.graphics.getWidth();
h = Gdx.graphics.getheight();
camera = new OrthographicCamera(w, h);
camera.position.set(w / 2, h / 2, 0); // Change the height --> h
camera.update();
texture = new Texture(Gdx.files.internal("data/texture.png"));
tw = texture.getwidth();
th = texture.getHeight();
batch = new SpriteBatch();
}
public void render() {
batch.begin();
batch.draw(texture, camera.position.x - (tw / 2), camera.position.y - (th / 2));
batch.end();
}
}
I have an image of a face (250px X 250px) that is in an absolute layout element. I currently get the user's touch coordinates and using some maths calculate what has been touched (eg the nose), then do something accordingly.
My question is how to scale this to fit the screen width available. If I set the image (in the xml) to fill_parent, the coordinates are way out. Can this be remedied by converting the touch coordinates to dips (if so, how), or will I need to get the screen width (again convert into dips) and sort out the coordinate problem using more maths?
Any and all help appreciated.
pixels = dps * (density / 160)
The (density / 160) factor is known as the density scale factor, and get be retrieved in Java from the Display Metrics object. What you should do is store the position of the nose etc in terms of dips (which are the same as pixels on a screen with density 160), and then convert dips to pixels depending on what screen you are running on:
final static int NOSE_POSITION_DP = 10;
final float scale = getContext().getResources().getDisplayMetrics().density;
final int nosePositionPixels = (int) (NOSE_POSITION_DP * scale + 0.5f);
I have three useful functions in my library...
get Screen Density
public static float getDensity(Context context){
float scale = context.getResources().getDisplayMetrics().density;
return scale;
}
convert Dip to Pixels.
public static int convertDiptoPix(int dip){
float scale = getDensity();
return (int) (dip * scale + 0.5f);
}
convert Pixels to Dips.
public static int convertPixtoDip(int pixel){
float scale = getDensity();
return (int)((pixel - 0.5f)/scale);
}
A very simple way of doing this.
int value = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 250, (mContext).getResources().getDisplayMetrics());
public int getDip(int pixel)
{
float scale = getBaseContext().getResources().getDisplayMetrics().density;
return (int) (pixel * scale + 0.5f);
}
for the sake of simplicity let's assume that I'm making a simple Pong clone game for Android. Let's assume that it would only be playable in the landscape mode. (ignore square phones for now).
I'd like the game to look in the same scale on each phone, to the extent that if you took a screenshot of the game on QVGA phone, and resized the screenshot to WVGA size, it would look almost the same as would the game look on WVGA phone. In other words, the paddle's width should always be 1/32 of the screen width, the ball's diameter should always be 1/16 of the screen width.
What would be the proper way to paint the application? It would run in standard SurfaceView that would be drawn onto a Canvas.
Let's say that I have a high-resolution PNGs for the paddle, ball and for the game font (scoreboard, main menu).
Do I find out the physical resolution, then scale the Canvas via scale(float sx, float sy) to make all my Canvases (on QVGA and WVGA) have the same virtual resolution, and then draw exactly the same primitives on each position on each screen size?
Or can I use density-independent pixels (dip) somehow in the Canvas?
I only played once with the draw canvas functions and then switched all to opengl but the logic stays the same (I think).
first issue you'll want to keep a ratio constant form one phone to the other.
in my app I add a " black band" effect on each side.
in onSurfaceChanged, you'll want to calculate a ratio, this ratio will allow you to determine how much space you have to remove on each side to keep a consistent aspect to your drawing. this will give you a delta X or Y to apply to all your draws
the following code is something I adapted from the ogl version so it might need to be tweeked a bit
#Override
public void onSurfaceChanged(int w, int h){
float ratioPhysicScreen = nativeScreenResoltionW/nativeScreenResoltionH;
float ratioWanted = GameView.getWidth()/GameView.getHeight();
if(ratioWanted>ratioPhysicScreen){
newHeight = (int) (w*GameView.getHeight()/GameView.getWidth());
newWidth = w;
deltaY = (int) ((h-newHeight)/2);
deltaX = 0;
}else{
newWidth = (int) (h/GameView.getHeight()*GameView.getWidth());
newHeight = h;
deltaX = (int) ((w-newWidth)/2);
deltaY = 0;
}
then you'll also want to be able to draw your pictures on the canvas by knowing there size as well on the canvas than there real size and that where the difference in between image.getWidth() (actual size of the picture) and a image.getScaledWidth(canvas) which give you the size of the element in dp which means how big it will appear on the screen) is important. look at the example underneath.
public class MainMenuView extends PixelRainView{
private Bitmap bmpPlay = null;
private float playLeft = 0;
private float playRight = 0;
private float playBottom = 0;
private float playTop = 0;
public MainMenuView(Context context, AttributeSet attrs) {
super(context,attrs);
}
#Override
public void unLoadBitmaps() {
super.unLoadBitmaps();
if(bmpPlay != null){
bmpPlay.recycle();
bmpPlay = null;
}
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(bmpPlay == null){
bmpPlay = getBitmapFromRessourceID(R.drawable.play_bt);
playLeft = (this.getWidth()-bmpPlay.getScaledWidth(canvas))/2;
playRight = playLeft + bmpPlay.getScaledWidth(canvas);
playTop = (this.getHeight()-bmpPlay.getScaledHeight(canvas))/2;
playBottom = playTop+bmpPlay.getScaledHeight(canvas);
}
canvas.drawBitmap(bmpPlay,playLeft, playTop, null);
}
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
float x = event.getX();
float y = event.getY();
//test central button
if(x>playLeft && x<playRight && y<playBottom && y>playTop){
Log.e("jason", "touched play");
PixelRainView.changeView(R.layout.composedlayoutgroup);
}
return super.onTouchEvent(event);
}
}
This should solve all your ratio problem cross plateforms
I would suggest opengl because it will simplify your need for keeping a constant aspect but I guess its not an option ^^
Hope this helps you enough
Use dips and paint the paddles instead of using PNGs
I think that there are only 3 standard resolutions available (ldip, mdip, hdip) and you should have a resource folder for each of them.
http://developer.android.com/guide/practices/screens_support.html
If you manually scale your PNG's to fit each of the 3 available resolutions, the phone should load the specific resource folder in a transparent way