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