Contemporary Games Can Take Up To Three Years To Develop

Game Developer on Ulitzer

Subscribe to Game Developer on Ulitzer: eMailAlertsEmail Alerts newslettersWeekly Newsletters
Get Game Developer on Ulitzer: homepageHomepage mobileMobile rssRSS facebookFacebook twitterTwitter linkedinLinkedIn


Game Developer Authors: William Schmarzo, Shelly Palmer, Steve Mordue, Qamar Qrsh, David Balaban

Related Topics: Java Developer Magazine, Game Developer

Java Developer : Article

Java Gaming: 2D Rendering

Part 2 looks at 2D rendering and performance details

Part 1 of this article ("Java Gaming: Understanding the Basic Concepts," [JDJ, Vol. 9, issue 10]) covered the basics of a game framework. Part 2 goes into more depth on the actual 2D rendering specifics and the resulting demo: the Ping program (see Figure 1).

2D Rendering
Game rendering is a subject that has great depth and complexity. This article focuses on the topics that we believe are the most important to 2D games and Java games programmers:

  • Fullscreen and DisplayMode management
  • Buffering
  • Images
  • Video memory constraints
  • Performance tip: intermediate images
Fullscreen and DisplayMode Management
A game developer must decide whether to run a game in fullscreen mode (where it occupies the entire monitor display) or windowed mode (where it is one of many windows on the user's desktop). Both modes are appropriate for different types of games. For example, a game that is supposed to be all-consuming while being played would work best in fullscreen mode, allowing it to take over the machine and let the user be completely mesmerized by the experience. Alternatively, a game that is fun to dive into and out of briefly, such as a card game, may best be one of many tasks on a user's monitor (allowing them to pretend to actually get real work done at the same time).

Another approach is to be flexible and allow the game to run in fullscreen mode or in windowed mode. This property could be user configurable and the game written to work well either way.

Fullscreen Mode
To put your game into fullscreen mode first create a Frame, make it undecorated (this removes the window decorations such as the title bar and close/resize icons), and then tell the appropriate GraphicsDevice to switch into fullscreen mode on that Frame:


Frame gf;
void initFullScreen(GraphicsDevice gd) {
  // initialize the main app frame
  gf = new Frame("Game Frame");
  gf.setUndecorated(true);
  // disable repaint mechanism
  gf.setIgnoreRepaint(true);
  // the next call shows the window
  gd.setFullScreenWindow(gf);
 }

The call to setIgnoreRepaint() is used because the game loop described in the first part of the article handles all rendering explicitly, meaning we don't need the usual toolkit mechanism of repainting the window.

DisplayMode
If your game chooses to run in fullscreen mode, you also have the ability to switch the DisplayMode or resolution of the monitor. This is platform dependent and currently exists only on Windows, although we hope to have this capability available for other platforms in the future. The following code is an example of how to set the DisplayMode.


if (gd.isDisplayChangeSupported()) {
  DisplayMode myDM = new DisplayMode(640, 480, 32, 60);
  try {
    gd.setDisplayMode(myDM);
  } catch (IllegalArgumentException iae) {
    DisplayMode dms[] = gd.getDisplayModes();
    for (DisplayMode dm : dms) {
      if (dm.getWidth() == 640 && 
						dm.getHeight() == 480)
					{ 
					  gd.setDisplayMode(dm);
					   return;
      }
    }
  }
}

This code could be used in the initFullScreen() method to set the current resolution to 640 pixels wide by 480 pixels high with 32 bits per pixel color depth and 60 frames per second refresh rate. Appropriate error checking is used, beginning with a check that the system can handle switching display modes (some platforms don't currently allow this; others allow it only when running in fullscreen mode). Then, if the program cannot set the exact display mode that it would prefer, it iterates over the possibilities, looking for a match that is close enough.

Buffering
Games, or any applications that want fast and smooth animation, should use buffering instead of drawing directly to the screen. Drawing to the screen works fine in cases such as simple and static GUIs; however, for constantly changing graphics, rendering each item to the screen causes flashing artifacts that are disturbing to the user. It's far better to render all objects to a back buffer and then copy the buffer to the screen all at once. This technique is called double buffering and makes for much smoother animations and more enveloping game experiences.

There are several approaches game developers can take in creating a double-buffered application.

Use Swing
Swing already has a back buffer built in. When the program renders to the Graphics object passed to an overridden paintComponent() method in a Swing component subclass, it's usually rendering to the Swing back buffer. This buffer is eventually copied to the screen. You as a developer do not have to worry about the details of this buffer; your program renders things into the right places and Swing takes care of the rest.

This approach works well for many simple games, but for games that want full control over all of the rendering aspects (such as those that use their own rendering loop), leaving the buffering under the control of other code such as Swing may not be sufficient. These games may want to control exactly when that buffer is rendered to and copied to the screen. For example, to achieve a consistent frame rate it's necessary to control exactly when rendering and buffering occur. For these applications Swing's double buffering is not the answer.

Manual Buffer Image Creation and Management
The traditional way to do double buffering manually is to create an offscreen image, render to that image in the main game loop, and then call Graphics.drawImage() from that image to the screen Graphics when the frame rendering is complete.

In JDK 1.4, the new twist to this approach was the introduction of VolatileImage, which made it possible to create the offscreen image in hardware-accelerated memory on the graphics device.

BufferStrategy
Also in JDK 1.4, the new API of BufferStrategy was introduced. This API boils down the actual work in managing buffers into the basics that a developer needs, allowing the program to create a back buffer (which can be either a Flip or Blt buffer, discussed below), get the Graphics for that buffer, render to the Graphics, and then tell the buffer to show() itself. These buffers may get hardware accelerated (by using VolatileImage under the hood) without the hassles inherent in managing VolatileImages. Moreover, the ability for BufferStrategy to use the most appropriate back buffer (Flip or Blt) means that you always use the same API and let the BufferStrategy implementation take care of the details.

Creating and using a BufferStrategy is shown below. Assuming the same gf Frame variable used in initFullScreen() above, the program can initialize the BufferStrategy as follows:


gf.createBufferStrategy(2);
// 2 means one back buffer and one screen (2 total)
BufferStrategy strategy = gf.getBufferStrategy();

and use it later in the rendering process:


Graphics g = strategy.getDrawGraphics();
// render to the Graphics object appropriately
// ...
// now show the back buffer on the screen
strategy.show();

Flipping and Blitting Buffers
When an application is running in fullscreen mode on some platforms, createBufferStrategy may create a FlipBufferStrategy. Otherwise, a BltBufferStrategy will be used. FlipBufferStrategy will get the buffer contents onto the screen by a simple pointer switch (swapping the pointers for the back buffer and the screen memory). The BltBufferStrategy will always copy the contents onto the screen (by calling Graphics.drawImage() internally).

Buffer flipping is an inherently faster operation than buffer copying since it requires only a pointer swap. However, flipping will usually wait for the next vertical refresh event to occur. This means that you may actually get a slower frame rate overall even though the actual flip operation can happen very quickly. This slowdown is because you are now pegged at a maximum of the refresh rate of the video card (60 times per second, or whatever it is set at).

Another implication of buffer flipping is that your application will have smoother animations in general because the buffers are swapped instantly at a time when there are no other changes on the screen (because the flip happens between vertical refreshes). Imagine if the screen was in mid-repaint and was halfway through drawing an object that is currently moving in your game. If you flip buffers, the refresh will finish before the buffers swap, thus that whole frame remains consistent to itself. If you copy from the back buffer to the screen while the refresh is happening (as would be the case with BltBufferStrategy), you might get a "tearing" artifact where that moving object is seen with its top half in the previous location and its bottom half in the new location during the same screen refresh. This tearing artifact is seen in Figures 2-4.

In general, we advise using BufferStrategy for game programming; it gives game developers the hands-on control they desire while taking care of many of the tedious details of buffer management for you.

Images
There are mainly two types of images used heavily in games.

Sprites
Sprites are images that are rendered to seldom or never (perhaps they are loaded or created/rendered once at startup and never touched again), but are copied from quite often, perhaps once or more per frame. Examples include icons in a GUI or player character images.

Backbuffer
This is an image that is rendered to often (say, several times per frame) and copied from once per frame.

In Java, the best images to use in these cases are:

  • Managed images for sprites: These are images whose main copy is stored in the Java heap (such as a BufferedImage), but for which we might create a cached accelerated copy in video memory. Although operations to the image are not hardware-accelerated (because we don't enable hardware acceleration for rendering to Java heap-based images), it is the copies from the images that you really care about (since these are the operations that occur over and over) and we will, if possible, accelerate those operations.
  • VolatileImage orBufferStrategy for Backbuffer: Only by using these APIs can you create a back buffer that is capable of accelerating rendering both to and from the image.
ImageIO
Note also that ImageIO is a good package to keep in mind for general image loading and saving. This package was new in 1.4 and was created to be a more general purpose and robust image reading/writing facility than the old image-loading APIs of previous releases. The following are some of the reasons to consider using ImageIO for your future applications.

Synchronous
The old image APIs assumed that you wanted to load your images asynchronously and then deal with them when they were loaded. This is still a valid way to go for many uses, but not all, and people end up forcing synchronization by either using the MediaTracker API or by using ImageIcon (which is merely a Swing wrapper around the Toolkit image facility coupled with MediaTracker).

More Image Formats
There are currently more image formats supported in ImageIO, and any future work we do for further image formats is expected to happen only for the ImageIO APIs. The currently supported formats in ImageIO readers include JPEG, BMP, GIF (read only), WBMP, and PNG.

Pluggable Reader/Writer API
If you have some other custom image format that you need to use, you can write a plugin for ImageIO instead of writing the entire image loading/saving framework from scratch.

More Hands-on Capability
The old image APIs did not encourage image manipulation; you could load an image, but you couldn't really get at the bits (either the pixel data or the metadata) very easily. ImageIO supports both easier pixel access (it creates BufferedImage objects) as well as metadata access. More control equals more power, and more power is a wonderful thing.

All Images Are Modifiable
The old image APIs created images that were read-only. The ImageIO images (BufferedImage objects) are all writeable.

Note too that ImageIO images (in addition to all other image types) are "managed" in JDK 5.0; we'll automatically accelerate copies from these images as described above under the "Managed Images for Sprites" discussion.

Loading an image using ImageIO can be done with the following snippet:


BufferedImage bi = ImageIO.read(new File("Duke.png"));

Video Memory Management
Accelerated video memory (VRAM) can be a very constrained resource on some machines, especially ones with low-end graphics cards, such as most laptops. When a program creates images to live in video memory, it's using up that scarce resource in a way that may force other, more important, images to live in system memory instead.

As with any scarce resource, it's important to manage VRAM carefully, especially with a performance-oriented application that benefits from having exactly the right images accelerated at exactly the right times.

Currently, VRAM must be tracked manually using simple methods to inquire about the availability of VRAM:


GraphicsDevice.getAvailableAcceleratedMemory();

and force images to dispose of any associated VRAM resources:


Image.flush();

In addition, you can use the ImageCapabilities API to determine whether any given image is accelerated:


Image.getCapabilities(GraphicsConfiguration gc);
ImageCapabilities.isAccelerated();

Performance Tip: Intermediate Images
We will leave you with one general performance tip for 2D rendering: if you ever need to render anything even mildly complex several times, consider prerendering it as an image first and then simply calling drawImage() from that image from then on. Consider the following examples.

Transformed Images
Suppose you have an original image (say, a sprite) that you want to transform (scale, rotate, whatever) prior to rendering, and then you intend to render it in this transformed way several times. It's probably much faster to create an intermediate image to hold the transformed result and then simply call drawImage() from that intermediate image than it is to make us transform the original image every time you render it.

Text
Currently we render text through software routines (except in our new, cool OpenGL rendering pipeline, available in JDK 5.0 but disabled by default). We'll eventually cache text characters (glyphs) as accelerated images, but you could do the same in the meantime. In fact, you could do even better than that: you could cache entire strings as images. Say, for example, you want to display the string "Score:" at the top of the screen on every frame. It's always the same text, in the same font, at the same size, and in the same color. Why not create an intermediate image, render the text to that image, and then call drawImage() from that intermediate image from then on? It's far faster for us to copy from that image than to go through the work of rendering the actual characters every time. As if these advantages were not enough, you can take the same approach with such text variations as custom fonts, anti-aliased text, and large font sizes, all without any performance penalty that you might currently experience by using our default text rendering; once it's an image, all we need to do is copy it.

Anything
It's easy to see that the above approach of rendering transformed images or text into intermediate images could be taken with any kind of rendering whatsoever. A snowflake that consists of a lot of lines? A complex Shape that takes a long time to render? An icon you copy around often? All you need to do is create the image, get the Graphics for it, render your object to the image, and then call drawImage thereafter whenever you would have otherwise called the actual rendering operations.

There are (of course) a couple of important details in this approach that we should mention.

Image Type
You'll need to create an image of the appropriate type for your desired objective. For example, if you are using an intermediate image to render text, you'll need a transparent image so that the background of that image does not get copied along with the text characters. Similarly, if you are using an intermediate image for a rotated version of your image, you probably want the area outside that rotated image to be transparent. If you are using an intermediate image to do antialiasing or translucent rendering, you'll obviously want a translucent image. Note that copies from translucent images may be much slower than copies from opaque or transparent images, so you may want to test the alternatives to determine what will work best for your situation.

Size vs Speed
The only downside to using intermediate images is that you are trading potentially faster performance for an increased memory footprint; every one of those additional intermediate images consumes a chunk of memory. You need to determine whether that increased memory usage is worth the trade-off of better performance.

Intermediate Images: For More Information
This topic is covered in more depth with sample code on Sun's developer site; check out the article "Intermediate Images" at http://java.sun.com/developer/technicalArticles/Media/intimages.

Demo: Ping
The source code and related information for this demo can be found at http://ping.dev.java.net.

We wrote the Ping application to demonstrate points that we were trying to make around the game framework described in Part 1 and general 2D game programming issues and performance tips. It's not supposed to be a game that lasts you through months of sleepless nights (or even through a half hour of twiddling). It should, however, be a fairly complete example of a simple game framework and should have enough sample code and algorithms in it to show how you might use similar approaches in a game that is more complete and awesome. In short, we expect you to spend far more time with the Ping code than you will playing the actual Ping game. We hope that you can use the code and approaches in it to create something truly great.

The code should stand pretty much on its own, especially given that Ping uses the game framework and 2D rendering tips described in this two-part article. However, as with any application that takes more than 50 lines of source code, a little high-level explanation would probably be helpful. The next section will introduce the game and describe the architecture at a high level. It will also call out methods and approaches of particular interest, especially in the context of this article. However, we don't intend to cover everything of interest in the demo here; we would suggest you dive into the code to see for yourself how it all works.

Game Description
I can't imagine anyone who has read this far into the article who has not seen a game similar to Ping, so a description of the game is probably redundant. But in the interests of completeness...

Ping is a simple 2D version of tennis, where the ball bounces around the playing area. The object of each player (represented by paddles on the left and right) is to keep the ball in play on his or her side. As long as you keep hitting the ball back, the game goes on. When a player misses the ball, the ball hits the back wall behind that player's paddle and that player loses a life. When three lives are lost, the game is over and the other player wins.

Code Overview
The application starts in Ping.java; this is where the main() method is that starts everything. This is also where the main game loop (run()) and main render function (render()) reside; these functions are very similar to what we described in Part 1 of this article.

Some of the other classes worth calling out here include:

  • DynamicGameObjects: This class keeps track of all movable objects in the game, which includes the ball and the left and right paddles. In general, this class forwards actions on dynamic objects to all of the applicable objects. For example, when DynamicGameObjects.render() is called, that function simply calls render() on the ball and paddles.
  • ForegroundObjects: This class keeps track of all the objects that will be painted on top of the game, such as the score. See Hud, below, for more details.
  • StaticGameObjects: This class manages the placement and rendering of the game boundaries (the walls).
  • Paddle: This class is responsible for rendering each paddle (the position is dependent on whether the paddle was instantiated as the right or left paddle) as well as tracking events. The Paddle instances listen for keyboard events and accumulate the events as described in Part 1 of this article. Then later Dynamic-GameObjects.gatherInput() is called, which defers to Paddle.gatherInput() to actually process the accumulated input events. The input events are processed to determine total down time as well as acceleration and, finally, paddle position for this frame.
  • Ball: Holds the position and trajectory information for the ball and is responsible for updating that data through the updatePosition() method. This class also handles the collision logic for the ball in processCollisions().
  • Collider: Handles the collision detection algorithm. It uses a simple parametric collision approach, where it detects the time at which a collision occurs for both the walls and the paddles.
  • Background: Simply holds a background image for the game screen and handles rendering that image to the back buffer for every frame. Note the use of an intermediate image (bgImage-Scaled) to cache the scaled version of the image so that we need only copy the image and not scale it on the fly.
  • GameLogic: Handles all of the information and state for making the game playable instead of just having a ball bouncing around on the screen.
  • Hud: Handles the state and rendering of the "Heads Up Display," or the GUI that sits on top of the game. This includes things like the score and the help pane.
  • PingFrame: This class extends JFrame and customizes our window for the game, including switching into fullscreen mode, setting the display mode, and handling keyboard events that affect the overall game.
  • SoundFactory: Handles much of the simple audio processing, such as loading, starting, and stopping sound clips.
  • ZoomyBackground: This part of Ping is completely gratuitous, but we thought it looked cool. It's based on code that Jim Graham (another member of the Java 2D team) developed for a previous JavaOne conference to show some of the simple yet cool effects you can achieve with Java2D. We use this class as a mindless intermission animation while waiting for the user to start the game.
That should do it for the explanation of the program; we suggest you dive in at this point to see how things work.

Summary
This article outlined a generic game framework that, although light on details, covers the essentials for what most games would need to do. It described some of the issues that game developers face, in terms of both algorithms and complexity as well as Java-specific programming.

The framework and examples developed in this article are mostly suitable for simpler 2D games; the complexities and issues in 3D programming are very specific to those types of games and we did not get into those rather more involved issues here. Note, however, that the main framework and rendering issues covered here are also issues in the 3D space, so anything gleaned here can only help in the larger world of 3D development.

Since there is so much that we didn't cover (and didn't have any hope of covering; game development is a hugely complex area of programming), it's worthwhile to pursue some of the resources we have listed below, as well as many other good sources of information in books and Web sites.

Resources

  • The Ping source code and related information: http://ping.dev.java.net
  • Information and forums specific to Java desktop client development: http://javadesktop.org
  • Information and forums specific to Java game development. Projects include sample games as well as core game development technologies such as the Java OpenGL bindings: http://javagaming.org
  • Information, forums, and projects for all Java developers: http://java.net
  • Information about the overall Java platform, including articles such as the "Intermediate Images" mentioned above: http://java.sun.com
  • More Stories By Chet Haase

    Chet is an architect in the Java Client Group at Sun Microsystems. He spends most of his time working on graphics and performance issues and futures.

    More Stories By Dmitri Trembovetski

    Dmitri Trembovetski is an engineer in the Java 2D team at Sun Microsystems, Inc., where he focuses on graphics rendering and performance issues for the Solaris and Linux platforms. Read his frequent posts on the forums at http://javagaming.org.

    Comments (1) View Comments

    Share your thoughts on this story.

    Add your comment
    You must be signed in to add a comment. Sign-in | Register

    In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.


    Most Recent Comments
    Anindya 12/30/04 05:32:34 AM EST

    that was very informative for a game enthusiast like me ... i have been working on java and j2ee for the last 2 years ... but i always had a inclination for game programing ... but whenever i searched on the tutorials ... they were using C, C++, VC++ and specialised softwares like 3dsMax7, bryce, maya etc. ... since i am a inexperienced programer ... i was very discouraged ... i thought i had to learn all of them to be succesfull game developer ... but your article has kindled a flame of hope in me ...
    can we build a game like age of empires or max payne in java too? can we incorporate openGL and DirectX with java?? ... can we design a framework or API solely dedicated to game programing??... can we java developers show the C++ deveopers that we too can build absorbing games?? ...i don't know .. may be these are immature questions ... it would be great if someone expert can lend his helping hand to me