Current area: HOME -> Java Games tutorial #4
|
Java Games tutorial #4
We're
now ready to write the "game setup" code. You've
probably already guessed that this state allows the player to
"set up" the board. Pretty tricky, eh?
You
might think we could just jump into the code that lets the players
(human and computer) place their ships. We could,
but we'd quickly run into all sorts of problems: how do we record
a ship's position? How do we decide when we're placing the
submarine as opposed to the cruiser? How do we put a ship
"on" the board?
So,
before we write a lick of code, we're going to have to decide how
our classes will interact. Since we're placing ships on the
board, the two classes on which we'll focus are the Ship
and the Board (hmm...that's not too obvious, is it?).
In
this section, we're going to do exactly that--list out the
functionality we need to support the game setup state and
translate that list into code requirements for our classes.
Setting
Up the Setup
When
our game enters the "setup" state, it has to perform
several functions. These are:
-
Allow
the players (human and computer) to manipulate all 5 ships
(aircraft carrier, battleship, cruiser, submarine, and PT
boat).
-
Set
the ships' positions.
-
Set
the ships' orientations (up, down, left, or right).
-
Place
the ships on the board.
-
Draw
the ships on the board.
This
task list implies several characteristics about the ships and the
board. First, ships must understand their type (carrier, PT boat,
etc), position, and orientation. Second, the Board must have some
way of knowing which ships occupy which squares. Finally, either
the ships or the board has to be able to draw occupied squares.
So,
how does this translate into code?
First
off, we can see that the Ship class is going to have to track
several variables: a type (aircraft carrier, battleship,
cruiser, sub, or PT boat), a position (given as a row and
column on the board), an orientation (given as up,
down, left, or right), and a size (5 for the aircraft
carrier, 4 for the battleship, 3 for the cruiser and sub, and 2
for the PT boat).
Using
our recent experience with constants as a guide, let's add the
following code to the top of the Ship class. It will help us record the
"ship type" and orientation information:
// Class variables.
public static final int TYPE_AIRCRAFT_CARRIER = 0;
public static final int TYPE_BATTLESHIP = 1;
public static final int TYPE_CRUISER = 2;
public static final int TYPE_SUBMARINE = 3;
public static final int TYPE_PT_BOAT = 4;
public static final int ORIENTATION_UP = 0;
public static final int ORIENTATION_RIGHT = 1;
public static final int ORIENTATION_DOWN = 2;
public static final int ORIENTATION_LEFT = 3;
// Instance variables.
private int type;
private int orientation;
Let's
also add some integer variables to track the row and column of the
ship's starting point, as well as the size of the ship:
// Instance variables.
private int type;
private int orientation;
private int row;
private int column;
private int size;
By now, you probably know that we're going to initialize these
instance variables in the constructor, so let's add that next:
public Ship(int shipType, int dir,
int row, int col, int length) {
this.type = shipType;
this.orientation = dir;
this.row = row;
this.column = col;
this.size = length;
}
Notice that we're initializing all of the ship's variables
using input parameters. In other words, when we create the ship
using new, we'll do something like
this:
Ship sub = new Ship(Ship.TYPE_SUBMARINE,
Ship.ORIENTATION_RIGHT, 3, 2, 3);
This will call the constructor we just wrote, with shipType
containing TYPE_SUBMARINE (defined as
3 above), dir
containing ORIENTATION_RIGHT (1), row
containing 3, col containing 2, and length
containing 3. When the code inside the constructor executes, it
will reduce to this:
this.type = 3;
this.orientation = 1;
this.row = 3;
this.col = 2;
this.size = 3;
So much for the ship.
True, it doesn't really do much right now, but we can add more
functionality later, when we know exactly what we need it to do.
So, what about the board?
We mentioned above that the board needs to track where the
ships fall. How could we go about this? Well, if we picture a
traditional Battleship board in our minds, we see a grid with
holes in each square. We fit the ships' pegs into the holes, which
causes the ships to occupy a set number of adjacent grid squares.
So, really, we can imagine the board as a collection of cells,
each of which can hold 1 piece of a ship. Figure 2 can summarizes
this thought.

Figure
2--the battleship placed on the grid.
Here
we see the battleship (length 4), with its starting position at
row 4, column 4 (remember that Java starts numbering at 0), and an
"up" orientation. We also see that the board contains
100 cells (10 x 10) and four of those cells contain pieces of the
battleship.
The
next question is, "How do we represent those 100 cells in
code?"
A
Wide Array of Choices
Well,
one way would be to introduce an integer variable for each cell in
the grid, and store a number in the variable to represent the type
of ship occupying the space. For instance, we might have something
like this inside Board.java:
//
Additional class variables.
public
static final int ID_EMPTY = 0;
public
static final int ID_BATTLESHIP = 1;
public
static final int ID_AIRCRAFT_CARRIER = 2;
public
static final int ID_CRUISER = 3;
public
static final int ID_SUBMARINE = 4;
public
static final int ID_PT_BOAT = 5;
//
Additional instance variables.
private
int cell_00; // Row 0, column 0.
private
int cell_01; // Row 0, column 1.
private
int cell_02; // etc.
...
private
int cell_10;
private
int cell_11;
...
private
int cell_97; // Row 9, column 7.
private
int cell_98; // Row 9, column 8.
private
int cell_99; // Row 9, column 9.
The
cells would correspond to the grid in the following way:

Figure
3--Matching cell variables to locations on the grid.
This
would behave just fine--except that it's a lot of work! As
you can see, we'd have to declare 100 'cell' variables by hand. It
would be much better if we could have Java handle all the variable
creation automatically.
Fortunately,
there is a way to do this. If you think back to
tutorial #2, we talked about
arrays of variables
. In English, 'array' means (amongst other thans),
"An impressive display of numerous people or objects."
More simply, it is "a collection". Arrays in Java are
pretty much the same thing. If you have several pieces of related
data--for example, a list of names--you can store them in an
array.
We're
going to explore the basics of arrays, then use one to help us
store information about the ships on the game board.
One,
Two, Buckle Your Shoe...
Previously,
we've worked with 1 dimensional arrays. That's just fancy
jargon for "a list you can access using only one index".
The list of names serves as a perfect example. Suppose you have
the following list:
Kirk,
Spock, Scotty, Uhura
and
you want to store them in a 1 dimensional array. In Java, you
might do the following:
private
String[] names = {"Kirk", "Spock",
"Scotty", "Uhura"};
Recall
that the brackets ('[' and ']')
tell Java that the variable 'names'
is an array. Now, if you want to print the third name in the list,
you might write the following:
System.out.println(names[2]);
(where
we use '2' because Java starts numbering the names from 0).
This
is a classic 1 dimensional array--you can access all the names
using a single number (called an index).
However,
our battleship grid is a little different. It would be much easier
for us to use two numbers to access each cell--one for the row and
one for the column. How can we do that?
The
answer involves using a 2 dimensional array. The format of
such an array should look familiar:
private
int[][] gridCells;
Notice
how this variable requires two sets of brackets, whereas names
required only one. We need the extra set to tell Java that this is
a 2 dimensional array. Also, notice that we didn't initialize
gridCells the way we did names
(i.e., we set names equal to {"Kirk",
"Spock",...}, but didn't set gridCells
equal to anything). This means that gridCells
isn't quite ready to use. The above statement tells Java to ready
a container to hold our array, but doesn't put anything in
that container. To do that, we need to create a new int
array and assign it to gridCells. We
could do that as follows:
gridCells
= new int[10][10];
Now
that we've created a two-dimensional array, we just have to access
its elements. If we wanted to store a value in the third row and
first column, we would do this:
this.gridCells[2][0]
= 15;
Similarly,
if we wanted to retrieve the value in the 10th row and 10th
column, we would write:
int
value = this.gridCells[9][9];
That's
all there is to it. So now we're ready to see how this applies to
Battleship.
She
Cells Sea Schells...
It
seems pretty clear that we're going to have to add a 2-dimensional
array to Board.java. We'll need to initialize it to the proper
size in Board's constructor, and add a method to place a ship into
the board. For the moment, we won't worry about the contents of
that method. Also, if you recall the last tutorial, we put a
1-dimensional array of Ships in the Board class, and called it fleet.
We're going to have to initialize the fleet
array as well. It needs to hold 5 ships.
Putting
those ideas together, here's the new code to add to Board.java:
// Class variables.
private static final int ROW_COUNT = 10;
private static final int COLUMN_COUNT = 10;
private static final int ROW_PIXEL_HEIGHT = 20;
private static final int COLUMN_PIXEL_WIDTH = 20;
private static final int SHIPS_PER_FLEET = 5;
// Instance variables.
private RedPeg[] hitMarkers;
private WhitePeg[] missMarkers;
private Ship[] fleet;
private int[][] gridCells;
// Methods.
/**
* Constructor for objects of class Board
*/
public Board()
{
this.fleet = new Ship[SHIPS_PER_FLEET];
this.gridCells = new int[ROW_COUNT][COLUMN_COUNT];
}
/**
* Add a ship to the grid.
*/
public void addShip(Ship newShip) {
// Put code here to add the ship to the board.
}
Note that the comment in cyan is a placeholder--we'll be
replacing it with more code in the near future.
Last, But Not Least
We're almost finished--we've pretty much fleshed out the
fundamental behavior of the ship and the board. However, we still
have to deal with the "Put code here to add the ship to the
board" part. Doing so requires that we touch on a bit more
theory before writing the code.
First off, how do we represent the ships within the grid? We
know the grid stores numbers. We also know that each ship type has
an associated number. One easy solution involves marrying these
two ideas: we just enter a ship's type value into every
grid call into which it falls. Figure 4 illustrates this.

Figure 4--Storing ships' type
values in the grid.
In the diagram, the blue grid shows the Board's visual
representation while the green grid shows the contents of the 2
dimensional gridCells array.
This solution works well, with one drawback. Remember how we
said that Java initializes all variables to 0? Well, 2 dimensional
arrays are no exception. When Java executes the
this.gridCells = new
int[ROW_COUNT][COLUMN_COUNT];
command, it set's the value of every cell to 0. Notice
the value that we're using for the aircraft carrier: also 0. This
creates a problem, because the computer can't tell the difference
between an "aircraft carrier 0" and a zero just sitting
around in a newly-initialized cell.
There are two solutions to this. First, we could change the
value that we're using for the aircraft carrier to something other
than zero. Second, we could fill the grid with some value besides
zero when we create it. We're going to opt for the second method.
I Code You So
We're now ready write some code. Rather than spell it out for
you, we're going to present it in all its glory, then briefly
discuss the highlights. Much of it you've already seen in one form
or another, so we don't need to get into long explanations anyway.
First off, we need to add a couple of lines to Ship. Let's
start by adding a new Ship type:
public static final int TYPE_NONE = -1;
public static final int TYPE_AIRCRAFT_CARRIER = 0;
public static final int TYPE_BATTLESHIP = 1;
public static final int TYPE_CRUISER = 2;
public static final int TYPE_SUBMARINE = 3;
public static final int TYPE_PT_BOAT = 4;
After the constructor, we need to add some getter
functions. Getters are special functions that return the
value of of an object's instance variables. You'll see what we
mean:
public int getType() {
return this.type;
}
public int getOrientation() {
return this.orientation;
}
public int getSize() {
return this.size;
}
public int getRow() {
return this.row;
}
public int getColumn() {
return this.column;
}
Next,
we're going to modify Board.java's constructor to fill the grid cells with something other than
zero:
public Board()
{
this.gridCells = new int[ROW_COUNT][COLUMN_COUNT];
// Fill the grid cells with empty spaces.
int i = 0;
while (i < ROW_COUNT) {
int j = 0;
while (j < COLUMN_COUNT) {
this.gridCells[i][j] = Ship.TYPE_NONE;
j++;
}
i++;
}
}
This routine involves a very important concept known as
"nested loops". However, rather than interrupting our
current train of thought to explain that topic, we'll continue
with our current code. When you reach a good stopping point in the
current discussion, you can learn about nested loops here.
Finally,
we'll add the code that actually places a ship into the board's
grid cells:
public void addShip(Ship newShip) {
int row = newShip.getRow();
int col = newShip.getColumn();
int orientation = newShip.getOrientation();
int i = 0;
// Add the ship to the fleet array.
this.fleet[newShip.getType()] = newShip;
if (orientation == Ship.ORIENTATION_UP) {
while (i < newShip.getSize()) {
this.gridCells[row - i][col] = newShip.getType();
i++;
}
}
else if (orientation == Ship.ORIENTATION_RIGHT) {
while (i < newShip.getSize()) {
this.gridCells[row][col + i] = newShip.getType();
i++;
}
}
else if (orientation == Ship.ORIENTATION_DOWN) {
while (i < newShip.getSize()) {
this.gridCells[row + i][col] = newShip.getType();
i++;
}
}
else {
// Orientation must be LEFT.
while (i < newShip.getSize()) {
this.gridCells[row][col - i] = newShip.getType();
i++;
}
}
}
Rather
than explicitly explain how this code works, we'll discuss its
behavior for the case of the ship shown in figure 2. Referring
to that figure, we see that the player has placed the battleship
at row 4, column 4, with an orientation of "up". If we
called addShip with a Ship of TYPE_BATTLESHIP,
with row = 4, column
= 4, and orientation = ORIENTATION_UP,
the following code:
int row = newShip.getRow();
int col = newShip.getColumn();
int orientation = newShip.getOrientation();
int i = 0;
would
reduce to
int row = 4;
int col = 4;
int orientation = 0;
int i = 0;
and
Java would execute the first of the if...else
branches (corresponding to an orientation
of ORIENTATION_UP, which we
originally defined as 0). That if...else
branch executes the following while
loop:
while (i < newShip.getLength()) {
this.gridCells[row - i][col] = newShip.getType();
i++;
}
where
i starts at 0, and newShip.getLength()
returns 4 (because the battleship is 4 spaces long). Since row
and column both equal 4, this code
reduces to:
while
(i < 4) {
this.gridCells[4 - i][4] = 1;
i++;
}
When
i=0, the statement immediately after
the while reduces to:
this.gridCells[4
- 0][4] = 1;
Similarly,
when i=1, 2, and
3, the statement immediately after the while
reduces to:
this.gridCells[4
- 1][4] = 1;
this.gridCells[4 - 2][4] = 1;
this.gridCells[4 - 3][4] = 1;
In
other words, this places the value '1' in grid cells (4, 4), (3,
4), (2, 4), and (1, 4)--which is exactly what we want, according
to figure 2.
One
other point: notice this code:
// Add the ship to the fleet array.
this.fleet[newShip.getType()] = newShip;
This stores the new Ship object in the fleet array. This
has no effect on the gridCells, but it does allow the board
to track which ship is the battleship, the carrier, etc. We'll
need this information in later tutorials.
That's
pretty much it for storing ship information on the board. You
might want to run through the right, down, and left cases to make
sure you know how the code behaves.
Draw,
Pard'ner!
Last,
but certainly not least, we need to write code that draws the
ships to the board. We already have code in the Board class to
draw the grids, so it seems logical to add some more that draws
the ships. That's exactly what we're going to do.
Before
we write the code, let's consider how we want it to look.
Consulting figure 2, we see that the ships will appear as gray
boxes that don't quite fill the grid cells. We could do something
fancier, but for now, let's keep things simple. We'll add more
interesting graphics to the game in a later tutorial (though we'll
save these for the Ships' status displays, and stick with gray
boxes on the grid).
Now
let's write an outline for the grid drawing method, which we'll
later translate into code. The concept is really pretty simple:
-
Start
with the first row.
-
On
the current row, start at the first column.
-
If
the cell at the current row and column isn't empty, draw a
gray box.
-
Move
on to the next column.
-
If
we haven't run out of columns, go back to step 3.
-
If
we've run out of columns, move on to the next row.
-
If
we haven't run out of rows, go back to step 2.
-
If
we're out of rows, we're done.
Now
let's write the code that goes with this outline. We're going to
introduce a new kind of loop--called a do loop--in
this method. You can read about the particulars of do loops
here.
Here's
the code:
public void drawShips(Graphics gfx, Color shipColor,
int startX, int startY) {
// Set the draw color.
gfx.setColor(shipColor);
int row = 0; // Start at the first row.
do {
int col = 0;// Start at the first column.
do {
// Is the cell empty?
if (this.gridCells[row][col] != Ship.TYPE_NONE) {
// No--the cell contains part of a ship.
// Calculate the starting position of the cell.
int x = startX + col * COLUMN_PIXEL_WIDTH;
int y = startY + row * ROW_PIXEL_HEIGHT;
// Draw in a box that's smaller than the cell.
gfx.fillRect(x + 2, y + 2,
COLUMN_PIXEL_WIDTH - 4,
ROW_PIXEL_HEIGHT - 4);
}
col++; // Move on to the next column.
} while (col < COLUMN_COUNT);
row++; // Move on to the next row.
} while (row < ROW_COUNT);
}
Notice
that our friend the nested loop is back. If you haven't already
read up on them, now might be a good
time.
We're
not going to explain this code in detail. Notice how our comments
(shown in green) mirror the steps of our code outline. You should
be able to figure out what's going on by comparing the comments
and the outline.
We
will say this: the fillRect command draws and fills
a rectangle, and has the following parameters:
fillRect(upperLeftX,
upperLeftY, width, length);
In
our code, x and y
represent the pixel coordinates of the current grid cell's upper
left corner. Therefore, x+2 and y+2
define a point 2 pixels over and 2 pixels down from the cell's
upper left corner. We want our gray box to end 2 pixels from the
bottom and 2 pixels from the right of the grid's bottom right
corner, which means the width would be 4 pixels smaller than the
cell width (COLUMN_PIXEL_WIDTH) and 4
pixels smaller than the cell height (ROW_PIXEL_HEIGHT).
Plugging in these values to the fillRect
command, we get:
fillRect(x
+ 2, y + 2, COLUMN_PIXEL_WIDTH - 4, ROW_PIXEL_HEIGHT - 4);
Now
all that remains is to call the ship-drawing routine. This is
simple--just add the following two lines to the drawGrids
method inside BattleshipApp.java:
this.redBoard.drawShips(gfx, Color.gray,
RED_GRID_X, RED_GRID_Y);
this.blueBoard.drawShips(gfx, Color.gray,
BLUE_GRID_X, BLUE_GRID_Y);
That's
it for the new ship and board code. We made a lot of changes.
Let's try compiling to see if everything works. If not, you can
find working code samples here.
Testing,
Testing, 1, 2, 3...
We've
just added some basic functionality to the game, but it doesn't do
anything by itself. In theory, we can now add ships to the game
board and have them appear on screen. We need that capability to
allow the computer and the user to place their ships. However, we
don't really know if our new code works. We could ignore this
potential problem and just jump right into the ship placement
code--but that's not such a hot idea. Consider what would happen
if we wrote the placement code and it didn't work. We wouldn't
know if the problem stemmed from the placement code or the
underlying ship and board code we wrote in the last section.
The
moral of the story is this: always test your code whenever you
can, even if it means "inventing" data you'll later
remove!
In
our case, we're going to have to create some "test
ships" and place them at fixed locations on the board. This
will allow us to see if the storage and drawing code works. We can
debug any problems until our test case works exactly as expected.
After that, we'll delete our test code and start writing the ship
placement code.
It
might seem like a waste to write a test case and then delete
it--but believe me, it saves a great deal of time in the long run.
For
our test case, let's use the layout shown in figure 4:
-
Aircraft
carrier: size 5, row 7, column 3, orientation right.
-
Battleship:
size 4, row 1, column 4, orientation down.
-
Cruiser:
size 3, row 8, column 0, orientation up.
-
Submarine:
size 3, row 9, column 8, orientation left.
-
PT
Boat: size 2, row 2, column 7, orientation down.
Let's
add some code the Board's constructor that automatically creates
our test fleet:
public Board()
{
this.gridCells = new int[ROW_COUNT][COLUMN_COUNT];
// Fill the grid cells with empty spaces.
int i = 0;
while (i < ROW_COUNT) {
int j = 0;
while (j < COLUMN_COUNT) {
this.gridCells[i][j] = Ship.TYPE_NONE;
j++;
}
i++;
}
// Create a test fleet.
// REMOVE THIS CODE AFTER TESTING IS COMPLETE!
Ship testShip;
testShip = new Ship(Ship.TYPE_AIRCRAFT_CARRIER,
Ship.ORIENTATION_RIGHT, 7, 3, 5);
this.addShip(testShip);
testShip = new Ship(Ship.TYPE_BATTLESHIP,
Ship.ORIENTATION_DOWN, 1, 4, 4);
this.addShip(testShip);
testShip = new Ship(Ship.TYPE_CRUISER,
Ship.ORIENTATION_UP, 8, 0, 3);
this.addShip(testShip);
testShip = new Ship(Ship.TYPE_SUBMARINE,
Ship.ORIENTATION_LEFT, 9, 8, 3);
this.addShip(testShip);
testShip = new Ship(Ship.TYPE_PT_BOAT,
Ship.ORIENTATION_DOWN, 2, 7, 2);
this.addShip(testShip);
}
Now
we have a test fleet!
As
a side note, look at how we're using testShip
in the above code. It's what we call a temporary variable.
We're using it to hold each new ship so we can pass it into the addShip
method. Other than that, we don't use it for anything. In fact, we
could eliminate it entirely by combining statements as follows:
this.addShip(new
Ship(Ship.TYPE_PT_BOAT,
Ship.ORIENTATION_DOWN, 2, 7, 2));
However,
using testShip makes the code a
little easier to read.
At
long last, we're ready to run our code and see what happens.
Compile everything and run it. If all goes well, both the red and
blue grids should have ships on them, laid out as shown in figure
4.
When
you run the code, you'll probably see a tiny error--the gray boxes
we used for the ship are one pixel too small in both the x and y
directions. This is easy to fix--just change the fillRect
command in the Board's drawShips
method to this:
gfx.fillRect(x + 2, y + 2,
COLUMN_PIXEL_WIDTH - 3,
ROW_PIXEL_HEIGHT - 3);
Compile
and run again, and see if everything worked out.
Once
you get the code working, go ahead and play with it. Change the
position of the test fleet. Change the color of the ships from Color.gray
to something else (Color.pink?). Mess
around. Have fun.
When
you're done, remove the code that creates the test fleet. Then,
get ready to write some AI (artificial intelligence) code.
|