In 2D game development, managing visual components and user interactions efficiently requires a structured rendering pipeline. LibGDX provides the com.badlogic.gdx.scenes.scene2d package, which introduces two foundational concepts: the Stage and Actors. A Stage acts as a centralized manager that handles event dispatching, coordinate transformations, and recursive drawing. All interactive or visible elements are registered as Actors. Notab, every Actor can also contain other Actors, enabling nested hierarchical layouts.
Under the hood, a Stage maintains a root Group, a SpriteBatch for optimized texture rendering, and a Viewport/Camera system for screen-to-world coordinate mapping. When integrating this system into a game loop, input routing must be explicitly assigned:
Gdx.input.setInputProcessor(gameStage);
This delegates touch, keyboard, and mouse events to the stage for propagation down the actor tree.
To demonstrate practical usage, we will construct a minimal game entry point, a custom character actor, and a control button. The architecture separates the listener lifecycle from domain logic.
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.scenes.scene2d.Stage;
public class SceneDemo extends ApplicationAdapter {
private Stage mainStage;
private PlayerCharacter hero;
private ControlButton moveTrigger;
@Override
public void create() {
// Initialize stage matching viewport dimensions
mainStage = new Stage();
// Instantiate entities
hero = new PlayerCharacter("hero_sprite");
moveTrigger = new ControlButton("right_arrow.png");
// Attach to hierarchy
mainStage.addActor(hero);
mainStage.addActor(moveTrigger);
// Route hardware inputs
Gdx.input.setInputProcessor(mainStage);
}
@Override
public void render() {
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// Synchronize state with elapsed time
mainStage.act(Gdx.graphics.getDeltaTime());
mainStage.draw();
}
@Override
public void dispose() {
mainStage.dispose();
}
}
Each Actor overrides draw() for rendering and optionally implements hit() for precise collision boundaries. By default, bounding box detection works, but custom logic ensures accurate touch zones. Positional updates happen implicit through actor properties.
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.scenes.scene2d.Actor;
public class PlayerCharacter extends Actor {
private Texture spriteSheet;
public PlayerCharacter(String texturePath) {
super(texturePath);
spriteSheet = new Texture(Gdx.files.internal(texturePath));
setWidth(spriteSheet.getWidth());
setHeight(spriteSheet.getHeight());
}
@Override
public void draw(SpriteBatch batch, float parentAlpha) {
batch.draw(spriteSheet, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(), 1f, 1f, getRotation());
}
@Override
public Actor hit(float x, float y) {
boolean insideX = x >= 0 && x <= getWidth();
boolean insideY = y >= 0 && y <= getHeight();
return (insideX && insideY) ? this : null;
}
}
Butons inherit from Actor but focus on event callbacks. When a tap occurs, the button queries its parent hierarchy to locate sibling entities. Modifying coordinates triggers subsequent render frames automatically.
import com.badlogic.gdx.scenes.scene2d.Actor;
public class ControlButton extends Actor {
@Override
public boolean touchDown(float x, float y, int pointer) {
// Traverse up to find sibling entities by name
Actor target = getParent().findActor("hero_sprite");
if (target != null) {
target.setX(target.getX() + 50f);
}
return true; // Indicates event consumption
}
}
Static sprites often lack polish. LibGDX simplifies sprite sheet animation via the Animation<T> class. Animations operate on a delta-t accumulator, sampling pre-defined TextureRegion arrays based on playback speed and looping behavior.
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.scenes.scene2d.Actor;
public class AnimatedCharacter extends Actor {
private Animation<TextureRegion> walkCycle;
private float animationStateTimer;
private final float FRAME_DURATION = 0.08f;
public AnimatedCharacter(String leftPose, String rightPose) {
super("animated_hero");
// Load base textures and convert to regions for atlas compatibility
TextureRegion regionA = new TextureRegion(new Texture(Gdx.files.internal(leftPose)));
TextureRegion regionB = new TextureRegion(new Texture(Gdx.files.internal(rightPose)));
// Interleave frames for smooth back-and-forth movement
TextureRegion[] frameSequence = {regionA, regionB, regionB, regionA};
walkCycle = new Animation<>(FRAME_DURATION, frameSequence);
setBounds(0, 0, regionA.getMinU(), regionA.getMinV());
}
@Override
public void draw(SpriteBatch batch, float parentAlpha) {
animationStateTimer += Gdx.graphics.getDeltaTime();
TextureRegion currentFrame = walkCycle.getKeyFrame(animationStateTimer, true);
batch.draw(currentFrame, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(), 1f, 1f, getRotation());
}
}
Modern workflows typically load textures from sprite sheets rather than individual files. The TextureRegion.split() method allows runtime slicing, optimizing memory bandwidth and reducing draw calls. Additionally, LibGDX includes built-in widgets like Table, Button, and TextButton under com.badlogic.gdx.scenes.scene2d.ui, which streamline complex interface construction without manual coordinate math.