Home  >  

Writing the Pac-Man Game in JavaFX - Part 3

Author photo
AddThis Social Bookmark Button

Haining Henry Zhang with James L. Weaver

Previous parts in the Pac-Man series
Writing the Pac-Man Game in JavaFX - Part 1
Writing the Pac-Man Game in JavaFX - Part 2

We are now ready to create the ghosts in our game. The four ghosts, namely Blinky(red), Pinky(pink), Inky(cyan) and Clyde(orange), are trapped inside a cage when a game starts. After some time, they get out of the cage one by one and start roaming the maze. Their goal is to catch the Pac-Man. The Pac-Man dies if he is touched by one of the ghosts. If the Pac-Man swallows a magic dot, he has the power to eat ghosts for a while. During this time, the ghosts turn hollow and move more slowly.

There are two parts for writing the code for ghosts. First part is to create the animation. The second part is to implement an algorithm to control how the ghosts move inside the maze. The second part is the most interesting and crucial thing of this game. We will elaborate the algorithm in the next article. For now, we just use a simpler one for testing the animation.

Animation of Ghosts

A ghost can have three kinds of appearance. One is its normal look in its original color. The second is a hollow ghost. The third is a flashing hollow style when it is about to turn back to its original color. So we need three sets of frames for the animation. Just like the Pac-Man character, every set of frames contains 4 pictures. To make a ghost look differently, we can switch the set of frames when the status of a ghost changes. For example, below are three set of pictures for the red ghost Blinky.

In terms of moving approaches, the ghosts have three styles: roaming the maze, crawling slowly when they turn hollow, and circling in the cage. Since the first two are the same except the moving speed is different, we basically need to have two kinds of logic to handle the moving of a ghost: outside and inside the cage respectively.

When we wrote the code of the Pac-Man character, we subclassed from MovingObject. This class abstracts the common logic needed for a character. Let's write the Ghost class by extending MovingObject again. Below is the code of Ghost.fx:

 
/*
 * Ghost.fx
 *
 * Created on 2009-1-28, 14:26:09
 */

package pacman;

import java.lang.Math;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.CustomNode;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.Node;
import pacman.MazeData;

/**
 * @author Henry Zhang
 */

public class Ghost extends CustomNode, MovingObject{
 
   public def TRAPPED=10;
 
   // the pacman character
   public var pacMan: PacMan;
 
   public var hollowImage1 = Image {
      url: "{__DIR__}images/ghosthollow2.png"
      }
   public var hollowImage2 = Image {
      url: "{__DIR__}images/ghosthollow3.png"
      }
   public var hollowImage3 = Image {
      url: "{__DIR__}images/ghosthollow1.png"
      }
 
   // images for ghosts when they become hollow
   public var hollowImg =
     [ hollowImage1,
       hollowImage2,
       hollowImage1,
       hollowImage2 ];
 
   // images for ghosts when they become hollow and flashing
   public var flashHollowImg =
     [ hollowImage1,
       hollowImage3,
       hollowImage1,
       hollowImage3 ];
 
   // time for a ghost to stay hollow
   var hollowMaxTime: Integer = 80;
   var hollowCounter : Integer;
 
   // the images of animation
   public var defaultImage1: Image;
   public var defaultImage2: Image;
 
   def  defaultImg  = [
         defaultImage1,
         defaultImage2,
         defaultImage1,
         defaultImage2,
   ];
 
   // animation images
   var images = defaultImg;
 
   // initial direction and position of a ghost, used in status reset
   public var initialLocationX : Number;
   public var initialLocationY : Number;
   public var initialDirectionX : Number;
   public var initialDirectionY : Number;
 
   // time to stay in the cage
   public var trapTime: Integer;
   public var trapCounter: Integer=0;
 
   // variables to decide if ghost should chase man, and with what probability
   public var changeFactor = 0.75;
 
 
   // the flag is set if a ghost becomes hollow
   public var isHollow: Boolean = false;
 
   // the GUI of a ghost
   var ghostNode : ImageView = ImageView {
      x: bind imageX  - 13
      y: bind imageY  - 13
      image: bind images[currentImage]
      }
 
   postinit {
      initialLocationX = x;
      initialLocationY = y;
      initialDirectionX = xDirection;
      initialDirectionY = yDirection;
  
      resetStatus();
    }
 
   // reset the status of a ghost and place it into the cage
   public function resetStatus() {
      x = initialLocationX;
      y = initialLocationY;
  
      xDirection = initialDirectionX;
      yDirection = initialDirectionY;
  
      isHollow = false;
  
      moveCounter = 0;
      trapCounter = 0;
      currentImage = 0;
  
      imageX = MazeData.calcGridX(x);
      imageY = MazeData.calcGridY(y);
  
      images = defaultImg;
      state = TRAPPED;
          
      timeline.keyFrames[0].time = 50ms;
  
      visible = true;
      start();
    }
 
 
   public function changeToHollowGhost() {
      hollowCounter = 0;
      isHollow = true;
  
      // switch the animation images
      images = hollowImg;
  
      // make it moves slower
      timeline.stop();
      timeline.keyFrames[0].time = 140ms;
      timeline.play();
    }
 
   // decide whether to change the current direction of a ghost
   public function changeDirectionXtoY(mustChange: Boolean): Void {
      if ( not mustChange and Math.random() > changeFactor ) {
         return;  // no change of direction
       }
  
      // will change to a Y direction if possible
      var goUp = MoveDecision {
         x: this.x
         y: this.y - 1 };
      var goDown = MoveDecision {
         x: this.x
         y: this.y + 1
       };
  
      // evaluate the moving choices to pick the best one
      goUp.evaluate();
      goDown.evaluate();
  
      if ( goUp.score < 0 and goDown.score < 0 )
        return;  // no change of direction
  
      var continueGo =  MoveDecision {
         x: this.x + xDirection
         y: this.y
       };
  
      continueGo.evaluate();
  
      if ( continueGo.score > 0 and continueGo.score > goUp.score
           and continueGo.score > goDown.score ) {
         return;
       }
  
      var decision = -1; // make it goes up first, then decide if we need to change it
      if ( goUp.score  < 0 )
        decision = 1
      else
        if ( goDown.score > 0 ) {
           // random pick
           if ( Math.random() > 0.5 )
             decision = 1;
       }
  
      yDirection = decision;
      xDirection = 0;
  
    }
 
   // decide whether to change the current direction of a ghost
   public function changeDirectionYtoX(mustChange: Boolean): Void {
  
      if ( not mustChange and Math.random() > changeFactor )
        return;  // no change of direction
  
      // will change to X directions if possible
      var goLeft = MoveDecision {
         x: this.x - 1
         y: this.y
       };
  
      var goRight = MoveDecision {
         x: this.x + 1
         y: this.y
       };
  
      // evaluate the moving choices to pick the best one
      goLeft.evaluate();
      goRight.evaluate();
  
      if ( goLeft.score < 0 and goRight.score < 0 ) {
         return;  // no change of direction
       }
  
      var continueGo = MoveDecision {
         x: this.x
         y: this.y + yDirection
       };
  
      continueGo.evaluate();
  
      if ( continueGo.score > 0 and continueGo.score > goLeft.score
           and continueGo.score > goRight.score ) {
         return;
       }
  
      // make it goes up first, then decide if we need to change it to down
      var decision = -1;
      if ( goLeft.score  < 0 )
        decision = 1
      else
        if ( goRight.score > 0 ) {
           // random pick
           if ( Math.random() > 0.5 )
             decision = 1;
       }
  
      xDirection=decision;
      yDirection = 0;
    }
 
   // move the ghost horizontally
   public function moveHorizontally() {
  
      moveCounter++;
  
      if ( moveCounter > ANIMATION_STEP - 1) {
         moveCounter=0;
         x += xDirection;
         imageX= MazeData.calcGridX(x);
   
         var nextX = xDirection + x;
   
         if ( y == 14 and ( nextX <= 1 or nextX >= 28) ) {
            if ( nextX < - 1 and xDirection < 0 ) {
               x=MazeData.GRID_SIZE;
               imageX= MazeData.calcGridX(x);
             }
            else
              if ( nextX > 30 and xDirection > 0) {
                 x=0;
                 imageX= MazeData.calcGridX(x);
               }
          }
         else
           if (nextX < 0 or nextX > MazeData.GRID_SIZE) {
              changeDirectionXtoY(true)
            }
         else
           if ( MazeData.getData(nextX, y) == MazeData.BLOCK ) {
              changeDirectionXtoY(true)
            }
           else {
              changeDirectionXtoY(false);
            }
       }
      else {
         imageX += xDirection * MOVE_SPEED;
       }
    }
 
   // move the ghost vertically
   public function moveVertically() {
        
      moveCounter++;
  
      if ( moveCounter > ANIMATION_STEP - 1) {
         moveCounter = 0;
         y += yDirection;
         imageY = MazeData.calcGridX(y);
   
         var nextY= yDirection + y;
         if ( nextY < 0 or nextY > MazeData.GRID_SIZE) {
            changeDirectionYtoX(true);
          }
         else
           if ( MazeData.getData(x, nextY) == MazeData.BLOCK ) {
              changeDirectionYtoX(true);
            }
           else {
              changeDirectionYtoX(false);
            }
       }
      else {
         imageY += yDirection * MOVE_SPEED;
       }
    }
 
   // move the ghost horizontally in the cage
   public function moveHorizontallyInCage() {
      
      moveCounter++;
  
      if ( moveCounter > ANIMATION_STEP - 1) {
   
         moveCounter=0;
         x += xDirection;
         imageX = MazeData.calcGridX(x);
   
         var nextX = xDirection + x;
   
         if ( nextX < 12 ) {
            xDirection = 0;
            yDirection = 1;
          }
         else
           if ( nextX > 17) {
              xDirection = 0;
              yDirection = -1;
            }
       }
      else {
         imageX += xDirection * MOVE_SPEED;
       }
    }
 
   // move the ghost vertically in a cage
   public function moveVerticallyInCage() {
  
      moveCounter++;
  
      if ( moveCounter > ANIMATION_STEP - 1) {
         moveCounter=0;
         y += yDirection;
         imageY= MazeData.calcGridX(y) + 8;
   
         var nextY = yDirection + y;
   
         if ( nextY < 13 ) {
            yDirection = 0;
            xDirection = -1;
          }
         else
           if ( nextY > 15) {
              yDirection = 0;
              xDirection = 1;
            }
       }
      else {
         imageY += yDirection * MOVE_SPEED;
       }
    }
 
   public function hide() {
      visible=false;
          timeline.stop();
    }
 
   // move one tick
   public override function moveOneStep() {
  
      if ( state == MOVING or state == TRAPPED ) {
         if ( xDirection != 0 ) {
            if ( state == MOVING )
              moveHorizontally()
            else
              moveHorizontallyInCage();
          }
         else
           if ( yDirection != 0 ) {
              if ( state == MOVING )
                moveVertically()
              else
                moveVerticallyInCage();
            }
   
         if ( currentImage < ANIMATION_STEP - 1 )
           currentImage++
         else {
            currentImage=0;
            if ( state == TRAPPED ) { 
               trapCounter++;
     
               if ( trapCounter > trapTime and x == 14 and y == 13) {
                  // go out of the cage
                  y = 12;
      
                  xDirection = 0;
                  yDirection = -1;
                  state = MOVING;
                }
             }
          }
       }
  
      // check to see if need to switch back to a normal status
      if ( isHollow ) {
       
         hollowCounter++;
   
         if ( hollowCounter == hollowMaxTime - 30 )
           images = flashHollowImg
         else
           if ( hollowCounter > hollowMaxTime ) {
              isHollow = false;
              images = defaultImg;
                    
              timeline.stop();
              timeline.keyFrames[0].time = 50ms;
              timeline.play();
            }
       }
    }
 
   public override function create(): Node {
          return ghostNode;
    }
 
}

The variable defaultImg is a sequence of images used as a ghost normal look. The variable hollowImg and flashHollowImg store two sets of images for the states when a ghost becomes hollow and flashing. Similar to the PacMan class, the moving logic is handled in the function moveOneStep(). When a ghost is inside the cage, the function moveHorizontallyInCage() and moveVerticallyInCage() make the ghost turning around and around inside the cage. When a ghost gets out of the cage, two functions moveHorizontally() and moveVertically() control its roaming behavior. The variable trapTime determines how long a ghost stays in the cage before it gets out. Properly choosing the values of this instance variable makes four ghosts coming out the cage in a fixed order(Blinky-Pinky-Inky-Clyde). The below code in the function moveOneStep() sets free the ghost after a pre-defined time.

 
  public override function moveOneStep() {
         . . . . . .
 
         if ( state == TRAPPED ) { 
            trapCounter++;
  
            if ( trapCounter > trapTime and x == 14 and y == 13) {
               // go out of the cage
               y = 12;
   
               xDirection = 0;
               yDirection = -1;
               state = MOVING;
             }
          }
 
        . . . . . .
   }

The function changeToHollowGhost() turns a ghost into a hollow style. What it does is switching the animation pictures and slowing down the moving speed of a ghost.

 
  public function changeToHollowGhost() {
     hollowCounter = 0;
     isHollow = true;
 
     // switch the animation images
     images = hollowImg;
 
     // make it moves slower
     timeline.stop();
     timeline.keyFrames[0].time = 140ms;
     timeline.play();
   }

After a ghost becomes hollow, it resumes to its normal color after a period of time. The second half of the function moveOneStep() uses a counter to keep track of the time and flashes the ghost just before it turns into its normal color. From this part, we can see how the switching of 3 sets of pictures works.

 
  public override function moveOneStep() {
 
     . . . . . .
 
     // check to see if need to switch back to a normal status
     if ( isHollow ) {
      
        hollowCounter++;
  
        if ( hollowCounter == hollowMaxTime - 30 )
          images = flashHollowImg
        else
          if ( hollowCounter > hollowMaxTime ) {
             isHollow = false;
             images = defaultImg;
                   
             timeline.stop();
             timeline.keyFrames[0].time = 50ms;
             timeline.play();
           }
      }
   }

Roaming the Maze

As we mentioned previously, the algorithm that governs the ghosts' moving is the heart of this program. For the purpose of testing the ghosts' animation, for now, we apply a "random" moving algorithm, ie. the ghosts run arbitrarily inside the maze. In next article, we will implement a more complex algorithm. The function changeDirectionYtoX( Boolean ) and changeDirectionXtoY( Boolean ) give out decisions of whether a ghost should keep its current direction, or make a left or right turn. For illustration, let's take an in-depth look at the function changeDirectionYtoX( Boolean ).

 
  // decide whether to change the current direction of a ghost
  public function changeDirectionYtoX(mustChange: Boolean): Void {
 
     if ( not mustChange and Math.random() > changeFactor )
       return;  // no change of direction
 
     // will change to a X direction if possible
     var goLeft = MoveDecision {
        x: this.x - 1
        y: this.y
      };
 
     var goRight = MoveDecision {
        x: this.x + 1
        y: this.y
      };
 
     // evaluate the moving choices to pick the best one
     goLeft.evaluate();
     goRight.evaluate();
 
     if ( goLeft.score < 0 and goRight.score < 0 ) {
        return;  // no change of direction
      }
 
     var continueGo = MoveDecision {
        x: this.x
        y: this.y + yDirection
      };
 
     continueGo.evaluate();
 
     if ( continueGo.score > 0 and continueGo.score > goLeft.score
          and continueGo.score > goRight.score ) {
        return;
      }
 
     // make it goes up first, then decide if we need to change it to down
     var decision = -1;
     if ( goLeft.score  < 0 )
       decision = 1
     else
       if ( goRight.score > 0 ) {
          // random pick
          if ( Math.random() > 0.5 )
            decision = 1;
      }
 
     xDirection=decision;
     yDirection = 0;
   }

When a ghost is moving vertically, this function determines the next direction. Possible decisions include: turning left, turning right, and continue with the current direction. A class MoveDecision is used to model a tentative decision. See the below code:

 
/*
 * MoveDecision.fx
 *
 * Created on 2009-1-28, 14:42:00
 */

package pacman;

/**
 * @author Henry Zhang
 */

public class MoveDecision {
 
   // x and y of an intended move
   public var x: Number;
   public var y: Number;
 
   public var score: Number;
 
   // evaluate if the move is valid,
   // if it is invalid, returns -1;
   // if it is valid, compute its score for ranking the final decision
   public function evaluate( ):Void {
      if ( x < 1 or y < 1 or y >= MazeData.GRID_SIZE or x >= MazeData.GRID_SIZE){
         score = -1;
         return ;
       }
  
      var status = MazeData.getData(x, y);
      if ( status == MazeData.BLOCK ) {
         score = -1;
         return ;
       }
  
      // rank it as a default score
      score = 1;
    }
}

The evaluate() function evaluates a moving decision and gives a score. A ghost simply picks the decision with highest ranking score. In a random moving algorithm, all decisions are given an equal score 1. If a move leads the ghost to hitting a wall, the ranking score is (-1), which automatically eliminates it from being a candidate decision. If a ghost reaches a wall, the argument mustChange of changeDirectionYtoX(Boolean) is set so that a ghost always gets a change of direction. If this argument is false, the decision to change direction is affected by a random factor changeFactor. This allows the moving behavior of a ghosts more unpredictable, hence the player cannot guess the moving pattern of a ghost. The changeDirectionXtoY(Boolean) has a similar logic and it determines the moving decision when a ghost is going horizontally.

Running the Game

Now we are ready to put things together and have some fun running the program. We add in some code to Maze.fx, putting four ghosts on stage:

 
public class Maze extends CustomNode {
 
   . . . . . 
 
   public var ghostBlinky = Ghost {
      defaultImage1: Image {
         url: "{__DIR__}images/ghostred1.png"
       }
  
      defaultImage2: Image {
         url: "{__DIR__}images/ghostred2.png"
       }
  
       maze: this
       pacMan: pacMan
       x: 17
       y: 15
       xDirection: 0
       yDirection: -1
       trapTime: 1
      };
 
    public var ghostPinky = Ghost {
       defaultImage1:Image {
           url: "{__DIR__}images/ghostpink1.png"
        }
  
       defaultImage2:Image {
          url: "{__DIR__}images/ghostpink2.png"
        }
       
       maze: this
       pacMan: pacMan
       x: 12
       y: 14
       xDirection: 0 
       yDirection: 1
       trapTime: 10
     };
 
    public var ghostInky = Ghost {
       defaultImage1:Image {
          url: "{__DIR__}images/ghostcyan1.png"
        }
       defaultImage2:Image {
          url: "{__DIR__}images/ghostcyan2.png"
        }
  
       maze: this
       pacMan: pacMan
       x: 13
       y: 15
       xDirection: 1
       yDirection: 0
       trapTime: 40
     };
 
    public var ghostClyde = Ghost {
       defaultImage1:Image {
           url: "{__DIR__}images/ghostorange1.png"
        }
       defaultImage2:Image {
          url: "{__DIR__}images/ghostorange2.png"
        }
  
       maze: this
       pacMan: pacMan
       x: 15
       y: 14
       xDirection: -1
       yDirection: 0
       trapTime: 60
     };
 
    public var ghosts = [ghostBlinky, ghostPinky, ghostInky, ghostClyde];
 
     . . . . . . 
 
    postinit {
  
      . . . . . .
  
       insert pacMan into group.content;
  
       insert ghosts into group.content;
  
       insert WallBlackRectangle{ x1:-3, y1:13, x2:0, y2:15 } into group.content;
       insert WallBlackRectangle{ x1:29, y1:13, x2:31, y2:15 } into group.content;
     }

Run the program and you can see four ghosts roaming the maze. You can control the Pac-Man character by keyboard to eat dots. However, the ghosts cannot eat the Pac-Man even they meet each other. We will implement this part in next article. Click on the below screenshot and see it for yourself:



Download Source Code

Related Features

Read more from Haining Henry Zhang. Haining Henry Zhang's Atom feed

Comments

4 Comments

Gene Techno said:

I'm having a problem

public class PacMan extends CustomNode, MovingObject {
public class Ghost extends CustomNode, MovingObject{

Both have error on them saying "Only non-mixin based class allowed."

I tried redoing it, and downloading the source codes

Going on ahead, still same error, could some one help me?

tedo379@yahoo.com

Thanks

Henry Zhang said:

Hi Tedo379,

This is due to JavaFX 1.2 removed multi inheritance, please refer to the comments of article 3 for more detail:

http://www.insideria.com/2009/06/writing-the-pac-man-game-in-ja-3.html

Does it have an online feature for multipile players? Can you go FFA? This is my fovorite type of game. It allows players who are actually good to win for their own skills a beat a bunch of pro or noobs.

Gabriel Hirjoi said:

Hi ..
Very nice tutorial ..
Thank you for sharing ..
I have only one question ..
Do you have the Pac-Man game implementation for Java ?

free2b_thesame@yahoo.com
Best regards ..

Leave a comment


Tag Cloud

Question of the Week: Open Source Flex Projects

What would you say are the 5 most prominent open source projects in the Flex world?

Answer

Latest Features

Recommended for You

@InsideRIA on Twitter

Archives

  • Or, visit our complete archive.  

About This Site

Welcome to the premiere community site for all things RIA sponsored by O'Reilly Media and Adobe Systems Incorporated.