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
Writing the Pac-Man Game in JavaFX - Part 4
In previous articles, we finished writing most of the code of the Pac-Man game. In article 3, we implemented a simple algorithm for the ghosts to catch the Pac-Man. The ghosts randomly decides in which direction they move. They do not chase the Pac-Man even they are very close to him. This makes the game less challenging. In fact, the behavior of the ghosts are the most tricky part of the game. According to Iwatani, the author of the original arcade game, he had designed each ghost with its own distinct personality in order to keep the game from becoming too difficult or boring to play. (More info) However, there is generally no conclusion on what behavior of the ghosts are good for the players.
When writing our game, we need to find a good algorithm to implement the behavior of the ghosts. There are a few aspects that we should consider when designing this algorithm.
First, the ghosts can go after the Pac-Man character. The "random" behavior makes the game too easy to play. Therefore, no matter how far away from the Pac-Man character, a ghost should always be able to get closer to the Pac-Man and capture him. In the arcade game, the most famous chaser is Blinky(the red ghost). Its personality is called "Shadow", which means it tails after the Pac-Man character like a shadow. Sometimes, Blinky can run after the Pac-Man for more than 10 turns. It is one of the most exciting moments for a player to shake off Blinky by making lots of turnings along the track.
Second, a ghost cannot always trace the Pac-Man character, otherwise the game will be extremely difficult to play. Once a ghost starts tracing the Pac-Man character, there should be a limit to the time of a continuous tracing. For this reason, we should have some kind of a counter or a similar mechanism to make the ghost give up tracing.
Third, we need to introduce some randomness in the algorithm. Given the same condition, a ghost may not always have the same behavior. Randomness in the behavior avoids patterns in a ghost's movement, which makes it harder for a player to guess the next move of the ghost.
Fourth, whether the ghosts have their own personality, i.e. different behavior. We can apply an individual algorithm for each ghost. The other approach is to have the same algorithm with varied parameters for each ghost. To simplify programming, we will choose the latter in our code.
Fifth, when a ghost determines its moving direction, whether it should consider the position of other ghosts. For example, two ghosts can surround the Pac-Man by going into two different tracks. It is an implementation decision of the programmer whether to have this logic in place. In our code, we do not implement this part.
We will tackle the above issues one by one. Let's revisit part of the current code(see below) of the class Maze and Ghost and explain their functionalities in more detail here. In Ghost.fx, we have two functions, moveHorizontally() and moveVertically(), to handle the moving logic of a ghost. When a ghost reaches a point of the grid, i.e. at the last frame of an animation cycle, it has a chance to decide whether to change its current moving direction or to remain unchanged. If it hits a wall or is going out of the maze, a change in the direction is necessary. If a ghost is at an intersection, it can also determine whether to make a left or right turn. The function changeDirectionXtoY(mustChange: Boolean) and changeDirectionYtoX(mustChange: Boolean) contain the logic of making a decision. When the boolean argument mustChange is set to true, it means a decision of change must be made. Otherwise, a change is optional. To choose which direction to go next, a ghost could have three possible choices: continuing on the current direction, making a left turn or making a right turn. The class MoveDecision is used to evaluate each choice and rank them with a score. Invalid choices, such as hitting a wall, are eliminated automatically by given a negative score. For those valid moves, a positive score is returned. A ghost will finally follow the choice of highest score. If there is a tie in the scores, a random pick determines the final choice. Ghost.fx
public class Ghost extends CustomNode, MovingObject{
. . . . .
// 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;
}
}
. . . . . .
}
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 key logic of making a decision lies in the function MoveDecision.evaluate(). We are going to modify this function to always force the ghost to run towards the Pac-Man. An optimized algorithm for a ghost to chase the Pac-Man is to always follow the shortest path to catch the Pac-Man. However, the complexity of such an algorithm to find a shortest path on a grid is at least of the order of n*n. This makes it almost impossible to use in a game. For this reason, we need to find a simpler algorithm.
After some thinking, I found that the distance between a ghost and the Pac-Man is a good ranking metric. The shorter the distance is, the higher the score is given to a particular choice. The advantages of using the distance as a metric are obvious. It is very simple and can be caculated easily. Besides, this algorithm makes a ghost move in the direction that has the shortest distance to the Pac-Man. To illustrate this algorithm, let's look at the below figure.

In the figure, the ghost Blinky is moving into an intersection from the right to the left. When it reaches the intersection, it has three possible choices of its next movement: to go up, to go down and to continue heading left. Going down is not a valid move because it hits the border of the maze. So we need to compare the other two options. The below table shows the computation of the distance of the two possible moves:
| Choice | X distance | Y distance | Total |
| Intersection | 3 | 10 | 13 |
| Up | 3 | 9 | 12 |
| Left | 4 | 10 | 14 |
As shown in the table, the distance from the intersection to the Pac-Man character is 13 (The distance between two adjacent dots is 1). If Blinky goes up, the distance is reduced to 12. If it heads left, the distance becomes 14. Therefore, going up seems a better choice for Blinky. In this way, Blinky should be able to get closer and closer to the Pac-Man and eventually catches him.
Of course, this simple algorithm does not take into consideration for the walls in the maze. For this reason, sometimes the calculated score does not in fact represent the shortest path. However, this inaccuracy makes the ghosts appear "stupid" in the game, which is the randomness we want to achieve in the behavior. So we are going to implement it in our code. We rewrite the class MoveDecision. When the function evaluate() calculates a score, it takes in two arguments: the reference to Pac-Man instance and whether the ghost is in a hollow state. The variable distance is used to compute the score. If the ghost is going after the Pac-Man character, the score is 500-distance, which means a shorter distance yields a higher score. If the Pac-Man is hunting the ghosts(when they are hollow), the score is caculated as 500+distance. This makes the ghosts running away from the Pac-Man.
/*
* MoveDecision.fx
*
* Created on 2009-1-28, 14:42:00
*/
package pacman;
import java.lang.Math;
import pacman.MazeData;
import pacman.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( pacMan: PacMan, isHollow: Boolean ): 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 ;
}
var distance = Math.abs( x - pacMan.x ) + Math.abs( y - pacMan.y );
if ( isHollow )
score = 500 + distance // mode to run away from Pac-Man
else
score = 500 - distance; // mode to chase Pac-Man
}
}
In the class Ghost, we modify two functions: changeDirectionXtoY() and changeDirectionYtoX() and let the ghosts always pick the direction with the highest score.
// 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(pacMan, isHollow);
goDown.evaluate(pacMan, isHollow);
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(pacMan, isHollow);
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 ) {
if ( goDown.score > goUp.score) {
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(pacMan, isHollow);
goRight.evaluate(pacMan, isHollow);
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(pacMan, isHollow);
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 ) {
if ( goRight.score > goLeft.score) {
decision = 1;
}
}
xDirection=decision;
yDirection = 0;
}
If we run the program again, wow, four ghosts are endlessly chasing the Pac-Man. The game becomes very difficult to play. So we are going to introduce some randomness into the algorithm to make it easier for the player. First, we define two variables in the Ghost class: chaseFactor and chaseCount. The chaseFactor is a probability for a ghost to chase the Pac-Man. The chaseCount is the continuous period of time that a ghost can go after the Pac-Man character. Its initial value is a random number. Every time a ghost chase the Pac-Man, the value of chaseCount is decreased by 1. If chaseCount reaches 0, we let the ghost have a random move. By choosing the proper values of these variables, we can adjust the probabilty that a ghost runs after the Pac-Man character. The corresponding code of this revised algorithm is shown below: Ghost.fx
public class Ghost extends CustomNode, MovingObject{
. . . . . .
// variables to determine if a ghost should chase pacman,
// and the probability
public var changeFactor = 0.75;
public var chaseFactor = 0.5;
public var chaseCount = 0;
// 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(pacMan, isHollow);
goDown.evaluate(pacMan, isHollow);
if ( goUp.score < 0 and goDown.score < 0 )
return; // no change of direction
if ( Math.random() < chaseFactor and chaseCount == 0 )
chaseCount += Math.random() * 10 + 3;
var continueGo = MoveDecision {
x: this.x + xDirection
y: this.y
};
continueGo.evaluate(pacMan, isHollow);
if ( continueGo.score > 0 and continueGo.score > goUp.score
and continueGo.score > goDown.score and chaseCount>0) {
chaseCount--;
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 ) {
if ( chaseCount > 0 ) {
if ( goDown.score > goUp.score) {
decision = 1;
chaseCount -- ;
}
}
else {
// 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(pacMan, isHollow);
goRight.evaluate(pacMan, isHollow);
if ( goLeft.score < 0 and goRight.score < 0 ) {
return; // no change of direction
}
if ( Math.random() < chaseFactor and chaseCount == 0 )
chaseCount += Math.random() * 10 + 3;
var continueGo = MoveDecision {
x: this.x
y: this.y + yDirection
};
continueGo.evaluate(pacMan, isHollow);
if ( continueGo.score > 0 and continueGo.score > goLeft.score
and continueGo.score > goRight.score and chaseCount>0 ) {
chaseCount --;
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 ) {
if ( chaseCount > 0 ) {
if ( goRight.score > goLeft.score) {
decision = 1;
chaseCount--;
}
}
else { // random pick
if ( Math.random() > 0.5 )
decision = 1;
}
}
xDirection=decision;
yDirection = 0;
}
. . . . . .
}
Now, we have a better behavior of the ghosts and the game becomes much more interesting. Though our algorithm may not be optimal, we can control the difficulty of the game based on the level. For example, as the level goes up, we can increase the chaseFactor to let the ghosts chase the Pac-Man more frequently. Personality of ghosts can also be achieved by assigning different sets of values to the parameters of the algorithm.
Conclusions
We have walked through the process of building the Pac-Man game in JavaFX. We explored some of the key features of the JavaFX language, such as binding, animation, transformation, Java class integration and multiple inheritant. In general, these features reduces our coding effort in building rich GUI application.
To celebrate our hard work, let's play the completed game by clicking the below button:






Facebook Application Development
This was posted on oreilly Radar and describes how the ghosts move in pac man. Its quite interesting.
http://home.comcast.net/~jpittman2/pacman/pacmandossier.html
Found here:
http://radar.oreilly.com/2009/02/four-short-links-20-feb-2009.html
Thanks for your comments. The design of a more sophisicated chasing algorithm can borrow some ideas from that site.
The latest code had been updated for JavaFX 1.2. Please check out the download page on http://www.javafxgame.com
It worked nicely on my Mac. There’s no sound though.
Great job showcasing what JavaFX can do. I’ve been looking forward to demos of such quality ever since JavaFX 1.0 was released.
So I just spent awhile playing the pacman game . One thing I like about this java version was that the gameplay was much cleaner. When I played the arcade version the old controls often don't pick up when I would make a turn. This gameplay seemed much smoother by comparison. Since the basics are done it would be cool to add some new options. Maybe you could plug in graphics (like your bosses face) for the ghost. It could keep all the old rules. It would just be a way to spice it up. Jill Houston Real Estate Blog
After the release of JavaFX 1.0, I wrote a Pac-Man game using the JavaFX API. Many people were quite interested in the game. They either enjoyed playing it or asked me for the details fo the JavaFX code. JavaFX expert Jim Weaver invited me to write some articles about the process of building the game. After a few weeks’ hard work, with Jim’s constructive ideas and great help in proofreading, I finished the articles. Now they are published on insideRIA.com, an O’Reilly’s web site, as featured articles. business opportunity There will be 5 articles in total and they will run through the coming 5 weeks.
I still don't get why ever javafx example is a webstart app.
Excuse me I have some problem. I can't see my pacman, but when
pacman is eaten I can see it. How can I solve this problem.
thank you.
Very nice