Home  >  

Writing the Pac-Man Game in JavaFX - Part 4

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
Writing the Pac-Man Game in JavaFX - Part 3

In the last article, we developed a preliminary version of the Pac-Man game. Four ghosts are randomly roaming the maze and a Pac-Man character can be controlled by a player. Now, we write some more code for the interaction between ghosts and the Pac-Man:

  1. Pac-Man eats a ghost after he gobbles a magic dot.
  2. A ghost eats the Pac-Man when it touches him.

Pac-Man eats Ghosts

In the function moveOneStep(), we add in code to check if the Pac-Man character has gobbled a magic dot. If he has, we change the ghosts into a hollow state. When the Pac-Man character meets a ghost, he can eat the ghost if it is hollow. Let's add three functions into Maze.fx.
 
public class Maze extends CustomNode {
 
   // counter for ghosts eaten
   var ghostEatenCount : Integer;
 
   // text to be displayed for score of eating a ghost
   var scoreText = [
     ScoreText {
        content: "200"
        visible: false;
      },
     ScoreText {
          content: "400"
          visible: false;
      },
     ScoreText {
          content: "800"
          visible: false;
      },
     ScoreText {
          content: "1600"
          visible: false;
      },
   ];
 
   . . . . . .
 
   var group : Group =
   Group {
      content: [
      (retangles for drawing the maze)
      . . . . . .,
      scoreText
    ];
  
    . . . . . .
  
    // determine if pacman meets a ghost
    public function hasMet(g:Ghost): Boolean {
   
       def distanceThreshold = 22;
   
       var x1 = g.imageX;
       var x2 = pacMan.imageX;
   
       var diffX = Math.abs(x1-x2);
   
       if ( diffX >= distanceThreshold ) return false;
   
       var y1 = g.imageY;
       var y2 = pacMan.imageY;
       var diffY = Math.abs(y1-y2);
   
       if ( diffY >= distanceThreshold ) return false;
   
       // calculate the distance to see if pacman touches the ghost
       if ( diffY*diffY + diffX*diffX <= distanceThreshold*distanceThreshold )
         return true;
   
       return false;
     }
  
    public function pacManMeetsGhosts() {
   
       for ( g in ghosts )
         if ( hasMet(g) )
           if ( g.isHollow ) {
              pacManEatsGhost(g);
            }
     }
  
    public function pacManEatsGhost(g: Ghost) {
         
       ghostEatenCount++;
   
       var s = 1;
       for ( i in [1..ghostEatenCount] ) s = s + s;
   
       pacMan.scores += s*100;
   
       var st = scoreText[ghostEatenCount-1];
       st.x = g.imageX - 10;
       st.y = g.imageY;
   
       g.stop();
       g.resetStatus();
       g.trapCounter = -10;
   
       st.showText();
     }
 }

The function hasMet() computes the distance between the Pac-Man and a ghost. If they touch each other, this function returns true. The distance between two objects can be calculated as

 
d = sqrt( (x1-x2)*(x1-x2)+(y1-y2)*(y1-y2) )

If the distance is below a threshold, we consider two objects overlapping eath other. Since this function will be invoked very frequently during the game, we optimize the function to make it run a bit faster. We replace using the expensive Math.sqrt() function by an equivalent equation:

 
if ( diffY*diffY + diffX*diffX <= distanceThreshold*distanceThreshold ) 
             return true;
Further, when the Pac-Man and a ghost is far away from each other, we can simplify the algorithm by applying two checkings before the expensive distance calculation:
 
if ( diffY >= distanceThreshold ) return false;
if ( diffX >= distanceThreshold ) return false;

The function pacManEatsGhost() eliminates a eaten ghost from the maze. The ghost instance then gets reset and thrown back to the cage. In the original game, a pair of ghost's eyes are traversing the maze before they reach the cage and turn back into a ghost. For simplicity, we do not implement this part and just put the ghost directly back to the cage.

The class ScoreText is a subclass of Text and it displays the score when a ghost is eaten by the Pac-Man. The score remains visible for 2 seconds and then disappears. The code of the class ScoreText is as below. An instance of Timeline hides the text at the end of its animation.

 
/*
 * ScoreText.fx
 *
 * Created on 2009-2-6, 17:52:42
 *
 * text object for showing scores of eating a ghost, then disappears after 2s
 *
 */

package pacman;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.paint.Color;

/**
 * @author Henry Zhang
 */

public class ScoreText extends Text{
   override var font = Font { size: 11 };
 
   override var fill = Color.YELLOW;
 
   var timeline= Timeline {
      repeatCount: 1
      keyFrames: [
        KeyFrame {
           time: 2s
           action: function() {
              visible = false;
            }
         }
      ]
      };
 
 
   public function showText() {
      visible = true;
      timeline.stop();
      timeline.play();
    }
}

In function moveOneStep() of PacMan, we add in a line at the end:

 
  public override function moveOneStep() {
 
     // handle keyboard input only when pac-man is at a point of the grid
     if ( currentImage == 0 )
       handleKeyboardInput();
 
     if ( state == MOVING) {
        if ( xDirection != 0 )
          moveHorizontally();
  
        if ( yDirection != 0 )
          moveVertically();
  
        // switch to the image of the next frame
        if ( currentImage < ANIMATION_STEP - 1  )
          currentImage++
        else {
           currentImage=0;
           updateScores();
         }
      }
     maze.pacManMeetsGhosts();
   }

Run the program now and we get a "never-die" version of PacMan game. Pac-Man can eat ghosts but not vice versa. Click on the below button and enjoy this special edition of the Pac-Man Game.



Ghost eats Pac-Man

If a ghost touches the Pac-Man character, he loses a life. An animation shows the dying of the Pac-Man character. This animation displays a pie and shrinks into nothing at the end (see below figure).

We create a class DyingPacMan to perform this animation. Since the pie can be drawn by a standard shape class Arc in JavaFX, we just subclass the class Arc and add in some animation to it. The source code is listed below:

 
/*
 * DyingPacMan.fx
 *
 * Created on 2009-2-6, 17:52:42
 */

package pacman;

import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.shape.Arc;

/**
 * @author Henry Zhang
 */

public class DyingPacMan extends Arc {
 
   public var maze : Maze;
 
   var timeline = Timeline {
      repeatCount: 1
      keyFrames: [
      
        KeyFrame {
           time: 600ms
           action: function() {
              // hide the pacMan character and ghosts before the animation
              maze.pacMan.visible = false;
              
              for ( g in maze.ghosts ) {
                 g.hide();
               }
    
              visible = true;
            }
           values: [ startAngle => 90, length=>360 ];  
         },
  
        KeyFrame {
           time: 1800ms
           action: function() {
              visible = false;
            }
           values: [ startAngle => 270 tween Interpolator.LINEAR,
                     length => 0 tween Interpolator.LINEAR ]
         },
      ]
    }
 
   public function startAnimation(x: Number, y: Number) : Void {
  
      startAngle = 90;
      centerX = x;
      centerY = y;
  
      timeline.play();
    }
}

We create an instance of Timeline. It defines two instances of keyFrame. To make the game pause for a short while after a ghost captures the Pac-Man, we let the first keyFrame start at 600ms. This keyFrame has two tasks:

a) Hide the Pac-Man character and all the ghosts, then make the dying PacMan animation visible. The action attribute contains a function for this purpose.
b) Initialize the variable startAngle and length. They are inherited from the Arc class. By changing their values, we can achieve the animation effect of a shrinking pie.

In the second keyFrame, we define the interpolation of two variables. Interpolation is another animation feature provided by JavaFX. During the animation, from 600ms to 1800ms, startAngle changes from 90 to 270, while length changes from 360 to 0. The interpolation method defined by the tween keyword is Interpolator.LINEAR. It allows the value of a variable evenly(i.e. linearly) changes between two frames. JavaFX API takes care of the adjustment of the values between two adjacent keyFrames. The below figure illustrates how the two variables change to control the shape of the pie.

The next step is to put an instance of DyingPacMan into the maze and start the animation when a ghost captures the Pac-Man.

 
public class Maze extends CustomNode {
 
   . . . . . .
   public var dyingPacMan =
     DyingPacMan {
        maze: this
        centerX: 0 
        centerY: 0
        radiusX: 13  
        radiusY: 13
        startAngle: 90
        length: 360
        type: ArcType.ROUND
        fill: Color.YELLOW
        visible: false
     } ;
   . . . . . .
 
   var group : Group =
   Group {
      content: [
  
        . . . . . .
  
        scoreText, 
        dyingPacMan
      ]
    }; // end Group
 
   . . . . . . 
 
   public function pacManMeetsGhosts() {
  
      for ( g in ghosts )
        if ( hasMet(g) )
          if ( g.isHollow ) {
             pacManEatsGhost(g);
           }
          else {
             for ( ghost in ghosts )
               ghost.stop();
   
             pacMan.stop();
   
             dyingPacMan.startAnimation(pacMan.imageX, pacMan.imageY);
             break;
           }
    }
 
   . . . . . .
}

Game Levels

So far, we have completed most of the coding. It is a fair game now because the Pac-Man and ghosts both have the ability to eat each other. The remain task is to give three lives to the Pac-Man character and have some levels of the game. If the Pac-Man character eats up all the dots, the game enters into the next level. If Pac-Man character dies during a game, the life count is decreased by 1. When the player's score exceeds 10000, a bonus life is awarded. If all lives are used, the game is over. We reset the status of all objects and restart the game. The source code of Maze.fx and PacMan.fx are modified. The changes are highlighted in bold text below.

 
/*
 * Maze.fx
 *
 * Created on 2008-12-20, 20:22:15
 */

package pacman;

import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import pacman.MazeData;
import pacman.PacMan;
import pacman.WallBlackLine;
import pacman.WallBlackRectangle;
import pacman.WallRectangle;
import javafx.scene.input.*;
import javafx.scene.text.Text;
import javafx.scene.text.Font;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import java.lang.Math;
import javafx.scene.shape.ArcType;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;

/**
 * @author Henry Zhang
 */

public class Maze extends CustomNode {
 
   // counter for ghosts eaten
   var ghostEatenCount : Integer;
 
   // text to be displayed for score of eating a ghost
   var scoreText = [
     ScoreText {
        content: "200"
        visible: false;
      },
     ScoreText {
          content: "400"
          visible: false;
      },
     ScoreText {
          content: "800"
          visible: false;
      },
     ScoreText {
          content: "1600"
          visible: false;
      },
   ];
 
   // Pac Man Character
   public var pacMan : PacMan = PacMan{ maze:this x:15 y:18 } ;
 
   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: 20
     };
 
    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: 30
     };
 
   public var ghosts = [ghostBlinky, ghostPinky, ghostInky, ghostClyde];
 
   public var dyingPacMan =
     DyingPacMan {
        maze: this
        centerX: 0
        centerY: 0
        radiusX: 13
        radiusY: 13
        startAngle: 90
        length: 360
        type: ArcType.ROUND
        fill: Color.YELLOW
        visible: false;
     } ;
 
   // the pac man image
   var pacmanImage =Image {
      url: "{__DIR__}images/left1.png"
    }
 
   // images showing how many lives remaining
   var livesImage = [
     ImageView {
        image: pacmanImage
        x: MazeData.calcGridX(18)
        y: MazeData.calcGridY(30)
        visible: bind livesCount > 0
      },
     ImageView {
        image: pacmanImage
        x: MazeData.calcGridX(16)
        y: MazeData.calcGridY(30)
        visible: bind livesCount > 1
      },
     ImageView {
        image: pacmanImage
        x: MazeData.calcGridX(14)
        y: MazeData.calcGridY(30)
        visible: bind livesCount > 2
      }
   ];
 
   // level of the game
   public var level : Integer = 1;
 
   // flag to add a life to the player if the first time scores exceed 10000
   var addLifeFlag: Boolean = true;
 
   // current lives of the player
   var livesCount = 2;
 
   // message to start a game
   var waitForStart: Boolean = true;
   var messageBox = Group {
      content: [
        Rectangle {
           x: MazeData.calcGridX(5)
           width: MazeData.GRID_GAP * 19
           y: MazeData.calcGridY(21)
           height: MazeData.GRID_GAP *5
           stroke: Color.RED
           strokeWidth: 5
        
           fill: Color.CYAN
           opacity: 0.75
           arcWidth: 25
           arcHeight: 25
         },
        Text {
           font: Font { size: 19 }
           x: MazeData.calcGridX(7)
           y: MazeData.calcGridY(24)
   
           content: "PRESS ANY KEY TO START"
           fill: Color.RED
         }
      ]
    };
 
  // whether the last finished game is won by the player
  var lastGameResult: Boolean = false;
 
  // text of game winning
  var gameResultText =
    Text {
       font: Font { size: 20
                    }
       x: MazeData.calcGridX(11)
       y: MazeData.calcGridY(11)+8
  
       content: bind if ( lastGameResult )
                       " YOU WIN "
                     else
                       "GAME OVER";
       fill: Color.RED
       visible: false;
     } ;
 
  var flashingCount: Integer = 0;
  var flashingTimeline: Timeline =
    Timeline {
       repeatCount: 5
       keyFrames : [
         KeyFrame {
            time : 500ms
            action: function() {
               gameResultText.visible = not gameResultText.visible;
               if ( ++flashingCount == 5) {
                  messageBox.visible = true;
                  waitForStart = true;
                }
             }
          }
       ]
     };
 
   var group : Group =
   Group {
      content: [
        Rectangle {
           x:0
           y:0
           width: MazeData.calcGridX(MazeData.GRID_SIZE + 2)
           height: MazeData.calcGridY(MazeData.GRID_SIZE + 3)
           fill: Color.BLACK
         },
  
        WallRectangle{ x1:0 y1:0 x2:MazeData.GRID_SIZE y2:MazeData.GRID_SIZE },
  
        WallRectangle { x1:14 y1:-0.5 x2:15 y2:4 },
        WallBlackRectangle { x1:13.8 y1:-1 x2:15.3 y2:0 },
  
        WallRectangle { x1:2 y1:2 x2:5 y2:4 },
        WallRectangle { x1:7 y1:2 x2:12 y2:4 },
        WallRectangle { x1:17 y1:2 x2:22 y2:4 },
        WallRectangle { x1:24 y1:2 x2:27 y2:4 },
        WallRectangle { x1:2 y1:6 x2:5 y2:7 },
  
        WallRectangle { x1:14 y1:6.2 x2:15 y2:10 },
        WallRectangle { x1:10 y1:6 x2:19 y2:7 },
        WallBlackLine { x1:14 y1:7 x2:15 y2:7 },
  
        WallRectangle { x1:7.5 y1:9 x2:12 y2:10 },
        WallRectangle { x1:7 y1:6 x2:8 y2:13 },
        WallBlackLine { x1:8 y1:9 x2:8 y2:10 },
  
        WallRectangle { x1:17 y1:9 x2:21.5 y2:10 },
        WallRectangle { x1:21 y1:6 x2:22 y2:13 },
        WallBlackLine { x1:21 y1:9 x2:21 y2:10 },
  
        WallRectangle { x1:24 y1:6 x2:27 y2:7 },
  
        WallRectangle { x1:-1 y1:9 x2:5 y2:13 },
        WallRectangle { x1:24 y1:9 x2:MazeData.GRID_SIZE + 1 y2:13 },
        WallBlackLine { x1:0 y1:13 x2:0 y2:15  },
        WallBlackLine { x1:MazeData.GRID_SIZE y1:13 x2:MazeData.GRID_SIZE y2:15},
  
        //cage and the gate
        WallRectangle { x1:10 y1:12 x2:19 y2:17 },
        WallRectangle { x1:10.5 y1:12.5 x2:18.5 y2:16.5 },
        Rectangle {
           x: MazeData.calcGridX(13)
           width: MazeData.GRID_GAP * 3
           y: MazeData.calcGridY(12)
           height: MazeData.GRID_GAP / 2
           stroke: Color.GREY
           fill: Color.GREY
         },
  
        WallRectangle { x1:7.5 y1:19 x2:12 y2:20 },
        WallRectangle { x1:7 y1:15 x2:8 y2:23 },
        WallBlackLine { x1:8 y1:19 x2:8 y2:20 },
  
        WallRectangle { x1:17 y1:19 x2:21.5 y2:20 },
        WallRectangle { x1:21 y1:15 x2:22 y2:23 },
        WallBlackLine { x1:21 y1:19 x2:21 y2:20 },
  
        WallRectangle { x1:14 y1:19 x2:15 y2:27 },
        WallRectangle { x1:10 y1:22 x2:19 y2:23 },
        WallBlackLine { x1:14 y1:22 x2:15 y2:22 },
        WallBlackLine { x1:14 y1:23 x2:15 y2:23 },
  
        WallRectangle { x1:2 y1:25 x2:5 y2:27 },
        WallRectangle { x1:17 y1:25 x2:22 y2:27 },
  
        WallRectangle { x1:7 y1:25 x2:12 y2:27 },
        WallRectangle { x1:24 y1:25 x2:27 y2:27 },
  
        WallRectangle { x1:-1 y1:15 x2:5 y2:17 },
        WallRectangle { x1:4 y1:19 x2:5 y2:23 },
        WallRectangle { x1:2 y1:19 x2:4.5 y2:20 },
        WallBlackRectangle { x1:4 y1:19.05 x2:5 y2:20.2 },
        WallRectangle { x1:-1 y1:22 x2:2 y2:23 },
  
        WallRectangle { x1:24 y1:15 x2:MazeData.GRID_SIZE + 1 y2:17 },
        WallRectangle { x1:24 y1:19 x2:25 y2:23 },
        WallRectangle { x1:24.5 y1:19 x2:27 y2:20 },
        WallBlackRectangle { x1:24 y1:19.05 x2:25 y2:20.2 },
        WallRectangle { x1:27 y1:22 x2:MazeData.GRID_SIZE + 1 y2:23 },
  
        WallBlackRectangle { x1:-2 y1:8 x2:0 y2:MazeData.GRID_SIZE },
        WallBlackRectangle {
            x1:MazeData.GRID_SIZE
            y1:8
            x2:MazeData.GRID_SIZE + 2
            y2:MazeData.GRID_SIZE
         },
  
        Rectangle {
           x: MazeData.calcGridX(-0.5)
           y: MazeData.calcGridY(-0.5)
           width: (MazeData.GRID_SIZE + 1) * MazeData.GRID_GAP
           height: (MazeData.GRID_SIZE + 1) * MazeData.GRID_GAP
           strokeWidth: MazeData.GRID_STROKE
           stroke: Color.BLUE
           fill: null
           arcWidth: 12
           arcHeight: 12
         },
        Line {
           startX: MazeData.calcGridX(-0.5)
           endX: MazeData.calcGridX(-0.5)
           startY: MazeData.calcGridY(13)
           endY: MazeData.calcGridY(15)
           stroke: Color.BLACK
           strokeWidth: MazeData.GRID_STROKE + 1
         },
        Line {
           startX: MazeData.calcGridX(MazeData.GRID_SIZE + 0.5)
           endX: MazeData.calcGridX(MazeData.GRID_SIZE + 0.5)
           startY: MazeData.calcGridY(13)
           endY: MazeData.calcGridY(15)
           stroke: Color.BLACK
           strokeWidth: MazeData.GRID_STROKE + 1
         },
        Line {
           startX: MazeData.calcGridX(-0.5)
           endX: MazeData.calcGridX(0)
           startY: MazeData.calcGridY(13)
           endY: MazeData.calcGridY(13)
           stroke: Color.BLUE
           strokeWidth: MazeData.GRID_STROKE
         },
        Line {
           startX: MazeData.calcGridX(-0.5)
           endX: MazeData.calcGridX(0)
           startY: MazeData.calcGridY(15)
           endY: MazeData.calcGridY(15)
           stroke: Color.BLUE
           strokeWidth: MazeData.GRID_STROKE
         },
        Line {
           startX: MazeData.calcGridX(MazeData.GRID_SIZE + 0.5)
           endX: MazeData.calcGridX(MazeData.GRID_SIZE)
           startY: MazeData.calcGridY(13)
           endY: MazeData.calcGridY(13)
           stroke: Color.BLUE
           strokeWidth: MazeData.GRID_STROKE
         },
        Line {
           startX: MazeData.calcGridX(MazeData.GRID_SIZE + 0.5)
           endX: MazeData.calcGridX(MazeData.GRID_SIZE)
           startY: MazeData.calcGridY(15)
           endY: MazeData.calcGridY(15)
           stroke: Color.BLUE
           strokeWidth: MazeData.GRID_STROKE
         },
        Text {
           font: Font {
                  size: 20
                  }
           x: MazeData.calcGridX(0), 
           y: MazeData.calcGridY(MazeData.GRID_SIZE + 2)
           content: bind "SCORES: {pacMan.scores} "
           fill: Color.YELLOW
         },
        scoreText,
        dyingPacMan,
       livesImage,
        gameResultText,
        Text { 
           font: Font { size: 20
                        }
           x: MazeData.calcGridX(22)
           y: MazeData.calcGridY(MazeData.GRID_SIZE + 2)
           content: bind "LEVEL: {level}"
           fill: Color.YELLOW
         },
  
      ]
    }; // end Group
 
   // put dots into the maze
   postinit {
      putDotHorizontally(2,13,1);
      putDotHorizontally(16,27,1);
      putDotHorizontally(2,27,5);
      putDotHorizontally(2,27,28);
  
      putDotHorizontally(2,13,24);
      putDotHorizontally(16,27,24);
  
      putDotHorizontally(2,5,8);
      putDotHorizontally(9,13,8);
      putDotHorizontally(16,20,8);
      putDotHorizontally(24,27,8);
  
      putDotHorizontally(2,5,18);
      putDotHorizontally(9,13,21);
      putDotHorizontally(16,20,21);
      putDotHorizontally(24,27,18);
  
      putDotHorizontally(2,3,21);
      putDotHorizontally(26,27,21);
  
      putDotVertically(1,1,8);
      putDotVertically(1,18,21);
      putDotVertically(1,24,28);
  
      putDotVertically(28,1,8);
      putDotVertically(28,18,21);
      putDotVertically(28,24,28);
  
      putDotVertically(6,2,27);
      putDotVertically(23,2,27);
  
      putDotVertically(3,22,23);
      putDotVertically(9,22,23);
      putDotVertically(20,22,23);
      putDotVertically(26,22,23);
  
      putDotVertically(13,25,27);
      putDotVertically(16,25,27);
  
      putDotVertically(9,6,7);
      putDotVertically(20,6,7);
  
      putDotVertically(13,2,4);
      putDotVertically(16,2,4);
  
      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;
  
      insert messageBox into group.content;
    }
 
   public override function create(): Node {
      return group;
    } // end create()
 
   public override var onKeyPressed = function ( e: KeyEvent ) : Void {
      // wait for the player's keyboard input to start the game
      if ( waitForStart ) {
         waitForStart = false;
         startNewGame();
         return;
       }
  
      if ( e.code == KeyCode.VK_DOWN )
        pacMan.setKeyboardBuffer( pacMan.MOVE_DOWN )
      else
        if ( e.code == KeyCode.VK_UP )
          pacMan.setKeyboardBuffer( pacMan.MOVE_UP )
        else
          if ( e.code == KeyCode.VK_RIGHT )
            pacMan.setKeyboardBuffer( pacMan.MOVE_RIGHT )
          else
            if ( e.code == KeyCode.VK_LEFT )
              pacMan.setKeyboardBuffer( pacMan.MOVE_LEFT );
    }
 
 
   // create a Dot GUI object
   public function createDot( x1: Number,  y1:Number, type:Integer ): Dot {
      var d = Dot {
         x: MazeData.calcGridX(x1)
         y: MazeData.calcGridY(y1)
         dotType: type
         visible: true
         }
  
      if ( d.dotType == MazeData.MAGIC_DOT )
        d.playTimeline();
  
      // set the dot type in data model
      MazeData.setData( x1, y1, type );
  
      // set dot reference
      MazeData.setDot( x1, y1, d );
  
      return d;
    }
 
   // put dots into the maze as a horizontal line
   public function putDotHorizontally(x1: Integer, x2: Integer, y: Number ) {
  
      var dots =
      for ( x in [ x1..x2] )
      if ( MazeData.getData(x,y) == MazeData.EMPTY ) {
         var dotType: Integer;
   
         if ( (x == 28 or x == 1) and (y == 3 or y == 26) )
           dotType = MazeData.MAGIC_DOT
         else
           dotType = MazeData.NORMAL_DOT;
   
         createDot( x, y, dotType )
       }
      else   [] ;
  
      insert dots into group.content;
    }
 
   // put dots into the maze as a vertical line
   public function putDotVertically(x: Integer, y1: Integer, y2: Number ) {
  
      var dots =
      for ( y in [ y1..y2] )
      if ( MazeData.getData(x,y) == MazeData.EMPTY ) {
         var dotType: Integer;
   
         if ( (x == 28 or x == 1) and (y == 3 or y == 26) )
           dotType = MazeData.MAGIC_DOT
         else
           dotType = MazeData.NORMAL_DOT;
   
         createDot( x, y, dotType )
       }
      else  [];
  
      insert dots into group.content;
    }
 
   public function makeGhostsHollow() {
   
      ghostEatenCount = 0;
  
      for ( g in ghosts )
        g.changeToHollowGhost();
    }
 
   // determine if pacman meets a ghost
   public function hasMet(g:Ghost): Boolean {
  
      def distanceThreshold = 22;
  
      var x1 = g.imageX;
      var x2 = pacMan.imageX;
  
      var diffX = Math.abs(x1-x2);
  
      if ( diffX >= distanceThreshold ) return false;
  
      var y1 = g.imageY;
      var y2 = pacMan.imageY;
      var diffY = Math.abs(y1-y2);
  
      if ( diffY >= distanceThreshold ) return false;
  
      // calculate the distance to see if pacman touches the ghost
      if ( diffY*diffY + diffX*diffX <= distanceThreshold*distanceThreshold )
        return true;
  
      return false;
    }
 
   public function pacManMeetsGhosts() {
  
      for ( g in ghosts )
        if ( hasMet(g) )
          if ( g.isHollow ) {
             pacManEatsGhost(g);
           }
          else {
             for ( ghost in ghosts )
               ghost.stop();
   
             pacMan.stop();
   
             dyingPacMan.startAnimation(pacMan.imageX, pacMan.imageY);
             break;
           }
    }
 
   public function pacManEatsGhost(g: Ghost) {
        
      ghostEatenCount++;
  
      var s = 1;
      for ( i in [1..ghostEatenCount] ) s = s + s;
  
      pacMan.scores += s*100;
      if ( addLifeFlag and pacMan.scores >= 10000 ) {
         addLife();
       }
  
      var st = scoreText[ghostEatenCount-1];
      st.x = g.imageX - 10;
      st.y = g.imageY;
  
      g.stop();
      g.resetStatus();
      g.trapCounter = -10;
  
      st.showText();
    
    }
 
   // reset status and start a new game
   public function startNewGame() {
  
      messageBox.visible = false;
      pacMan.resetStatus();
  
      gameResultText.visible = false;
  
      if ( lastGameResult == false ) {
         level = 1;
         addLifeFlag = true;
         pacMan.scores = 0;
         pacMan.dotEatenCount = 0;
   
         livesCount = 2;
       }
      else { 
         lastGameResult = false;
         level ++;
       }
  
      for ( x in [1..MazeData.GRID_SIZE] )
        for ( y in [1..MazeData.GRID_SIZE] ) {
           var dot : Dot = MazeData.getDot( x, y ) as Dot ;
   
           if ( dot != null and not dot.visible ) 
             dot.visible = true;
         }
  
      for ( g in ghosts ) {
         g.resetStatus();
       }
    }
 
   // reset status and start a new level
   public function startNewLevel() {
  
      lastGameResult = true;
  
      pacMan.hide();
      pacMan.dotEatenCount = 0;
  
      for ( g in ghosts ) {
         g.hide();
       }
  
      flashingCount = 0;
      flashingTimeline.playFromStart();
    }
 
   // reset status and start a new life
   public function startNewLife() {
  
      // reduce a life of Pac-Man
      if ( livesCount > 0 ) {
         livesCount--;
       }
      else {
         lastGameResult = false;
         flashingCount = 0;
         flashingTimeline.playFromStart();
         return;
       }
  
      pacMan.resetStatus();
  
      for ( g in ghosts ) {
         g.resetStatus();
       }
    }
 
   public function addLife():Void {
  
      if ( addLifeFlag ) {
         livesCount ++;
         addLifeFlag = false;
       }
    }
}
 
/*
 * PacMan.fx
 *
 * Created on 2009-1-1, 11:50:58
 */

package pacman;

import javafx.scene.CustomNode;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.Node;
import javafx.scene.transform.Rotate;
import pacman.MazeData;
import pacman.MovingObject;

/**
 * @author Henry Zhang
 */

public class PacMan extends CustomNode, MovingObject {
 
   public var defaultImage: Image = Image {
      url: "{__DIR__}images/left1.png"
    };
 
   // images for animation
   def images = [
     defaultImage,
     Image {
        url: "{__DIR__}images/left2.png"
      },
     defaultImage,
     Image {
        url: "{__DIR__}images/round.png"
      }
   ];
 
   // the number of dots eaten
   public var dotEatenCount : Integer = 0;
 
   // scores of the game
   public var scores: Integer = 0;
     
   // angles of rotating the images
   def rotationDegree = [0, 90, 180, 270];
 
   // GUI image of the man
   var pacmanImage : ImageView = ImageView {
      x: bind imageX - 13
      y: bind imageY - 13
      image: bind images[currentImage]
      transforms: Rotate {
         angle: bind rotationDegree[currentDirection]
         pivotX: bind imageX
         pivotY: bind imageY
         }
     }
 
   // buffer to keep the keyboard input
   var keyboardBuffer: Integer = -1;
 
   // current direction of Pacman
   var currentDirection: Integer = MOVE_LEFT;
 
   postinit {
      imageX = MazeData.calcGridX(x);
      imageY = MazeData.calcGridX(y);
      
      xDirection = -1;
      yDirection = 0;
  
    }
 
 
   public override function create(): Node {
      return pacmanImage;
    }
 
   // moving horizontally
   public function moveHorizontally() {
  
      moveCounter++;
  
      if ( moveCounter < ANIMATION_STEP ) {
         imageX += xDirection * MOVE_SPEED;
       }
      else {
         moveCounter = 0;
         x += xDirection;
         
         imageX = MazeData.calcGridX(x);
   
         // the X coordinate of the next point in the grid
         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 // check if the character hits a wall
         if ( MazeData.getData(nextX, y) == MazeData.BLOCK ) {
            state = STOP;
          }
       }
    }
 
   // moving vertically
   public function moveVertically() {
        
      moveCounter++;
  
      if ( moveCounter < ANIMATION_STEP ) {
         imageY += yDirection * MOVE_SPEED;
       }
      else {
         moveCounter = 0;
         y += yDirection;
         imageY = MazeData.calcGridX(y);
   
         // the Y coordinate of the next point in the grid
         var nextY = yDirection + y;
   
         // check if the character hits a wall
         if ( MazeData.getData(x, nextY) == MazeData.BLOCK ) {
            state = STOP;
          }
       }
    }
 
   // turn pac-man to the right
   public function moveRight(): Void {
  
      if ( currentDirection == MOVE_RIGHT )  return;
  
      var nextX = x + 1;
  
      if ( nextX >= MazeData.GRID_SIZE)  return;
  
      if ( MazeData.getData(nextX, y) == MazeData.BLOCK )   return;
  
      xDirection = 1;
      yDirection = 0;
  
      keyboardBuffer = -1;
      currentDirection = MOVE_RIGHT;
  
      state = MOVING;
    }
 
   // turn pac-man to the left
   public function moveLeft(): Void {
  
      if ( currentDirection == MOVE_LEFT )   return;
  
      var nextX = x - 1;
  
      if ( nextX <= 1)  return;
  
      if ( MazeData.getData(nextX, y) == MazeData.BLOCK )   return;
  
      xDirection = -1;
      yDirection = 0;
  
      keyboardBuffer = -1;
      currentDirection = MOVE_LEFT;
  
      state = MOVING;
    }
 
   // turn pac-man going up
   public function moveUp(): Void {
  
      if ( currentDirection == MOVE_UP )  return;
  
      var nextY = y - 1;
  
      if ( nextY <= 1)   return;
  
      if ( MazeData.getData(x,nextY) == MazeData.BLOCK )  return;
  
      xDirection = 0;
      yDirection = -1;
  
      keyboardBuffer = -1;
      currentDirection = MOVE_UP;
  
      state = MOVING;
    }
 
   // turn pac-man going down
   public function moveDown(): Void {
  
      if ( currentDirection == MOVE_DOWN ) return;
  
      var nextY = y + 1;
  
      if ( nextY >= MazeData.GRID_SIZE )  return;
  
      if ( MazeData.getData(x,nextY) == MazeData.BLOCK )  return;
  
      xDirection = 0;
      yDirection = 1;
  
      keyboardBuffer = -1;
      currentDirection = MOVE_DOWN;
  
      state = MOVING;
    }
 
   // handle keyboard input
   public function handleKeyboardInput(): Void {
      if ( keyboardBuffer < 0)
      return;
  
      if ( keyboardBuffer == MOVE_LEFT )
        moveLeft()
      else
        if ( keyboardBuffer == MOVE_RIGHT )
          moveRight()
        else
          if ( keyboardBuffer == MOVE_UP )
            moveUp()
          else
            if ( keyboardBuffer == MOVE_DOWN )
              moveDown();
    }
 
   public function setKeyboardBuffer( k: Integer): Void {
      keyboardBuffer = k;
    }
 
   // update scores if a dot is eaten
   public function updateScores() : Void {
      if ( y != 14 or ( x > 0 and x < MazeData.GRID_SIZE ) ) {
         var dot : Dot = MazeData.getDot( x, y ) as Dot ;
   
         if ( dot != null and dot.visible ) {
            scores += 10;
            dot.visible = false;
            dotEatenCount ++;
    
            if ( scores >= 10000 ) {
               maze.addLife();
             }
    
            if ( dot.dotType == MazeData.MAGIC_DOT ) {
               maze.makeGhostsHollow();
             }
    
            // check if the player wins and should start a new level
            if ( dotEatenCount >= MazeData.DOT_TOTAL )
              maze.startNewLevel();
          }
       }
    }
 
   public function hide() {
      visible=false;
      timeline.stop();
    }
 
   // handle animation of one tick
   public override function moveOneStep() {
  
      // handle keyboard input only when pac-man is at a point of the grid
      if ( currentImage == 0 )
        handleKeyboardInput();
  
      if ( state == MOVING) {
         if ( xDirection != 0 )
           moveHorizontally();
   
         if ( yDirection != 0 )
           moveVertically();
   
         // switch to the image of the next frame
         if ( currentImage < ANIMATION_STEP - 1  )
           currentImage++
         else {
            currentImage=0;
            updateScores();
          }
       }
  
      maze.pacManMeetsGhosts();    
    }
 
   // place Pac-Man at the startup position for a new game
   public function resetStatus() {
      state = MOVING;
      currentDirection = MOVE_LEFT;
      xDirection = -1;
      yDirection = 0;
      
      keyboardBuffer = -1;
      currentImage = 0;
      moveCounter = 0;
  
      x=15;
      y=18;
  
      imageX = MazeData.calcGridX(x);
      imageY = MazeData.calcGridY(y);
  
      visible = true;
      start();
    }
}

You can click on the below picture and play the completed game now. As we mentioned in previous article, the ghosts are moving in a random fashion. This makes the game less challenging. In the next article, we will discuss a better algorithm of the ghost's moving behavior.



Download Source Code

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

Comments

31 Comments

sinh said:

It not work JavaFx 1.2.
It corresponds to JavaFx. Ver.1.2

To get Pac-Man working with JavaFX 1.2, make the following 2 changes:

1. In MovingObject.fx line 18, change the declaration of the MovingObject class from "abstract" to "mixin". So change this:

public abstract class MovingObject {

to this:

public mixin class MovingObject {


2. The code will now compile, but still won't handle key events. To fix the key inputs, in Maze.fx line 484, add a call to requestFocus(). So change this:

public override function create(): Node {
return group;
} // end create()

to this:

public override function create(): Node {
requestFocus();
return group;
} // end create()

That is what worked for me using NetBeans 6.5.1 on Mac OS X 32-bit Intel with J2SE 5.0.

Henry Zhang said:

The code in the articles are for JavaFX 1.0/1.1. So it won't work for JavaFX 1.2 without modification. JavaFX 1.2 removed multi inheritance so we need to use "mixin" instead.

Patrick, thanks for the quick fix.

Ryan said:

The code looks pretty cool. Being a video game lover, I have played so much pac man since my childhood. Now, I work for a free web dating site but sometime do squeeze in time to play tetris now and then.

I never thought of developing the game from scratch though, but involved in javascript UI, the animation ideas you have used in dying pacman can be used in moving, sliding, wiping divs around too.

anhela said:

Very nice review! I searched for some tutorials at http://www.picktorrent.com , but found nothing informative, your article helped me much!

Crash Cages said:

very cool but the question is why you want to do this rather then code a newer game ?

johny said:

I really like this alot but you need to be careful and have your Crash Cage installed correctly

I love PacMan, it's incredible how now-a-days it's still a fun game to play despite all those highly advanced 3D games!

Bookmarked :)

Oyun said:

really good and useful tutorial.very thanks

This brings back memories of when I used to program back in the 1980s but sadly my programming skills are now lacking.

Pacman - one of the all time best games that always had that intensity and excitement.

John said:

I can recall playing this game on the old Commodore Vic20. How many can remember that?

Hockey Picks

Sott said:

Great tutorial. Recently rediscovered PacMan thru an Iphone app. Such a simple game yet it produces so much game play. Many of todays games could benefit from such game play!

Keep up the good Work
Scott
Irish Cinema Site

Doug said:

The game is great and always has been. I place it on my Blackberry every day still!
Pazzo

Johny said:

If pacman was Aval on the new blackberry stom, that would be awesome!
Track Time

Andreas and Paul said:

Pac the Pac-Man! It's a great game. Thanks for write in java.

Suchmaschineneintrag ║ Linkaufbau ║ Pressebericht ║ Suchmaschinenoptimierung ║ Webdesign

gices said:

Don't understand why you need a distance threshold? When pacman touches or the ghost or vice versa, is the distance threshold not equal to zero?

Henry Zhang said:

hi gices,

Thanks for your comments.

The distance is caculated between the center of two objects. So when their borders touch each other, their centersmay not be necessary at the same location. So this thredhold should be greater than zero. For more info, you can check out JavaFX Game Applications.

JIN said:

Hello.

Can i run this code in Blue J ???

thank you.

Autoradio said:

this is so cool how you write this in javafx. This is one of these games that will never die. I will have to try this.

Arthur said:

Jesus, I loved Pacman so much! Now thanks to you everyone could build their own version.

Bob said:

really good tutorial. very useful information

Statii Beton said:

Pacman was a great game in my youth. Loving to see a solution to revive the game in JavaFX. Thanks for the guide

Pete said:

wicked .. gonna have a crack myself as just a simple PHP coder my self :)
Always a classic game and nice little bit of coding son :)

@John - Yep I can remember playing on aVIC20 :) Old Skool mate

Pete

Fred said:

Thanks for Source Code...

Fred
http://www.seoprom.de

Andrew said:

Oh, man! It brings back the memories. It's been a while but I still remember the countless hours wasted playing it! Thanks for the code.
brokerage reviews

Singapore said:

I've got played so way percent man since my childhood. Currently, I work for a corporate gift website but someday do squeeze in time to play tetris now and then.

hi3w said:

That's as great as sem~

Mike said:

Thanks for posting this code!

Hip Hop

Magnet said:

Very nice review! I searched for some tutorials at http://www.btscene.com , but found nothing informative, your article helped me much!

Audio Note said:

I never thought of developing the game from scratch though, however involved in javascript UI, the animation ideas you have used in dying pacman will be employed in moving, sliding, wiping divs around too, it's like our tube amp.

Klebefolie said:

Thanks for posting this code! Best Regards by Wall Decal

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.