In this section we are going to take a look at the Scene2D library. The first thing you need to be aware of is scene2d is entirely optional! If you don’t want to use it, don’t. All the other parts, except the bits built over Scene2D, will continue to work just fine. Additionally if you want to use Scene2D for parts of your game ( such as a HUD overlain over your game ) you can.
So, what is scene2D? In a nutshell, it’s a 2D scene graph. So you might be asking “what’s a scene graph?”. Good Question! Essentially a scene graph is a data structure for storing the stuff in your world. So if your game world is composed of dozens or hundreds of sprites, those sprites are stored in the scene graph. In addition to holding the contents of your world, Scene2D provides a number of functions that it performs on that data. Things such as hit detection, creating hierarchies between game objects, routing input, creating actions for manipulating a node over time, etc.
You can think of Scene2D as a higher level framework for creating a game built over top of the LibGDX library. Oh it also is used to provide a very good UI widget library… something we will discuss later.
The object design of Scene2D is built around the metaphor of a play ( or at least I assume it is ). At the top of the hierarchy you have the stage. This is where your play (game) will take place. The Stage in turn contains a Viewport… think of this like, um… camera recording the play ( or the view point of someone in the audience ). The next major abstraction is the Actor, which is what fills the stage with… stuff. This name is a bit misleading, as Actor doesn’t necessarily mean a visible actor on stage. Actors could also include the guy running the lighting, a piece of scenery on stage, etc. Basically actors are the stuff that make up your game. So basically, you split your game up into logical Scenes ( be it screens, stages, levels, whatever makes sense ) composed of Actors. Again, if the metaphor doesn’t fit your game, you don’t need to use Scene2D.
So, that’s the idea behind the design, let’s look at a more practical example. We are simply going to create a scene with a single stage and add a single actor to it.
It’s important to be using the most current version of LibGDX, as recent changes to Batch/SpriteBatch will result in the following code not working!
package com.gamefromscratch; 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.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Stage; public class SceneDemo implements ApplicationListener { public class MyActor extends Actor { Texture texture = new Texture(Gdx.files.internal("data/jet.png")); @Override public void draw(Batch batch, float alpha){ batch.draw(texture,0,0); } } private Stage stage; @Override public void create() { stage = new Stage(Gdx.graphics.getWidth(),Gdx.graphics.getHeight(),true); MyActor myActor = new MyActor(); stage.addActor(myActor); } @Override public void dispose() { stage.dispose(); } @Override public void render() { Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); stage.draw(); } @Override public void resize(int width, int height) { } @Override public void pause() { } @Override public void resume() { } }
Also note, I also added the jet image I used in earlier examples to the assets folder as a file named jet.png. See the earlier tutorials if you are unsure of how to do this. When you run the application you should see:
As you can see it’s fairly simple process working with Stage2D. We create an embedded Actor derived class named MyActor. MyActor simply loads it’s own texture from file. The key part is the draw() method. This will be called every frame by the stage containing the actor. It is here you draw the actor to the stage using the provided Batch. Batch is the interface that SpriteBatch we saw earlier implements and is responsible for batching up drawing calls to OpenGL. In this example we simply draw our Texture to the batch at the location 0,0. Your actor could just as easily be programmatically generated, from a spritesheet, etc. One thing I should point out here, this example is for brevity, in a real world scenario you would want to manage things differently, as every MyActor would leak it’s Texture when it is destroyed!
In our applications create() method we create our stage passing in the app resolution. The true value indicates that we want to preserve our devices aspect ratio. Once our stage is created, we create an instance of MyActor and add it to the stage with a call to stage.addActor(). Next up in the render() function, we clear the screen then draw the stage by calling the draw() method. This in turn calls the draw() method of every actor the stage contains. Finally you may notice that we dispose of stage in our app’s dispose() call to prevent a leak.
So, that is the basic anatomy of a Scene2D based application. One thing I didn’t touch upon is actually having actors do something or how you would control one. The basic process is remarkably simple with a couple potential gotchas. Let’s look at an updated version of this code, the changes are highlighted:
package com.gamefromscratch; 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.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; public class SceneDemo2 implements ApplicationListener { public class MyActor extends Actor { Texture texture = new Texture(Gdx.files.internal("data/jet.png")); float actorX = 0, actorY = 0; public boolean started = false; public MyActor(){ setBounds(actorX,actorY,texture.getWidth(),texture.getHeight()); addListener(new InputListener(){ public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { ((MyActor)event.getTarget()).started = true; return true; } }); } @Override public void draw(Batch batch, float alpha){ batch.draw(texture,actorX,actorY); } @Override public void act(float delta){ if(started){ actorX+=5; } } } private Stage stage; @Override public void create() { stage = new Stage(); Gdx.input.setInputProcessor(stage); MyActor myActor = new MyActor(); myActor.setTouchable(Touchable.enabled); stage.addActor(myActor); } @Override public void 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:
Click the jet sprite and it’s action will start. Let’s take a closer look at the code now.
Let’s start with the changes we made to the MyActor class. The most obvious change you will see is the addition of a constructor. I did this so I could add an event listener to our actor, that works a lot like event listeners we worked with earlier when dealing with input. This one however passes an InputEvent class as a parameter which contains the method getTarget(), which is the Actor that was touched. We simply cast it to a MyActor object and set the started boolean to true. One other critical thing you may notice is the setBounds() call. This call is very important! If you inherit from Actor, you need to set the bounds or it will not be click/touch-able! This particular gotcha cost me about an hour of my life. Simply set the bounds to match the texture your Actor contains. Another thing you may notice is a lot of the examples and documentation on Actor event handling is currently out of date and there were some breaking changes in the past!
Other than the constructor, the other major change we made to MyActor was the addition of the act() method. Just like with draw(), there is an act() method that is called on every actor on the stage. This is where you will update your actor over time. In many other frameworks, act would instead be called update(). In this case we simply add 5 pixels to the X location of our MyActor each frame. Of course, we only do this once the started flag has been set.
In our create() method, we made a couple small changes. First we need to register an InputProcessor. Stage implements one, so you simply pass the stage object to setInputProcessor(). As you saw earlier, the stage handles calling the InputListeners of all the child actors. We also set the actor as Touchable, although I do believe this is the default behavior. If you want to make it so an actor cannot be touched/clicked, pass in Touchable.disabled instead. The only other change is in the render() method, we now call stage.act() passing in the elapsed time since the previous frame. This is is what causes the various actors to have their act() function called.
Scene2D is a pretty big subject, so I will be dealing with it over several parts.