Sunday, August 2, 2009

Learn Apache mod_rewrite: 13 Real-world Examples

Apache's low-cost, powerful set of features make it the server of choice for organizations around the world. One of its most valuable treasures is the mod_rewrite module, the purpose of which is to rewrite a visitor's request URI in the manner specified by a set of rules.

Writing the Pac-Man Game in JavaFX - Part 3

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

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

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

Animation of Ghosts

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

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

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


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


package pacman;

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

/**
* @author Henry Zhang
*/


public class Ghost extends CustomNode, MovingObject{

public def TRAPPED=10;

// the pacman character
public var pacMan: PacMan;

public var hollowImage1 = Image {
url: "{__DIR__}images/ghosthollow2.png"
}
public var hollowImage2 = Image {
url: "{__DIR__}images/ghosthollow3.png"
}
public var hollowImage3 = Image {
url: "{__DIR__}images/ghosthollow1.png"
}

// images for ghosts when they become hollow
public var hollowImg =
[ hollowImage1,
hollowImage2,
hollowImage1,
hollowImage2 ];

// images for ghosts when they become hollow and flashing
public var flashHollowImg =
[ hollowImage1,
hollowImage3,
hollowImage1,
hollowImage3 ];

// time for a ghost to stay hollow
var hollowMaxTime: Integer = 80;
var hollowCounter : Integer;

// the images of animation
public var defaultImage1: Image;
public var defaultImage2: Image;

def defaultImg = [
defaultImage1,
defaultImage2,
defaultImage1,
defaultImage2,
];

// animation images
var images = defaultImg;

// initial direction and position of a ghost, used in status reset
public var initialLocationX : Number;
public var initialLocationY : Number;
public var initialDirectionX : Number;
public var initialDirectionY : Number;

// time to stay in the cage
public var trapTime: Integer;
public var trapCounter: Integer=0;

// variables to decide if ghost should chase man, and with what probability
public var changeFactor = 0.75;


// the flag is set if a ghost becomes hollow
public var isHollow: Boolean = false;

// the GUI of a ghost
var ghostNode : ImageView = ImageView {
x: bind imageX - 13
y: bind imageY - 13
image: bind images[currentImage]
}

postinit {
initialLocationX = x;
initialLocationY = y;
initialDirectionX = xDirection;
initialDirectionY = yDirection;

resetStatus();
}

// reset the status of a ghost and place it into the cage
public function resetStatus() {
x = initialLocationX;
y = initialLocationY;

xDirection = initialDirectionX;
yDirection = initialDirectionY;

isHollow = false;

moveCounter = 0;
trapCounter = 0;
currentImage = 0;

imageX = MazeData.calcGridX(x);
imageY = MazeData.calcGridY(y);

images = defaultImg;
state = TRAPPED;

timeline.keyFrames[0].time = 50ms;

visible = true;
start();
}


public function changeToHollowGhost() {
hollowCounter = 0;
isHollow = true;

// switch the animation images
images = hollowImg;

// make it moves slower
timeline.stop();
timeline.keyFrames[0].time = 140ms;
timeline.play();
}

// decide whether to change the current direction of a ghost
public function changeDirectionXtoY(mustChange: Boolean): Void {
if ( not mustChange and Math.random() > changeFactor ) {
return; // no change of direction
}

// will change to a Y direction if possible
var goUp = MoveDecision {
x: this.x
y: this.y - 1 };
var goDown = MoveDecision {
x: this.x
y: this.y + 1
};

// evaluate the moving choices to pick the best one
goUp.evaluate();
goDown.evaluate();

if ( goUp.score < class="category1">and goDown.score < class="category1">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 < decision =" 1" class="category1">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 < class="category1">and goRight.score < class="category1">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 < decision =" 1" class="category1">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 < class="category2">x=MazeData.GRID_SIZE;
imageX= MazeData.calcGridX(x);
}
else
if ( nextX > 30 and xDirection > 0) {
x=0;
imageX= MazeData.calcGridX(x);
}
}
else
if (nextX < class="category1">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 < class="category1">or nextY > MazeData.GRID_SIZE) {
changeDirectionYtoX(true);
}
else
if ( MazeData.getData(x, nextY) == MazeData.BLOCK ) {
changeDirectionYtoX(true);
}
else {
changeDirectionYtoX(false);
}
}
else {
imageY += yDirection * MOVE_SPEED;
}
}

// move the ghost horizontally in the cage
public function moveHorizontallyInCage() {

moveCounter++;

if ( moveCounter > ANIMATION_STEP - 1) {

moveCounter=0;
x += xDirection;
imageX = MazeData.calcGridX(x);

var nextX = xDirection + x;

if ( nextX < xdirection =" 0;" ydirection =" 1;" class="category1">else
if ( nextX > 17) {
xDirection = 0;
yDirection = -1;
}
}
else {
imageX += xDirection * MOVE_SPEED;
}
}

// move the ghost vertically in a cage
public function moveVerticallyInCage() {

moveCounter++;

if ( moveCounter > ANIMATION_STEP - 1) {
moveCounter=0;
y += yDirection;
imageY= MazeData.calcGridX(y) + 8;

var nextY = yDirection + y;

if ( nextY < ydirection =" 0;" xdirection =" -1;" class="category1">else
if ( nextY > 15) {
yDirection = 0;
xDirection = 1;
}
}
else {
imageY += yDirection * MOVE_SPEED;
}
}

public function hide() {
visible=false;
timeline.stop();
}

// move one tick
public override function moveOneStep() {

if ( state == MOVING or state == TRAPPED ) {
if ( xDirection != 0 ) {
if ( state == MOVING )
moveHorizontally()
else
moveHorizontallyInCage();
}
else
if ( yDirection != 0 ) {
if ( state == MOVING )
moveVertically()
else
moveVerticallyInCage();
}

if ( currentImage < class="category1">else {
currentImage=0;
if ( state == TRAPPED ) {
trapCounter++;

if ( trapCounter > trapTime and x == 14 and y == 13) {
// go out of the cage
y = 12;

xDirection = 0;
yDirection = -1;
state = MOVING;
}
}
}
}

// check to see if need to switch back to a normal status
if ( isHollow ) {

hollowCounter++;

if ( hollowCounter == hollowMaxTime - 30 )
images = flashHollowImg
else
if ( hollowCounter > hollowMaxTime ) {
isHollow = false;
images = defaultImg;

timeline.stop();
timeline.keyFrames[0].time = 50ms;
timeline.play();
}
}
}

public override function create(): Node {
return ghostNode;
}

}

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


public override function moveOneStep() {
. . . . . .

if ( state == TRAPPED ) {
trapCounter++;

if ( trapCounter > trapTime and x == 14 and y == 13) {
// go out of the cage
y = 12;

xDirection = 0;
yDirection = -1;
state = MOVING;
}
}

. . . . . .
}

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


public function changeToHollowGhost() {
hollowCounter = 0;
isHollow = true;

// switch the animation images
images = hollowImg;

// make it moves slower
timeline.stop();
timeline.keyFrames[0].time = 140ms;
timeline.play();
}

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


public override function moveOneStep() {

. . . . . .

// check to see if need to switch back to a normal status
if ( isHollow ) {

hollowCounter++;

if ( hollowCounter == hollowMaxTime - 30 )
images = flashHollowImg
else
if ( hollowCounter > hollowMaxTime ) {
isHollow = false;
images = defaultImg;

timeline.stop();
timeline.keyFrames[0].time = 50ms;
timeline.play();
}
}
}

Roaming the Maze

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


// decide whether to change the current direction of a ghost
public function changeDirectionYtoX(mustChange: Boolean): Void {

if ( not mustChange and Math.random() > changeFactor )
return; // no change of direction

// will change to a X direction if possible
var goLeft = MoveDecision {
x: this.x - 1
y: this.y
};

var goRight = MoveDecision {
x: this.x + 1
y: this.y
};

// evaluate the moving choices to pick the best one
goLeft.evaluate();
goRight.evaluate();

if ( goLeft.score < class="category1">and goRight.score < class="category1">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 < decision =" 1" class="category1">else
if ( goRight.score > 0 ) {
// random pick
if ( Math.random() > 0.5 )
decision = 1;
}

xDirection=decision;
yDirection = 0;
}

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


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


package pacman;

/**
* @author Henry Zhang
*/


public class MoveDecision {

// x and y of an intended move
public var x: Number;
public var y: Number;

public var score: Number;

// evaluate if the move is valid,
// if it is invalid, returns -1;
// if it is valid, compute its score for ranking the final decision
public function evaluate( ):Void {
if ( x < class="category1">or y < class="category1">or y >= MazeData.GRID_SIZE or x >= MazeData.GRID_SIZE){
score = -1;
return ;
}

var status = MazeData.getData(x, y);
if ( status == MazeData.BLOCK ) {
score = -1;
return ;
}

// rank it as a default score
score = 1;
}
}

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

Running the Game

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


public class Maze extends CustomNode {

. . . . .

public var ghostBlinky = Ghost {
defaultImage1: Image {
url: "{__DIR__}images/ghostred1.png"
}

defaultImage2: Image {
url: "{__DIR__}images/ghostred2.png"
}

maze: this
pacMan: pacMan
x: 17
y: 15
xDirection: 0
yDirection: -1
trapTime: 1
};

public var ghostPinky = Ghost {
defaultImage1:Image {
url: "{__DIR__}images/ghostpink1.png"
}

defaultImage2:Image {
url: "{__DIR__}images/ghostpink2.png"
}

maze: this
pacMan: pacMan
x: 12
y: 14
xDirection: 0
yDirection: 1
trapTime: 10
};

public var ghostInky = Ghost {
defaultImage1:Image {
url: "{__DIR__}images/ghostcyan1.png"
}
defaultImage2:Image {
url: "{__DIR__}images/ghostcyan2.png"
}

maze: this
pacMan: pacMan
x: 13
y: 15
xDirection: 1
yDirection: 0
trapTime: 40
};

public var ghostClyde = Ghost {
defaultImage1:Image {
url: "{__DIR__}images/ghostorange1.png"
}
defaultImage2:Image {
url: "{__DIR__}images/ghostorange2.png"
}

maze: this
pacMan: pacMan
x: 15
y: 14
xDirection: -1
yDirection: 0
trapTime: 60
};

public var ghosts = [ghostBlinky, ghostPinky, ghostInky, ghostClyde];

. . . . . .

postinit {

. . . . . .

insert pacMan into group.content;

insert ghosts into group.content;

insert WallBlackRectangle{ x1:-3, y1:13, x2:0, y2:15 } into group.content;
insert WallBlackRectangle{ x1:29, y1:13, x2:31, y2:15 } into group.content;
}

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



Download Source Code


www.insideria.com

Writing the Pac-Man Game in JavaFX - Part 2

In the last article, we designed a data model and drew a maze with dots spread into the maze. Now we are ready to create the Pac-Man character. The Pac-Man character is controlled by the game player to move around the maze. While he is moving, he keeps gobbling dots along the path. To implement the Pac-Man character, we divide the coding into a few tasks so that we can create it bit by bit:

  1. Basic animation: the Pac-Man character continually open and close mouth, but he does not move
  2. Moving animation: the Pac-Man character moves inside the maze
  3. Player Controlling: the player controls the moving direction of the Pac-Man character
  4. Gobbling dots: the Pac-Man gobbles dots

Basic Animation

Let's start from the simplest thing first. We create the basic animation of the Pac-Man character. The Pac-Man character at this phase does not move but can keep opening and closing his mouth. The javafx.animation package, part of the JavaFX API, provides the easy-to-use functionality for animation. We are going to use the Timeline class to implement the animation. During an animation, properties such as speed, shape, color and location are constantly changing to achieve the desired behavior. The Timeline class allows us to update the values of animation properties along the progression of time. The Timeline.Keyframes attribute can be used to define the order of frames. We create four pictures shown below for Pac-Man's animation:

When we keep switching the above pictures(frames) of the Pac-Man character, it generates the animation effect of opening and closing the mouth. We are going to write two classes: MovingObject.fx and PacMan.fx. The MovingObject class abstracts some common attributes that we could later use to implement the Ghost class. The PacMan class extends MovingObject to display the Pac-Man character. Here is the code:

MovingObject.fx:


/*
* MovingObject.fx
*
* Created on 2009-1-1, 11:40:49
*/


package pacman;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import pacman.Maze;
import pacman.MazeData;

/**
* @author Henry Zhang
*/


public abstract class MovingObject {

// animation frames total and movement distance
public def ANIMATION_STEP=4;
public def MOVE_SPEED = MazeData.GRID_GAP / ANIMATION_STEP;

public def MOVING = 1;
public def STOP =0;

public def MOVE_LEFT=0;
public def MOVE_UP=1;
public def MOVE_RIGHT=2;
public def MOVE_DOWN=3;

public var maze: Maze;
public var state : Integer;

public var currentImage=0;
public var moveCounter: Integer=0;

// grid coordinates
public var x: Number;
public var y: Number;

// graphical coordinates
public var imageX: Number ;
public var imageY: Number ;

public var xDirection: Number = 0;
public var yDirection: Number = 0;

public var timeline: Timeline = createTimeline();

public function stop() {
timeline.stop();
}

public function pause() {
timeline.pause();
}

public function start() {
timeline.play();
}

// animation time line, moving the pacman
public function createTimeline(): Timeline {
Timeline {
repeatCount: Timeline.INDEFINITE
keyFrames: [
KeyFrame {
time: 250ms
action: function() {
moveOneStep();
}
}
]
}
}

public abstract function moveOneStep(): Void;
}

PacMan.fx:


/*
* 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 pacman.MazeData;

/**
* @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"
}
];

// GUI image of the man
var pacmanImage : ImageView = ImageView {
x: bind imageX - 13
y: bind imageY - 13
image: bind images[currentImage]
}

postinit {
imageX = MazeData.calcGridX(x);
imageY = MazeData.calcGridX(y);

state = MOVING;
start();
}


public override function create(): Node {
return pacmanImage;
}

// handle animation of one tick
public override function moveOneStep() {

if ( state == MOVING) {

// switch to the image of the next frame
if ( currentImage < class="category1">else {
currentImage=0;
}
}
}
}

The MovingObject class defines an abstract function moveOneStep() which is called every 200 millisecond. Subclasses should implement this function to create frames of the animation. The PacMan class extends both the CustomNode and MovingObject classes. In Java, a class can implement a few interfaces. In JavaFX's grammar, there is no interface, so multiple inheritance is used here. The attribute images is a sequence containing four pictures of the animation frames. When the function moveOneStep() is invoked every 200ms, the value of the attribute pacmanImage is rotated to the next picture in the sequence. In this way, the animation of Pac-Man's opening and closing his mouth is accomplished.

Let's add in some code to the Maze class so that we can see the result of our animation. First, add a statement to create an instance of PacMan:


public class Maze extends CustomNode {

// Pac Man Character
public var pacMan : PacMan = PacMan{ maze:this x:23 y:5};

. . . .
Then in the postinit block, we put the PacMan instance into the maze:

postinit {

. . . . .

insert pacMan into group.content;
}
Now, let's run the program and you can see that the Pac-Man character keeps biting. For illustration and testing purpose, we set an interval of 200ms between frames. This is a relatively large interval and it is kind of slow for playing. We will reduce this interval a bit later as we move forward. Click on the below image to view how the program runs so far:



Animation of Pac-Man Moving

We now can make the Pac-Man character moving inside the maze. First, let me explain the purpose of a pair of variables in the MovingObject class: xDirection and yDirection. They are used to store the horizontal and vertical direction of a character. See below table:

Moving DirectionxDirection yDirection
Left-10
Right10
Up0-1
Down01

Since we have four frames for a cycle of animation, we can change the position (i.e. x or y coordinates) of the character when we update the picture of a frame. So a constant variable MOVE_SPEED, the moving speed of a character is defined in the MovingObject class and is computed as:


public def ANIMATION_STEP=4;
public def MOVE_SPEED = MazeData.GRID_GAP / ANIMATION_STEP;
During every animation clock cycle, we update the x or y coordinates of a character by a delta of MOVE_SPEED. After 4 clock cycles, the Pac-Man character moves to the next point of the grid either horizontally or vertically. Based on this algorithm, we add two functions moveHorizontally() and moveVertically() into PacMan class.

public class PacMan extends CustomNode, MovingObject {

. . . . . .

// moving horizontally
public function moveHorizontally() {

moveCounter++;

if ( moveCounter < class="category1">else {
moveCounter = 0;
x += xDirection;

imageX = MazeData.calcGridX(x);

// the X coordinate of the next point in the grid
var nextX = xDirection + x;

// check if the character hits a wall
if ( MazeData.getData(nextX, y) == MazeData.BLOCK ) {
state = STOP;
}
}
}

// moving vertically
public function moveVertically() {

moveCounter++;

if ( moveCounter < class="category1">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;
}
}
}

. . . . . .
}

The two functions are similar to each other, so let's take a look at the function moveHorizontally(). When the character's position is between two points of the grid, we use this statement to move it:


imageX += xDirection * MOVE_SPEED;

When the character reaches a point, we check whether it hits a wall of the maze. If it does, we make it stop:


// check if the character hits a wall
if ( MazeData.getData(nextX, y) == MazeData.BLOCK ) {
state = STOP;
}

Now, we can write some codes to test the moving of the Pac-man character. In PacMan class, we add in the moving code in the function moveOnetStep():


// handle animation of one tick
public override function moveOneStep() {

if ( state == MOVING) {

if ( xDirection != 0 )
moveHorizontally();

if ( yDirection != 0 )
moveVertically();

// switch to the image of the next frame
if ( currentImage < class="category1">else {
currentImage=0;
}
}
}
In the postinit block, we set the initial direction of the pac-man character:

postinit {
imageX = MazeData.calcGridX(x);
imageY = MazeData.calcGridX(y);

xDirection = -1;
yDirection = 0;

state = MOVING;
start();
}

Run the program now and we can see the Pac-Man moving to the left and stop at the border of the maze.



You can try other moving directions by changing the values of xDirection and yDirection in the postinit block. Refer to the table in previous section for possible combinations. Since we have not yet handled the part to turn the Pac-man's mouth, he always faces to the left no matter which direction he moves in.

One last thing is to deal with a special case in the Pac-man's passing through the tunnel. The Pac-man character can walk into the tunnel to reach the other side of the maze. We put in some handling in function moveHorizontally() for this purpose.


public function moveHorizontally() {

moveCounter++;

if ( moveCounter < class="category1">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 < class="category2">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;
}
}
}
In Maze.fx, we add two WallBlackRectangle objects to create the clipping effect of the Pac-man passing the tunnel.

postinit {

. . . . .

insert pacMan 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;
}

Then we can test this code by placing the Pac-Man character at the position (5,14):


public var pacMan : PacMan = PacMan{ maze:this x:5 y:14 };
So far, we completed the moving part of the Pac-man character.

Player's Controlling

Now, we start to work on the player's keyboard controlling. In PacMan class, we define two attributes:


// buffer to keep the keyboard input
var keyboardBuffer: Integer = -1;

// current direction of Pacman
var currentDirection: Integer = MOVE_LEFT;

The keyboardBuffer attribute is used to store the keyboard input(keypress event). It will be consumed when the pac-man character's position is valid for the turn. Buffered keyboard input can be overwritten by subsequent keypressed event. For this reason, an experienced player usually presses a key well before the Pac-man reaches a turning point.

The currentDirection is an attribute to determine which direction the pac-man faces to.

In PacMan.fx, we create a few functions as below. The function moveRight(), moveLeft(), moveUp() and moveDown() are to change the direction of the Pac-Man character based on keyboard events.


// 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 < class="category1">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;
}
In moveOneSteop() function, add in two lines of code to handle keyboard events during each tick of the animation:

public override function moveOneStep() {

// handle keyboard input only when pac-man is at a point of the grid
if ( currentImage==0 )
handleKeyboardInput();

. . . . . .
}

As we mentioned previously, the Pac-Man always faces to the left in our code. Let's modify our codes a bit to enable the Pac-Man to "turn" his mouth. A common approach is to use a separate set of pictures when the character is moving in a particular direction. However, we going to utilize the transformation feature of JavaFX to achieve this goal. Instead of switching to another set of pictures, we just rotate the picture of each frame to face to the correct direction. JavaFX makes it very simple to accomplish. Let's change some code in PacMan.


public class PacMan extends CustomNode, MovingObject {

. . . . .

// 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
}
}

. . . . .

The transforms attribute of the ImageView class allows us to apply the rotation we need. An instance of Rotate defines the angle and the center of the rotation. Binding is used again to automatically update the GUI of the character.

To accept keyboard events, we override the onKeyPressed attribute of the Maze class:


public override var onKeyPressed = function ( e: KeyEvent ) : Void {

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 );
}

To get better visual effect, we now reduce the interval of animation keyframes. In function MovingObject.createTimeline(), we change the time attribute to 50ms.


public function createTimeline(): Timeline {
Timeline {
repeatCount: Timeline.INDEFINITE
keyFrames: [
KeyFrame {
time: 50ms
action: function() {
moveOneStep();
}
}
]
}
}

Run the program and you can control the pac-man character's moving by arrow keys. Click the below button to view it online:

click to run

Gobbling Dots

The last part of the Pac-man's animation is gobbling the dots. We first create two attributes in the PacMan class: dotEatenCount and scores.


// the number of dots eaten
public var dotEatenCount : Integer = 0;

// scores of the game
public var scores: Integer = 0;

Then we write a function updateScores() to check if a dot is gobbled by the Pac-man character. The scores is updated accordingly.


public function updateScores() : Void {
if ( y != 14 or ( x > 0 and x < class="category1">var dot : Dot = MazeData.getDot( x, y ) as Dot ;

if ( dot != null and dot.visible ) {
scores += 10;
dot.visible = false;
dotEatenCount ++;
}
}
}

In function moveOneStep(), we invoke the updateScores() as below:


public override function moveOneStep() {

. . . . . .

if ( currentImage < class="category1">else {
currentImage=0;
updateScores();
}
. . . . . .

}

Finally, we add a Text instance as a scoreboard under the maze. It displays the scores as Pac-man eats the dots. The content of the Text instance is bound to pacMan.scores.


public class Maze extends CustomNode {

. . . . . .

var group : Group =
Group {
content: [

. . . . . .

Text {
font: Font {
size: 20
}
x: MazeData.calcGridX(0),
y: MazeData.calcGridY(MazeData.GRID_SIZE + 2)
content: bind "SCORES: {pacMan.scores} "
fill: Color.YELLOW
}
]
. . . . . .

By now, we completed the all the animation part of the Pac-man character. A player can control the pac-man character by keyboard and score by gobbling the dots. Click on the below button to run and play it.

click to run

Download Source Code


www.insideria.com

Writing the Pac-Man Game in JavaFX - Part 1

When I was young I was fascinated by arcade games. One of my favorites was the Pac-Man game. Recently, when I was learning the JavaFX language, I decided to write the game in JavaFX. Based on my experience in other programming languages, I assumed there would be some amount of work in building a game such as Pac-Man, giving me a good feel for RIA development in JavaFX.

Data Model of the Maze

Before writing the JavaFX code, it is first necessary to design the data model. A data model is a way to represent physical objects with data structures. The functional programming style of JavaFX makes it easy to bind the UI to the model. When designing a data model, I usually consider two aspects: performance and space. Performance means that the data should be accessed via an efficient approach. For example, a hash table is usually faster than a linked list when a keyword-based search is performed. Performance is an important consideration for games that are constantly taking a player's input and updating graphical objects. Games like Pac-Man or Space Invaders fall into this category. The other design consideration for a data model is memory space. I still remember the time when I was programming on an APPLE II with only 48KB RAM. Much effort was spent on minimizing memory consumption. Fortunately, our Pac-Man game is not data-intensive, so I wasn't very concerned about the space issue.

Keeping the above analysis in mind, we will now start building the data model. We can treat the board as an NxN grid. The wall of the maze can be drawn by lines connecting the points of the grid. Naturally, a 2-dimensional array is the best way to model this grid. Each point of the grid may have one of the following four types:

TypeValueExplanation
BLOCK1The point is part of a "wall" of the maze
NORMAL_DOT 2 The point contains a normal dot
MAGIC_DOT 3 The point contains a magic dot
EMPTY 0 The point does not have any of the above objects at it

For example, the picture shown below is the upper-left corner of the maze, and the corresponding data in the array is shown on the right:



0123456
01111111
11222222
21211112
31310012
41211112
51222222
61211112
71211112
81222222
91111112

This data model will be accessed very frequently when the Pac-Man character and ghosts are moving inside the maze, therefore, an efficient data structure should be used. A 2-dimensional (2D) array is a good choice because its access time is almost a constant. Though it is common in modern programming languages to have 2D arrays, JavaFX sequences (an array-like structure in JavaFX) are single dimensional. For this reason, I decided to use a Java class to hold this array. I created a few methods for accessing the model or converting the grid data into drawing coordinates. Note that the ability to leverage Java from within JavaFX is one of the very powerful features of JavaFX.


package pacman;

/**
* MazeData.java
*
* @author Henry Zhang
*
* a 2D array for data model of the maze
*
*/


public class MazeData {

public final static int GRID_SIZE = 29;
public final static int EMPTY = 0;
public final static int BLOCK = 1;
public final static int NORMAL_DOT = 2;
public final static int MAGIC_DOT = 3;

public static int DOT_TOTAL = 0;
public static int mazeData[][] = new int[GRID_SIZE + 1][GRID_SIZE + 1];

public final static int GRID_GAP = 16;
public final static int GRID_STROKE = 2;
final static int xoffset = GRID_GAP * 2;
final static int yoffset = GRID_GAP * 2;

public static int makeInRange(int a) {
if (a < a =" 0;" class="category1">else if (a > GRID_SIZE) {
a = GRID_SIZE;
}

return a;
}

// set the grid of maze data to be BLOCK
public static void setBlockMazeData(int x1, int y1, int x2, int y2) {
x1 = makeInRange(x1);
y1 = makeInRange(y1);
x2 = makeInRange(x2);
y2 = makeInRange(y2);

for (int i = x1; i <= x2; i++) { mazeData[i][y1] = BLOCK; mazeData[i][y2] = BLOCK; } for (int i = y1; i <= y2; i++) { mazeData[x1][i] = BLOCK; mazeData[x2][i] = BLOCK; } } public static double calcGridX(double x) {
return GRID_GAP * x + xoffset;
}

public static double calcGridY(double y) {
return GRID_GAP * y + yoffset;
}

public static int getData(int x, int y) {
return mazeData[x][y];
}

public static void setData(int x, int y, int value) {
mazeData[x][y] = value;

if ((value == MAGIC_DOT) || (value == NORMAL_DOT)) {
DOT_TOTAL++;
}
} // end setData
}

Drawing the Maze

Once we have the data model of the maze, we can start drawing the maze based on this model. There are basically two approaches for drawing the maze. One approach is to draw the maze directly with JavaFX code. Another approach is use an image file such as a PNG or JPG. The image could then be used as a background picture in our Pac-Man game. I choose the first approach because it is easier to link the GUI to our data model.

JavaFX provides some standard APIs for basic shapes such as lines, circles and rectangles. We can use Line and Rectangle classes as building blocks for most parts of the maze. First, we write a class named WallRectangle to draw the walls of the maze. In the code shown below, (x1,y1) and (x2,y2) are the coordinates of the upper-left and bottom-right corner of a rectangle. Here is the code:


/*
* WallRectangle.fx
*
* Created on 2008-12-25, 16:08:28
*/


package pacman;

import javafx.scene.CustomNode;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import pacman.MazeData;

/**
* @author Henry Zhang
*/


public class WallRectangle extends CustomNode {

public var x1: Number;
public var y1: Number;
public var x2: Number;
public var y2: Number;

public override function create(): Node {
Rectangle {
x: MazeData.calcGridX(x1)
y: MazeData.calcGridY(y1)
width: MazeData.calcGridX(x2) - MazeData.calcGridX(x1)
height: MazeData.calcGridY(y2) - MazeData.calcGridY(y1)
strokeWidth: MazeData.GRID_STROKE
stroke: Color.BLUE
arcWidth: 12
arcHeight: 12
}
}
}

Next, we start to work on the Maze class. The Maze class extends CustomNode class and overrides the create() function. In the create() function, we place WallRectangle and Line instances to construct the maze. Here is the source code of the Maze class:


/*
* 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;

/**
* @author Henry Zhang
*/


public class Maze extends CustomNode {

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 },

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 },

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 },

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 },

//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 },

WallRectangle { x1:17 y1:19 x2:21.5 y2:20 },
WallRectangle { x1:21 y1:15 x2:22 y2:23 },

WallRectangle { x1:14 y1:19 x2:15 y2:27 },
WallRectangle { x1:10 y1:22 x2:19 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 },
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 },
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
},

]
}; // end Group

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

}

Now we'll write a Main class to put the maze onto the stage and display it:


/*
* Main.fx
*
* Created on 2008-12-20, 12:02:26
*/


package pacman;

import javafx.scene.Scene;
import javafx.stage.Stage;

/**
* @author Henry Zhang
*/


Stage{
title: "PACMAN"
width: MazeData.calcGridX(MazeData.GRID_SIZE + 2)
height: MazeData.calcGridY(MazeData.GRID_SIZE + 5)
scene: Scene{
content: [ Maze {}
]
}
}

Run the program and we have the first version of our maze:



Launch

The maze is almost done except that some lines are overlapping each other. This is not a problem, because we can put some black lines and rectangles to hide those overlapping areas so that the maze looks nice. Let's write two classes, WallBlackRectangle and WallBlackLine, for this purpose. The WallBlackRectangle class covers a rectangular area. The WallBlackLine class draws a black line in the maze. In the previous section, our WallRectangle class extends the CustomNode class. We do the same thing for the WallBlackRectangle here. The WallBlackLine class demonstrates another way to achieve the same functionality. We subclass the JavaFX Line class and use a postinit block (which is invoked upon instantiation after the instance variables have been assigned values) to change the details of the Line object.


/*
* WallBlackRectangle.fx
*
* Created on 2008-12-27, 16:35:42
*/


package pacman;

import javafx.scene.CustomNode;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import pacman.MazeData;

/**
* @author Henry Zhang
*/


public class WallBlackRectangle extends CustomNode {

public var x1: Number;
public var y1: Number;
public var x2: Number;
public var y2: Number;

public override function create(): Node {
Rectangle {
x: MazeData.calcGridX(x1) + MazeData.GRID_STROKE
y: MazeData.calcGridY(y1) + MazeData.GRID_STROKE
width: MazeData.GRID_GAP * (x2-x1) - MazeData.GRID_STROKE * 2
height: MazeData.GRID_GAP * (y2-y1) - MazeData.GRID_STROKE * 2
strokeWidth: MazeData.GRID_STROKE
stroke: Color.BLACK
arcWidth: 3
arcHeight: 3
}
}
}

/*
* WallBlackLine.fx
*
* Created on 2008-12-27, 17:52:58
*/


package pacman;

import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import pacman.MazeData;

/**
* @author Henry Zhang
*/


public class WallBlackLine extends Line {

public var x1: Number;
public var y1: Number;
public var x2: Number;
public var y2: Number;

postinit {
strokeWidth = MazeData.GRID_STROKE + 1;
stroke = Color.BLACK;

if ( x1 == x2 ) { // vertically line
startX = MazeData.calcGridX(x1);
startY = MazeData.calcGridY(y1) + MazeData.GRID_STROKE;
endX = MazeData.calcGridX(x2);
endY = MazeData.calcGridY(y2) - MazeData.GRID_STROKE;
}
else { // horizontal line
startX = MazeData.calcGridX(x1) + MazeData.GRID_STROKE;
startY = MazeData.calcGridY(y1);
endX = MazeData.calcGridX(x2) - MazeData.GRID_STROKE;
endY = MazeData.calcGridY(y2);
}
} // end postinit
}

In the Maze class, we put in some instances of the above classes into the group variable:


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
},

]
}; // end Group

After these adjustments, we have the Pac-Man maze shown below:



Launch

Before moving forward to the next step, I would like to initialize the data model as we draw the maze, i.e. to set the points related to a wall to a value of BLOCK. One of the benefits of doing so is that the data model is always in sync with the GUI. If you want to modify the layout of the maze later, you can just change the drawing code and the data model is adjusted automatically. This can be achieved by adding a postinit block into the WallRectangle class:


public class WallRectangle extends CustomNode {

. . . . . .

postinit {
// initialize the data model while drawing the maze
MazeData.setBlockMazeData(x1, y1, x2, y2);
}

. . . . . .

}

The method MazeData.setBlockMazeData(x1,y1,x2,y2) updates the data model by setting all of the points of a rectangle to the value BLOCK. The values x1, y1 and x2, y2 are the coordinates of the rectangle's two corners.

Drawing the Dots

Now that the maze is drawn, let's work on the dots. There are two types of dots in the game: normal dots and magic dots. The magic dots are bigger in size and they continually flash. If the Pac-Man character gobbles the magic dots, he has the power to eat ghosts for a short period of time. Our Dot class extends the CustomNode class, a nd adds functionality to achieve the desired behavior. Take a look at the source code in Dot.fx, shown below:


/*
* Dot.fx
*
* Created on 2008-12-21, 21:59:45
*/


package pacman;

/**
* @author Henry Zhang
*/


import java.lang.Math;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.CustomNode;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;

public class Dot extends CustomNode {

public var dotType: Integer;

// location of the dot
public var x : Number ;
public var y : Number ;

// radius of the dot
public var r: Number =
if ( dotType == MazeData.MAGIC_DOT ) 5 else 1;

// the dot
var circle = Circle{
centerX: x
centerY: y
radius: bind r
fill: Color.YELLOW
visible: bind visible // bind to Dot.visible
} ;

// variables for magic dot's growing/shrinking animation
public var animationRadius: Number = 3;
public var delta: Number = -1;
var timeline: Timeline;

// create the animation timeline for magic dot
public function createTimeline(): Timeline {
Timeline {
repeatCount: Timeline.INDEFINITE
keyFrames: [
KeyFrame {
time: 250ms
action: function() {
doOneTick();
}
}
]
}
}

public function playTimeline() {
if ( timeline == null )
timeline = createTimeline();

timeline.play();
}

// do the animation
public function doOneTick () {

if ( visible == false )
return;

animationRadius += delta;
var x = Math.abs(animationRadius) + 3;

if ( x > 5 ) {
delta = -delta;
}

r = x;
}

public override function create(): Node {
return circle;
}
}

The Circle class is used to display the dots. For a magic dot, we continually change its radius to create the blinking effect. A Timeline instance generates an animation frame every 250ms. The doOneTick() function is invoked each time to adjust the value of the radius. Binding, an important feature of JavaFX, is used in the Circle object to link it with the data model. During an animation process, there is no need to manually update the GUI object because its radius is bound to the model.

Now that the Dot class is ready, we can put dots into the maze. To accomplish this, we'll add the three functions shown below to the Maze class:


// 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( x, y, dotType ) ;

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;
}

The createDot() function creates a Dot object based on its coordinates (x,y) and its type (NORMAL_DOT or MAGIC_DOT). Again, while we are creating the dots, we bind the dot status to our data model with the following statement:

  // set the dot type to data model
MazeData.setData( x, y, dotType ) ;

The putDotHorizontally() function places a horizontal line of dots into the maze and makes four of them the magic dots.

The putDotVertically() function is almost the same as putDotHorizontally() except that it puts dots in a vertical fashion.

Last thing is to put all the dots into the maze. We add some code to the postinit block of the Maze class:


public class Maze extends CustomNode {

. . . . . .

// 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);
}
. . . . .
}

If you'd like to see the result so far, run the program and you'll get a maze populated with dots, four of which are the flashing magic dots:



Launch

Building an Index of Dot References

In preparation for the next phase,we need to do one more thing. During the game, we need a fast way to get the reference of a Dot object at point (x, y). Our current code does not support an efficient reference. So in MazeData.java, we define a 2D array dotPointers to store the references to these dots. Two accessor methods are added as well, using the Object type to store the references to JavaFX Dot instances.


public class MazeData {

. . . . . .

static Object dotPointers[][] = new Object[GRID_SIZE + 1][GRID_SIZE + 1];

. . . . . .

public static Object getDot(int x, int y) {
return dotPointers[x][y];
}

public static void setDot(int x, int y, Object dot) {
dotPointers[x][y] = dot;
}

. . . . . .
}

In Maze.createDot(), we add a line to update the pointer in the data model:


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;
}

Congratulations! You've completed the first phase of the Pac-Man game in which a maze and its dots are drawn. In subsequent articles, we will introduce the Pac-Man character and ghosts.

Download Source Code


www.insideria.com