Wednesday, 2 July 2014

Java - Game Development - Series 1 - Bubbles

Here we go folks, this is hopefully an interesting - if long winded - series of videos of my live coding you a game in Java.


Game Idea
The game is a pretty simple idea, its just bubbles on a screen, when you click on them and they have a matching colour neighbour they "pop", the columns drop down and then the empty columns shuffle to the right.

Scoring is each bubble is worth 2 to the power of the number of bubbles popped... so 2 + 4 + 8 + 16 etc for each bubble... and then when there are no more moves its game over.

There's no menu, no reset, no confusion... We just need to double buffer the frame and loop the game around based on the mouse clicking...

Our development environment is the Java Development Kit, so you can pretty much get this set up for ANY platform - your PC, your Mac, on your tablet, your phone - so give it a try...

Video Play List
You can watch the whole - silent (I hope) video series here - just throw on tunes of your choice and let the play list run away with you.

Video Series
Individually, with my explanations...


That's of course for Linux, my chosen operating system for my development series...

Once built I can carry the .class file to ANY machine able to run Java and run it, pretty neat!


So that's "Hello World"...


That's the double buffering on the Frame, and the loop.


Right the first iteration we have the grid data and we draw it with rectangles...


Now we need to create the images, load them, and draw them because lets face it, Bubbles are round, not square!


Interacting with the mouse means mapping the click point to the bubble being pressed on.


Once we click on something, we really need to pop the bubbles around it of the same colour... This leaves a gap...


So now we move the Bubbles down in each column as they leave gaps.


This is a bit of debugging, but we shuffle the empty columns from left to right.


Finally, we score each bubble popped in a scaling factor and we add a check for no more moves which sets game over.

Summing Up
In our code we have created a lot of functions, and you could see in the one code file by the end I was moving around the code a lot, but had no structure, functions could be anywhere.  I'd perhaps go back and order the functions alphabetically, or into regions in other languages (C++ or C#) just to make things easier to maintain and move around.

The code itself is not bad, it's not great, but it is a lot better than other code I've seen created on the fly like this, and I've added the minimalist comments just to help guide people through the code.

During the development you saw my use of "System.out.println", I used this to help debug because I was just coding with an editor & the compiler, if I had an IDE (eclipse perhaps) set up with a debugger I'd not have used that development hack to get into the look of things.

I'd also have to say the code is not thread safe, I could be processing the grid whilst a click comes in on platforms which thread differently... So beware of that.

Your Development
If you've got this far, and got the code, then you can follow along how the current code was built up... But what could  you do to improve the code?

Well, I'd like to see you guys and gals come back to me with improved versions of this tackling the following:

i) Adding a Restart Button - when game over flags up - add a button/image to the bottom right to reset the game back to the start... Difficulty Easy.

ii) When each bubble pops, animate it popping... Difficulty Hard.

iii) Add an animation for the Score each "pop" summing up... Difficulty Medium.

iv) Add a high score chart, which saves the player name as 3 initials input by the user... Difficulty Easy/Medium.

v) Turn the game into an Applet to embed in a web page!... Difficulty Medium.

Please Note: iv and v above are not compatible, you can't save to disk from an applet.

Source Code
import java.*;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Cursor;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.event.*;
import java.util.Random;
import java.util.ArrayList;
import java.lang.Math;
import java.io.*;
import javax.imageio.*;

public class bubbles
extends Canvas
implements MouseListener, WindowListener
{
// The maximum number of colours
// we're going to handle
private final int c_MaxColours = 5;

// The screen size we're working to
private final int c_ScreenWidth = 450;
private final int c_ScreenHeight = 320;

// master running flag
private boolean m_GameRunning;

// Drawing strategy
private BufferStrategy m_Strategy;

// We need a score!
private long m_Score;
private int m_PoppedThisCycle;
private boolean m_GameOver;

// Our game has bubbles, which you click to pop
// and the bubbles move down in the heap... you
// can only click and pop bubbles with at least
// one neighbour the same colour...

// We need a grid of bubbles, and need to know
// the size of the bubbles...

private final int c_BubbleSize = 30;
private final int c_GridWidth = c_ScreenWidth / c_BubbleSize;
private final int c_GridHeight = c_ScreenHeight / c_BubbleSize;

// The live grid data
private int[][] m_Grid;
private int m_GridWidth, m_GridHeight;

// The images of our bubbles
private ArrayList<BufferedImage> m_Images;

/// Constructor
public bubbles ()
{
// The score & game over reset
m_Score = 0;
m_GameOver = false;

// Create the container frame for this
// application
JFrame l_container = new JFrame("Bubbles");

// Get a panel from the container
JPanel l_Panel = (JPanel)l_container.getContentPane();
l_Panel.setPreferredSize(new Dimension(c_ScreenWidth, c_ScreenHeight));
l_Panel.setLayout(null);

// Set up the canvas
this.setBounds(0, 0, c_ScreenWidth, c_ScreenHeight);
l_Panel.add(this);

// Set to self repaint, in order to speed up
// the drawing process
this.setIgnoreRepaint(true);

// Finally, make the window visible
l_container.pack();
l_container.setResizable(false);
l_container.setVisible(true);

// Add this as the close handler
l_container.addWindowListener(this);

// Add this to listen to its own
// mouse events
this.addMouseListener(this);

// Create double buffering
this.createBufferStrategy(2);
m_Strategy = this.getBufferStrategy();

if ( LoadImages() )
{
InitGrid();
}
else
{
//System.out.println ("Error, unable to load images");
}
}

// Draw the score
private void DrawScore(Graphics2D p_g)
{
p_g.setColor(Color.black);
p_g.drawString("Score: " + String.valueOf(m_Score), 10, c_ScreenHeight - 5);
}

// Draw the game over
private void DrawGameOver(Graphics2D p_g)
{
if ( m_GameOver )
{
p_g.setColor(Color.black);
p_g.drawString("GAME OVER", (c_ScreenWidth / 2) - 30, c_ScreenHeight - 5);
}
}

// Load all the images in order of the colour
// 0 = red
// 1 = blue
// 2 = yellow
// 3 = green
// 4 = pink
private boolean LoadImages()
{
boolean l_result = false;

ArrayList l_tempImages = new ArrayList();
if ( AddImageToArray(l_tempImages, LoadImage("red.png")))
{
if ( AddImageToArray(l_tempImages, LoadImage("blue.png")))
{
if ( AddImageToArray(l_tempImages,LoadImage("yellow.png")))
{
if ( AddImageToArray(l_tempImages, LoadImage("green.png")))
{
if ( AddImageToArray(l_tempImages, LoadImage("pink.png")))
{
l_result = true;
m_Images = l_tempImages;
}
}
}
}
}

return l_result;
}

// Add an image to the array
// if none are null, so its
// safe to add things
private boolean AddImageToArray(ArrayList p_ImageArray, BufferedImage p_Image)
{
boolean l_result = false;
if ( p_Image != null &&
p_ImageArray != null )
{
p_ImageArray.add(p_Image);
l_result  = true;
}
return l_result;
}

// load the buffered image
private BufferedImage LoadImage(String p_Filename)
{
BufferedImage l_result = null;
try
{
l_result = ImageIO.read(new File("./images/" + p_Filename));
}
catch (IOException ioe)
{
//System.out.println("File not found [" + p_Filename + "] in images folder");
}
return l_result;
}

// Initialise the grid with random colours
private void InitGrid()
{
// The grid itself
m_GridWidth = c_GridWidth;
m_GridHeight = c_GridHeight;
m_Grid = new int[m_GridHeight][m_GridWidth];

// Random number gen
Random l_Rand = new Random();
// Randomize the start position
for (int y = 0; y < m_GridHeight; ++y)
{
for (int x = 0; x < m_GridWidth; ++x)
{
int l_RandomColour = l_Rand.nextInt(c_MaxColours);
m_Grid[y][x] = l_RandomColour;
}
}
}

private void DrawGrid (Graphics2D l_g)
{
for (int y = 0; y < m_GridHeight; ++y)
{
for (int x = 0; x < m_GridWidth; ++x)
{
if ( m_Grid[y][x] != -1 ) // not a blank
{
DrawBubble(l_g, x, y);
}
}
}
}

private void DrawBubble (Graphics2D l_g, int p_x, int p_y)
{
if ( m_Grid[p_y][p_x] != -1 )
{
l_g.drawImage(
(BufferedImage)m_Images.get(m_Grid[p_y][p_x]),
(c_BubbleSize * p_x),
(c_BubbleSize * p_y),
null);
}
}

/// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-**
/// The window listener events
// Exit the game loop
public void windowClosing(WindowEvent e)
{
m_GameRunning = false;
}

public void windowOpened(WindowEvent e)
{
}

public void windowClosed(WindowEvent e)
{
}

public void windowActivated(WindowEvent e)
{
}

public void windowDeactivated(WindowEvent e)
{
}

public void windowIconified(WindowEvent e)
{
}

public void windowDeiconified (WindowEvent e)
{
}
/// *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-**

// Function to sleep the system
// for an amount of time - this is
// very rough and ready fornow
private void Yield ()
{
try
{
Thread.sleep(10);
}
catch (Exception e)
{
// Throw away
}
}

// Draw the time on screen
private void DrawTime (Graphics2D l_g)
{
l_g.setColor(Color.black);
l_g.drawString(String.valueOf(System.currentTimeMillis()), 10, 50);
}

// The function to draw something
private void Draw(Graphics2D l_g)
{
//DrawTime (l_g);
DrawGrid(l_g);
DrawScore(l_g);
DrawGameOver(l_g);
}

// Drops one column down
private boolean DropColumn (int p_Col)
{
boolean l_HasDropped = false;
for (int i = 0; i < m_GridHeight-1; ++i)
{
if ( m_Grid[i][p_Col] != -1 &&
m_Grid[i+1][p_Col] == -1 )
{
m_Grid[i+1][p_Col] = m_Grid[i][p_Col];
m_Grid[i][p_Col] = -1;
l_HasDropped = true;
}
}
return l_HasDropped;
}

// Now we need to shuffle bubbles down, whenever
// there is a gap below them....
private void ProcessDrop()
{
boolean l_flagged = false;
for (int i = 0; i < m_GridWidth; ++i)
{
if ( DropColumn(i) )
{
l_flagged = true;
}
}

if ( l_flagged )
{
ShuffleColumns();

if ( !CheckForMoves() )
{
m_GameOver = true;
}
}
}

private boolean IsColumnEmpty(int p_Col)
{
boolean l_empty = true;
for (int i = 0; i < m_GridHeight; ++i)
{
if ( m_Grid[i][p_Col] != -1 )
{
l_empty = false;
break;
}
}
if ( l_empty )
{
//System.out.println ("WE HAVE AN EMPTY COLUMN!");
}
return l_empty;
}

private void ShuffleColumnsFromLeftOf(int p_Col)
{
//System.out.println ("Shuffling columns left of " + p_Col);
if ( p_Col > 0 )
{
for (int i = p_Col; i > 0; --i)
{
for (int y = 0; y < m_GridHeight; ++y)
{
m_Grid[y][i] = m_Grid[y][i-1];
m_Grid[y][i-1] = -1;
}
}
}
}

// Empty columns, we'll collapse to the right
private void ShuffleColumns()
{
for (int i = 0; i < m_GridWidth; ++i)
{
if ( IsColumnEmpty(i) )
{
ShuffleColumnsFromLeftOf(i);
}
}
}

// The function to perform the game loop
public void Run()
{
m_GameRunning = true;

// Game timing/update delta
long l_lastLoopTime = System.currentTimeMillis();
long l_now;
long l_delta;

while ( m_GameRunning )
{
// Update the time
l_now = System.currentTimeMillis();
l_delta = l_now - l_lastLoopTime;
l_lastLoopTime = l_now;

// Now we need to draw?....
Graphics2D l_Graphics = (Graphics2D)m_Strategy.getDrawGraphics();

// Clear the graphics
l_Graphics.setColor(Color.white);
l_Graphics.fillRect(0, 0, c_ScreenWidth, c_ScreenHeight);

// Draw the game?
Draw(l_Graphics);

// Flip the buffer to show what's been draw
l_Graphics.dispose();
m_Strategy.show();

Yield();

ProcessDrop();
}
System.exit(0);
}

// Handle clicking on a bubble
private void ClickAt(int p_x, int p_y)
{
////System.out.println ("ClickAt " + p_x + "   " + p_y);


// Only process popping when we are NOT
// in game over
if ( !m_GameOver )
{
if ( p_x > 0 && p_y < (c_BubbleSize * m_GridWidth))
{
if ( p_y > 0 && p_y < (c_BubbleSize * m_GridHeight))
{
int l_px = (p_x / c_BubbleSize);
int l_py = (p_y / c_BubbleSize);

// Check whether this bubble can be popped
if ( CheckPop(l_px, l_py) )
{
// Now we can process the pop, and chose later to
// not "check pop" first, as you wish.
ProcessPop(100, l_px, l_py);
}

}
}
}
}

// Rather than just blank the target
// lets "pop" it and propagate the
// pops over to each neighbour
private void ProcessPop (int p_Colour, int p_x, int p_y)
{
// The tartget colour tells us whether
// we;re the start pop, or a follow up
int l_match = -1;
if ( p_Colour == 100 )
{
l_match = m_Grid[p_y][p_x]; // Get the colour target from the grid
m_PoppedThisCycle = 0; // First possible pop
}
else
{
l_match = p_Colour; // Take the input colour as our target
}

// Now, we've already checked pop, so technically
// don't need to do this, but we'll check, and let you
// carry on... to change the code as you want later
if ( l_match == m_Grid[p_y][p_x] )
{
// The neighbour index
int l_up = p_y - 1;
int l_down = p_y + 1;
int l_left = p_x - 1;
int l_right = p_x + 1;

// Pop the current position
m_Grid[p_y][p_x] = -1;

// Score the pop
++m_PoppedThisCycle;
m_Score += (2 * m_PoppedThisCycle);

// Pop up
if ( l_up > -1 )
{
ProcessPop(l_match, p_x, l_up);
}
// Pop down
if ( l_down < m_GridHeight )
{
ProcessPop(l_match, p_x, l_down);
}
// Pop left
if ( l_left > -1 )
{
ProcessPop(l_match, l_left, p_y);
}
// Pop right
if ( l_right < m_GridWidth )
{
ProcessPop(l_match, l_right, p_y);
}
}
}

// Function to check whether the
// bubble in the location can be
// popped, which means that its not
// already blank, and has a neighbour
// of the same colour.
private boolean CheckPop(int p_x, int p_y)
{
//System.out.println("CheckPop " + p_x + "  " + p_y);

boolean l_result = false;

int l_targetColour = m_Grid[p_y][p_x];

//System.out.println ("TargetColour is " + l_targetColour);

if ( l_targetColour != -1 )
{
// Get the indices for the neighbours
int l_up = p_y - 1;
int l_down = p_y + 1;
int l_left = p_x - 1;
int l_right = p_x + 1;

// Check upwards
if ( l_up > -1 )
{
if ( m_Grid[l_up][p_x] == l_targetColour )
{
l_result = true;
//System.out.println ("UP!");
}
}
// check down
if ( !l_result && l_down < m_GridHeight )
{
if ( m_Grid[l_down][p_x] == l_targetColour )
{
l_result = true;
//System.out.println ("DOWN!");
}
}
// Check left
if ( !l_result && l_left > -1 )
{
if ( m_Grid[p_y][l_left] == l_targetColour )
{
l_result = true;
//System.out.println ("LEFT!");
}
}
// check right
if ( !l_result && l_right < m_GridWidth )
{
if ( m_Grid[p_y][l_right] == l_targetColour )
{
//System.out.println ("RIGHT!");
l_result = true;
}
}
}

return l_result;
}

///************************************************
/// Mouse listener handlers
///************************************************
public void mouseClicked(MouseEvent e)
{
if ( e.getButton() == MouseEvent.BUTTON1 )
{
ClickAt(e.getX(), e.getY());
}
}

public void mousePressed(MouseEvent e)
{
}

public void mouseReleased(MouseEvent e)
{
}

public void mouseEntered(MouseEvent e)
{
}

public void mouseExited(MouseEvent e)
{
}
///************************************************

// This function checks the grid
// for a single move being available
private boolean CheckForMoves()
{
boolean l_result = false;

for (int y = 0; y < m_GridHeight; ++y)
{
for (int x = 0; x < m_GridWidth; ++x)
{
int l_colour = m_Grid[y][x];

// doh forgot the blanks!
if ( l_colour != -1 )
{
int l_up = y - 1;
int l_down = y + 1;
int l_left = x - 1;
int l_right = x + 1;

if ( l_up > 0 )
{
if ( m_Grid[l_up][x] == l_colour )
{
//System.out.println ("Move Remains [" + x + ", " + y + "] to up");
l_result = true;
break;
}
}
if ( l_down < m_GridHeight )
{
if ( m_Grid[l_down][x] == l_colour )
{
//System.out.println ("Move Remains [" + x + ", " + y + "] to down");
l_result = true;
break;
}
}
if ( l_left > 0 )
{
if ( m_Grid[y][l_left] == l_colour )
{
//System.out.println ("Move Remains [" + x + ", " + y + "] to left");
l_result = true;
break;
}
}
if ( l_right < m_GridWidth )
{
if ( m_Grid[y][l_right] == l_colour )
{
//System.out.println ("Move Remains [" + x + ", " + y + "] to right");
l_result = true;
break;
}
}
}
}

if ( l_result )
{
break;
}
}

return l_result;
}

/// The main entry point, creates
/// a copy of the game and starts it
/// running
public static void main (String[] p_args)
{
bubbles l_Instance = new bubbles();
l_Instance.Run();
}

}

2 comments:

  1. It is showing error..
    "Error, unable to load images"
    Please Help me out..!

    ReplyDelete
    Replies
    1. it can't load your images... It's being quite specific... Maybe they're not in the folder with the .class file... maybe they're not the right name... check your code.

      Delete