In the first part of this series we looked at how to create a basic applet, display text and shapes, and get random numbers. In this much anticipated second part (sorry for the delay and thanks for all the emails) I will cover how to use threads and double buffering as well as how to get input from the mouse and keyboard to create a simple air hockey game.
The Game
Most of you are probably familiar with the popular game of Pong. For those of you who aren't, the game consists of two paddles and a puck which is hit back and forth until someone misses it. A miss by a player equals a point for their opponent and after a predetermined number of points one player will win. The one change that we will make is that we will make our game into an air hockey game which I'm sure many of you are familiar with. The only real difference between our game and a game of Pong will be that our air hockey game could have smaller goal areas, but we will discuss this in more detail later on. Now, this may seem rather simple, but it can be an extremely fun little game to play and is an excellent place for us to start our journey into the world of java games.
Before we get into the topics of this article we should first examine what we will need for the game so we know what we will need to focus on. There are four important phases that we will need to look for in most games: input, processing, output, and loop.
The input phase consists of getting the intended actions from the players by looking at the states of the keyboard and mouse. We can then interpret the states of these input devices and have the game respond appropriately to them. For our game of air hockey the main action that the user is going to want to perform is moving their paddle up and down. While it is not found in standard pong like games we may want to also allow for the paddle to moved left and right. It is not immediately apparent that we may want to use the mouse, but we will find a possible use for it later on.
The processing phase consists of moving the paddles based on the input, moving the puck, and checking for collisions. The types of collisions that we are looking at are puck to paddle, puck to goal area, and paddle to edge of board. If we allow for the user to move the paddles left and right we also have to check for paddle to paddle collisions. Another item that we should keep track of in the processing phase is the current state of the game. There are three main states that we will probably want to have for our game: puck moving, puck set to go after goal or at beginning of game, and game over.
The third phase is to display the appropriate output on the screen. The means that we are going to have to display the two paddles, the puck, and the scores for each player. We might also want to display some additional information, but for now we will stick with the basics.
The fourth and final phase is what I loop. The purpose of loop is to perform the previous three phases over and over. How much fun would our air hockey game be if we only accepted input once, only calculated puck and paddle movement once, and only drew the paddles and puck once? The answer of course is not very much fun at all. In order to ensure that the input, processing, and output occurs again and again until the game is over we will end up using threads which I will talk about in a moment.
Before we get our hands dirty with some coding I want to say that it is important to roughly sketch out what you want to do in your game before you start to code. It is natural for programmers to want to jump right in and code , but it is important to at least spend a little time going over what you want to do and how you are going to go about doing it. This time spent planning will ensure that you have a clear and focused goal, that your idea is feasible, and that you go about your programming in an organized manner.
Threads
As I mentioned before in our initial discussion about the game we need to have a loop capability that will allow us repeat the input, processing, and output phases over and over again. In order to do this in our java applet we are going to use a concept called threads. By creating a thread and starting that thread we gain access to a new method called run. Inside this new method we will create a loop that will enable our game to run properly (ie. allowing for a repeat of input, processing, and output).
Let's now work through a simple example applet in order to get familiar with the basics of threaded applets. This applet should be in a file called SampleThread.java. The first lines to appear in any of our applets are the imports. In this particular example because we aren't doing anything fancy all we need is the following import.
import java.applet.*;
This allows us to create our basic applet and use the methods necessary to get an applet up and running. The next line in our code is where we actually create our class that represents our java applet. What name do we give to our class? If you said SampleThread then you are absolutely right. The reason we name our class SampleThread is that we have named our code file SampleThread.java and the name of the class must always match up with that filename.
public class SampleThread extends Applet implements Runnable
{
You will notice that after we stated that the class would extend the Applet class we add on the fact that the class will implement Runnable. By implementing Runnable we allow our applet to be threaded and gain access to the method run which we will talk about in a minute. Next we must declare a couple of variables which will allow our thread demonstration to produce something to show what is happening.
Thread t;
int i;
The first variable is going to be our thread that will be created in our applet. The second variable is simply an integer counter that is used to demonstrate that we now have the capability to loop which was the fourth phase that I discussed before.
Now it is time to write the code for the methods that our class will contain. In the past we have used two main methods: init and paint. We will be using these again, but this time we will also be including a new method, run, which will become a key portion of our applets in the future.
public void init()
{
t = new Thread(this);
t.start();
i = 0;
}
The init method contains a couple of lines that get our thread up and running. The first line is responsible for creating a new thread for the applet. The reason we type this inside the brackets is that when our applet is actually running we have an instance of our SampleThread class. The init method is then run for this instance and the thread is created for this instance. It is possible to have multiple instances of a class so each instance will have a thread setup for that particular instance. If that still isn't 100% clear don't worry about it for the time being and hopefully with a little time it will become clearer. After we create our new thread we need to make sure that we start the thread so that our run method, which we will discuss next, is called. I also initialize my counter, i, to zero so that I know the exact value that it begins with. I know that you will find many programmers who just assume that their variables are going to be initialized to zero if they are integers, but this is a dangerous assumption. I have on occasion forgotten to initialize a variable to zero when programming and lucky for me my computer has been nice enough to fix this by doing it for me. One time after programming for a couple of days at home and having no problems with the code or how the program was running I took the program to school. When running the program at school I had no idea why a particular part of the program that ran perfectly at home had a small glitch in it when I ran it at school. After a lot of debugging I finally stumbled across a variable that I was assuming was initialized to zero. It turns out that while my computer at home was nice enough to initialize variables to zero for me the computer at school thought it would be nice to initialize my integer variables to some number other than zero. The moral of this story is don't assume anything and always make sure that you initialize your variables before you use them.
The next method that we need to include in our program is the run method. The run method is called when we create a thread in our applet. Inside the run the method we want to loop until the applet is done or the user leaves. For now in order to ensure that we stay in the run method until the user leaves our page we will use a while loop with the keyword true used as the condition. By using the keyword true as the condition we create an infinite loop that will go until either we explicitly break out of it or the user leaves the page containing the applet. It is important not to be scared of infinite loops because they can be very useful to us if we use them properly.
public void run()
{
while(true)
{
i++;
repaint();
try {
t.sleep(1000/30);
} catch (InterruptedException e) { ; }
}
}
You will notice that inside the while loop I always increment the variable i and call the repaint method. The repaint method calls my paint method and if I didn't call repaint in the while loop any changes that were made to the game (ie. puck moving) would not show up on the screen. The reason I have the variable i is so that each time paint is called the variable i contains a new value which will be displayed on the screen in place of the old one to show that we are looping and that we now know how to implement the fourth phase of our four important phases.
The last method that we need to implement is the paint method. The paint method is where we take care of the output that we would like to have displayed on the screen. For this applet the paint method is very simple because the only thing that it does is print a string out to the screen which shows the value of i and makes it look marginally pretty by concatenating the string i = to the front of it.
public void paint(Graphics g)
{
g.drawString("i = "+i,10,20);
}
}
If you compile and run this applet you will see that the applet displays the ever increasing value of i on the screen. While this may not seem very exciting it is a fundamental concept that will be critical to getting our air hockey game up and running.
Keyboard
The next topic we need to look at is how to get the users input from the keyboard. It is actually very easy to check out input from the keyboard because java provides us with two new methods: keyDown and keyUp.
One important point that I should mention before we continue is that before an applet will receive keyboard events (ie. call the keyDown or keyUp methods) the user will need to click on the applet area in order to get the focus. The applet only receives keyboard events when it has the focus.
The following is the syntax for the keyDown method. The keyUp method is identical except of course the method name is changed to keyUp from keyDown.
public boolean keyDown(Event e, int key)
{
message = "value = " + key;
return true;
}
You will notice that the keyDown method takes an integer parameter. This integer value contains the value that represents the key which was pressed. I would recommend that you set up a method similar to the one that I have outlined above and then in your paint method you can print out message (a string) so that you can see the values that are associated with each key.
We now have the capability of accepting input from the keyboard and I think that you will agree with me that it is fairly straight forward.
Mouse
Now that we are up to speed on how to get input from the keyboard we can turn our attention to the mouse. The mouse is just as easy as the keyboard to use, but we have more methods at our disposal when we deal with the mouse.
The following are the methods that we can use for the mouse:
public boolean mouseEnter(Event e, int x, int y)
public boolean mouseExit(Event e, int x, int y)
public boolean mouseDown(Event e, int x, int y)
public boolean mouseUp(Event e, int x, int y)
public boolean mouseMove(Event e, int x, int y)
public boolean mouseDrag(Event e, int x, int y)
One important point to note here is that for each of the above methods you can simply return true.
All of the methods for the mouse are fairly clear and understandable. You will notice that each method receives the x and y coordinates of the mouse location with respect to the upper left-hand corner of the applet window.
The mouseEnter and mouseExit methods are called automatically when the mouse enters or exits the applet area. The mouseDown and mouseUp methods are called when a mouse button is pressed or released. The mouseMove method is called when the mouse is moved in the applet area. The final method, mouseDrag, is called when the mouse is moved in the applet area while a mouse button is pressed.
The best way to test out these different mouse methods is to set up a basic applet that prints a string out to the screen. The string should be changed by the various mouse methods to reflect which method was called and the current x, y location of the mouse.
You should now have a fairly good idea about what it takes to work with input from the keyboard and mouse in java applets. It is now time to go back into the realm of output.
Double Buffering
We have been focusing on the input and looping phases of our game so far, but now it is time to get back into looking at the output phase. Double buffering is a technique that will be familiar to anyone who has done much in the way of game programming. A common problem that you will notice as soon as you make an applet with threads is that you get an annoying flicker. This flicker comes from the fact that we are drawing bits and pieces of our game when we should be drawing all of our game at once.
The trick to fixing the flicker problem is actually very simple. The first step that we want to take is to setup a couple of variables.
Image offscreenImage;
Graphics offscr;
The variable offscreenImage is going to be our backbuffer where we will draw what will be appearing next on screen. We need the variable offscr to hold the graphics context of our backbuffer so that we can later draw a variety of stuff into it just like we normally would in our paint method.
width = size().width;
height = size().height;
offscreenImage = createImage(width, height);
offscr = offscreenImage.getGraphics();
The next important step that we need to take is to setup our backbuffer so that we actually have some space to draw into and then we need to get its graphics context which we will store in offscr. You will notice that I use width and height to store the dimensions of my applet space and that I then use these values to create a backbuffer that is the same size. In order to get the graphics context of this backbuffer I simply have to call the method getGraphics in my variable offscreenImage and we are set. One other note that you need to watch out for is that these four lines of code should be placed inside the init method so that they are executed when the applet first starts up.
offscr.setColor(Color.black);
offscr.fillRect(0, 0, width, height);
offscr.setColor(Color.blue);
offscr.fillArc(0,0,width,height,startAngle,arcAngle);
g.drawImage(offscreenImage, 0, 0, this);
This next segment of code is an example of what will be in your paint method. The first step is to clear the backbuffer. We do this by simply creating a filled rectangle that has the dimensions of our applet space. Take care to note that rather than using the usual graphics context g we are instead using our backbuffer which is represented by offscr. You can clear the backbuffer to any color that you want, but I decided here that black was a good color. We can then proceed to draw any shapes or text that we would like to display while making sure that we use offscr rather than g. Once we are ready to flip our buffers and display what is in the backbuffer on the screen we then use g.drawImage to display the buffer on the screen. This way we draw everything at once. If you don't remember what the parameters of the drawImage method stand for make sure you check back to part one of this series.
public void update(Graphics g)
{
paint(g);
}
This piece of code that we need to add to our program to get double buffering working is the update method. Normally the update method clears the screen and then paints, but this will again lead to flicker so we need to make sure that all it does is paint. If you try writing a program with the above code, but leave out the update method change that I have shown you will still see flicker and all your work will be for naught so take care.
Coding the Game
So now that we have a basic idea of how to use threads, double buffering, and the mouse and keyboard we can start working on an air hockey game. I will cover what needs to be coded and how we could go about doing this here and you can look at the sample code which is available on my web site (www.cpsc.ucalgary.ca/~kinga). The code may look kind of confusing, but in the next article I will talk about how to use additional class and methods to make our code look more organized and easier to read.
First off we are going to need to accept input so that the players can move their paddles up and down. The easiest way to accomplish this is going to be to set up a keyDown function with 4 if statements that test the key pressed against the up and down keys for the two players. These if statements will end up changing the value in a variable holding the y position of the appropriate paddle. This y position will be used for the drawing of the paddle and for collision tests to ensure that the paddle doesn't go off the edge of the board.
If you want to expand the game a bit an extra that you could could add in here would be allowing the paddles to move left and right as well as up and down. This means that you will have to add some extra collision detection code in to check the paddles against the edges of the board and the other paddle. You could also try adding in some mouse support for your game. This could include the ability to use the mouse to move one of the paddles around or you could use the mouse to create some sort of menu system.
The third step that we need to cover is the output on the screen. Now you are probably saying what happened to step number two, but don't worry it is coming shortly. The output that we need for the screen is very, very simple. All we really need are two filled rectangles for the paddles, an oval for the puck, and some text to display the score. If we wanted to we could make things more complicated by having a nice background for the game to be played on (an image or something dynamic like a starfield), having a puck whose color is constantly changing, or even obstacles on the board (this could get complicated so don't start off trying to do this).
The second phase which I talked about at the very beginning is the processing stage. So what needs to be processed in our game and what variables will we most likely need? The goal of the game is to have a puck bouncing back and forth on the board and it should bounce off the walls and the paddles. First off we are going to need to have the coordinates of the puck and the two paddles. This means that we will need to have an integer x,y value for each of them. Next we are going to need to have a speed value for the puck. The speed values will be integers which represent the change in the x and y postion of the puck in each time step. With this much information we can move the puck in our run method and test the positions of the puck and paddles to see if there is a collision.
This brings up a good point: how do we detect collisions. There are a lot of complicated formulas for checking collision, but there are some simple methods that will be sufficient for us.
Paddle-Board Collision
For our purposes I'm going to assume that the board is the size of the entire applet area. This means that the extents of the board are from 0 to size().width and 0 to size().height. You will notice that here that I have used size().width and size().height here to represent the maximum height and width of the applet space. If you use these exactly as I have written here in your programs they will provide you with the values that you desire. Testing the paddle for collision with the edge of the board is as simple as having if statements to check whether or not the y value of the paddle is greater than the maximum height or less then zero. If you allow the paddle to move left and right then you can check to see if the x value of the paddle is less then 0 or greater than the maximum width. If we discover that there is a collision or we have gone over the edge of the board then all we have to do is move the paddle back onto the board (eg. 0 or max. height - paddle size).
Paddle-Paddle Collision
We only need to worry about paddle to paddle collision if we allow the user to move his paddle to the left and right. You could even avoid having to worry about this type of collision if you said that a player couldn't move his paddle past the center of the board. Another option would be to let the paddles simply pass through each other with no collision at all. I would recommend one of the above options since the number of times the two paddles are going to get close enough for a collision is almost zero. If you are still interested in pursuing this type of collision then I would recommend that you follow the process that I will discuss in a moment for paddle and puck collisions. This method involves taking the x and y position of the paddle and comparing it against the four edges of the other paddle to see if it is inside the other paddle. If a collision has occurred then we want to move it out of collision.
Paddle-Puck Collision
Checking for collisions between a paddle and the puck isn't easy, but it is a very important part of the game. You could try to get fancy here, but I recommend that you keep it simple. I believe that the easiest way to check for a collision is to check the x and y coordinates of the puck against the four edges of the paddle. The four edges of the paddle are: paddlex, paddlex + paddle_width, paddley, and paddley + paddle_height. With this knowledge in hand it is a simple matter of creating an if statement for each paddle which will consist of four parts testing the four aforementioned edges against the x and y of the puck to see if the puck is inside the paddle. If it is unclear what is going on I would recommend drawing a little diagram of what the situation looks like and this should make things easier for you. Once we have discovered that a collision has occurred then we need to come up with some means of resolving the collision. The best way to do this is to take the speed of the puck in the x direction and negate it. By negating it we get very believable bouncing of the puck off of the paddle. You could also try to add in the possibility of the puck bouncing off the bottom part of the paddle so the x would stay the same, but the y would change. While this might initially sound like a great thing to do it doesn't necessarily add a lot to the game so try it out and decide for yourself whether it is worth the extra effort.
One problem that you might come across is a shimmy. This is where the puck collides repeatedly with the paddle and ends up shimmying up or down the paddle before it finally breaks away. This is something that you want to avoid at all cost because it makes your game seem unprofessional and incomplete. A possible solution for this problem is to check the speed of the puck in the x direction. If the puck is coming towards the paddle then it can be checked for a collision. If on the other hand it is going away from the paddle then we can ignore any collisions that we might think we have.
Puck-Board Collision
Testing for collisions between the puck and the edge of the board is identical to the test that we performed when testing for collisions between the paddles and the board. The only tricky part that we have to watch out for here is that we must check the x and y values of the puck against the extents of the board. Again to resolve this particular type of collision all we have to do is negate the x or y speed of the puck depending on the edge of the board that we hit.
Puck-Goal Collision
This is going to be the same process as what we had to carry out for the paddle to board collision test except in this case we want to check the puck against a limited part of the board. As a starting point lets assume that the goal area consists of the entire left and right hand parts of the board. This means that we can check the x value of the puck against 0 and the maximum width of the board. If we register a collision here then we must increase the score for the appropriate player and put the puck back at the center of the board.
The fourth and final phase is to loop. The loop as we discussed before is accomplished by using threads and the run method. If you are unsure about this you can either refer back to the threads section or check out the sample code for the air hockey game which is available on my web site.
You are now well on the way to creating a great air hockey game. Once you have a basic game up and running you should start playing around and trying to make additions and alterations to your game. Once you have finished your game I would appreciate it if you would send me a copy of your code ([email="kinga@cpsc.ucalgary.ca"]kinga@cpsc.ucalgary.ca[/email]) and your applet so I can post it on my web page for others to see and learn from. For now my web site is www.cpsc.ucalgary.ca/~kinga, but I'm in need of a new location so when I find one I will inform you of the new address.
Coming Soon!!!
Well we covered a lot of ground in this article. Hopefully you have you learned quite a bit and are starting to get ideas about how you could take what you have learned so far and make a game of your own. In part 3 I am going to cover using the AWT for buttons, checkboxes, etc., some basic points on classes and code organization, and neural networks. Then in part 4 we will go into arrays and how they can be used for games like tetris, nibbles, and anything else that can be put on a grid. If you want to check out some sample java applets you can come to my web page, www.cpsc.ucalgary.ca/~kinga, which will be operational until the end of the summer, but then I will be in search of a new web space because I'm graduating from university. If you have any questions or comments feel free to email me at [email="kinga@cpsc.ucalgary.ca"]kinga@cpsc.ucalgary.ca[/email].