Tuesday, January 19, 2010

Animating the Sprite









Animating the Sprite


Figure 19-6 shows the visible methods in Animator.


Animator performs three core tasks:


  • It adds animation sequences to its schedule in response to calls from the KeyBehavior object.

  • It periodically removes an animation operation from its schedule and executes it. The execution typically changes the sprite's position and pose. A removal is triggered by the arrival of a Java 3D WakeupOnElapsedTime event.

  • It updates the user's viewpoint in response to calls from KeyBehavior.



Figure 19-6. Public methods in the Animator class




Adding an Animation Sequence


The majority of the public methods in Animator (rotClock( ), rotCounterClock( ), moveLeft( ), moveRight( ), punch( ), moveForward( ), moveBackwards( ), and toggleAc-tive( )) execute in a similar way when called by the KeyBehavior object. They add a predefined animation sequence to a schedule (which is implemented as an ArrayList called animSchedule):



public void moveForward( )
{ addAnims(forwards); }



forwards is an array of strings, which represents the animation sequence for moving the sprite forward one step:



private final static String forwards[] = {"walk1", "walk2", "stand"};



"walk1", "walk2", and so forth are the names of the 3DS files holding the sprite's pose This correspondence is used to load the models when AnimTour3D starts.


A requirement of an animation sequence is that it ends with "stand". One reason for this is that a sequence should end with the model in a neutral position, so the next sequence can follow on smoothly from the previous one. The other reason is the Animator object uses "stand" to detect the end of a sequence.


addAnims( ) adds the sequence to the end of animSchedule and increments seqCount, which stores the number of sequences currently in the schedule:



synchronized private void addAnims(String ims[])
{ if (seqCount < MAX_SEQS) { // not too many animation sequences
for(int i=0; i < ims.length; i++)
animSchedule.add(ims[i]);
seqCount++;
}
}



The maximum number of sequences in the schedule at any time is restricted to MAX_SEQS. This ensures that a key press (or, equivalently, an animation sequence) isn't kept waiting too long in the schedule before being processed. This would happen if the user pressed a key continuously, causing a long schedule to form.


By limiting the number of sequences, the Animator briefly ignores key presses when the waiting animation sequence gets too long. However, once the animation sequence gets smaller (after some of it has been processed), key presses will be accepted.


addAnims( ) is synchronized so it's impossible for the animSchedule to be read while being extended.




Processing an Animation Operation


The Animator constructor creates a WakeupCondition object based on the time delay passed to it from WrapAnimTour3D:



public Animator(int td, AnimSprite3D b, TransformGroup vTG)
{ timeDelay = new WakeupOnElapsedTime(td);
// the rest of Animator's initialization
}



This condition is registered in initialize( ) so processStimulus( ) will be called every td milliseconds:



public void processStimulus( Enumeration criteria )
{ // don't bother looking at the criteria
String anim = getNextAnim( );
if (anim != null)
doAnimation(anim);
wakeupOn( timeDelay );
}



processStimulus( ) is short since there's no need to examine the wake-up criteria. Since it's been called is enough because a call occurs every td milliseconds.


getNextAnim( ) wants to remove an animation operation from animSchedule. However, the ArrayList may be empty, so the method can return null:



synchronized private String getNextAnim( )
{ if (animSchedule.isEmpty( ))
return null;
else {
String anim = (String) animSchedule.remove(0);
if (anim.equals("stand")) // end of a sequence
seqCount;
return anim;
}
}



getNextAnim( ) is synchronized to enforce mutual exclusion on animSchedule. If the retrieved operation is "stand", then the end of an animation sequence has been reached, and seqCount is decremented. My defense of this wonderful design is that "stand" performs two useful roles: it signals the end of a sequence (as here), and changes the sprite pose to a standing position, which is a neutral stance before the next sequence begins.


doAnimation( ) can process an animation operation (represented by a String) in two ways: the operation may trigger a transformation in the user's sprite (called bob), and/or cause a change to the sprite's pose. In addition, it may be necessary to update the user's viewpoint if the sprite has moved:



private void doAnimation(String anim)
{ /* Carry out a transformation on the sprite.
Note: "stand", "punch1", "punch2" have no transforms
*/
if ( anim.equals("walk1") || anim.equals("walk2")) // forward
bob.moveBy(0.0, MOVERATE/2); // half a step
else if ( anim.equals("rev1") || anim.equals("rev2")) // back
bob.moveBy(0.0, -MOVERATE/2); // half a step
else if (anim.equals("rotClock"))
bob.doRotateY(-ROTATE_AMT); // clockwise rot
else if (anim.equals("rotCC"))
bob.doRotateY(ROTATE_AMT); // counterclockwise rot
else if (anim.equals("mleft")) // move left
bob.moveBy(-MOVERATE,0.0);
else if (anim.equals("mright")) // move right
bob.moveBy(MOVERATE,0.0);
else if (anim.equals("toggle")) {
isActive = !isActive; // toggle activity
bob.setActive(isActive);
}

// update the sprite's pose, except for "toggle"
if (!anim.equals("toggle"))
bob.setPose(anim);

viewerMove( ); // update the user's viewpoint
} // end of doAnimation( )



The first part of doAnimation( ) specifies how an animation operation is translated into a sprite transformation. One trick is shown in the processing of the forward and backwards sequences. These sequences are defined as:



private final static String forwards[] = {"walk1","walk2","stand"};
private final static String backwards[] = {"rev1","rev2","stand"};



The forwards sequence is carried out in response to the user pressing the down arrow. What happens? The sequence is made up of three poses ("walk1", "walk2", and "stand"), so the sequence will be spread over three activations of processStimulus( ). This means that the total sequence will take 3*<time delay> to be evaluated, which is about 60 ms. Multiple steps forward are achieved by adding multiple copies of the forward sequence to the Animator's scheduler list.


The punching animation sequence is defined as:



private final static String punch[] =
{"punch1", "punch1", "punch2", "punch2", "stand"};



Since "punch1" and "punch2" appear twice, they will be processed twice by processStimulus( ), which means their effects will last for 2*<time delay>. Consequently, the poses will be on the screen for twice the normal time, suggesting that the sprite is holding its stance.




Updating the User's Viewpoint


Animator uses the viewpoint manipulation code developed in TouristControls (in Chapter 18). As a sprite moves, the viewpoint sticks with it, staying a constant distance away unless the user zooms the viewpoint in or out.


The initial viewpoint is set up in Animator's constructor via a call to setViewer( ), which is the same method as in TouristControls.


The new problem with Animator is when to update the viewpoint. It shouldn't be updated until the animation operation (e.g., "walk1") is executed. For that reason, the viewpoint update method, viewerMove( ), is called at the end of doAnimation( ).


The final aspects of viewpoint adjustment are the keys i and o, which zoom the viewpoint in and out. The keys are immediately processed in Animator by shiftViewer( ), which changes the viewpoint based on the sprite's current position:



public void shiftInViewer( )
{ shiftViewer(-ZSTEP); } // move viewer negatively on z-axis

public void shiftOutViewer( )
{ shiftViewer(ZSTEP); }

private void shiftViewer(double zDist)
// move the viewer inwards or outwards
{ Vector3d trans = new Vector3d(0,0,zDist);
viewerTG.getTransform( t3d );
toMove.setTranslation(trans);
t3d.mul(toMove);
viewerTG.setTransform(t3d);
}



The shift operations aren't scheduled like the other sprite movement commands. As a consequence, the Animator changes the viewpoint immediately, even if a large number of sprite movement key presses precede the i or o keys. This behavior may be disconcerting to a user since the viewpoint seems to change too soon before earlier sprite moves have been carried out.


An obvious question is why do I support this strange behavior? Why not schedule the viewpoint zooming along with the sprite animation? The answer is to illustrate that viewpoint and sprite changes can be separated.










    No comments: