CPSC 124, Spring 2006
Lab 8: Tron (or Events and State)

In this lab, you will be working on a simple game based on the LightCycle Duel from the movie Tron. In the game, two players control invisible lightcycles that can travel up, down, left, or right in an arena. The players control the direction in which each lightcycle moves. The lightcycles leave visible trails. When one of the players collides with one of the trails or with one of the walls of the arena, that player loses the game. The players control the lightcycles through the keyboard. One player uses the left, right, up, and down arrow keys, while the other uses the A, S, D, and W keys. In order for the keyboard to function, the game has to be activated by clicking on the arena area. Even then, a player's lightcycle doesn't start moving until that player hits one of the direction keys. Here is an applet that shows the completed game:

To begin the lab, you should start up Eclipse, begin a new project, and import all of the files from the directory /classes/s06/cs124/lab8, including: Tron.java, TronApplet.java, TronPanel.java, and MosaicPanel.java. These files define the starter version of the program, which was demonstrated in class on Friday. You will only have to work on the file TronPanel.java. The program uses a MosaicPanel to represent the "arena" area where the action takes place. You will not need to work on MosaicPanel.java, although you can look at it if you want to learn more about the API that it defines. Tron.java can be run as an application; you should run this file during development inside Eclipse. TronApplet is only necessary if you want to post your applet to a web page.

Exercise: The exercise is to upgrade the starter version of TronPanel.java to a full two-player game. Since it takes two players to play the game, you can choose to work on this lab with a partner. If you do work with a partner, make sure that both names appear in the comment at the top of the program! The exercise is due next week in lab, as usual.


Some Background Information

The arena in the Tron game is actually a MosaicPanel, defined in the file MosaicPanel.java. A MosaicPanel represents a grid of rectangles, where each rectangle can be assigned its own color. It has a lot of options and therefore a lot of methods, but the only ones that you need to know about are the ones related to the colors of the rectangles. Each rectangle is specified by a row number and a column number. The rows are numbered from 0 to ROWS-1, and the columns from 0 to COLUMNS-1, where ROWS and COLUMNS are constants defined in the TronPanel class. The MosaicPanel object in the Tron program is referred to as arena. Some methods in this object that you might need include:

This program is an event-driven program that uses a wide variety of events. The motion of the cycles is driven by a Timer which generates a stream of ActionEvents. While the game is in progress, each ActionEvent moves the two lightcycles one square forward in their current directions of motion. The user generates KeyEvents, which are used to change the direction of the lightcycles. KeyEvents are complicated because they are directed to one particular component on the screen, namely the component that has the "input focus." Often, the input focus appears to the user as a blinking cursor, but in the Tron program, the border of the arena changes from gray to cyan to indicate that it has received the focus. The game is only in progress when the arena has the focus. A FocusEvent is generated when the arena gains or loses focus; these events can be used to start and stop the game. Finally, the program uses MouseEvents to let the user control the input focus. When the user clicks the arena, the arena gains the input focus, which in turn causes the game to start running.

To complete the program, you will have to work with the methods that handle ActionEvents, FocusEvents, and KeyEvents. (The simple MouseEvent handlers won't have to be changed.) You should read the comments on the event-handling methods in TronPanel.java. The ones that you have to worry about are actionPerformed, keyPressed, focusGained, and focusLost.

It will also be important for you to understand how state is managed in the program. "State" refers to the data that is stored in member variables. Events can cause the state to change, and the response to an event can depend on the current state. The major part of the state of the starter version of the program is stored in the member variables direction, currentRow, and currentColumn. The direction changes in the keyPressed method when the user hits one of the arrow keys. The value of direction is used in actionPerformed to decide how to move the lightcycle. Note that the arrow keys do not move the cycle -- they merely set the direction of motion. The actual motion takes place in response to ActionEvents generated by the timer. The position of the lightcycle is stored in currentRow and currentColumn. These variables are used in the actionPerformed method, which is responsible for moving the cycle. They are initialized in the focusGained method at the start of the game.

Add a Second Player

The first thing you should do to the program is to add a second player, without worrying about crashes for the time being.

Obviously, if there are two players, each player has its own position and its own direction. So to keep track of the relevant state, you'll need two "direction" variables, two "currentRow" variables, and two "currentColumn" variables. Currently, there is only one set of variables. When you add the new variables, you might want to change the names of the current set of variables. (Eclipse makes it easy to change a variable's name: Go to the declaration of the variable and double-click the name of the variable to select it. Then right-click on the variable name. Go to the "Refactor" submenu, and select "Rename." A dialog will pop up where you can enter the new name for the variable. This will change the name of the variable in the declaration and also everywhere it is used, even in other files in the project.)

Once you have the new variables, they have to be initialized along with the old variables in the focusGained method. The second player's direction has to change when the A, S, D, or W key is pressed. The change occurs in the keyPressed method. (Note that the constants KeyEvent.VK_A, KeyEvent.VK_S, KeyEvent.VK_D, and KeyEvent.VK_W can be used in this method to refer to the letter keys.) And the actionPerformed method has to move the second player in the same manner that it does the first.

Also, you will want to use two different colors (not random colors) for the trails left by the two players as they move.

When you have made these changes, try them out. You should be able to control both players using the appropriate keys.

Handling Crashes

The game has to end when one of the players crashes into one of the trails or into a wall. Crashes occur when the player moves, that is, in the actionPerformed method. You will have to improve this method to detect crashes and stop the game when one occurs. Right now, when a player hits a wall, it simply doesn't move forward. Instead of just stopping, it should crash and the game should end. Assuming it does not hit a wall, when the player tries to move forward, it should crash when the square that it is trying to move into is already colored. If the square is black, it's OK to move into it; if not, there is a crash.

What should you do when a crash occurs? You should certainly end the game; you can do that with message.requestFocus which will move the input focus away from the arena and, in turn, stop the timer that drives the game action. You might want to change the message that is displayed under the arena. My program also colors the losing player's trail to match the color of the winning player. (To do this, it goes through all the rows and columns, tests the color of each rectangle, and resets the color if it needs to be changed.)

Since there will be many places in the code where you might detect a crash, you might want to write one or two subroutines to handle all the stuff that needs to be done when the game ends. You could have two subroutines, such as redWins() and blueWins() or a single subroutine that has a parameter that tells it which player won.

Nice Touches

Once you have a working game, there are a few nice touches that you can add. If you want full credit, you should do some of these (one hard one or two less-hard ones, more for possible extra credit).

For one thing, if a player tries to make a 180 turn, he will immediately crash. For example, if a player is moving right and tries to change the direction to left, he will crash because he is heading back into his own trail. Although in some sense this is the right behavior, it is annoying. It would be better to ignore attempts to make a 180 degree turn. If a player is moving right, and hits his "left" key, it should be ignored. If a player is moving up and hits his "down" key, it should be ignored, and so on.

Handling input focus can tricky. It is possible for the input focus to move in the middle of a game, even if there has been no crash. For example, this will happen if there is a mouse click on the message. This could be used to pause the game, but your program might not handle pauses very well, because the focusGained and focusLost methods have no way of knowing whether the game has ended or has just been paused. To improve handling of focus, you could use some auxiliary method to keep track of whether or not a game is in progress. You can do this with a member variable of type boolean that is set to true when a game begins and to false when the game ends because of a crash. The focusGained and focusLost methods can check the value of this variable to tell whether the game has ended.

What does your program do when both players crash on the same move? Ideally, it should be a tie, but your program probably awards the game to the first player whose crash is detected. (To see what my program does, head the two players directly towards each other.) Fixing this is a little tricky. One way to do it is to use two local variables of type boolean in the actionPerformed method. Use one variable for each player. Set the player's variable to true if a crash is detected for that player, but don't stop processing at the point where the crash is detected. At the end of the actionPerformed method, after checking both players for crashing, you can test whether one or both players have crashed; if so, end the game and declare a winner or a tie as appropriate.

If you are very ambitious, you might want to implement multi-game matches instead of single games. (My program does not do this.) To do this, you have to keep track of the number of games won by each player. When a player has won, say, three games, that player wins the match. You could include the score in the message that you show to the user. Alternatively, you could add another message label at the top of the program to display the current status of the match.


David J. Eck, March 2006