By Andrew Davison
Price: $44.95 USD
£31.95 GBP
Cover | Table of Contents | Colophon
http://www.javaperformancetuning.com/) is a good source for performance tips, with links to tools and other resources. A recent benchmarking of Java vs. C++ by Keith Lea caused quite a stir (http://www.theserverside.com/news/thread.tss?thread_id=26634). He found that Java may sometimes be faster than C++. The response from the C++ crowd was typically vitriolic.http://www.ej-technologies.com/products/jprofiler/overview.html), can help identify code using excessive amounts of memory.http://java-source.net/.http://www-106.ibm.com/developerworks/java/library/j-perf06304/. Another possibility is GC Portal (http://java.sun.com/developer/technicalArticles/Programming/GCPortal/).http://www.phantom.net/index.php). It's essentially a PC running an embedded Windows XP installation, with an nVidia graphics card, a hard drive, and a broadband connection. Most importantly for Java gaming, the Phantom will come with a complete JRE. It was demoed during Electronic Entertainment Exposition (E3) in 2004, where it was shown running Law and Order: Dead on the Money (which uses Java 3D).http://www.kaffe.org/http://www.puzzlepirates.com/)
http://www.chromethegame.com/en/show.php)
http://www.lawandordergame.com/index2.htm)
http://www.abandonedcastle.com/)
http://www.puppygames.net/info.php?game=Alien_Flux)
http://www.javagaming.org), initially manned by volunteers. In 2003, the Game Technology Group was formed, and JavaGaming.org received a substantial makeover as part of the creation of the new java.net portal (http://www.java.net) aimed at the technical promotion of Java. Java.net hosts many discussion forums, user groups, projects, communities, and news. The communities include: Java Desktop, Java Education and Learning, Java Enterprise, and Java Games.http://www.javagaming.org or http://community.java.net/games/. The site includes Java games forums, projects, news, weblogs, a wiki (http://wiki.java.net/bin/view/Games/WebHomeJPanel subclass (called GamePanel), which acts as a canvas for drawing 2D graphics (e.g., lines, circles, text, images). The animation is managed by a thread, which ensures that it progresses at a consistent rate, as independent of the vagaries of the hardware and OS as possible. The rate is measured in terms of frames per second (FPS), where a frame corresponds to a single rendering of the application to the canvas.GamePanel is gradually refined and expanded through the chapter, introducing the following notions:{update, render, sleep} animation loopJPanel is employed as a drawing surface, and an animation loop is embedded inside a thread local to the panel. The loop consists of three stages: game update, rendering, and a short sleep.GamePanel, including the run() method containing the animation loop. As the chapter progresses, additional methods and global variables will be added to GamePanel, and some of the existing methods (especially run()) will be changed and extended.
public class GamePanel extends JPanel implements Runnable
{
private static final int PWIDTH = 500; // size of panel
private static final int PHEIGHT = 400;
private Thread animator; // for the animation
private volatile boolean running = false; // stops the animation
private volatile boolean gameOver = false; // for game termination
// more variables, explained later
// :
public GamePanel()
{ setBackground(Color.white); // white background
setPreferredSize( new Dimension(PWIDTH, PHEIGHT));
// create game components
// ...
} // end of GamePanel()
public void addNotify()
/* Wait for the JPanel to be added to the
JFrame/JApplet before starting. */
{
super.addNotify(); // creates the peer
startGame(); // start the thread
}
private void startGame()
// initialise and start the thread
{
if (animator == null || !running) {
animator = new Thread(this);
animator.start();
}
} // end of startGame()
public void stopGame()
// called by the user to stop execution
{ running = false; }
public void run()
/* Repeatedly update, render, sleep */
{
running = true;
while(running) {
gameUpdate(); // game state is updated
gameRender(); // render to a buffer
repaint(); // paint with the buffer
try {
Thread.sleep(20); // sleep a bit
}
catch(InterruptedException ex){}
}
System.exit(0); // so enclosing JFrame/JApplet exits
} // end of run()
private void gameUpdate()
{ if (!gameOver) // update game state ...
}
// more methods, explained later...
} // end of GamePanel class
repaint() in run()'s animation loop:
while(running) {
gameUpdate(); // game state is updated
gameRender(); // render to a buffer
repaint(); // paint with the buffer
try {
Thread.sleep(20); // sleep a bit
}
catch(InterruptedException ex){}
}
repaint() is only a request, it's difficult to know when the repaint has been completed. This means that the sleep time in the animation loop is little more than a guess; if the specified delay is too long, then the animation speed is impaired for no reason. If the delay is too short, then repaint requests may be queued by the JVM and skipped if the load becomes too large.repaint() part of the rendering is done by the JVM and cannot be easily measured.run():
public void run()
/* Repeatedly update, render, sleep */
{
running = true;
while(running) {
gameUpdate(); // game state is updated
gameRender(); // render to a buffer
paintScreen(); // draw buffer to screen
try {
Thread.sleep(20); // sleep a bit
}
catch(InterruptedException ex){}
}
System.exit(0);
} // end of run()
private void paintScreen()
// actively render the buffer image to the screen
{
Graphics g;
try {
g = this.getGraphics(); // get the panel's graphic context
if ((g != null) && (dbImage != null))
g.drawImage(dbImage, 0, 0, null);
Toolkit.getDefaultToolkit().sync(); // sync the display on some systems
g.dispose();
}
catch (Exception e)
{ System.out.println("Graphics context error: " + e); }
} // end of paintScreen()GamePanel, a frame corresponds to a single pass through the update-render-sleep loop inside run(). Therefore, the desired 100 FPS imply that each iteration of the loop should take 1000/100 == 10 ms. This iteration time is stored in the period variable in GamePanel.period gives the sleep time required to maintain the desired FPS. For instance, 100 FPS mean a period of 10 ms, and if the update/render steps take 6 ms, then sleep() should be called for 4 ms. Of course, this is different on each platform, so must be calculated at runtime.run() method includes timing code and the sleep time calculation:
public void run()
/* Repeatedly: update, render, sleep so loop takes close
to period ms */
{
long beforeTime, timeDiff, sleepTime;
beforeTime = System.currentTimeMillis();
running = true;
while(running) {
gameUpdate();
gameRender();
paintScreen();
timeDiff = System.currentTimeMillis() - beforeTime;
sleepTime = period - timeDiff; // time left in this loop
if (sleepTime <= 0) // update/render took longer than period
sleepTime = 5; // sleep a bit anyway
try {
Thread.sleep(sleepTime); // in ms
}
catch(InterruptedException ex){}
beforeTime = System.currentTimeMillis();
}
System.exit(0);
} // end of run()
timeDiff holds the execution time for the update and render steps, which becomes part of the sleep time calculation.run() depends on a good timer and the accuracy of the sleep() call. The previous major section dealt with alternatives to currentTimeMillis(). In this section, I consider ways of improving the sleep() code in run(), so the required frame rate is consistently achieved.SleepAcc
class measures sleep accuracy. Example 2-4 calls sleep() with increasingly small values and measures the actual sleep time using the Java 3D timer.
import java.text.DecimalFormat;
import com.sun.j3d.utils.timer.J3DTimer;
public class SleepAcc
{
private static DecimalFormat df;
public static void main(String args[])
{
df = new DecimalFormat("0.##"); // 2 dp
// test various sleep values
sleepTest(1000);
sleepTest(500);
sleepTest(200);
sleepTest(100);
sleepTest(50);
sleepTest(20);
sleepTest(10);
sleepTest(5);
sleepTest(1);
} // end of main()
private static void sleepTest(int delay)
{
long timeStart = J3DTimer.getValue();
try {
Thread.sleep(delay);
}
catch(InterruptedException e) {}
double timeDiff =
((double)(J3DTimer.getValue() - timeStart))/(1000000L);
double err = ((delay - timeDiff)/timeDiff) * 100;
System.out.println("Slept: " + delay + " ms J3D: " +
df.format(timeDiff) + " ms err: " +
df.format(err) + " %" );
} // end of sleepTest()
} // end of SleepAcc class
D>java SleepAcc
Slept: 1000 ms J3D: 999.81 ms err: 0.02 %
Slept: 500 ms J3D: 499.54 ms err: 0.09 %
Slept: 200 ms J3D: 199.5 ms err: 0.25 %
Slept: 100 ms J3D: 99.56 ms err: 0.44 %
Slept: 50 ms J3D: 49.59 ms err: 0.82 %
Slept: 20 ms J3D: 20.53 ms err: -2.59 %
Slept: 10 ms J3D: 10.52 ms err: -4.91 %
Slept: 5 ms J3D: 5.42 ms err: -7.78 %
Slept: 1 ms J3D: 1.15 ms err: -13.34 %
: // more lines until ctrl-C is typed
public void run()
// Repeatedly update, render, sleep
{ ...
running = true;
while(running) {
gameUpdate(); // game state is updated
gameUpdate(); // game state is updated again
gameRender(); // render to a buffer
paintScreen(); // paint with the buffer
// sleep a bit
}
System.exit(0);
} // end of run()
gameUpdate() more than once during each iteration. However, too many additional calls will cause the game to flicker, as too many successive states are not rendered. Each update adds to the execution time, which will further reduce the maximum achievable FPS value.run() is:
private static int MAX_FRAME_SKIPS = 5;
// no. of frames that can be skipped in any one animation loop
// i.e the games state is updated but not rendered
public void run()
/* Repeatedly update, render, sleep so loop takes close
to period nsecs. Sleep inaccuracies are handled.
The timing calculation use the Java 3D timer.
Overruns in update/renders will cause extra updates
to be carried out so UPS tild== requested FPS
*/
{
long beforeTime, afterTime, timeDiff, sleepTime;
long overSleepTime = 0L;
int noDelays = 0;
Thread.suspend() and resume(). These methods are deprecated for a similar reason to Thread.stop();suspend() can cause an applet/application to suspend at any point in its execution. This can easily lead to deadlock if the thread is holding a resource since it will not be released until the thread resumes.Thread class recommends using wait() and notify() to implement pause and resume functionality. The idea is to suspend the animation thread, but the event dispatcher thread will still respond to GUI activity. To implement this approach, I introduce an isPaused Boolean, which is set to true via pauseGame():
// global variable
private volatile boolean isPaused = false;
public void pauseGame()
{ isPaused = true; }
public void run()
// Repeatedly (possibly pause) update, render, sleep
// This is not a good approach, and is shown for illustration only.
{ ...
running = true;
while(running) {
try {
if (isPaused) {
synchronized(this) {
while (isPaused && running)
wait();
}
}
} // of try block
catch (InterruptedException e){}
gameUpdate(); // game state is updated
gameRender(); // render to a buffer
paintScreen(); // paint with the buffer
// sleep a bit
}
System.exit(0);
} // end of run()
isPaused flag is detected in run() and triggers a wait() call to suspend the animation thread. The flag must be volatile so run() is sure to see the change made by pauseGame() (otherwise the variable may be cached locally).resumeGame() or stopGame(), both of which call notify(). These methods must be synchronized so the animation thread doesn't miss the notification and remain suspended indefinitely:JPanel. But other ways of implementing animation in Java exist, and I'll briefly consider two of them:java.util.timer
javax.swing.Timer) is used as the basis of animation examples in many Java textbooks.Timer object to "tick" every few milliseconds. Each tick sends an event to a specified ActionEvent listener, triggering a call to actionPerformed(). actionPerformed() calls repaint() to send a repaint request to the JVM. Eventually, repainting reaches the paintComponent() method for the JPanel, which redraws the animation canvas. These stages are shown in Figure 2-1, which represents the test code in SwingTimerTest.java.
SwingTimerTest class uses the Swing timer to draw the current average FPS values repeatedly into a JPanel. The period for the timer is obtained from the requested FPS given on the command line. The average FPS are calculated every second, based on FPS values collected over the previous 10 seconds.main() reads in the user's required FPS and converts them to a period. It creates a JFrame and puts the SwingTimerPanel inside it.SwingTimerTest() constructor creates the timer and sends its "ticks" to itself:new Timer(period, this).start();
WormChase game. In Chapter 4, I will continue the comparisons, concentrating on several kinds of full-screen applications.WormChase application on the left and the applet version on the right.
WormChase, and a few issues apply to all the versions which need to be considered before we begin.WormChase application are investigated here: one using the Java 3D timer and the other using the System timer. A comparison of the two will show when the Java 3D timer is beneficial.WormChase, and a few issues apply to all the versions which need to be considered before we begin.WormChase application are investigated here: one using the Java 3D timer and the other using the System timer. A comparison of the two will show when the Java 3D timer is beneficial.WormChase, changing every J3DTimer.getValue() call to System.nanoTime().WormChase versions in this chapter and the next use the same game-specific classes (i.e., Worm and Obstacles, shown throughout this chapter). They employ a similar WormPanel class, which corresponds to the GamePanel animation class in Chapter 2.JFrame while the applet utilizes JApplet. This requires changes to how game pausing and resumption are triggered, and the way of specifying the required FPS.reportStats() method detailed in the section "Swing Timer Animation" in Chapter 2. The main change to that method is that the average UPS are calculated alongside the average FPS. The overall aim of the testing is to see if the animation loop can deliver 80 to 85 FPS. Failing this, the programs should produce 80 to 85 updates per second without an excessive number of frames being skipped.WormChase application. The class names and public methods are shown.
WormChase is in the directory Worm/WormP/.WormChase is the top-level JFrame, managing the GUI, and processing window events. WormPanel is the game panel holding the threaded animation loop.Worm class maintains the data structures and methods for the on-screen worm. The Obstacles
class handles the blue boxes. Worm and Obstacles have their own draw() method, which is called by WormPanel to render the worm and boxes.WormChase, including all its variables and methods.
main() function in WormChase reads the requested FPS from the command line, converting it to a delay in nanoseconds, which is passed to the WormChase() constructor:
public static void main(String args[])
{
int fps = DEFAULT_FPS;
if (args.length != 0)
fps = Integer.parseInt(args[0]);
long period = (long) 1000.0/fps;
System.out.println("fps: " + fps + "; period: " +period+ " ms");
new WormChase(period*1000000L); // ms --> nanosecs
}
WormChase constructor creates the WormPanel canvas, as well as two text fields for displaying the number of boxes added to the scene (jtfBox) and the current time (jtfTime). These text fields can be updated via two public methods:
public void setBoxNumber(int no)
{ jtfBox.setText("Boxes used: " + no); }
public void setTimeSpent(long t)
{ jtfTime.setText("Time Spent: " + t + " secs"); }
setBoxNumber() is called from the Obstacles object when a new box (obstacle) is created. setTimeSpent() is called from WormPanel.WormChase implements WindowListener). Pausing is triggered by window deactivation or iconification; the application resumes when the window is activated or de-iconified, and the clicking of the window close box causes termination:
public void windowActivated(WindowEvent e)
{ wp.resumeGame(); }
public void windowDeactivated(WindowEvent e)
{ wp.pauseGame(); }
public void windowDeiconified(WindowEvent e)
{ wp.resumeGame(); }
public void windowIconified(WindowEvent e)
{ wp.pauseGame(); }
public void windowClosing(WindowEvent e)
{ wp.stopGame(); }WormPanel class is similar to the GamePanel class developed in Chapter 2, with some additional methods for drawing the game scene. WormPanel contains an extended version of the reportStats() method used for timing the Swing and utility timers in Chapter 2, called printStats(). Its principal extension is to report the average UPS (updates per second) in addition to the average FPS.WormPanel methods is given in Figure 3-4.WormPanel constructor sets up the game components and initializes timing elements:
public WormPanel(WormChase wc, long period)
{
wcTop = wc;
this.period = period;
setBackground(Color.white);
setPreferredSize( new Dimension(PWIDTH, PHEIGHT));
setFocusable(true);
requestFocus(); // now has focus, so receives key events
readyForTermination();
// create game components
obs = new Obstacles(wcTop);
fred = new Worm(PWIDTH, PHEIGHT, obs);
addMouseListener( new MouseAdapter() {
public void mousePressed(MouseEvent e)
{ testPress(e.getX(), e.getY()); }
});
// set up message font
font = new Font("SansSerif", Font.BOLD, 24);
metrics = this.getFontMetrics(font);
// initialise timing elements
fpsStore = new double[NUM_FPS];
upsStore = new double[NUM_FPS];
for (int i=0; i < NUM_FPS; i++) {
fpsStore[i] = 0.0;
upsStore[i] = 0.0;
}
} // end of WormPanel()
WormPanel from WormChase and stored in a global variable. readyForTermination() is the same as in Chapter 2: a KeyListener monitors the input for termination characters (e.g., Ctrl-C), then sets the running Boolean to false.fpsStore[] and Worm class stores coordinate information about the worm in a circular buffer. It includes testing methods for checking if the player has clicked near the worm's head or body and includes methods for moving and drawing the worm.Point objects in a cells[] array. Each point represents the location of one of the black circles of the worm's body (and the red circle for its head). As the worm grows, more points are added to the array until it is full; the worm's maximum extent is equivalent to the array's size.cells[] array where the point for the new head can be stored.cells[] array is gradually filled and then reused. The two indices, headPosn and tailPosn, make it simple to modify the head and tail of the worm, and nPoints records the length of the worm.
Point objects which store the (x, y) coordinates of the worm's parts. The numbers are included in the figure to indicate the order in which the array is filled and over-written; they are not part of the actual data structure, which is defined like so:
private static final int MAXPOINTS = 40;
private Point cells[];
private int nPoints;
private int tailPosn, headPosn; // tail and head of buffer
// additional variables already defined
cells = new Point[MAXPOINTS]; // initialise buffer
nPoints = 0;
headPosn = -1; tailPosn = -1;Obstacles object maintains an array of Rectangle objects called boxes. Each object contains the top-left hand coordinate of a box and the length of its square sides.Obstacles class are synchronized since the event thread of the game could add a box to the obstacles list (via a call to add()) while the animation thread is examining or drawing the list.add() is defined as
synchronized public void add(int x, int y)
{
boxes.add( new Rectangle(x,y, BOX_LENGTH, BOX_LENGTH));
wcTop.setBoxNumber( boxes.size() ); // report new no. of boxes
}
setBoxNumber().WormPanel delegates the task of drawing the obstacles to the Obstacles object, by calling draw():
synchronized public void draw(Graphics g)
// draw a series of blue boxes
{
Rectangle box;
g.setColor(Color.blue);
for(int i=0; i < boxes.size(); i++) {
box = (Rectangle) boxes.get(i);
g.fillRect( box.x, box.y, box.width, box.height);
}
} // end of draw()
Worm communicates with Obstacles to determine if its new head (a Point object, p) intersects with any of the boxes:
synchronized public boolean hits(Point p, int size)
{
Rectangle r = new Rectangle(p.x, p.y, size, size);
Rectangle box;
for(int i=0; i < boxes.size(); i++) {
box = (Rectangle) boxes.get(i);
if (box.intersects(r))
return true;
}
return false;
} // end of hits()
WormChase is a windowed application, with an animation loop driven by the Java 3D timer. Can it support frame rates of 80 to 85 FPS?|
Requested FPS
|
20
|
50
|
80
|
100
|
|---|---|---|---|---|
|
Windows 98
|
20/20
|
48/50
|
81/83
|
96/100
|
|
Windows 2000
|
20/20
|
43/50
|
59/83
|
58/100
|
|
Windows XP
|
20/20
|
50/50
|
83/83
|
100/100
|