CroftSoft / Library / Tutorials / Java Swing Sprite Animation

Title | Applet | Background | AnimationCanvas | Resources


AnimationCanvas

Our first class is AnimationCanvas, a Swing JComponent subclass implementation which periodically generates requests to repaint itself. Each time it is repainted, it updates the positions of the sprites and then redraws them at their new locations. The reader is encouraged to take a moment to review the class source code file AnimationCanvas.java in its entirety before proceeding on to the following line-by-line explanation.


package com.croftsoft.core.sprite;



import java.awt.Graphics;

import javax.swing.JComponent;



import com.croftsoft.core.lang.NullArgumentException;

import com.croftsoft.core.lang.lifecycle.Lifecycle;

import com.croftsoft.core.util.Metronome;



/*********************************************************************

Class AnimationCanvas exists in subpackage com.croftsoft.core.sprite, a library of reusable sprite animation code. It imports other classes from the com.croftsoft.core package hierarchy, a collection of reusable Java classes available to the reader from the CroftSoft Code Library under the terms of an Open Source license. These classes include the following:

  • NullArgumentException
    An IllegalArgumentException subclass that ensures method arguments are never null.
  • Lifecycle
    An interface describing the semantics for objects that go through the common init(), start(), stop(), destroy() lifecycle as originally defined in Applet.
  • Metronome
    Periodically runs a task such as repainting a scene at a specified frequency. Metronome can be stopped and restarted as necessary.


public final class  AnimationCanvas

  extends JComponent

  implements Lifecycle

//////////////////////////////////////////////////////////////////////

AnimationCanvas is a Swing-based JComponent subclass. It implements the interface Lifecycle methods described above so that its animation can be stopped and restarted as necessary. The class is declared final to discourage inappropriate subclass implementations.


//////////////////////////////////////////////////////////////////////

{



/** 41 ms (film quality animation of 24 frames per second). */

public static final long  DEFAULT_REPAINT_PERIOD = 1000 / 24;



//



private final Metronome  metronome;



//



private UpdaterSource  updaterSource;



private PainterSource  painterSource;



private boolean        isUpdating;

DEFAULT_REPAINT_PERIOD is the initial value for the time interval between repaint requests. It is declared public so that it can be referenced as the initial and default restore value for programs that manipulate the repaint period.

The internal reference to a Metronome is used to start, stop, and restart animation at a specified frame rate.

The UpdaterSource and PainterSource interface references are used to provide AnimationCanvas with the arrays of Updater and Painter instances required during the repaint operation. These references are not final as they may be changed during runtime.

The isUpdating flag is used to determine whether a repaint request should force an incremental update of the Painter positions or simply refresh the old scene as it was.


/*********************************************************************

* Main constructor.

*********************************************************************/

public  AnimationCanvas (

  UpdaterSource  updaterSource,

  PainterSource   painterSource )

//////////////////////////////////////////////////////////////////////

{

  setUpdaterSource ( updaterSource );



  setPainterSource  ( painterSource  );



  setOpaque ( true );



  metronome = new Metronome (

    new Runnable ( ) { public void  run ( ) { repaint ( ); } },

    DEFAULT_REPAINT_PERIOD, true );

}

Methods setUpdaterSource() and setPainterSource() simply save references to the UpdaterSource and PainterSource passed in as constructor arguments.

JComponent method setOpaque() is used to indicate whether a subclass implementation has any transparent areas. For reasons of efficiency, the AnimationCanvas subclass marks itself as opaque, i.e., it has no transparent regions, so that any components behind and completely obscured by this object will not be drawn unnecessarily. If AnimationCanvas were to be displayed as an odd shape such as a circle, it would be necessary to set opaque to false so that components behind the transparent regions would be repainted as necessary. For this implementation, however, a simple rectangular display is assumed.

The Metronome constructor requires an instance of Runnable to be executed periodically. The AnimationCanvas constructor uses an anonymous inner class implementation of Runnable which simply delegates calls to its run() method back to the AnimationCanvas repaint() method inherited from superclass Component.

The initial value for the time interval between repaint requests is set to the DEFAULT_REPAINT_PERIOD. Film provides the eye with the illusion of smooth motion by displaying sampled snapshots of the world at the rate of 24 frames per second (fps). By calling its repaint() method at intervals of 1/24th of a second per frame, AnimationCanvas can achieve a similar result. Note that since the repaint period is given in milliseconds, the value provided should be 1000 divided by the desired frame rate in frames per second (1000/fps).

The third argument to the Metronome contructor method, a boolean value of true, indicates that the Metronome instance should use a subordinate, or "daemon", Thread to periodically run the Runnable. Daemon threads differ from normal threads in that they terminate automatically when all normal threads have expired. This makes them ideal for running fire-and-forget background processes. By specifying the use of a daemon thread, AnimationCanvas ensures that a containing program can complete without requiring explicit control of the Metronome thread.


/*********************************************************************

* Convenience constructor.

*********************************************************************/

public  AnimationCanvas ( )

//////////////////////////////////////////////////////////////////////

{

  this ( EmptyUpdaterSource.INSTANCE, EmptyPainterSource.INSTANCE );

}

The zero argument convenience constructor provides default values for the main constructor arguments.

Interface implementations EmptyUpdaterSource and EmptyPainterSource simply return, respectively, zero-length, or "empty", arrays Updater.EMPTY_ARRAY and Painter.EMPTY_ARRAY when their getUpdaters() and getPainters() methods are called.

As these classes are stateless, it is unnecessary to have more than one instance of each loaded into memory at a time. This is enforced by using an implementation of the design pattern "Singleton" in which a single predefined instance, INSTANCE, is provided as a public static class variable while at the same time the constructor is declared private to prevent further instance creation.


public void  setUpdaterSource ( UpdaterSource  updaterSource )

//////////////////////////////////////////////////////////////////////

{

  NullArgumentException.check ( updaterSource );



  this.updaterSource = updaterSource;

}



public void  setPainterSource ( PainterSource  painterSource )

//////////////////////////////////////////////////////////////////////

{

  NullArgumentException.check ( painterSource );



  this.painterSource = painterSource;

}

These mutator methods permit the UpdaterSource and PainterSource instances to be replaced as necessary during animation. As object assignment is an atomic operation, there is no need to synchronize this method for fear of confusing a concurrent repaint operation. Multiple PainterSource and UpdaterSource instances can be preconstructed in memory and then instantly swapped in and out of the AnimationCanvas as required. For example, alternating the PainterSource can cause the scene to suddenly switch to a different view and, just as suddenly, back again. It is often necessary to replace the UpdaterSource when the PainterSource is replaced if the Updaters target a different set of Painters.

The static method check() throws a NullArgumentException if the argument is null. For this reason, the UpdaterSource and PainterSource variables can never be assigned null values. Knowing this removes the need to check for a null reference during the repaint operation which is called repeatedly. EmptyUpdaterSource and EmptyPainterSource, described above, are useful as temporary do-nothing placeholders, or "null objects", when other instances are not yet available.


/*********************************************************************

* Updates the repaint period.

*

* @param  repaintPeriod

*

*   The interval between repaint requests in milliseconds.

*********************************************************************/

public void  setRepaintPeriod ( long  repaintPeriod )

//////////////////////////////////////////////////////////////////////

{

  metronome.resetPeriodInMilliseconds ( repaintPeriod );

}

If the frame rate is too slow, the animation will appear jumpy instead of smooth. It is tempting, then, to maximize the frame rate by setting the repaint period to zero, forcing the AnimationCanvas to repaint itself as frequently as possible. This, however, can be a waste of CPU cycles as displaying the animation at a frame rate greater than the default value of about 24 frames per second (fps) may not be distinguishable to the human eye. Furthermore, at around 70 fps, the monitor refresh rate may be exceeded.


public synchronized void  init ( )

//////////////////////////////////////////////////////////////////////

{

  metronome.init ( );

}



public synchronized void  start ( )

//////////////////////////////////////////////////////////////////////

{

  isUpdating = true;



  metronome.start ( );

}



public synchronized void  stop ( )

//////////////////////////////////////////////////////////////////////

{

  isUpdating = false;



  metronome.stop ( );

}



public synchronized void  destroy ( )

//////////////////////////////////////////////////////////////////////

{

  metronome.destroy ( );

}

The lifecycle method init() should always be called before any of the other three lifecycle methods just as in Applet. The start() method and stop() methods are used to start/restart and stop the animation respectively. The destroy() method should be called when animation is no longer required in order to free up any resources held in readiness by the Metronome instance.

The start() and stop() methods modify the boolean flag isUpdating. When started or restarted, isUpdating is set to true to allow the Updates to be executed during the repaint operation in order to move the Painters about the screen. When stop() is called, isUpdating is set to false and the repaint operation will simply redraw the Painters as they were.

The lifecycle methods are synchronized to prevent concurrent operation. Among other things, this ensures that simultaneous calls to start() and stop() will not put the isUpdating flag setting in an invalid state. As it is expected that the lifecycle methods will be called infrequently, the slowdown due to method synchronization should not become a performance issue.

Method paintComponent()


/*********************************************************************

* Updates the updaters and paints the painters when called by repaint.

*********************************************************************/

public void  paintComponent ( Graphics  graphics )

//////////////////////////////////////////////////////////////////////

{

  if ( isUpdating )

  {

    long  updateTime = System.currentTimeMillis ( );



    Updater [ ]  updaters = updaterSource.getUpdaters ( );



    for ( int  i = 0; i < updaters.length; i++ )

    {

      Updater  updater = updaters [ i ];



      if ( updater == null )

      {

        break;

      }



      updater.update ( updateTime );

    }

  }



  Painter [ ]  painters = painterSource.getPainters ( );



  for ( int  i = 0; i < painters.length; i++ )

  {

    Painter  painter = painters [ i ];



    if ( painter == null )

    {

      break;

    }



    painter.paint ( this, graphics );

  }

}

The paintComponent() method is the most important section of code in the AnimationCanvas class. It could be said to be the "heart" of AnimationCanvas were it not for the existence of Metronome which, as described above, provides AnimationCanvas with its "pulse". The business of this method is to effect an incremental update of the Painter coordinates by executing the Updaters and then repainting the Painters in their new positions.

paint() vs. paintComponent()


/*********************************************************************

* Updates the updaters and paints the painters when called by repaint.

*********************************************************************/

public void  paintComponent ( Graphics  graphics )

//////////////////////////////////////////////////////////////////////

{

With AWT-based animation, subclasses of Component override method paint() to provide the custom code to paint the component surface. With Swing JComponent subclasses, however, the paint() method is also responsible for painting the component borders and children, if any. It does so by calling methods paintComponent(), paintBorder(), and paintChildren(), in that order. For this reason, JComponent subclass AnimationCanvas overrides the paintComponent() method instead of paint().

Repaint Request

The paintComponent() method is not called directly but rather indirectly by the repaint() method. As Swing component methods are not synchronized, update requests cannot be executed safely simultaneously by multiple threads running in parallel. Instead, each time repaint() is called, the request is queued for serial execution by the Swing update thread. If the animation logic calls the repaint() method at a rate faster than the paintComponent() method can execute, Swing will automatically coalesce, i.e., merge, the repaint requests in the queue as necessary. This means that the number of times that the paintComponent() method will be called will always be less than or equal to the number of times repaint() is called by the application specific animation code. Depending on the time it takes the paintComponent() to paint the painters, the desired frame rate may or may not be achieved.

Static Repaint


//////////////////////////////////////////////////////////////////////

{

  if ( isUpdating )

  {

When the AnimationCanvas lifecycle method stop() is called, all animation stops. This is accomplished by delegating the stop() call to the Metronome instance, which then ceases its generation of periodic repaint() requests, and by setting the AnimationCanvas instance boolean flag isUpdating to false. A repaint request is normally generated automatically by Swing whenever the surface of the component needs to be repainted by windowing events. This can occur, for example, when the component is first obscured and then uncovered by another GUI component such as a top level window. When the AnimationCanvas is stopped, such requests should not animate the Painters in addition to repainting them. As the execution of the update code block is conditional upon the isUpdating test, no Painter can have its position updated when it should remain static.

Update Time




    long  updateTime = System.currentTimeMillis ( );

Updater instances are responsible for updating the Painter positions over time to create animation. Many Updater implementations base their calculations on the difference in time since their update() method was last called, the time delta, or the current time. Others may not use the time as a variable at all. For those Updater instances that do use the current time or the time delta in their calculations, the current time is sampled from the system clock just before the update() methods of all of the Updaters are called. Passing this value as a method argument obviates each Updater instance from having to fetch this value separately. It also ensures that they are all using the same value for the current repaint operation so that the scene will reflect a valid snapshot in time.

Arrays vs. Iterators




    Updater [ ]  updaters = updaterSource.getUpdaters ( );

The UpdaterSource method returns an Object array instead of an Iterator instance as arrays provide direct control over memory allocation. Memory allocation and deallocation is a crucial consideration for a method that is typically called at the rate of 24 times a second for the life of the program. To prevent unnecessary object allocation and deallocation, it is expected that most UpdaterSource and PainterSource implementations will provide the same array instance created during initialization each time their respective getUpdaters() and getPainters() methods are called. A memory deallocation garbage collection run will often cause a noticeable freeze to an otherwise smooth animation so it is important to prevent excessive short-lived object creation. For this reason, the allocation of new Updater and Painter arrays that are immediately derefenced upon the completion of a single repaint operation is to be avoided if at all possible.

Ordered Iteration




    for ( int  i = 0; i < updaters.length; i++ )

    {

      Updater  updater = updaters [ i ];

The array elements are sorted to ensure that high priority Updater instances are executed before low priority Updater instances and background Painters are drawn before foreground Painters. The sorting and resorting of the array elements is typically accomplished by the UpdaterSource and PainterSource implementations as needed due to the addition and removal of elements. Whereas UpdaterSource implementations will typically always order by comparing priority levels whenever an Updater element is added or removed, the ordering by PainterSource implementations will vary by viewing angle. For example, a top-down view would dictate that Painters representing low-level objects such as automobiles be painted before high-level objects such as aircraft. A side view of those same objects, on the other hand, would require that farther objects be painted before nearby objects. It is expected that many PainterSource sorting algorithms will rely upon specialized Comparator implementations that order Painters by comparing their x, y, and z coordinates.

Null Element




      if ( updater == null )

      {

        break;

      }

While the number of Updaters and Painters will vary as they are added and removed by the animation logic, the lengths of the arrays that contain them are fixed, i.e., immutable. This means that the array lengths will oftentimes exceed the number of contained elements. For this reason, a null element is used to indicate the index position where the end of the valid elements has been reached. This policy is compatible with the operation of the toArray(Object[]) method implemented by the classes of the Collections Framework. This should prove useful in UpdaterSource and PainterSource implementations that rely internally upon Collections classes such as ArrayList and SortedSet.

Updater




      updater.update ( updateTime );

Updater instances perform periodic update tasks such as modifying the positions of the sprites to create animation. They can also be used for other housekeeping chores such as sampling the frame rate or polling for conditions.

Serial Execution

Updaters are always executed serially during the repaint operation just before the Painters are repainted. As the paintComponent() method calls are also serialized when the repaint requests are queued, the developer never needs to worry about concurrent processing issues. For example, if an alternative design had been chosen where the updates occurred in a separate thread from the repaint thread, sprites might be mislocated on the screen as their (x,y) coordinates were being read at the same time they were being modified. A jump from (0,0) to (1,1) could mistakenly result in a display of movement from (0,0) to (1,0) and then to (1,1) as first the x and then the y value were being updated while read.

As Swing also queues keyboard and mouse events for serial execution in the same thread used for repaintComponent() calls, such events can safely manipulate the data without need for synchronization. Under these assumptions, the repaint operation will always depict a valid snapshot of a given instance in time.

External Events

Events that are generated from outside of the Swing framework require special care. Incoming commands from a separately threaded network connection, for example, might attempt to update sprite coordinates while they were being repainted. In order to ensure that these external events are integrated gracefully, they can be appended to the same serial execution queue used by Swing for repaint requests and mouse and keyboard event handling. The static method invokeLater(), available in both the EventQueue and the SwingUtilities class, can be used for this purpose.

Painter




    painter.paint ( this, graphics );

A Painter is an object that knows where and how to paint the surface of a Component. Painter differs from Icon with its paintIcon(Component, Graphics, int x, int y) method in that it is not provided with (x,y) coordinates when its paint(Component,Graphics) method is called. Instead, Painter is assumed to know where to paint the Component and, if required, maintains (x,y) coordinate positions internally.

Some Painters may represent unique sprites in the modeled universe which fill a small area at a certain point in space such as a spaceship or a person. In contrast, other Painters may represent special effects that cover the entire component such as a tiled ground texture or snowfall. Depending on their implementation, they may execute by copying Image data or by drawing themselves pixel by pixel according to an internal algorithm. A Painter might paint scrolling text or a rotating polygon. All of these details are successfully hidden from AnimationCanvas which only knows how to communicate with its Painters instances via the abstract interface method paint(Component, Graphics).

Painting the Background

As it is often unnecessary, the AnimationCanvas paintComponent() method does not clear the component rectangle by filling it with the background color prior to overlaying the painters. For example, a Painter that paints an opaque background image across the entire area of the component would simply overwrite any previous frame painting, making pointless any first step of clearing the surface. The JComponent superclass implementation of paintComponent() clears the surface in this manner as a convenience for subclass implementations. Under the assumption that this preliminary step will frequently be unnecessary, however, the AnimationCanvas subclass implementation does not call super.paintComponent(). AnimationCanvas instead relies upon the Painters to completely repaint the component area to cover up any outdated pixels.

Double-Buffering

To prevent partially completed updates of the component surface from being displayed while it is in the process of being repainted, a technique known as double-buffering is used. In double-buffering, updates are first drawn to a separate offscreen memory buffer and then rapidly copied to the onscreen graphics buffer upon completion. In AWT Component subclasses, the update() method is often overridden to implement custom code for double-buffering to eliminate animation flicker when the background color is redrawn. In Swing, however, custom code is unnecessary and the update() method is not used as JComponents are already double-buffered by default.


Title | Applet | Background | AnimationCanvas | Resources

 
 
 
CroftSoft
 
 
-About
-Library
--Books
--Code
--Courses
--Links
--Media
--Tutorials
-People
-Portfolio
-Update
 
 
Google
CroftSoft Web
 

Creative Commons License
© 2005 CroftSoft Inc.
You may copy this webpage under the terms of the
Creative Commons Attribution License.