Download the source code for this post Download the source code for this post

Welcome back. I’ve been putting libGDX sprites through their paces over the past couple of weeks, which is why it’s been a while since my last tutorial. I’ve got to the point now, where I can confidently cover the next three installments on Sprites that I have planned.

In my previous post we worked through modifying the default libGDX project, to make it display a sprite in that crazy XNA way, where the origin (0,0) is at the top-left corner of the screen.

Even though reside down under, in the paradise that is New Zealand, even I can’t be doing with things the other way around. I know, I know, it’s like that because of math blah de blah, but that doesn’t make it any easier for people coming from XNA or the 80’s micro-computers, where things are the more natural way around when dealing with sprites. That’s enough ranting from me, so let’s get down to business.

By the end of this tutorial, we will have an application which bounces 50 sprites around the screen at different velocities, as seen by the video below.

XNASpriteBatch

In my last tutorial, we fixed the inverted y-axis issue, by creating a camera and then using that camera’s combined matrix with our SpriteBatch. To make this easier, we will crate a new XNASpriteBatch class, which hides those details away.

public class XNASpriteBatch extends SpriteBatch {

	private final OrthographicCamera camera;

	public XNASpriteBatch(float viewportWidth, float viewportHeight) {
		super();

		camera = new OrthographicCamera(viewportWidth, viewportHeight);
		camera.setToOrtho(true, viewportWidth, viewportHeight);
		camera.update();
		setProjectionMatrix(camera.combined);
	}

	public void update(){
		camera.update();
		setProjectionMatrix(camera.combined);
	}

	public OrthographicCamera getCamera() {
		return camera;
	}
}

XNASpriteBatch extends SpriteBatch, so it is-a SpriteBatch and therefore inherits all SpriteBatch functionality. All we are doing is performing all of the camera initialisation within this class, to save us worrying about it again. Now, if we want XNA-style co-ordinates, we just use a XNASpriteBatch instead of a SpriteBatch.

XNASpriteBatch’s update() method deals with modifying SpriteBatch’s camera, so will need calling prior to each render operation. Let’s hook this new class in.

ublic class Sprite2 extends ApplicationAdapter {
	XNASpriteBatch batch;					// <---
	Texture img;
	private TextureRegion region;

	@Override
	public void create () {
		img = new Texture("badlogic.jpg");
		region = new TextureRegion(img);
		region.flip(false, true);
	}

	private void update(){
		batch.update();					// <---
	}

	@Override
	public void render () {
		update();					// <---
		Gdx.gl.glClearColor(1, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
		batch.begin();
		batch.draw(region, 0, 0);
		batch.end();
	}

	@Override
	public void resize(int width, int height) {
		super.resize(width, height);
		batch = new XNASpriteBatch(width, height);	// <---
	}

	@Override
	public void dispose() {
		super.dispose();
		batch.dispose();
		img.dispose();
	}
}

Note that I have added an update() method to our main game class. This method will be called at the beginning of our render() method, so is an ideal place to place any code that requires an update prior to rendering, such as our XNASpriteBatch.

When you run this code, it will produce exactly the same result as before - our sprite placed in the top-left corner of the screen.

XNASprite

Last time we used a TextureRegion to wrap our image, so that we could easily flip it vertically, to stop it from being displayed upside down. This time, I’m going to use a Sprite. Sprite extends TextureRegion, so is-a TextureRegion.

The reason that I want to use Sprite, is that they can store x and y positions, so that we don’t have to in our main class. This becomes important when we want to render multiple sprites.

Okay, I lied a bit. I’m actually going to extend the Sprite with a new XNASprite class:

public class XNASprite extends Sprite {

	public XNASprite(Texture texture) {
		super(texture);		
		xnaInit();
	}

	private void xnaInit(){
		flip(false, true);
	}			
}

Doing this, we can also hide the explicit y-axis flip, by initiating it from our constructor.

Now, let’s modify our code to make use of this class.

public class Sprite2 extends ApplicationAdapter {
	XNASpriteBatch batch;
	Texture img;
	private XNASprite sprite; 				// <--

	@Override
	public void create () {
		img = new Texture("badlogic.jpg");
		sprite = new XNASprite(img);			// <--
		sprite.setPosition(0, 0);			// <--
	}

	private void update(){
		batch.update();
	}

	@Override
	public void render () {
		update();
		Gdx.gl.glClearColor(1, 0, 0, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
		batch.begin();
		sprite.draw(batch);				// <--		
		batch.end();
	}

	@Override
	public void resize(int width, int height) {
		super.resize(width, height);
		batch = new XNASpriteBatch(width, height);
	}

	@Override
	public void dispose() {
		super.dispose();
		batch.dispose();
		img.dispose();
	}
}

So, we crate our sprite, set its position and call draw() on the sprite to render it. No need for explicit flipping anymore. yay!

Scaling

Okay, before we start bouncing this thing about, let’s scale it, so that we have a bit more space to bounce about in. To do this, we’ll use the setScale() method, to make our sprite half the size.

public void create () {
	img = new Texture("badlogic.jpg");
	sprite = new XNASprite(img);
	sprite.setPosition(0, 0);
	sprite.setScale(0.5f);		// <--
}

Let’s run our program to see what happens.

Sprite Helper Classes

Ello, ello. What’s going on here then? (imagine a cockney police officer’s voice for full effect). Well, it turns out that Sprites have the notion of an origin, which affects scaling and rotation. By default, this origin sits in the middle of our Sprite.

You can imagine our sprite being sucked in towards the center during the scaling operation, which produces the effect above (i.e. it is no longer at the top-left of the screen). What we want to do, is make the scaling origin the top-left corner of our Sprite.

To do this, we will simply modify our XNASprite code again, to save us worrying about it in the future.

@Override
public void setScale(float scaleXY) {
	setOrigin(0, 0);
	super.setScale(scaleXY);
}

We override the setScale() method and call setOrigin() before calling super.setScale(). Another annoyance eradicated.

Sprite Movement

You remember ye olde XNA days, where we move sprites based on the elapsed time since the last frame was drawn? If not, we used to do something like this:

Vector2 position;
Vector2 velocity;

public override void Update(GameTime gameTime){
     position += velocity * (float)gameTime.ElapsedGameTime.TotalSeconds;
}

Here we have Vector2 structs holding the sprite’s current position and velocity (magnitude and direction). We multiply the velocity by the fractional elapsed game time since the last frame and add it to the sprite’s position. This ensures that movement is time based, so that, on slower computers, the sprite will move further than it does on a faster computer - should it need to. So, the game will run at approximately the same speed on any device.

Things are very similar with libGDX. One thing we won’t do is Vector arithmetic like above. Instead, we will work with the x and y vector components directly, to avoid garbage generation. Garbage will slow our game down. Remember, in C#, Vectors are structs, which are allocated on the stack, rather than the heap. This concept doesn’t exist in Java.

Right, lets get to the code. The first thing we will do is add a velocity vector to our XNASprite class, along with some getters and setters:

private Vector2 velocity = new Vector2();

public float getVelocityX(){
	return velocity.x;
}

public float getVelocityY(){
	return velocity.y;
}

public void setVelocityX(float x){
	velocity.x = x;
}

public void setVelocityY(float y){
	velocity.y = y;
}

In our create() method, we’ll set the velocity x and y components to a random number between 50 and 300. This means that our sprite will move N pixels per second along the x axis and N pixels per second along the y axis, where N is our random value.

public void create () {
	img = new Texture("badlogic.jpg");
	sprite = new XNASprite(img);
	sprite.setPosition(0, 0);
	sprite.setScale(0.5f);
	sprite.setVelocityX((float) MathUtils.random(50, 300));
	sprite.setVelocityY((float) MathUtils.random(50, 300));
}

Now all we need to do is make the blighter move. First we’ll add an update() method to our XNASprite class:

public void update(float delta){
	setX(getX() + delta * velocity.x);
	setY(getY() + delta * velocity.y);
}

This is doing exactly the same as the C# XNA snippet of code above - moving our Sprite based on elapsed time since the last frame.

Finally, we call our sprite’s update() method from our main update() method.

private void update(){
	float delta = Gdx.graphics.getDeltaTime();
	sprite.update(delta);
	batch.update();
}

Line 2 shows how to get the elapsed time since the last frame in libGDX. All of this looks very similar to XNA, which is great.

Now when you run this, our sprite will move in some random direction (right and down), at a random speed and eventually disappear off the screen.

Tidying the Rendering

If you look carefully, as the sprite moves across the screen, the background looks like it is animating. In one of my earlier texture tutorials, I talked about each corner of a texture being mapped to a vertex. Well, in our case, we have scaled our sprite, which can lead to floating point rounding errors when trying to match each corner of the texture to a vertex position.

We’ll use a cheap and cheerful approach to fix this, by modifying XNASprite.update() so that it rounds our sprite positions to the nearest Integer.

public void update(float delta){
	setX( Math.round(getX() + delta * velocity.x));
	setY( Math.round(getY() + delta * velocity.y));
}

Note, you should not round the positions in update() as I am doing here. x and y really need to stay as floats, otherwise we lose accuracy during the accumulation of delta time. The proper way to implement this is to round x and y during rendering. i.e. render at the rounded position, but do not store the rounded positions within your Sprite class.

Right, let’s detect when the sprite reaches the edge of its viewport and make it travel back in the opposite direction. To help with this, we will add some convenience methods to our XNASprite class.

 public void reverseVelocityX(){
 	velocity.x *= -1;
 }

 public void reverseVelocityY(){
 	velocity.y *= -1;
 }

 public float getScaledWidth(){
 	return getWidth() * getScaleX();
 }

 public float getScaledHeight(){
 	return getHeight() * getScaleY();
 }

Then we’ll add a new updateSprites() method, where we will update our Sprite’s position and then deal with detecting and handling collision with the sides of the viewport.

private void updateSprites(float delta){
	sprite.update(delta);
	float x = sprite.getX();
	float y = sprite.getY();

	// check x axis bounds
	if(x < 0){
		sprite.setX(0);
		sprite.reverseVelocityX();
	}else if(x > Gdx.graphics.getWidth() - sprite.getScaledWidth()){
		sprite.setX(Gdx.graphics.getWidth() - sprite.getScaledWidth());
		sprite.reverseVelocityX();		
	}

	// check y axis bounds
	if(y < 0){
		sprite.setY(0);
		sprite.reverseVelocityY();
	}else if(y > Gdx.graphics.getHeight() - sprite.getScaledHeight()){
		sprite.setY(Gdx.graphics.getHeight() - sprite.getScaledHeight());
		sprite.reverseVelocityY();		
	}
}

Finally, we hook this new method into our main update() method.

private void update(){
	float delta = Gdx.graphics.getDeltaTime();
	updateSprites(delta);
	batch.update();
}

Now we have a single sprite bouncing around the screen. How cute.

Sprites Galore

Let’s make this simple little application super cool. We’ll have 50 sprites, scaled at random sizes and all moving around at random velocities.

First we’ll hold an array of sprites instead of a single sprite.

private static int NUM_SPRITES = 50;
private XNASprite[] sprites = new XNASprite[NUM_SPRITES];

In our create() method, we just loop around creating sprites and shoving them into their allocated array slot.

public void create () {
	img = new Texture("badlogic.jpg");
	for(int i = 0; i < NUM_SPRITES; i++){
		XNASprite sprite = new XNASprite(img);
		sprites[i] = sprite;
		sprite.setPosition(0, 0);
		sprite.setScale(MathUtils.random(.9f)+.1f);		
		sprite.setVelocityX((float) MathUtils.random(50, 300));
		sprite.setVelocityY((float) MathUtils.random(50, 300));
	}
}

We also add a loop to our updateSprites() method, and update each one then check for viewport edge collision, as before.

private void updateSprites(float delta){
	for(int i = 0; i < NUM_SPRITES; i++){
		XNASprite sprite = sprites[i];
		sprite.update(delta);
		float x = sprite.getX();
		float y = sprite.getY();

		// check x axis bounds
		if(x < 0){
			sprite.setX(0);
			sprite.reverseVelocityX();
		}else if(x > Gdx.graphics.getWidth() - sprite.getScaledWidth()){
			sprite.setX(Gdx.graphics.getWidth() - sprite.getScaledWidth());
			sprite.reverseVelocityX();		
		}

		// check y axis bounds
		if(y < 0){
			sprite.setY(0);
			sprite.reverseVelocityY();
		}else if(y > Gdx.graphics.getHeight() - sprite.getScaledHeight()){
			sprite.setY(Gdx.graphics.getHeight() - sprite.getScaledHeight());
			sprite.reverseVelocityY();		
		}
	}
}

Finally we add a similar loop to our render() method.

public void render () {
	update();
	Gdx.gl.glClearColor(1, 0, 0, 1);
	Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
	batch.begin();
	for(int i = 0; i < NUM_SPRITES; i++){
		sprites[i].draw(batch);
	}
	batch.end();
}

Neat eh? Don’t stare at it for too long, or you’ll be hooked.

Coming up Next…

Next time I want to demonstrate the excellent mechanisms libGDX provides us with for animating our Sprites.

JK