Home >
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:
- Pac-Man eats a ghost after he gobbles a magic dot.
- 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;
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.
- Validation in Flex with Hamcrest-AS3
- Using UDP socket connections for low-latency and loss-tolerant scenarios in AIR 2 (Part 3)
- Using UDP socket connections for low-latency and loss-tolerant scenarios in AIR 2 (Part 2)
- Using UDP socket connections for low-latency and loss-tolerant scenarios in AIR 2 (Part 1)
- The Bluffer's Guide to JavaFX, part 2







Facebook Application Development
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.
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.
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.
Very nice review! I searched for some tutorials at http://www.picktorrent.com , but found nothing informative, your article helped me much!
very cool but the question is why you want to do this rather then code a newer game ?
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 :)
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.
I can recall playing this game on the old Commodore Vic20. How many can remember that?
Hockey Picks
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
The game is great and always has been. I place it on my Blackberry every day still!
Pazzo
If pacman was Aval on the new blackberry stom, that would be awesome!
Track Time
Pac the Pac-Man! It's a great game. Thanks for write in java.
Suchmaschineneintrag ║ Linkaufbau ║ Pressebericht ║ Suchmaschinenoptimierung ║ Webdesign
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?
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.
Hello.
Can i run this code in Blue J ???
thank you.
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.
Jesus, I loved Pacman so much! Now thanks to you everyone could build their own version.
really good tutorial. very useful information
Pacman was a great game in my youth. Loving to see a solution to revive the game in JavaFX. Thanks for the guide
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
Thanks for Source Code...
Fred
http://www.seoprom.de
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
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.
That's as great as sem~
Thanks for posting this code!
Hip Hop
Very nice review! I searched for some tutorials at http://www.btscene.com , but found nothing informative, your article helped me much!
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.
Thanks for posting this code! Best Regards by Wall Decal