You have learned how to use the Graphics2D
class to program graphics using vector shapes and bitmap images, and you have even seen a nearly complete game written from scratch. You have learned how to load and play sound files and MIDI music files, and how to program the keyboard and mouse. By all accounts, you have the tools to create many different games already. But there are some tricks of the trade—secrets of the craft—that will help you to make your games stand out in the crowd and impress. This chapter discusses the game loop and its vital importance to a smooth-running game. You will learn about threads and timing, and you will take the Asteroids-style game created in Chapters 3 and 5 into completely new territory, as it is modified extensively in the following pages.
Here are the specific topics you will learn about:
Using timing methods
Starting and stopping a thread
Using a thread for the game loop
The key to creating a game loop to facilitate the needs of a high-speed game is Java’s multithreading capability. Threads are such an integral part of Java that it makes a special thread available to your program just for this purpose. This special thread is called Runnable,
an interface class. However, it’s entirely possible to write a Java game without threads by just using a simple game loop. I’ll show you how to do this first, and then we’ll take a look at threads as an even better form of game loop.
An interface class is an abstract class with properties and methods that are defined but not implemented. A program that uses an interface class is said to consume it, and must implement all of the public methods in the interface. Typical examples include KeyListener
and Runnable.
The Runnable
interface gives your program its own awareness. I realize this concept sounds a lot like artificial intelligence, but the term awareness is a good description of the Runnable
interface. Before Runnable,
your Java programs have been somewhat naive, capable of only processing during the screen refresh. Let’s take a look at an example. Figure 10.1 shows the SimpleLoop program. As you can see, this program doesn’t do anything in real time; it waits until you press a key or click the mouse before drawing a rectangle.
// SimpleLoop program import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.util.*; public class SimpleLoop extends JFrame implements KeyListener, MouseListener { Random rand = new Random(); public static void main(String[] args) { new SimpleLoop(); } public SimpleLoop() { super("SimpleLoop"); setSize(500,400); setVisible(true); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); addKeyListener(this); addMouseListener(this); } public void paint(Graphics g) { Graphics2D g2d = (Graphics2D) g; //create a random rectangle int w = rand.nextInt(100); int h = rand.nextInt(100); int x = rand.nextInt(getSize().width - w); int y = rand.nextInt(getSize().height - h); Rectangle rect = new Rectangle(x,y,w,h); //generate a random color int red = rand.nextInt(256); int green = rand.nextInt(256); int blue = rand.nextInt(256); g2d.setColor(new Color(red,green,blue)); //draw the rectangle g2d.fill(rect); } //handle keyboard events public void keyReleased(KeyEvent k) { } public void keyTyped(KeyEvent k) { } public void keyPressed(KeyEvent k) { repaint(); } //handle mouse events public void mouseEntered(MouseEvent m) { } public void mouseExited(MouseEvent m) { } public void mouseReleased(MouseEvent m) { } public void mouseClicked(MouseEvent m) { } public void mousePressed(MouseEvent m) { repaint(); } }
The Random
class is located in the java.awt.util
class along with many other utility classes that provide commonly needed tools to your program. To use Random
in your program, you must include this class with the following import
statement:
import java.awt.util.*;
This program has no loop whatsoever, so it cannot process anything in real time—not spaceships, asteroids, jumping Italian plumbers, yellow dot-eaters, or female spelunkers packing dual Berettas. The only thing this program can do is draw a single rectangle at a time.
There are some ways you can make the program a little more interactive. The only problem with the mouse and keyboard listener interfaces is that you have to implement all of them or none of them. That reminds me of Yoda’s famous saying, “Do, or do not. There is no ‘try.’” An interface class is an all-or-nothing proposition that tends to junk up your source code, not to mention that it takes a lot of work to type in all of those interface event methods every time! But there’s no real workaround for the unused event methods.
I’ve been thinking about a way to use all of these interface classes (such as Runnable
and the input listeners) by tucking them away into a class outside of the main program. This support class would provide my main program with real events when things happen, such as key presses, mouse movement, and other events. Perhaps this is the birth of an idea that will become some sort of game engine? Let’s wait and see what Part III has in store.
There is a serious problem with this program because it was supposed to just add a new rectangle every time the user presses a key or mouse button, not redraw the entire window—with a single rectangle left over. There is definitely something odd going on because this program should have worked as expected.
Well, it turns out that Java has been screwing with the screen without permission—or rather, by default. The JFrame
class, which is the basis for the SimpleLoop program, provides many default event methods that do certain things for you. You don’t even need to implement paint()
if you don’t want to, and the JFrame
base class will provide it for your program. Granted, nothing will be drawn on the window as a result, but the compiler won’t give you an error. This differs from an interface class (such as KeyListener)
that mandates that you must implement all of its abstract methods. So it’s pretty obvious by this difference in functionality that JFrame
is not an interface class, but a fully functioning class.
What happens, then, when you implement a JFrame
class method such as paint()
? These methods are essentially empty inside the JFrame
class. Oh, they exist and are not abstract, but they don’t do anything. The JFrame
class defines these methods in such a way that you can override them in your program. For instance, in the SimpleLoop program, SimpleLoop is actually the name of the class, and it inherits from JFrame.
Therefore, SimpleLoop has the opportunity to override any of the methods in JFrame
that it wants to, including paint().
Now you finally have an opportunity to add a real loop to this program. But just for kicks, what do you think would happen if you added a while()
loop to the constructor? I tried it, so you should try it too. Doing this will cause the window to quickly fill up with rectangles! The only problem with this kind of loop is that none of our program’s other events will come up because we’ve trapped the program inside the while
loop. The call to repaint the window is only the last step in a game. First, you move your game objects around on the screen, perform collision testing, and so forth. You can perform all of these steps in the paint()
event, but that limits the program to a very low frame rate. The following is a suggestion on what we might try to do (note: this is not actual code that you should add to the SimpleLoop program—it’s just an exploration of an idea).
//the game loop while (true) { //move the game objects updateObjects(); //perform collision testing testCollisions(); //redraw the window repaint(); }
This would work, but we’re still locking out processes that communicate with our program through events such as the keyboard and mouse handlers.
We use the Runnable
interface class to add threading support to a game. This interface has a single event method called run()
that represents the thread’s functionality. We can create this run()
method in a game, and that will act like the game loop. The thread just calls run()
once, so you must add a while
loop to this method. The while
loop will be running inside a thread that is separate from the main program. By implementing Runnable,
a game becomes multithreaded. In that sense, it will support multiple processors or processor cores! For instance, if you have an Intel Core2 Duo or another multi-core processor, you should be able to see the applet running in a thread that is separate from the web browser or applet viewer program.
To get a thread running in an applet, you have to create an instance of the Thread
class, and then start it running using the applet’s start()
event. First, let’s create the thread object:
Thread thread; Next, create the thread in the constructor method: thread = new Thread(this); thread.start();
Then you can write the code for the thread’s loop in the run()
event method (part of Runnable).
We’ll take a look at what goes inside run()
in a moment.
Let’s take a look at the entire ThreadedLoop project, and then I’ll explain the loop for the thread inside the run()
method. The key portions of code have been highlighted in bold.
// ThreadedLoop program import java.awt.*; import java.lang.*; import javax.swing.*; import java.util.*; public class ThreadedLoop extends JFrame implements Runnable { Random rand = new Random(); //the main thread Thread thread; //count the number of rectangles drawn long count = 0, total = 0; public static void main(String[] args) { new ThreadedLoop(); } public ThreadedLoop() { super("ThreadedLoop"); setSize(500,400); setVisible(true); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); thread = new Thread(this); thread.start(); } //thread run event public void run() { long start = System.currentTimeMillis(); //acquire the current thread Thread current = Thread.currentThread(); //here's the new game loop while (current == thread) { try { Thread.sleep(0); } catch(InterruptedException e) { e.printStackTrace(); } //draw something repaint(); //figure out how fast it's running if (start + 1000 < System.currentTimeMillis ()){ start = System.currentTimeMillis(); total = count; count = 0; } } } //JFrame paint event public void paint(Graphics g) { Graphics2D g2d = (Graphics2D) g; //create a random rectangle int w = rand.nextInt(100); int h = rand.nextInt(100); int x = rand.nextInt(getSize().width - w); int y = rand.nextInt(getSize().height - h); Rectangle rect = new Rectangle(x,y,w,h); //generate a random color int red = rand.nextInt(256); int green = rand.nextInt(256); int blue = rand.nextInt(256); g2d.setColor(new Color(red,green,blue)); //draw the rectangle g2d.fill(rect); //add another to the counter count++; g2d.setColor(Color.WHITE); g2d.fill(new Rectangle(0,360,500,400)); g2d.setColor(Color.BLACK); g2d.drawString("Rectangles per second: " + total, 10, 380); } }
Now let’s examine this run()
event that is called by the Runnable
interface. There’s a lot going on in this method. First, a variable called start
gets the current time in milliseconds (System.currentTimeMillis()).
This value is used to pause once per second to print out the total number of rectangles that have been drawn (see Figure 10.2).
Next, a local variable is set to the current thread, and then a while
loop is created.
Thread current = Thread.currentThread();
This local thread makes sure that our loop only processes thread events intended for the game loop because you can use multiple threads in a program.
while (current == thread)
The core of the thread
loop includes a call to Thread.sleep(0),
which is a placeholder for slowing the game down to a consistent frame rate. Right now it’s running as fast as possible because the sleep()
method is being passed a 0. This single method call requires an error handler because it throws an Interrupted Exception
if the thread is ever interrupted by another thread. This is an advanced subject that doesn’t concern us. If the thread is interrupted, it’s not a big deal—we’ll just ignore any such error that might crop up.
try { Thread.sleep(0); } catch(InterruptedException e) { e.printStackTrace(); }
After this call to sleep (which will slow the game to a consistent frame rate), then we can call game-related methods to update objects on the screen, perform collision testing, perform game logic to interact with enemies, and so on. In the block of code that follows, you can see some timing code inside an if
statement. This code prints out the number of rectangles that have been drawn by the paint()
event during the past 1,000 milliseconds (which equals 1 second).
//draw something repaint(); //figure out how fast it's running if (start + 1000 < System.currentTimeMillis()) { start = System.currentTimeMillis(); count = 0; }
The single call to repaint()
actually makes this program do something; all of the rest of the code helps this method call to do its job effectively. The run()
event contains the new threaded game loop.
Aside from the sample game in Chapter 3, this program might have been your first exposure to multithreaded programming. Java makes the process very easy compared to other languages. I’ve used several threading libraries such as Posix threads, Boost threads, and Windows threads to add thread support in my C++ programs in Linux and Windows, and it’s not very easy to use at all compared to Java’s built-in support for threads. We will continue to use threads in every subsequent chapter, so you will have had a lot of exposure to this subject by the time you complete the book.
This was a heavyweight chapter that covered some very difficult subjects. But the idea is to get up the most difficult part of a hill so you can reach the peak, and then head on down the other side. That is what this chapter represents—the last few steps up to the peak. You have now learned the most difficult and challenging aspects of writing a Java game at this point, and you are ready to start heading down the hill at a more leisurely pace in the upcoming chapters. This chapter explained:
How to create a threaded game loop
How to override default applet methods
How to manipulate a bitmap with transformations
The following questions will help you to determine how well you have learned the subjects discussed in this chapter. The answers are provided in Appendix A, “Chapter Quiz Answers.”
The following exercises will test your comprehension of the topics covered in this chapter by making some important changes to the projects.
The ThreadedLoop program runs at breakneck speed with no delay. Modify the thread delay value to see whether you can slow down the number of rectangles being drawn to just 1,000 rectangles per second.
3.144.121.45