The object of the game is for Pacman to eat as much of the "food" on the game board as possible, without running into the monsters that are also roaming the board. Each food item eaten scores 1 point for the player. If Pacman occupies the same board square as any monster, then Pacman loses a life. After all lives are used, the game is over.
There are a few more details, as follows. On the board, you'll notice a few larger food items (we'll call them "power pills"). Each power pill eaten scores 20 points. When Pacman eats one of these pills, the monsters appear as "ghosts" for a period of 10 seconds. During this time, Pacman is free to run into them, which causes them to disappear and then materialize in their starting positions in the center of the board.
When Pacman has eaten all the food on the board (including all the power pills), Pacman gets an extra life (up to a maximum of 5) and the board is reset. Pacman is returned to the starting position, all the food and power pills show up again, and the monsters are returned to their starting positions.
There will be many threads involved in this application, and it's important to plan carefully so that they all interact correctly and the game works properly. These threads will include:
In the project folder, you will find several useful classes. Read and understand the code and documentation for each. Ask if you have questions about any of these classes.
The project folder also contains a folder called images, in which you'll find images of the Pacman character, as well as a monster image and a ghost image. You can create new images if you like, but these were provided to save you time. Feel free to use them.
A word of advice: Be mindful of synchronization concerns so that you avoid threads seeing inconsistent states. At the same time, be careful to avoid deadlock, as discussed in class.
public void run() method to contain a
simple loop that steps through the current animation sequence by repeatedly
calling the setImage method inherited from Picture and supplying the images
of the animation sequence in order. When you get to the end of the sequence,
the next image should be the first one in the sequence. (The mod (%) operator
will be useful for computing this index.) Note that your loop should run
forever.
pacmanSequences and monsterSequences
in the Startup.java file.
In your testing, make sure you can switch among the different animation
sequences. (Ensure that your run() method
would still work if the animation sequences were
of different lengths. Think about what would happen if your running thread
was near the end of a long sequence and another thread changed the animation
sequence to an index of a short sequence.)
reset() should put the component back at
its original grid position provided and set its destination to that same
location so that the component doesn't move.
setRate should take as a parameter
the number of pixels to move per second and save it for use by the
run method. This could be used to speed up or slow down
the game.
setDirectionRequest should take in
two integers, x and y, and save them in instance variables
as the currently requested direction, but it should not actually
set off moving in that direction (that will be done later, in setDirection).
At most one of the x and y parameters
should be non-zero, and the non-zero value should
be either 1 or -1. So, basically, the x and y values indicate whether
the component should move up (0,-1), down (0,1), left (-1,0), or right (1,0).
The request should be saved inside the object until either:
setDirectionRequest is made (the user
changed his/her mind).
setDirection should examine the
BoardMap object to determine how far the piece can move in the
most recently requested
direction from its current grid position. If it can move at all in that
direction, the destination
should be set accordingly (to move as far as possible in that direction),
and the direction request instance variables should be reset to 0.
In addition, if directional animation was specified in the constructor,
then the animation sequence number of the animated picture should be
set as follows: right - 0, down - 1, left - 2, up - 3. That way,
the user of the SpriteMover can create four different animation sequences
so the animation looks different depending on the direction of travel.
takeStep takes no parameters
and is responsible for one pixel of movement of the component.
First, it should check to see if the component's current located is
exactly at a grid square boundary (in other words, if its current x and
y position are both evenly divisible by the square size). If so,
it should call checkMap (see below), providing the current
grid position, and it should process the pending direction request (if any),
since there may be an opportunity to move in that direction.
Then, regardless of whether or not the component is at a square boundary,
if the component is not already at it's destination,
the method should move the component by one pixel in the desired direction
and call the method performNotification (see below).
The method takeStep should return true if the
component has reached its destination, and false otherwise.
checkMap and performNotification
should be defined with empty method bodies. The idea is to allow
subclasses of the SpriteMover to be created in which specialized behavior
is performed at each grid location and/or for each movement.
You'll see this later on, when you create a PacmanMover and a MonsterMover.
public void run() method
should be an infinite loop, continuously calling takeStep
at the rate specified. However, it's
silly to waste processing time on the computer while the component is
already at it's destination, so you should suspend the thread
when the destination is reached. How will it wake up?
You should resume the thread from a different method,
whenever a new destination is established.
Be careful about your synchronization to avoid deadlock.
setDirectionRequest method on your
SpriteMover object, providing the appropriate x and y values.
(To determine the integer values reported in KeyEvent
for the keys you want to use, you can
look them up in Flanagan, or you can find out through experimentation by
having your listener first just print out the integer value for each
keystroke.) Make sure that the frame is selected, or your keystrokes
will be ignored. If you're still not getting keystroke events,
try calling the requestFocus() method on your frame.
Test thoroughly. Make sure that your SpriteMover detects the walls properly and that the animation direction works properly.
The monsters in the game need to move on their own. To achieve this, you'll define a thread that has the "brains" of a monster:
run() method for the MonsterBrains class to
continously call setDirectionRequest on
the SpriteMover, say 5 or 10 times per second. The particular direction
should be chosen randomly. Make sure the direction is legal so you dont send
the monster off on a diagonal.
The nextInt() method of java.util.Random may be helpful here in conjunction
with the mod (%) operator.
At this point, you have Pacman under user control and monsters move in random directions, but Pacman and the monsters do not interact. We need to have a way for the monsters to be aware of Pacman's position so that appropriate action can take place when collisions occur. We will accomplish this through object sharing.
Since the SpriteMovers all all have a reference to the BoardMap object, we can use that object as a communication center, where information from one thread can be stored for later observation by another thread. However, since Pacman and the monsters must behave differently with respect to that data, we'll create two subclasses of SpriteMover, called PacmanMover and MonsterMover, in which we can define the differences in their behavior. Then we'll add some instance variables to the BoardMap class, with appropriate mutators and accessors, so that the two different kinds of SpriteMovers can communicate through the BoardMap object.
score inside the PacmanMover,
and increment it every time Pacman consumes piece of food.
setText to report the score in
the label whenever the score is updated.
Inside of BoardMap, define a top-level (static)
inner class called OverlapChecker.
Its setLocation method should
take two parameters, a Rectangle and a PacmanMover, and store them
in insance variables. Its overlaps method should take in another Rectangle as a parameter
and call Rectangle's provided intersects method
to determine if the two rectangles overlap. If they do intersect, it should
set both instance variables to null and return the PacmanMover.
If they don't intersect, it should just return null.
Add an instance variable to keep an OverlapChecker object inside of BoardMap. Initialize it to a new OverlapChecker in BoardMap constructor, and provide an accessor that returns the OverlapChecker so that other objects can use it.
getBounds method of Component to get a Rectangle that defines
the current position of the component on the screen.)
At this point, the OverlapChecker inside of the BoardMap object knows
about Pacman's current position at all times. So, we need to create a
MonsterMover that pays attention to this information. In a new file called
MonsterMover.java, create MonsterMover class that extends SpriteMover.
In the constructor, get the OverlapChecker from the BoardMap and save it
in an instance variable. Override the checkMap method inside
MonsterMover to call the overlaps method on the OverlapChecker,
passing in the monster's current position. If a PacmanMover is returned,
we know there was a collision. For now, just call reset
on the Pacman mover to put Pacman back at the starting position when there
is a collision.
Add an instance variable called lives inside the PacmanMover,
and decrement it every time it is reset.
You can accomplish this by overriding the reset method...
just remember to also call the reset method of the superclass.
In Startup.java, create
a java.awt.Label object and add it at the top of the frame (in the
BorderLayout.NORTH position).
Pass this object to the PacmanMover,
either in the constructor or in a separate method. Modify the PacmanMover
to call setText to report the number of lives in
the label initially, and whenever the number of lives is updated.
You can start out with 3 lives. When the number of lives falls to zero,
stop the thread so that Pacman doesn't continue to move.
monsterStatus
inside of BoardMap, and provide an accessor and mutator method to get and
set the current monster status. BoardMap already defines
constants for the three possible
status flags: MONSTER (0), GHOST (1), and TRANSITION (2).
checkMap method so that it
looks at the shared monsterStatus variable, and changes animation sequence
number in the AnimatedPicture for the monster
according to the status flag. That way, the player can tell the current
status of the monster. (During the transition period, the animation sequence contains both the monster and ghost images, so the monster will appear to flash between them. When you create the monster AnimatedPicture, set the frame rate low enough --- say 4 frames per second --- so you can see the flashing.)
Also in checkMap, have the MonsterMover
decide whether to reset the Pacman mover or reset itself, depending
on the current monsterStatus. When the MonsterMover resets itself, it
should call a capturedGhost method on the PacmanMover (which
you need to write) so that the PacmanMover can add 50 points to the
player's score.
run() method should
be an infinite loop. Inside the loop, there should be a try
block that should first set the monsterStatus
in the BoardMap to be GHOST. Then, it should sleep 10 seconds.
Then, it should set the status to TRANSITION. Then, sleep 2 more seconds.
Finally, it should set the status to MONSTER and suspend itself.
The catch block should just ignore any InterruptedException.
You should also provide a method in TimerThread called reset
that resets the timer by calling both interrupt and then
resume on the thread itself (this).
Why call interrupt? Think about what would happen if Pacman eats a power pill while the monsters are ghosts. Simply resuming the thread would not give Pacman any extra time. However, interrupting such a thread would cause the current iteration of the loop to end, thereby resetting the timer to the beginning of its cycle.
checkMap to reset the GhostTimer whenever Pacman eats
a PowerPill. Also, let the PowerPill score 20 points.
While you're there, also modify checkMap to notice if the
board has been cleared (there's an accessor for this already in
BoardMap). If so, call reset on both the board and
the Pacman mover, and give Pacman a bonus life (up to a maximum of five
lives). You may
want to call super.reset here, because you've already
overridden the local reset method to subtract a life, but be sure
to update the text label regardless, so the player sees how many lives
remain. (Note: In the "real" Pacman game, the monsters go back to start
when the board is cleared, but we're not requiring you to do that. You
can leave them roaming around wherever they happen to be.)
Important: Remember that because the interleaving of threads is unpredictable, no amount of testing is a guarantee. You need to reason about the program in order to prove that bad things won't happen. Spend time looking through your code at access protection modifiers, and also synchronization modifiers. Make sure that critical data access is synchronized, so that a thread can't see an inconsistent state. Also, ensure that deadlock can't occur. (For example, it's unlikely that you want suspend and resume calls for a thread inside of synchronized methods, because if you suspend inside a synchronized thread, you continue to hold the lock, so no other thread could get into the other synchronized method in which resume is called.)