Setting up Graphics and a Game Loop in Pure Java
There are game libraries and game frameworks for the JVM, but they all integrate with OpenGL and bundle some native libraries — typically three sets of native libraries for each of Windows, MacOS, and Linux. However, for many 2D games, you’d be fine with a pure Java solution just using the standard library API.
That said, there’s a “right” way to do game graphics in pure Java, and when I tried googling around, it didn’t seem like any of the tutorials taught that way, so this article here will teach it.
The Absolute Basics of Swing
As a super quick summary of the history of UI in Java: In the beginning, there were applets. We don’t care about applets anymore, so I won’t discuss them further. Sun then built a UI API called “AWT”, the Abstract Window Toolkit. Then, on top of AWT, Sun built another UI API called “Swing”. And then Sun/Oracle built another UI API called “JavaFX”.
We won’t be using JavaFX, so I won’t mention that one any further. Instead, we’ll use Swing, which is built on top of AWT, and sometimes we’ll “reach past” Swing to work directly with AWT. And in fact, we’ll only barely touch the surface of Swing and AWT, as most 2D games don’t need to do anything sophisticated with them.
The first thing to know about Swing is that whenever you interact with the Swing API, you need to do so from the Swing thread. When your Java program first starts off, it has a main thread. When you first interact with Swing, it automatically creates a Swing thread. So any time you do something with the Swing API, you’re “supposed to” do so from the Swing thread. If you don’t, your application may sometimes work and sometimes fail, because interacting with Swing outside of the Swing thread introduces race conditions. Since your application sometimes appears to work even if you accidentally access the Swing API from the wrong thread, this is a common mistake that’s difficult to debug. It’s just an unfortunate reality of working with Swing that you’ll have to get used to.
If you find a tutorial on Swing, it’ll probably show you how to make a Hello World application like this:
import javax.swing.JFrame;
import java.awt.Dimension;
public class MyApplication {
public static void main(String[] args) {
JFrame window = new JFrame("Hello World!");
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setPreferredSize(new Dimension(800, 600));
window.pack();
window.setVisible(true);
}
}
This code is wrong. It’s interacting with the Swing API from the main thread. It’ll probably work, but every now and then, the code will fail due to the race conditions mentioned above. Here’s the correct version of the code:
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import java.awt.Dimension;
import java.lang.reflect.InvocationTargetException;
public class MyApplication {
public static void main(String[] args) throws InterruptedException, InvocationTargetException {
SwingUtilities.invokeAndWait(() -> {
JFrame window = new JFrame("Hello World!");
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setPreferredSize(new Dimension(800, 600));
window.pack();
window.setVisible(true);
});
}
}
SwingUtilities.invokeAndWait
takes a lambda, and executes it on the Swing thread, thus avoiding the race condition issue mentioned above.
Your Java application is considered running as long as there is at least one active non-daemon thread. In basic Java programs, there’s just the “main” thread, and as soon as that ends (e.g., you return from the main method), the program ends. However, in a Swing application, there are at least two non-daemon threads: the main thread and the Swing thread. So your program will keep running even if you return from the main method on the main thread, since the Swing thread is still active. To address this, we add a call to setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
. This informs Swing that when the user closes the window (on many OS’s, this is done by clicking the X button in the top right corner of the window), it should then exit the whole application.
pack()
performs layout on your window. Layout managers are a big topic; I won’t go into it in this article. Suffice it to say that this is the call that actually tries to resize your window to match its preferred size.
The paint method
In a traditional Swing application, you build up the UI of your application from components like text boxes, scroll bars, buttons, and so on. However, that’s not what we want to do for games.
In a slightly less traditional Swing application, you might design a custom component. In that case, you would implement (among other things) a paint
method that knows how to set the suitable pixels on the screen to the appropriate colors to create a visual representation of your component. A decent number of Java game tutorials will teach you this method of doing graphics for games, but again this is the wrong approach.
Under that paradigm, we’re assuming that the component doesn’t change much from frame to frame. So the expectation is that you draw the component once, and Swing or the Operating System can keep using the set of pixels you drew for perpetuity until there’s a state change, or until something temporarily hides your widget (for example, the user drags a window so that it partially or totally overlaps with your widget, and then drags that window away again). When your widget is no longer hidden, the OS and Swing will then send a request to your application code to repaint just the part of the component that was hidden.
For games, the paradigm we’d rather have is to have a rendering loop that’s called very frequently (say, 60 times a second), and that’s responsible for rendering the entire game window.
So basically, we won’t be using the paint
method.
A Basic Render Loop
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import java.awt.*;
import java.awt.image.BufferStrategy;
import java.lang.reflect.InvocationTargetException;
public class MyApplication {
public static void main(String[] args)
throws InterruptedException, InvocationTargetException {
SwingUtilities.invokeAndWait(() -> {
JFrame window = new JFrame("Hello World!");
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setPreferredSize(new Dimension(800, 600));
window.pack();
window.setIgnoreRepaint(true);
window.setVisible(true);
int numBuffers = 2;
ImageCapabilities accelerated =
new ImageCapabilities(true);
try {
window.createBufferStrategy(
numBuffers,
new BufferCapabilities(
accelerated,
accelerated,
BufferCapabilities.FlipContents.UNDEFINED
)
);
} catch (AWTException e) {
throw new RuntimeException(e);
}
new Thread(() -> {
BufferStrategy bufferStrategy =
window.getBufferStrategy();
while (true) {
Graphics g = bufferStrategy.getDrawGraphics();
try {
//Draw whatever you want here
int x = 10, y = 20, width = 100, height = 50;
g.setColor(Color.RED);
g.fillOval(x, y, width, height);
g.setColor(Color.BLACK);
g.drawOval(x, y, width, height);
} finally {
g.dispose();
}
bufferStrategy.show();
Thread.yield();
}
}).start();
});
}
}
Here’s the same code as before, but with a bit more added to give us our game rendering loop.
First, note that we added a call to window.setIgnoreRepaint(true)
. This tells Swing not to invoke our paint
method, since we will fully own updating the UI ourselves.
Next, we added some code to create a BufferStrategy from the JFrame
, specifying that we want a hardware-accelerated double buffer. The basic idea behind a double buffer is that the video card displays one buffer (the “front buffer”) while we draw on the other buffer (the “back buffer”) — and when we’re done drawing, we flip the two buffers so that the video card shows the buffer we just drew, and we can start drawing anew on the buffer we just received.
The BufferCapabilities.FlipContents
parameter specifies what state we want the buffer we receive to be in after a flip. You can choose other values here; I go with UNDEFINED
because I usually just always repaint the entire scene every frame, so I don’t care what state the buffer I receive is in — I fully overwrite that state every time.
Once we’ve created the BufferStrategy
on the JFrame
, we retrieve that BufferStrategy
and use it to get a Graphics
object that we can use to actually draw something. In the above example, I just draw a red oval with a black border, but obviously, you can change this to be whatever you need for your game.
Finally, we dispose of the Graphic
object after each frame, and then invoke the show()
method on the BufferStrategy
, which flips between the two buffers and causes whatever you drew to become visible on the screen. And at this point, although not necessary, I like to call Thread.yield()
to signal that this is a good point to let other threads have a chance to run.
Note that the rendering loop happens in its own thread, and not the Swing thread. First of all, this is safe, as documented in Oracle’s page on Passive vs. Active. The specific subset of the Swing API we’re using here doesn’t need to execute on the Swing thread. Second, while we could have run something similar to this code on the Swing thread anyway, I recommend not doing so. It’s easy to accidentally do something blocking or long-running, and if you do that on the Swing thread, that’ll cause the UI to become unresponsive (in Windows, this tends to show up as the application getting this semi-transparent grey overlay on top of it). Instead, if you do the long-running or blocking operation on another thread, the UI will remain responsive.
With that, you’ve got your basic render loop finished. There are obviously several more steps to make this into a full game, but that’s outside the scope of this article.