In this tutorial we will create a 2D game with libGDX, we are making a simple logic puzzle game: Take Ten (number game).
You can play the game online on Kongregate
You need to be familiar with Object-oriented programming, if not the tutorial will probably be too difficult. 🙁
You can follow the libGDX wiki to configure and create a project for libGDX:
The rules of the games are simple we will put these instrctions in the How To page of the game:The goal is to cross every number. You may cross two adjacent numbers by following at least one of these rules: – The two numbers are identical – The sum of the two numbers is ten – Two numbers separated with crossed numbers are adjacent – The first and the last number are adjacent When there are no more solutions, the game adds new numbers from the previous lines. The game may not be solvable.
In this class we will inherit the InputProcessor class and we will use the previous functions.
This class is small and use function from the main class Numbers.
public class EventHandler implements InputProcessor {
private Numbers num;
private TakeTen game;
// Constructor
public EventHandler(Numbers num, TakeTen game) {
this.game = game;
this.num = num;
}
// Exit the game or return to the menu
public boolean keyDown (int keycode) {
if(keycode == Input.Keys.ESCAPE) {
Gdx.app.exit();
} else if(keycode == Input.Keys.BACK || keycode == Input.Keys.BACKSPACE || keycode == Input.Keys.LEFT) {
game.setScreen(new Menu(game));
}
return false;
}
// Nothing to do
public boolean keyUp (int keycode) {
return false;
}
// Nothing to do
public boolean keyTyped (char character) {
return false;
}
// Nothing to do
public boolean mouseMoved (int x, int y) {
return false;
}
// Select the number when the mouse is pressed
public boolean touchDown (int x, int y, int pointer, int button) {
if(num.isSelected()) {
num.setTouchDown2(x, y, pointer, button);
} else {
num.setTouchDown(x, y, pointer, button);
}
return false;
}
// When the button is release
public boolean touchUp (int x, int y, int pointer, int button) {
num.setTouchUp(x, y);
return false;
}
// Update the position when selecting a number
public boolean touchDragged (int x, int y, int pointer) {
if(num.isNumSelected()) {
if(num.isSelected2()) {
float diff = Gdx.graphics.getHeight()- num.getPosSel2().y - y;
num.setPosition(num.getX(), num.getY() + diff);
num.setPositionSel2(x, y);
} else {
num.setPositionSel(x - 16, y + 16);
}
} else {
float diff = Gdx.graphics.getHeight()- num.getPosSel2().y - y;
num.setPosition(num.getX(), num.getY() + diff);
num.setPositionSel2(x, y);
}
return false;
}
// Update when the game is scrolled
public boolean scrolled (int amount) {
if(amount > 0) {
num.setPosition(num.getX(), num.getY() + 15);
} else {
num.setPosition(num.getX(), num.getY() - 15);
}
return false;
}
}
This class will stores every numbers on the screen and implement the rules of the games.
First let’s see the attributes of the class:
private Array<Integer> numbers; // Array of number
private Texture numTex; // Texture atlas of numbers
private int size; // size of a number in pixel (numbers are square images)
private float x ,y; // position of the screen
private Array<TextureRegion> rects; // sprites
private int indexSelected; // index of the selected number
private Vector2 posSelected, posSelected2; // position for the selected numbers
private boolean solutionFound; // if a solution has been found (for the solver)
private Viewport viewport; // viewport to keep the same ratio display on multiple screen
private boolean selected; // if a first number is selected
private boolean selected2; // if a number is dropped on another number
private boolean easy; // easy mode enabled
private int iSol; // index of the solution
And now let’s see the list of the methods whe have to implements:
// Constructor and destructor
public Numbers(float x, float y, String filename, Viewport viewport, boolean easy)
public void dispose()
// Main methods
public boolean checkSolution2() // find a solution
public boolean sumOrEquals(int i1, int i2) // check if the number can be crossed (see rules)
public boolean computeDistanceIndex(int i1, int i2) // Compute the distance between two numbers
public void setTouchDown(int x, int y, int pointer, int button) // call after a mouse button is pressed when nothing is selected
public void setTouchDown2(int x, int y, int pointer, int button) // call after a mouse button is pressed when a number is selected
public void checkBlackLine() // Check when there are a line with every numbers crossed
public void setTouchUp(int x, int y) // call when a mouse button is released
public void generateNewLine() // generate a new line of numbers
private int getIndexCollision(int xP, int yP) // get the index of the number collided
public void addNumber(Integer i) // add number to the array
public void render(SpriteBatch batch) // rendering method
// Getter and Setter methods
public boolean isNumSelected()
public Vector2 getPosSel2()
public float getX()
public float getY()
public boolean isSelected()
public boolean isSelected2()
public void setPositionSel(int x, int y)
public void setPositionSel2(int x, int y)
public void setPosition(float x, float y)
This seem like a lot of works but every method is short so it’s not very complicated :).
public Numbers(float x, float y, String filename, Viewport viewport, boolean easy) {
this.selected = false;
this.selected = false;
this.easy = easy;
this.viewport = viewport;
this.indexSelected = -1;
this.x = x;
this.y = y;
this.size = 32;
this.solutionFound = true;
this.iSol = 0;
numTex = new Texture(Gdx.files.internal(filename));
numTex.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
numbers = new Array<Integer>(900);
rects = new Array<TextureRegion>(10);
rects.add(new TextureRegion(numTex, 96, 64, 32, 32)); // 0
rects.add(new TextureRegion(numTex, 0, 96, 32, 32)); // 1
rects.add(new TextureRegion(numTex, 32, 96, 32, 32)); // 2
rects.add(new TextureRegion(numTex, 0, 64, 32, 32)); // 3
rects.add(new TextureRegion(numTex, 64, 96, 32, 32)); // 4
rects.add(new TextureRegion(numTex, 96, 96, 32, 32)); // 5
rects.add(new TextureRegion(numTex, 32, 64, 32, 32)); // 6
rects.add(new TextureRegion(numTex, 0, 32, 32, 32)); // 7
rects.add(new TextureRegion(numTex, 0, 0, 32, 32)); // 8
rects.add(new TextureRegion(numTex, 64, 64, 32, 32)); // 9
solutionFound = checkSolution2();
}
public void dispose() {
numTex.dispose();
}
// Find the index of a solution and return true if one is found
public boolean checkSolution2() {
for(int i = 0; i < numbers.size; i++) {
for (int j = 0; j < numbers.size; j++) {
if(sumOrEquals(i, j)) {
//Gdx.app.log("solution", i + ", " + j);
iSol = i;
return true;
}
}
}
return false;
}
// Return true if the number can be crossed
public boolean sumOrEquals(int i1, int i2) {
int iMin = 0;
for(int i = 0; i < numbers.size; i++) {
if(numbers.get(i) != 0) {
iMin = i;
break;
}
}
int iMax = 0;
for(int i = numbers.size - 1; i >=0; i--) {
if(numbers.get(i) != 0) {
iMax = i;
break;
}
}
if(i1 == i2 || i1 >= numbers.size || i2 >= numbers.size || i1 < 0 || i2 < 0) {
return false;
}
else if(numbers.get(i1).equals(0)) {
return false;
} else if(numbers.get(i2).equals(0)) {
return false;
} else if((i1 == iMin && i2 == iMax) || (i1 == iMax && i2 == iMin)) {
return numbers.get(i1).equals(numbers.get(i2)) || (numbers.get(i1) + numbers.get(i2)) == 10;
}
else if(computeDistanceIndex(i1, i2)) {
return numbers.get(i1).equals(numbers.get(i2)) || (numbers.get(i1) + numbers.get(i2)) == 10;
}
return false;
}
// If the distance between two numbers is exactly one return true
public boolean computeDistanceIndex(int i1, int i2) {
int min = 0;
int max = 0;
if(i1 > i2) {
min = i2;
max = i1;
} else if(i2 > i1) {
min = i1;
max = i2;
} else {
return false;
}
int latDistance = 0;
for(int i = (min+1); i <= max; i++) {
if(numbers.get(i) != 0) {
latDistance++;
}
}
int vertDistance = 0;
if(min%9 == max%9) {
for (int i = min + 9; i <= max; i = i + 9) {
if (i < numbers.size) {
if (numbers.get(i) != 0) {
vertDistance++;
}
}
}
}
if(vertDistance == 0 && latDistance == 0) {
return false;
} else if(vertDistance == 1 || latDistance == 1) {
return true;
}
return false;
}
public void setTouchDown(int x, int y, int pointer, int button) {
if(button == Input.Buttons.LEFT) {
int index = getIndexCollision(x, y);
if(index >= 0 && numbers.get(index) != 0) {
indexSelected = index;
float diff = Gdx.graphics.getHeight()/ Gdx.graphics.getWidth() - 800/480;
float ratio = 1.0f + diff;
setPositionSel((int)(x - 16*ratio), (int)(y + 16*ratio));
selected = true;
} else {
setPositionSel2(x, y);
}
}
}
public void setTouchDown2(int x, int y, int pointer, int button) {
if(button == Input.Buttons.LEFT) {
setPositionSel2(x, y);
selected2 = true;
}
}
public void checkBlackLine() {
boolean done = false;
for(int i = 0; i < numbers.size; i = i + 9) {
boolean check = true;
for(int j = 0; j < 9; j++) {
if((i+j) < numbers.size) {
if(numbers.get(i+j) != 0) {
check = false;
break;
}
} else {
check = false;
break;
}
}
if(check) {
for(int j = 0; j < 9; j++) {
numbers.removeIndex(i);
}
done = true;
}
}
if(done) {
checkBlackLine();
}
}
// Generate a new line of number and delete line of crossed number if needed
public void setTouchUp(int x, int y) {
if(indexSelected >= 0) {
int indexDrop = getIndexCollision(x, y);
if(indexDrop >= 0) {
if(sumOrEquals(indexDrop, indexSelected)) {
numbers.set(indexDrop, 0);
numbers.set(indexSelected, 0);
//Delete black lines
checkBlackLine();
solutionFound = checkSolution2();
if(!solutionFound) {
generateNewLine();
}
}
}
indexSelected = -1;
if(selected2) {
selected2 = false;
} else if(selected) {
selected = false;
}
}
}
// Generate a new line and find a new solution
public void generateNewLine() {
int size = numbers.size;
for(int i = 0; i < size; i++) {
if(numbers.get(i) != 0) {
addNumber(numbers.get(i));
}
}
solutionFound = checkSolution2();
}
// If the number is found return the index else return -1
private int getIndexCollision(int xP, int yP) {
//Gdx.app.log("Col pos", xP + ", " + yP);
for(int i = 0; i <= numbers.size/9; i++) {
for(int j = 0; j < 9; j++) {
//font.draw(batch, Integer.toString(numbers.get(i*9+j)), x + size*j, y + size*i);
if (i * 9 + j < numbers.size) {
Array<Vector2> r = new Array<Vector2>(4);
r.add(viewport.project(new Vector2(x + size*j, y - size*i)));
r.add(viewport.project(new Vector2(x + size*j + 32, y - size*i)));
r.add(viewport.project(new Vector2(x + size*j + 32, y - size*i + 32)));
r.add(viewport.project(new Vector2(x + size*j, y - size*i + 32)));
if(Intersector.isPointInPolygon(r, new Vector2(xP, Gdx.graphics.getHeight() - yP))) {
return i*9+j;
}
}
}
}
return -1;
}
public void addNumber(Integer i) {
numbers.add(i);
}
public void render(SpriteBatch batch) {
for(int i = 0; i <= numbers.size/9; i++) {
for(int j = 0; j < 9; j++) {
batch.setColor(Color.BLACK);
if(i*9+j < numbers.size) {
if(indexSelected < 0) { // No number selected
if(easy && (i * 9 + j) == iSol) {
batch.setColor(Color.RED);
}
batch.draw(rects.get(numbers.get(i * 9 + j)), x + size * j, y - size * i);
} else if(indexSelected != i*9+j) { // A number is selected
if(easy && (i * 9 + j) == iSol) {
batch.setColor(Color.RED);
}
batch.draw(rects.get(numbers.get(i * 9 + j)), x + size * j, y - size * i);
} else {
Vector2 v = viewport.unproject(new Vector2(posSelected.x, Gdx.graphics.getHeight() - posSelected.y));
if(easy && (i * 9 + j) == iSol) {
batch.setColor(Color.RED);
}
batch.draw(rects.get(numbers.get(i * 9 + j)), v.x, v.y);
}
}
}
}
}
public boolean isNumSelected() {
return indexSelected >= 0;
}
public Vector2 getPosSel2() {
return posSelected2;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
public boolean isSelected() {
return selected;
}
public boolean isSelected2() {
return selected2;
}
public void setPositionSel(int x, int y) {
this.posSelected = new Vector2(x, Gdx.graphics.getHeight() - y);
}
public void setPositionSel2(int x, int y) {
this.posSelected2 = new Vector2(x, Gdx.graphics.getHeight() - y);
}
public void setPosition(float x, float y) {
this.x = x;
this.y = y;
}
In this class we have implemented all the logic of the games. The next this to do is add a proper event handler.
To render the game we need to have a screen, so we create a new class implementing the Screen interface. In this class, we initialize everything we need for the game such as the camera, viewport, spritebatch, event handler and the main class Numbers.
public class GameScreen implements Screen {
final TakeTen game;
SpriteBatch batch;
private Numbers numbers;
private OrthographicCamera camera;
private Viewport viewport;
private boolean easy;
// Initilize the screem and the viewport
public GameScreen(final TakeTen game, boolean easy) {
this.game = game;
this.easy = easy;
camera = new OrthographicCamera();
viewport = new FillViewport(480, 800, camera);
viewport.apply();
camera.position.set(camera.viewportWidth/2,camera.viewportHeight/2,0);
batch = new SpriteBatch();
numbers = new Numbers(100, 700, "r-white.png", viewport, easy);
for (int i = 1; i < 20; i++) {
if (i == 10) {
} else if (i > 10) {
numbers.addNumber(1);
numbers.addNumber(i - 10);
} else {
numbers.addNumber(i);
}
}
Gdx.input.setCatchBackKey(true);
Gdx.input.setInputProcessor(new EventHandler(numbers, game));
}
@Override
public void render(float delta) {
camera.update();
batch.setProjectionMatrix(camera.combined);
Gdx.gl.glClearColor(1, 1, 1, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.begin();
numbers.render(batch);
batch.end();
}
@Override
public void resize(int width, int height) {
viewport.update(width, height);
}
@Override
public void show() {
}
@Override
public void hide() {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void dispose() {
batch.dispose();
numbers.dispose();
}
}
We will need a little tutorial to explain the rules and again we need a new Screen to render the tutorial.
public class HowToScreen implements Screen {
SpriteBatch batch;
BitmapFont fontTitle, font;
TakeTen game;
Viewport viewport;
OrthographicCamera camera;
public HowToScreen(final TakeTen game) {
this.game = game;
camera = new OrthographicCamera();
viewport = new FillViewport(480, 800, camera);
viewport.apply();
camera.position.set(camera.viewportWidth/2,camera.viewportHeight/2,0);
fontTitle = new BitmapFont(Gdx.files.internal("trebuc32.fnt"), false);
fontTitle.setColor(0, 0, 0, 1);
fontTitle.getRegion().getTexture().setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
font = new BitmapFont(Gdx.files.internal("trebuc16.fnt"), false);
font.setColor(0, 0, 0, 1);
font.getRegion().getTexture().setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
batch = new SpriteBatch();
InputMultiplexer multiplexer = new InputMultiplexer();
multiplexer.addProcessor(new EventHandler2(game));
multiplexer.addProcessor(new SwipGesture(new SwipGesture.DirectionListener() {
@Override
public void onUp() {
}
@Override
public void onRight() {
}
@Override
public void onLeft() {
game.setScreen(new Menu(game));
}
@Override
public void onDown() {
}
}));
Gdx.input.setInputProcessor(multiplexer);
}
@Override
public void render(float delta) {
camera.update();
batch.setProjectionMatrix(camera.combined);
Gdx.gl.glClearColor(1, 1, 1, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.begin();
fontTitle.draw(batch, "How to play?", 50, 750);
font.draw(batch, "The goal is to cross every number.\n You may cross two adjacent numbers by following\n at least one of these rules:\n\n" +
" - The two numbers are identical\n" +
" - The sum of the two numbers is ten\n\n" +
" - Two numbers separated with crossed \n" +
" numbers are adjacent\n" +
" - The first and the last number are adjacent\n\n" +
" When there are no more solutions, the game adds\n new numbers from the previous lines.\n\n" +
" The game may not be solvable.", 50, 700);
batch.end();
}
@Override
public void resize(int width, int height) {
viewport.update(width, height);
}
@Override
public void show() {
}
@Override
public void hide() {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void dispose() {
batch.dispose();
font.dispose();
fontTitle.dispose();
}
}
We need to have a main menu to choose between the tutorial and the game
public class Menu implements Screen {
final TakeTen game;
Skin skin;
Stage stage;
public Menu(TakeTen pgame){
this.game = pgame;
stage = new Stage(new StretchViewport(480, 800));
Gdx.input.setInputProcessor(stage);
skin = new Skin( Gdx.files.internal( "uiskin.json" ));
Table table=new Table();
table.setSize(480, 800);
final TextButton startGame = new TextButton("Start game",skin);
table.add(startGame).width(150).height(50);
table.row();
final TextButton tuto = new TextButton("Tutorial",skin);
table.add(tuto).width(150).padTop(10).padBottom(3);
table.row();
final TextButton howto = new TextButton("How to play?",skin);
table.add(howto).width(150).padTop(10).padBottom(3);
table.row();
TextButton quit = new TextButton("quit",skin);
table.add(quit).width(150).padTop(10);
table.row();
stage.addActor(table);
startGame.addListener(new ClickListener(){
@Override
public void clicked(InputEvent event, float x, float y) {
startGame.addAction(Actions.fadeOut(0.7f));
game.setScreen(new GameScreen(game, false));
}
});
howto.addListener(new ClickListener(){
@Override
public void clicked(InputEvent event, float x, float y) {
howto.addAction(Actions.fadeOut(0.7f));
game.setScreen(new HowToScreen(game));
}
});
tuto.addListener(new ClickListener(){
@Override
public void clicked(InputEvent event, float x, float y) {
tuto.addAction(Actions.fadeOut(0.7f));
game.setScreen(new GameScreen(game, true));
}
});
quit.addListener(new ClickListener(){
@Override
public void clicked(InputEvent event, float x, float y) {
Gdx.app.exit();
}
});
}
@Override
public void render(float delta) {
// clear the screen
Gdx.gl.glClearColor(1, 1, 1, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// let the stage act and draw
stage.act(delta);
stage.draw();
}
@Override
public void resize(int width, int height) {
stage.getViewport().update(width, height, false);
}
@Override
public void show() {
}
@Override
public void hide() {
stage.dispose();
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void dispose() {
stage.dispose();
}
}
The main class of the Game is rather small, we just need to render the starting point of the game : the menu
public class TakeTen extends Game {
SpriteBatch batch;
BitmapFont font;
public void create() {
batch = new SpriteBatch();
font = new BitmapFont();
this.setScreen(new Menu(this));
}
public void render() {
super.render();
}
public void dispose() {
batch.dispose();
font.dispose();
}
}
We have everything we need now we can play the game! But wait there is no fonts and no assets just the code.
We need to have the files for the font Trebuchet 16 and trebuchet 32
To generate the font file you can use Hiero, it is a bitmap font packing tool.
Each time you want to use a font you will need for a specific size two files:
To save you the trouble you can download the fonts : fonts.zip
You will need to have a uiskin file to have a skin for the menu : uiskin.zip you can also download the skin from the tests in libgdx
For the numbers we will use some images :
We have everything we need know to have something working.
The game is simple, but even a simple game can have some boilerplate code but what can we do (maybe use Unity :P).
You can download the source of the project for create a 2D game with libGDX on gamejolt!