Subpages:
1. Java2D API overview
2. Rendering charts
3. Rendering text strings
4. Rendering images
23.4 Rendering images
In this section we'll demonstrate the advantages of using Java2D for rendering images and having some fun. The following example is a simple implementation of the well-known Pac-man arcade game. We have designed it such that creating custom levels is very simple, and customizing the appearance is only a matter of building new 20x20 images (you can also change the size of the game's cells to accommodate images of different dimensions). Though there are no monsters, level changes, or sounds, these features are ready and waiting to be implemented by the inspired Pac-man enthusiast. We hope to provide you with a solid base to start from. Otherwise, if you've made it this far through the book without skipping any material, you surely deserve a Pac-man break.

Figure 23.5 Game2D with Pac-man in action.
<<figure23-5.gif>>
The Code: Game2D.java
see \Chapter23\3
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
public class Game2D extends JFrame
{
public Game2D() {
super("2D Game");
getContentPane().setLayout(new BorderLayout());
PacMan2D field = new PacMan2D(this);
getContentPane().add(field, BorderLayout.CENTER);
WindowListener wndCloser = new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
};
addWindowListener(wndCloser);
pack(); // no pun intended
setResizable(false);
setVisible(true);
}
public static void main(String argv[]) { new Game2D(); }
}
class PacMan2D extends JPanel
{
static final int N_IMG = 4;
static final int X_CELL = 20;
static final int Y_CELL = 20;
static final int N_STEP = 10;
static final double STEP = 0.1f;
protected int m_nx;
protected int m_ny;
protected int[][] m_flags;
protected double m_creatureX = 0;
protected double m_creatureY = 0;
protected int m_posX = 0;
protected int m_posY = 0;
protected int m_creatureVx = 1;
protected int m_creatureVy = 0;
protected int m_creatureDir = 0;
protected int m_creatureInd = 0;
protected int m_creatureNewVx = 1;
protected int m_creatureNewVy = 0;
protected int m_creatureNewDir = 0;
protected String[] m_data = {
"00000000000000000000", // 0
"00110000001000000000", // 1
"00010000001000000000", // 2
"00000000001011111100", // 3
"00110000010000111111", // 4
"00001000000000000000", // 5
"00000100001100011000", // 6
"00010000001000000011", // 7
"00000000111011110010", // 8
"01000100001000010010", // 9
"00000000000000000000", // 10
"00000010001111100100", // 11
"00011010000000000100", // 12
"00000000011111000100", // 13
"00001111100000000110", // 14
"00000000000000000010", // 15
"00000000001111000010", // 16
"00000000000001111010", // 17
"00011100000000011100", // 18
"00000111100000000000" }; // 19
protected Image m_wallImage;
protected Image m_ballImage;
protected Image[][] m_creature;
protected Thread m_runner;
protected JFrame m_parent;
public PacMan2D(JFrame parent) {
setBackground(Color.black);
m_parent = parent;
AffineTransform[] at = new AffineTransform[3];
at[0] = new AffineTransform(0, 1, -1, 0, Y_CELL, 0);
at[1] = new AffineTransform(-1, 0, 0, 1, X_CELL, 0);
at[2] = new AffineTransform(0, -1, -1, 0, Y_CELL, X_CELL);
ImageIcon icon = new ImageIcon("wall.gif");
m_wallImage = icon.getImage();
icon = new ImageIcon("ball.gif");
m_ballImage = icon.getImage();
m_creature = new Image[N_IMG][4];
for (int k=0; k<N_IMG; k++) {
int kk = k + 1;
icon = new ImageIcon("creature"+kk+".gif");
m_creature[k][0] = icon.getImage();
for (int d=0; d<3; d++) {
BufferedImage bi = new BufferedImage(X_CELL, Y_CELL,
BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = bi.createGraphics();
g2.drawImage(m_creature[k][0], at[d], this);
m_creature[k][d+1] = bi;
}
}
m_nx = m_data[0].length();
m_ny = m_data.length;
m_flags = new int[m_ny][m_nx];
for (int i=0; i<m_ny; i++)
for (int j=0; j<m_nx; j++)
m_flags[i][j] = (m_data[i].charAt(j)=='0' ? 0 : 1);
m_runner = new Thread() {
public void run() {
m_flags[m_posY][m_posX] = -1;
while (!m_parent.isShowing())
try { sleep(150); }
catch (InterruptedException ex) { return; }
while (true) {
m_creatureVx = m_creatureNewVx;
m_creatureVy = m_creatureNewVy;
m_creatureDir = m_creatureNewDir;
int j = m_posX+m_creatureVx;
int i = m_posY+m_creatureVy;
if (j >=0 && j < m_nx && i >= 0 && i < m_ny &&
m_flags[i][j] != 1) {
for (int k=0; k<N_STEP; k++) {
m_creatureX += STEP*m_creatureVx;
m_creatureY += STEP*m_creatureVy;
m_creatureInd++;
m_creatureInd = m_creatureInd % N_IMG;
final int x = (int)(m_creatureX*X_CELL);
final int y = (int)(m_creatureY*Y_CELL);
Runnable painter = new Runnable() {
public void run() {
PacMan2D.this.paintImmediately(
x-1, y-1, X_CELL+3, Y_CELL+3);
}
};
try {
SwingUtilities.invokeAndWait(painter);
} catch (Exception e) {}
try { sleep(40); }
catch (InterruptedException ex) { break; }
}
if (m_flags[i][j] == 0)
m_flags[i][j] = -1;
m_posX += m_creatureVx;
m_posY += m_creatureVy;
m_creatureX = m_posX;
m_creatureY = m_posY;
}
else
try { sleep(150); }
catch (InterruptedException ex) { break; }
}
}
};
m_runner.start();
KeyAdapter lst = new KeyAdapter() {
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_RIGHT:
m_creatureNewVx = 1;
m_creatureNewVy = 0;
m_creatureNewDir = 0;
break;
case KeyEvent.VK_DOWN:
m_creatureNewVx = 0;
m_creatureNewVy = 1;
m_creatureNewDir = 1;
break;
case KeyEvent.VK_LEFT:
m_creatureNewVx = -1;
m_creatureNewVy = 0;
m_creatureNewDir = 2;
break;
case KeyEvent.VK_UP:
m_creatureNewVx = 0;
m_creatureNewVy = -1;
m_creatureNewDir = 3;
break;
}
}
};
parent.addKeyListener(lst);
}
public Dimension getPreferredSize() {
return new Dimension(m_nx*X_CELL, m_ny*Y_CELL);
}
public Dimension getMaximumSize() {
return getPreferredSize();
}
public Dimension getMinimumSize() {
return getPreferredSize();
}
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
g2.setColor(getBackground());
g2.fill(g.getClip());
int x, y;
for (int i=0; i<m_ny; i++)
for (int j=0; j<m_nx; j++) {
x = j*X_CELL;
y = i*Y_CELL;
if (m_flags[i][j] == 1)
g2.drawImage(m_wallImage, x, y, this);
else if (m_flags[i][j] == 0)
g2.drawImage(m_ballImage, x, y, this);
}
x = (int)(m_creatureX*X_CELL);
y = (int)(m_creatureY*Y_CELL);
g2.drawImage(
m_creature[m_creatureInd][m_creatureDir], x, y, this);
}
}
Understanding the Code
Class Game2D
This class merely creates a JFrame and places an instance of our PacMan2D component in it.
Class PacMan2D
This component represents a simple Pac-man implementation. Several class constants are defined:
int N_IMG: number of slides to produce animation of moving Pac-man.
int X_CELL: horizontal size of a cell in the arcade.
int Y_CELL: vertical size of a cell in the arcade.
int N_STEP: number of steps used to produce smooth movement from one cell to another.
double STEP: the length of one step (in terms of cells).
Instance variables:
int m_nx: number of cell columns in a level.
int m_ny: number of cell rows in a level.
int[][] m_flags: two-dimensional array describing the current state of each cell.
double m_creatureX: current x-coordinate of Pac-man in terms of cells. May be fractional because of smooth motion.
double m_creatureY: current y-coordinate of Pac-man in terms of cells.
int m_posX: current x-coordinate of the cell in which Pac-man resides.
int m_posY: current y-coordinate of the cell in which Pac-man resides.
int m_creatureVx: current x-component of Pac-man's velocity (may be 1, 0, or -1).
int m_creatureVy: current y-component of Pac-man's velocity (may be 1, 0, or -1).
int m_creatureDir: current direction of Pac-man's motion (0-east, 1-south, 2-west, 3-north). Used for quick selection of the proper image from our m_creature 2D array (see below).
int m_creatureInd: the index of the current slide in Pac-man's animation.
int m_creatureNewVx: a new value for m_creatureVx (assigned by the player via keyboard, but not yet accepted by the game).
int m_creatureNewVy: a new value for m_creatureVy.
int m_creatureNewDir: a new value for m_creatureDir.
String[] m_data: an array of Strings which determine the location of walls in a level.
Image m_wallImage: an image of a wall occupying one cell.
Image m_ballImage: an image of a pellet (to be eaten by Pac-man) occupying one cell.
Image[][] m_creature: 2D array of Pac-man images.
Thread m_runner: the thread which runs this game.
The constructor of PacMan2D performs all necessary initialization. First we create three AffineTransforms to be used for flipping our Pac-man images:
AffineTransform[] at = new AffineTransform[3];
at[0] = new AffineTransform(0, 1, -1, 0, Y_CELL, 0);
at[1] = new AffineTransform(-1, 0, 0, 1, X_CELL, 0);
at[2] = new AffineTransform(0, -1, -1, 0, Y_CELL, X_CELL);
Then, we read in our wall and ball images, and create a 2D array of Pac-man's images. The first row in this array is filled with animated slide images of Pac-man read from four prepared image files. Each of these images represents Pac-man facing east in one of his chomping positions. The next three rows are filled with the same images, but each are transformed to face south (second row), west (third row), and north (fourth row). These flipped images are created in three steps:
Create an empty BufferedImage instance the size of the original image.
Retrieve its Graphics2D context to draw into that image.
Use the overloaded drawImage() method to use an AffineTransform instance (prepared above) as a parameter and render the transformed image.
Each resulting image is stored in our m_creature array.
The configuration of the game's level is encoded in the m_data String array: '0' characters correspond to pellets, '1' characters correspond to walls. This information is then parsed and stored in the m_flags array. Thus, the size of the m_data array also determines the size of the level (the product of m_nx and m_ny).
Note: The m_data String array can be easily modified to produce a new level. We also can easily modify the program to read this information from an external file. These features would be natural enhancements to make if we were to expand upon this game.
The thread m_runner represents the engine of this game. First, it waits while the parent frame is shown on the screen (otherwise wild visual effects may appear). The endless while loop manages Pac-man's movement on the screen. The direction of Pac-man's motion may have been changed by the user since the last cycle, so we reassign three parameters which determine that direction from storage variables (m_creatureVx from m_creatureNewVx etc.). This insures that Pac-man's direction will not change in the middle of a cycle.
We then calculate the coordinates of the next cell i and j Pac-man will visit. If these coordinates lie inside the level and do not correspond to a wall cell (a 1), we smoothly move Pac-man to the new position. Otherwise we wait until the user provides a new direction.
The movement of Pac-man from the current cell to a new one is split into N_STEP steps. On each step we determine the fractional coordinates, m_creatureX and m_creatureY (in cell units). Then we call paintImmediately() to redraw a portion of the level surrounding Pac-man's current location, and pause for 40 ms:
final int x = (int)(m_creatureX*X_CELL);
final int y = (int)(m_creatureY*Y_CELL);
Runnable painter = new Runnable() {
public void run() {
PacMan2D.this.paintImmediately(
x-1, y-1, X_CELL+3, Y_CELL+3);
}
};
try {
SwingUtilities.invokeAndWait(painter);
} catch (Exception e) {}
try { sleep(40); }
catch (InterruptedException ex) { break; }
The paintImmediately() method can be used to force very quick repaints but should only be called from within the AWT event dispatching thread. Additionally, because we do not want any other painting or movement to occur while this paint takes place, we wrap the call in a Runnable and send it to the event queue with invokeAndWait() (refer back to chapter 2 for a discussion of painting and multithreading issues).
When the creature's relocation is over, we eat a pellet (by setting the m_flags array element corresponding to the current cell to -1) and adjust Pac-man's coordinate variables.
To listen for the user's keyboard activity we create a KeyAdapter instance and add it to the parent component. This KeyAdapter processes arrow keys (up, down, left, and right) and assigns new values to the m_creatureNewVx, m_creatureNewVy, and m_creatureNewDir variables accordingly. The program flow is not interrupted, these new values will be requested only on the next thread cycle as discussed above. Note that if the keypads are pressed too fast, only the last typed value will affect the Pac-man's direction.
The getPreferredSize() method determines the size of the level, which is simply based on the number and size of cells. Finally the paintComponent() method is responsible for rendering the whole game. This process is relatively simple: we render the level using two images (wall and ball) and draw Pac-man's image (taken from our 2D m_creature array) in the proper location.
Running the Code
Figure 23.5 shows Pac-man in action. Try the game and have some fun. Experiment with modifying the level and icons for the wall, ball, and Pac-man himself. If you like this example, you might go further and add monsters, score, sound effects, various levels and level changing, and other full-featured game characteristics.


RSS feed Java FAQ News