Under the sea

Explore an "under the sea" application and its animation engine

In version 1.4 of the Java 2 SDK, Sun Microsystems introduced support for full-screen exclusive mode, an operating system feature that lets programs obtain exclusive access to and render their output to the entire screen, which results in high-performance graphics. (Microsoft Windows implements full-screen exclusive mode via its DirectX technology, for example.) This Java Fun and Games installment reveals this support in the context of an "under the sea" application and its animation engine.

This article first introduces you to the "under the sea" (UTS) application, where you learn about the application's image and audio resources. The article next explores the application's architecture in terms of the application class, the animation and audio classes, and the resource-loader class. You next tour the animation engine's engine class, animation and audio interfaces, and exception classes. In closing, this article focuses on how to deploy the application.

Note
Unlike most of the previous Java Fun and Games installments, which I wrote from the perspective of J2SE 1.4, this installment requires Java SE 5.0. For thorough coverage of Java's support for full-screen exclusive mode, consult the Java Tutorial.

Introducing UTS

I've created a UTS application that works with the animation engine to animate underwater sea life over the entire screen. This application's source code, audio/image resource, and miscellaneous files can be downloaded from Resources.

This figure presents a single animation frame. This frame reveals a suitable background, an animated angel fish, two instances of an animated tiger barb, two instances of an animated zebra fish, two instances of an animated plant, and three instances of an animated bubble sequence.

The figure shows various graphics images that I've combined into a somewhat realistic underwater scene: I created the background image with the Terragen scenery generator and enhanced the resulting image to achieve a bluish haze in the distance; the angel fish, tiger barb, and zebra fish images were made available to me courtesy of Dave Sutton at Sevenoaks Art; and the bubbles/plants images are my own creations.

Each type of fish requires 16 images (8 for moving left to right, and 8 for moving right to left), the bubble sequence requires 8 images, and the plant animation requires 3 images. The background image and one other image (used for hiding the mouse cursor) round out the image resources. These image resources are organized into gif files that are stored in a hierarchical resource directory structure, rooted in the images directory:

images
  angelfish
    lfish1.gif
    lfish2.gif
    lfish3.gif
    lfish4.gif
    lfish5.gif
    lfish6.gif
    lfish7.gif
    lfish8.gif
    rfish1.gif
    rfish2.gif
    rfish3.gif
    rfish4.gif
    rfish5.gif
    rfish6.gif
    rfish7.gif
    rfish8.gif
  bubbles
    bubbles1.gif
    bubbles2.gif
    bubbles3.gif
    bubbles4.gif
    bubbles5.gif
    bubbles6.gif
    bubbles7.gif
    bubbles8.gif
  misc
    background.gif
    white.gif
  plant
    plant1.gif
    plant2.gif
    plant3.gif
  tigerbarb
    lfish1.gif
    lfish2.gif
    lfish3.gif
    lfish4.gif
    lfish5.gif
    lfish6.gif
    lfish7.gif
    lfish8.gif
    rfish1.gif
    rfish2.gif
    rfish3.gif
    rfish4.gif
    rfish5.gif
    rfish6.gif
    rfish7.gif
    rfish8.gif
  zebrafish
    lfish1.gif
    lfish2.gif
    lfish3.gif
    lfish4.gif
    lfish5.gif
    lfish6.gif
    lfish7.gif
    lfish8.gif
    rfish1.gif
    rfish2.gif
    rfish3.gif
    rfish4.gif
    rfish5.gif
    rfish6.gif
    rfish7.gif
    rfish8.gif

Along with presenting animations, the UTS application is capable of playing an audio clip (in the Sun AU format) to add realism. This audio clip can be toggled off and on by pressing the "A" key.

The audio clip produces an underwater bubbling sound effect. As with both the bubbles and plants images, this audio clip is my own creation. It is stored in the hierarchical resource directory structure, rooted in the audios directory:

audios
  bubbles.au

Application architecture

The architecture of the UTS application divides into an application class, several animation and audio classes, and a class for loading audio and image resources. This section's exploration of these classes illustrates how to create an application that works with the animation engine.

Application class

The UTS application class specifies the following:

  • Constants describing the desired display mode and the desired animation frame rate (the number of animation frames to render each second)
  • Method public static void main(String [] args) to create various animation and audio objects, and the animation engine; register the animation and audio objects with the engine; run the animations and play audio; shut down the engine; and terminate the application
  • A method for returning random integers

The code below illustrates the UTS class:

 

public class UTS { // Screen width (in pixels)

final static int WIDTH = 800;

// Screen height (in pixels)

final static int HEIGHT = 600;

// Color mode -- 32 indicates true-color (32-bit) mode

final static int BITDEPTH = 32;

// Animation frame rate, measured in frames per second

final static int FPS = 20;

public static void main (String [] args) throws Exception { // Create the animations.

AngelFish af = new AngelFish (WIDTH, HEIGHT-AngelFish.height- rnd (Fish.VERTICAL_RANGE), WIDTH, HEIGHT, 3, AngelFish.LEFT, 0);

Background bg = new Background ();

Bubbles b1 = new Bubbles (168, 251-Bubbles.height, 3); Bubbles b2 = new Bubbles (376, 205-Bubbles.height, 3); Bubbles b3 = new Bubbles (679, 309-Bubbles.height, 3);

Plant p1 = new Plant (20, HEIGHT-Plant.height, 3); Plant p2 = new Plant (WIDTH-Plant.width-20, HEIGHT-Plant.height, 3);

TigerBarb tb1 = new TigerBarb (-TigerBarb.width, HEIGHT-TigerBarb.height- rnd (Fish.VERTICAL_RANGE), WIDTH, HEIGHT, 1, TigerBarb.RIGHT, 2); TigerBarb tb2 = new TigerBarb (-TigerBarb.width, HEIGHT-TigerBarb.height- rnd (Fish.VERTICAL_RANGE), WIDTH, HEIGHT, 2, TigerBarb.RIGHT, 1);

ZebraFish zf1 = new ZebraFish (-ZebraFish.width, HEIGHT-ZebraFish.height- rnd (Fish.VERTICAL_RANGE), WIDTH,

HEIGHT, 2, ZebraFish.RIGHT, 1); ZebraFish zf2 = new ZebraFish (-ZebraFish.width, HEIGHT-ZebraFish.height- rnd (Fish.VERTICAL_RANGE), WIDTH, HEIGHT, 3, ZebraFish.RIGHT, 0);

// Create the audio.

BubblesAudio ba = new BubblesAudio ();

// Create an animation engine for a WIDTHxHEIGHTxBITDEPTH screen, where // the overall animation rate does not exceed FPS. Enter fullscreen // exclusive mode and switch the display mode to WIDTHxHEIGHTxBITDEPTH.

AnimationEngine engine; engine = new AnimationEngine (WIDTH, HEIGHT, BITDEPTH, FPS);

// Register the animations with the animation engine. The order in // which animations are registered determines their Z-order -- which // animations cover other animations. Earlier registered animations are // covered by latter registered animations.

engine.register (bg); engine.register (b1); engine.register (b2); engine.register (b3); engine.register (zf1); engine.register (af); engine.register (tb1); engine.register (tb2); engine.register (zf2); engine.register (p1); engine.register (p2);

// Register the audio with the animation engine.

engine.register (ba);

// Run the animations and play the audio.

engine.runAnimations ();

// Shutdown the animation engine, reverting the display mode to the

// display mode that was in effect before creating the animation engine.

engine.shutdown ();

// Terminate the application: System.exit is necessary.

System.exit (0); }

public static int rnd (int limit) { // Return a randomly-selected integer ranging from 0 through limit-1.

return (int) (Math.random ()*limit); } }

Although the UTS class is easy to understand, you might be curious about my decision to create the animation and audio objects before creating the animation engine. Creating the animation and audio objects causes resources to load; cumulative resource loading takes time, and engine creation results in the screen switching to full-screen exclusive mode. If I chose to create the animation and audio objects after the engine, you would observe a blank screen for a few seconds (on slower computers) before the animation starts.

Animation and audio classes

The UTS application includes several classes that describe various animations and render animation frames: AngelFish, Bubbles, Plant, TigerBarb, and ZebraFish—classes AngelFish, TigerBarb, and ZebraFish share a common Fish superclass. A Background class is also present. Although this class is implemented similarly to the other animation classes, the background is not animated.

Each animation class loads its image resources in a static initializer. This implies that the image resources load exactly once (when the class is loaded instead of each time an object is created), which reduces the application's memory usage. In contrast, I chose to have the Background class load its image resource in its constructor—I don't need to create multiple Background objects. The following Bubbles class reveals its static initializer loading its image resources:

 

class Bubbles implements Animation { private static Image imBubbles [];

static int height;

static { imBubbles = ResourceLoader.loadImages ("images/bubbles", "bubbles", 8); height = imBubbles [0].getHeight (null); }

private int x;

private int y;

private int numFramesToSkip;

private int nextFrame;

private int frameCounter;

Bubbles (int x, int y, int numFramesToSkip) { this.x = x; this.y = y; this.numFramesToSkip = numFramesToSkip; }

public void render (Graphics g) { g.drawImage (imBubbles [nextFrame], x, y, null);

if (numFramesToSkip == 0 || ++frameCounter == numFramesToSkip) { if (++nextFrame == imBubbles.length) nextFrame = 0;

frameCounter = 0; } } }

The Bubbles class and the earlier UTS class demonstrate a second advantage to using a static initializer. Because an image's width and height are available after the class has loaded (all images must share the same width and height in a sequence of images), their dimensions can be obtained to help define the animation's initial position. For example, UTS's main() method retrieves the value of Bubbles.height when calculating the upper row for each image in each bubbles animation.

The Bubbles constructor saves both the initial position and a "number of frames to skip" value. This value determines how quickly the animation moves to the next frame. Larger values slow down this movement, which lets you assign different speeds to animations.

Assigning a speed to an animation is not the same thing as specifying how quickly an animation moves across the screen. For example, the Fish class adjusts a fish's movement by adding or subtracting a velocity to/from its current horizontal position—x += vx; and x -= vx;. The animation speed lets you determine how quickly the fish moves its fins and tail; the "movement across the screen" speed lets you determine how quickly the fish reaches the left or right side of the screen.

Application UTS also specifies a BubblesAudio class for retrieving an audio clip and a delay factor. After creating a BubblesAudio object and registering it with the animation engine, no further work is required because the engine invokes the methods at the appropriate times:

 

class BubblesAudio implements Audio { private AudioClip ac;

BubblesAudio () { ac = ResourceLoader.loadAudioClip ("audios/bubbles.au"); }

public AudioClip getAudioClip () { return ac; }

public int getDelay () { return UTS.FPS+UTS.rnd (100); } }

Resource-loader class

The ResourceLoader utility class conveniently loads the application's audio and image resources, hiding the loading details to support future implementation changes. As you've previously seen, its methods are invoked by the various animation and audio classes. Methods include:

  • public static AudioClip loadAudioClip(String name): Loads the audio clip identified by name and returns this clip as a java.applet.AudioClip. If the audio clip cannot be loaded, null returns.
  • public static Image loadImage(String name): Loads the image identified by name and returns this image as a java.awt.Image. If the image cannot be loaded, null returns.
  • public static Image [] loadImages(String dir, String prefix, int count): Repeatedly calls the previous method to load count images from directory dir into an array. Names share a common prefix and are sequentially numbered, starting with 1.

Animation engine

The animation engine consists of an engine class, animation and audio interfaces, and exception classes. This section's tour of these classes and interfaces teaches you about full-screen exclusive mode and gives you the necessary knowledge for customizing the animation engine.

Engine class

The AnimationEngine class is the heart of the animation engine. This class provides a constructor, animation, and audio registration methods, a method to run the animations and play the audio, and a method that shuts down the animation engine:

  • public AnimationEngine(int width, int height, int bitDepth, int fps): Constructs the animation engine, entering full-screen exclusive mode with the appropriate display settings.
  • public void register(Animation animation): Registers an animation object with the animation engine. Two of this object's methods are called by runAnimations().
  • public void register(Audio audio): Registers an audio object with the animation engine. One of this object's methods is called by runAnimations().
  • public void runAnimations(): Enters a loop that runs registered animations and plays registered audio. The loop continues until the Esc key is pressed or a mouse button is clicked.
  • public void shutdown(): Shuts down the animation engine by terminating full-screen exclusive mode, restoring the display mode to the mode that was previously in effect.

The AnimationEngine constructor initializes the animation engine by performing a variety of tasks. Tasks related to full-screen exclusive mode include making sure that full-screen exclusive mode is supported, creating a display mode, switching to full-screen exclusive mode, establishing the display mode, and creating a buffer strategy that determines the form of buffering used to obtain high-performance graphics output. These and other tasks are illustrated in the following AnimationEngine excerpt:

 

public AnimationEngine (int width, int height, int bitDepth, int fps) throws NoFullScreenException, UnsupportedDisplayModeException { GraphicsEnvironment ge; ge = GraphicsEnvironment.getLocalGraphicsEnvironment ();

monitor = ge.getDefaultScreenDevice (); if (!monitor.isFullScreenSupported ()) throw new NoFullScreenException ();

DisplayMode myMode; myMode = new DisplayMode (width, height, bitDepth, DisplayMode.REFRESH_RATE_UNKNOWN);

DisplayMode [] availModes = monitor.getDisplayModes ();

int i = 0; for (i = 0; i < availModes.length; i++) if (availModes [i].getWidth () == myMode.getWidth () && availModes [i].getHeight () == myMode.getHeight () && availModes [i].getBitDepth () == myMode.getBitDepth ()) break;

if (i == availModes.length) throw new UnsupportedDisplayModeException ();

this.width = width; this.height = height; this.fps = fps;

animations = new ArrayList<Animation> ();

Frame frame = new Frame (); frame.setUndecorated (true); frame.setResizable (false); frame.setIgnoreRepaint (true);

frame.addKeyListener (new KeyAdapter () { public void keyPressed (KeyEvent e) {

if (e.getKeyCode () == KeyEvent.VK_ESCAPE) done = true; }

public void keyTyped (KeyEvent e) {

if (e.getKeyChar () == 'a') playAudio = !playAudio; } });

frame.addMouseListener (new MouseAdapter () { public void mousePressed (MouseEvent e) { done = true; } });

monitor.setFullScreenWindow (frame); monitor.setDisplayMode (myMode);

frame.createBufferStrategy (2); strategy = frame.getBufferStrategy ();

Toolkit toolkit = Toolkit.getDefaultToolkit (); Image imWhite = ResourceLoader.loadImage ("images/misc/white.gif"); Cursor hidden = toolkit.createCustomCursor (imWhite, new Point (0, 0), ""); frame.setCursor (hidden); }

After obtaining the graphics environment and identifying the environment's default screen device, the constructor invokes java.awt.GraphicsDevice's public boolean isFullScreenSupported() method to determine if full-screen exclusive mode is supported for this device. If full-screen exclusive mode is supported, this method returns true. Otherwise, this method returns false, which leads to a thrown exception.

Moving forward, the values passed in AnimationEngine's width, height, and bitDepth parameters are passed to java.awt.DisplayMode's public DisplayMode(int width, int height, int bitDepth, int refreshRate) constructor (along with a constant that indicates unknown display refresh rate), and a display mode is created. If this mode matches a display mode supported by the default screen device, the engine is created; otherwise, an exception is thrown.

Java's support for full-screen exclusive mode requires a window to be created that will occupy the entire screen. This window is either a java.awt.Window instance or a subclass of Window—like java.awt.Frame. After creating the Frame instance, the constructor configures this instance to make sure that the frame is not decorated, the frame cannot be resized, and any paint messages sent to the frame by the underlying operating system are ignored.

The GraphicsDevice class provides a public void setFullScreenWindow(Window w) method for switching to and from full-screen exclusive mode. This method takes a java.awt.Window (or subclass) argument that identifies the window to use as a full-screen window. After creating and configuring the Frame instance, the engine's constructor passes this instance to setFullScreenWindow(), which immediately enters full-screen exclusive mode.

The GraphicsDevice class also provides a public void setDisplayMode(DisplayMode dm) method that switches a screen device's display mode to the specified display mode. The engine's constructor invokes this method to switch the default screen device's display mode to the previously established mode. This display mode exists for as long as full-screen exclusive mode is in effect. As you will see, shutting down the animation engine restores the previous display mode.

Following the establishment of the display mode, the AnimationEngine constructor establishes a buffer strategy—a buffering technique for improving the performance of graphics output—for the previously created frame. The public void createBufferStrategy(int numBuffers) method is called to establish the best-possible double-buffering strategy. This strategy is subsequently returned by invoking public BufferStrategy getBufferStrategy(), which I discuss later.

Note
The AnimationEngine constructor reveals a three-step technique for hiding the mouse cursor. The first step loads a 32-by-32-pixel transparent GIF. Step 2 invokes java.awt.Toolkit's public Cursor createCustomCursor(Image cursor, Point hotSpot, String name) method to convert this image into a java.awt.Cursor. And the third step establishes this cursor as the cursor for the frame window that serves as the full-screen exclusive mode window.

The runAnimations() method handles all animation and audio-playing responsibilities. When invoked, this method performs various setup tasks and enters an animation loop, which continues until the user requests that the loop terminate:

 

public void runAnimations () { int frameDelay = 1000/fps;

AudioClip ac = audio.getAudioClip ();

int framesToWait = audio.getDelay ();

int framesCounter = 0;

while (!done) { startMillis = System.currentTimeMillis ();

do { do { Graphics g = strategy.getDrawGraphics ();

for (int i = 0; i < animations.size (); i++) animations.get (i).render (g);

g.dispose (); } while (strategy.contentsRestored ());

strategy.show (); } while (strategy.contentsLost ());

while (System.currentTimeMillis ()-startMillis < frameDelay) { if (!clipPlayed && ac != null && playAudio) { if (++framesCounter == framesToWait) { ac.play (); framesToWait = audio.getDelay (); framesCounter = 0; }

clipPlayed = true; } }

clipPlayed = false; } }

The animation loop—the while (!done) loop—runs at a specific frame rate. As revealed by the excerpt, this rate is used to calculate a frame delay in milliseconds. The startMillis = System.currentTimeMillis (); assignment statement establishes the start time for the next animation frame. After all rendering has completed, while (System.currentTimeMillis ()-startMillis < frameDelay) iterates until the frame delay has passed.

For measuring frame rate, the frame-delay while loop is more accurate than a thread sleep. Thread sleep adds a number of milliseconds to the time taken to render all animation frames. In contrast, the frame-delay while loop adds frameDelay and less rendering-time in milliseconds, achieving an exact frame rate. However, if the computer is too slow or if ac.play (); takes extra milliseconds to execute before the frame-delay while loop ends, momentary animation delays—the jitters—will occur.

Note
Animations will not necessarily run at the frames-per-second rate—they may run slower due to the computer's processor speed, the number of running processes and threads, and other factors. However, frames per second does guarantee that the animations will never run faster than this rate. This means that you'll never see the fish move across the screen in a blur on a very fast computer, for example.

After obtaining the start time for the next animation frame, the current iteration of the animation loop enters a pair of nested do-while loops that work with four java.awt.Image.BufferStrategy methods to render this frame:

  • public abstract Graphics getDrawGraphics(): Returns a graphics context that is passed to each animation's render() method. A new context must be obtained prior to the rendering; it is disposed of when rendering completes.
  • public abstract boolean contentsRestored(): Returns true if the drawing buffer has been recently restored from a lost state and reinitialized to the default background color (white).
  • public abstract void show(): Makes the next available buffer visible by either copying the memory (blitting) or changing the display pointer (flipping)—depending on the buffer strategy.
  • public abstract boolean contentsLost(): Returns true if the drawing buffer was lost since the last call to getDrawGraphics().

To stop the animation engine, UTS invokes AnimationEngine's shutdown() method. This method invokes the monitor object's setFullScreenWindow() method with a null argument, which exits full-screen exclusive mode and restores the previous display mode:

 public void shutdown ()
{
   monitor.setFullScreenWindow (null);
}

Animation and audio interfaces

The animation engine relies on interfaces to separate animations and audio details from the engine itself. This separation makes possible future animation and audio changes to UTS without having to change the engine—it is a perfect example of interface usefulness.

The Animation interface presents a single rendering method: public void render(Graphics g), which renders a new animation frame using the methods in the java.awt.Graphics argument and advances (as necessary) the animation to the next animation frame. The Graphics argument can be cast to a java.awt.Graphics2D.

The Audio interface presents a pair of methods:

  • public AudioClip getAudioClip(): Returns an AudioClip that is played by the animation engine in a loop. If null returns, the animation engine ignores the audio. The animation engine calls this method exactly once, before entering the animation loop.
  • public int getDelay(): Returns an integer that specifies a delay measured in animation frames. The animation engine calls this method immediately after starting to play the audio clip, which lets UTS introduce a random factor in determining when the clip plays.

Exception classes

I've designed AnimationEngine's constructor to throw exceptions. The first exception, an instance of the NoFullScreenException class, is thrown if full-screen exclusive mode is not supported. The second exception, an instance of the UnsupportedDisplayModeException class, is thrown if the combination of width, height, and bit-depth passed to the constructor—the display mode—is not supported. These exceptions are checked, which means that they must be handled or declared in a method's throws clause.

Deploying UTS

Although UTS is easy to compile (javac UTS.java) and run (java UTS), it is cumbersome to deploy this application's classfiles, resource files, and resource directories. I've anticipated this challenge by employing ResourceLoader.class.getResource (name) in the ResourceLoader class. This method call can return the named resource from a jar file, such as the jar file that the following JAR command creates for the UTS application:

 jar cfm uts.jar manifest.mf *.class audios images

The JAR command assumes that the current directory contains UTS classfiles, an audios directory, and an images directory. It creates a uts.jar file with the main class identified by manifest.mf's Main-Class: UTS entry.

Following uts.jar's creation, you can easily deploy this jar file to any computer that runs Java SE 5.0 (or higher). To launch the UTS application stored in this jar file, invoke the following command:

 java -jar uts.jar

Conclusion

Working with full-screen exclusive mode isn't difficult, as demonstrated by this article's animation engine and UTS application. I encourage you to modify this engine and share your improvements with fellow readers. Perhaps I'll post your improved engine (giving credit where credit is due) in a future article. You might also want to consider introducing new animations into UTS, modifying this application's background (possibly making it animated), and replacing the audio. Have fun!

Jeff Friesen is a freelance software developer and educator specializing in C, C++, and Java technology.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies