Thursday, 3 March 2011

Unpacking Simon Squared: My mini framework-independent animation library

Animation is the trick a computer uses to make math come alive. Our minds are wired to see movement. A video game where the characters and props jumped instantly between the positions where the math says they ought to be would be incomprehensible and unplayable. Which explains why I spent a good part of the few precious days I had building Simon Squared on developing a little animation framework.

I’m pretty pleased with the result. It’s by no means polished or complete, but it makes it very easy to implement one-off animations, and to schedule groups of animations together. Best of all, it’s not tied to the XNA framework – you could use it to implement animation in Windows Forms if you so desired!

Fluent Animations

Here’s one example:

_storyboard.Plan()
    .AfterDelay(TimeSpan.FromSeconds(0.5))
    .Begin(b => b.Animate(this, (Shape target, float value) => target.RotationY = value)
                 .From(RotationY)
                 .To(RotationY + MathHelper.Pi)
                 .In(TimeSpan.FromSeconds(MovementAnimationTime)));

This shows off several aspects of the framework.

First, the Storyboard class. This is responsible for keeping track of all the animations that are currently in flight, or scheduled for later on. In my Game’s Update method I call

_storyboard.AdvanceTimeTo(gameTime.TotalGameTime);
and that takes care of updating all the animations that are in progress. 

The Plan() method returns a StoryboardPlanner which is used to schedule animations using a fluent interface. No prizes for guessing what AfterDelay does: it leaves a gap of what ever duration you specify before starting subsequent animations. These animations are scheduled using the Begin method: you pass it a function which uses the AnimationFactory it supplies to fabricate animations to your exact specifications.

Most important of all is the Animate method. In the definition of the Animate is the secret sauce that makes this animation framework independent of whatever framework it is you’re trying to animate. It works like this:

  1. you set up an animation, RotationY goes from Pi to Pi/2 in 2 seconds, for example;
  2. the animation framework calculates values as the animation progresses;
  3. every time it calculates a new value, it calls the lambda function that you’ve provided, so that you can update the appropriate object with the new value.

The first parameter I’m passing to Animate is the object that has the property I want to animation, RotationY. Then I pass in a lambda function that receives both the target object, and the value that should be assigned to the target property or field1.

How the animations work

When it comes to the actual animations, I’ve abstracted the majority of the animation logic into a couple of base classes, which makes it very easy to add animations for different types.

Here are the critical parts of the Animation base class:

public abstract class Animation
{
    // snip

    public void UpdateForTime(TimeSpan currentElapsedTime)
    {
        var normalisedTime = GetNormalisedTimeInAnimation(currentElapsedTime);
        if (normalisedTime.HasValue)
        {
            SetValueForTime(normalisedTime.Value);
        }
    }

    protected abstract void SetValueForTime(double normalisedTime);

    protected double? GetNormalisedTimeInAnimation(TimeSpan currentElapsedTime)
    {
        var normalisedPosition = 0.0;

        var millisecondsAfterStart = currentElapsedTime.TotalMilliseconds - StartTime.TotalMilliseconds;
        if (millisecondsAfterStart < 0)
        {
            return null;
        } 
        else if (millisecondsAfterStart > Duration.TotalMilliseconds && RepeatMode == RepeatMode.None)
        {
            return 1;
        }
        else
        {
            var quotient = millisecondsAfterStart / Duration.TotalMilliseconds;
            var integerPart = Math.Floor(quotient);

            normalisedPosition = quotient - integerPart;

            if (AutoReverse && (integerPart % 2) == 1)
            {
                normalisedPosition = 1 - normalisedPosition;
            }
        }

        return normalisedPosition;
    }
}

Start at the top: UpdateForTime is called by the Storyboard on every frame, to inform the animation of the time, and to give it an opportunity to update its target.

The first thing Animation does then is to work out the normalised time – that is a value between 0 and 1 indicating the progress through the animation. If the clock says that we’re bang on the animation’s start time then we return 0; halfway through (however long the animation lasts), and the normalised time is 0.5; and a clock time of StartTime + Duration gets a normalised time of 1. All this is calculated by GetNormalisedTimeInAnimation. Notice from the code how it is also smart enough to handle animations that repeat over and over, and animations that cycle -  repeating, but reversing when they get to the end value.

Once the normalised value has been calculated it is passed to SetValueForTime which is an abstract method on the Animation base class.

Next level down in the hierarchy we have SetterInvokingAnimation: 

public abstract class SetterInvokingAnimation<TValue, TTarget> : Animation where TTarget : class 
{
    public TValue StartValue { get; set; }

    public TValue EndValue { get; set; }

    private WeakReference _target;
    private readonly Action<TTarget, TValue> _setter;

    public SetterInvokingAnimation(TTarget target, Action<TTarget, TValue> setter)
    {
        _setter = setter;
        _target = new WeakReference(target);
    }

    protected override void SetValueForTime(double normalisedTime)
    {
        var value = GetValueForNormalisedTime(normalisedTime);
        var target = _target.Target as TTarget;
        if (target != null)
        {
            _setter(target, value);
        }
    }

    protected abstract TValue GetValueForNormalisedTime(double normalisedTime);
}

This is the class that takes care of pushing calculated values to the target - though it doesn't actually calculate any values itself – it leaves that to derived classes, by way of the GetValueForNormalisedTime abstract method.

And here is one of those derived class, FloatAnimation. Since it is descended from some hard-working ancestors, there’s little it has to do – just a simple mathematical interpolation between the StartValue and EndValue of the animation:

public class FloatAnimation<TTarget> : SetterInvokingAnimation<float,TTarget> where TTarget : class 
{
    public FloatAnimation(TTarget target, Action<TTarget, float> propertySetter) : base(target, propertySetter)
    {
    }

    protected override float GetValueForNormalisedTime(double normalisedTime)
    {
        var delta = EndValue - StartValue;
        var value = StartValue + delta * normalisedTime;

        return (float)value;
    }
}

So there you have it: a whistle-stop tour of my mini-animation framework.

See it for yourself

You can see the full source code - in use - in my Simon Squared game up on Codeplex. And if you shout loud enough, I might pull it out of there and stick it in a NuGet for easy consumption in your own games.

Go check it out – and then make sure you tell Red Gate how much you love it, over on my Simon Squared competition entry page!


  1. I could have defined Animate like this:

    Animate(value => this.RotationY = value);
    which would have looked a lot neater. But that has the downside of capturing a reference to my target object in a closure and smuggling it down into the animations which are held by the Storyboard; and since my Storyboard is longer-lived than many of the objects that I’m animating, this would have had the effect of keeping my target objects alive when they would otherwise have been Garbage Collection. So I pass in the target object separately so that I can hold it using a WeakReference, and then I use a lambda function that is, in effect, a static method.

    Another way, of course, would be to remove animations from the Storyboard explicitly (and it does support that). But I'm lazy, and it's much easier to use this fire-and-forget approach!

1 comments:

Beats Wireless said...

 Thanks for reading my article about  . If you'd like to know How to Choose  and  , please come and visit  .

Post a Comment