Using fixed screen coordinates in libGDX

There may be a time when you really have no intention to deal with OpenGL’s Y-up coordinate system. For example you might have some legacy game code you’re porting which uses screen coordinates at a fixed resolution. Wouldn’t it be nice if you could just use those values as is? With a bit of tweaking, it turned out to be quite easy. I’m using libGDX, which is a cross-platform OpenGL ES Java framework, but this code can be easily ported to whichever OpenGL framework you prefer.

What does this code do?

The code below creates a full screen viewport with a coordinate system that matches your desired resolution and aspect ratio. It adds undrawable areas to the sides or the top and bottom of the screen if the aspect doesn’t match that of the window or the device’s screen. You can position your sprites properly using integer coordinates, with (0,0) being in the top-left corner, just as it was a low-resolution screen. It doesn’t magically make the viewport low resolution though, so there are a few things to keep in mind.

OpenGL texture filters

OpenGL texture filters

If you want to make your graphics look pixelated, make sure to use the GL_NEAREST texture filter instead of e.g. GL_LINEAR, or your low resolution texture might appear washed out as it is upscaled.

High resolution transformations

High resolution transformations

You can still render high resolution textures and models on top of your low resolution sprites. In fact all transformations you apply will still use the device’s higher resolution. Only integer coordinates will align your sprites to your low-resolution “pixels” and only if they’re in the same scale.

If you wish to render a 3D scene in low resolution, you will need to use other techniques, like rendering into a low-resolution framebuffer object.

Setting up the viewport

For the project I’m working on I had to use fixed 640×480 resolution for the in-game screens. The easiest way I’ve found is to create a base class which implements the Screen libGDX interface that has callbacks for most events your application may receive, for example when the user resizes the window or the orientation of the mobile device changes. Here is my GameScreen class that does all that’s needed:

package com.ostudio.ocamel.bases;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.OrthographicCamera;

public class GameScreen implements Screen {
	public OrthographicCamera camera;
	
	public void initViewport(float width, float height, float aspect) {
		// Get the window size in pixels
		float w = Gdx.graphics.getWidth();
		float h = Gdx.graphics.getHeight();
		
		float vw, vh; // Viewport size in screen coordinates
		float ox, oy; // Viewport offset in screen coordinates
		
		// Check aspect ratio
		if(w > h * aspect) {
			// Black bars on the sides
			vh = h;
			vw = Math.round(vh * aspect);
			oy = 0;
			ox = (w - vw) / 2;
		} else {
			// Black bars on top and bottom
			vw = w;
			vh = Math.round(nw * (1 / aspect));
			ox = 0;
			oy = (h - vh) / 2;
		}
		
		// Create camera with the desired resolution
		camera = new OrthographicCamera(width, height);
		
		// Move camera center to push 0,0 into the corner
		camera.translate(width / 2, height / 2);
		
		// Set Y to point downwards
		camera.setToOrtho(true, width, height);
		
		// Update camera matrix
		camera.update();
		
		// Set viewport to restrict drawing
		Gdx.gl20.glViewport((int)ox, (int)oy, (int)vw, (int)vh);
	}
	
	@Override
	public void render(float delta) {
	}

	@Override
	public void resize(int width, int height) {
		// Set up viewport when window is resized
		initViewport(640, 480, 4.0f / 3.0f);
	}

	@Override
	public void show() {
		// Set up viewport on first load
		initViewport(640, 480, 4.0f / 3.0f);
	}

	@Override
	public void hide() {
	}

	@Override
	public void pause() {
	}

	@Override
	public void resume() {
	}

	@Override
	public void dispose() {
	}
}

The initViewport() method does all the calculations that’s needed to set up a camera and restrict drawing inside the viewport. It needs to be called the first time the screen is shown and whenever the screen size changes. You can now easily create all your game screens by extending this one, just make sure to call super.show() and super.resize() if you override those methods.

Aspect ratio had to be added as an additional parameter to avoid distortion of resolutions with non-square pixels, where it’s not possible to calculate from width and height alone. It is most evident with 320×200, which was designed to be used with 4:3 screens, but the calculated width/height ratio is 16:10. Its square pixel couterpart is 320×240, but back in the good old days of 256 colours its memory requirement was sadly over the convenient 64k page limit.

Using sprites

One side effect of using a Y-down coordinate system is that all your textures and sprites will be flipped, so don’t forget to call flip(false, true) on all TextureRegion objects before using them. If you’re using TextureAtlas with sprite sheets, you can pass an extra parameter to the constructor to automatically flip all regions loaded from it, but for some reason, it didn’t always work for me, so I had to flip them all in a loop. After flipping, you can load and use sprites right away, they will automatically be the proper size scaled to your fixed coordinate system’s resolution:

package com.ostudio.ocamel.screens;

import java.util.HashMap;

import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.ostudio.ocamel.bases.GameScreen;

public class Episode1Screen extends GameScreen {
	
	TextureAtlas atlas;
	Sprite oatilla;
	SpriteBatch batch;

	@Override
	public void show() {
		super.show();
		
		// Create spritebatch for rendering
		batch = new SpriteBatch();
		
		// Load sprite sheet and flip regions for Y-down
		atlas = new TextureAtlas("data/ocamel.pack");
		for(AtlasRegion ar : atlas.getRegions()) {
			ar.flip(false, true);
		}
		
		// Load sprite
		oatilla = atlas.createSprite("oatilla");
	}
	
	@Override
	public void hide() {
		super.hide();
		
		// Release resources
		batch.dispose();
		batch = null;
		atlas.dispose();
		atlas = null;
	}
	
	@Override
	public void render(float delta) {
		// Clear viewport for new frame
		Gdx.gl.glClearColor(0,0,0,1);
		Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
		
		// Apply superclass' camera to sprite batch
		batch.setProjectionMatrix(camera.combined);
		
		batch.begin();
		
		// Position sprite in viewport coords
		// (0,0) is top-left corner
		oatilla.setPosition(114, 71);
		oatilla.draw(batch);

		batch.end();
	}
}
Share Button
Posted in Blog Tagged with: , , , ,
0 comments on “Using fixed screen coordinates in libGDX
1 Pings/Trackbacks for "Using fixed screen coordinates in libGDX"
  1. […] previous article on using fixed screen coordinates might have been a bit misleading and according to search keywords some of my readers expected to […]

Leave a Reply