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:
Type | Value | Explanation |
BLOCK | 1 | The 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:
|
|
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:
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:
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:
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.
www.insideria.com
Hi, Great.. Tutorial is just awesome..It is really helpful for a newbie like me.
ReplyDeleteI am a regular follower of your blog. Really very informative post you shared here. Kindly keep blogging.
If anyone wants to become a Java developer learn from Java EE Online Training from India.
or learn thru Java EE Online Training from India .
Nowadays Java has tons of job opportunities on various vertical industry.