Introduction: Java Game Programming Tutorial - Flappy Bird Redux

Picture of Java Game Programming Tutorial - Flappy Bird Redux

In this tutorial, I will demonstrate how to make a basic 2D game in Java by writing a basic Flappy Bird game. The time it takes you to complete the tutorial is almost wholly dependent on your Java skill-level. My goal is 1-2 hours for you to accomplish at an intermediate skill level. If you're a beginner, don't be alarmed! I provide all code you need alongside conceptual explanations to guide you through this process.

I hope to show how simple a basic game is to make when you have a few hours on your hands. I go into every programming project with the desire to learn something new - this project taught me a few tricks. I hope it does the same for you.

With that being said, let's dive right in, starting with a demo of the game you will make! (without the music, of course) You can download the .jar file (and project source code) at the end of this step to play with.

Step 1: Concept Building / Graphic Design

Picture of Concept Building / Graphic Design

This is always the first stage of building any game. Here you make sketches and draft ideas on your game's functionality. Never start a game by programming. Your code will be written and rewritten wasting a significant amount of time. Take time to put together a "95% model," which has everything you think your game will need on the conceptual level. You will inevitably think of added functionality while programming, so have the vast majority of the concept finalized beforehand.

Because my game idea was remaking the popular Flappy Bird, this stage was limited to designing the graphics to be used in the program. I designed a static blue bird and a pipe for the obstacle. I rotated the pipe 180 degrees and used two separate images for the top and bottom pipes. All three images are found above and they should be named as follows:

blue_bird.png
tube_bottom.png
tube_top.png

I wouldn't grab the step's images above for your program. Instead, extract the images from the .zip I include below this step to ensure you have exactly what is needed. You should place the images in a folder called "resources" which you will place under the bin folder in your program's files. This is only necessary based on the code I provide; however you may change the folder name to something of your preference.

I used Photoshop Elements to design the images. One important factor to remember in designing your graphics, should you choose to do so, is to use only interlaced png images and remove the background from your images. This ensures the transparency of everything besides your graphic.

In the concept building phase, you should also get an idea of the GUI layout and general gameplay characteristics of the game you will write. For example, in this game, I envisioned the game to begin on a splash screen. The background would be the same as the game's background (moving pipes from right to left). In the center of the screen will be a button to click when you're ready to play, and every time a round begins, you will fade to and from a black screen. This can all be seen in the demo video I provided in the previous step.

Step 2: Understanding a Game

The last thing we need to do before jumping to the much-anticipated programming bits is take a second to address what you need to have a functioning game.

Your game needs several things to function: the player, the enemies, the obstacles, ability handle user input, and detect collisions. In addition, you need something to create the game loop - a process that periodically updates everything in the game.

Now we must sketch out the classes we expect to require. At a minimum, we will need one class that handles building the GUI, the game clock (the game loop), and game logic (collect and handle user input & collisions). You should also have one class for every unique player, enemy, or obstacle. Unique is important - write one class for the enemy, then create an instance of this class for each enemy needed.

Step 3: The Programming Environment

For the beginners:

Your programming environment is the medium you use to translate your code into something your computer understands. There are a few IDEs (Integrated Development Environments) I'm familiar with. First is BlueJ, which would be my recommendation to use for the newest programmers. After you are comfortable with programming, I'd start using Eclipse or NetBeans - they have handy features like auto-completion. As a side note for these two: you want the SE version not the EE version. EE is for web developers.

Resources on the respective download pages will describe what you need to do to start programming with their IDE. In this 'ible, I will be using the newest version of Eclipse (Mars).

Additionally, you will need to download the most recent JDK (Java development kit) from Oracle. This is the bread and butter of developing anything in Java.

Step 4: Starting Simple

Now we will start programming! *and there was much rejoicing* (sorry for the cheesy Monty Python humor)

We will start by building the primary class, which I called TopClass, and we will build just the skeleton as you see below. All this does so far is create a full-screen frame with no content.

The main method simply creates a new thread from which the GUI-building and general game function operates. You need to run your game in another thread to allow the GUI to stay functional. If you didn't do this, the game loop would lock up the interface, not allowing the user to close the program while playing the game.

The comments should explain the rest of the code.

import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import javax.swing.*;

public class TopClass {
	//global constant variables
	private static final int SCREEN_WIDTH = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth();
        private static final int SCREEN_HEIGHT = (int) Toolkit.getDefaultToolkit().getScreenSize().getHeight();
        //global variables
	//global swing objects
	private JFrame f = new JFrame("Flappy Bird Redux");
	
	//other global objects
	private static TopClass tc = new TopClass();
	
	/**
	 * Default constructor
	 */
	public TopClass() {
	}
	
	/**
	 * Main executable method invoked when running .jar file
	 * @param args
	 */
	public static void main(String[] args) {
		//build the GUI on a new thread
		
		javax.swing.SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				tc.buildFrame();
				//create a new thread to keep the GUI responsive while the game runs
				Thread t = new Thread() {
					public void run() {
						//in here we will call a function to start the game
					}
				};
				t.start();
			}
		});
	}
	
	/**
	 * Method to construct the JFrame and add the program content
	 */
	private void buildFrame() {
	        Image icon = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("resources/blue_bird.png"));
	        f.setContentPane(createContentPane()); //adds the main content to the frame
                f.setResizable(true); //true, but game will not function properly unless maximized!
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                f.setAlwaysOnTop(false);
                f.setVisible(true);
                f.setMinimumSize(new Dimension(SCREEN_WIDTH*1/4, SCREEN_HEIGHT*1/4)); //set to prevent collapse to tiny window upon resizing
                f.setExtendedState(JFrame.MAXIMIZED_BOTH); //maximize the JFrame
                f.setIconImage(icon); //set the icon
	}
	
	private JPanel createContentPane() {
		topPanel = new JPanel(); //top-most JPanel in layout hierarchy
		return topPanel; //return a blank panel
	}
}

Step 5: Player and Obstacle Classes

Since we're starting with the skeleton of classes, we now advance to writing the player and obstacle classes. By default, we must know that these classes serve a basic purpose: load the resource image used by the object, scale the image to the desired dimensions, get and set the X and Y coordinates (the upper left-hand corner of the images; xLoc and yLoc), and get the width and height of the object's image.

Because I've already completed this project, I'm going to drop a spoiler on you all: for collision detection, explained in a later step, you will also need a method that returns a rectangle that encompasses the object, as well as a method that returns the object's BufferedImage. Now that you know everything that's required, I will give you the full code for the Bird, BottomPipe, and TopPipe classes. All three classes are identically structured, differing only in naming conventions.

When the classes are first instantiated, the desired width and height of the images are passed to the classes in the constructor, which will automatically scale the image by calling the scale method. Lastly, you'll notice there are getWidth and getHeight methods that contain a try/catch block. This is to help with preventing logic issues in TopClass.

You may notice when you create the BufferedImage object, it is created using the ARGB type. This is based on the desire for an alpha channel to be included. You will see why in the collision detection step.

import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Image;
import java.awt.image.BufferedImage;

public class Bird {
	//global variables
	private Image flappyBird;
	private int xLoc = 0, yLoc = 0;
	
	/**
	 * Default constructor
	 */
	public Bird(int initialWidth, int initialHeight) {
		flappyBird = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("resources/blue_bird.png"));
		scaleBird(initialWidth, initialHeight);
	}
	
	/**
	 * Method to scale the bird sprite into the desired dimensions
	 * @param width The desired width of the flappy bird
	 * @param height The desired height of the flappy bird
	 */
	public void scaleBird(int width, int height) {
		flappyBird = flappyBird.getScaledInstance(width, height, Image.SCALE_SMOOTH);		
	}
	
	/**
	 * Getter method for the flappyBird object.
	 * @return Image
	 */
	public Image getBird() {
		return flappyBird;
	}
	
	/**
	 * Method to obtain the width of the Bird object
	 * @return int
	 */
	public int getWidth() {
		try {
			return flappyBird.getWidth(null);
		}
		catch(Exception e) {
			return -1;
		}
	}
	
	/**
	 * Method to obtain the height of the Bird object
	 * @return int
	 */
	public int getHeight() {
		try {
			return flappyBird.getHeight(null);
		}
		catch(Exception e) {
			return -1;
		}
	}
	
	/**
	 * Method to set the x location of the Bird object
	 * @param x
	 */
	public void setX(int x) {
		xLoc = x;
	}
	
	/**
	 * Method to get the x location of the Bird object
	 * @return int
	 */
	public int getX() {
		return xLoc;
	}
	
	/**
	 * Method to set the y location of the Bird object
	 * @param y
	 */
	public void setY(int y) {
		yLoc = y;
	}
	
	/**
	 * Method to get the y location of the Bird object
	 * @return int
	 */
	public int getY() {
		return yLoc;
	}
	
	/**
	 * Method used to acquire a Rectangle that outlines the Bird's image
	 * @return Rectangle outlining the bird's position on screen
	 */
	public Rectangle getRectangle() {
		return (new Rectangle(xLoc, yLoc, flappyBird.getWidth(null), flappyBird.getHeight(null)));
	}
	
	/**
	 * Method to acquire a BufferedImage that represents the Bird's image object
	 * @return Bird's BufferedImage object
	 */
	public BufferedImage getBI() {
		BufferedImage bi = new BufferedImage(flappyBird.getWidth(null), flappyBird.getHeight(null), BufferedImage.TYPE_INT_ARGB);
		Graphics g = bi.getGraphics();
		g.drawImage(flappyBird, 0, 0, null);
		g.dispose();
		return bi;
	}
}
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Image;
import java.awt.image.BufferedImage;

public class BottomPipe {
	//global variables
	private Image bottomPipe;
	private int xLoc = 0, yLoc = 0;
	
	/**
	 * Default constructor
	 */
	public BottomPipe(int initialWidth, int initialHeight) {
		bottomPipe = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("resources/tube_bottom.png"));
		scaleBottomPipe(initialWidth, initialHeight);
	}
	
	/**
	 * Method to scale the BottomPipe sprite into the desired dimensions
	 * @param width The desired width of the BottomPipe
	 * @param height The desired height of the BottomPipe
	 */
	public void scaleBottomPipe(int width, int height) {
		bottomPipe = bottomPipe.getScaledInstance(width, height, Image.SCALE_SMOOTH);		
	}
	
	/**
	 * Getter method for the BottomPipe object.
	 * @return Image
	 */
	public Image getPipe() {
		return bottomPipe;
	}
	
	/**
	 * Method to obtain the width of the BottomPipe object
	 * @return int
	 */
	public int getWidth() {
		return bottomPipe.getWidth(null);
	}
	
	/**
	 * Method to obtain the height of the BottomPipe object
	 * @return int
	 */
	public int getHeight() {
		return bottomPipe.getHeight(null);
	}
	
	/**
	 * Method to set the x location of the BottomPipe object
	 * @param x
	 */
	public void setX(int x) {
		xLoc = x;
	}
	
	/**
	 * Method to get the x location of the BottomPipe object
	 * @return int
	 */
	public int getX() {
		return xLoc;
	}
	
	/**
	 * Method to set the y location of the BottomPipe object
	 * @param y
	 */
	public void setY(int y) {
		yLoc = y;
	}
	
	/**
	 * Method to get the y location of the BottomPipe object
	 * @return int
	 */
	public int getY() {
		return yLoc;
	}
	
	/**
	 * Method used to acquire a Rectangle that outlines the BottomPipe's image
	 * @return Rectangle outlining the BottomPipe's position on screen
	 */
	public Rectangle getRectangle() {
		return (new Rectangle(xLoc, yLoc, bottomPipe.getWidth(null), bottomPipe.getHeight(null)));
	}
	
	/**
	 * Method to acquire a BufferedImage that represents the TopPipe's image object
	 * @return TopPipe's BufferedImage object
	 */
	public BufferedImage getBI() {
		BufferedImage bi = new BufferedImage(bottomPipe.getWidth(null), bottomPipe.getHeight(null), BufferedImage.TYPE_INT_ARGB);
		Graphics g = bi.getGraphics();
		g.drawImage(bottomPipe, 0, 0, null);
		g.dispose();
		return bi;
	}
}
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Image;
import java.awt.image.BufferedImage;

public class TopPipe {
	//global variables
	private Image topPipe;
	private int xLoc = 0, yLoc = 0;

	/**
	 * Default constructor
	 */
	public TopPipe(int initialWidth, int initialHeight) {
		topPipe = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("resources/tube_top.png"));
		scaleTopPipe(initialWidth, initialHeight);
	}

	/**
	 * Method to scale the topPipe sprite into the desired dimensions
	 * @param width The desired width of the topPipe
	 * @param height The desired height of the topPipe
	 */
	public void scaleTopPipe(int width, int height) {
		topPipe = topPipe.getScaledInstance(width, height, Image.SCALE_SMOOTH);		
	}

	/**
	 * Getter method for the TopPipe object.
	 * @return Image
	 */
	public Image getPipe() {
		return topPipe;
	}

	/**
	 * Method to obtain the width of the TopPipe object
	 * @return int
	 */
	public int getWidth() {
		return topPipe.getWidth(null);
	}

	/**
	 * Method to obtain the height of the TopPipe object
	 * @return int
	 */
	public int getHeight() {
		return topPipe.getHeight(null);
	}

	/**
	 * Method to set the x location of the TopPipe object
	 * @param x
	 */
	public void setX(int x) {
		xLoc = x;
	}

	/**
	 * Method to get the x location of the TopPipe object
	 * @return int
	 */
	public int getX() {
		return xLoc;
	}

	/**
	 * Method to set the y location of the TopPipe object
	 * @param y
	 */
	public void setY(int y) {
		yLoc = y;
	}

	/**
	 * Method to get the y location of the TopPipe object
	 * @return int
	 */
	public int getY() {
		return yLoc;
	}

	/**
	 * Method used to acquire a Rectangle that outlines the TopPipe's image
	 * @return Rectangle outlining the TopPipe's position on screen
	 */
	public Rectangle getRectangle() {
		return (new Rectangle(xLoc, yLoc, topPipe.getWidth(null), topPipe.getHeight(null)));
	}
	
	/**
	 * Method to acquire a BufferedImage that represents the TopPipe's image object
	 * @return TopPipe's BufferedImage object
	 */
	public BufferedImage getBI() {
		BufferedImage bi = new BufferedImage(topPipe.getWidth(null), topPipe.getHeight(null), BufferedImage.TYPE_INT_ARGB);
		Graphics g = bi.getGraphics();
		g.drawImage(topPipe, 0, 0, null);
		g.dispose();
		return bi;
	}
}

Step 6: The Graphics Class

This is the class that puts nearly everything you see in the game on the screen. I will put the skeleton of the class below and explain what's going on.

import javax.swing.*;
import java.awt.Graphics;

public class PlayGameScreen extends JPanel {
	//global variables
	private int screenWidth, screenHeight;
	private boolean isSplash = true;
	/**
	 * Default constructor for the PlayGameScreen class
	 */
	public PlayGameScreen(int screenWidth, int screenHeight, boolean isSplash) {
		this.screenWidth = screenWidth;
		this.screenHeight = screenHeight;
		this.isSplash = isSplash;
	}
	
	/**
	 * Manually control what's drawn on this JPanel by calling the paintComponent method
	 * with a graphics object and painting using that object
	 */
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		//perform drawing operations
	}
}

A few items of note in this code is as follows. Firstly, this particular class extends the JPanel class, meaning this class may be treated as if it is a JPanel itself. As it is a JPanel, it will be added to the JFrame as discussed in the next step.

The screenWidth and screenHeight are passed into this class when instantiated, as well as a boolean that is used to indicate whether the splash screen is the screen currently in use. It's useful to recognize that the splash screen and the game screen differ by only the bird's presence. This means, you may reuse code!

Step 7: Fleshing the Game: I

At this point, all of the classes you need are created; you just need to flesh out TopClass and PlayGameScreen. Starting with TopClass, we will complete the GUI by finishing the createContentPane() method, as well as adding the ActionListener.

In create content pane, we first set the topPanel's background color to black because when we fade the game's screen at the start of a round, it is topPanel that is shown. Next we create an OverlayLayout, which is beneficial because we can make sure the button is on top of the graphic panel, which is not otherwise attainable. Following this, we instantiate the button, a global variable, change a few of its settings, add an ActionListener, then add the button to topPanel. Finally, we instantiate our global PlayGameScreen variable, passing in the screen width, screen height, and a boolean indicating we want the splash screen.

As part of implementing TopClass with ActionListener, we need to create the public method actionPerformed(ActionEvent e). This allows us the ability to process an action that has occurred (i.e. the button being clicked).

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Image;
import java.awt.Color;
import java.awt.LayoutManager;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;

public class TopClass implements ActionListener {
	//global constant variables
	private static final int SCREEN_WIDTH = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth();
	private static final int SCREEN_HEIGHT = (int) Toolkit.getDefaultToolkit().getScreenSize().getHeight();
	
	//global variables
	
	//global swing objects
	private JFrame f = new JFrame("Flappy Bird Redux");
	private JButton startGame;
	private JPanel topPanel; //declared globally to accommodate the repaint operation and allow for removeAll(), etc.
	
	//other global objects
	private static TopClass tc = new TopClass();
	private static PlayGameScreen pgs; //panel that has the moving background at the start of the game
	
	/**
	 * Default constructor
	 */
	public TopClass() {
	}
	
	/**
	 * Main executable method invoked when running .jar file
	 * @param args
	 */
	public static void main(String[] args) {
		//build the GUI on a new thread
		
		javax.swing.SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				tc.buildFrame();
				
				//create a new thread to keep the GUI responsive while the game runs
				Thread t = new Thread() {
					public void run() {
						//run the game here
					}
				};
				t.start();
			}
		});
	}
	
	/**
	 * Method to construct the JFrame and add the program content
	 */
	private void buildFrame() {
		Image icon = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("resources/blue_bird.png"));
		f.setContentPane(createContentPane());
        	f.setResizable(true);
	        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	        f.setAlwaysOnTop(false);
	        f.setVisible(true);
	        f.setMinimumSize(new Dimension(SCREEN_WIDTH*1/4, SCREEN_HEIGHT*1/4));
	        f.setExtendedState(JFrame.MAXIMIZED_BOTH);
	        f.setIconImage(icon);
	        f.addKeyListener(this);
	}
	
	private JPanel createContentPane() {
		topPanel = new JPanel(); //top-most JPanel in layout hierarchy
		topPanel.setBackground(Color.BLACK);
		
		//allow us to layer the panels:
		LayoutManager overlay = new OverlayLayout(topPanel);
		topPanel.setLayout(overlay);
		
		//Start Game JButton
		startGame = new JButton("Start Playing!");
		startGame.setBackground(Color.BLUE);
		startGame.setForeground(Color.WHITE);
		startGame.setFocusable(false);
		startGame.setFont(new Font("Calibri", Font.BOLD, 42));
		startGame.setAlignmentX(0.5f); //center horizontally on-screen
		startGame.setAlignmentY(0.5f); //center vertically on-screen
		startGame.addActionListener(this);
		topPanel.add(startGame);
		
		//must add last to ensure button's visibility
		pgs = new PlayGameScreen(SCREEN_WIDTH, SCREEN_HEIGHT, true); //true --> we want pgs to be the splash screen
		topPanel.add(pgs);
		
		return topPanel;
	}
	
	/**
	 * Implementation for action events
	 */
	public void actionPerformed(ActionEvent e) {
		if(e.getSource() == startGame) {
			//do something
		}
	}
}

Step 8: Fleshing the Game: II

Now it's time to create the moving background that we see on the splash screen. This requires adding the gameScreen method to TopClass, as well as several setter methods in PlayGameScreen.

We begin by discussing the addition of the gameScreen method in TopClass - this is where the game clock resides. First, create two instances of BottomPipe and TopPipe. As soon as one set of pipes exits the screen, they will be repositioned so they come back onscreen. You can set the pipe width and height variables to whatever you want, but I based them on screen size to optimize the game for different screen sizes.

The xLoc1, xLoc2, yLoc1, and yLoc2 variables reference the coordinates of the two BottomPipe objects; the TopPipe objects' locations will be relative to the BottomPipe locations. I created a helper method called bottomPipeLoc() that generates a random integer that will be used for the BottomPipe y coordinate. This number must allow both the TopPipe and BottomPipe objects to remain onscreen.

On the next line of code we create a variable of type long to hold the current system time - this gives us a value for the time the game clock starts. This variable is updated at the end of each iteration of the game clock in the IF statement to store subsequent start times of each game clock iteration. The principle of the game clock is to keep iterating through the while loop until the difference between the current system time and the iteration start time is greater than a predetermined value (in milliseconds). The game clock will keep iterating as long as loopVar is true; this will only be changed to false when some form of collision is detected.

The game clock code includes comments to explain what's going on there. The order is: update element locations, then parse those updated elements to the class that draws them, and finally actually update the panel to see the changes.

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Image;
import java.awt.Color;
import java.awt.LayoutManager;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;

public class TopClass implements ActionListener {
	//global constant variables
	private static final int SCREEN_WIDTH = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth();
	private static final int SCREEN_HEIGHT = (int) Toolkit.getDefaultToolkit().getScreenSize().getHeight();
	private static final int PIPE_GAP = SCREEN_HEIGHT/5; //distance in pixels between pipes
	private static final int PIPE_WIDTH = SCREEN_WIDTH/8, PIPE_HEIGHT = 4*PIPE_WIDTH;
	private static final int UPDATE_DIFFERENCE = 25; //time in ms between updates
	private static final int X_MOVEMENT_DIFFERENCE = 5; //distance the pipes move every update
	private static final int SCREEN_DELAY = 300; //needed because of long load times forcing pipes to pop up mid-screen
	
	//global variables
	private boolean loopVar = true; //false -> don't run loop; true -> run loop for pipes
	
	//global swing objects
	private JFrame f = new JFrame("Flappy Bird Redux");
	private JButton startGame;
	private JPanel topPanel; //declared globally to accommodate the repaint operation and allow for removeAll(), etc.
	
	//other global objects
	private static TopClass tc = new TopClass();
	private static PlayGameScreen pgs; //panel that has the moving background at the start of the game
	
	/**
	 * Default constructor
	 */
	public TopClass() {
		
	}
	
	/**
	 * Main executable method invoked when running .jar file
	 * @param args
	 */
	public static void main(String[] args) {
		//build the GUI on a new thread
		
		javax.swing.SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				tc.buildFrame();
				
				//create a new thread to keep the GUI responsive while the game runs
				Thread t = new Thread() {
					public void run() {
						tc.gameScreen(true);
					}
				};
				t.start();
			}
		});
	}
	
	/**
	 * Method to construct the JFrame and add the program content
	 */
	private void buildFrame() {
		Image icon = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("resources/blue_bird.png"));
		f.setContentPane(createContentPane());
                f.setResizable(true);
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                f.setAlwaysOnTop(false);
                f.setVisible(true);
                f.setMinimumSize(new Dimension(SCREEN_WIDTH*1/4, SCREEN_HEIGHT*1/4));
                f.setExtendedState(JFrame.MAXIMIZED_BOTH);
                f.setIconImage(icon);
                f.addKeyListener(this);
	}
	
	private JPanel createContentPane() {
		topPanel = new JPanel(); //top-most JPanel in layout hierarchy
		topPanel.setBackground(Color.BLACK);
		//allow us to layer the panels
		LayoutManager overlay = new OverlayLayout(topPanel);
		topPanel.setLayout(overlay);
		
		//Start Game JButton
		startGame = new JButton("Start Playing!");
		startGame.setBackground(Color.BLUE);
		startGame.setForeground(Color.WHITE);
		startGame.setFocusable(false); //rather than just setFocusabled(false)
		startGame.setFont(new Font("Calibri", Font.BOLD, 42));
		startGame.setAlignmentX(0.5f); //center horizontally on-screen
		startGame.setAlignmentY(0.5f); //center vertically on-screen
		startGame.addActionListener(this);
		topPanel.add(startGame);
		
		//must add last to ensure button's visibility
		pgs = new PlayGameScreen(SCREEN_WIDTH, SCREEN_HEIGHT, true); //true --> we want pgs to be the splash screen
		topPanel.add(pgs);
		
		return topPanel;
	}
	
	/**
	 * Implementation for action events
	 */
	public void actionPerformed(ActionEvent e) {
		if(e.getSource() == startGame) {
			//do something
		}
	}
	
	/**
	 * Method that performs the splash screen graphics movements
	 */
	private void gameScreen(boolean isSplash) {
		BottomPipe bp1 = new BottomPipe(PIPE_WIDTH, PIPE_HEIGHT);
		BottomPipe bp2 = new BottomPipe(PIPE_WIDTH, PIPE_HEIGHT);
		TopPipe tp1 = new TopPipe(PIPE_WIDTH, PIPE_HEIGHT);
		TopPipe tp2 = new TopPipe(PIPE_WIDTH, PIPE_HEIGHT);
		
		//variables to track x and y image locations for the bottom pipe
		int xLoc1 = SCREEN_WIDTH+SCREEN_DELAY, xLoc2 = (int) ((double) 3.0/2.0*SCREEN_WIDTH+PIPE_WIDTH/2.0)+SCREEN_DELAY;
		int yLoc1 = bottomPipeLoc(), yLoc2 = bottomPipeLoc();
		
		//variable to hold the loop start time
		long startTime = System.currentTimeMillis();
		
		while(loopVar) {
			if((System.currentTimeMillis() - startTime) > UPDATE_DIFFERENCE) {
				//check if a set of pipes has left the screen
				//if so, reset the pipe's X location and assign a new Y location
				if(xLoc1 < (0-PIPE_WIDTH)) {
					xLoc1 = SCREEN_WIDTH;
					yLoc1 = bottomPipeLoc();
				}
				else if(xLoc2 < (0-PIPE_WIDTH)) {
					xLoc2 = SCREEN_WIDTH;
					yLoc2 = bottomPipeLoc();
				}
				
				//decrement the pipe locations by the predetermined amount 
				xLoc1 -= X_MOVEMENT_DIFFERENCE;
				xLoc2 -= X_MOVEMENT_DIFFERENCE;
				
				//update the BottomPipe and TopPipe locations
				bp1.setX(xLoc1);
				bp1.setY(yLoc1);
				bp2.setX(xLoc2);
				bp2.setY(yLoc2);
				tp1.setX(xLoc1);
				tp1.setY(yLoc1-PIPE_GAP-PIPE_HEIGHT); //ensure tp1 placed in proper location
				tp2.setX(xLoc2);
				tp2.setY(yLoc2-PIPE_GAP-PIPE_HEIGHT); //ensure tp2 placed in proper location
				
				//set the BottomPipe and TopPipe local variables in PlayGameScreen by parsing the local variables
				pgs.setBottomPipe(bp1, bp2);
				pgs.setTopPipe(tp1, tp2);
				
				//update pgs's JPanel
				topPanel.revalidate();
				topPanel.repaint();
				
				//update the time-tracking variable after all operations completed
				startTime = System.currentTimeMillis();
			}
		}
	}
	
	/**
	 * Calculates a random int for the bottom pipe's placement
	 * @return int
	 */
	private int bottomPipeLoc() {
		int temp = 0;
		//iterate until temp is a value that allows both pipes to be onscreen
		while(temp <= PIPE_GAP+50 || temp >= SCREEN_HEIGHT-PIPE_GAP) {
			temp = (int) ((double) Math.random()*((double)SCREEN_HEIGHT));
		}
		return temp;
	}
}

Below, you will see the updated PlayGameScreen class that reflects the addition of the setter methods used above. We will add a little bit of code to flesh out the paintComponent method as well. First we sequentially set the graphics color and create the rectangle for the sky and ground, then draw the black line between them. Next we draw the BottomPipe and TopPipe items. These elements must not be null (i.e. they must have been created) in order to draw them.

Following this, we want to draw "Flappy Bird" in big letters near the top of the screen. First we set up a try-catch statement to try to set the font. The first font may not exist on computers, and if it doesn't, we progress to the catch statement where the font gets set to a more universal font. We also want to make sure the message text is centered on screen, so we get the width of the message we're going to draw by using the FontMetrics line. Finally we draw the message on screen after the try-catch block.

The next change made was the addition of a few simple setter methods. These should be self-explanatory. The final change is the addition of the sendText method. We will use this when the game ends to send "Game Over" to be drawn. That text will replace the existing message variable's text.

import javax.swing.*;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Color;

public class PlayGameScreen extends JPanel {
	//default reference ID
	private static final long serialVersionUID = 1L;
	
	//global variables
	private int screenWidth, screenHeight;
	private boolean isSplash = true;
	private String message = "Flappy Bird";
	private Font primaryFont = new Font("Goudy Stout", Font.BOLD, 56), failFont = new Font("Calibri", Font.BOLD, 56);
	private int messageWidth = 0;
	private BottomPipe bp1, bp2;
	private TopPipe tp1, tp2;

	/**
	 * Default constructor for the PlayGameScreen class
	 */
	public PlayGameScreen(int screenWidth, int screenHeight, boolean isSplash) {
		this.screenWidth = screenWidth;
		this.screenHeight = screenHeight;
		this.isSplash = isSplash;
	}
	
	/**
	 * Manually control what's drawn on this JPanel by calling the paintComponent method
	 * with a graphics object and painting using that object
	 */
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		
		g.setColor(new Color(89, 81, 247)); //color for the blue sky
		g.fillRect(0, 0, screenWidth, screenHeight*7/8); //create the sky rectangle
		g.setColor(new Color(147, 136, 9)); //brown color for ground
		g.fillRect(0, screenHeight*7/8, screenWidth, screenHeight/8); //create the ground rectangle
		g.setColor(Color.BLACK); //dividing line color
		g.drawLine(0, screenHeight*7/8, screenWidth, screenHeight*7/8); //draw the dividing line
		
		//objects must be instantiated before they're drawn!
		if(bp1 != null && bp2 != null && tp1 != null && tp2 != null) {
			g.drawImage(bp1.getPipe(), bp1.getX(), bp1.getY(), null);
			g.drawImage(bp2.getPipe(), bp2.getX(), bp2.getY(), null);
			g.drawImage(tp1.getPipe(), tp1.getX(), tp1.getY(), null);
			g.drawImage(tp2.getPipe(), tp2.getX(), tp2.getY(), null);
		}
		
		//needed in case the primary font does not exist
		try {
			g.setFont(primaryFont);
			FontMetrics metric = g.getFontMetrics(primaryFont);
			messageWidth = metric.stringWidth(message);
		}
		catch(Exception e) {
			g.setFont(failFont);
			FontMetrics metric = g.getFontMetrics(failFont);
			messageWidth = metric.stringWidth(message);
		}
		g.drawString(message, screenWidth/2-messageWidth/2, screenHeight/4);
	}
	
	/**
	 * Parsing method for PlayGameScreen's global BottomPipe variables
	 * @param bp1 The first BottomPipe
	 * @param bp2 The second BottomPipe
	 */
	public void setBottomPipe(BottomPipe bp1, BottomPipe bp2) {
		this.bp1 = bp1;
		this.bp2 = bp2;
	}
	
	/**
	 * Parsing method for PlayGameScreen's global TopPipe variables
	 * @param tp1 The first TopPipe
	 * @param tp2 The second TopPipe
	 */
	public void setTopPipe(TopPipe tp1, TopPipe tp2) {
		this.tp1 = tp1;
		this.tp2 = tp2;
	}
	
	/**
	 * Method called to parse a message onto the screen
	 * @param message The message to parse
	 */
	public void sendText(String message) {
		this.message = message;
	}
}

Step 9: The Fade Operation

At this point in time, we have much of what's needed to get the game wrapped up: the pipes are moving on the screen, the player and obstacle classes are finished, the PlayGameScreen class is nearly finished, and we're nearly finished with the TopClass.

This step is a matter of making the game flow better. If we didn't use this, we would have an abrupt jump from a splash screen to a game screen and it would be very displeasing to play. The fadeOperation method in TopClass will be performing a 'simple' fade-to-black and fade-from-black to start game play.

The fade operation will be triggered using the actionPerformed method. When the source of the ActionEvent is the startGame button, we change loopVar to false to tell the game loop to stop running. Next we call fadeOperation().

The first part of writing fadeOperation() is starting a new thread to handle the changes to come. Within the new thread's run() method, we first remove the button from the primary content panel (topPanel) and the graphics panel, then refresh topPanel. We then create a temporary JPanel (called temp) that we will add on top of topPanel; this is the panel we fade (remember topPanel's background is black).

We create a new variable to hold the alpha, then set temp's background to a transparent black JPanel. We add temp to topPanel and add the PaintGameScreen instance (pgs) back on top of that, followed by a refresh.

At this point, we need a while loop to perform the actual fade to black. As in the game loop, we create a variable to hold the iteration time, but this time, the while loop end condition is when the temp panel's alpha value is 255 (fully transparent). Set up an IF statement to tell us how often to affect changes. I then used a simple If-Else statement to dictate how the fade occurs (mostly linearly here). You can do whatever you want here. After this, you set temp's background and refresh topPanel.

Once the fade to black is complete, we remove everything from topPanel, re-add the temp panel, create a new instance of PlayGameScreen (overriding pgs), remove the title text, and add pgs back to topPanel. To fade from black, we essentially perform the opposite logic of the fade-to-black operation.

Once complete, we need to inform the game that it's allowed to begin. To do this, we created a global object called buildComplete. At the end of fadeOperation, we manually trigger an ActionEvent on buildComplete.

The final change we make on this step is to the actionPerformed method. We create a new conditional (else-if) for buildComplete. In here, we create a new thread, and in its run() method, we change the loopVar back to true (allow the game clock to run again) and call the gameScreen method, this time passing in false - we will see why on the next step.

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Image;
import java.awt.Color;
import java.awt.LayoutManager;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;

public class TopClass implements ActionListener {
	//global constant variables
	private static final int SCREEN_WIDTH = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth();
	private static final int SCREEN_HEIGHT = (int) Toolkit.getDefaultToolkit().getScreenSize().getHeight();
	private static final int PIPE_GAP = SCREEN_HEIGHT/5; //distance in pixels between pipes
	private static final int PIPE_WIDTH = SCREEN_WIDTH/8, PIPE_HEIGHT = 4*PIPE_WIDTH;
	private static final int UPDATE_DIFFERENCE = 25; //time in ms between updates
	private static final int X_MOVEMENT_DIFFERENCE = 5; //distance the pipes move every update
	private static final int SCREEN_DELAY = 300; //needed because of long load times forcing pipes to pop up mid-screen
	
	//global variables
	private boolean loopVar = true; //false -> don't run loop; true -> run loop for pipes
	private Object buildComplete = new Object();
	
	//global swing objects
	private JFrame f = new JFrame("Flappy Bird Redux");
	private JButton startGame;
	private JPanel topPanel; //declared globally to accommodate the repaint operation and allow for removeAll(), etc.
	
	//other global objects
	private static TopClass tc = new TopClass();
	private static PlayGameScreen pgs; //panel that has the moving background at the start of the game
	
	/**
	 * Default constructor
	 */
	public TopClass() {
		
	}
	
	/**
	 * Main executable method invoked when running .jar file
	 * @param args
	 */
	public static void main(String[] args) {
		//build the GUI on a new thread
		
		javax.swing.SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				tc.buildFrame();
				
				//create a new thread to keep the GUI responsive while the game runs
				Thread t = new Thread() {
					public void run() {
						tc.gameScreen(true);
					}
				};
				t.start();
			}
		});
	}
	
	/**
	 * Method to construct the JFrame and add the program content
	 */
	private void buildFrame() {
		Image icon = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("resources/blue_bird.png"));
		f.setContentPane(createContentPane());
        	f.setResizable(true);
	        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	        f.setAlwaysOnTop(false);
	        f.setVisible(true);
	        f.setMinimumSize(new Dimension(SCREEN_WIDTH*1/4, SCREEN_HEIGHT*1/4));
	        f.setExtendedState(JFrame.MAXIMIZED_BOTH);
	        f.setIconImage(icon);
	        f.addKeyListener(this);
	}
	
	private JPanel createContentPane() {
		topPanel = new JPanel(); //top-most JPanel in layout hierarchy
		topPanel.setBackground(Color.BLACK);
		//allow us to layer the panels
		LayoutManager overlay = new OverlayLayout(topPanel);
		topPanel.setLayout(overlay);
		
		//Start Game JButton
		startGame = new JButton("Start Playing!");
		startGame.setBackground(Color.BLUE);
		startGame.setForeground(Color.WHITE);
		startGame.setFocusable(false); //rather than just setFocusabled(false)
		startGame.setFont(new Font("Calibri", Font.BOLD, 42));
		startGame.setAlignmentX(0.5f); //center horizontally on-screen
		startGame.setAlignmentY(0.5f); //center vertically on-screen
		startGame.addActionListener(this);
		topPanel.add(startGame);
		
		//must add last to ensure button's visibility
		pgs = new PlayGameScreen(SCREEN_WIDTH, SCREEN_HEIGHT, true); //true --> we want pgs to be the splash screen
		topPanel.add(pgs);
		
		return topPanel;
	}
	
	/**
	 * Implementation for action events
	 */
	public void actionPerformed(ActionEvent e) {
		if(e.getSource() == startGame) {
			//stop the splash screen
			loopVar = false;
			
			fadeOperation();
		}
		else if(e.getSource() == buildComplete) {
			Thread t = new Thread() {
				public void run() {
					loopVar = true;
					tc.gameScreen(false);
				}
			};
			t.start();
		}
	}
	
	/** 
	 * Perform the fade operation that takes place before the start of rounds
	 */
	private void fadeOperation() {
		Thread t = new Thread() {
			public void run() {
				topPanel.remove(startGame);
				topPanel.remove(pgs);
				topPanel.revalidate();
				topPanel.repaint();
				
				//panel to fade
				JPanel temp = new JPanel();
				int alpha = 0; //alpha channel variable
				temp.setBackground(new Color(0, 0, 0, alpha)); //transparent, black JPanel
				topPanel.add(temp);
				topPanel.add(pgs);
				topPanel.revalidate();
				topPanel.repaint();
				
				long currentTime = System.currentTimeMillis();
				long startTime = currentTime;
				
				while((System.currentTimeMillis() - startTime) > FADE_TIME_MILLIS || temp.getBackground().getAlpha() != 255) {
					if((System.currentTimeMillis() - startTime) > UPDATE_DIFFERENCE/2) {
						if(alpha < 255 - 10) {
							alpha += 10;
						}
						else {
							alpha = 255;
						}
						
						temp.setBackground(new Color(0, 0, 0, alpha));
					
						topPanel.revalidate();
						topPanel.repaint();
						startTime = System.currentTimeMillis();
					}
				}
				
				topPanel.removeAll();
				topPanel.add(temp);
				pgs = new PlayGameScreen(SCREEN_WIDTH, SCREEN_HEIGHT, false);
				pgs.sendText(""); //remove title text
				topPanel.add(pgs);
				
				while((System.currentTimeMillis() - startTime) > FADE_TIME_MILLIS || temp.getBackground().getAlpha() != 0) {
					if((System.currentTimeMillis() - startTime) > UPDATE_DIFFERENCE/2) {
						if(alpha > 10) {
							alpha -= 10;
						}
						else {
							alpha = 0;
						}
						
						temp.setBackground(new Color(0, 0, 0, alpha));
					
						topPanel.revalidate();
						topPanel.repaint();
						startTime = System.currentTimeMillis();
					}
				}
				
				actionPerformed(new ActionEvent(buildComplete, -1, "Build Finished"));
			}
		};
		
		t.start();
	}
	
	/**
	 * Method that performs the splash screen graphics movements
	 */
	private void gameScreen(boolean isSplash) {
		BottomPipe bp1 = new BottomPipe(PIPE_WIDTH, PIPE_HEIGHT);
		BottomPipe bp2 = new BottomPipe(PIPE_WIDTH, PIPE_HEIGHT);
		TopPipe tp1 = new TopPipe(PIPE_WIDTH, PIPE_HEIGHT);
		TopPipe tp2 = new TopPipe(PIPE_WIDTH, PIPE_HEIGHT);
		
		//variables to track x and y image locations for the bottom pipe
		int xLoc1 = SCREEN_WIDTH+SCREEN_DELAY, xLoc2 = (int) ((double) 3.0/2.0*SCREEN_WIDTH+PIPE_WIDTH/2.0)+SCREEN_DELAY;
		int yLoc1 = bottomPipeLoc(), yLoc2 = bottomPipeLoc();
		
		//variable to hold the loop start time
		long startTime = System.currentTimeMillis();
		
		while(loopVar) {
			if((System.currentTimeMillis() - startTime) > UPDATE_DIFFERENCE) {
				//check if a set of pipes has left the screen
				//if so, reset the pipe's X location and assign a new Y location
				if(xLoc1 < (0-PIPE_WIDTH)) {
					xLoc1 = SCREEN_WIDTH;
					yLoc1 = bottomPipeLoc();
				}
				else if(xLoc2 < (0-PIPE_WIDTH)) {
					xLoc2 = SCREEN_WIDTH;
					yLoc2 = bottomPipeLoc();
				}
				
				//decrement the pipe locations by the predetermined amount
				xLoc1 -= X_MOVEMENT_DIFFERENCE;
				xLoc2 -= X_MOVEMENT_DIFFERENCE;
				
				//update the BottomPipe and TopPipe locations
				bp1.setX(xLoc1);
				bp1.setY(yLoc1);
				bp2.setX(xLoc2);
				bp2.setY(yLoc2);
				tp1.setX(xLoc1);
				tp1.setY(yLoc1-PIPE_GAP-PIPE_HEIGHT); //ensure tp1 placed in proper location
				tp2.setX(xLoc2);
				tp2.setY(yLoc2-PIPE_GAP-PIPE_HEIGHT); //ensure tp2 placed in proper location
				
				//set the BottomPipe and TopPipe local variables in PlayGameScreen by parsing the local variables
				pgs.setBottomPipe(bp1, bp2);
				pgs.setTopPipe(tp1, tp2);
				
				//update pgs's JPanel
				topPanel.revalidate();
				topPanel.repaint();
				
				//update the time-tracking variable after all operations completed
				startTime = System.currentTimeMillis();
			}
		}
	}
	
	/**
	 * Calculates a random int for the bottom pipe's placement
	 * @return int
	 */
	private int bottomPipeLoc() {
		int temp = 0;
		//iterate until temp is a value that allows both pipes to be onscreen
		while(temp <= PIPE_GAP+50 || temp >= SCREEN_HEIGHT-PIPE_GAP) {
			temp = (int) ((double) Math.random()*((double)SCREEN_HEIGHT));
		}
		return temp;
	}
}

Step 10: Adding the Bird

In this step, we will add the Bird player to the screen and its associated movement logic. This doesn't involve much new code - the changes will be seen by adding a KeyListener to allow the user to make the bird jump and start a new game, making a few additions in gameScreen, adding logic to update the game score, and finalizing the PlayGameScreen class.

A few new global variables have been introduced in this step: BIRD_WIDTH, BIRD_HEIGHT, BIRD_X_LOCATION, BIRD_JUMP_DIFF, BIRD_FALL_DIFF, and BIRD_JUMP_HEIGHT for the constants; gamePlay, birdThrust, birdFired, released, and birdYTracker for the global variables.

To begin, we are going to add the key listener, so import the KeyEvent and KeyListener classes, add KeyListener to the one of the implemented classes that TopClass uses, and create the default keyPressed(...), keyReleased(...), and keyTyped(...) methods. Only create the skeleton for these three methods; do not fill them out yet.

------
Within gameScreen, we make a few additions to add the Bird's functionality to the game. First create an instance of Bird near the top of the method, create two variables to track the bird's X (unchanging) and Y coordinates. It's worthwhile to note, y=0 is the top of the screen, so when the bird jumps, you actually decrement the bird's y coordinate.

Within the game loop, you will see several conditional statement additions relevant to the bird; each will check if we're actually on the game and not the splash screen with !isSplash. In the first conditional we test if the bird has been told to move (user pressed the space bar, which changes birdFired to true). If so, we update the global bird Y coordinate variable to be the locally key Y coordinate and change birdFired to false.

Next we test if the bird still has thrust, meaning we look at whether the bird is still moving from the previous space bar press. If so, we check whether telling the bird to jump again will force it off the top of the screen. If not, we allow a full jump. Otherwise set the bird's y coordinate to zero (top of the screen), update the global variable, and change birdThrust to false, as the bird cannot jump anymore.

The next else statement indicates that the bird's jump operation has completed, so update the global variable and change birdThrust to false so the bird may not move vertically anymore.

The next else if statement tells the bird to fall when there is no issued command.

After the BottomPipe and TopPipe X and Y coordinates are set, we have a quick IF statement to do the same for updating the Bird object's X and Y coordinates, as well as setting the Bird object in pgs.

In the final conditional, we call the updateScore method in TopClass to test whether the score should be updated. Please note, in the condition statement for this IF statement, we check whether the bird object's width is not zero. This is to avoid an odd resource-loading issue that arises during collision detection in the next step. Just know you need that code here.

------
Moving on to the updateScore method, all we do here is test whether the Bird object has passed one of the BottomPipe objects. We do this by checking if it's between the outside edge and no further than the outside edge plus X_MOVEMENT_DIFFERENCE. If it's within this range, call the incrementJump method within PlayGameScreen to update the game score.

------
We designate three keys to be in use in this program: the space bar will jump the bird, the 'b' key will start a new round after we lose, and the escape key will completely exit the program. In the keyPressed method, the first IF statement checks that if the space bar is pressed, the game is being played (gamePlay == true), and the space bar has been released, then do the following:

* First check if birdThrust is true, which checks if the bird is still moving from previously pressing the space bar. If so, change birdFired to true to register the new button press.
* Next change the birdThrust variable to true to start the bird moving if it isn't already
* Finally indicate that the space bar has been registered and all related operations are complete by changing released to false.

Next we check if the game is no longer being played (i.e. a collision was detected), and if the 'b' button was pressed, then we reset the bird's starting height, remove any thrust it may have had when the collision was detected, and simulate the startGame button press (restarting the game is no different than pressing startGame).

Finally if the escape button was pressed, we want to entirely exit the game, which is accomplished using System.exit(0).

Within keyReleased, we simply register the space bar being pressed by changing released to true (within IF statement).

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Image;
import java.awt.Color;
import java.awt.LayoutManager;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.*;

public class TopClass implements ActionListener, KeyListener {
	//global constant variables
	private static final int SCREEN_WIDTH = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth();
	private static final int SCREEN_HEIGHT = (int) Toolkit.getDefaultToolkit().getScreenSize().getHeight();
	private static final int PIPE_GAP = SCREEN_HEIGHT/5; //distance in pixels between pipes
	private static final int PIPE_WIDTH = SCREEN_WIDTH/8, PIPE_HEIGHT = 4*PIPE_WIDTH;
	private static final int BIRD_WIDTH = 120, BIRD_HEIGHT = 75;
	private static final int UPDATE_DIFFERENCE = 25; //time in ms between updates
	private static final int X_MOVEMENT_DIFFERENCE = 5; //distance the pipes move every update
	private static final int SCREEN_DELAY = 300; //needed because of long load times forcing pipes to pop up mid-screen
	private static final int BIRD_X_LOCATION = SCREEN_WIDTH/7;
	private static final int BIRD_JUMP_DIFF = 10, BIRD_FALL_DIFF = BIRD_JUMP_DIFF/2, BIRD_JUMP_HEIGHT = PIPE_GAP - BIRD_HEIGHT - BIRD_JUMP_DIFF*2;
	
	//global variables
	private boolean loopVar = true; //false -> don't run loop; true -> run loop for pipes
	private boolean gamePlay = false; //false -> game not being played
	private boolean birdThrust = false; //false -> key has not been pressed to move the bird vertically
	private boolean birdFired = false; //true -> button pressed before jump completes
	private boolean released = true; //space bar released; starts as true so first press registers
	private int birdYTracker = SCREEN_HEIGHT/2 - BIRD_HEIGHT;
	private Object buildComplete = new Object();
	
	//global swing objects
	private JFrame f = new JFrame("Flappy Bird Redux");
	private JButton startGame;
	private JPanel topPanel; //declared globally to accommodate the repaint operation and allow for removeAll(), etc.
	
	//other global objects
	private static TopClass tc = new TopClass();
	private static PlayGameScreen pgs; //panel that has the moving background at the start of the game
	
	/**
	 * Default constructor
	 */
	public TopClass() {
		
	}
	
	/**
	 * Main executable method invoked when running .jar file
	 * @param args
	 */
	public static void main(String[] args) {
		//build the GUI on a new thread
		
		javax.swing.SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				tc.buildFrame();
				
				//create a new thread to keep the GUI responsive while the game runs
				Thread t = new Thread() {
					public void run() {
						tc.gameScreen(true);
					}
				};
				t.start();
			}
		});
	}
	
	/**
	 * Method to construct the JFrame and add the program content
	 */
	private void buildFrame() {
		Image icon = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("resources/blue_bird.png"));
		f.setContentPane(createContentPane());
	        f.setResizable(true);
	        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	        f.setAlwaysOnTop(false);
	        f.setVisible(true);
	        f.setMinimumSize(new Dimension(SCREEN_WIDTH*1/4, SCREEN_HEIGHT*1/4));
	        f.setExtendedState(JFrame.MAXIMIZED_BOTH);
	        f.setIconImage(icon);
	        f.addKeyListener(this);
	}
	
	private JPanel createContentPane() {
		topPanel = new JPanel(); //top-most JPanel in layout hierarchy
		topPanel.setBackground(Color.BLACK);
		//allow us to layer the panels
		LayoutManager overlay = new OverlayLayout(topPanel);
		topPanel.setLayout(overlay);
		
		//Start Game JButton
		startGame = new JButton("Start Playing!");
		startGame.setBackground(Color.BLUE);
		startGame.setForeground(Color.WHITE);
		startGame.setFocusable(false); //rather than just setFocusabled(false)
		startGame.setFont(new Font("Calibri", Font.BOLD, 42));
		startGame.setAlignmentX(0.5f); //center horizontally on-screen
		startGame.setAlignmentY(0.5f); //center vertically on-screen
		startGame.addActionListener(this);
		topPanel.add(startGame);
		
		//must add last to ensure button's visibility
		pgs = new PlayGameScreen(SCREEN_WIDTH, SCREEN_HEIGHT, true); //true --> we want pgs to be the splash screen
		topPanel.add(pgs);
		
		return topPanel;
	}
	
	/**
	 * Implementation for action events
	 */
	public void actionPerformed(ActionEvent e) {
		if(e.getSource() == startGame) {
			//stop the splash screen
			loopVar = false;
			
			fadeOperation();
		}
		else if(e.getSource() == buildComplete) {
			Thread t = new Thread() {
				public void run() {
					loopVar = true;
					gamePlay = true;
					tc.gameScreen(false);
				}
			};
			t.start();
		}
	}
	
	public void keyPressed(KeyEvent e) {
		if(e.getKeyCode() == KeyEvent.VK_SPACE && gamePlay == true && released == true){
			//update a boolean that's tested in game loop to move the bird
			if(birdThrust) { //need this to register the button press and reset the birdYTracker before the jump operation completes
				birdFired = true;
			}
			birdThrust = true;
			released = false;
		}
		else if(e.getKeyCode() == KeyEvent.VK_B && gamePlay == false) {
			birdYTracker = SCREEN_HEIGHT/2 - BIRD_HEIGHT; //need to reset the bird's starting height
			birdThrust = false; //if user presses SPACE before collision and a collision occurs before reaching max height, you get residual jump, so this is preventative
			actionPerformed(new ActionEvent(startGame, -1, ""));
		}
		if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
			System.exit(0);
		}
	}
	
	public void keyReleased(KeyEvent e) {
		if(e.getKeyCode() == KeyEvent.VK_SPACE) {
			released = true;
		}
	}
	
	public void keyTyped(KeyEvent e) {
		
	}
	
	/**
	 * Perform the fade operation that take place before the start of rounds
	 */
	private void fadeOperation() {
		Thread t = new Thread() {
			public void run() {
				topPanel.remove(startGame);
				topPanel.remove(pgs);
				topPanel.revalidate();
				topPanel.repaint();
				
				//panel to fade
				JPanel temp = new JPanel();
				int alpha = 0; //alpha channel variable
				temp.setBackground(new Color(0, 0, 0, alpha)); //transparent, black JPanel
				topPanel.add(temp);
				topPanel.add(pgs);
				topPanel.revalidate();
				topPanel.repaint();
				
				long currentTime = System.currentTimeMillis();
				
				while(temp.getBackground().getAlpha() != 255) {
					if((System.currentTimeMillis() - currentTime) > UPDATE_DIFFERENCE/2) {
						if(alpha < 255 - 10) {
							alpha += 10;
						}
						else {
							alpha = 255;
						}
						
						temp.setBackground(new Color(0, 0, 0, alpha));
					
						topPanel.revalidate();
						topPanel.repaint();
						currentTime = System.currentTimeMillis();
					}
				}
				
				topPanel.removeAll();
				topPanel.add(temp);
				pgs = new PlayGameScreen(SCREEN_WIDTH, SCREEN_HEIGHT, false);
				pgs.sendText(""); //remove title text
				topPanel.add(pgs);
				
				while(temp.getBackground().getAlpha() != 0) {
					if((System.currentTimeMillis() - currentTime) > UPDATE_DIFFERENCE/2) {
						if(alpha > 10) {
							alpha -= 10;
						}
						else {
							alpha = 0;
						}
						
						temp.setBackground(new Color(0, 0, 0, alpha));
					
						topPanel.revalidate();
						topPanel.repaint();
						currentTime = System.currentTimeMillis();
					}
				}
				
				actionPerformed(new ActionEvent(buildComplete, -1, "Build Finished"));
			}
		};
		
		t.start();
	}
	
	/**
	 * Method that performs the splash screen graphics movements
	 */
	private void gameScreen(boolean isSplash) {
		BottomPipe bp1 = new BottomPipe(PIPE_WIDTH, PIPE_HEIGHT);
		BottomPipe bp2 = new BottomPipe(PIPE_WIDTH, PIPE_HEIGHT);
		TopPipe tp1 = new TopPipe(PIPE_WIDTH, PIPE_HEIGHT);
		TopPipe tp2 = new TopPipe(PIPE_WIDTH, PIPE_HEIGHT);
		Bird bird = new Bird(BIRD_WIDTH, BIRD_HEIGHT);
		
		//variables to track x and y image locations for the bottom pipe
		int xLoc1 = SCREEN_WIDTH+SCREEN_DELAY, xLoc2 = (int) ((double) 3.0/2.0*SCREEN_WIDTH+PIPE_WIDTH/2.0)+SCREEN_DELAY;
		int yLoc1 = bottomPipeLoc(), yLoc2 = bottomPipeLoc();
		int birdX = BIRD_X_LOCATION, birdY = birdYTracker;
		
		//variable to hold the loop start time
		long startTime = System.currentTimeMillis();
		
		while(loopVar) {
			if((System.currentTimeMillis() - startTime) > UPDATE_DIFFERENCE) {
				//check if a set of pipes has left the screen
				//if so, reset the pipe's X location and assign a new Y location
				if(xLoc1 < (0-PIPE_WIDTH)) {
					xLoc1 = SCREEN_WIDTH;
					yLoc1 = bottomPipeLoc();
				}
				else if(xLoc2 < (0-PIPE_WIDTH)) {
					xLoc2 = SCREEN_WIDTH;
					yLoc2 = bottomPipeLoc();
				}
				
				//decrement the pipe locations by the predetermined amount
				xLoc1 -= X_MOVEMENT_DIFFERENCE;
				xLoc2 -= X_MOVEMENT_DIFFERENCE;
				
				if(birdFired && !isSplash) {
					birdYTracker = birdY;
					birdFired = false;
				}
				
				if(birdThrust && !isSplash) {
					//move bird vertically
					if(birdYTracker - birdY - BIRD_JUMP_DIFF < BIRD_JUMP_HEIGHT) {
						if(birdY - BIRD_JUMP_DIFF > 0) {
							birdY -= BIRD_JUMP_DIFF; //coordinates different
						}
						else {
							birdY = 0;
							birdYTracker = birdY;
							birdThrust = false;
						}
					}
					else {
						birdYTracker = birdY;
						birdThrust = false;
					}
				}
				else if(!isSplash) {
					birdY += BIRD_FALL_DIFF;
					birdYTracker = birdY;
				}
				
				//update the BottomPipe and TopPipe locations
				bp1.setX(xLoc1);
				bp1.setY(yLoc1);
				bp2.setX(xLoc2);
				bp2.setY(yLoc2);
				tp1.setX(xLoc1);
				tp1.setY(yLoc1-PIPE_GAP-PIPE_HEIGHT); //ensure tp1 placed in proper location
				tp2.setX(xLoc2);
				tp2.setY(yLoc2-PIPE_GAP-PIPE_HEIGHT); //ensure tp2 placed in proper location
				
				if(!isSplash) {
					bird.setX(birdX);
					bird.setY(birdY);
					pgs.setBird(bird);
				}
				
				//set the BottomPipe and TopPipe local variables in PlayGameScreen by parsing the local variables
				pgs.setBottomPipe(bp1, bp2);
				pgs.setTopPipe(tp1, tp2);
				
				if(!isSplash && bird.getWidth() != -1) { //need the second part because if bird not on-screen, cannot get image width and have cascading error in collision
					updateScore(bp1, bp2, bird);
				}
				
				//update pgs's JPanel
				topPanel.revalidate();
				topPanel.repaint();
				
				//update the time-tracking variable after all operations completed
				startTime = System.currentTimeMillis();
			}
		}
	}
	
	/**
	 * Calculates a random int for the bottom pipe's placement
	 * @return int
	 */
	private int bottomPipeLoc() {
		int temp = 0;
		//iterate until temp is a value that allows both pipes to be onscreen
		while(temp <= PIPE_GAP+50 || temp >= SCREEN_HEIGHT-PIPE_GAP) {
			temp = (int) ((double) Math.random()*((double)SCREEN_HEIGHT));
		}
		return temp;
	}
	
	/**
	 * Method that checks whether the score needs to be updated
	 * @param bp1 First BottomPipe object
	 * @param bp2 Second BottomPipe object
	 * @param bird Bird object
	 */
	private void updateScore(BottomPipe bp1, BottomPipe bp2, Bird bird) {
		if(bp1.getX() + PIPE_WIDTH < bird.getX() && bp1.getX() + PIPE_WIDTH > bird.getX() - X_MOVEMENT_DIFFERENCE) {
			pgs.incrementJump();
		}
		else if(bp2.getX() + PIPE_WIDTH < bird.getX() && bp2.getX() + PIPE_WIDTH > bird.getX() - X_MOVEMENT_DIFFERENCE) {
			pgs.incrementJump();
		}
	}
}

Lastly in this step, we finish the code for the PlayGameScreen class. To wrap this up, we create a global Bird object, add a global variable that tracks the score, create a global variable for the width of the score text, add a couple lines of code in the paintComponent method, and create three simple methods.

In paintComponent, we add the conditional statement to check if we're no longer on the splash screen and ensure the bird is not null. If satisfied, we draw the bird object. In the try-catch block, assign scoreWidth based on the current score using FontMetrics. Lastly, if we're not on the splash screen, draw the number of successful jumps onscreen.

We create three simple methods now. First, setBird sets the Bird object within PlayGameScreen, incrementJump increments the global jump variable, and getScore returns the number of successful jumps (no functionality in this game).

import javax.swing.*;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Color;

public class PlayGameScreen extends JPanel {
	//default reference ID
	private static final long serialVersionUID = 1L;
	
	//global variables
	private int screenWidth, screenHeight;
	private boolean isSplash = true;
	private int successfulJumps = 0;
	private String message = "Flappy Bird";
	private Font primaryFont = new Font("Goudy Stout", Font.BOLD, 56), failFont = new Font("Calibri", Font.BOLD, 56);
	private int messageWidth = 0, scoreWidth = 0;
	private BottomPipe bp1, bp2;
	private TopPipe tp1, tp2;
	private Bird bird;</p><p>	/**
	 * Default constructor for the PlayGameScreen class
	 */
	public PlayGameScreen(int screenWidth, int screenHeight, boolean isSplash) {
		this.screenWidth = screenWidth;
		this.screenHeight = screenHeight;
		this.isSplash = isSplash;
	}
	
	/**
	 * Manually control what's drawn on this JPanel by calling the paintComponent method
	 * with a graphics object and painting using that object
	 */
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		
		g.setColor(new Color(89, 81, 247)); //color for the blue sky
		g.fillRect(0, 0, screenWidth, screenHeight*7/8); //create the sky rectangle
		g.setColor(new Color(147, 136, 9)); //brown color for ground
		g.fillRect(0, screenHeight*7/8, screenWidth, screenHeight/8); //create the ground rectangle
		g.setColor(Color.BLACK); //dividing line color
		g.drawLine(0, screenHeight*7/8, screenWidth, screenHeight*7/8); //draw the dividing line
		
		//objects must be instantiated before they're drawn!
		if(bp1 != null && bp2 != null && tp1 != null && tp2 != null) {
			g.drawImage(bp1.getPipe(), bp1.getX(), bp1.getY(), null);
			g.drawImage(bp2.getPipe(), bp2.getX(), bp2.getY(), null);
			g.drawImage(tp1.getPipe(), tp1.getX(), tp1.getY(), null);
			g.drawImage(tp2.getPipe(), tp2.getX(), tp2.getY(), null);
		}
		
		if(!isSplash && bird != null) {
			g.drawImage(bird.getBird(), bird.getX(), bird.getY(), null);
		}
		
		//needed in case the primary font does not exist
		try {
			g.setFont(primaryFont);
			FontMetrics metric = g.getFontMetrics(primaryFont);
			messageWidth = metric.stringWidth(message);
			scoreWidth = metric.stringWidth(String.format("%d", successfulJumps));
		}
		catch(Exception e) {
			g.setFont(failFont);
			FontMetrics metric = g.getFontMetrics(failFont);
			messageWidth = metric.stringWidth(message);
			scoreWidth = metric.stringWidth(String.format("%d", successfulJumps));
		}
		
		g.drawString(message, screenWidth/2-messageWidth/2, screenHeight/4);
		
		if(!isSplash) {
			g.drawString(String.format("%d", successfulJumps), screenWidth/2-scoreWidth/2, 50);
		}
	}
	
	/**
	 * Parsing method for PlayGameScreen's global BottomPipe variables
	 * @param bp1 The first BottomPipe
	 * @param bp2 The second BottomPipe
	 */
	public void setBottomPipe(BottomPipe bp1, BottomPipe bp2) {
		this.bp1 = bp1;
		this.bp2 = bp2;
	}
	
	/**
	 * Parsing method for PlayGameScreen's global TopPipe variables
	 * @param tp1 The first TopPipe
	 * @param tp2 The second TopPipe
	 */
	public void setTopPipe(TopPipe tp1, TopPipe tp2) {
		this.tp1 = tp1;
		this.tp2 = tp2;
	}
	
	/**
	 * Parsing method for PlayGameScreen's global Bird variable
	 * @param bird The Bird object
	 */
	public void setBird(Bird bird) {
		this.bird = bird;
	}
	
	/**
	 * Method called to invoke an increase in the variable tracking the current
	 * jump score
	 */
	public void incrementJump() {
		successfulJumps++;
	}
	
	/**
	 * Method called to return the current jump score
	 * @return
	 */
	public int getScore() {
		return successfulJumps;
	}
	
	/**
	 * Method called to parse a message onto the screen
	 * @param message The message to parse
	 */
	public void sendText(String message) {
		this.message = message;
	}
}

Step 11: Detecting Collisions

Picture of Detecting Collisions

This is the final step. The game presently has full functionality with pipe movement, bird movement, etc. Now we will write code that determines when the player has lost within TopClass. We will accomplish this with one line of code within the game loop and two methods (collisionDetection and collisionHelper). Below you see a video of some collision testing.

Foremost, create the skeleton for collisionDetection (so eclipse doesn't get angry at you), then add "collisionDetection(bp1, bp2, tp1, tp2, bird);" right before the updateScore method is called in the game loop.

If you think about it, there are five possible collisions that can happen. The bird could collide with one of the four pipes or the ground. This means you can create a helper method that handles the identical logic for the four possible pipe collisions. Because of this, we will start with the collisionHelper method.

Logically, this is how we are going to detect a collision (something I developed, so I'm not sure how efficient it is compared to other collision-detection methods):

* We need the player and obstacle classes to have methods that return a Rectangle object and BufferedImage object
* Using the Rectangles, we check whether the bird's Rectangle intersects a particular pipe's Rectangle
* If there is an intersection, obtain the range of coordinates that bound the intersection (first x, final x, first y, final y)
* Using the bird and pipe BufferedImages, test each pixel in the collision area for each BufferedImage; if the pixel is not transparent (remember the graphics are interlaced) for both the bird and pipe, then a collision has occured

Knowing this, we start working on collisionHelper. Rectangles are nice because you can test whether intersections have occurred, as well as keep track of screen position of the intersection area.

We create a new rectangle that is the intersection between the bird and pipe. The firstI variable is the first X pixel to iterate from; it is the difference between the far left side of the intersection rectangle and the bird's (r1) far left X coordinate. The firstJ variable is the first Y pixel to iterate from; it is the difference between the top side of the intersection rectangle and the bird's top Y coordinate. These two are used for referencing the bird object.

We also need helper variables to use when referencing the collision object. The bp1XHelper and bp1YHelper variables use similar logic as firstI and firstJ, except they reference the bird and collision objects.

For our iterative analysis, we create a for loop nested in another for loop, iterating from the firstI/J to the width/height of the bird object (r.getWidth() + firstI is the same as r1.getWidth()). Embedded within the inner loop, we have a conditional statement that tests the pixel transparency. b1.getRGB(i, j) & 0xFF000000 simply grabs the alpha value of the pixel and if it does not have the value of 0x00, there is a non-transparent pixel present. We test for b1 and b2, and if both are non-transparent, there has been a collision.

Provided a collision, occurs, we send "Game Over" to PlayGameScreen for painting across the screen, end the game loop by changing loopVar to false, indicate the game has ended by changing gamePlay to false, and break from the loop.

------
Now, fleshing out collisionDetection is fairly simple. Start by calling collisionHelper for the bird and each of the pipe objects, passing in their relative Rectangle and BufferedImage objects. Next, test if there has been a collision with the "ground" by testing if the bottom of the bird has exceeded the position of the ground (SCREEN_HEIGHT*7/8). If so, send "Game Over," end the game loop, and indicate the game has ended.

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Image;
import java.awt.Color;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import javax.swing.*;

public class TopClass implements ActionListener, KeyListener {
	//global constant variables
	private static final int SCREEN_WIDTH = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth();
	private static final int SCREEN_HEIGHT = (int) Toolkit.getDefaultToolkit().getScreenSize().getHeight();
	private static final int PIPE_GAP = SCREEN_HEIGHT/5; //distance in pixels between pipes
	private static final int PIPE_WIDTH = SCREEN_WIDTH/8, PIPE_HEIGHT = 4*PIPE_WIDTH;
	private static final int BIRD_WIDTH = 120, BIRD_HEIGHT = 75;
	private static final int UPDATE_DIFFERENCE = 25; //time in ms between updates
	private static final int X_MOVEMENT_DIFFERENCE = 5; //distance the pipes move every update
	private static final int SCREEN_DELAY = 300; //needed because of long load times forcing pipes to pop up mid-screen
	private static final int BIRD_X_LOCATION = SCREEN_WIDTH/7;
	private static final int BIRD_JUMP_DIFF = 10, BIRD_FALL_DIFF = BIRD_JUMP_DIFF/2, BIRD_JUMP_HEIGHT = PIPE_GAP - BIRD_HEIGHT - BIRD_JUMP_DIFF*2;
	
	//global variables
	private boolean loopVar = true; //false -> don't run loop; true -> run loop for pipes
	private boolean gamePlay = false; //false -> game not being played
	private boolean birdThrust = false; //false -> key has not been pressed to move the bird vertically
	private boolean birdFired = false; //true -> button pressed before jump completes
	private boolean released = true; //space bar released; starts as true so first press registers
	private int birdYTracker = SCREEN_HEIGHT/2 - BIRD_HEIGHT;
	private Object buildComplete = new Object();
	
	//global swing objects
	private JFrame f = new JFrame("Flappy Bird Redux");
	private JButton startGame;
	private JPanel topPanel; //declared globally to accommodate the repaint operation and allow for removeAll(), etc.
	
	//other global objects
	private static TopClass tc = new TopClass();
	private static PlayGameScreen pgs; //panel that has the moving background at the start of the game
	
	/**
	 * Default constructor
	 */
	public TopClass() {
		
	}
	
	/**
	 * Main executable method invoked when running .jar file
	 * @param args
	 */
	public static void main(String[] args) {
		//build the GUI on a new thread
		
		javax.swing.SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				tc.buildFrame();
				
				//create a new thread to keep the GUI responsive while the game runs
				Thread t = new Thread() {
					public void run() {
						tc.gameScreen(true);
					}
				};
				t.start();
			}
		});
	}
	
	/**
	 * Method to construct the JFrame and add the program content
	 */
	private void buildFrame() {
		Image icon = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("resources/blue_bird.png"));
		
		f.setContentPane(createContentPane());
        f.setResizable(true);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setAlwaysOnTop(false);
        f.setVisible(true);
        f.setMinimumSize(new Dimension(SCREEN_WIDTH*1/4, SCREEN_HEIGHT*1/4));
        f.setExtendedState(JFrame.MAXIMIZED_BOTH);
        f.setIconImage(icon);
        f.addKeyListener(this);
	}
	
	private JPanel createContentPane() {
		topPanel = new JPanel(); //top-most JPanel in layout hierarchy
		topPanel.setBackground(Color.BLACK);
		//allow us to layer the panels
		LayoutManager overlay = new OverlayLayout(topPanel);
		topPanel.setLayout(overlay);
		
		//Start Game JButton
		startGame = new JButton("Start Playing!");
		startGame.setBackground(Color.BLUE);
		startGame.setForeground(Color.WHITE);
		startGame.setFocusable(false); //rather than just setFocusabled(false)
		startGame.setFont(new Font("Calibri", Font.BOLD, 42));
		startGame.setAlignmentX(0.5f); //center horizontally on-screen
		startGame.setAlignmentY(0.5f); //center vertically on-screen
		startGame.addActionListener(this);
		topPanel.add(startGame);
		
		//must add last to ensure button's visibility
		pgs = new PlayGameScreen(SCREEN_WIDTH, SCREEN_HEIGHT, true); //true --> we want pgs to be the splash screen
		topPanel.add(pgs);
		
		return topPanel;
	}
	
	/**
	 * Implementation for action events
	 */
	public void actionPerformed(ActionEvent e) {
		if(e.getSource() == startGame) {
			//stop the splash screen
			loopVar = false;
			
			fadeOperation();
		}
		else if(e.getSource() == buildComplete) {
			Thread t = new Thread() {
				public void run() {
					loopVar = true;
					gamePlay = true;
					tc.gameScreen(false);
				}
			};
			t.start();
		}
	}
	
	public void keyPressed(KeyEvent e) {
		if(e.getKeyCode() == KeyEvent.VK_SPACE && gamePlay == true && released == true){
			//update a boolean that's tested in game loop to move the bird
			if(birdThrust) { //need this to register the button press and reset the birdYTracker before the jump operation completes
				birdFired = true;
			}
			birdThrust = true;
			released = false;
		}
		else if(e.getKeyCode() == KeyEvent.VK_B && gamePlay == false) {
			birdYTracker = SCREEN_HEIGHT/2 - BIRD_HEIGHT; //need to reset the bird's starting height
			birdThrust = false; //if user presses SPACE before collision and a collision occurs before reaching max height, you get residual jump, so this is preventative
			actionPerformed(new ActionEvent(startGame, -1, ""));
		}
		if(e.getKeyCode() == KeyEvent.VK_ESCAPE) {
			System.exit(0);
		}
	}
	
	public void keyReleased(KeyEvent e) {
		if(e.getKeyCode() == KeyEvent.VK_SPACE) {
			released = true;
		}
	}
	
	public void keyTyped(KeyEvent e) {
		
	}
	
	/**
	 * Perform the fade operation that take place before the start of rounds
	 */
	private void fadeOperation() {
		Thread t = new Thread() {
			public void run() {
				topPanel.remove(startGame);
				topPanel.remove(pgs);
				topPanel.revalidate();
				topPanel.repaint();
				
				//panel to fade
				JPanel temp = new JPanel();
				int alpha = 0; //alpha channel variable
				temp.setBackground(new Color(0, 0, 0, alpha)); //transparent, black JPanel
				topPanel.add(temp);
				topPanel.add(pgs);
				topPanel.revalidate();
				topPanel.repaint();
				
				long currentTime = System.currentTimeMillis();
				
				while(temp.getBackground().getAlpha() != 255) {
					if((System.currentTimeMillis() - currentTime) > UPDATE_DIFFERENCE/2) {
						if(alpha < 255 - 10) {
							alpha += 10;
						}
						else {
							alpha = 255;
						}
						
						temp.setBackground(new Color(0, 0, 0, alpha));
					
						topPanel.revalidate();
						topPanel.repaint();
						currentTime = System.currentTimeMillis();
					}
				}
				
				topPanel.removeAll();
				topPanel.add(temp);
				pgs = new PlayGameScreen(SCREEN_WIDTH, SCREEN_HEIGHT, false);
				pgs.sendText(""); //remove title text
				topPanel.add(pgs);
				
				while(temp.getBackground().getAlpha() != 0) {
					if((System.currentTimeMillis() - currentTime) > UPDATE_DIFFERENCE/2) {
						if(alpha > 10) {
							alpha -= 10;
						}
						else {
							alpha = 0;
						}
						
						temp.setBackground(new Color(0, 0, 0, alpha));
					
						topPanel.revalidate();
						topPanel.repaint();
						currentTime = System.currentTimeMillis();
					}
				}
				
				actionPerformed(new ActionEvent(buildComplete, -1, "Build Finished"));
			}
		};
		
		t.start();
	}
	
	/**
	 * Method that performs the splash screen graphics movements
	 */
	private void gameScreen(boolean isSplash) {
		BottomPipe bp1 = new BottomPipe(PIPE_WIDTH, PIPE_HEIGHT);
		BottomPipe bp2 = new BottomPipe(PIPE_WIDTH, PIPE_HEIGHT);
		TopPipe tp1 = new TopPipe(PIPE_WIDTH, PIPE_HEIGHT);
		TopPipe tp2 = new TopPipe(PIPE_WIDTH, PIPE_HEIGHT);
		Bird bird = new Bird(BIRD_WIDTH, BIRD_HEIGHT);
		
		//variables to track x and y image locations for the bottom pipe
		int xLoc1 = SCREEN_WIDTH+SCREEN_DELAY, xLoc2 = (int) ((double) 3.0/2.0*SCREEN_WIDTH+PIPE_WIDTH/2.0)+SCREEN_DELAY;
		int yLoc1 = bottomPipeLoc(), yLoc2 = bottomPipeLoc();
		int birdX = BIRD_X_LOCATION, birdY = birdYTracker;
		
		//variable to hold the loop start time
		long startTime = System.currentTimeMillis();
		
		while(loopVar) {
			if((System.currentTimeMillis() - startTime) > UPDATE_DIFFERENCE) {
				//check if a set of pipes has left the screen
				//if so, reset the pipe's X location and assign a new Y location
				if(xLoc1 < (0-PIPE_WIDTH)) {
					xLoc1 = SCREEN_WIDTH;
					yLoc1 = bottomPipeLoc();
				}
				else if(xLoc2 < (0-PIPE_WIDTH)) {
					xLoc2 = SCREEN_WIDTH;
					yLoc2 = bottomPipeLoc();
				}
				
				//decrement the pipe locations by the predetermined amount
				xLoc1 -= X_MOVEMENT_DIFFERENCE;
				xLoc2 -= X_MOVEMENT_DIFFERENCE;
				
				if(birdFired && !isSplash) {
					birdYTracker = birdY;
					birdFired = false;
				}
				
				if(birdThrust && !isSplash) {
					//move bird vertically
					if(birdYTracker - birdY - BIRD_JUMP_DIFF < BIRD_JUMP_HEIGHT) {
						if(birdY - BIRD_JUMP_DIFF > 0) {
							birdY -= BIRD_JUMP_DIFF; //coordinates different
						}
						else {
							birdY = 0;
							birdYTracker = birdY;
							birdThrust = false;
						}
					}
					else {
						birdYTracker = birdY;
						birdThrust = false;
					}
				}
				else if(!isSplash) {
					birdY += BIRD_FALL_DIFF;
					birdYTracker = birdY;
				}
				
				//update the BottomPipe and TopPipe locations
				bp1.setX(xLoc1);
				bp1.setY(yLoc1);
				bp2.setX(xLoc2);
				bp2.setY(yLoc2);
				tp1.setX(xLoc1);
				tp1.setY(yLoc1-PIPE_GAP-PIPE_HEIGHT); //ensure tp1 placed in proper location
				tp2.setX(xLoc2);
				tp2.setY(yLoc2-PIPE_GAP-PIPE_HEIGHT); //ensure tp2 placed in proper location
				
				if(!isSplash) {
					bird.setX(birdX);
					bird.setY(birdY);
					pgs.setBird(bird);
				}
				
				//set the BottomPipe and TopPipe local variables in PlayGameScreen by parsing the local variables
				pgs.setBottomPipe(bp1, bp2);
				pgs.setTopPipe(tp1, tp2);
				
				if(!isSplash && bird.getWidth() != -1) { //need the second part because if bird not on-screen, cannot get image width and have cascading error in collision
					collisionDetection(bp1, bp2, tp1, tp2, bird);
					updateScore(bp1, bp2, bird);
				}
				
				//update pgs's JPanel
				topPanel.revalidate();
				topPanel.repaint();
				
				//update the time-tracking variable after all operations completed
				startTime = System.currentTimeMillis();
			}
		}
	}
	
	/**
	 * Calculates a random int for the bottom pipe's placement
	 * @return int
	 */
	private int bottomPipeLoc() {
		int temp = 0;
		//iterate until temp is a value that allows both pipes to be onscreen
		while(temp <= PIPE_GAP+50 || temp >= SCREEN_HEIGHT-PIPE_GAP) {
			temp = (int) ((double) Math.random()*((double)SCREEN_HEIGHT));
		}
		return temp;
	}
	
	/**
	 * Method that checks whether the score needs to be updated
	 * @param bp1 First BottomPipe object
	 * @param bp2 Second BottomPipe object
	 * @param bird Bird object
	 */
	private void updateScore(BottomPipe bp1, BottomPipe bp2, Bird bird) {
		if(bp1.getX() + PIPE_WIDTH < bird.getX() && bp1.getX() + PIPE_WIDTH > bird.getX() - X_MOVEMENT_DIFFERENCE) {
			pgs.incrementJump();
		}
		else if(bp2.getX() + PIPE_WIDTH < bird.getX() && bp2.getX() + PIPE_WIDTH > bird.getX() - X_MOVEMENT_DIFFERENCE) {
			pgs.incrementJump();
		}
	}
	
	/**
	 * Method to test whether a collision has occurred
	 * @param bp1 First BottomPipe object
	 * @param bp2 Second BottomPipe object
	 * @param tp1 First TopPipe object
	 * @param tp2 Second TopPipe object
	 * @param bird Bird object
	 */
	private void collisionDetection(BottomPipe bp1, BottomPipe bp2, TopPipe tp1, TopPipe tp2, Bird bird) {
		collisionHelper(bird.getRectangle(), bp1.getRectangle(), bird.getBI(), bp1.getBI());
		collisionHelper(bird.getRectangle(), bp2.getRectangle(), bird.getBI(), bp2.getBI());
		collisionHelper(bird.getRectangle(), tp1.getRectangle(), bird.getBI(), tp1.getBI());
		collisionHelper(bird.getRectangle(), tp2.getRectangle(), bird.getBI(), tp2.getBI());
		
		if(bird.getY() + BIRD_HEIGHT > SCREEN_HEIGHT*7/8) { //ground detection
			pgs.sendText("Game Over");
			loopVar = false;
			gamePlay = false; //game has ended
		}
	}
	
	/**
	 * Helper method to test the Bird object's potential collision with a pipe object.
	 * @param r1 The Bird's rectangle component
	 * @param r2 Collision component rectangle
	 * @param b1 The Bird's BufferedImage component
	 * @param b2 Collision component BufferedImage
	 */
	private void collisionHelper(Rectangle r1, Rectangle r2, BufferedImage b1, BufferedImage b2) {
		if(r1.intersects(r2)) {
			Rectangle r = r1.intersection(r2);
			
			int firstI = (int) (r.getMinX() - r1.getMinX()); //firstI is the first x-pixel to iterate from
			int firstJ = (int) (r.getMinY() - r1.getMinY()); //firstJ is the first y-pixel to iterate from
			int bp1XHelper = (int) (r1.getMinX() - r2.getMinX()); //helper variables to use when referring to collision object
			int bp1YHelper = (int) (r1.getMinY() - r2.getMinY());
			
			for(int i = firstI; i < r.getWidth() + firstI; i++) { //
				for(int j = firstJ; j < r.getHeight() + firstJ; j++) {
					if((b1.getRGB(i, j) & 0xFF000000) != 0x00 && (b2.getRGB(i + bp1XHelper, j + bp1YHelper) & 0xFF000000) != 0x00) {
						pgs.sendText("Game Over");
						loopVar = false; //stop the game loop
						gamePlay = false; //game has ended
						break;
					}
				}
			}
		}
	}
}

Step 12: You're Done

Picture of You're Done

Now the easy part here: export your project as a Runnable JAR file. Change the file type from .jar to .zip. Navigate into the zip file and create a folder called "resources". Inside of this folder, deposit the three resource images we use (or your own): "blue_bird.png", "tube_bottom.png", and "tube_top.png". Change the file type back to .jar. You should be able to run your game now!


*Whew*

Hopefully that was easy enough to understand. Please let me know if there's anything I can do to make this tutorial easier to follow. I had a good time writing this program, and hopefully you do too! If you liked this Instructable, please vote for it in the Gaming Contest!

Comments

ChristopherM302 (author)2017-12-03

Hey.

I have one problem after writing all the code:

1: in the TopClass() {} I get a " class is not abstract and does not override abstract method keyReleased()" error.

do you know why this problem might occur?

Hey!

I believe the issue is you need to make sure you have "implements KeyListener" following the TopClass declaration, i.e., "public class TopClass implements KeyListener" {...}

thanks for the quick reply!

my class looks like this :

public class TopClass implements ActionListener, KeyListener {}

I'm using BlueJ.

By chance, does your KeyReleased function look like this: "public void keyPressed(KeyEvent e)" {...}? I'm wondering if either you haven't included this function or you have the wrong function signature

Thanks I hadn't implemented the KeyReleased() function. D'OH!

But still. When i start the program, only the welcome screen is showing. without pictures and only the text, no moving background and fades :/ then i get an nullpointerexception when I press the start button:

Exception in thread "Thread-1" java.lang.NullPointerException at TopClass$3.run(TopClass.java:173)

Im new to programming, so apologies if I ask dumb questions!

MILOisSuperMan (author)2017-10-17

why the bird and tubes doesn't appear?

leet304 (author)2017-05-03

I am having an issue with doing this project on mac.

I had to download a file viewer to insert a folder into the zipped file without unzipping it. But when I go to open the file when changed back to .jar it doesn't open. Any ideas?

Stratofortress (author)leet3042017-05-06

I apologize for my slow response and my lack of familiarity with macs. When I was writing this, I remember the .jar failing to open when clicking on it - the reason I found was there was an exception being thrown because the resources could not be found...simply because I hadn't included the resources directory *facepalm*. Try to run the .jar file from the command line (I'm not sure how that works in macs). If there's an exception being thrown, it'll show up in the console, and we can further debug this. My thought is the process you used did some actual file compression of the resources, resulting in a FileNotFoundException.

leet304 (author)Stratofortress2017-05-17

I got it to run great with BlueJ compiler. One thing I am trying to do is set it to where when you get past a set number of successful jumps the speed with increase. The problem Im having is that those two variables are on separate files.

leet304 (author)Stratofortress2017-05-08

Okay so i downloaded your .jar file and it runs great. Maybe i can use that and just alter some of the visuals.

leet304 (author)Stratofortress2017-05-08

Thank you for your reply man. I ran it in terminal on my mac and it doesn't give any messages. I can run it on my school PC, but the game doesn't always run right. Sometimes the bird wont show up and sometimes the bird wont move.

JavaExplorator (author)2016-05-02

Hi, thank you for this very interesting guide.
The Flappy Bird.jar file is working, but the source code isn't, when i copy your source code (classes), the game doesn't start. Here is the error that eclipse gives me:

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException

at sun.awt.SunToolkit.getImageFromHash(Unknown Source)

at sun.awt.SunToolkit.getImage(Unknown Source)

at theEndlessJumper.TopClass.buildFrame(TopClass.java:82)

at theEndlessJumper.TopClass.access$1(TopClass.java:81)

at theEndlessJumper.TopClass$1.run(TopClass.java:65)

at java.awt.event.InvocationEvent.dispatch(Unknown Source)

at java.awt.EventQueue.dispatchEventImpl(Unknown Source)

at java.awt.EventQueue.access$500(Unknown Source)

at java.awt.EventQueue$3.run(Unknown Source)

at java.awt.EventQueue$3.run(Unknown Source)

at java.security.AccessController.doPrivileged(Native Method)

at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)

at java.awt.EventQueue.dispatchEvent(Unknown Source)

at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)

at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)

at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)

at java.awt.EventDispatchThread.pumpEvents(Unknown Source)

at java.awt.EventDispatchThread.pumpEvents(Unknown Source)

at java.awt.EventDispatchThread.run(Unknown Source)


I tried to solve it, but I wasn't able to, it would be realy nice if you could help me.

Thanks for the comment JavaExplorator, I'll see if I can help. I suspect the NullPointerException is derived from the compiler not finding the image resources. If you refer to step 1, you will see a description on how and where to copy these images for the program to compile properly (from the .zip file named 'resources' at the end of step 1). Let me know if you already tried this and I will see what else could be done!

Oh I see, I did put it in the bin folder but not inside the package folder, thats why it didn't work. Thanks, works perfectly now !

VoxelCraft (author)2016-03-26

Nice! I tried this, but when i run it it just says "How would you like to open this file?" and shows options for how I would open it. :|

What does the program icon look like? Is it blank, or does it have a sort of coffee cup?

About This Instructable

11,605views

26favorites

License:

Bio: I vastly enjoy DIY projects, especially those involving woodworking. I'm an avid Java programmer, computer animator, and electronics enthusiast.
More by Stratofortress:Java Game Programming Tutorial - Flappy Bird ReduxHarmonographs: a Subtle ArtformHow to Merge PDF Documents
Add instructable to: