So far we’ve look at what Scene2D provides in terms of Actors, Actions as well as handling input, now we will look at some of the scene management functionality it provides. One of the very powerful capabilities of Scene2D is grouping. Let’s jump right in with an example. In this example I used these two graphics:
(look, there is a space here!)
By the way, those are two different images. One is a jet and the second is the engine exhaust. Let,s take a look at grouping them together in a scene as a single transformable entity, a pretty common game development task.
package com.me.mygdxgame; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Group; import com.badlogic.gdx.scenes.scene2d.Stage; import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*; public class SceneManagementDemo implements ApplicationListener { private Stage stage; private Group group; @Override public void create() { stage = new Stage(Gdx.graphics.getWidth(),Gdx.graphics.getHeight(),true); final TextureRegion jetTexture = new TextureRegion(new Texture("data/jet.png")); final TextureRegion flameTexture = new TextureRegion(new Texture("data/flame.png")); final Actor jet = new Actor(){ public void draw(Batch batch, float alpha){ batch.draw(jetTexture, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(), getScaleX(), getScaleY(), getRotation()); } }; jet.setBounds(jet.getX(), jet.getY(), jetTexture.getRegionWidth(), jetTexture.getRegionHeight()); final Actor flame = new Actor(){ public void draw(Batch batch, float alpha){ batch.draw(flameTexture, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(), getScaleX(), getScaleY(), getRotation()); } }; flame.setBounds(0, 0, flameTexture.getRegionWidth(), flameTexture.getRegionHeight()); flame.setPosition(jet.getWidth()-25, 25); group = new Group(); group.addActor(jet); group.addActor(flame); group.addAction(parallel(moveTo(200,0,5),rotateBy(90,5))); stage.addActor(group); } @Override public void dispose() { stage.dispose(); } @Override public void render() { Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); stage.act(Gdx.graphics.getDeltaTime()); stage.draw(); } @Override public void resize(int width, int height) { } @Override public void pause() { } @Override public void resume() { } }
When you run it, you will see:
As you can see, once grouped, the attached actors inherit any transformations applied to the group. The code in this example should be pretty straight forward at this point, not much new here. First we load our two images as TextureRegions. We then create an actor for each, in both cases setting it’s boundaries with setBounds() or it wont render correctly. For each Actor we implement the full batch.draw() function to make sure rotation and scaling are properly rendered. Finally for the flame texture, we set it’s position relative to the jet Actor.
Then we create a new Group object, this is the secret sauce behind Scene2D grouping. Then instead of adding our two Actors to the Scene, we instead add them to the Group, which is then added to the Scene. So that we can actually see something happening in this example, we apply a moveTo and rotateBy Action to our group. We covered Actions in the last tutorial post if you want more details. One important thing I didn’t show here is, it is possible to translate the individual Actors within the Group.
One other aspect of Scene2D is determining if a hit occurred. Back in Scene2D Tutorial Part 1 we saw the touchDown function, which is called when an Actor within a Scene is touched. Now we will briefly look at the logic driving this process. The Stage’s hit() method is called, which in turn calls the hit() method of every Actor within the stage. hit() passes in un-translated coordinates, making the process easier on the developer. The default hit() method simply checks the bounding box of the Actor… the following example instead checks the bounding circle… in case you had a say… circular object!
package com.me.mygdxgame; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Stage; import com.badlogic.gdx.scenes.scene2d.Touchable; import java.util.Random; public class SceneManagementDemo implements ApplicationListener { // Create an Actor "Jet" that displays the TextureRegion passed in class Jet extends Actor { private TextureRegion _texture; public Jet(TextureRegion texture){ _texture = texture; setBounds(getX(),getY(),_texture.getRegionWidth(), _texture.getRegionHeight()); this.addListener(new InputListener(){ public boolean touchDown(InputEvent event, float x, float y, int pointer, int buttons){ System.out.println("Touched" + getName()); setVisible(false); return true; } }); } // Implement the full form of draw() so we can handle rotation and scaling. public void draw(Batch batch, float alpha){ batch.draw(_texture, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(), getScaleX(), getScaleY(), getRotation()); } // This hit() instead of checking against a bounding box, checks a bounding circle. public Actor hit(float x, float y, boolean touchable){ // If this Actor is hidden or untouchable, it cant be hit if(!this.isVisible() || this.getTouchable() == Touchable.disabled) return null; // Get centerpoint of bounding circle, also known as the center of the rect float centerX = getWidth()/2; float centerY = getHeight()/2; // Square roots are bad m'kay. In "real" code, simply square both sides for much speedy fastness // This however is the proper, unoptimized and easiest to grok equation for a hit within a circle // You could of course use LibGDX's Circle class instead. // Calculate radius of circle float radius = (float) Math.sqrt(centerX * centerX + centerY * centerY); // And distance of point from the center of the circle float distance = (float) Math.sqrt(((centerX - x) * (centerX - x)) + ((centerY - y) * (centerY - y))); // If the distance is less than the circle radius, it's a hit if(distance <= radius) return this; // Otherwise, it isnt return null; } } private Jet[] jets; private Stage stage; @Override public void create() { stage = new Stage(Gdx.graphics.getWidth(),Gdx.graphics.getHeight(),true); final TextureRegion jetTexture = new TextureRegion(new Texture("data/jet.png")); jets = new Jet[10]; // Create/seed our random number for positioning jets randomly Random random = new Random(); // Create 10 Jet objects at random on screen locations for(int i = 0; i < 10; i++){ jets[i] = new Jet(jetTexture); //Assign the position of the jet to a random value within the screen boundaries jets[i].setPosition(random.nextInt(Gdx.graphics.getWidth() - (int)jets[i].getWidth()) , random.nextInt(Gdx.graphics.getHeight() - (int)jets[i].getHeight())); // Set the name of the Jet to it's index within the loop jets[i].setName(Integer.toString(i)); // Add them to the stage stage.addActor(jets[i]); } Gdx.input.setInputProcessor(stage); } @Override public void dispose() { stage.dispose(); } @Override public void render() { Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); stage.act(Gdx.graphics.getDeltaTime()); stage.draw(); } @Override public void resize(int width, int height) { } @Override public void pause() { } @Override public void resume() { } }
When you run this app, 10 randomly located jet images will be drawn on screen.
As you click each, it will disappear. One important thing to realize is hit() is evaluated in reverse order objects are created. So, the last object you add to the stage will be the first one hit() if more than one object occupy the same space. The determining function whether a Jet has been touched or not is hit(). hit() works by creating a bounding circle to fit the image within, then checking to see if the mouse pointer is within the bounds of that circle. When a hit occurs, we return the object hit, otherwise we return null.
The nice thing about this system is the user doesn’t have to worry about any translations applied to it’s parent or other translations that have occurred. It’s also important to realize this is a pretty derived example. If you removed the overriden hit() method, the default implementation would actually work better. You do NOT need to provide a hit() method in your derived Actor classes unless the default doesn’t fit your needs. This example is merely to show how the Scene2D hit detection works, and how to implement a custom detector. Should you wish to say, implement pixel-perfect detection, you could do it this way. I commented this example a bit more than I regularly do, to explain the bits I’ve glossed over.