http://www.gamefromscratch.com/post/2014/09/08/Guest-Tutorial-Making-Flappy-Bird-using-Overlap2D-and-LibGDX.aspx
github:https://github.com/azakhary/OverlappyBird
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:
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.
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.
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.
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)
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:
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 480x800 (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(); } }