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.

Comments

18 Responses to “LibGDX: Example of Scene2D application with Event Handlers.”

  1. Rodolfo on February 1st, 2012 7:28 am

    Thank you for this post 😀 i was searching for one example using actors and stages 🙂

  2. Seth Sectioner on February 3rd, 2012 7:27 am

    This is a great example, however may you please post an example of this which uses Box2D? (Scene2D + Box2D tends to be a mess for me.)

  3. Waldeinburg on April 22nd, 2012 12:56 pm

    I understand from your earlier post that your Director-Scene-Layer model is inspired by Cocos2d. Which advantages do you think this has compared to using libGDX’s own Game, Screen and Group classes? They seem comparable to me.

  4. admin on April 22nd, 2012 1:55 pm

    Waldeinburg, That’s a good question. In honesty I didn’t look at the Game and Screen classes because I was porting the elements from my own framework which I had loosely based on Cocos2d. I just went ahead and created somewhat comparable elements. The only extra bits (more or less) I have added are the entry/exit methods (very handy) and the event handling which I think gives some nicely loosely coupled elements (or a rats nest if you go overboard).

  5. android developer on May 5th, 2012 10:31 pm

    i have some questions:
    1.do the collision detection uses a simple rectangular check or is it more sophisticated?
    2.can you please add the ability to keep aspect ratio?
    3.the android project didn’t work . only the desktop one . can you please fix it?
    4.after playing with it for a while , it froze. please check it out.
    5.can you please create a super short sample that includes touch , collision detection , and keeping aspect ratio?

  6. admin on May 6th, 2012 1:47 pm

    1. The collision detection is a simple rectangular intersection check. The relevant method is in the SceneHelper class.
    2. I think I know what you are asking here and the stretching of the view as you resize outside the aspect ratio is an attribute of the libGDX Stage. This can be switched off. I’m not sure what the effect of that would be. I have chosen 320×480 to match the HVGA resolution. I think handling the target view size is maybe a topic outside the scope of what this demo was supposed to be illustrating which is the Scene2D API.
    3. The Android project was broken. Thank you for pointing this out. It is a side effect of the updated AVD for Eclipse. I have rectified this in all the demos.
    4. I haven’t seen a freeze on the desktop. Needs investigation.
    5. Again, I would have to look into that but it’s a good suggestion.

  7. Bill on May 23rd, 2012 4:20 am

    Nice demo. I had been hunting for samples that used the scene2d functionality and it is pretty slim. There is quite a bit to libgdx, but samples seem to be rare.

  8. admin on May 23rd, 2012 4:06 pm

    If you stick to the concept of composing the scene-graph with views and actions you can get some very elegant code regardless of my extensions to it. Even with the few demos I have put together here I can see a case for the event handling as it decouples the component pieces although it’s possible to go mad and make a real mess.

  9. Bill on May 23rd, 2012 7:28 pm

    I had created my game originally in HTML5 with PhoneGap, but the performance was abysmal. I had high hopes for it since it would also run on iOS, but there were just too many performance issues and bugs to deal with.

    One thing I am trying to determine, is do I have to translate my x/y coordinates based upon the scaling factor for a stretched screen (i.e. touch position = x * (stretched height / target height)) for determining a touch position? Or does libgdx do this calculation automatically when reporting x/y in a touchDown listener?

  10. admin on May 23rd, 2012 8:03 pm

    If the view size is altered from the initial width and height given to the Stage then any touch co-ordinates have to be scaled by the stretch-factor. I’m pretty sure that is right. You can test this by changing the size of the desktop window and touching the view.

  11. Bill on May 23rd, 2012 10:47 pm

    That makes sense. It seems to be working that way for me.

    Any idea how I might implement a “touchOver” or “touchEnter” type event on an actor? I have my input for touchDown, touchUp and touchDragged registered on the actor, but if a user touches a different part of the screen and then drags their finger into the bounds of the actor, no events are fired and the hit method is never called. It seems the touchMoved event never fires for the actor. Any thoughts would be appreciated.

  12. Bill on May 23rd, 2012 11:01 pm

    One idea I had was just to extend the stage myself in my own stage class, then override the touchDragged in my stage class and do a hit test call on my actors during the touchDragged event and fire an event on the actor. There should be a “touchMove” type event (sort of like a mousemove).

  13. admin on May 24th, 2012 8:13 am

    If you examine the AppController class you will see the method

    @Override
    public void resize(int width, int height)
    {
      // Recalculate scale factors for touch events.
      director.recalcScaleFactors(width, height);
    }
    

    This is catching the resize and recalculating the appropriate scale factors. All you need to do in your touch handlers is ensure the x,y values are scaled accordingly i.e.

    touchActor.x = (x * director.getScaleFactorX());;
    touchActor.y = director.getHeight() - (y * director.getScaleFactorY());;
    

    Frustratingly the next demo I am working on does exactly what you are asking in terms of detecting when an actor is touched from drag etc. but it’s not quite ready. If you can hold-on I will be publishing it within the next few days as it’s almost complete. If you can’t wait email me and I will send you the code.

  14. Bill on May 24th, 2012 5:45 pm

    That makes sense. What I did to get the “touchOver” event is extend the Stage class with my own class and created an override on touchDragged and created my own event in my actor. The type checking and casting to my own actor type is probably slowing it down quite a bit, so I could just reuse one of the existing events instead of creating my own. Here is what I did (sorry I don’t know how to format code in comments):

    	@Override
    	public boolean touchDragged(int x, int y, int pointer) {
    		Actor actor = super.hit(x, y);
    		if (actor != null) {
    			if (actor.getClass() == MyActor.class) {
    				MyActor myActor = (MyActor) actor;
    				myActor.touchOver(x, y, pointer);
    			}			
    		}
    
    		return super.touchDragged(x, y, pointer);
    	}
    
    
  15. admin on May 24th, 2012 6:32 pm

    Ah, yes I see what you’ve done. That will work as long as it’s an actor and not a Group you are trying to drill into. Here is some code from my update SceneHelper (as yet unpublished) which will drill down as far as a specified target class.

    
    private static final Vector2 point = new Vector2();
    
    public static Actor hit(float x, float y, Group group, Class targetClass)
    	{
    		List<Actor> children = group.getActors();
    
    		Actor hit = null;
    		boolean found = false;
    		int index = children.size() - 1;
    		while (!found && index >= 0)
    		{
    			Actor child = children.get(index);
    
    			if (child.getClass().isAssignableFrom(targetClass))
    			{
    				Group.toChildCoordinates(child, x, y, point);
    
    				if (child.hit(point.x, point.y)!=null)
    				{
    					found = true;
    					hit = child;
    				}
    				else if (child instanceof Group)
    				{
    					child = hit(x, y, (Group) child, targetClass);
    				}
    			}
    
    			index--;
    		}
    
    		return hit;
    	}
    
  16. Bill on May 24th, 2012 8:23 pm

    Excellent. I hadn’t been handling groups yet. That should work pretty good. Thanks.

  17. alexy on September 24th, 2012 9:58 am

    Hi,

    I actually need to create layers as you did in the GameScene class. But its showing error, I mean the layer class is not even existing to be able to import it. Is that not included in the present libgdx version’s jar package? Please help.
    Thanks.

  18. admin on September 24th, 2012 12:22 pm

    Hi alexy. The ‘Layer’ class is not part of the libGDX library. It is a class I have defined as part of my own extensions to the base libGDX library along with my own ‘Scene’ which extends the Stage. Please examine the ‘netthreads-libgdx’ separate project which is bundled along with my demos.

Leave a Reply