This material is based on the Youtube tutorial: How to make Games: Flappy Bird by @BrentAureli
- Open the libGDX wizard tool (gdx-setup.jar) and generate a new project with the following attributes:
Name:FlappyDemo
Package:com.missionbit.game
Game class:FlappyDemo
Destination:/Users/missionbit/Desktop/Projects/FlappyDemo
Android SDK:/Users/missionbit/Library/Android/sdk
Sub Projects: SelectDesktop
andAndroid
Extensions: SelectBox2d
Advanced: SelectIDEA
and clickSave
Hint: The Java convention is to use all lowercase for package names, but class names are always capitalized
- Click
Generate
and wait until the console says “BUILD SUCCESSFUL” - Close the window and open Android Studio.
Import project
>>/Users/missionbit/Desktop/Projects/FlappyDemo
>>OK
- Navigate to
Edit Configuration
>> Click the+
button >> SelectApplication
>> Fill out the following fields:
Name:Desktop
Main class:com.missionbit.game.desktop.DesktopLauncher
Working directiory:/Users/missionbit/Desktop/Projects/FlappyDemo/android/assets
Use classpath of module: Selectdesktop
- Click
Apply
and thenOK
.
- Navigate to
core/src/com.missionbit.game
>> Right-click (control + click) and selectNew
>Package
.
Name:states
>> OK
Hint: Don't forget that we use lowercase for package names
- Right-click the newly created states package and select
New
>Java Class
Name:State
>> OK
Hint: Don't forget that we capitalize class names
- Make the State class abstract
- Each state needs: a camera to locate a position in the game world, a mouse or some sort of pointer to indicate where the user clicks, which is defined as a Vector3, ie, a (x,y) coordinate, and a GameStateManager to manage our state transitions (for example, from menu to play, or from play to pause).
So we must declare the following State instance variables:
protected OrthographicCamera cam;
protected Vector3 mouse;
protected GameStateManager gsm;
- Create a protected State constructor that takes in a GameStateManager and initializes all instance variables.
- Create the following methods:
protected abstract void handleInput();
public abstract void update(float dt);
public abstract void render(SpriteBatch sb);
public abstract void dispose();
Hint: It’s important to dispose of any Texture or other media when we’re done using them to prevent any kind of memory leaks.
- Navigate to the FlappyBird class, and declare the following instance variables:
public static final int WIDTH = 480;
public static final int HEIGHT = 800;
public static final String TITLE = "Flappy Bird";
private GameStateManager gsm;
private SpriteBatch batch;
Hint: We should only have one SpriteBatch in the entire game. They are pretty heavy.
- Now navigate to
desktop/src/com.missionbit.game.desktop
, openDesktopLauncher
, and insert these 3 lines inside the main method, right in the middle, in between the two lines of code (one that declares and initializes config, and the other that instantiates aLwjglApplication
):
config.width = FlappyDemo.WIDTH;
config.height = FlappyDemo.HEIGHT;
config.title = FlappyDemo.TITLE;
- Create a new Java class inside of the states package named
GameStateManager
with the following code:
public class GameStateManager {
private Stack<State> states;
public GameStateManager() {
states = new Stack<State>();
}
public void push(State state) {
states.push(state);
}
public void pop() {
states.pop();
}
public void set(State state) {
states.pop();
states.push(state);
}
public void update(float dt) {
states.peek().update(dt);
}
public void render(SpriteBatch sb) {
states.peek().render(sb);
}
}
- Create a new Java class inside of the states package named
MenuState
, which extendsState
. - Right-click MenuState and select
Generate
>Implement Methods…
- Select
MenuState
,handleInput
,update
,render
, anddispose
and click OK. Then make sure all methods are public.
Hint: If there are any class names in red, option+click them to import the necessary packages.
- In the
FlappyDemo
class, initialize and push the GameStateManager inside thecreate()
method. Your code should look like this:
@Override
public void create () {
batch = new SpriteBatch();
gsm = new GameStateManager();
gsm.push(new MenuState(gsm));
Gdx.gl.glClearColor(0, 0, 0, 1);
}
- In the
render()
method, update and render yourgsm
. Your code should look like this:
@Override
public void render () {
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
gsm.update(Gdx.graphics.getDeltaTime());
gsm.render(batch);
}
- Finally, make sure to dispose of your batch:
@Override
public void dispose () {
batch.dispose();
}
- Run (you should see a black screen)
- Go to https://github.com/BrentAureli/FlappyDemo and download the repo. Unzip it and copy the images from android > assets to the android > assets in your project
- Inside
MenuState
, create the instance variables:
private Texture background;
private Texture playBtn;
- Option+click on Texture to import class. In the constructor, initialize them to:
background = new Texture("bg.png");
playBtn = new Texture("playBtn.png");
- Stil in MenuState, inside of the render method, create a batch to draw the background and play button images:
sb.begin();
sb.draw(background, 0, 0, FlappyDemo.WIDTH, FlappyDemo.HEIGHT);
sb.draw(playBtn, (FlappyDemo.WIDTH / 2) - (playBtn.getWidth() / 2), FlappyDemo.HEIGHT / 2);
sb.end();
Hint: Think of the sprite batch as a container, it needs to be opened and closed.
Also, notice the code for centering the play button. If we just divide it by 2, the bottom left corner of the button would be at the center, so we need to offset it by half of its width.
- Inside the dispose method, dispose of the background and play button:
background.dispose();
playBtn.dispose();
- Run.
- Create a new Java class named "PlayState" inside the package states.
- PlayState extends State
- Right-click the class name and select Generate > Implement methods...
- Right-click the class name and select Generate > Constructor
- In the PlayState class, create an instance variable named bird of type Texture and initialize it with "bird.png", and draw the bird at position (50,50)
- In the MenuState class, the update method checks if user did something by calling the handleInput method
- The handleInput uses Gdx.input.justTouched() to check for user input, and, if the user did something, it calls gsm.set(new PlayState(gsm)) and dispose()
- Run. Now the button should be clickable and, when clicked, the little bird should appear at the bottom left corner. Notice that the bird appears very small. We can use our camera to set a viewport, so we see only a small portion of our game world. It'll appear as if the image was zoomed in.
- In our PlayState constructor, set our orthographic camera (which was created in State) to the bottom left quarter of the screen, with the y-axis pointing up.
Hint: the libGDX OrthographicCamera has a method setToOrtho that takes 3 parameters: (boolean yDown, float viewportWidth, float viewportHeight). This "sets this camera to an orthographic projection, centered at (viewportWidth/2, viewportHeight/2), with the y-axis pointing up or down." --Check out the documentation
- In our PlayState render method, tell the SpriteBatch to set the projection matrix to the camera viewport
Hint: the libGDX SpriteBatch has a setProjectionMatrix method that takes in a Matrix4 projection and "Sets the projection matrix to be used by this Batch" --Check out the documentation
Hint 2: the libGDX OrthographicCamera has an instance variable "combined" of the type Matrix4, which represents "the combined projection and view matrix" --Check out the documentation
- Run. You should see the screen zoomed in on the bottom left quarter
- Create a new package called sprites, and inside it a new Java class named "Bird"
- Our Bird class needs the following attributes:
- a position, to determine where the bird is in the game world
- a texture, to determine the visual representation of the bird, ie, what will be drawn on the screen
- a velocity, to determine in which direction the bird moves (up, down, left, right)
- a gravity, to determine the acceleration pulling the bird down (so he falls when not flying) Declare these instance variables as:
private Vector3 position;
private Vector3 velocity;
private Texture bird;
private static final int GRAVITY = -15;
- Setup the Bird constructor, which takes a x and a y as integers:
public Bird(int x, int y){
position = new Vector3(x, y, 0);
velocity = new Vector3(0, 0, 0);
bird = new Texture("bird.png");
}
- Create a method update to recalculate the bird's position in our game world. Note that we need to scale the velocity by the change in time (represented by
dt
) before adding it to our position.
public void update(float dt){
velocity.add(0, GRAVITY, 0);
velocity.scl(dt);
position.add(0, velocity.y, 0);
velocity.scl(1 / dt);
}
-
Right-click on the class name (
Bird
) andGenerate...
>Getter
> Selectposition
andbird
> ClickOK
. RefactorgetBird()
togetTexture()
. -
Navigate to the
PlayState
class and remove the bird Texture.
- Recreate bird as an instance variable of the type Bird.
- Initialize bird in the position (50, 100).
- Inside the
update()
method, handle user input and update the bird's position.
@Override
public void update(float dt) {
handleInput();
bird.update(dt);
}
- Change the render method so it draws the updated bird.
sb.draw(bird.getTexture(), bird.getPosition().x, bird.getPosition().y);
- Run. You should see the bird falling down. Try changing its initial coordinates to see it falling from different areas on the screen.
-
Inside of the Bird class, create a public void method called
jump
. This method sets thevelocity.y
to 250. -
In the PlayState class, use the
handleInput
method to make the bird jump whenever the user clicks the screen.
Hint: Use
Gdx.input.justTouched()
to check for user input.
-
Still in the PlayState, create a private instance variable for the background and call it bg. Initialize it to
bg.png
. -
In the render method, draw the background.
Hint: The coordinates for the background are ((cam.position.x - cam.viewportWidth / 2), 0). Why?
-
Test it.
-
Make it so the game stops if the bird hits the ground (aka as the bottom of the screen). To do that, edit the method update. When the
position.y
goes below zero, set it to zero (remember if statements). In addition, only addGRAVITY
if the bird is not already at zero. -
Test it.
-
Inside the sprites package, create a class Tube.
-
Tube needs two Textures, topTube and bottomTube.
Hint: Instance variables should be private.
- Create a constructor that takes in a
float x
indicating the position where the tube should start.
Hint: Take a look at our image assets and figure out which one we should use to initialize our tubes.
- Tube needs a few more instance variables:
private static final int TUBE_GAP = 100; //opening between tubes
private static final int LOWEST_OPENING = 120; //lowest position the top of the bottom tube can be, must be above 90 to be above ground level
private static final int FLUCTUATION = 130; //may adjust to keep top tube in view
private Vector2 posTopTube, posBottomTube;
private Random rand;
- Initialize the tubes position using Random.
rand = new Random();
posTopTube = new Vector2(x, rand.nextInt(FLUCTUATION) + LOWEST_OPENING + TUBE_GAP);
posBottomTube = new Vector2(x, posTopTube.y - TUBE_GAP - bottomTube.getHeight());
-
Generate Getters for top/bottom tubes and their respective positions.
-
Inside the PlayState, create a tube and initialize its position on the x-axis to 100.
-
Draw the tubes.
Hint: Take a look at how we drew the bird.
- Run it multiple times. What happens to the tube position?
- In the Tube class, create a
reposition
method that takes in afloat x
and sets the positions of the top and bottom tubes.
Hint: You can use the same positions used to initialize posTopTube and posBotTube since they were positioned randomly.
Hint: Vector2 has aset
method.
- In the PlayState class, create the following constants:
private static final int TUBE_SPACING = 125;
private static final int TUBE_COUNT = 4;
-
Replace the tube in the PlayState class with an array of Tubes (
Array<Tube> tubes
). -
Initialize the array and add
TUBE_COUNT
tubes usingtubes.add(new Tube(i * (TUBE_SPACING + Tube.TUBE_WIDTH)));
-
In the Tube class, create a public constant for the TUBE_WIDTH and set it to 52.
-
In the PlayState class, inside the update method, create the logic to reposition tubes when they get out of the camera viewport. Note that you will have to check it for each tube.
if(cam.position.x - cam.viewportWidth / 2 > tube.getPosTopTube().x + tube.getTopTube().getWidth()){
tube.reposition(tube.getPosTopTube().x +((Tube.TUBE_WIDTH + TUBE_SPACING) * TUBE_COUNT));
}
-
In the
render
method, remember to render all the tubes instead of just one. -
In the Bird class, create a constant
MOVEMENT
and set it to 100. This represents the horizontal movement of the bird. -
In the update method, make sure the position on the x axis is also updated by passing in
MOVEMENT * dt
as a paramenter to the x argument. -
In the PlayState class, update the camera's position in the game world based on the position of the bird. To do that, inside the update method set the
cam.position.x
equal tobird.getPosition().x + 80
. -
At the end of the update method, tell libGDX that the camera has been repositioned by calling
cam.update();
.
-
To handle collisions, we'll draw rectangles around the tubes. In the class
Tube
, declare two objects of typeRectangle
:boundsTop
andboundsBot
. -
Initialize them in the constructor.
Hint: The constructor for Rectangle takes in 4 parameters: the x coordinate, the y coordinate, the width, and the height. Since we want these rectangles to completely cover the tubes, how can we use instance variables and attributes from the Tube class to pass in the desired parameters for each rectangle?
- Remember to also reposition the rectangles in the
reposition
method.
Hint: Rectangle has a
setPosition(float x, float y)
method.
-
Create a
collides
method that takes in aRectangle player
and returns true if the player overlaps theboundsTop
orboundsBot
, but false otherwise. -
Now we need to create a
Rectangle bounds
for the bird. Don't forget to initialize it. -
Every time the bird moves we need to update its bounds. Use the
setPosition(float x, float y)
to update the bounds whenever the bird moves. -
Create a getter method for
bounds
. -
In the
PlayState
class, we already have afor loop
that cycles through ourtubes
inside theupdate
method. We can use that samefor loop
to check if thebird
collides with anytube
. If there is a collision, callgsm.set(new PlayState(gsm))
andbreak
out of the loop.
-
In the
GameStateManager
, every time wepop
a state we shoulddispose
of it since we can't reuse states. Note thatstates.pop()
is called twice in this class. -
In the
MenuState
class, remove the call todispose
from thehandleInput
method because now we're disposing of states when wepop
them off. What happens when the methoddispose
from theMenuState
class gets called? -
In the
PlayState
class, we need to dispose ofbg
,bird
, and all of thetubes
. Note that you'll have to implement thedispose
methods for theBird
andTube
classes. -
Add log messages to the
dispose
methods in PlayState and MenuState. -
Run. You should see the log messages every times the game restarts.
-
The ground will be created the same way as the tubes: it'll be declared as a Texture that gets repositioned every time it goes off of the screen. Create
ground
as an instance variable of the classPlayState
, and initialize it usingground.png
. -
Create
groundPos1
andgroundPos2
as instance variables of typeVector2
. InitializegroundPos1
to the positions of the bottom left corner of our cameracam
. InitializegroundPos2
to the position directly to the right ofgroundPos1
. -
Draw the
ground
twice, at positionsgroundPos1
andgroundPos2
. -
Run. What do you notice?
-
Create a constant
GROUND_Y_OFFSET
that equals to -50, and change the initial y value ofgroundPos1
andgroundPos2
to this constant. -
Create a method
updateGround()
that checks if each of the ground right edges is to the left of the left edge of the viewport, ie, it checks if each of the ground projections are out of the camera's field of vision. If it is, use the methodadd(float x, float y)
from theVector2
class to update the ground position, ie,groundPos1
orgroundPos2
. -
Make sure to call
updateGround()
in theupdate
method. -
Kill the bird if it hits the ground. You can do that by checking if the bird's position on the y axis is less than or equal to the y position of the top edge of the ground.
Hint: The top edge of the ground is the ground's height + offset.
Hint: To kill the bird, we do the same as we did when the bird collides with a tube: restart the game.
-
Make sure to
dispose
of the ground Texture. -
Try it out!
-
Inside of the
sprites
package, create a new classAnimation
. -
The
Animation
class needs the following attributes:
Array<TextureRegion> frames; //where we store all of our frames
float maxFrameTime; //this determines how long a frame needs to stay in view before switching to the next one
float currentFrameTime; //how long the animation has been in the current frame
int frameCount; //number of frames in our animation
int frame; //the current frame we're in
Hint: Are these
private
,protected
, orpublic
?
- Now we need to initialize these instance variables in the constructor:
public Animation(TextureRegion region, int frameCount, float cycleTime){
frames = new Array<TextureRegion>();
int frameWidth = region.getRegionWidth() / frameCount;
for(int i = 0; i < frameCount; i++){
frames.add(new TextureRegion(region, i * frameWidth, 0, frameWidth, region.getRegionHeight()));
}
this.frameCount = frameCount;
maxFrameTime = cycleTime / frameCount;
frame = 0;
}
- We also need an
update
method to cycle through the frames.
public void update(float dt){
currentFrameTime += dt;
if(currentFrameTime > maxFrameTime){
frame++;
currentFrameTime = 0;
}
if(frame >= frameCount)
frame = 0;
}
- Finally, we need a getter for the frames.
public TextureRegion getFrame(){
return frames.get(frame);
}
-
In the
Bird
class, we need to create abirdAnimation
, which will be initialized withbirdanimation.png
, a frame count of 3, and a cycle time of 0.5f. -
Now that we have a
birdAnimation
, we need to get rid ofbird
and replace it with the newtexture
, but remember this one is 3 times as wide. -
In
update
, update thebirdAnimation
and don't forget to pass in the delta time. -
In
getTexture
, now we need to returnbirdAnimation.getFrame()
instead. -
Also update the
dispose
method to dispose of the newtexture
instead ofbird
.
-
Find the music and sound effect files in our assets folder.
-
Inside
FlappyDemo
, create amusic
instance variable of the typeMusic
and initialize it toGdx.audio.newMusic(Gdx.files.internal("music.mp3"))
-
inside the constructor, set the configurations for
music
as follows:
music.setLooping(true);
music.setVolume(0.1f);
music.play();
-
Create a dispose method (Generate > Override) and dispose of
music
. -
To create the sound effect, go to the
Bird
class and create an instance variableSound flap
, which will be initialized toGdx.audio.newSound(Gdx.files.internal("sfx_wing.ogg"))
-
Inside the
jump
method, callflap.play()
. To change the volume of the sound effect, pass in a float, say, 0.5f. -
Dispose of
flap
.
-
Go to android > AndroidManifest.xml and change the screenOrientation to portrait.
-
Copy the
cam.setToOrtho...
from thePlayState
to theMenuState
. -
Copy the
sb.setProjectionMatrix...
from thePlayState
to theMenuState
. -
Update the following lines in the
MenuState
:
sb.draw(background, 0,0);
sb.draw(playBtn, cam.position.x - playBtn.getWidth() / 2, cam.position.y);
-
Instead of creating a
Texture
for each tube, reuse the same 2 Textures by making them static. -
Notice that the game doesn't seem to detect collision sometimes when the bird hits the top tube. Fix it.
-
When restarting the game, go to the Menu screen instead of just starting over, or create a "try again" screen.
-
Count scores.
-
Create different levels of difficulty.
-
Make the bird flip over when falling to the ground.
-
When the user hits "play", make the bird only start flying after the user taps the screen for the first time. Indicate what needs to be done (ie, give instructions on how to play).