|
Putting up obstacles
A wide open board is rather booring and makes the game strategy too simplistic. In this lesson, we'll add some water squares to our map. First we have to paint the water on our map and then we'll have to keep our unit from moving onto those squares.
|
This is meant to be a replacement when we do things (javascript menus) that might get hidden behind the applet.
|
Painting the obstacles
Feedback
Now lets add some water to our map. We'll add three lakes: two little lakes, one at (3,3) and the other at (8,8) and one big lake in the midle of the board encompassing 4 map squares. Painting the map is fairly easy. We just add a few lines to the end of our paint function:
public void paint (Graphics canvas) {
canvas.setColor (Color.BLACK);
canvas.fillRect (0, 0, 300, 300);
canvas.setColor (Stakeout.unitColor);
canvas.fillRect ((Stakeout.unitColumn - 1) * 30, (Stakeout.unitRow - 1) * 30, 30, 30);
canvas.setColor (Color.BLUE);
canvas.fillRect ((3 - 1) * 30, (3 - 1) * 30, 30, 30);
canvas.fillRect ((8 - 1) * 30, (8 - 1) * 30, 30, 30);
canvas.fillRect ((5 - 1) * 30, (5 - 1) * 30, 60, 60);
}
Eliminating repeated operations
Do you notice a pattern with the new lines? Filling a rectangle requires us to figure out the upper-left corner coordinate of the row / column that we want to color. In all cases, it requires us to subtract 1 from the row and column and then add multiply the result by 30. We are repeating this process multiple times. Later in our game we will have to perform the operations even more. What if on one of the occassions we accidentally typed "+ 1" instead of "- 1" (actually a very common error). Then we would have a defect in our program, commonly called a "bug". We can save ourselves the possibility of creating a bug like this by creating a function to perform this process for us. We can add the following code to our Component class:
// Takes a row or column number
// Returns the upper (for row) or left (for column) coordinate for that row / column
public int UpperLeft (int gridNumber) {
return (gridNumber - 1) * 30;
}
Then we can use this new function in our paint function to eliminate the repeated code like so:
public void paint (Graphics canvas) {
canvas.setColor (Color.BLACK);
canvas.fillRect (0, 0, 300, 300);
canvas.setColor (Stakeout.unitColor);
canvas.fillRect (UpperLeft (Stakeout.unitColumn), UpperLeft (Stakeout.unitRow), 30, 30);
canvas.setColor (Color.BLUE);
canvas.fillRect (UpperLeft (3), UpperLeft (3), 30, 30);
canvas.fillRect (UpperLeft (8), UpperLeft (8), 30, 30);
canvas.fillRect (UpperLeft (5), UpperLeft (5), 60, 60);
}
This doesn't save us a lot of typing but it will give us a lot of piece of mind. If we accidentally type "UpperLert" instead of "UpperLeft", the compiler will complain that there is no such function and we will quickly realize our mistake. But if we type "+ 1" instead of "- 1", the compiler will not complain (it's a perfectly legal mathematical operation), but we will have a bug and it may not be easy to determine why.
Eliminating repeated constants
We still have some repetition in our code. The number "30" gets repeated a lot. Whenever we have common constants like this we should create a constant variable to replace them.
public static final int GRIDSIZE = 30;
We place this code at the top of our StakeoutComponent class since that's the class where we're going to use it the most. The "final" keyword prevents us from accidentally changing the value later. If we do, we will get a compiler error. Notic that we're following the capitalization convention for constant values. This is a good habit to get into. Now we have to change our program to use this variable.
public void paint (Graphics canvas) {
canvas.setColor (Color.BLACK);
canvas.fillRect (0, 0, 300, 300);
canvas.setColor (Stakeout.unitColor);
canvas.fillRect (
UpperLeft (Stakeout.unitColumn),
UpperLeft (Stakeout.unitRow),
GRIDSIZE,
GRIDSIZE
);
canvas.setColor (Color.BLUE);
canvas.fillRect (UpperLeft (3), UpperLeft (3), GRIDSIZE, GRIDSIZE);
canvas.fillRect (UpperLeft (8), UpperLeft (8), GRIDSIZE, GRIDSIZE);
canvas.fillRect (UpperLeft (5), UpperLeft (5), 60, 60);
}
// Takes a row or column number
// Returns the upper (for row) or left (for column) coordinate for that row / column
public int UpperLeft (int gridNumber) {
return (gridNumber - 1) * GRIDSIZE;
}
The reason we create a constant variable for this is that it is very easy to make mistakes. We could accidentally type "39" instead of "30" (they're very close to each other on the keyboard). This is another situation that will result in a bug that will not be caught by the compiler therefore introducing a bug in our program. Again, it doesn't save us any typing (in fact it creates more) but the peace of mind that we get from not having to worry about stupid little mistakes like this is well worth it.
Constants and maintenance
There's actually an even more important reason for creating constant variables to represent these types of values in our game. If we later decide that we want to change the size of grids on our map to be 50 pixels in size. We'd have to go through the whole program and change every "30" to a "50". If we missed one, we'd have a bug in our program. When we use a constant variable instead we only have to change one place in our code and bingo, everything works... Well, actually not quite. We forgot that the 60 is actually derived from 30 (30 * 2). And that's not all. The "300" is also a derived value (30 * 10). Let's change them to use our constant too.
class StakeoutComponent extends Component {
public static final int GRIDSIZE = 30;
public void StakeoutComponent () {
setSize (GRIDSIZE * 10, GRIDSIZE * 10);
setVisible (true);
}
public void paint (Graphics canvas) {
canvas.setColor (Color.BLACK);
canvas.fillRect (0, 0, GRIDSIZE * 10, GRIDSIZE * 10);
canvas.setColor (Stakeout.unitColor);
canvas.fillRect (
UpperLeft (Stakeout.unitColumn),
UpperLeft (Stakeout.unitRow),
GRIDSIZE,
GRIDSIZE
);
canvas.setColor (Color.BLUE);
canvas.fillRect (UpperLeft (3), UpperLeft (3), GRIDSIZE, GRIDSIZE);
canvas.fillRect (UpperLeft (8), UpperLeft (8), GRIDSIZE, GRIDSIZE);
canvas.fillRect (UpperLeft (5), UpperLeft (5), GRIDSIZE * 2, GRIDSIZE * 2);
}
// Takes a row or column number
// Returns the upper (for row) or left (for column) coordinate for that row / column
public int UpperLeft (int gridNumber) {
return (gridNumber - 1) * GRIDSIZE;
}
}
There's another constant in our code that gets repeated a lot. It's the number 10. But 10 actually stands for 2 different values. It is both our map height and our map width. Since we might later want to use non-square maps, let's create two separate constants to establish these values and then change our code to use them. Note that we have to change our startup sequence and our keyPressed function too. Just as with our other non-constant public static variables, we have to prefix them with the class name when we refer to them outside of the class in which they are declared. Here's the whole program:
import java.io.*;
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
class StakeoutKeyAdapter extends KeyAdapter {
public void keyPressed (KeyEvent event) {
int keyId = event.getKeyCode ();
if (keyId == KeyEvent.VK_UP
&& Stakeout.unitRow > 1) {
Stakeout.unitRow = Stakeout.unitRow - 1;
}
else if (keyId == KeyEvent.VK_DOWN
&& Stakeout.unitRow < StakeoutComponent.MAPHEIGHT) {
Stakeout.unitRow = Stakeout.unitRow + 1;
}
else if (keyId == KeyEvent.VK_LEFT
&& Stakeout.unitColumn > 1) {
Stakeout.unitColumn = Stakeout.unitColumn - 1;
}
else if (keyId == KeyEvent.VK_RIGHT
&& Stakeout.unitColumn < StakeoutComponent.MAPWIDTH) {
Stakeout.unitColumn = Stakeout.unitColumn + 1;
}
Stakeout.component.repaint ();
}
}
class StakeoutWindowAdapter extends WindowAdapter {
public void windowClosing (WindowEvent e) {
System.out.println ("Game over!");
System.exit (0);
}
}
class StakeoutComponent extends Component {
public static final int GRIDSIZE = 30;
public static final int MAPWIDTH = 10;
public static final int MAPHEIGHT = 10;
public void StakeoutComponent () {
setSize (GRIDSIZE * MAPWIDTH, GRIDSIZE * MAPHEIGHT);
setVisible (true);
}
public void paint (Graphics canvas) {
canvas.setColor (Color.BLACK);
canvas.fillRect (0, 0, GRIDSIZE * MAPWIDTH, GRIDSIZE * MAPHEIGHT);
canvas.setColor (Stakeout.unitColor);
canvas.fillRect (
UpperLeft (Stakeout.unitColumn),
UpperLeft (Stakeout.unitRow),
GRIDSIZE,
GRIDSIZE
);
canvas.setColor (Color.BLUE);
canvas.fillRect (UpperLeft (3), UpperLeft (3), GRIDSIZE, GRIDSIZE);
canvas.fillRect (UpperLeft (8), UpperLeft (8), GRIDSIZE, GRIDSIZE);
canvas.fillRect (UpperLeft (5), UpperLeft (5), GRIDSIZE * 2, GRIDSIZE * 2);
}
// Takes a row or column number
// Returns the upper (for row) or left (for column) coordinate for that row / column
public int UpperLeft (int gridNumber) {
return (gridNumber - 1) * GRIDSIZE;
}
}
public class Stakeout {
public static int unitRow = 0;
public static int unitColumn = 0;
public static Color unitColor = Color.BLACK;
public static StakeoutComponent component;
public static void main (String args[]) throws IOException {
System.out.println ("Welcome to Stakeout!");
// Prompt for the unit color
boolean invalidColor = true;
while (invalidColor) {
System.out.println ("What color do you want your unit to be? [red or green]");
String input = CmdInput.GetInput ();
if (input.contentEquals ("red")) {
System.out.println ("You have chosen to be the Red Rebels!");
unitColor = Color.RED;
invalidColor = false;
}
else if (input.contentEquals ("green")) {
System.out.println ("You have chosen to be the Green Goblins!");
unitColor = Color.GREEN;
invalidColor = false;
}
else {
System.out.println ("You entered an invalid color!");
}
}
// Prompt for the initial unit row
boolean invalidRow = true;
while (invalidRow) {
System.out.println ("What row do you want to start on? [1-" + StakeoutComponent.MAPHEIGHT + "]");
String input = CmdInput.GetInput ();
int startRow = Integer.valueOf (input).intValue ();
if (startRow >= 1
&& startRow <= StakeoutComponent.MAPHEIGHT) {
System.out.println ("Starting row is " + startRow);
unitRow = startRow;
invalidRow = false;
}
else {
System.out.println ("You entered an invalid row!");
}
}
// Prompt for the initial unit column
boolean invalidColumn = true;
while (invalidColumn) {
System.out.println ("What column do you want to start on? [1-" + StakeoutComponent.MAPWIDTH + "]");
String input = CmdInput.GetInput ();
int startColumn = Integer.valueOf (input).intValue ();
if (startColumn >= 1
&& startColumn <= StakeoutComponent.MAPWIDTH) {
System.out.println ("Starting column is " + startColumn);
unitColumn = startColumn;
invalidColumn = false;
}
else {
System.out.println ("You entered an invalid column!");
}
}
// Setup the window
System.out.println ("Creating the Stakeout window...");
JFrame window = new JFrame ();
StakeoutWindowAdapter windowAdapter = new StakeoutWindowAdapter ();
window.addWindowListener (windowAdapter);
StakeoutKeyAdapter keyAdapter = new StakeoutKeyAdapter ();
window.addKeyListener (keyAdapter);
window.setTitle ("Stakeout Window Title");
window.setSize (340, 370);
component = new StakeoutComponent ();
window.add (component);
window.setVisible (true);
}
}
Obstacles the hard way
Feedback
Knights can't go in water (they sink with all that heavy armor). While it might provide for interesting game play to allow the users to drown their own units, it's probably best if we just don't allow them to do that. A naive implementation would be to do it in the same way that we prevented the user from going off of the screen. Lets see what that would look like. The following code would handle the lake at (3,3).
public void keyPressed (KeyEvent event) {
int keyId = event.getKeyCode ();
if (keyId == KeyEvent.VK_UP
&& Stakeout.unitRow > 1
&& ! (Stakeout.unitRow - 1 == 3
&& Stakeout.unitColumn == 3)) {
Stakeout.unitRow = Stakeout.unitRow - 1;
}
else if (keyId == KeyEvent.VK_DOWN
&& Stakeout.unitRow < StakeoutComponent.MAPHEIGHT
&& ! (Stakeout.unitRow + 1 == 3
&& Stakeout.unitColumn == 3)) {
Stakeout.unitRow = Stakeout.unitRow + 1;
}
else if (keyId == KeyEvent.VK_LEFT
&& Stakeout.unitColumn > 1
&& ! (Stakeout.unitColumn - 1 == 3
&& Stakeout.unitRow == 3)) {
Stakeout.unitColumn = Stakeout.unitColumn - 1;
}
else if (keyId == KeyEvent.VK_RIGHT
&& Stakeout.unitColumn < StakeoutComponent.MAPWIDTH
&& ! (Stakeout.unitColumn + 1 == 3
&& Stakeout.unitRow == 3)) {
Stakeout.unitColumn = Stakeout.unitColumn + 1;
}
Stakeout.component.repaint ();
}
That's quite a complex conditional. We're calculating the new position (based on the direction the unit is being moved) and them we're ensuring that the new position is not (3,3). All of this extra code is necessary and it only handles one of our lakes. We would have to add another four lines for every other lake that we had on our map. What's more, it is impossible for this code to be dynamic. In the prototype the user can create his own map. That map is used as input into the program and it dynamically disallows the user from entering the water squares. This dynamism is impossible with this implementation. As the title of this section implies, there is an easier way, but for that we have to learn something new...
Instead of coding our map directly into the program we're going to establish some data to represent the map and then we'll use that data to both paint the map and determine where the user can and cannot move. The way we will represent this map is with a 2-dimensional array. An array is basically a grid that can hold any type of object. Since our map is also a grid, it is the natural container to hold our data. Each sqaure in our array will correspond to the square on our map with the same coordinates. The data it will hold is a single integer indicating whether it is a land square or a water square. We will use constant ints to represent these values. We'll place this array in our StakeoutComponent class since that's where all our other map stuff is.
class StakeoutComponent extends Component {
public static final int GRIDSIZE = 30;
public static final int MAPWIDTH = 10;
public static final int MAPHEIGHT = 10;
public static final int LAND = 0;
public static final int WATER = 1;
public int [] [] map = {
{LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND },
{LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND },
{LAND, LAND, WATER, LAND, LAND, LAND, LAND, LAND, LAND, LAND },
{LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND },
{LAND, LAND, LAND, LAND, WATER, WATER, LAND, LAND, LAND, LAND },
{LAND, LAND, LAND, LAND, WATER, WATER, LAND, LAND, LAND, LAND },
{LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND },
{LAND, LAND, LAND, LAND, LAND, LAND, LAND, WATER, LAND, LAND },
{LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND },
{LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND, LAND },
};
...
}
Just like other variable declarations we declare the type of the variable, but in this case we're declaring the type of variable that will be contained in the grid. You use the square brackets to indicate that this new variable is an array and since we are creating a two-dimensional array, we use two square brackets. This is followed the the name for this new array ("map" in this case). Next we assign an initial value to the array. Initial values for arrays are done using braces. The outmost braces are there for the first dimension and the inner braces are for the 2nd dimension. Within the 2nd dimension we fill in the various entries with the values for those squares separated by commas. Later on, to refer to one of the entries in the array we simply place the coordinates in the brackets to refer to that value.
int terrainType = map [1][1];
Translating coordinates
The coordinates in our array don't exactly match up with our map. Arrays are always zero-base. That means that the first entry has index 0. So actually to access the value at coordinate (1,1) we'd have to use "map [0][0]" and to access the value at coordinate (3,4) we'd have to user "map [2][3]". So whenever we use our map array we have to make sure we subtract one from the coordinates.
Painting the map array
Here we will see another reason why loops are very useful. We can use a loop to iterate over the values in a container like our map array. What we want to do when we paint the map is to go through each value in our array and paint each one in its correct position. Here's how we do it.
public void paint (Graphics canvas) {
canvas.setColor (Color.BLACK);
canvas.fillRect (0, 0, GRIDSIZE * MAPWIDTH, GRIDSIZE * MAPHEIGHT);
int rowIndex = 0;
while (rowIndex < map.length) {
int columnIndex = 0;
while (columnIndex < map [rowIndex].length) {
if (map [rowIndex][columnIndex] == LAND) {
canvas.setColor (Color.BLACK);
}
else if (map [rowIndex][columnIndex] == WATER) {
canvas.setColor (Color.BLUE);
}
else {
canvas.setColor (Color.PINK);
assert (false);
}
canvas.fillRect (UpperLeft (columnIndex + 1), UpperLeft (rowIndex + 1), GRIDSIZE, GRIDSIZE);
columnIndex++;
}
rowIndex++;
}
// Paint the unit
canvas.setColor (Stakeout.unitColor);
canvas.fillRect (UpperLeft (Stakeout.unitColumn), UpperLeft (Stakeout.unitRow), GRIDSIZE, GRIDSIZE);
}
It turns out it's a quite a bit more complex than what we had before, but the advantage is that it can handle any number of lakes, and doesn't get any bigger whereas with our hard-coded painting, we'd have to add a line for each lake that we paint on the screen. Besides that this is much more maintainable. All we have to do is change the map array and our paint code instantly works. No changes are necessary.
Remember to increment the index
For loops to take care of the index for us
Changing UpperLeft to take the 0-based offset
assertions
moving the unit painting after painting the map
Storing the coordinates internally as zero-based > changing startup sequence
Limiting movement with the map array
We'd also like to use this information to determine where a unit can and cannot move to a certain location.
public void keyPressed (KeyEvent event) {
int keyId = event.getKeyCode ();
int newRow = Stakeout.unitRow;
int newColumn = Stakeout.unitColumn;
if (keyId == KeyEvent.VK_UP) {
newRow = newRow - 1;
}
else if (keyId == KeyEvent.VK_DOWN) {
newRow = newRow + 1;
}
else if (keyId == KeyEvent.VK_LEFT) {
newColumn = newColumn - 1;
}
else if (keyId == KeyEvent.VK_RIGHT) {
newColumn = newColumn + 1;
}
if (newRow >= 1 && newRow <= StakeoutComponent.MAPHEIGHT
&& newColumn >= 1 && newColumn <= StakeoutComponent.MAPWIDTH
&& Stakeout.component.map [newRow - 1][newColumn - 1] == StakeoutComponent.LAND) {
Stakeout.unitRow = newRow;
Stakeout.unitColumn = newColumn;
Stakeout.component.repaint ();
}
}
Note that this is quite a bit simpler and handles all the lakes, not just the first one.
|
|
|