Wednesday, March 17, 2010

Animation looping using delegates

I have created an small XNA application that renders a animated sprite that I load from a sprite sheet. A sprite sheet is a image file that has a series of frames or pictures of an object that is different from frame to frame. Imagine a series of frames that show a door opening. Running the animation the other way would display the door closing. Running the animation forward and then backward, again and again, would show the door opening, then closing, repeating. Finally one could cycle the animation, and start over, and run it again. There are at least 5 ways to cycle this single linear animation. I'll list them:

1. Foward, then stop.
2. Forward, then forward again, looping.
3. Reverse, then stop.
4. Reverse, then reverse again, looping.
5. Forward, then reverse, looping.
The first attempt to code this might look like this:


class animation : IUpdatable
{
private AnimationEdge eventType;
private enum Direction { forward, reverse }
private Direction direction = Direction.forward;
private AnimationMode mode = AnimationMode.stop;
private int current;
private Frame[] frames;
public enum AnimationEdge { begin, end, cycle }
public enum AnimationMode {
forward_cycle = 1,
reverse_cycle = 2,
forward_to_ending,
reverse_to_beginning,
forward_reverse_cycle,
stop
}
:
public void Update()
{
switch (this.mode) {
case AnimationMode.forward_cycle:
this.current++;
if (this.frames.Length <= this.current) {
this.current = 0;
this.InvokeAnimationEdgeEvent(AnimationEdge.cycle);
}
break;
case AnimationMode.forward_to_ending:
this.current++;
if (this.frames.Length <= this.current) {
this.current = this.frames.Length - 1;
this.InvokeAnimationEdgeEvent(AnimationEdge.end);
}
break;
:
case AnimationMode.forward_reverse_cycle:
if (Direction.forward == this.direction) this.current++;
else this.current--;
if (this.frames.Length <= this.current) {
this.current = this.frames.Length - 1;
this.direction = Direction.reverse;
this.InvokeEdgeEvent(AnimationEdge.cycle);
} else if (this.current < 0) {
this.current = 0;
this.direction = Direction.forward;
this.InvokeEdgeEvent(AnimationEdge.cycle);
}
break; // whew!
:


Generalizing, on each update, we:
1. Increment the current frame - based on both mode, and direction.
2. If the current frame is out-of-bounds, we fix it, and direction.
3. If we made it to the edge, we pulse an event.
Note the complexity of this state machine. Some modes require three tests per update. There has to be a better way. There is. Delegates.


private Action increment = noop;
private Func<bool> condition = noop_false;
private Action fixup = noop;


Here is the actual update code:


private void IncFrame()
{
this.increment();
if (this.condition()) {
this.fixup();
this.InvokeEdgeEvent(this.eventType));
}
}


Note a couple of things about this. It is private. I actually call this from somewhere else in my class. The event type is a function of the kind of animation. It is part of the instance of the current animation.

I may not want to sync my animation frame rate to my update rate. The update may be called with a lot of jitter. Perhaps the update rate suffers no jitter, but this animation does. There are reasons one may want jitter in their animation. Perhaps to devote more frames to a part of the animation where there is more going on. By decoupling the animation rate from the update rate, one can author animations which have different rates, and varying rates. Look toward VBR coding for more information.

Really, in the animation, it only matters what the current frame is when displaying the animation.


public void Draw(DateTime now, Point target, IRenderingEngine rE)
{
this.SetCorrectFrame(now);
rE.Render(this.Frames[this.current].Image,target,this.Frames[this.current].Rectangle);
}

private void SetCorrectFrame(DateTime now)
{
TimeSpan delta = now - this.lastDrawTime;
if (delta >= this.Frames[this.current].Timespan) {
this.lastDrawTime = now;
this.IncFrame();
}
}


We'll need to set the mode of the animation.


public void SetMode(AnimationMode m)
{
this.mode = m;
if (this.mode == AnimationMode.stop) {
this.increment = noop;
this.condition = noop_false;
this.fixup = noop;
}
if ((this.mode == AnimationMode.forward_cycle) || (this.mode == AnimationMode.forward_to_ending)) {
this.increment = this.forward;
this.condition = this.pastEnd;
}
if ((this.mode == AnimationMode.reverse_cycle) || (this.mode == AnimationMode.reverse_to_beginning)) {
this.increment = this.reverse;
this.condition = this.pastBegin;
}
if ((this.mode == AnimationMode.reverse_cycle)||(this.mode == AnimationMode.forward_to_ending))
this.fixup = this.setEnd;
if ((this.mode == AnimationMode.reverse_to_beginning)||(this.mode == AnimationMode.forward_cycle))
this.fixup = this.setBegin;
if (this.mode == AnimationMode.forward_reverse_cycle) {
this.increment = this.forward;
this.condition = this.pastEnd;
this.fixup = this.forward_fixup;
}
if ((this.mode == AnimationMode.forward_cycle) || (this.mode == AnimationMode.reverse_cycle) || (this.mode == AnimationMode.forward_reverse_cycle))
this.eventType = AnimationEdge.cycle;
else this.eventType = AnimationEdge.end;
}


The actual delegates functions are trivial.


private void forward_fixup()
{
this.setEnd();
this.condition = this.pastBegin;
this.increment = this.reverse;
this.fixup = this.reverse_fixup;
}
private void reverse_fixup()
{
this.setBegin();
this.condition = this.pastEnd;
this.increment = this.forward;
this.fixup = this.forward_fixup;
}
private void setBegin() { this.current = 0; }
private void setEnd() { this.current = this.Frames.Length - 1; }
private static void noop() { }
private static bool noop_false() { return false; }
private static bool noop_true() { return true; }
private void forward() { this.current++; }
private void reverse() { this.current--; }
private bool pastEnd() { return (this.current >= this.Frames.Length); }
private bool pastBegin() { return (this.current < 0); }


The great thing about this is we have general-cased this to the point where even the forward-reverse mode is not special. In summary, we have replaced a shinola-load of switch/case/if statements with the simple:


increment();
if (condition()) {
fixup();
InvokeEdgeEvent(eventType));
}


Powerful.

No comments:

Post a Comment