<%@ EnableSessionState=true %> <% Option Explicit dim thisurl thisURL=Request.ServerVariables("PATH_INFO") & "?" & Request.ServerVariables("QUERY_STRING") const title="Java Games tutorial #4" %>
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:

  1. Allow the players (human and computer) to manipulate all 5 ships (aircraft carrier, battleship, cruiser, submarine, and PT boat).

  2. Set the ships' positions.

  3. Set the ships' orientations (up, down, left, or right).

  4. Place the ships on the board.

  5. 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:

 

  1. Start with the first row.

  2. On the current row, start at the first column.

  3. If the cell at the current row and column isn't empty, draw a gray box.

  4. Move on to the next column.

  5. If we haven't run out of columns, go back to step 3.

  6. If we've run out of columns, move on to the next row.

  7. If we haven't run out of rows, go back to step 2.

  8. 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.