LibGDX: Scene2d Demo with Java Universal Tween Engine

So I have modified my original demo somewhat to implement the Asteroid and ship pulse animations to use the Java Universal Tween Engine. I have also fixed some stuff and added some new features.

I put the star field behind the menu layer.

Game Test

I used Hiero to convert an existing free font to bring it into the application. BTW you can declare multiple fonts in the uiskin.json file i.e.

com.badlogic.gdx.graphics.g2d.BitmapFont:
{ default-font: { file: digital-7_60.fnt },
large-font: { file: digital-7_70.fnt }
},

I had to generate a second json file to hold a different sized default font because you can’t specify the font name for TextButton and I wanted a bigger size to match a phone touch-screen real-estate.

Taking a nod from the Tween engine I have created a proper callback action. This will take a count (<0 for infinite) and duration and repeatedly execute a call-back method. I could not use the completion handler in LibGDX actions as it resets the handler reference every time it get's called.

I have created two new action types TimelineAction and TweenAction. These encapsulate the running and management of the tweens. They are associated with an Actor and will animate it until complete. They manage the release of the tween back into the pool
(more about this further on).

The movement and rotation of the asteroids is implemented using the Timeline feature from the tween engine. Note the COMPLETE event callback to send an event which will trigger the removal of the element from the view.

int durationMove = (int) ((Math.random() * MAX_DURATION + MIN_DURATION) * 1000);
int rotate = (int) (Math.random() * 1440)-720;

Timeline timeline = Timeline.createParallel()
		.beginParallel()
		.push(Tween.to(this, ActorAccessor.POSITION_XY, durationMove).target(x, 0 - this.height).ease(Linear.INOUT))
		.beginSequence()
		.push(Tween.to(this, GroupAccessor.ROTATION, 0).target(0))
		.push(Tween.to(this, GroupAccessor.ROTATION, durationMove).target(rotate))
		.end()
		.end()
		.addCallback(EventType.COMPLETE, this)
		.start();

TimelineAction timelineAction = TimelineAction.$(timeline);

action(timelineAction);

Something of interest to note here. I want the rotation to last the length of the screen traversal and I want it to start from zero. I could set the “rotation” value of the sprite explicitly at the start of each run or I can do a non-Tween which will initially rotate to zero. This is what I have done here. There doesn’t seem to be a way to set the initial value in the Timeline/Tween setup itself any other way although I am open to suggestions.

The pulse is a straight Tween. Again, note the COMPLETE event callback to send an event which will trigger the removal of the element from the view.

Tween tween = Tween.to(this, ActorAccessor.POSITION_XY, duration)
                    .target(x, height)
                    .ease(Quad.OUT)
                    .addCallback(EventType.COMPLETE, this)
                    .start();
tweenAction = TweenAction.$(tween);

action(tweenAction);

I have extended the Actor and Group classes to manage the clean up of actions which are prematurely removed from the view. What I mean by this is – imagine a pulse is moving up the screen, animated by TweenAction. In normal operation the animation would complete, the “done” flag would get set and the “act” method would call the “finish” method on the associated actions which will put the respective actions back in the pool. If the pulse hits an Asteroid on the way up we should probably set the “done” flag on the PulseSprite and let it clean up itself but because of the way I am pooling these items I am having to override the “remove” method and force the “finish” method on all associated actions myself. This is a work in progress and I am still having a think about the best approach here. It works for now and you should have no leaking objects.

Demo code here.

LibGDX: Example of Scene2D application with Event Handlers.

Note: This demo has been updated considerably since this original post. The common elements have been factored out into their own library and the application mavenised to provide an easier build process. You can find all the details here.

This is the original post so some of the details have changed…..

I have written a simple demo using LibGDX and some of the ideas I had kicking around from a previous demo. I am quite pleased how this has turned out. I’m going to join the chorus of praise for LibGDX and say it is a lot easier to use than I thought it would be. I was putting off ditching my own framework and adopting it but it was actually fun to use and has an impressive amount of  knowledge behind it. The ability to develop on the desktop and then run it on Android is a killer feature.

This demo is a kind of mixture of framework items and source artefacts to implement a noddy shoot-em-up. The point is to show off how to assemble something which uses mostly the Scene2d classes.

Here is the application running on the desktop.

Game Test

So what is interesting about this demo?

This is a bit of an experiment to see if I could take what I had and make it scale up a bit more. The key concept is as follows:

The demo code is here.

Demo in detail



Some of the source code below is abbreviated, use the source download rather than cutting and pasting if you are interested in the source.


The Frame animation Sprite implements an Actor which takes spritesheet and implements straightforward animation. You cannot run actions like rotate on this as Actor does not have this attribute. You can make it move though. This lifts some of the example code from the LibGDX samples and reworks it into an Actor.

public class FrameSprite extends Actor
{
	private TextureRegion[] frames;

	private Animation animation;

	private TextureRegion currentFrame;

	private float stateTime;
	private boolean looping;

	public FrameSprite(TextureRegion texture, int rows, int cols, float frameDuration, boolean looping)
	{
		this.looping = looping;

		int tileWidth = texture.getRegionWidth() / cols;
		int tileHeight = texture.getRegionHeight() / rows;
		TextureRegion[][] tmp = texture.split(tileWidth, tileHeight);
		frames = new TextureRegion[cols * rows];

		int tileWidth = texture.getRegionWidth() / cols;
		int tileHeight = texture.getRegionHeight() / rows;
		TextureRegion[][] tmp = texture.split(tileWidth, tileHeight);
		frames = new TextureRegion[cols * rows];

		int index = 0;
		for (int i = 0; i < rows; i++)
		{
			for (int j = 0; j < cols; j++)
			{
				frames[index++] = tmp[i][j];
			}
		}

		width = tileWidth;
		height = tileHeight;

		animation = new Animation(frameDuration, frames);
		stateTime = 0f;

	}

	/**
	 * Reset animation.
	 *
	 * You can use this to ensure the animation plays from the start again. It's
	 * handy if you have one-shot animations like explosions but you are using
	 * re-usable Sprites. You must reset the animation to ensure the animation
	 * plays back again.
	 */
	public void resetAnimation()
	{
		stateTime = 0;
	}

	/**
	 * Check to see if animation finished.
	 *
	 * @param stateTime
	 *
	 * @return True if finished.
	 */
	public boolean isAnimationFinished()
	{
		return animation.isAnimationFinished(stateTime);
	}

}

The Animated Sprite class composes the FrameSprite to allow actions such as rotate and scale to work upon it.

public class AnimatedSprite extends Group
{
	private FrameSprite frameSprite;

	/**
	 * Create sprite.
	 *
	 * @param texture
	 *            The animation texture.
	 * @param rows
	 *            The animation texture rows.
	 * @param cols
	 *            The animation texture rows.
	 * @param frameDuration
	 *            The animation frame duration.
	 */
	public AnimatedSprite(TextureRegion textureRegion, int rows, int cols, float frameDuration)
	{
		frameSprite = new FrameSprite(textureRegion, rows, cols, frameDuration, true);

		this.width = frameSprite.width;
		this.height = frameSprite.height;

		addActor(frameSprite);
	}

	@Override
	public Actor hit(float x, float y)
	{
	    return super.hit(x, y);
	}
}

A scene is a Stage whichs maps to the size of the view and implements an InputMultiplexer to which input events are routed. When the Director makes a scene active it sets the chosen Scene multiplexer as the destination for all input events. Note also “entry” and “exit” scene methods. These get called when a scene is activated and removed respectively.

public class Scene extends Stage implements Node
{
	private static final int DEFAULT_LAYER_CAPACITY = 10;

	/**
	 * Associated input multiplexer.
	 */
	private InputMultiplexer inputMultiplexer;

	/**
	 * Stage elements as nodes. We need this so we can call enter and exit on
	 * actors in order to manage registration and de-registration of event
	 * handlers.
	 */
	private Array nodes;

	public Scene()
	{
		super(Director.instance().getWidth(), Director.instance().getHeight(), Director.instance().isStretch());

		inputMultiplexer = new InputMultiplexer(this);

		nodes = new Array(DEFAULT_LAYER_CAPACITY);
	}

	/**
	 * Get input multiplexer.
	 *
	 * @return The input multiplexer.
	 */
	public InputMultiplexer getInputMultiplexer()
	{
		return inputMultiplexer;
	}

	/**
	 * Add scene layer ensuring it adopts the same size as the owning scene.
	 *
	 * Note layer in nodes list.
	 *
	 * @param group
	 */
	public void addLayer(Layer layer)
	{
		layer.width = this.width;
		layer.height = this.height;

		nodes.add(layer);

		super.addActor(layer);
	}

	/**
	 * Handle pre-display tasks.
	 *
	 */
	@Override
	public void enter()
	{
		int size = nodes.size;
		for (int i = 0; i < size; i++)
		{
			nodes.get(i).enter();
		}
	}

	/**
	 * Handle post-display tasks.
	 *
	 */
	@Override
	public void exit()
	{
		int size = nodes.size;
		for (int i = 0; i < size; i++)
		{
			nodes.get(i).exit();
		}
	}

}

The Director maintains a note of the chosen size. It handles setting the current scene, running the render “tick”, updating the event mechanism and updating current actions associated with the active scene. It also handles recalculating the scaling offsets for touch/mouse events if you stretch the size of the screen.

public class Director
{
	private static final boolean DEFAULT_STRETCH = true;

	private static Director instance = null;

	private ActorEventSource eventSource;

	private int width;
	private int height;
	private boolean stretch;

	private Scene scene;

	private float scaleFactorX;
	private float scaleFactorY;

	/**
	 * Access singleton instance
	 *
	 * @return instance of class
	 */
	public synchronized static Director instance()
	{
		if (instance == null)
		{
			instance = new Director();
		}

		return instance;
	}

	/**
	 * Create reference to command pipeline.
	 *
	 */
	public Director()
	{
		scene = null;

		stretch = DEFAULT_STRETCH;

		// Latch onto event source.
		eventSource = ActorEventSource.instance();

		// These are scale factors for adjusting touch events to the actual size
		// of the view-port.
		scaleFactorX = 1;
		scaleFactorY = 1;
	}

	/**
	 * Update main loop.
	 *
	 */
	public void update()
	{
		// Update events.
		eventSource.update();

		// Update View
		Gdx.gl.glClearColor(0, 0, 0, 1);
		Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

		if (scene != null)
		{
			scene.act(Gdx.graphics.getDeltaTime());

			scene.draw();
		}
		else
		{
			Gdx.app.log("WTF!", "No scene");
		}
	}

	/**
	 * Set the current scene.
	 *
	 * @param scene
	 */
	public synchronized void setScene(Scene scene)
	{
		// If already active scene...
		if (this.scene != null)
		{
			// Exit stage left..
			this.scene.exit();
		}

		this.scene = scene;

		if (this.scene != null)
		{
			// Enter stage right..
			this.scene.enter();

			// NOTE: Route input events to the scene.
			Gdx.input.setInputProcessor(scene.getInputMultiplexer());
		}

	}

	/**
	 * Adjust the scale factors for touch/mouse events to match the size of the
	 * stage.
	 *
	 * @param width
	 *            The new width.
	 * @param height
	 *            The new height.
	 */
	public void recalcScaleFactors(int width, int height)
	{
		scaleFactorX = (float) this.width / width;
		scaleFactorY = (float) this.height / height;
	}
}

A Layer is a holder which implements InputProcessor and the default “enter” and “exit” handlers.

A scene can hold multiple layers which may or may not receive input. Here is the main GameScene.

public class GameScene extends Scene
{
	private Layer gameLayer;
	private Layer shipLayer;
	private Layer pulseLayer;
	private Layer asteroidLayer;
	private Layer explosionLayer;
	private Layer statsLayer;

	/**
	 * Main game scene.
	 *
	 */
	public GameScene()
	{
		// ---------------------------------------------------------------
		// Control layer
		// ---------------------------------------------------------------
		gameLayer = new GameLayer(this.width, this.height);

		getInputMultiplexer().addProcessor(gameLayer);

		addLayer(gameLayer);

		// ---------------------------------------------------------------
		// Pulse layer.
		// ---------------------------------------------------------------
		pulseLayer = new PulseLayer(this.width, this.height);

		addLayer(pulseLayer);

		// ---------------------------------------------------------------
		// Asteroid Layer
		// ---------------------------------------------------------------
		asteroidLayer = new AsteroidLayer(this.width, this.height);

		addLayer(asteroidLayer);

		explosionLayer = new ExplosionLayer(this.width, this.height);

		addLayer(explosionLayer);

		// ---------------------------------------------------------------
		// Ship layer
		// ---------------------------------------------------------------
		shipLayer = new ShipLayer(this.width, this.height);

		getInputMultiplexer().addProcessor(shipLayer);

		addLayer(shipLayer);

		// ---------------------------------------------------------------
		// Statistics layer
		// ---------------------------------------------------------------
		statsLayer = new StatsLayer(this.width, this.height);

		addLayer(statsLayer);
	}

	public Layer getShipLayer()
	{
		return shipLayer;
	}

	public Group getPulseLayer()
	{
		return pulseLayer;
	}

	public Group getAsteroidLayer()
	{
		return asteroidLayer;
	}

}

Implemented layers register themselves with the event mechanism when visible and de-register when they are no longer within an active scene. This is to avoid routing events to elements which do not need them and also means you can (technically) generate new instances without having to get weird behaviour where events are getting sucked up elsewhere. NOTE: I learnt the hard way to keep this stuff as simple as possible from my previous attempt at this demo. I was routing everything through source and observers including input events and it was a nightmare to debug.

Events

Lets look into the event handling more closely.

The  AsteroidLayer has a delayed callback which when triggered launches an Asteroid from the top of the screen by generating a random position and running the associated actions for the sprite.
/**
 * Launch a sprite from pool (if one available).
 *
 */
private void handleStartAsteroid()
{
	// Get free sprite from pool.
	AsteroidSprite sprite = pool.obtain();

	// Set running.
	sprite.run();

	// Add to view.
	addActor(sprite);
}

Once the asteroid is “running” it too has a periodic call back which checks to see if it has collided with anything and if so send an event to indicate which kind of collision.

Listening for collision events are the layers:

Lets take a look at an event handler for the AsteroidLayer:

/**
 * Handle events.
 *
 */
@Override
public boolean handleEvent(ActorEvent event)
{
	boolean handled = false;

	switch (event.getId())
	{
		case GameActorEvents.EVENT_START_ASTEROID:
			handleStartAsteroid();
			handled = true;
			break;
		case GameActorEvents.EVENT_COLLISION_ASTEROID_PULSE:
			handlePulseCollision(event.getActor());
			handled = true;
			break;
		case GameActorEvents.EVENT_END_ASTEROID:
			handleEndAsteroid(event.getActor());
			handled = true;
			break;
		default:
			break;
	}

	return handled;
}

/**
 * Launch a sprite from pool (if one available).
 *
 */
private void handleStartAsteroid()
{
	// Get free sprite from pool.
	AsteroidSprite sprite = pool.obtain();

	// Set running.
	sprite.run();

	// Add to view.
	addActor(sprite);
}

/**
 * Handle pulse hitting asteroid.
 *
 * @param actor
 */
private void handlePulseCollision(Actor actor)
{
	// Run explosion sprite
	this.director.sendEvent(GameActorEvents.EVENT_START_ASTEROID_EXPLOSION, actor);

	handleEndAsteroid(actor);

	// Update score.
	GameStats.instance().incScore();
}

/**
 * Handle controller events.
 *
 * @param event
 *            The actor event.
 *
 * @return True if handled.
 */
private void handleEndAsteroid(Actor source)
{
	// Free this from pool so it can be re-used.
	pool.free((AsteroidSprite) source);

	// Remove from view.
	removeActor(source);
}

You can see that as events are routed to the layer it responds accordingly. If the handler returns true the event will be removed from the event list and will not be passed on to any any other handlers.

Actions

Actions are used to run the sprite movements. As an example lets examine the PulseSprite, which is a simple animation, it has to move from the ship position to the top of the screen.


/**
 * Run actions for actor.
 *
 * @param x
 * @param y
 *
 * @return The main actions object.
 */
public void run(float x, float y)
{
	// ---------------------------------------------------------------
	// BECAUSE THE 'ACTION' METHOD DOES NOT CLEAR THE EXISTING LIST IT ADDS
	// TO IT YOU MUST CLEAR ACTIONS ASSOCIATED WITH ACTOR. THIS IS
	// BECAUSE WE ARE RECYCLING SPRITES.
	// ---------------------------------------------------------------
	clearActions();

	// Set initial position
	this.x = x;
	this.y = y;

	// Calculate the duration to cover distance at pixels-per-sec.
	float height = Director.instance().getHeight();
	float distance = height - y;
	float duration = PIXELS_PER_SEC_FACTOR * distance;

	// Move from source to top of screen.
	MoveTo moveTo = MoveTo.$(x, height, duration);
	moveTo.setCompletionListener(this);

	// Run
	action(moveTo);
}

/**
 * Handles pulse action complete.
 *
 */
@Override
public void completed(Action action)
{
        // Send notification that pulse has completed.
	Director.instance().sendEvent(GameActorEvents.EVENT_END_PULSE, this);
}

Note, we assign a completion handler to the move actions. If the pulse reaches the top of the screen without hitting anything then the completion handler will get triggered and it will send a event from the sprite to the PulseLayer which will remove it from the view, killing any further action execution as of course actions are associated with their Actor.

Because the PulseSprite is pooled we have to clear the associated action list when it is reused to clear out any unfinished or complete actions.

The demo code is here. This uses 0.9.3 snapshot from the Google Code subversion repository.

I am currently rewriting this again to bring in the Universal Tween Engine. That will be the next post.

Important

Only the “Menu”, “About” and “Game” view are implemented. To navigate out of a view PRESS THE ESC KEY. In Android you can press the back key.

Update #1

This project has been moved to gitHub. There have been a lot of improvements to the codebase including mavenizing it and separating out my netthreads-libgdx extensions library which grew out of this and subsequent demos.

Android : Europa Game Library

Europa is a Android 2d game library which I have been working on for the past 5 months on and off.

It is heavily influenced by the design principles of Cocos2d a 2d game engine which exists in Python form and as an influential iPhone framework.

This was created originally out of some frustration with the existing engines that were available. They all seem to focus heavily on providing classes which abstract the graphics layers of the Android platform. What I really wanted was a proper framework which I could use to organise the game elements. This desire had come about from my previous attempt to write a simple shoot-em-up style game. I got so far with the sprites but got bogged down in how to ‘manage’ the scene elements. When I examined cocos2d I was struck how similar the idea of ‘actions’ was to my original design where I was associating command objects with what I now know as elements from the scene graph.

In cocos2d there are actions which run in sequence on each tick of the game loop and are attached to ‘nodes’ where a node is merely an abstraction and can be sub-classed to implement scenes, layers and sprites. The actions form the basis of all animation and what’s more is they can be composed together to form chains of operations e.g. move from a to b and rotate by an amount. Europa basically follows the same design with a Director, scenes, layers, nodes and an action pipeline which operates in the node elements.

So far I have implemented

Main

The framework has been designed with good practise in mind so there are lots of design patterns plus most importantly this has been written with Android in mind right from the start. When I say ‘android in mind’ I mean that there is nowhere where it creates objects with the game loop unless it is deliberate. This was a major design goal from the start.

I have also tried to keep the class hierarchy as flat as I can so it’s exceptionally lean and mean in terms of how it hangs together.

Main

I am quite close to a version of the framework I am happy with and plan to publish it as a work in progress when I get a bit more into the shoot-em-up demo that I am using to iron out the parts needed.

 

Update #1

I am no where near releasing this in reality. I rewrote all the graphics subsystem to use matrices for the transformation between the coordinate systems which was a massive mind bender as that stuff is all new to me. Now am at a stalemate. The library is supposed to support Canvas and OpenGL but I have reached the point where I have to decide whether to ditch the Canvas support but I can’t let it go. The problem is that Canvas does not support lots of features like offloading the images onto the graphics memory and also the sprite drawing is so slow when there are lots of images. The caching of images is not efficient.  Analysis paralysis has set in big stylee. I know I should just ditch the Canvas support but cannot bring myself to design it out. I’ll come back to it when I’m ready.

Update #2

I have scrapped this version and am now using LibGDX instead coupled with some of the same concepts.

← Previous Page