Showing posts with label Java 2D games. Show all posts
Showing posts with label Java 2D games. Show all posts

Step By Step Java 2D games tutorials For Beginners

Java 2D games tutorial

This is Java 2D games tutorial. In this tutorial, you will learn the basics of 2D game programming in Java. The Java 2D games tutorial is suitable for beginners and intermediate programmers.

Table of contents

Continue Reading

Sokoban With Java 2D games

Sokoban

In this part of the Java 2D games tutorial, we will create a Java Sokoban game clone.

Sokoban

Sokoban is another classic computer game. It was created in 1980 by Hiroyuki Imabayashi. Sokoban means a warehouse keeper in Japanese. The player pushes boxes around a maze. The objective is to place all boxes in designated locations.

Development

We control the sokoban object with cursor keys. We can also press the R key to restart the level. When all bags are placed on the destination areas, the game is finished. We draw "Completed" string in the left upper corner of the window.
Board.java
package sokoban;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import javax.swing.JPanel;

public class Board extends JPanel {

private final int OFFSET = 30;
private final int SPACE = 20;
private final int LEFT_COLLISION = 1;
private final int RIGHT_COLLISION = 2;
private final int TOP_COLLISION = 3;
private final int BOTTOM_COLLISION = 4;

private ArrayList walls = new ArrayList();
private ArrayList baggs = new ArrayList();
private ArrayList areas = new ArrayList();
private Player soko;
private int w = 0;
private int h = 0;
private boolean completed = false;

private String level =
" ######\n"
+ " ## #\n"
+ " ##$ #\n"
+ " #### $##\n"
+ " ## $ $ #\n"
+ "#### # ## # ######\n"
+ "## # ## ##### ..#\n"
+ "## $ $ ..#\n"
+ "###### ### #@## ..#\n"
+ " ## #########\n"
+ " ########\n";

public Board() {

addKeyListener(new TAdapter());
setFocusable(true);
initWorld();
}

public int getBoardWidth() {
return this.w;
}

public int getBoardHeight() {
return this.h;
}

public final void initWorld() {

int x = OFFSET;
int y = OFFSET;

Wall wall;
Baggage b;
Area a;


for (int i = 0; i < level.length(); i++) {

char item = level.charAt(i);

if (item == '\n') {
y += SPACE;
if (this.w < x) {
this.w = x;
}

x = OFFSET;
} else if (item == '#') {
wall = new Wall(x, y);
walls.add(wall);
x += SPACE;
} else if (item == '$') {
b = new Baggage(x, y);
baggs.add(b);
x += SPACE;
} else if (item == '.') {
a = new Area(x, y);
areas.add(a);
x += SPACE;
} else if (item == '@') {
soko = new Player(x, y);
x += SPACE;
} else if (item == ' ') {
x += SPACE;
}

h = y;
}
}

public void buildWorld(Graphics g) {

g.setColor(new Color(250, 240, 170));
g.fillRect(0, 0, this.getWidth(), this.getHeight());

ArrayList world = new ArrayList();
world.addAll(walls);
world.addAll(areas);
world.addAll(baggs);
world.add(soko);

for (int i = 0; i < world.size(); i++) {

Actor item = (Actor) world.get(i);

if ((item instanceof Player)
|| (item instanceof Baggage)) {
g.drawImage(item.getImage(), item.x() + 2, item.y() + 2, this);
} else {
g.drawImage(item.getImage(), item.x(), item.y(), this);
}

if (completed) {
g.setColor(new Color(0, 0, 0));
g.drawString("Completed", 25, 20);
}

}
}

@Override
public void paint(Graphics g) {
super.paint(g);
buildWorld(g);
}

class TAdapter extends KeyAdapter {

@Override
public void keyPressed(KeyEvent e) {

if (completed) {
return;
}


int key = e.getKeyCode();


if (key == KeyEvent.VK_LEFT) {
if (checkWallCollision(soko,
LEFT_COLLISION)) {
return;
}

if (checkBagCollision(LEFT_COLLISION)) {
return;
}

soko.move(-SPACE, 0);

} else if (key == KeyEvent.VK_RIGHT) {

if (checkWallCollision(soko,
RIGHT_COLLISION)) {
return;
}

if (checkBagCollision(RIGHT_COLLISION)) {
return;
}

soko.move(SPACE, 0);

} else if (key == KeyEvent.VK_UP) {

if (checkWallCollision(soko,
TOP_COLLISION)) {
return;
}

if (checkBagCollision(TOP_COLLISION)) {
return;
}

soko.move(0, -SPACE);

} else if (key == KeyEvent.VK_DOWN) {

if (checkWallCollision(soko,
BOTTOM_COLLISION)) {
return;
}

if (checkBagCollision(BOTTOM_COLLISION)) {
return;
}

soko.move(0, SPACE);

} else if (key == KeyEvent.VK_R) {
restartLevel();
}

repaint();
}
}

private boolean checkWallCollision(Actor actor, int type) {

if (type == LEFT_COLLISION) {

for (int i = 0; i < walls.size(); i++) {
Wall wall = (Wall) walls.get(i);
if (actor.isLeftCollision(wall)) {
return true;
}
}
return false;

} else if (type == RIGHT_COLLISION) {

for (int i = 0; i < walls.size(); i++) {
Wall wall = (Wall) walls.get(i);
if (actor.isRightCollision(wall)) {
return true;
}
}
return false;

} else if (type == TOP_COLLISION) {

for (int i = 0; i < walls.size(); i++) {
Wall wall = (Wall) walls.get(i);
if (actor.isTopCollision(wall)) {
return true;
}
}
return false;

} else if (type == BOTTOM_COLLISION) {

for (int i = 0; i < walls.size(); i++) {
Wall wall = (Wall) walls.get(i);
if (actor.isBottomCollision(wall)) {
return true;
}
}
return false;
}
return false;
}

private boolean checkBagCollision(int type) {

if (type == LEFT_COLLISION) {

for (int i = 0; i < baggs.size(); i++) {

Baggage bag = (Baggage) baggs.get(i);
if (soko.isLeftCollision(bag)) {

for (int j=0; j < baggs.size(); j++) {
Baggage item = (Baggage) baggs.get(j);
if (!bag.equals(item)) {
if (bag.isLeftCollision(item)) {
return true;
}
}
if (checkWallCollision(bag,
LEFT_COLLISION)) {
return true;
}
}
bag.move(-SPACE, 0);
isCompleted();
}
}
return false;

} else if (type == RIGHT_COLLISION) {

for (int i = 0; i < baggs.size(); i++) {

Baggage bag = (Baggage) baggs.get(i);
if (soko.isRightCollision(bag)) {
for (int j=0; j < baggs.size(); j++) {

Baggage item = (Baggage) baggs.get(j);
if (!bag.equals(item)) {
if (bag.isRightCollision(item)) {
return true;
}
}
if (checkWallCollision(bag,
RIGHT_COLLISION)) {
return true;
}
}
bag.move(SPACE, 0);
isCompleted();
}
}
return false;

} else if (type == TOP_COLLISION) {

for (int i = 0; i < baggs.size(); i++) {

Baggage bag = (Baggage) baggs.get(i);
if (soko.isTopCollision(bag)) {
for (int j = 0; j < baggs.size(); j++) {

Baggage item = (Baggage) baggs.get(j);
if (!bag.equals(item)) {
if (bag.isTopCollision(item)) {
return true;
}
}
if (checkWallCollision(bag,
TOP_COLLISION)) {
return true;
}
}
bag.move(0, -SPACE);
isCompleted();
}
}

return false;

} else if (type == BOTTOM_COLLISION) {

for (int i = 0; i < baggs.size(); i++) {

Baggage bag = (Baggage) baggs.get(i);
if (soko.isBottomCollision(bag)) {
for (int j = 0; j < baggs.size(); j++) {

Baggage item = (Baggage) baggs.get(j);
if (!bag.equals(item)) {
if (bag.isBottomCollision(item)) {
return true;
}
}
if (checkWallCollision(bag,
BOTTOM_COLLISION)) {
return true;
}
}
bag.move(0, SPACE);
isCompleted();
}
}
}

return false;
}

public void isCompleted() {

int num = baggs.size();
int compl = 0;

for (int i = 0; i < num; i++) {
Baggage bag = (Baggage) baggs.get(i);
for (int j = 0; j < num; j++) {
Area area = (Area) areas.get(j);
if (bag.x() == area.x()
&& bag.y() == area.y()) {
compl += 1;
}
}
}

if (compl == num) {
completed = true;
repaint();
}
}

public void restartLevel() {

areas.clear();
baggs.clear();
walls.clear();
initWorld();
if (completed) {
completed = false;
}
}
}
The game is simplified. It only provides the very basic functionality. The code is than easier to understand. The game has one level.
private final int OFFSET = 30;
private final int SPACE = 20;
private final int LEFT_COLLISION = 1;
private final int RIGHT_COLLISION = 2;
private final int TOP_COLLISION = 3;
private final int BOTTOM_COLLISION = 4;
The wall image size is 20x20px. This reflects the SPACE constant. The OFFSET is the distance between the borders of the window and the game world. There are four types of collisions. Each one is represented by a numerical constant.
private ArrayList walls = new ArrayList();
private ArrayList baggs = new ArrayList();
private ArrayList areas = new ArrayList();
The walls, baggs and areas are special containers, which will hold all the walls, baggs and areas of the game.
private String level =
" ######\n"
+ " ## #\n"
+ " ##$ #\n"
+ " #### $##\n"
+ " ## $ $ #\n"
+ "#### # ## # ######\n"
+ "## # ## ##### ..#\n"
+ "## $ $ ..#\n"
+ "###### ### #@## ..#\n"
+ " ## #########\n"
+ " ########\n";
This is the level of the game. Except for the space, there are five characters. The hash (#) stands for a wall. The dollar ($) represents the box to move. The dot (.) character represents the place where we must move the box. The at character (@) is the sokoban. And finally the new line character (\n) starts a new row of the world.
public final void initWorld() {

int x = OFFSET;
int y = OFFSET;
...
The initWorld() method initiates the game world. It goes through the level string and fills the above mentioned lists.
} else if (item == '$') {
b = new Baggage(x, y);
baggs.add(b);
x += SPACE;
In case of the dollar character, we create a Baggage object. The object is appended to the baggs list. The x variable is increased accordingly.
public void buildWorld(Graphics g) {
...
The buildWorld() method draws the game world on the window.
ArrayList world = new ArrayList();
world.addAll(walls);
world.addAll(areas);
world.addAll(baggs);
world.add(soko);
We create a world list which includes all objects of the game.
for (int i = 0; i < world.size(); i++) {

Actor item = (Actor) world.get(i);

if ((item instanceof Player)
|| (item instanceof Baggage)) {
g.drawImage(item.getImage(), item.x() + 2, item.y() + 2, this);
} else {
g.drawImage(item.getImage(), item.x(), item.y(), this);
}
...
We iterate through the world container and draw the objects. The player and the baggage images are a bit smaller. We add 2px to their coordinates to center them.
if (completed) {
g.setColor(new Color(0, 0, 0));
g.drawString("Completed", 25, 20);
}
If the level is completed, we draw "Completed" in the upper left corner of the window.
if (key == KeyEvent.VK_LEFT) {
if (checkWallCollision(soko,
LEFT_COLLISION)) {
return;
}

if (checkBagCollision(LEFT_COLLISION)) {
return;
}

soko.move(-SPACE, 0);
...
Inside the keyPressed() method, we check what keys were pressed. We control the sokoban object with the cursor keys. If we press the left cursor key, we check if the sokoban collides with a wall or with a baggage. If it does not, we move the sokoban to the left.
} else if (key == KeyEvent.VK_R) {
restartLevel();
}
We restart the level, if we press the R key.
if (type == LEFT_COLLISION) {

for (int i = 0; i < walls.size(); i++) {
Wall wall = (Wall) walls.get(i);
if (actor.isLeftCollision(wall)) {
return true;
}
}
return false;
...
The checkWallCollision() method was created to ensure, that the sokoban or a baggage don't pass the wall. There are four types of collisions. The above lines check for the left collision.
private boolean checkBagCollision(int type) {

}
The checkBagCollision() is a bit more involved. A baggage can collide with a wall, with a sokoban object or with another baggage. The baggage can be moved only if it collides with a sokoban and does not collide with another baggage or a wall. When the baggage is moved, it is time to check, if the level is completed by calling the isCompleted() method.
for (int i = 0; i < num; i++) {
Baggage bag = (Baggage) baggs.get(i);
for (int j = 0; j < num; j++) {
Area area = (Area) areas.get(j);
if (bag.x() == area.x()
&& bag.y() == area.y()) {
compl += 1;
}
}
}
The isCompleted() method checks, if the level is completed. We get the number of bags. We compare the x, y coordinates of all the bags and the destination areas.
if (compl == num) {
completed = true;
repaint();
}
The game is finished, when the completed variable equals the number of bags in the game.
public void restartLevel() {

areas.clear();
baggs.clear();
walls.clear();
initWorld();
if (completed) {
completed = false;
}
}
If we do some bad move, we can restart the level. We delete all objects from the important lists and initiate the world again. The completed variable is set to false.
Actor.java
package sokoban;

import java.awt.Image;

public class Actor {

private final int SPACE = 20;

private int x;
private int y;
private Image image;

public Actor(int x, int y) {
this.x = x;
this.y = y;
}

public Image getImage() {
return this.image;
}

public void setImage(Image img) {
image = img;
}

public int x() {
return this.x;
}

public int y() {
return this.y;
}

public void setX(int x) {
this.x = x;
}

public void setY(int y) {
this.y = y;
}

public boolean isLeftCollision(Actor actor) {
if (((this.x() - SPACE) == actor.x()) &&
(this.y() == actor.y())) {
return true;
} else {
return false;
}
}

public boolean isRightCollision(Actor actor) {
if (((this.x() + SPACE) == actor.x())
&& (this.y() == actor.y())) {
return true;
} else {
return false;
}
}

public boolean isTopCollision(Actor actor) {
if (((this.y() - SPACE) == actor.y()) &&
(this.x() == actor.x())) {
return true;
} else {
return false;
}
}

public boolean isBottomCollision(Actor actor) {
if (((this.y() + SPACE) == actor.y())
&& (this.x() == actor.x())) {
return true;
} else {
return false;
}
}
}
This is the Actor class. The class is a base class for other actors in the game. It encapsulates the basic functionality of an object in the Sokoban game.
public boolean isLeftCollision(Actor actor) {
if (((this.x() - SPACE) == actor.x()) &&
(this.y() == actor.y())) {
return true;
} else {
return false;
}
}
This method checks, if the actor collides with another actor (wall, baggage, sokoban) to the left.
Wall.java
package sokoban;

import java.awt.Image;
import java.net.URL;
import javax.swing.ImageIcon;

public class Wall extends Actor {

private Image image;

public Wall(int x, int y) {
super(x, y);

URL loc = this.getClass().getResource("wall.png");
ImageIcon iia = new ImageIcon(loc);
image = iia.getImage();
this.setImage(image);

}
}
This is the Wall class. It inherits from the Actor class. Upon construction, it loads a wall image from the filesystem.
Player.java
package sokoban;

import java.awt.Image;
import java.net.URL;
import javax.swing.ImageIcon;

public class Player extends Actor {

public Player(int x, int y) {
super(x, y);

URL loc = this.getClass().getResource("sokoban.png");
ImageIcon iia = new ImageIcon(loc);
Image image = iia.getImage();
this.setImage(image);
}

public void move(int x, int y) {
int nx = this.x() + x;
int ny = this.y() + y;
this.setX(nx);
this.setY(ny);
}
}
This is the Player class. It is the class to create the sokoban object.
public void move(int x, int y) {
int nx = this.x() + x;
int ny = this.y() + y;
this.setX(nx);
this.setY(ny);
}
This class has a move() method, which moves the object inside the world.
Baggage.java
package sokoban;

import java.awt.Image;
import java.net.URL;
import javax.swing.ImageIcon;

public class Baggage extends Actor {

public Baggage(int x, int y) {
super(x, y);
URL loc = this.getClass().getResource("baggage.png");
ImageIcon iia = new ImageIcon(loc);
Image image = iia.getImage();
this.setImage(image);
}

public void move(int x, int y) {
int nx = this.x() + x;
int ny = this.y() + y;
this.setX(nx);
this.setY(ny);
}
}
This is the class for the Baggage object. This object is movable, so it has the move() method also.
Area.java
package sokoban;

import java.awt.Image;
import java.net.URL;
import javax.swing.ImageIcon;

public class Area extends Actor {

public Area(int x, int y) {
super(x, y);

URL loc = this.getClass().getResource("area.png");
ImageIcon iia = new ImageIcon(loc);
Image image = iia.getImage();
this.setImage(image);
}
}
The Area class. It is the object, on which we try to place the baggages.
Sokoban.java
package sokoban;

import javax.swing.JFrame;


public final class Sokoban extends JFrame {

private final int OFFSET = 30;

public Sokoban() {
InitUI();
}

public void InitUI() {
Board board = new Board();
add(board);

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(board.getBoardWidth() + OFFSET,
board.getBoardHeight() + 2*OFFSET);
setLocationRelativeTo(null);
setTitle("Sokoban");
}


public static void main(String[] args) {
Sokoban sokoban = new Sokoban();
sokoban.setVisible(true);
}
}
This is the main class.
Sokoban
Figure: Sokoban
This was the Sokoban game.
Continue Reading

Minesweeper With Java 2D games

Minesweeper

In this part of the Java 2D games tutorial, we will create a Minesweeper game clone.

Minesweeper

Minesweeper is a popular board game shipped with many operating systems by default. The goal of the game is to sweep all mines from a mine field. If the player clicks on the cell which contains a mine, the mine detonates and the game is over. Further a cell can contain a number or it can be blank. The number indicates how many mines are adjacent to this particular cell. We set a mark on a cell by right clicking on it. This way we indicate, that we believe, there is a mine.
The following Minesweeper clone is a modified and simplified version of an Java applet which I found at javaside.com.
Board.java
package mines;

import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import java.util.Random;

import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class Board extends JPanel {

private final int NUM_IMAGES = 13;
private final int CELL_SIZE = 15;

private final int COVER_FOR_CELL = 10;
private final int MARK_FOR_CELL = 10;
private final int EMPTY_CELL = 0;
private final int MINE_CELL = 9;
private final int COVERED_MINE_CELL = MINE_CELL + COVER_FOR_CELL;
private final int MARKED_MINE_CELL = COVERED_MINE_CELL + MARK_FOR_CELL;

private final int DRAW_MINE = 9;
private final int DRAW_COVER = 10;
private final int DRAW_MARK = 11;
private final int DRAW_WRONG_MARK = 12;

private int[] field;
private boolean inGame;
private int mines_left;
private Image[] img;
private int mines = 40;
private int rows = 16;
private int cols = 16;
private int all_cells;
private JLabel statusbar;


public Board(JLabel statusbar) {

this.statusbar = statusbar;

img = new Image[NUM_IMAGES];

for (int i = 0; i < NUM_IMAGES; i++) {
img[i] =
(new ImageIcon(this.getClass().getResource((i)
+ ".png"))).getImage();
}

setDoubleBuffered(true);

addMouseListener(new MinesAdapter());
newGame();
}


public void newGame() {

Random random;
int current_col;

int i = 0;
int position = 0;
int cell = 0;

random = new Random();
inGame = true;
mines_left = mines;

all_cells = rows * cols;
field = new int[all_cells];

for (i = 0; i < all_cells; i++)
field[i] = COVER_FOR_CELL;

statusbar.setText(Integer.toString(mines_left));


i = 0;
while (i < mines) {

position = (int) (all_cells * random.nextDouble());

if ((position < all_cells) &&
(field[position] != COVERED_MINE_CELL)) {


current_col = position % cols;
field[position] = COVERED_MINE_CELL;
i++;

if (current_col > 0) {
cell = position - 1 - cols;
if (cell >= 0)
if (field[cell] != COVERED_MINE_CELL)
field[cell] += 1;
cell = position - 1;
if (cell >= 0)
if (field[cell] != COVERED_MINE_CELL)
field[cell] += 1;

cell = position + cols - 1;
if (cell < all_cells)
if (field[cell] != COVERED_MINE_CELL)
field[cell] += 1;
}

cell = position - cols;
if (cell >= 0)
if (field[cell] != COVERED_MINE_CELL)
field[cell] += 1;
cell = position + cols;
if (cell < all_cells)
if (field[cell] != COVERED_MINE_CELL)
field[cell] += 1;

if (current_col < (cols - 1)) {
cell = position - cols + 1;
if (cell >= 0)
if (field[cell] != COVERED_MINE_CELL)
field[cell] += 1;
cell = position + cols + 1;
if (cell < all_cells)
if (field[cell] != COVERED_MINE_CELL)
field[cell] += 1;
cell = position + 1;
if (cell < all_cells)
if (field[cell] != COVERED_MINE_CELL)
field[cell] += 1;
}
}
}
}


public void find_empty_cells(int j) {

int current_col = j % cols;
int cell;

if (current_col > 0) {
cell = j - cols - 1;
if (cell >= 0)
if (field[cell] > MINE_CELL) {
field[cell] -= COVER_FOR_CELL;
if (field[cell] == EMPTY_CELL)
find_empty_cells(cell);
}

cell = j - 1;
if (cell >= 0)
if (field[cell] > MINE_CELL) {
field[cell] -= COVER_FOR_CELL;
if (field[cell] == EMPTY_CELL)
find_empty_cells(cell);
}

cell = j + cols - 1;
if (cell < all_cells)
if (field[cell] > MINE_CELL) {
field[cell] -= COVER_FOR_CELL;
if (field[cell] == EMPTY_CELL)
find_empty_cells(cell);
}
}

cell = j - cols;
if (cell >= 0)
if (field[cell] > MINE_CELL) {
field[cell] -= COVER_FOR_CELL;
if (field[cell] == EMPTY_CELL)
find_empty_cells(cell);
}

cell = j + cols;
if (cell < all_cells)
if (field[cell] > MINE_CELL) {
field[cell] -= COVER_FOR_CELL;
if (field[cell] == EMPTY_CELL)
find_empty_cells(cell);
}

if (current_col < (cols - 1)) {
cell = j - cols + 1;
if (cell >= 0)
if (field[cell] > MINE_CELL) {
field[cell] -= COVER_FOR_CELL;
if (field[cell] == EMPTY_CELL)
find_empty_cells(cell);
}

cell = j + cols + 1;
if (cell < all_cells)
if (field[cell] > MINE_CELL) {
field[cell] -= COVER_FOR_CELL;
if (field[cell] == EMPTY_CELL)
find_empty_cells(cell);
}

cell = j + 1;
if (cell < all_cells)
if (field[cell] > MINE_CELL) {
field[cell] -= COVER_FOR_CELL;
if (field[cell] == EMPTY_CELL)
find_empty_cells(cell);
}
}

}

public void paint(Graphics g) {

int cell = 0;
int uncover = 0;


for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {

cell = field[(i * cols) + j];

if (inGame && cell == MINE_CELL)
inGame = false;

if (!inGame) {
if (cell == COVERED_MINE_CELL) {
cell = DRAW_MINE;
} else if (cell == MARKED_MINE_CELL) {
cell = DRAW_MARK;
} else if (cell > COVERED_MINE_CELL) {
cell = DRAW_WRONG_MARK;
} else if (cell > MINE_CELL) {
cell = DRAW_COVER;
}


} else {
if (cell > COVERED_MINE_CELL)
cell = DRAW_MARK;
else if (cell > MINE_CELL) {
cell = DRAW_COVER;
uncover++;
}
}

g.drawImage(img[cell], (j * CELL_SIZE),
(i * CELL_SIZE), this);
}
}


if (uncover == 0 && inGame) {
inGame = false;
statusbar.setText("Game won");
} else if (!inGame)
statusbar.setText("Game lost");
}


class MinesAdapter extends MouseAdapter {
public void mousePressed(MouseEvent e) {

int x = e.getX();
int y = e.getY();

int cCol = x / CELL_SIZE;
int cRow = y / CELL_SIZE;

boolean rep = false;


if (!inGame) {
newGame();
repaint();
}


if ((x < cols * CELL_SIZE) && (y < rows * CELL_SIZE)) {

if (e.getButton() == MouseEvent.BUTTON3) {

if (field[(cRow * cols) + cCol] > MINE_CELL) {
rep = true;

if (field[(cRow * cols) + cCol] <= COVERED_MINE_CELL) {
if (mines_left > 0) {
field[(cRow * cols) + cCol] += MARK_FOR_CELL;
mines_left--;
statusbar.setText(Integer.toString(mines_left));
} else
statusbar.setText("No marks left");
} else {

field[(cRow * cols) + cCol] -= MARK_FOR_CELL;
mines_left++;
statusbar.setText(Integer.toString(mines_left));
}
}

} else {

if (field[(cRow * cols) + cCol] > COVERED_MINE_CELL) {
return;
}

if ((field[(cRow * cols) + cCol] > MINE_CELL) &&
(field[(cRow * cols) + cCol] < MARKED_MINE_CELL)) {

field[(cRow * cols) + cCol] -= COVER_FOR_CELL;
rep = true;

if (field[(cRow * cols) + cCol] == MINE_CELL)
inGame = false;
if (field[(cRow * cols) + cCol] == EMPTY_CELL)
find_empty_cells((cRow * cols) + cCol);
}
}

if (rep)
repaint();

}
}
}
}
First we will define the constants used in our game.
private final int NUM_IMAGES = 13;
private final int CELL_SIZE = 15;
There are 13 images used in this game. A cell can be surrounded by maximum of 8 mines, so we need numbers 1..8. We need images for an empty cell, a mine, a covered cell, a marked cell and finally for a wrongly marked cell. The size of each of the images is 15x15 px.
private final int COVER_FOR_CELL = 10;
private final int MARK_FOR_CELL = 10;
private final int EMPTY_CELL = 0;
...
A mine field is an array of numbers. For example 0 denotes an empty cell. Number 10 is used for a cell cover as well as for a mark. Using constants improves readability of the code.
private int[] field;
The field is an array of numbers. Each cell in the field has a specific number. E.g. a mine cell has number 9. A cell with number 2, meaning it is adjacent to two mines, has number two. The numbers are added. For example, a covered mine has number 19, 9 for the mine and 10 for the cell cover etc.
private int mines = 40;
private int rows = 16;
private int cols = 16;
The minefield in our game has 40 hidden mines. There are 16 rows and 16 columns in this field. So there are 256 cells together in the minefield.
for (int i = 0; i < NUM_IMAGES; i++) {
img[i] =
(new ImageIcon(this.getClass().getResource((i)
+ ".png"))).getImage();
}
Here we load our images into the image array. The images are named 0.png, 1.png ... 12.png.
The newGame() initiates the Minesweeper game.
all_cells = rows * cols;
field = new int[all_cells];

for (i = 0; i < all_cells; i++)
field[i] = COVER_FOR_CELL;
These lines set up the mine field. Every cell is covered by default.
while (i < mines) {

position = (int) (all_cells * random.nextDouble());

if ((position < all_cells) &&
(field[position] != COVERED_MINE_CELL)) {

current_col = position % cols;
field[position] = COVERED_MINE_CELL;
...
In the while cycle we randomly position all mines in the field.
cell = position - cols;
if (cell >= 0)
if (field[cell] != COVERED_MINE_CELL)
field[cell] += 1;
Each of the cells can be surrounded up to 8 cells. (This does not apply to the border cells.) We raise the number for adjacent cells for each of the randomly placed mine. In our example, we add 1 to the top neighbor of the cell in question.
In the find_empty_cells() method, we find empty cells. If the player clicks on a mine cell, the game is over. If he clicks on a cell adjacent to a mine, he uncovers a number indicating how many mines the cell is adjacent to. Clicking on an empty cell leads to uncovering many other empty cells plus cells with a number that form a border around a space of empty borders. We use a recursive algorithm to find empty cells.
cell = j - 1;
if (cell >= 0)
if (field[cell] > MINE_CELL) {
field[cell] -= COVER_FOR_CELL;
if (field[cell] == EMPTY_CELL)
find_empty_cells(cell);
}
In this code, we check the cell that is left to an empty cell in question. If it is not empty, uncover it. If it is empty, repeat the whole process by recursively calling the find_empty_cells() method.
The paint() method turns numbers into images.
if (!inGame) {
if (cell == COVERED_MINE_CELL) {
cell = DRAW_MINE;
} else if (cell == MARKED_MINE_CELL) {
cell = DRAW_MARK;
} else if (cell > COVERED_MINE_CELL) {
cell = DRAW_WRONG_MARK;
} else if (cell > MINE_CELL) {
cell = DRAW_COVER;
}
}
If the game is over and we lost, we show all uncovered mines if any and show all wrongly marked cells if any.
g.drawImage(img[cell], (j * CELL_SIZE),
(i * CELL_SIZE), this);
This code line draws every cell.
In the mousePressed() method we react to mouse clicks. The Minesweeper game is controlled solely by mouse. We react to left and right mouse clicks.
field[(cRow * cols) + cCol] += MARK_FOR_CELL;
mines_left--;
If we right click on an unmarked cell, we add MARK_FOR_CELL to the number representing the cell. This leads to drawing a covered cell with a mark in the paint() method.
if (field[(cRow * cols) + cCol] > COVERED_MINE_CELL) {
return;
}
Nothing happens, if we click on the covered & marked cell. It must by first uncovered by another right click and only then it is possible to left click on it.
field[(cRow * cols) + cCol] -= COVER_FOR_CELL;
Left click removes a cover from the cell.
if (field[(cRow * cols) + cCol] == MINE_CELL)
inGame = false;
if (field[(cRow * cols) + cCol] == EMPTY_CELL)
find_empty_cells((cRow * cols) + cCol);
In case we left clicked on a mine, the game is over. If we left clicked on an empty cell, we call the find_empty_cells() method, which recursively finds all adjacent empty cells.
Mines.java
package mines;

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;


public class Mines extends JFrame {

private final int WIDTH = 250;
private final int HEIGHT = 290;

private JLabel statusbar;

public Mines() {

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(WIDTH, HEIGHT);
setLocationRelativeTo(null);
setTitle("Minesweeper");

statusbar = new JLabel("");
add(statusbar, BorderLayout.SOUTH);

add(new Board(statusbar));

setResizable(false);
setVisible(true);
}

public static void main(String[] args) {
new Mines();
}
}
This is the main class.
Minesweeper
Figure: Minesweeper
In this part of the Java 2D games tutorial, we created a Java clone of the Minesweeper game.
Continue Reading

Space Invaders With Java 2D games

Space Invaders

In this part of the Java 2D games tutorial we will create a simple Space Invaders game clone.
Space Invaders is an arcade video game designed by Tomohiro Nishikado. It was first released in 1978. The player controls a cannon. He is about to save the Earth from invasion of evil space invaders.

Development

In our java clone we have 24 invaders. These aliens heavily shell the ground. When the player shoots a missile, he can shoot another one only when it hits an alien or the top of the Board. The player shoots with the alt key. Aliens launch randomly their bombs. Each alien shoots a bomb only after the previous one hits the bottom.
SpaceInvaders.java
package spaceinvaders;

import javax.swing.JFrame;

public class SpaceInvaders extends JFrame implements Commons {

public SpaceInvaders()
{
add(new Board());
setTitle("Space Invaders");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(BOARD_WIDTH, BOARD_HEIGTH);
setLocationRelativeTo(null);
setVisible(true);
setResizable(false);
}

public static void main(String[] args) {
new SpaceInvaders();
}
}
This is the main class.
Commons.java
package spaceinvaders;

public interface Commons {

public static final int BOARD_WIDTH = 358;
public static final int BOARD_HEIGTH = 350;
public static final int GROUND = 290;
public static final int BOMB_HEIGHT = 5;
public static final int ALIEN_HEIGHT = 12;
public static final int ALIEN_WIDTH = 12;
public static final int BORDER_RIGHT = 30;
public static final int BORDER_LEFT = 5;
public static final int GO_DOWN = 15;
public static final int NUMBER_OF_ALIENS_TO_DESTROY = 24;
public static final int CHANCE = 5;
public static final int DELAY = 17;
public static final int PLAYER_WIDTH = 15;
public static final int PLAYER_HEIGHT = 10;
}
The Commons.java file has some common constants. They are self-explanatory.
Alien.java
package spaceinvaders;

import javax.swing.ImageIcon;


public class Alien extends Sprite {

private Bomb bomb;
private final String shot = "../spacepix/alien.png";

public Alien(int x, int y) {
this.x = x;
this.y = y;

bomb = new Bomb(x, y);
ImageIcon ii = new ImageIcon(this.getClass().getResource(shot));
setImage(ii.getImage());

}

public void act(int direction) {
this.x += direction;
}

public Bomb getBomb() {
return bomb;
}

public class Bomb extends Sprite {

private final String bomb = "../spacepix/bomb.png";
private boolean destroyed;

public Bomb(int x, int y) {
setDestroyed(true);
this.x = x;
this.y = y;
ImageIcon ii = new ImageIcon(this.getClass().getResource(bomb));
setImage(ii.getImage());
}

public void setDestroyed(boolean destroyed) {
this.destroyed = destroyed;
}

public boolean isDestroyed() {
return destroyed;
}
}
}
This is the Alien sprite. Each alien has an inner Bomb class.
public void act(int direction) {
this.x += direction;
}
The act() method is called from the Board class. It is used to position an alien in horizontal direction.
public Bomb getBomb() {
return bomb;
}
The getBomb() method is called, when the alien is about to drop a bomb.
Player.java
package spaceinvaders;

import java.awt.event.KeyEvent;

import javax.swing.ImageIcon;


public class Player extends Sprite implements Commons{

private final int START_Y = 280;
private final int START_X = 270;

private final String player = "../spacepix/player.png";
private int width;

public Player() {

ImageIcon ii = new ImageIcon(this.getClass().getResource(player));

width = ii.getImage().getWidth(null);

setImage(ii.getImage());
setX(START_X);
setY(START_Y);
}

public void act() {
x += dx;
if (x <= 2)
x = 2;
if (x >= BOARD_WIDTH - 2*width)
x = BOARD_WIDTH - 2*width;
}

public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();

if (key == KeyEvent.VK_LEFT)
{
dx = -2;
}

if (key == KeyEvent.VK_RIGHT)
{
dx = 2;
}

}

public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();

if (key == KeyEvent.VK_LEFT)
{
dx = 0;
}

if (key == KeyEvent.VK_RIGHT)
{
dx = 0;
}
}
}
This is the Player sprite. We control the cannon with the cursor keys.
private final int START_Y = 280; 
private final int START_X = 270;
These are the initial coordinates of the player sprite.
if (key == KeyEvent.VK_LEFT)
{
dx = -2;
}
If we press the left cursor key, the dx variable is set to -2. Next time the act() method is called, the player moves to the left.
if (key == KeyEvent.VK_LEFT)
{
dx = 0;
}

if (key == KeyEvent.VK_RIGHT)
{
dx = 0;
}
If we release the left or the right cursor, the dx variable is set to zero. The player sprite stops moving.
Shot.java
package spaceinvaders;

import javax.swing.ImageIcon;


public class Shot extends Sprite {

private String shot = "../spacepix/shot.png";
private final int H_SPACE = 6;
private final int V_SPACE = 1;

public Shot() {
}

public Shot(int x, int y) {

ImageIcon ii = new ImageIcon(this.getClass().getResource(shot));
setImage(ii.getImage());
setX(x + H_SPACE);
setY(y - V_SPACE);
}
}
This is the Shot sprite. The shot is triggered with the alt key. The H_SPACE and the V_SPACE constants are used to position the missile appropriately.
Sprite.java
package spaceinvaders;

import java.awt.Image;

public class Sprite {

private boolean visible;
private Image image;
protected int x;
protected int y;
protected boolean dying;
protected int dx;

public Sprite() {
visible = true;
}

public void die() {
visible = false;
}

public boolean isVisible() {
return visible;
}

protected void setVisible(boolean visible) {
this.visible = visible;
}

public void setImage(Image image) {
this.image = image;
}

public Image getImage() {
return image;
}

public void setX(int x) {
this.x = x;
}

public void setY(int y) {
this.y = y;
}
public int getY() {
return y;
}

public int getX() {
return x;
}

public void setDying(boolean dying) {
this.dying = dying;
}

public boolean isDying() {
return this.dying;
}
}
This is the basic Sprite class. Other sprites inherit from it. It has some common functionality.
Board.java
package spaceinvaders;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Toolkit;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;

import javax.swing.ImageIcon;
import javax.swing.JPanel;


public class Board extends JPanel implements Runnable, Commons {

private Dimension d;
private ArrayList aliens;
private Player player;
private Shot shot;

private int alienX = 150;
private int alienY = 5;
private int direction = -1;
private int deaths = 0;

private boolean ingame = true;
private final String expl = "../spacepix/explosion.png";
private final String alienpix = "../spacepix/alien.png";
private String message = "Game Over";

private Thread animator;

public Board()
{

addKeyListener(new TAdapter());
setFocusable(true);
d = new Dimension(BOARD_WIDTH, BOARD_HEIGTH);
setBackground(Color.black);

gameInit();
setDoubleBuffered(true);
}

public void addNotify() {
super.addNotify();
gameInit();
}

public void gameInit() {

aliens = new ArrayList();

ImageIcon ii = new ImageIcon(this.getClass().getResource(alienpix));

for (int i=0; i < 4; i++) {
for (int j=0; j < 6; j++) {
Alien alien = new Alien(alienX + 18*j, alienY + 18*i);
alien.setImage(ii.getImage());
aliens.add(alien);
}
}

player = new Player();
shot = new Shot();

if (animator == null || !ingame) {
animator = new Thread(this);
animator.start();
}
}

public void drawAliens(Graphics g)
{
Iterator it = aliens.iterator();

while (it.hasNext()) {
Alien alien = (Alien) it.next();

if (alien.isVisible()) {
g.drawImage(alien.getImage(), alien.getX(), alien.getY(), this);
}

if (alien.isDying()) {
alien.die();
}
}
}

public void drawPlayer(Graphics g) {

if (player.isVisible()) {
g.drawImage(player.getImage(), player.getX(), player.getY(), this);
}

if (player.isDying()) {
player.die();
ingame = false;
}
}

public void drawShot(Graphics g) {
if (shot.isVisible())
g.drawImage(shot.getImage(), shot.getX(), shot.getY(), this);
}

public void drawBombing(Graphics g) {

Iterator i3 = aliens.iterator();

while (i3.hasNext()) {
Alien a = (Alien) i3.next();

Alien.Bomb b = a.getBomb();

if (!b.isDestroyed()) {
g.drawImage(b.getImage(), b.getX(), b.getY(), this);
}
}
}

public void paint(Graphics g)
{
super.paint(g);

g.setColor(Color.black);
g.fillRect(0, 0, d.width, d.height);
g.setColor(Color.green);

if (ingame) {

g.drawLine(0, GROUND, BOARD_WIDTH, GROUND);
drawAliens(g);
drawPlayer(g);
drawShot(g);
drawBombing(g);
}

Toolkit.getDefaultToolkit().sync();
g.dispose();
}

public void gameOver()
{

Graphics g = this.getGraphics();

g.setColor(Color.black);
g.fillRect(0, 0, BOARD_WIDTH, BOARD_HEIGTH);

g.setColor(new Color(0, 32, 48));
g.fillRect(50, BOARD_WIDTH/2 - 30, BOARD_WIDTH-100, 50);
g.setColor(Color.white);
g.drawRect(50, BOARD_WIDTH/2 - 30, BOARD_WIDTH-100, 50);

Font small = new Font("Helvetica", Font.BOLD, 14);
FontMetrics metr = this.getFontMetrics(small);

g.setColor(Color.white);
g.setFont(small);
g.drawString(message, (BOARD_WIDTH - metr.stringWidth(message))/2,
BOARD_WIDTH/2);
}

public void animationCycle() {

if (deaths == NUMBER_OF_ALIENS_TO_DESTROY) {
ingame = false;
message = "Game won!";
}

// player

player.act();

// shot
if (shot.isVisible()) {
Iterator it = aliens.iterator();
int shotX = shot.getX();
int shotY = shot.getY();

while (it.hasNext()) {
Alien alien = (Alien) it.next();
int alienX = alien.getX();
int alienY = alien.getY();

if (alien.isVisible() && shot.isVisible()) {
if (shotX >= (alienX) &&
shotX <= (alienX + ALIEN_WIDTH) &&
shotY >= (alienY) &&
shotY <= (alienY+ALIEN_HEIGHT) ) {
ImageIcon ii =
new ImageIcon(getClass().getResource(expl));
alien.setImage(ii.getImage());
alien.setDying(true);
deaths++;
shot.die();
}
}
}

int y = shot.getY();
y -= 4;
if (y < 0)
shot.die();
else shot.setY(y);
}

// aliens

Iterator it1 = aliens.iterator();

while (it1.hasNext()) {
Alien a1 = (Alien) it1.next();
int x = a1.getX();

if (x >= BOARD_WIDTH - BORDER_RIGHT && direction != -1) {
direction = -1;
Iterator i1 = aliens.iterator();
while (i1.hasNext()) {
Alien a2 = (Alien) i1.next();
a2.setY(a2.getY() + GO_DOWN);
}
}

if (x <= BORDER_LEFT && direction != 1) {
direction = 1;

Iterator i2 = aliens.iterator();
while (i2.hasNext()) {
Alien a = (Alien)i2.next();
a.setY(a.getY() + GO_DOWN);
}
}
}


Iterator it = aliens.iterator();

while (it.hasNext()) {
Alien alien = (Alien) it.next();
if (alien.isVisible()) {

int y = alien.getY();

if (y > GROUND - ALIEN_HEIGHT) {
ingame = false;
message = "Invasion!";
}

alien.act(direction);
}
}

// bombs

Iterator i3 = aliens.iterator();
Random generator = new Random();

while (i3.hasNext()) {
int shot = generator.nextInt(15);
Alien a = (Alien) i3.next();
Alien.Bomb b = a.getBomb();
if (shot == CHANCE && a.isVisible() && b.isDestroyed()) {

b.setDestroyed(false);
b.setX(a.getX());
b.setY(a.getY());
}

int bombX = b.getX();
int bombY = b.getY();
int playerX = player.getX();
int playerY = player.getY();

if (player.isVisible() && !b.isDestroyed()) {
if ( bombX >= (playerX) &&
bombX <= (playerX+PLAYER_WIDTH) &&
bombY >= (playerY) &&
bombY <= (playerY+PLAYER_HEIGHT) ) {
ImageIcon ii =
new ImageIcon(this.getClass().getResource(expl));
player.setImage(ii.getImage());
player.setDying(true);
b.setDestroyed(true);;
}
}

if (!b.isDestroyed()) {
b.setY(b.getY() + 1);
if (b.getY() >= GROUND - BOMB_HEIGHT) {
b.setDestroyed(true);
}
}
}
}

public void run() {

long beforeTime, timeDiff, sleep;

beforeTime = System.currentTimeMillis();

while (ingame) {
repaint();
animationCycle();

timeDiff = System.currentTimeMillis() - beforeTime;
sleep = DELAY - timeDiff;

if (sleep < 0)
sleep = 2;
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
System.out.println("interrupted");
}
beforeTime = System.currentTimeMillis();
}
gameOver();
}

private class TAdapter extends KeyAdapter {

public void keyReleased(KeyEvent e) {
player.keyReleased(e);
}

public void keyPressed(KeyEvent e) {

player.keyPressed(e);

int x = player.getX();
int y = player.getY();

if (ingame)
{
if (e.isAltDown()) {
if (!shot.isVisible())
shot = new Shot(x, y);
}
}
}
}
}
The main logic of the game is located in the Board class.
for (int i=0; i < 4; i++) {
for (int j=0; j < 6; j++) {
Alien alien = new Alien(alienX + 18*j, alienY + 18*i);
alien.setImage(ii.getImage());
aliens.add(alien);
}
}

player = new Player();
shot = new Shot();
In the gameInit() method we set up 24 aliens. The alien image size is 12x12px. We put 6px space among the aliens. We also create the player and the shot objects.
public void drawBombing(Graphics g) {

Iterator i3 = aliens.iterator();
while (i3.hasNext()) {
Alien a = (Alien) i3.next();

Alien.Bomb b = a.getBomb();
if (!b.isDestroyed()) {
g.drawImage(b.getImage(), b.getX(), b.getY(), this);
}
}
}
The drawBombing() method draws bombs launched by the aliens.
if (ingame) {

g.drawLine(0, GROUND, BOARD_WIDTH, GROUND);
drawAliens(g);
drawPlayer(g);
drawShot(g);
drawBombing(g);
}
Inside the paint() method, we draw the ground, the aliens, the player, the shot and the bombs.
Next we will examine the animationCycle() method.
if (deaths == NUMBER_OF_ALIENS_TO_DESTROY) {
ingame = false;
message = "Game won!";
}
If we destroy all aliens, we win the game. (24 in this game)
if (alien.isVisible() && shot.isVisible()) {
if (shotX >= (alienX) &&
shotX <= (alienX + ALIEN_WIDTH) &&
shotY >= (alienY) &&
shotY <= (alienY+ALIEN_HEIGHT) ) {
ImageIcon ii =
new ImageIcon(getClass().getResource(expl));
alien.setImage(ii.getImage());
alien.setDying(true);
deaths++;
shot.die();
}
}
If the shot triggered by the player collides with an alien, the alien ship is destroyed. More precisely, the dying flag is set. We use it to display an explosion. The deaths variable increases and the shot sprite is destroyed.
if (x  >= BOARD_WIDTH - BORDER_RIGHT && direction != -1) {
direction = -1;
Iterator i1 = aliens.iterator();
while (i1.hasNext()) {
Alien a2 = (Alien) i1.next();
a2.setY(a2.getY() + GO_DOWN);
}
}
If the aliens reach the right end of the Board, they move down and change their direction to the left.
Iterator it = aliens.iterator();

while (it.hasNext()) {
Alien alien = (Alien) it.next();
if (alien.isVisible()) {

int y = alien.getY();
if (y > GROUND - ALIEN_HEIGHT) {
ingame = false;
message = "Invasion!";
}
alien.act(direction);
}
}
Aliens move. If they reach the bottom, the invasion begins.
int shot = generator.nextInt(15);
Alien a = (Alien) i3.next();
Alien.Bomb b = a.getBomb();
if (shot == CHANCE && a.isVisible() && b.isDestroyed()) {
b.setDestroyed(false);
b.setX(a.getX());
b.setY(a.getY());
}
This is the code that determines whether the alien will drop a bomb. The alien must not be destroyed. Eg. it must be visible. The bomb's destroyed flag must be set. In other words, it is alien's first bomb dropping or previous dropped bomb already hit the ground. If these two conditions are fulfilled, the bombing is left to the chance.
if (!b.isDestroyed()) {
b.setY(b.getY() + 1);
if (b.getY() >= GROUND - BOMB_HEIGHT) {
b.setDestroyed(true);
}
}
If the bomb is not destroyed, it goes one px to the ground. If it hits the bottom, the destroyed flag is set. The alien is now ready to drop another bomb.
public void keyReleased(KeyEvent e) {
player.keyReleased(e);
}
The actual processing of this particular KeyEvent is delegated to the player sprite.
Space Invaders
Figure: Space Invaders
This was the Space Invaders game.
Continue Reading

Pacman With Java 2D games

Pacman

In this part of the Java 2D games tutorial we will create a simple Pacman game clone.
Pacman is an arcade game originally developed by a Japanese company Namco in 1980. Pacman became one of the most popular arcade games ever.

Development

The following code example is a remake of a Pacman game by Brian Postma available at javaboutique. I have modified and simplified the code. So that it is easier to understand.
The goal of the game is to collect all the points in the maze and avoid the ghosts. The pacman is animated in two ways. His position in the maze and his body. We animate his body with four images, depending on the direction. The animation is used to create the illusion of pacman opening and closing his mouth. The maze consists of 15 x 15 squares. The structure of the maze is based on a simple array of integers. Pacman has three lives. We also count the score.
The game consists of two files.
Board.java
package pacman;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.Timer;


public class Board extends JPanel implements ActionListener {

Dimension d;
Font smallfont = new Font("Helvetica", Font.BOLD, 14);

FontMetrics fmsmall, fmlarge;
Image ii;
Color dotcolor = new Color(192, 192, 0);
Color mazecolor;

boolean ingame = false;
boolean dying = false;

final int blocksize = 24;
final int nrofblocks = 15;
final int scrsize = nrofblocks * blocksize;
final int pacanimdelay = 2;
final int pacmananimcount = 4;
final int maxghosts = 12;
final int pacmanspeed = 6;

int pacanimcount = pacanimdelay;
int pacanimdir = 1;
int pacmananimpos = 0;
int nrofghosts = 6;
int pacsleft, score;
int deathcounter;
int[] dx, dy;
int[] ghostx, ghosty, ghostdx, ghostdy, ghostspeed;

Image ghost;
Image pacman1, pacman2up, pacman2left, pacman2right, pacman2down;
Image pacman3up, pacman3down, pacman3left, pacman3right;
Image pacman4up, pacman4down, pacman4left, pacman4right;

int pacmanx, pacmany, pacmandx, pacmandy;
int reqdx, reqdy, viewdx, viewdy;

final short leveldata[] =
{ 19, 26, 26, 26, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 22,
21, 0, 0, 0, 17, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20,
21, 0, 0, 0, 17, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20,
21, 0, 0, 0, 17, 16, 16, 24, 16, 16, 16, 16, 16, 16, 20,
17, 18, 18, 18, 16, 16, 20, 0, 17, 16, 16, 16, 16, 16, 20,
17, 16, 16, 16, 16, 16, 20, 0, 17, 16, 16, 16, 16, 24, 20,
25, 16, 16, 16, 24, 24, 28, 0, 25, 24, 24, 16, 20, 0, 21,
1, 17, 16, 20, 0, 0, 0, 0, 0, 0, 0, 17, 20, 0, 21,
1, 17, 16, 16, 18, 18, 22, 0, 19, 18, 18, 16, 20, 0, 21,
1, 17, 16, 16, 16, 16, 20, 0, 17, 16, 16, 16, 20, 0, 21,
1, 17, 16, 16, 16, 16, 20, 0, 17, 16, 16, 16, 20, 0, 21,
1, 17, 16, 16, 16, 16, 16, 18, 16, 16, 16, 16, 20, 0, 21,
1, 17, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 20, 0, 21,
1, 25, 24, 24, 24, 24, 24, 24, 24, 24, 16, 16, 16, 18, 20,
9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 25, 24, 24, 24, 28 };

final int validspeeds[] = { 1, 2, 3, 4, 6, 8 };
final int maxspeed = 6;

int currentspeed = 3;
short[] screendata;
Timer timer;


public Board() {

GetImages();

addKeyListener(new TAdapter());

screendata = new short[nrofblocks * nrofblocks];
mazecolor = new Color(5, 100, 5);
setFocusable(true);

d = new Dimension(400, 400);

setBackground(Color.black);
setDoubleBuffered(true);

ghostx = new int[maxghosts];
ghostdx = new int[maxghosts];
ghosty = new int[maxghosts];
ghostdy = new int[maxghosts];
ghostspeed = new int[maxghosts];
dx = new int[4];
dy = new int[4];
timer = new Timer(40, this);
timer.start();
}

public void addNotify() {
super.addNotify();
GameInit();
}


public void DoAnim() {
pacanimcount--;
if (pacanimcount <= 0) {
pacanimcount = pacanimdelay;
pacmananimpos = pacmananimpos + pacanimdir;
if (pacmananimpos == (pacmananimcount - 1) || pacmananimpos == 0)
pacanimdir = -pacanimdir;
}
}


public void PlayGame(Graphics2D g2d) {
if (dying) {
Death();
} else {
MovePacMan();
DrawPacMan(g2d);
moveGhosts(g2d);
CheckMaze();
}
}


public void ShowIntroScreen(Graphics2D g2d) {

g2d.setColor(new Color(0, 32, 48));
g2d.fillRect(50, scrsize / 2 - 30, scrsize - 100, 50);
g2d.setColor(Color.white);
g2d.drawRect(50, scrsize / 2 - 30, scrsize - 100, 50);

String s = "Press s to start.";
Font small = new Font("Helvetica", Font.BOLD, 14);
FontMetrics metr = this.getFontMetrics(small);

g2d.setColor(Color.white);
g2d.setFont(small);
g2d.drawString(s, (scrsize - metr.stringWidth(s)) / 2, scrsize / 2);
}


public void DrawScore(Graphics2D g) {
int i;
String s;

g.setFont(smallfont);
g.setColor(new Color(96, 128, 255));
s = "Score: " + score;
g.drawString(s, scrsize / 2 + 96, scrsize + 16);
for (i = 0; i < pacsleft; i++) {
g.drawImage(pacman3left, i * 28 + 8, scrsize + 1, this);
}
}


public void CheckMaze() {
short i = 0;
boolean finished = true;

while (i < nrofblocks * nrofblocks && finished) {
if ((screendata[i] & 48) != 0)
finished = false;
i++;
}

if (finished) {
score += 50;

if (nrofghosts < maxghosts)
nrofghosts++;
if (currentspeed < maxspeed)
currentspeed++;
LevelInit();
}
}

public void Death() {

pacsleft--;
if (pacsleft == 0)
ingame = false;
LevelContinue();
}


public void moveGhosts(Graphics2D g2d) {
short i;
int pos;
int count;

for (i = 0; i < nrofghosts; i++) {
if (ghostx[i] % blocksize == 0 && ghosty[i] % blocksize == 0) {
pos =
ghostx[i] / blocksize + nrofblocks * (int)(ghosty[i] / blocksize);

count = 0;
if ((screendata[pos] & 1) == 0 && ghostdx[i] != 1) {
dx[count] = -1;
dy[count] = 0;
count++;
}
if ((screendata[pos] & 2) == 0 && ghostdy[i] != 1) {
dx[count] = 0;
dy[count] = -1;
count++;
}
if ((screendata[pos] & 4) == 0 && ghostdx[i] != -1) {
dx[count] = 1;
dy[count] = 0;
count++;
}
if ((screendata[pos] & 8) == 0 && ghostdy[i] != -1) {
dx[count] = 0;
dy[count] = 1;
count++;
}

if (count == 0) {
if ((screendata[pos] & 15) == 15) {
ghostdx[i] = 0;
ghostdy[i] = 0;
} else {
ghostdx[i] = -ghostdx[i];
ghostdy[i] = -ghostdy[i];
}
} else {
count = (int)(Math.random() * count);
if (count > 3)
count = 3;
ghostdx[i] = dx[count];
ghostdy[i] = dy[count];
}

}
ghostx[i] = ghostx[i] + (ghostdx[i] * ghostspeed[i]);
ghosty[i] = ghosty[i] + (ghostdy[i] * ghostspeed[i]);
DrawGhost(g2d, ghostx[i] + 1, ghosty[i] + 1);

if (pacmanx > (ghostx[i] - 12) && pacmanx < (ghostx[i] + 12) &&
pacmany > (ghosty[i] - 12) && pacmany < (ghosty[i] + 12) &&
ingame) {

dying = true;
deathcounter = 64;

}
}
}


public void DrawGhost(Graphics2D g2d, int x, int y) {
g2d.drawImage(ghost, x, y, this);
}


public void MovePacMan() {
int pos;
short ch;

if (reqdx == -pacmandx && reqdy == -pacmandy) {
pacmandx = reqdx;
pacmandy = reqdy;
viewdx = pacmandx;
viewdy = pacmandy;
}
if (pacmanx % blocksize == 0 && pacmany % blocksize == 0) {
pos =
pacmanx / blocksize + nrofblocks * (int)(pacmany / blocksize);
ch = screendata[pos];

if ((ch & 16) != 0) {
screendata[pos] = (short)(ch & 15);
score++;
}

if (reqdx != 0 || reqdy != 0) {
if (!((reqdx == -1 && reqdy == 0 && (ch & 1) != 0) ||
(reqdx == 1 && reqdy == 0 && (ch & 4) != 0) ||
(reqdx == 0 && reqdy == -1 && (ch & 2) != 0) ||
(reqdx == 0 && reqdy == 1 && (ch & 8) != 0))) {
pacmandx = reqdx;
pacmandy = reqdy;
viewdx = pacmandx;
viewdy = pacmandy;
}
}

// Check for standstill
if ((pacmandx == -1 && pacmandy == 0 && (ch & 1) != 0) ||
(pacmandx == 1 && pacmandy == 0 && (ch & 4) != 0) ||
(pacmandx == 0 && pacmandy == -1 && (ch & 2) != 0) ||
(pacmandx == 0 && pacmandy == 1 && (ch & 8) != 0)) {
pacmandx = 0;
pacmandy = 0;
}
}
pacmanx = pacmanx + pacmanspeed * pacmandx;
pacmany = pacmany + pacmanspeed * pacmandy;
}


public void DrawPacMan(Graphics2D g2d) {
if (viewdx == -1)
DrawPacManLeft(g2d);
else if (viewdx == 1)
DrawPacManRight(g2d);
else if (viewdy == -1)
DrawPacManUp(g2d);
else
DrawPacManDown(g2d);
}

public void DrawPacManUp(Graphics2D g2d) {
switch (pacmananimpos) {
case 1:
g2d.drawImage(pacman2up, pacmanx + 1, pacmany + 1, this);
break;
case 2:
g2d.drawImage(pacman3up, pacmanx + 1, pacmany + 1, this);
break;
case 3:
g2d.drawImage(pacman4up, pacmanx + 1, pacmany + 1, this);
break;
default:
g2d.drawImage(pacman1, pacmanx + 1, pacmany + 1, this);
break;
}
}


public void DrawPacManDown(Graphics2D g2d) {
switch (pacmananimpos) {
case 1:
g2d.drawImage(pacman2down, pacmanx + 1, pacmany + 1, this);
break;
case 2:
g2d.drawImage(pacman3down, pacmanx + 1, pacmany + 1, this);
break;
case 3:
g2d.drawImage(pacman4down, pacmanx + 1, pacmany + 1, this);
break;
default:
g2d.drawImage(pacman1, pacmanx + 1, pacmany + 1, this);
break;
}
}


public void DrawPacManLeft(Graphics2D g2d) {
switch (pacmananimpos) {
case 1:
g2d.drawImage(pacman2left, pacmanx + 1, pacmany + 1, this);
break;
case 2:
g2d.drawImage(pacman3left, pacmanx + 1, pacmany + 1, this);
break;
case 3:
g2d.drawImage(pacman4left, pacmanx + 1, pacmany + 1, this);
break;
default:
g2d.drawImage(pacman1, pacmanx + 1, pacmany + 1, this);
break;
}
}


public void DrawPacManRight(Graphics2D g2d) {
switch (pacmananimpos) {
case 1:
g2d.drawImage(pacman2right, pacmanx + 1, pacmany + 1, this);
break;
case 2:
g2d.drawImage(pacman3right, pacmanx + 1, pacmany + 1, this);
break;
case 3:
g2d.drawImage(pacman4right, pacmanx + 1, pacmany + 1, this);
break;
default:
g2d.drawImage(pacman1, pacmanx + 1, pacmany + 1, this);
break;
}
}


public void DrawMaze(Graphics2D g2d) {
short i = 0;
int x, y;

for (y = 0; y < scrsize; y += blocksize) {
for (x = 0; x < scrsize; x += blocksize) {
g2d.setColor(mazecolor);
g2d.setStroke(new BasicStroke(2));

if ((screendata[i] & 1) != 0) // draws left
{
g2d.drawLine(x, y, x, y + blocksize - 1);
}
if ((screendata[i] & 2) != 0) // draws top
{
g2d.drawLine(x, y, x + blocksize - 1, y);
}
if ((screendata[i] & 4) != 0) // draws right
{
g2d.drawLine(x + blocksize - 1, y, x + blocksize - 1,
y + blocksize - 1);
}
if ((screendata[i] & 8) != 0) // draws bottom
{
g2d.drawLine(x, y + blocksize - 1, x + blocksize - 1,
y + blocksize - 1);
}
if ((screendata[i] & 16) != 0) // draws point
{
g2d.setColor(dotcolor);
g2d.fillRect(x + 11, y + 11, 2, 2);
}
i++;
}
}
}

public void GameInit() {
pacsleft = 3;
score = 0;
LevelInit();
nrofghosts = 6;
currentspeed = 3;
}


public void LevelInit() {
int i;
for (i = 0; i < nrofblocks * nrofblocks; i++)
screendata[i] = leveldata[i];

LevelContinue();
}


public void LevelContinue() {
short i;
int dx = 1;
int random;

for (i = 0; i < nrofghosts; i++) {
ghosty[i] = 4 * blocksize;
ghostx[i] = 4 * blocksize;
ghostdy[i] = 0;
ghostdx[i] = dx;
dx = -dx;
random = (int)(Math.random() * (currentspeed + 1));
if (random > currentspeed)
random = currentspeed;
ghostspeed[i] = validspeeds[random];
}

pacmanx = 7 * blocksize;
pacmany = 11 * blocksize;
pacmandx = 0;
pacmandy = 0;
reqdx = 0;
reqdy = 0;
viewdx = -1;
viewdy = 0;
dying = false;
}

public void GetImages()
{

ghost = new ImageIcon(Board.class.getResource("../pacpix/ghost.png")).getImage();
pacman1 = new ImageIcon(Board.class.getResource("../pacpix/pacman.png")).getImage();
pacman2up = new ImageIcon(Board.class.getResource("../pacpix/up1.png")).getImage();
pacman3up = new ImageIcon(Board.class.getResource("../pacpix/up2.png")).getImage();
pacman4up = new ImageIcon(Board.class.getResource("../pacpix/up3.png")).getImage();
pacman2down = new ImageIcon(Board.class.getResource("../pacpix/down1.png")).getImage();
pacman3down = new ImageIcon(Board.class.getResource("../pacpix/down2.png")).getImage();
pacman4down = new ImageIcon(Board.class.getResource("../pacpix/down3.png")).getImage();
pacman2left = new ImageIcon(Board.class.getResource("../pacpix/left1.png")).getImage();
pacman3left = new ImageIcon(Board.class.getResource("../pacpix/left2.png")).getImage();
pacman4left = new ImageIcon(Board.class.getResource("../pacpix/left3.png")).getImage();
pacman2right = new ImageIcon(Board.class.getResource("../pacpix/right1.png")).getImage();
pacman3right = new ImageIcon(Board.class.getResource("../pacpix/right2.png")).getImage();
pacman4right = new ImageIcon(Board.class.getResource("../pacpix/right3.png")).getImage();

}

public void paint(Graphics g)
{
super.paint(g);

Graphics2D g2d = (Graphics2D) g;

g2d.setColor(Color.black);
g2d.fillRect(0, 0, d.width, d.height);

DrawMaze(g2d);
DrawScore(g2d);
DoAnim();
if (ingame)
PlayGame(g2d);
else
ShowIntroScreen(g2d);

g.drawImage(ii, 5, 5, this);
Toolkit.getDefaultToolkit().sync();
g.dispose();
}

class TAdapter extends KeyAdapter {
public void keyPressed(KeyEvent e) {

int key = e.getKeyCode();

if (ingame)
{
if (key == KeyEvent.VK_LEFT)
{
reqdx=-1;
reqdy=0;
}
else if (key == KeyEvent.VK_RIGHT)
{
reqdx=1;
reqdy=0;
}
else if (key == KeyEvent.VK_UP)
{
reqdx=0;
reqdy=-1;
}
else if (key == KeyEvent.VK_DOWN)
{
reqdx=0;
reqdy=1;
}
else if (key == KeyEvent.VK_ESCAPE && timer.isRunning())
{
ingame=false;
}
else if (key == KeyEvent.VK_PAUSE) {
if (timer.isRunning())
timer.stop();
else timer.start();
}
}
else
{
if (key == 's' || key == 'S')
{
ingame=true;
GameInit();
}
}
}

public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();

if (key == Event.LEFT || key == Event.RIGHT ||
key == Event.UP || key == Event.DOWN)
{
reqdx=0;
reqdy=0;
}
}
}

public void actionPerformed(ActionEvent e) {
repaint();
}
}
The Commons.java file has some common constants.
final short leveldata[] =
{ 19, 26, 26, 26, 18, ... };
These numbers make up the maze. They provide information out of which we create the corners and the points. For example number 19 in the upper left corner means, that the square will have top and left borders and a point. (16 + 2 + 1)
 public void doAnim() {
pacanimcount--;
if (pacanimcount <= 0) {
pacanimcount = pacanimdelay;
pacmananimpos = pacmananimpos + pacanimdir;
if (pacmananimpos == (pacmananimcount - 1) || pacmananimpos == 0)
pacanimdir = -pacanimdir;
}
}
The doAnim() counts the pacmananimpos variable, which determines what pacman image is drawn. There are four pacman images. There is also a pacanimdelay variable, which makes the animation a bit slower. Otherwise the pacman would open his mouth too fast.
boolean finished = true;

while (i < nrofblocks * nrofblocks && finished) {
if ((screendata[i] & 16) != 0)
finished = false;
i++;
}
This code is part of the checkMaze() method. It checks, if there are any points left for the Pacman to eat. Number 16 stands for a point. If all points are consumed, we move to the next level.
Next we will examine the moveGhosts() method. The ghosts move one square and then decide, if they change the direction.
if (ghostx[i] % blocksize == 0 && ghosty[i] % blocksize == 0) {
Continue only if you have finished moving one square.
pos = ghostx[i] / blocksize + nrofblocks * (int)(ghosty[i] / blocksize);
This line determines, where the ghost is situated. In which position/square. There are 225 theoretical positions. (A ghost cannot move over walls. )
if ((screendata[pos] & 1) == 0 && ghostdx[i] != 1) {
dx[count] = -1;
dy[count] = 0;
count++;
}
If there is no obstacle on the left and the ghost is not already moving to the right, the ghost will move to the left. What does this code really mean? If the ghost enters a tunnel, he will continue in the same direction until he is out of the tunnel. Moving of ghosts is partly random. We do not apply this randomness inside long tunnels. The ghost might get stuck there.
if (pacmanx > (ghostx[i] - 12) && pacmanx < (ghostx[i] + 12) &&
pacmany > (ghosty[i] - 12) && pacmany < (ghosty[i] + 12) &&
ingame) {

dying = true;
deathcounter = 64;
}
If there is a collision between ghosts and a pacman, the pacman dies.
Next we are going to examine the movePacman() method. The reqdx and reqdy variables are determined in the TAdapter inner class. These variables are controlled with cursor keys.
if ((ch & 16) != 0) {
screendata[pos] = (short)(ch & 15);
score++;
}
If the pacman moves to a position, where there is a point, we remove it from the maze and increase the score value.
if ((pacmandx == -1 && pacmandy == 0 && (ch & 1) != 0) ||
(pacmandx == 1 && pacmandy == 0 && (ch & 4) != 0) ||
(pacmandx == 0 && pacmandy == -1 && (ch & 2) != 0) ||
(pacmandx == 0 && pacmandy == 1 && (ch & 8) != 0)) {
pacmandx = 0;
pacmandy = 0;
}
If the pacman cannot move further it his current direction, there is a standstill.
public void drawPacMan(Graphics2D g2d) {
if (viewdx == -1)
drawPacManLeft(g2d);
else if (viewdx == 1)
drawPacManRight(g2d);
else if (viewdy == -1)
drawPacManUp(g2d);
else
drawPacManDown(g2d);
}
There are four possible directions for a pacman. There are four images for all directions. The images are used to animate pacman opening a closing his mouth.
The drawMaze() method draws the maze out of the numbers in the screendata array. Number 1 is a left border, 2 is a top border, 4 is a right border, 8 is a bottom border and 16 is a point. We simply go through all 225 squares int the maze. For example we have 9 in the screendata array. We have the first bit (1) and the fourth bit (8) set. So we draw a bottom and a left border on this particular square.
if ((screendata[i] & 1) != 0) // draws left
{
g2d.drawLine(x, y, x, y + blocksize - 1);
}
Draw a left border if the first bit of a number is set.
PacMan.java
package packman;

import javax.swing.JFrame;

import pacman.Board;


public class PacMan extends JFrame
{

public PacMan()
{
add(new Board());
setTitle("Pacman");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(380, 420);
setLocationRelativeTo(null);
setVisible(true);
}

public static void main(String[] args) {
new PacMan();
}
}
This is a PacMan file with a main method.
Pacman
Figure: Pacman
This was the Pacman game.
Continue Reading

Tetris With Java 2D games

Tetris

In this chapter, we will create a Tetris game clone in Java Swing. It is modified and simplified.

Tetris

The Tetris game is one of the most popular computer games ever created. The original game was designed and programmed by a Russian programmer Alexey Pajitnov in 1985. Since then, Tetris is available on almost every computer platform in lots of variations. Even my mobile phone has a modified version of the Tetris game.
Tetris is called a falling block puzzle game. In this game, we have seven different shapes called tetrominoes. S-shape, Z-shape, T-shape, L-shape, Line-shape, MirroredL-shape and a Square-shape. Each of these shapes is formed with four squares. The shapes are falling down the board. The object of the Tetris game is to move and rotate the shapes, so that they fit as much as possible. If we manage to form a row, the row is destroyed and we score. We play the tetris game until we top out.

Tetrominoes
Figure: Tetrominoes

The development

We do not have images for our tetris game, we draw the tetrominoes using Swing drawing API. Behind every computer game, there is a mathematical model. So it is in Tetris.
Some ideas behind the game.
  • We use a Timer class to create a game cycle
  • The tetrominoes are drawn
  • The shapes move on a square by square basis (not pixel by pixel)
  • Mathematically a board is a simple list of numbers
I have simplified the game a bit, so that it is easier to understand. The game starts immediately, after it is launched. We can pause the game by pressing the p key. The space key will drop the tetris piece immediately to the bottom. The d key will drop the piece one line down. (It can be used to speed up the falling a bit.) The game goes at constant speed, no acceleration is implemented. The score is the number of lines, that we have removed.
Tetris.java
package tetris;

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;


public class Tetris extends JFrame {

JLabel statusbar;


public Tetris() {

statusbar = new JLabel(" 0");
add(statusbar, BorderLayout.SOUTH);
Board board = new Board(this);
add(board);
board.start();

setSize(200, 400);
setTitle("Tetris");
setDefaultCloseOperation(EXIT_ON_CLOSE);
}

public JLabel getStatusBar() {
return statusbar;
}

public static void main(String[] args) {

Tetris game = new Tetris();
game.setLocationRelativeTo(null);
game.setVisible(true);

}
}
In the Tetris.java file, we set up the game. We create a board on which we play the game. We create a statusbar.
board.start();
The start() method starts the Tetris game. Immediately, after the window appears on the screen.
Shape.java
package tetris;

import java.util.Random;
import java.lang.Math;


public class Shape {

enum Tetrominoes { NoShape, ZShape, SShape, LineShape,
TShape, SquareShape, LShape, MirroredLShape };

private Tetrominoes pieceShape;
private int coords[][];
private int[][][] coordsTable;


public Shape() {

coords = new int[4][2];
setShape(Tetrominoes.NoShape);

}

public void setShape(Tetrominoes shape) {

coordsTable = new int[][][] {
{ { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
{ { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } },
{ { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } },
{ { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } },
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } },
{ { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } },
{ { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } },
{ { 1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }
};

for (int i = 0; i < 4 ; i++) {
for (int j = 0; j < 2; ++j) {
coords[i][j] = coordsTable[shape.ordinal()][i][j];
}
}
pieceShape = shape;

}

private void setX(int index, int x) { coords[index][0] = x; }
private void setY(int index, int y) { coords[index][1] = y; }
public int x(int index) { return coords[index][0]; }
public int y(int index) { return coords[index][1]; }
public Tetrominoes getShape() { return pieceShape; }

public void setRandomShape()
{
Random r = new Random();
int x = Math.abs(r.nextInt()) % 7 + 1;
Tetrominoes[] values = Tetrominoes.values();
setShape(values[x]);
}

public int minX()
{
int m = coords[0][0];
for (int i=0; i < 4; i++) {
m = Math.min(m, coords[i][0]);
}
return m;
}


public int minY()
{
int m = coords[0][1];
for (int i=0; i < 4; i++) {
m = Math.min(m, coords[i][1]);
}
return m;
}

public Shape rotateLeft()
{
if (pieceShape == Tetrominoes.SquareShape)
return this;

Shape result = new Shape();
result.pieceShape = pieceShape;

for (int i = 0; i < 4; ++i) {
result.setX(i, y(i));
result.setY(i, -x(i));
}
return result;
}

public Shape rotateRight()
{
if (pieceShape == Tetrominoes.SquareShape)
return this;

Shape result = new Shape();
result.pieceShape = pieceShape;

for (int i = 0; i < 4; ++i) {
result.setX(i, -y(i));
result.setY(i, x(i));
}
return result;
}
}
The Shape class provides information about a tetris piece.
enum Tetrominoes { NoShape, ZShape, SShape, LineShape, 
TShape, SquareShape, LShape, MirroredLShape };
The Tetrominoes enum holds all seven tetris shapes. Plus the empty shape called here NoShape.
public Shape() {

coords = new int[4][2];
setShape(Tetrominoes.NoShape);

}
This is the constructor of the Shape class. The coords array holds the actual coordinates of a Tetris piece.
coordsTable = new int[][][] {
{ { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } },
{ { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } },
{ { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } },
{ { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } },
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } },
{ { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } },
{ { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } },
{ { 1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }
};
The coordsTable array holds all possible coordinate values of our tetris pieces. This is a template from which all pieces take their coordiate values.
for (int i = 0; i < 4 ; i++) {
for (int j = 0; j < 2; ++j) {
coords[i][j] = coordsTable[shape.ordinal()][i][j];
}
}
Here we put one row of the coordiate values from the coordsTable to a coords array of a tetris piece. Note the use of the ordinal() method. In C++, an enum type is esencially an integer. Unlike in C++, Java enums are full classes. And the ordinal() method returns the current position of the enum type in the enum object.
The following image will help understand the coordinate values a bit more. The coords array saves the coordinates of the tetris piece. For example, numbers { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } , represent a rotated S-shape. The following diagram illustrates the shape.
Coordinates
Figure: Coordinates
 public Shape rotateLeft() 
{
if (pieceShape == Tetrominoes.SquareShape)
return this;

Shape result = new Shape();
result.pieceShape = pieceShape;

for (int i = 0; i < 4; ++i) {
result.setX(i, y(i));
result.setY(i, -x(i));
}
return result;
}
This code rotates the piece to the left. The square does not have to be rotated. That's why we simply return the reference to the current object. Looking at the previous image will help to understand the rotation.
Board.java
package tetris;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;

import tetris.Shape.Tetrominoes;


public class Board extends JPanel implements ActionListener {


final int BoardWidth = 10;
final int BoardHeight = 22;

Timer timer;
boolean isFallingFinished = false;
boolean isStarted = false;
boolean isPaused = false;
int numLinesRemoved = 0;
int curX = 0;
int curY = 0;
JLabel statusbar;
Shape curPiece;
Tetrominoes[] board;



public Board(Tetris parent) {

setFocusable(true);
curPiece = new Shape();
timer = new Timer(400, this);
timer.start();

statusbar = parent.getStatusBar();
board = new Tetrominoes[BoardWidth * BoardHeight];
addKeyListener(new TAdapter());
clearBoard();
}

public void actionPerformed(ActionEvent e) {
if (isFallingFinished) {
isFallingFinished = false;
newPiece();
} else {
oneLineDown();
}
}


int squareWidth() { return (int) getSize().getWidth() / BoardWidth; }
int squareHeight() { return (int) getSize().getHeight() / BoardHeight; }
Tetrominoes shapeAt(int x, int y) { return board[(y * BoardWidth) + x]; }


public void start()
{
if (isPaused)
return;

isStarted = true;
isFallingFinished = false;
numLinesRemoved = 0;
clearBoard();

newPiece();
timer.start();
}

private void pause()
{
if (!isStarted)
return;

isPaused = !isPaused;
if (isPaused) {
timer.stop();
statusbar.setText("paused");
} else {
timer.start();
statusbar.setText(String.valueOf(numLinesRemoved));
}
repaint();
}

public void paint(Graphics g)
{
super.paint(g);

Dimension size = getSize();
int boardTop = (int) size.getHeight() - BoardHeight * squareHeight();


for (int i = 0; i < BoardHeight; ++i) {
for (int j = 0; j < BoardWidth; ++j) {
Tetrominoes shape = shapeAt(j, BoardHeight - i - 1);
if (shape != Tetrominoes.NoShape)
drawSquare(g, 0 + j * squareWidth(),
boardTop + i * squareHeight(), shape);
}
}

if (curPiece.getShape() != Tetrominoes.NoShape) {
for (int i = 0; i < 4; ++i) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
drawSquare(g, 0 + x * squareWidth(),
boardTop + (BoardHeight - y - 1) * squareHeight(),
curPiece.getShape());
}
}
}

private void dropDown()
{
int newY = curY;
while (newY > 0) {
if (!tryMove(curPiece, curX, newY - 1))
break;
--newY;
}
pieceDropped();
}

private void oneLineDown()
{
if (!tryMove(curPiece, curX, curY - 1))
pieceDropped();
}


private void clearBoard()
{
for (int i = 0; i < BoardHeight * BoardWidth; ++i)
board[i] = Tetrominoes.NoShape;
}

private void pieceDropped()
{
for (int i = 0; i < 4; ++i) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
board[(y * BoardWidth) + x] = curPiece.getShape();
}

removeFullLines();

if (!isFallingFinished)
newPiece();
}

private void newPiece()
{
curPiece.setRandomShape();
curX = BoardWidth / 2 + 1;
curY = BoardHeight - 1 + curPiece.minY();

if (!tryMove(curPiece, curX, curY)) {
curPiece.setShape(Tetrominoes.NoShape);
timer.stop();
isStarted = false;
statusbar.setText("game over");
}
}

private boolean tryMove(Shape newPiece, int newX, int newY)
{
for (int i = 0; i < 4; ++i) {
int x = newX + newPiece.x(i);
int y = newY - newPiece.y(i);
if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
return false;
if (shapeAt(x, y) != Tetrominoes.NoShape)
return false;
}

curPiece = newPiece;
curX = newX;
curY = newY;
repaint();
return true;
}

private void removeFullLines()
{
int numFullLines = 0;

for (int i = BoardHeight - 1; i >= 0; --i) {
boolean lineIsFull = true;

for (int j = 0; j < BoardWidth; ++j) {
if (shapeAt(j, i) == Tetrominoes.NoShape) {
lineIsFull = false;
break;
}
}

if (lineIsFull) {
++numFullLines;
for (int k = i; k < BoardHeight - 1; ++k) {
for (int j = 0; j < BoardWidth; ++j)
board[(k * BoardWidth) + j] = shapeAt(j, k + 1);
}
}
}

if (numFullLines > 0) {
numLinesRemoved += numFullLines;
statusbar.setText(String.valueOf(numLinesRemoved));
isFallingFinished = true;
curPiece.setShape(Tetrominoes.NoShape);
repaint();
}
}

private void drawSquare(Graphics g, int x, int y, Tetrominoes shape)
{
Color colors[] = { new Color(0, 0, 0), new Color(204, 102, 102),
new Color(102, 204, 102), new Color(102, 102, 204),
new Color(204, 204, 102), new Color(204, 102, 204),
new Color(102, 204, 204), new Color(218, 170, 0)
};


Color color = colors[shape.ordinal()];

g.setColor(color);
g.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2);

g.setColor(color.brighter());
g.drawLine(x, y + squareHeight() - 1, x, y);
g.drawLine(x, y, x + squareWidth() - 1, y);

g.setColor(color.darker());
g.drawLine(x + 1, y + squareHeight() - 1,
x + squareWidth() - 1, y + squareHeight() - 1);
g.drawLine(x + squareWidth() - 1, y + squareHeight() - 1,
x + squareWidth() - 1, y + 1);

}

class TAdapter extends KeyAdapter {
public void keyPressed(KeyEvent e) {

if (!isStarted || curPiece.getShape() == Tetrominoes.NoShape) {
return;
}

int keycode = e.getKeyCode();

if (keycode == 'p' || keycode == 'P') {
pause();
return;
}

if (isPaused)
return;

switch (keycode) {
case KeyEvent.VK_LEFT:
tryMove(curPiece, curX - 1, curY);
break;
case KeyEvent.VK_RIGHT:
tryMove(curPiece, curX + 1, curY);
break;
case KeyEvent.VK_DOWN:
tryMove(curPiece.rotateRight(), curX, curY);
break;
case KeyEvent.VK_UP:
tryMove(curPiece.rotateLeft(), curX, curY);
break;
case KeyEvent.VK_SPACE:
dropDown();
break;
case 'd':
oneLineDown();
break;
case 'D':
oneLineDown();
break;
}

}
}
}
Finally, we have the Board.java file. This is where the game logic is located.
...
isFallingFinished = false;
isStarted = false;
isPaused = false;
numLinesRemoved = 0;
curX = 0;
curY = 0;
...
We initialize some important variables. The isFallingFinished variable determines, if the tetris shape has finished falling and we then need to create a new shape. The numLinesRemoved counts the number of lines, we have removed so far. The curX and curY variables determine the actual position of the falling tetris shape.
setFocusable(true);
We must explicitly call the setFocusable() method. From now, the board has the keyboard input.
timer = new Timer(400, this);
timer.start();
Timer object fires one or more action events after a specified delay. In our case, the timer calls the actionPerformed() method each 400 ms.
public void actionPerformed(ActionEvent e) {
if (isFallingFinished) {
isFallingFinished = false;
newPiece();
} else {
oneLineDown();
}
}
The actionPerformed() method checks if the falling has finished. If so, a new piece is created. If not, the falling tetris piece goes one line down.
Inside the paint() method, we draw the all objects on the board. The painting has two steps.
for (int i = 0; i < BoardHeight; ++i) {
for (int j = 0; j < BoardWidth; ++j) {
Tetrominoes shape = shapeAt(j, BoardHeight - i - 1);
if (shape != Tetrominoes.NoShape)
drawSquare(g, 0 + j * squareWidth(),
boardTop + i * squareHeight(), shape);
}
}
In the first step we paint all the shapes, or remains of the shapes, that have been dropped to the bottom of the board. All the squares are rememberd in the board array. We access it using the shapeAt() method.
if (curPiece.getShape() != Tetrominoes.NoShape) {
for (int i = 0; i < 4; ++i) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
drawSquare(g, 0 + x * squareWidth(),
boardTop + (BoardHeight - y - 1) * squareHeight(),
curPiece.getShape());
}
}
In the second step, we paint the actual falling piece.
private void dropDown()
{
int newY = curY;
while (newY > 0) {
if (!tryMove(curPiece, curX, newY - 1))
break;
--newY;
}
pieceDropped();
}
If we press the space key, the piece is dropped to the bottom. We simply try to drop the piece one line down until it reaches the bottom or the top of another fallen tetris piece.
private void clearBoard()
{
for (int i = 0; i < BoardHeight * BoardWidth; ++i)
board[i] = Tetrominoes.NoShape;
}
The clearBoard() method fills the board with empty NoSpapes. This is later used at collision detection.
 private void pieceDropped()
{
for (int i = 0; i < 4; ++i) {
int x = curX + curPiece.x(i);
int y = curY - curPiece.y(i);
board[(y * BoardWidth) + x] = curPiece.getShape();
}

removeFullLines();

if (!isFallingFinished)
newPiece();
}
The pieceDropped() method puts the falling piece into the board array. Once again, the board holds all the squares of the pieces and remains of the pieces that has finished falling. When the piece has finished falling, it is time to check, if we can remove some lines off the board. This is the job of the removeFullLines() method. Then we create a new piece. More precisely, we try to create a new piece.
private void newPiece()
{
curPiece.setRandomShape();
curX = BoardWidth / 2 + 1;
curY = BoardHeight - 1 + curPiece.minY();

if (!tryMove(curPiece, curX, curY)) {
curPiece.setShape(Tetrominoes.NoShape);
timer.stop();
isStarted = false;
statusbar.setText("game over");
}
}
The newPiece() method creates a new tetris piece. The piece gets a new random shape. Then we compute the initial curX and curY values. If we cannot move to the initial positions, the game is over. We top out. The timer is stopped. We put game over string on the statusbar.
 private boolean tryMove(Shape newPiece, int newX, int newY)
{
for (int i = 0; i < 4; ++i) {
int x = newX + newPiece.x(i);
int y = newY - newPiece.y(i);
if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
return false;
if (shapeAt(x, y) != Tetrominoes.NoShape)
return false;
}

curPiece = newPiece;
curX = newX;
curY = newY;
repaint();
return true;
}
The tryMove() method tries to move the tetris piece. The method returns false, if it has reached the board boundaries or it is adjacent to the already fallen tetris pieces.
for (int i = BoardHeight - 1; i >= 0; --i) {
boolean lineIsFull = true;

for (int j = 0; j < BoardWidth; ++j) {
if (shapeAt(j, i) == Tetrominoes.NoShape) {
lineIsFull = false;
break;
}
}

if (lineIsFull) {
++numFullLines;
for (int k = i; k < BoardHeight - 1; ++k) {
for (int j = 0; j < BoardWidth; ++j)
board[(k * BoardWidth) + j] = shapeAt(j, k + 1);
}
}
}
Inside the removeFullLines() method, we check if there is any full row among all rows in the board. If there is at least one full line, it is removed. After finding a full line we increase the counter. We move all the lines above the full row one line down. This way we destroy the full line. Notice, that in our Tetris game, we use so called naive gravity. This means, that the squares may be left floating above empty gaps.
Every tetris piece has four squares. Each of the squares is drawn with the drawSquare() method. Tetris pieces have different colors.
 g.setColor(color.brighter());
g.drawLine(x, y + squareHeight() - 1, x, y);
g.drawLine(x, y, x + squareWidth() - 1, y);
The left and top sides of a square are drawn with a brighter color. Similarly, the bottom and right sides are drawn with darker colors. This is to simulate a 3D edge.
We control the game with a keyboard. The control mechanism is implemented with a KeyAdapter. This is an inner class that overrides the keyPressed() method.
case KeyEvent.VK_RIGHT:
tryMove(curPiece, curX + 1, curY);
break;
If we pressed the left arrow key, we try to move the falling piece one square to the left.
Tetris
Figure: Tetris
This was the Tetris game.
Continue Reading