LibGDX Tutorial 3C: Scene management

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:

jet(look, there is a space here!)flame

 

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:

SceneManagementCompressed

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! Smile

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. 

SceneManagement2Compressed

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.


Previous PartTable Of ContentsNext Part
Scroll to Top