Java Game Programming Part III: The Power of Arrays
If you have programmed in other languages or have worked with Java you have probably seen arrays in action. For those of you who are unfamiliar with arrays and how they work I will briefly outline them here.
When we are programming we use single variables which can be viewed as a lone box in memory. A typical problem that we will come across when programming is the need to store the information for an 8x8 board. By following the single variable method we would be required to declare 64 variables and wouldn't be allowed to use for loops for our initialization or when we wanted to traverse the whole board. This hardly seems like an efficient way to program and would not only result in large programs, but also in frustrated programmers.
Eg. Single variable method of storing an 8x8 board
int board64;[/code][/indent] In order to make our lives easier and to make our code at least semi-readable by others we are fortunate that there is a better way. An array is a collection of variables that can be referred to by one name and accessed with an index. For example, if we wanted to access the second element in the second row of the board we would type a line of code that contained [i]board[/i]. You will notice that the indexes are one less than the number that I was looking for originally. The reason for this is that in an 8x8 array the indexes go from 0-7 for both dimensions. By using arrays we can now use for loops to traverse through the index values of the array and initialize or make changes to all of the data in the array.
Eg. Array method of storing an 8x8 board
[indent][code]int board = new int;[/code][/indent] Now doesn't that look a lot nicer than the 64 lines that would have been required with the other method? You will notice that in my example above I used a two dimensional array. One, two, three, and maybe even four dimensional arrays are the ones that you will generally see, but for the most part you will only use one and two dimensional arrays.
You will notice that when we create our arrays we must use memory allocation to set up the dimensions that we would like. This means that with the name of the variable we include an empty set of square brackets, , for every dimension that we want the array to have. We then have the choice of allocating the memory for the array at the time we declare the variable or we can defer this until later on in our program when we are going to use it. Whenever we decide to allocate the memory we set the variable equal to the keyword [i]new[/i], followed by the type of the data which will be found in the array, and then sets of square brackets for every dimension of the array. This time, however, the sets of square brackets instead of being empty will contain the number of "squares" in each dimension.
This may seem a little unfamiliar at first for those of you who have never seen arrays before, but once we start to use arrays in the games later on in this article you will quickly pick up on how they work.
Now that we have taken a look at how to work with arrays in Java it is important to take a look at a useful data structure that is also available to us, the Vector. The Vector is fancy array that Java allows us to use. This fancy array dynamically alters its size to fit the data that we store in it and also makes access to the elements within extremely easy. This means that we can keep track of a number of elements without knowing in advance how many there will be.
Useful Vector methods are:
[list][*]addElement()[*]elementAt()[*]firstElement()[*]lastElement()[*]isEmpty()[*]size()[/list] To initially create a Vector we declare our variable as normal and then set that variable equal to a new Vector.
[indent][code]Vector sample = new Vector(0,1);[/code][/indent] The first parameter in the constructor for the new Vector is the initial capacity that we want it to have. The second parameter is the number of new elements that will be added to our Vector when we run out of elements that we can store information in. If you think that you will be adding a large number of elements to your Vector you may wish to have larger values for the initial capacity and the capacity increase. A problem that we have to watch out for here is that we don't end up wasting space (ie. having empty elements sitting around).
When new elements are added via the addElement method they are added to the end of the Vector.
[indent][code]int number = 5;
sample.addElement(number);[/code][/indent] In order to get elements out of the Vector we use the elementAt method and pass to it the index of the element that we wish to access. Another useful method that can be used in conjunction with the elementAt method is the size method. The size method allows us to find out the number of elements that we have in our Vector and the value passed back from this method could be used as an upper limit in a for loop.
[indent][code]int number2 = (int)sample.elementAt(1);[/code][/indent] The methods firstElement and lastElement are similar to elementAt, but they return either the first or last element and are not passed an index value.
While you may not immediately find a use for the Vector in your games it is a powerful tool that is available to you to use. I am sure that you will find a point where a Vector will fill an important position in one of your games and make your life easier.
When you hear programmers talking about Java one of the phrases that comes up frequently is [i]object-oriented programming[/i]. It seems that everyone who is anyone is on the OOP bandwagon these days so we should take a look and see if there is anything we can get from this.
Now you may think that working with classes is going to be complicated and it can be, but we have actually already been working with classes. That's right! If you look back at our Java applets they have been located inside a big class and we have also used classes created by others to make our lives easier. You can actually get away with only ever using this one class and never creating any additional ones, but if you want to make a game of any depth or with any complexity you should learn to love classes or at least realize that they are worth the effort it takes to learn them.
Classes consist of two main parts: member variables and methods. Member variables are variables which can be accessed by all of the methods within the class and possibly by methods outside the class. The member methods are basically the same as member variables in that they can be accessed by all of the methods within the class and possibly by methods outside the class. Now you are probably asking why is it possible that the methods and variables might not be accessible from outside the class. The answer to this question is that methods inside a class can either be private or public. If they are public then any method outside the class can access/call it, but if they are private then only methods inside the class can access/call it.
Now that we have a basic idea of what classes are why should we bother using them? The reason we want to use classes is that they help us to organize our code, make our code more intuitive, and enable us to make complex programs with greater ease. An example of a situation in which a class would be useful is in an arcade shoot'em-up with spaceships. What is the difference from one spaceship to the next? The location and what the ships happen to be doing are different from one to ship to the next. Is the type of information that we keep track of for the spaceships different from one to the next? Can one spaceship do anything different from the next spaceship? The answer to both of these questions is generally no. By creating a class for a spaceship we can create a template of information and actions that can be called upon for all of our ships without having to type out a bunch of code for each ship.
Eg. SpaceShip class
private int amount_of_ammo;
private float x_coordinate;
private float y_coordinate;
private float z_coordinate;
public void MoveShip();
public void Shoot();
};[/code][/indent] Now this is only a sample class so I haven't included any code for the methods in the class, but this gives you an idea of the setup that we would need. Once I have this class laid out I could then create an array of ships that I could access and manipulate.
Eg. Creating an array of SpaceShips
[indent][code]SpaceShip aliens = new SpaceShip;[/code][/indent] This is all that we need to worry about for the time being in terms of classes. There are even more modifiers available for you to use so if you are interested I would encourage you to read more about this subject. I hope that one point you will take from this section is that classes can be used effectively in your code to make your life easier and to make your code neater and more organized.
[size="5"][b]Files in Java Applets[/b][/size]
Before we get into discussing the two games we will make we need to first look at how to read in information from files. Due to security issues and considerations with applets we have to go about our file input in a slightly different manner.
The first step we need to take is to open up a connection to our file which will be located on our computer, not the user's computer. In order to set up this connection we must create a variable of type URL. URL is in the java.net package so at the beginning of our program we need to have an import statement for this.
URL url;[/code][/indent] When we want to open up the connection to the file we create a new URL object and pass the parameters getCodeBase() and the name of the file we want to open. The name of the file can either be a constant string or a variable. In order to get our program to compile we need to make sure that we embed the URL creation in a try...catch statement. The exception that we need to catch is MalformedURLException.
url = new URL(getCodeBase(), "test.txt");
catch (final MalformedURLException e)
}[/code][/indent] Once we have created our connection to our file we need to then open it up so that we get input from it. The first step that we need to take here is to create a variable to represent our input stream which will be of type InputStream. In order to use the class InputStream we need to make sure that we import everything in the java.io package at the top of our program.
InputStream stream;[/code][/indent] The next step that we need to take is to take our connection to the file and open it up for input. In order to do that we take the variable that represents our connection and call the method openStream() to get a stream that we can get input from. We then store this stream in our variable of type InputStream. Just as before when we were dealing with creating a URL connection we need to make sure that we use a try...catch statement to catch the IOException error.
stream = url.openStream();
catch (final IOException e)
}[/code][/indent] Now that we have our input stream we need a nice way to be able to read in the input from this file. In order to do this we are going to create a variable of type StreamTokenizer. The class StreamTokenizer has some nice features that allow us to specify characters that will signify that comments are to follow; characters that are whitespace; characters which are parts of words or units that we will be reading out, and it also provides a nice means of grabbing tokens from the file.
[indent][code]StreamTokenizer tokenizer;[/code][/indent] In order to create our stream tokenizer we need to create a new StreamTokenizer object to which we will pass our input stream variable that we set up previously. Again this piece of code needs to be inside a try...catch statement that will catch the exception IOException.
tokenizer = new StreamTokenizer(stream);
catch (final IOException e)
}[/code][/indent] Now that we have our StreamTokenizer set up to perform input from our file we need to make sure that we set a couple of options so that our tokens will be read out properly. The first option that we need to set is the range of characters that will make up all of the words that we will be reading in from the file. The first parameter to the wordChars method that we will use for this task is the start of the character range and the second parameter is the end of the character range. These same parameters are used for the whitespaceChars method. This method stores the range of characters that will be considered to be whitespace in our input file.
tokenizer.whitespaceChars(' ',' ');[/code][/indent] In order to get the next token from the file we need to use the nextToken method. The nextToken method returns an integer that represents either the end of file, or tells us that the token is a number/string. As you will see in the sample code below there are constants provided that we can use for these (TT_EOF, TT_NUMBER, TT_WORD).
if ((token = tokenizer.nextToken()) != tokenizer.TT_EOF)
message = "Number: " + tokenizer.nval;
message = "Word: " + tokenizer.sval;
}[/code][/indent] Armed with this ability we now have the capability to store board configurations or other information in text files that our games can read in. This will be important in the next couple of sections when we discuss how to make PacMan and Tetris.
[size="5"][b]Arrays in Games[/b][/size]
Now you might be saying to yourself that this is all fine and good, but are arrays really that useful when I'm making a game. Well the answer is yes. There are several games that can be built with arrays as the fundamental element and we will cover some of them here. An important skill to learn is taking the new concepts that you learn with any programming language and then thinking about how you could use them when doing game programming or just doing programming in general. By taking the concepts and looking at them from all angles you end up building yourself a toolkit of options and skills that can be brought into action on command.
I think that most people have played Pacman or a game similar to it. The goal of the game is to collect as many coins as possible without being killed by the evil ghosts that come around and try to kill you. You will notice in my version of the game on my website I used evil pacmen rather than ghosts.
The first part of our PacMan game that we want to look at is how to setup our array and how to draw what the array represents on the screen. The squares on our board will consist of either black (not a wall) or blue (wall) squares. On top of those squares that are not a wall there is the possibility for a coin or one of our pacmen to be there. So we will have one of 4 possibilities:
[list][*]square is a wall[*]square is not a wall and has nothing on it[*]square is not a wall and has a coin on it[*]square is not a wall and has a pacman on it[/list] Now you might be saying, "Isn't it possible for a pacman and a coin to be on the same square?" This will happen, but for drawing purposes we would only draw the pacman because he will have eaten the coin. To store the information for our board we will want to have a two dimensional array of characters. We will then store one of five characters in each element of this array: 'e' for enemy pacman, 'p' for our pacman, 'c' for coin, 'n' for a wall, and 'b' for nothing. In the init portion of our applet we will need to initialize our array with these character values to setup our board. We have two options when considering how to go about initializing our board: read the board characters in from a file or hardcode the values in the source code. Either of these options is perfectly fine, but you will find that by reading the board in from a file you make it easier to rotate through a variety of maps; your code will be easier to read; and it will be easier to edit your boards. Now that we have our board initialized the next step is to figure out how we can draw it. In order to draw the base portion of our board (wall or no wall) we need the following chunk of code.
case 'n': offscr.setColor(Color.blue);
offscr.fillRect(boardx+count2*blocksize,boardy+count1*blocksize,blocksize, blocksize);[/code][/indent] The code block above is going to be located inside a double for loop with the first for loop using count1 and the second one using count2. The switch statement should be fairly understandable. The goal of the switch is to pick the color we will draw for the square (blue for the wall; black for nothing). The switch statement uses the character in our two dimensional array as a basis for its decision. After we have picked the color all that is left to do is draw the rectangle that represents the element of the array that we are currently on. In order to draw this rectangle we use the following variables:
[list][*]boardx holds the offset from the edge of the applet area to the edge of the board on the x-axis[*]boardy holds the offset from the edge of the applet area to the edge of the board on the y-axis[*]count1 is the counter for the first for loop[*]count2 is the counter for the second for loop[*]blocksize is the size of the rectangle (square) that I want to draw[/list] The result of this code by itself would be to draw the board that we have created without displaying the coins or pacmen.
In order to display the coins all we have to do is draw a filled oval at the appropriate spot on our board if the square has the letter c in it. The last part left, as far as the display is concerned, is how we will draw the pacmen. This part is really up to you and your creativity but what I decided to do is use a filled arc. By using a filled arc and a little timer I was able to set up my little pacmen with mouths that open and close. You will notice in my code, however, that I have put the drawing code for my pacmen inside a class that is used for my pacman and the enemy pacmen. The reason for this is that it makes my code a lot neater and also gives me the ability to easily change the number of enemy pacmen that I have.
The next part that we need to discuss is how to represent the data and actions that are associated with the pacmen in the game.
The actions that a pacman can perform are: move, draw, and change direction. The first two are fairly obvious in what they do, but the use of the third one might not be immediately apparent. The reason a pacmen has to be able to change directions is that I want the mouth to be pointed in the direction that he is going. If you were to leave this ability out the game will still be okay, but it will be visually lacking. When you are making games like this you have to remember that the user will notice little details like this and if it is at all possible you should take care of them. The direction change will happen when the user presses one of the arrow keys on the keyboard so we will need to use what we learned last time about the keyDown method. The drawing of the pacman will be fairly simple and will involve drawing a filled arc that may be a circle if the mouth is closed or have a gap if the mouth is open. Out of the three actions that a pacman will perform the ability to move is the most complex in terms of all the options that must be considered. The options that we must keep in mind are: will he hit a wall ,another pacman, a coin, or nothing at all if he moves to the next square based on his current direction.
The data that we need to keep track of for a pacman are: whether the mouth is open; how long it takes for the mouth to open or close, the size of the mouth angle; the direction he is facing; whether he is controlled by the user or the computer; the current speed and maximum speed; the x and y location on the board; the color that he is drawn in; and a counter to keep track of how long it has been since he opened or closed his mouth. You may wish to add or remove items from this list of data that we are keeping track of, but I think that you will find that this is a good place to start.
After we have the pacmen set up, all that is left is to increase the score when a player goes over a coin and deal with what happens when the player dies. If you wanted you could also add in an introduction screen and even multiple levels for the user to play on.
This covers the basic topics that you will need to know when sitting down to program PacMan for yourself. You can also look at my web page ([url="http://www.cpsc.ucalgary.ca/%7Ekinga"]www.cpsc.ucalgary.ca/~kinga[/url]) to take a look at my solution.
Tetris, for those of you who have not played it before, is a game in which pieces of different shapes fall from the top of the screen and land on the bottom of the board area or on top of other pieces. When an entire row of the board is filled in with parts of pieces the row disappears, the rows above it shift down, and the player gets points. As the user gets more and more rows disappearing the speed at which the pieces fall is increased. If the user should ever fill the board to the very top without being able to eliminate any rows then they lose.
The first part of the tetris game that we will look at is the board. The representation and setup of the board will be almost identical to that of the PacMan board. The following are the various possibilities for a square on our board:
[list][*]empty square[*]square contains part of a piece[/list] For my version of the game that I created I had 5 different piece types that could fall down so I had the following characters that I looked for: y for yellow, r for red, b for blue, g for green, p for magenta, and n for black. To take this information stored in my two dimensional array and use it to draw the board on the screen I need to use the same style of switch statement that I showed you for pacman with the following as the case statements:
case 'y': offscr.setColor(Color.yellow); break; case 'r': offscr.setColor(Color.red); break; case 'b': offscr.setColor(Color.blue); break; case 'g': offscr.setColor(Color.green); break; case 'p': offscr.setColor(Color.magenta); break; case 'n': offscr.setColor(Color.black); break; I also end up using the same line of code to draw these rectangles (squares) on the screen.
The biggest part of making your Tetris game is creating the shapes that will fall and enabling them to rotate and collide properly. For the version that I have made I used the following shapes:
After you figure out how the basic shapes work you can experiment with your own shapes and variations.
The first consideration that we must look at is how do we pick the type of piece that will fall. I recommend setting up some random number like we discussed in part I and assigning a range of values to each of the pieces that you have. Once you have decided on the piece type that you want to have you can then place it on the board. You will notice that in my code I have stored the four board array locations of my piece in an array so that I can keep track of my piece locations with ease. Every time the piece moves we need to clear its previous location on the board and then put it in its new location on the board.
Once we have a piece moving down the board we need to allow the user to move and rotate it. The first piece of this puzzle is to watch for keyboard events. You will need to allow the user to rotate his piece as well as move it left and right. Moving a piece left or right isn't hard, but whenever you are moving a piece (left, right, down, rotating) you will need to make sure that you check to see if the new position is legal. An illegal position would be one where the piece overlaps another piece or moves off the board. If ,when we try to move our piece down, we discover that it is illegal then we know that this piece cannot be moved any lower and we can stop moving it and start the next piece. The hardest part of dealing with the movement of pieces is allowing them to rotate. You will notice that in my code for the rotation I break it down into the cases for each piece. Obviously we don't need to worry about rotating the square since its appearance never changes no matter how much we rotate it, but we need to look at all the other pieces.
We will take a look at the rotation for one piece and then you can look at my code for how the others will work.
When dealing with the rotation of the piece it is important to just look at the relative position change. In the transition from the first diagram to the second you will notice that the y-value of block number four decreases by one and the x-value decreases by one. In the transition from diagram two to three the y-value of block four again decreases by one, but the x-value increases by one. In order to code the rotations in for all the pieces I recommend that you draw out the transition diagram for every piece and go step by step through your code accounting for every possibility. Since we are dealing with our Tetris game in terms of arrays rather than just screen positions this rotation process is more complicated, but this representation makes a lot of other parts of the game (row elimination and collisions) much easier. As with the regular piece movement you need to make sure that before you let a piece be rotated you first check to see whether the new position is legal or not. In fact you will end up performing this task so often I would recommend that you set up a method that can perform this task (in my code this method is called colorCheck().
After we have moved the current piece as far down as it can go we must check the current state of the board and see if there are any rows that are completely full and if so then we must eliminate them. You will notice that in my example solution on my web page I start at the bottom of my board and work my way up. For every row that you eliminate you will give the user additional points based on the difficulty level and the number of rows they were able to eliminate at one time.
You now should have a basic understanding of what is involved in creating your own Tetris game. If you have any questions about this you can check out my version on my web page ([url="http://www.cpsc.ucalgary.ca/%7Ekinga"]www.cpsc.ucalgary.ca/~kinga[/url]) or you can email me ([email="firstname.lastname@example.org"]email@example.com[/email]).
[size="3"][b]Other Potential Array-Based Games[/b][/size]
In addition to the two games that we have looked at here in detail there are many other games that could be created by using an array base. A couple examples of other games are Nibbles and Snafu.
Nibbles is a popular game that a lot of you have probably seen coded in Basic. The game consists of two or more "snakes" moving around the board area trying to beat the other snakes to the power-ups that appear randomly on the board. The more power-ups you eat the longer your snake gets and the more difficult it is to maneuver around. This game can be expanded to allow for a variety of maps to be used and to allow for different power-ups that could shrink your "snake" or reverse the direction in which all of the "snakes" are moving.
Snafu is a game that some of you may remember from the old Commodore days. The game consists of two "snakes" moving around a map like nibbles, but the snakes leave a trail behind so the number of valid squares that the "snakes" can move to is continuously decreasing. The first "snake" to hit itself, the other "snake", or the wall loses.
You can even create maze style games or variations on PacMan and Tetris. The main idea to remember when you are deciding what game you would like to create is to be creative.
Now that we are comfortable with arrays we can move on to discussing AWT, artificial intelligence topics, and how to pass parameters to our java applets. These topics will be discussed in the next part of this series as we move away from the basics into the more advanced topics. If have any questions or comments feel free to email me at [email="firstname.lastname@example.org"]email@example.com[/email]. All of the sample programs related to what I have discussed here are located on my web page ([url="http://www.cpsc.ucalgary.ca/%7Ekinga"]www.cpsc.ucalgary.ca/~kinga[/url]). Thanks again for reading this series and sending me numerous emails with your comments and questions.