Advertisement

LibGDX – Making a Paged Level Selection Screen

Welp, it looks like it has been about a year since my last post, so I figured I’d get something up here before a full year went by. Today, I’ll be going over some code I put together to make a paged level selection screen with LibGDX’s Scene2D package. I endearingly refer to this as Angry Birds style, but it’s obviously not exclusive to Angry Birds and has found it’s way into countless games and apps. Anyway, continuing on…

First of all, a screenshot of what we will be making. Note, I am not an artist, so I will just be using the basic skin as can be found in the LibGDX Tests project. You’ll obviously want to use your imagination to make it nicer looking.

device-2013-05-09-062247

device-2013-05-09-062327

There, isn’t that nice? Still pictures don’t really do it justice but I am on a time limit here so they’ll have to do.

The best part is, Scene2D already provides most everything we need to accomplish this so we’ll have very minimal code to introduce the concept of pages, handle centering on the nearest page after completing a scroll or fling, and loading a button will all of our fancy images and text.


First, we’ll make a subclass of ScrollPane so we can add the concept of pages and centering on the nearest page.

package com.example.gdx.scene2d;

import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.utils.Array;
import com.esotericsoftware.tablelayout.Cell;

public class PagedScrollPane extends ScrollPane {

	private boolean wasPanDragFling = false;

	private float pageSpacing;

	private Table content;

	public PagedScrollPane () {
		super(null);
		setup();
	}

	public PagedScrollPane (Skin skin) {
		super(null, skin);
		setup();
	}

	public PagedScrollPane (Skin skin, String styleName) {
		super(null, skin, styleName);
		setup();
	}

	public PagedScrollPane (Actor widget, ScrollPaneStyle style) {
		super(null, style);
		setup();
	}

	private void setup() {
		content = new Table();
		content.defaults().space(50);
		super.setWidget(content);		
	}

	public void addPages (Actor... pages) {
		for (Actor page : pages) {
			content.add(page).expandY().fillY();
		}
	}

	public void addPage (Actor page) {
		content.add(page).expandY().fillY();
	}

	@Override
	public void act (float delta) {
		super.act(delta);
		if (wasPanDragFling && !isPanning() && !isDragging() && !isFlinging()) {
			wasPanDragFling = false;
			scrollToPage();
		} else {
			if (isPanning() || isDragging() || isFlinging()) {
				wasPanDragFling = true;
			}
		}
	}

	@Override
	public void setWidget (Actor widget) {
		throw new UnsupportedOperationException("Use PagedScrollPane#addPage.");
	}
	
	@Override
	public void setWidth (float width) {
		super.setWidth(width);
		if (content != null) {
			for (Cell cell : content.getCells()) {
				cell.width(width);
			}
			content.invalidate();
		}
	}

	public void setPageSpacing (float pageSpacing) {
		if (content != null) {
			content.defaults().space(pageSpacing);
			for (Cell cell : content.getCells()) {
				cell.space(pageSpacing);
			}
			content.invalidate();
		}
	}

	private void scrollToPage () {
		final float width = getWidth();
		final float scrollX = getScrollX();
		final float maxX = getMaxX();

		if (scrollX >= maxX || scrollX <= 0) return;

		Array<Actor> pages = content.getChildren();
		float pageX = 0;
		float pageWidth = 0;
		if (pages.size > 0) {
			for (Actor a : pages) {
				pageX = a.getX();
				pageWidth = a.getWidth();
				if (scrollX < (pageX + pageWidth * 0.5)) {
					break;
				}
			}
			setScrollX(MathUtils.clamp(pageX - (width - pageWidth) / 2, 0, maxX));
		}
	}

}

Finally, it’s just a matter of making the grid of buttons. In Scene2D, a Button is a Table, so it enjoys all the abilities that the table provides, including the ability to add and layout children as you like.

For our buttons we’ll have an image and a label at the top, with a table showing the number of stars they’ve earned on the level. Did I mention I am not an artist? We’ll just be using the default button background (tinted red, for fun) for our top image and tinted squares as our stars.


First we’ll create our PagedScrollPane, with each page being a table consisting of four columns and three rows:

PagedScrollPane scroll = new PagedScrollPane();
scroll.setFlingTime(0.1f);
scroll.setPageSpacing(25);
int c = 1;
for (int l = 0; l < 10; l++) {
	Table levels = new Table().pad(50);
	levels.defaults().pad(20, 40, 20, 40);
	for (int y = 0; y < 3; y++) {
		levels.row();
		for (int x = 0; x < 4; x++) {
			levels.add(getLevelButton(c++)).expand().fill();
		}
	}
	scroll.addPage(levels);
}

For each level we create a button, like so:

	/**
	 * Creates a button to represent the level
	 * 
	 * @param level
	 * @return The button to use for the level
	 */
	public Button getLevelButton(int level) {
		Button button = new Button(skin);
		ButtonStyle style = button.getStyle();
		style.up = 	style.down = null;
		
		// Create the label to show the level number
		Label label = new Label(Integer.toString(level), skin);
		label.setFontScale(2f);
		label.setAlignment(Align.center);		
		
		// Stack the image and the label at the top of our button
		button.stack(new Image(skin.getDrawable("top")), label).expand().fill();

		// Randomize the number of stars earned for demonstration purposes
		int stars = MathUtils.random(-1, +3);
		Table starTable = new Table();
		starTable.defaults().pad(5);
		if (stars >= 0) {
			for (int star = 0; star < 3; star++) {
				if (stars > star) {
					starTable.add(new Image(skin.getDrawable("star-filled"))).width(20).height(20);
				} else {
					starTable.add(new Image(skin.getDrawable("star-unfilled"))).width(20).height(20);
				}
			}			
		}
		
		button.row();
		button.add(starTable).height(30);
		
		button.setName("Level" + Integer.toString(level));
		button.addListener(levelClickListener);		
		return button;
	}

And here’s the whole file:

package com.example.gdx.scene2d;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.math.MathUtils;
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.ui.Button;
import com.badlogic.gdx.scenes.scene2d.ui.Button.ButtonStyle;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Slider;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.Skin.TintedDrawable;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle;
import com.badlogic.gdx.scenes.scene2d.utils.Align;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.esotericsoftware.tablelayout.Value;

public class PagedScrollPaneTest extends ApplicationAdapter {
	
	private Skin skin;
	private Stage stage;
	private Table container;

	public void create () {
		stage = new Stage(0, 0, false);
		skin = new Skin(Gdx.files.internal("data/uiskin.json"));
		skin.add("top", skin.newDrawable("default-round", Color.RED), Drawable.class);
		skin.add("star-filled", skin.newDrawable("white", Color.YELLOW), Drawable.class); 
		skin.add("star-unfilled", skin.newDrawable("white", Color.GRAY), Drawable.class);
		
		Gdx.input.setInputProcessor(stage);

		container = new Table();
		stage.addActor(container);
		container.setFillParent(true);

		PagedScrollPane scroll = new PagedScrollPane();
		scroll.setFlingTime(0.1f);
		scroll.setPageSpacing(25);
		int c = 1;
		for (int l = 0; l < 10; l++) {
			Table levels = new Table().pad(50);
			levels.defaults().pad(20, 40, 20, 40);
			for (int y = 0; y < 3; y++) {
				levels.row();
				for (int x = 0; x < 4; x++) {
					levels.add(getLevelButton(c++)).expand().fill();
				}
			}
			scroll.addPage(levels);
		}
		container.add(scroll).expand().fill();
	}

	public void render () {
		Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
		stage.act(Gdx.graphics.getDeltaTime());
		stage.draw();
		Table.drawDebug(stage);
	}

	public void resize (int width, int height) {
		stage.setViewport(width, height, false);
	}

	public void dispose () {
		stage.dispose();
		skin.dispose();
	}

	public boolean needsGL20 () {
		return false;
	}

	
	/**
	 * Creates a button to represent the level
	 * 
	 * @param level
	 * @return The button to use for the level
	 */
	public Button getLevelButton(int level) {
		Button button = new Button(skin);
		ButtonStyle style = button.getStyle();
		style.up = 	style.down = null;
		
		// Create the label to show the level number
		Label label = new Label(Integer.toString(level), skin);
		label.setFontScale(2f);
		label.setAlignment(Align.center);		
		
		// Stack the image and the label at the top of our button
		button.stack(new Image(skin.getDrawable("top")), label).expand().fill();

		// Randomize the number of stars earned for demonstration purposes
		int stars = MathUtils.random(-1, +3);
		Table starTable = new Table();
		starTable.defaults().pad(5);
		if (stars >= 0) {
			for (int star = 0; star < 3; star++) {
				if (stars > star) {
					starTable.add(new Image(skin.getDrawable("star-filled"))).width(20).height(20);
				} else {
					starTable.add(new Image(skin.getDrawable("star-unfilled"))).width(20).height(20);
				}
			}			
		}
		
		button.row();
		button.add(starTable).height(30);
		
		button.setName("Level" + Integer.toString(level));
		button.addListener(levelClickListener);		
		return button;
	}
	
	/**
	 * Handle the click - in real life, we'd go to the level
	 */
	public ClickListener levelClickListener = new ClickListener() {
		@Override
		public void clicked (InputEvent event, float x, float y) {
			System.out.println("Click: " + event.getListenerActor().getName());
		}
	};

}

Note, this is only one way to go about accomplishing this task and may not be the best way to do it, but it gets the job done so I’m happy with it.

6 Responses to “LibGDX – Making a Paged Level Selection Screen”

  • Mr.Krazy says:

    Hey nice tutorial, but can you also show how you setup your skin file, and your texture pack.

  • admin says:

    I just used the default skin from the libgdx tests.

  • David says:

    Very nice!!! Excellent tutorials you got there!
    anyways… in your code setScrollX
    setScrollX(MathUtils.clamp(pageX – (width – pageWidth) / 2, 0, maxX));

    i put my own code like these one,
    setScrollX(MathUtils.clamp(pageX, 0, maxX));
    the same result would happen, its because you already put
    if(scrollX < (pageX + pageWidth * 0.5)){
    break;
    }

    and the result is the starting page X of that page!

  • Wali says:

    Hi! Very nice tutorial. I have only one question. What can I do to avoid the Scrollpane being moved up and down? I only want it to move left and right. Sorry for my English 😛

  • Wali says:

    I could fix it! after creating the PagedScrollPane instance, I wrote the following line:

    scroll.setScrollingDisabled(false, true);

  • Mike says:

    Hey, could you please go into a bit more detail about what to do with the images? I can’t find them in the link you posted, and just using any old pngs with the same names doesn’t work. Thanks.