Guest Tutorial: Making Flappy Bird using Overlap2D and LibGDX

 

The following is a guest tutorial by Avetis showing how to use LibGDX and Overlap2D to create an Angry Birds like game.  Overlap2D is a level and UI editor handy for separating game coding from content creation.  Keep in mind, Overlap2D is fairly young, currently at 0.0.4 version. Now, on with the tutorial!

 


Making Flappy Bird with Overlap2D – Overlappy Tutorial

 

 

Agenda

In this tutorial we are going to create a FlappyBird clone game (for learning purposes only) using Java, LibGDX framework and accelerate it all by moving our UI, Level, and Asset management into Overlap2D Editor. One thing I figured while coding is that you can’t make a quick tutorial about a flappy bird clone, ever. It’s going to be long if we want details. It is easier though to use the explain approach instead of step by step guide.  So I am going to share all the sources and resources, and then go with you through setting this all up, show how things work, and hopefully this all will make sense.

 

Prerequisites

Java knowledge, basic LibGDX experience, computer, time. Make sure you use latest Overlap2D editor and runtimes (v 0.0.4 or higher)

Get latest LibGDX project setup here: http://bitly.com/1i3C7i3 ( Details here )
Get latest Overlap2D editor here: http://overlap2d.com

 

First – Get Game Assets (Resources)


First let’s download the assets. I made a small pack of images I found on the internet with Flappy Bird images, animations and font. You can get it here: http://overlap2d.com/?p=236

 

 

Setting up Your Overlap2D Project

 

Unzip your Overlap2D zip file, and run the Overlap2D.jar.

First things first, you are currently viewing demo project, so go to File and Create New Project. Call it OverlappyBird and then create the scene size, let it be 480 width, 800 in height. It’s a standard small mobile device resolution, and since Flappy Bird is a portrait game, it should be just right. Click Create Project.
You have now created an empty project and it’s ready for setup, now you need to import the assets. Here we have 3 types of assets, first is just image textures, and second is a sprite animation for bird itself, and lastly the TTF font for score label. I have separated them in folders so you can import easily. Click on File, import to library.

First click on “…” button near the import images text field to select your images; locate them, select all. When done click on import and they all will get imported to your assets area on the right middle panel. Then open same the dialog again, and this time find Import Sprite Animations (note: not spine, but sprite) Click on “…” button there and locate bird.atlas file. Click Import, and it will get added to your assets list as well.

Lastly open import dialog again, and import font, by locating ttf file.

Your project setup is ready.

 

Making The Main Scene

 

First let’s see how main scene looks in Overlap2D when it is finished:

 

image

 
I decided to make several layers to have things organized, bg layer for background image that is not moving, default layer for pipes and bird itself, ground layer for moving the ground that is covering pipes, ui layer for “get ready” popups and score labels, and finally dlg layer for end game dialog. The reason I am not putting dialog inside ui layer is that I can position it in its layer correctly, and later hide that layer so it will not intervene with my work.  So, go ahead and create this layers in the order I described from bottom to top. (bg layer goes below them all, and you can rearrange them by drag-and-dropping)

I also recommend you to open my example project (provided with resources). Open it with Overlap2D and see how things are positioned and grouped. (To open a project, click file, open, and then locate .pit file inside project.)

Compose your own project like I did,. Put background on bg layer, and add bird on default layer. Make sure it looks like the picture above.

The ground is going to be a moving part, so we need to have 2 of this near each other for seamless movement. Put two grounds near each other and then group them into composites, like on this picture, do not forget to give identifier to it for later use.

image

 

For the pipes we are going to do the following trick. First of all we are going to put pipes on top of each other with a gap, and convert them into composite, and then add to library to have kind of a prefab for a pipe column. Later in code we are going to pre-load 3 of this because there is never more than 3 columns of pipes visible on screen, so we can re-use the ones that are left of the screen for next pipe iteration.

image

Now some of the pipes are going to vary in position on Y axis. So there are a minimum and maximum values that it should not be below or above. To avoid having this values in code, I just put 2 of pipe columns on screen: one at the lowest point and the other at the highest, and give them identifiers in order to read those values from code later.

 

image

 

Next click on bird in properties box and add 2 custom variables, by clicking on custom variables button. First is jump_speed=400, and second is gravity=1000 (so you can later tweak game from editor not from code)

image

 

Next up, put a “hint” and “get ready” assets convert them into composite as they always go together and give them id to hide show from game. They should go to ui layer.

 

Making Menu Scene

 

This one is easier: click on File, Scenes, Create New Scene, and choose MenuScene as name.

Make sure to put up all things just like on this picture. And note that you should make a composite for ground agai n (as we do not yet share prefabs between the scenes).

Put a Play button as image and give it an id to add listeners later in your code.

Here is how it should look:

 

image

 

Overlap2D Part DONE!


Looks pretty good! Both scenes are set up and ready for some coding. Most importantly your scene and UI are very flexible and code-independent.

 

 

 

Setting up LibGDX Project and Getting Ready to Code:

 

Download the LibGDX setup application and setup an empty project (make sure to have checked box2d and freetype fonts), import it to your eclipse or other IDE, run the desktop app to make sure it works, then strip it from LibGDX picture  and make it just a black screen.

Make sure your desktop launcher has code to specify screen size of 480×800 (config variables in launcher).

We are going to render everything using a simple LibGDX Stage, so the code structure will be the following: GameStage extending Stage that will load all the Overlap2D data and display it; Main class that will be rendering GameStage in its render method.

For all the rest we are going to use iScript functionality of Overlap2D. Let me go a bit in depth for that. Overlap2D runtime provides an interface that you can implement to create a “brain” of any composite item. Basically it is your way of attaching logic to an object. In this example we are going to create 3 iScripts, one is going to get attached to entire mainScreen and be a GameScreenScript with all game logic, other will be MenuScreenScript for menu, and the last one will be BirdScript that we will attach to the bird. It is important to mention that you cannot attach script to an image, or sprite animation, so I have converted bird animation to composite item in order to do it (right click, convert into composite).

 

You can find the full project on my github:  https://github.com/azakhary/OverlappyBird

Here are the well commented sources:

 

OverlappyBird.java

package com.underwater.demo.overflappy;    import com.badlogic.gdx.ApplicationAdapter;  import com.badlogic.gdx.Gdx;  import com.badlogic.gdx.graphics.GL20;  import com.badlogic.gdx.graphics.Texture;  import com.badlogic.gdx.graphics.g2d.SpriteBatch;    /*   * This is our ApplicationListener the main thing that things start in libGDX   */  public class OverflappyBird extends ApplicationAdapter {          /*      * GameStage will be holding both menu and main game      */     private GameStage stage;          @Override     public void create () {        stage = new GameStage();     }       @Override     public void render () {        // Clearing the screen before each render        Gdx.gl.glClearColor(0, 0, 0, 1);        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);                // calling stage act method and passing delta time passed since last call        stage.act(Gdx.graphics.getDeltaTime());        // drawing all actors        stage.draw();             }  }

GameStage.java

package com.underwater.demo.overflappy;    import java.io.File;    import com.badlogic.gdx.Gdx;  import com.badlogic.gdx.scenes.scene2d.Stage;  import com.uwsoft.editor.renderer.DefaultAssetManager;  import com.uwsoft.editor.renderer.SceneLoader;    /*   * GameStage    */  public class GameStage extends Stage {          // Speed of pixels per second of how fast things move left (required both for                                                                  menu and the game,                                                                 thus put here)     public float gameSpeed = 200;       // Overlap2D  provides this easy asset manager that loads things as they are      provided by default when exporting from overlap     private DefaultAssetManager assetManager;          public GameStage() {        super();                // Set this is input processor so all actors would be able to listen to         touch events        Gdx.input.setInputProcessor(this);                // Initializing asset manager        assetManager = new DefaultAssetManager();                // providing the list of sprite animations which is one in this case, to         avoid directory listing coding        assetManager.spriteAnimationNames = new String[1]; assetManager.                                            spriteAnimationNames[0] = "bird";                // loading assets into memory        assetManager.loadData();                // Menu goes first        initMenu();     }          public void initMenu() {        clear();                // Creating Scene loader which can load an Overlap2D scene        SceneLoader menuLoader = new SceneLoader(assetManager);                // loading MenuScene.dt from assets folder        menuLoader.loadScene(Gdx.files.internal("scenes" + File.separator +                              "MenuScene.dt"));                // Initializing iScript MenuSceneScript that will be holding all menu         logic, and passing this stage for later use        MenuScreenScript menuScript = new MenuScreenScript(this);                // adding this script to the root scene of menu which is hold in         menuLoader.sceneActor        menuLoader.sceneActor.addScript(menuScript);                // Adding root actor to stage        addActor(menuLoader.sceneActor);                     }          public void initGame() {        clear();                // Creating Scene loader which can load an Overlap2D scene        SceneLoader mainLoader = new SceneLoader(assetManager);                // loading MainScene.dt from assets folder        mainLoader.loadScene(Gdx.files.internal("scenes" + File.separator +                              "MainScene.dt"));                // Initializing iScript GameSceneScript that will be holding all game, and         passing this stage for later use        GameScreenScript gameScript = new GameScreenScript(this, mainLoader);                // adding this script to the root scene of game which is hold in         mainLoader.sceneActor        mainLoader.sceneActor.addScript(gameScript);                // Adding root actor to stage        addActor(mainLoader.sceneActor);     }    }

GameScreenScript.java

package com.underwater.demo.overflappy;    import java.util.ArrayList;    import com.badlogic.gdx.math.MathUtils;  import com.badlogic.gdx.math.Vector2;  import com.badlogic.gdx.scenes.scene2d.InputEvent;  import com.badlogic.gdx.scenes.scene2d.Touchable;  import com.badlogic.gdx.scenes.scene2d.actions.Actions;  import com.badlogic.gdx.scenes.scene2d.actions.AddAction;  import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;  import com.uwsoft.editor.renderer.SceneLoader;  import com.uwsoft.editor.renderer.actor.CompositeItem;  import com.uwsoft.editor.renderer.actor.LabelItem;  import com.uwsoft.editor.renderer.script.IScript;    /*   * iScript for entire game logic   */  public class GameScreenScript implements IScript {          /*      * reference to GameStage      */     private GameStage stage;          /*      * Main actor that holds root of game screen      */     private CompositeItem game;          // Screen loader reference to be later used to retrieve prefabs from library     private SceneLoader loader;          // Game over Dialog actor     private CompositeItem gameOverDlg;          /*      * this will be holding 2-ground system composite item       */     private CompositeItem groundRotator;          /*      * Instead of holding bird actor we are going to hold birdscript that will       provide all bird logic and methods.      * Also it will have bird actor inside      */     private BirdScript bird;          // Hint Box actor the one that shown in begining of game     private CompositeItem hintBox;          // some helping booleans     private boolean gameStarted = false;     private boolean groundStop = false;          // going to hold what is the possible low and high position for a pipe column     private float minPipe;     private float maxPipe;          private int gameScore = 0;     private LabelItem scoreLbl;          // Going to hold 3 pipes here to reuse as pipe pool     private ArrayList<CompositeItem> pipes = new ArrayList<CompositeItem>();          public GameScreenScript(GameStage stage, SceneLoader loader) {        this.stage = stage;        this.loader = loader;     }          @Override     public void init(CompositeItem gameItem) {        game = gameItem;                gameScore = 0;                // Creating and holding BirdScript that will hold entire bird logic.        bird = new BirdScript(stage);        game.getCompositeById("bird").addScript(bird);                groundRotator = game.getCompositeById("groundRotator");        hintBox = game.getCompositeById("hintBox");        scoreLbl = game.getLabelById("scoreLbl");                // Adding listeners to listen for taps to make bird jump        game.addListener(new ClickListener() {           public boolean touchDown (InputEvent event, float x, float y, int                                      pointer, int button) {              return true;           }           public void touchUp (InputEvent event, float x, float y, int pointer,                                 int button) {              // screen tap was done              screenTap();           }        });                // Loading min/max positions from level editor        minPipe = game.getCompositeById("minPipe").getY();        maxPipe = game.getCompositeById("maxPipe").getY();          // Retrieving 3 pipe columns from library putting them into array,        // and adding on screen in minus coordinates so the will becom "availible"        for(int i = 0;  i < 3; i++) {           CompositeItem pipe = loader.getLibraryAsActor("pipeGroup");                        pipe.setX(-pipe.getWidth());                   game.addItem(pipe);                      pipes.add(pipe);        }                        // Making sure first pipe will be added not sooner then 3 seconds from now        game.addAction(Actions.sequence(Actions.delay(3.0f), Actions.run(new                        Runnable() {                      @Override           public void run() {              putPipe();           }        })));                // hiding game over dialog        gameOverDlg = game.getCompositeById("gameOverDlg");        // it should not listen for taps        gameOverDlg.setTouchable(Touchable.disabled);        gameOverDlg.setVisible(false);     }       @Override     public void act(float delta) {                // if game is not yet started or started (but most importantly not ended,                                                   ground is moving)         if(!groundStop) {           groundRotator.setX(groundRotator.getX() - delta * stage.gameSpeed);                if(groundRotator.getX() < -groundRotator.getWidth()/2) groundRotator.              setX(0);        }                // if game is started, so first tap fone, then we dhould check for         collisions and move pipes        if(gameStarted) {           for(int i = 0; i < pipes.size(); i++) {              // get pipe              CompositeItem pipe = pipes.get(i);                            // move it if it has positive coordinate              if(pipe.getX() > -pipe.getWidth()) {                 // if pipe was right of thebird, and will now become left of the                  bird, add to score                 if(pipe.getX() >= bird.getBirdCenter().x && pipe.getX() - delta *                     stage.gameSpeed < bird.getBirdCenter().x) {                    gameScore++;                 }                 pipe.setX(pipe.getX() - delta * stage.gameSpeed);              }                            //check for collision with bird              collisionCheck();           }        }                // update scorel label        scoreLbl.setText(gameScore+"");     }          /*      * Check for bird versus pipe row collision      */     private void collisionCheck() {        // iterate through all 3 pipes        for(int i = 0; i < pipes.size(); i++) {           CompositeItem pipe = pipes.get(i);                      // to make it easy going to think about bird as circle with 5 radius            Vector2 birdPoint = bird.getBirdCenter();                      // Is there collision? if yes stop the game and allow bird to fall           if(birdPoint.x+5 > pipe.getX() && birdPoint.x - 5 < pipe.getX() + pipe.              getWidth() && (pipe.getY() + 532 > birdPoint.y - 5 || pipe.getY()+              701 < birdPoint.y + 5)) {              stopGame();            }                      // Did bird hit the ground? not only stop the game but also            // disable gravity to keep from further falling, and consider bird dead            ( animations stop )           if(birdPoint.y-5 < groundRotator.getY()+groundRotator.getHeight()) {              if(!groundStop) {                 stopGame();              }              bird.disableGravity();              bird.getBird().setY(groundRotator.getY()+groundRotator.getHeight()+5)                           ;              // killitwithfire              bird.die();           }        }     }          /*      * Stops the game      */     private void stopGame() {        gameStarted = false;        groundStop = true;        game.clearActions();                // show end game dialog        showEndDialog();     }          /*      * showing end game dialog      */     public void showEndDialog() {        // enabling touch back, showing        gameOverDlg.setTouchable(Touchable.enabled);        gameOverDlg.setVisible(true);        // setting transparency to full        gameOverDlg.getColor().a = 0;        // and fading it in        gameOverDlg.addAction(Actions.fadeIn(0.4f));                // setting play button listener to replay the game        gameOverDlg.getImageById("playBtn").addListener(new ClickListener() {           public boolean touchDown (InputEvent event, float x, float y, int                                      pointer, int button) {              return true;           }           public void touchUp (InputEvent event, float x, float y, int pointer,                                 int button) {              stage.initGame();           }        });     }          /*      * Called when screen is tapped      */     private void screenTap() {        // if ground is not moving then bird is dead no actin required on tapp        if(groundStop) return;                // if game started just jump the bird        if(gameStarted) {           bird.jump();        } else {           // if game is not yet started, start the game and jump the bird           gameStarted = true;           hintBox.addAction(Actions.fadeOut(0.3f));           // and also enable gravity from now on           bird.enableGravity();           bird.jump();        }     }          /*      * get's availible pipe      * availible pipe is any pipe that is left of screen and not visible      */     public CompositeItem getAvailablePipe() {        for(int i = 0; i < pipes.size(); i++) {           if(pipes.get(i).getX() <= -pipes.get(i).getWidth()) {              return pipes.get(i);           }        }                return null;     }          /*      * this is called every X time to put a new pipe on the right      */     public void putPipe() {         // getting availible pipe        CompositeItem pipe = getAvailablePipe();                // when you die at bad moment, it can be null sometimes        if(pipe == null) return;                // put pipe column on the random hight from min to max range        pipe.setX(stage.getWidth());        pipe.setY(MathUtils.random(minPipe, maxPipe));                // schedule next pipe to be put in 1.3 seconds        game.addAction(Actions.sequence(Actions.delay(1.3f), Actions.run(new                        Runnable() {                      @Override           public void run() {              // call itself              putPipe();           }        })));     }    }

 

MenuScreenScript.java

 

package com.underwater.demo.overflappy;    import com.badlogic.gdx.math.MathUtils;  import com.badlogic.gdx.scenes.scene2d.InputEvent;  import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;  import com.uwsoft.editor.renderer.actor.CompositeItem;  import com.uwsoft.editor.renderer.actor.ImageItem;  import com.uwsoft.editor.renderer.actor.SpriteAnimation;  import com.uwsoft.editor.renderer.script.IScript;    /*   * iScript for menu logic   */  public class MenuScreenScript implements IScript {          /*      * reference to GameStage      */     private GameStage stage;          /*      * this is the main root menu actor to work with      */     private CompositeItem menu;          /*      * this will be holding 2-ground system composite item       */     private CompositeItem groundRotator;          /*      * this will be the bird sprite animation displayed in center of screen      */     private SpriteAnimation bird;          // this variables are used to wiggle bird up and down with sin function     private float iterator = 0;     private float birdInitialPos;          public MenuScreenScript(GameStage stage) {        this.stage = stage;     }       public void init(CompositeItem menuItem) {        menu = menuItem;                // Finding playButton by id and storing in variable        ImageItem playBtn = menuItem.getImageById("playBtn");                // Finding ground composite and storing in variable         groundRotator = menuItem.getCompositeById("groundRotator");                // Finding bird and storing in variable        bird = menuItem.getSpriteAnimationById("bird");                // let's remember where bird was initially        birdInitialPos = bird.getY();                // Adding a Click listener to playButton so we can start game when clicked        playBtn.addListener(new ClickListener() {           // Need to keep touch down in order for touch up to work normal (libGDX                                                                             awkward                                                                            ness)           public boolean touchDown (InputEvent event, float x, float y, int                                      pointer, int button) {              return true;           }           public void touchUp (InputEvent event, float x, float y, int pointer,                                 int button) {              // when finger is up, ask stage to load the game              stage.initGame();           }        });     }          /*      * This is called every frame      */     public void act(float delta) {        // moving ground left with game speed multiplied by delta as delta shows         what part of second was passed since last call        groundRotator.setX(groundRotator.getX() - delta * stage.gameSpeed);                     // if ground rotator got half way left, we can just put it back to 0, and         to eye it will look like it endlessly moves        if(groundRotator.getX() < -groundRotator.getWidth()/2) groundRotator.setX(           0);                // Now this part is to wiggle bird up and down, we are going change         iterator based on time passed        iterator += delta*400;                // Then figure out the bird offset from it's original position based on         iterator which is based on time passed, and do it with sinus function        float birdOffset = MathUtils.sinDeg(iterator)*5;                // put bird on it's original pos + offset        bird.setY(birdInitialPos + birdOffset);      }       }

 

BirdScript.java

 

package com.underwater.demo.overflappy;    import com.badlogic.gdx.math.MathUtils;  import com.badlogic.gdx.math.Vector2;  import com.uwsoft.editor.renderer.actor.CompositeItem;  import com.uwsoft.editor.renderer.actor.SpriteAnimation;  import com.uwsoft.editor.renderer.script.IScript;    /**   * Bird Script   * @author azakhary   * This is brain of the bird, it's physics and everything   */  public class BirdScript implements IScript {       /*      * reference to GameStage      */     private GameStage stage;          // Bird composite item actor     private CompositeItem bird;          // Inside bird composite actor there is the bird sprite animation actor     private SpriteAnimation birdAnimation;          // used to wiggle the bird in the air using Sine function     private float iterator = 0;          // current vertical velocity of the bird     private float currVerticalVelocity = 0;          // boolean to know if gravity is enabled or not     private boolean isGravityEnabled = false;          // to avoid jumping to rotation bird will try to always rotate a bit towards      desired rotation     private float desiredRotation;          // is it alive?     private boolean isAlive = true;               public BirdScript(GameStage stage) {        this.stage = stage;     }          @Override     public void init(CompositeItem item) {        bird = item;                // find animation from the composite        birdAnimation = bird.getSpriteAnimationById("birdAnimation");                // set origin of the bird in it's center, so it will rotate normally        bird.setOrigin(bird.getWidth()/2, bird.getHeight()/2);                // set desired rotation to current rotation which is 0        desiredRotation = bird.getRotation();     }       @Override     public void act(float delta) {                if(!isGravityEnabled && isAlive) {           // Wiggling when no gravity only           iterator += delta*400;           float birdOffset = MathUtils.sinDeg(iterator)*5;           birdAnimation.setY(birdOffset);        }                // aplying gravity every frame        gravity(delta);                // moving to new position based on current vertical velocity        bird.setY(bird.getY() + delta*currVerticalVelocity);                // manage bird rotation based on it's vertical speed        manageRotation(delta);             }          /*      * manage bird rotation based on it's vertical speed      * this is a part of code that is not interesting boring and whatever..      */     private void manageRotation(float delta) {        if(isGravityEnabled) {           if(currVerticalVelocity > -200) {              float rotation = currVerticalVelocity+200;              desiredRotation = rotation/15f;           }           if(currVerticalVelocity <= -200) {              float rotation = currVerticalVelocity+200;              if(rotation < -400) rotation = -400;              desiredRotation = rotation/4.4f;           }                      if(desiredRotation != bird.getRotation()) {              if(desiredRotation > bird.getRotation()) {                 bird.setRotation(bird.getRotation() + 900*delta);                 if(desiredRotation < bird.getRotation()) bird.setRotation(                    desiredRotation);              }              if(desiredRotation < bird.getRotation()) {                 bird.setRotation(bird.getRotation() - 900*delta);                 if(desiredRotation > bird.getRotation()) bird.setRotation(                    desiredRotation);              }           }        }     }          public void enableGravity() {        isGravityEnabled = true;     }          public void disableGravity() {        isGravityEnabled = false;     }          public void jump() {        // if bird is dead do not jump (I think I checked it somewhere already)        if(!isAlive) return;                // if bird is higher then screen then do not jump        if(bird.getY() > stage.getHeight()) return;                // if jumped get the custom variable jump_speed from bird actor and set it         as current vertical velocity        currVerticalVelocity = bird.getCustomVariables().getFloatVariable(                               "jump_speed");     }          /*      * Apply gravity each frame (get's delta time since last frame)      */     private void gravity(float delta) {        if(isGravityEnabled) {           // change curernt velocity based on gravity (gravity changes velocity                                                         every second by gravity                                                         amount)           currVerticalVelocity -= delta*bird.getCustomVariables().                                   getFloatVariable("gravity");        }     }          public CompositeItem getBird() {        return bird;     }          // get's the bird center coordinates as vector 2 needed for collision      detection in GameScreenScript     public Vector2 getBirdCenter() {        Vector2 vec = new Vector2(bird.getX() + bird.getWidth()/2, bird.getY() +                       bird.getHeight()/2);        return vec;     }          // Kills the bird, for reals     public void die() {        currVerticalVelocity = 0;          isAlive = false;        desiredRotation = 0;         bird.setRotation(0);        birdAnimation.pause();     }    }

Programming Design


Scroll to Top