Animation in J2ME :: Powerful Lightweight Mobile Animation Techniques
2007-05-31
Scope of Article
This article introduces the general-purpose Animation class I have developed and used in some of my MIDlets with the intention of helping others avoid writing animation-related boilerplate code.
Real usage examples for various scenarios are also provided to demonstrate the class’ ease-of-use.
Introduction: What is an animation?
If you have a background in graphics you can skip this definition but for those who are just getting their feet wet here goes…
An animation is a transformation, over time, of one or more of an object’s properties. Common properties that can be animated include but are not restricted to position, color, sprite frame, translucency and visibility.
A Familiar Example: The Walking Hero
Anyone who has played an RPG game is no doubt familiar with “the walking hero”. He is the character who, regardless of moving or not, is always walking in stride. His animations usually consist of 3 separate animation frames (left foot front, both feet side-by-side, and right foot front). He typically walks in 4 directions but for the purposes of this article we’ll just focus on his downward facing frames. As I mentioned just a moment ago he never stops animating. His animation sequence looks like this…
1. frame 1 (left foot front)
2. frame 2 (both feet side-by-side)
3. frame 3 (right foot front)
4. frame 2 (both feet side-by-side)
... back to step 1
This is a linear cycling animation sequence. That is to say, the sequence goes from index 1 to 4 then back to 1 to keep cycling. From the player’s perspective this 1,2,3,4,3,2,1,2,3,4…. sequence is invisible. It just seems as if the hero is walking normally. There is another, perhaps better/more accurate way we could achieve this type of animation sequence without the player noticing any differences.
1. frame 1 (left foot front)
2. frame 2 (both feet side-by-side)
3. frame 3 (right foot front)
... rewind (step 2, step 1)
By rewinding the animation we better model the situation since walking is actually a reciprocal motion, like that of a pendulum. We also shorten the animation sequence by one frame. With such a trivial example it might be difficult to grasp the benefits of rewinding but as the properties become more complex it really simplifies the logic.
Animation Sequence Flows
There are 2 styles of animation sequence flows which I’ll now outline.
1. Linear
When the animation sequence reaches the end it stops there.
2. Rewinding
When the animation cycle reaches the end it rewinds back to the beginning after which it then stops.
Cycles
Animations can run through a sequence once, an arbitrary number of times or even forever. The decision is yours. Coupled with the sequence flow styles rich effects can be produced. For example our walking hero can be implemented as a forever cycling, rewinding animation sequence. Conversely, an explosion can be created with a one-time linear animation sequence.
The Animation Class
Without further ado allow me to list the Animation class…
Animation.java
/**
* The Animation class represents the state of an object's view
* in transition. This transition is expressed through its current
* index within a number of frames. Additional control of this
* position index is offered to allow for rewinding and cyclical
* progressions.
*
* This class does not handle an object's rendering. It is the
* responsibility of the displayable object to use an Animation
* instance to track the index which represents some form of
* viewable state.
*
* For instance the index may reference images within an array
* (animation cels) and/or it may reference x,y positions within a
* different array (path-based animation).
*/
public class Animation {
/**
* Flag indicating that the animation loops without end.
* @see Constructor.
*/
public static final int LOOP_FOREVER = -1;
/**
* Flag indicating that the animation plays a single time only.
* @see Constructor.
*/
public static final int LOOP_DISABLE = 1;
/*
* The current index in the animation.
*/
protected int index;
/*
* The number of frames in the animation.
*/
protected int frameCount;
/*
* The step in frames to progress.
*/
protected int step;
/*
* Indicates whether the animation should rewind to the
* beginning upon completion.
*/
protected boolean rewind;
/*
* Indicates whether the animation is currently rewinding.
*/
protected boolean inRewind;
/*
* The number of times the animation should loop.
* Either LOOP_FOREVER, LOOP_DISABLE or any positive number.
*/
protected int loopCount;
/*
* The current loop.
*/
protected int loopIndex;
/*
* A flag indicating the Animation has completed.
*/
protected boolean finished;
/*
* The time that must pass in order for a call to progress() to have any
* effect on the animation state.
*/
protected long progressInterval;
/*
* The millisecond timestamp of the previous call to the progress method.
*/
protected long previousProgressCall;
/**
* Instantiates an Animation.
*
* @param frameCount The number of frames of which the animation consists.
* @param loopCount The number of loops to complete before terminating. Either LOOP_FOREVER, LOOP_DISABLE or any positive number.
* @param rewinds A flag to indicate that the animation should rewind prior to completing a cycle.
*/
public Animation(int frameCount, int loopCount, boolean rewinds) {
if(frameCount < 2) {
throw new IllegalArgumentException("frameCount must be greater than 1.");
}
if(loopCount < LOOP_FOREVER || loopCount == 0) {
throw new IllegalArgumentException("loopCount must be greater than 0 or LOOP_FOREVER.");
}
this.frameCount = frameCount;
this.loopCount = loopCount;
rewind = rewinds;
step = 1;
previousProgressCall = -1L;
}
/**
* Resets the Animation to its starting position.
*/
public void reset() {
index = 0;
loopIndex = 0;
inRewind = false;
finished = false;
}
/**
* Indicates whether the Animation is finished or not.
*
* @return true if finished, false otherwise.
*/
public boolean isFinished() {
if(loopCount == LOOP_FOREVER) {
return false;
}
return finished;
}
/**
* Returns the index.
*
* @return The index.
*/
public int getIndex() {
return index;
}
/**
* Arbitrarily sets the index to any point in the animation.
*
* @param index The index to set.
*/
public void setIndex(int index) {
if(index < 0 || index >= frameCount) {
throw new IllegalArgumentException("index must be between 0 and frameCount.");
}
this.index = index;
}
/**
* Returns the Animation's progression step factor.
*
* @return the step.
*/
public int getStep() {
return step;
}
/**
* Sets the Animation's progression step factor.
*
* @param step a number greater or equal to 1 and less than the frameCount.
*/
public void setStep(int step) {
if(step < 1 || step >= frameCount) {
throw new IllegalArgumentException("step must be greater or equal to 1 and less than the frameCount.");
}
this.step = step;
}
/**
* Progresses the Animation's state while enforcing that progression does
* not occur until the progress interval has elapsed.
*
* @param currentTimeMillis The current time in milliseconds.
*/
public int progress(long currentTimeMillis) {
if (previousProgressCall == -1) {
previousProgressCall = currentTimeMillis;
return index;
}
if (currentTimeMillis - previousProgressCall < progressInterval) {
return index;
}
previousProgressCall = currentTimeMillis;
return progress();
}
/**
* Progresses the Animation's state.
*
* The behavior of this method depends on the Animation's state.
* If rewinding is not used the last frame the index will reference will be
* the end frame. However if rewinding is used the last frame will
* reference the beginning frame.
*
* @return The Animation's current index after progressing.
*/
public int progress() {
if (finished) {
return index;
}
if (!inRewind) {
index += step;
}
else {
index -= step;
}
if(index >= frameCount) {
// The Animation has reached the end frame.
if(rewind) {
// The Animation will now rewind...
inRewind = true;
index -= (step * 2);
}
else {
// Begin the next animation cycle...
loopIndex++;
if(loopCount != LOOP_FOREVER && loopIndex >= loopCount) {
// The Animation has finished.
finished = true;
loopIndex--;
index -= step;
return index;
}
index = 0;
}
}
if(index <= 0 && inRewind) {
/*
* The Animation finished rewinding.
* Begin the next animation cycle...
*/
loopIndex++;
inRewind = false;
if(loopCount != LOOP_FOREVER && loopIndex > loopCount) {
// The Animation has finished.
finished = true;
return index;
}
index = 0;
}
return index;
}
/**
* Returns the number of frames in this Animation.
*
* @return the frame count.
*/
public int getFrameCount() {
return frameCount;
}
/**
* Returns the number of loops in this Animation.
*
* @return the loop count.
*/
public int getLoopCount() {
return loopCount;
}
/**
* Returns the Animation's current loop index.
*
* @return the loop index.
*/
public int getLoopIndex() {
return loopIndex;
}
/**
* Indicates whether the Animation rewinds upon reaching the end frame.
*
* @return true if rewinding is supported, false otherwise.
*/
public boolean rewinds() {
return rewind;
}
/**
* Indicates whether the Animation is currently in a rewind.
* A rewind is defined as a decrement in the index until the index equals
* 0 whereby the the Animation exists rewind mode and reenters normal mode.
*
* @return true if currently rewinding, false otherwise.
*/
public boolean isInRewind() {
return inRewind;
}
/**
* Sets the progression interval for use by the progress(long currentTimeMillis) method.
*
* @param progressInterval The time which must elapse between progressions in milliseconds.
*/
public void setProgressInterval(long progressInterval) {
this.progressInterval = progressInterval;
}
}
Synchronizing with the Game Loop
By coupling the Animation class with the FPS class an animation can be timed to advance with the flow of the game loop. But what if your animation wants to progress at a slower speed? With the animation class you can instruct it to progress only if a given milliseconds period has elapsed. This is extremely useful for creating dynamic progression schemes where a condition such as friction can cause a sprite to move slower like a golf ball flying against the wind.
How the Animation Class works
The Animation class is a general purpose number sequence and index/cursor maintainer. Unlike the MIDP 2.0 Sprite class, it isn’t tightly coupled to the property it is animating and in fact has no knowledge of any properties. Your objects use an Animation class instance for each animated property.
Our walking hero class, for instance, can have an instance of the Animation class that with each iteration of the game loop will advance the walking sequence to the next frame. The class will retrieve the current frame index from the animation instance at rendering time, draw the frame, then advance the frame index. For extra points let’s control the rate of progress so the animation does not occur too quickly.
The code might look something like this…
// A forever looping, and rewinding animation with 3 frames.
Animation walkAnim = Animation(3, Animation.LOOP_FOREVER, true);
// Make sure the animation progresses every 333ms (or 3 times per second)
walkAnim.setProgressInterval(333);
...
int walkFrame = walkAnim.getIndex();
Image walkImg = walkImages[walkFrame];
g.drawImage(walkImg, hero.getX(), hero.getY(), Graphics.LEFT | Graphics.TOP);
walkAnim.progress(System.currentTimeMillis());
Another class might utilize the Animation class to manage strobing color of a health indicator (like a heartbeat). Such an animation would cycles the red, green and blue elements of the health indicator’s color from 0 to 255 over and over again. This time lets make the animation run faster by setting the step to 4 instead of the
default 1.
// A forever looping, non-rewinding animation with 256 states.
Animation strobeAnim = Animation(256, Animation.LOOP_FOREVER, false);
// Set the step to 4 (index = 0, 4, 8, 12....)
strobeAnim.setStep(4);
...
int luminosity = strobeAnim.getIndex();
g.setColor(luminosity, luminosity, luminosity);
// draw health indicator using current color
strobe.draw();
strobeAnim.progress();
Pervasive Animation
By far the best case for using a dedicated Animation class is for implementing animations pervasively throughout your MIDlet. Weather conditions, sprite movements, projectile paths, etc… can all be easily managed in a declarative fashion by using the Animation class. Managing the various gears that drive an animation in a non-OOP fashion pervasively should be a last resort when trying to optimize your MIDlet’s performance/footprint.
Extending the Animation Class
To provide even richer animations it may be useful to extend or compliment the Animation class with an animation-scripting engine capable of managing multiple animations in tandem. Such an engine would provide an elegent and expressive means to time the animation progressions with respect to their states. Such facilities exist in other animation packages and could be easily integrated.
In closing
The Animation class listed in this tutorial is extremely general purpose and uncoupled from animatable properties for maximum flexibility. This tutorial cannot do the class justice however it serves as a starting point to adding an extra dimension of style to any MIDlet.
If you have feedback or questions please feel free to contact me at
j a y _ f u e r s t e n b e r g [AT MARK] y a h o o . c o . j p
Entry Filed under: graphics & animation, programming (advanced). .
Trackback this post