A Framework for Animations



In our Star Defense project, we only had a single animation for a sprite. That animation might have contained one frame, or multiple frames, and we could toggle the automatic playing of frames on and off. For exampke, we used a 4-frame, non animated "animation" for the player's ship, with each from displaying a different direction/status of the player's starfighter. For our "PowerUps", we used a 23 frame looping animation of a spinning barrel.

But in all cases, there was a single animation. Playing or not was our only control (other than play speed if it was playing).

In order to move beyond that structure, we need a way to define animations within our sprite sheet that we can then "play" whenever we need them. In Star Defense, for example, we could have combined the Enemy and Explosion sprite sheets into a single sheet and had a "flying" and an "exploding" animation defined for them. This would have meant resizing our explosions, so it might not have been practical, but it is an example of what we could have set up.

We will be keeping the restriction that all animations must be on a single horizontal line. We could have done this differently by defining an array of rectangles for our FrameAnimation class, but I find it simpler to calculate the locations from a single rectangle instead, so that will be an "exercise for the reader".

In order to define our animations, we will create a class called FrameAnimation. It is a very simple class that, essentially, hold a rectangle and a frame counter. Since it will handle it's own updates, we will add a few more things to it, but on the whole it will be straightforward.

As an example, in our "Princess" image, we will define two animations. The red rectangles represent the "left" animation, while the green rectangles represent the "right" animation. Each animation has 4 frames (numbered 0 thru 3) and is 32x64 pixels in size.

Create a new class in your project called FrameAnimation. In order for us to be able to use the Rectangle class, we will need:

using Microsoft.Xna.Framework;


at the top of the file. We don't need the other namespaces of the XNA Framework because we won't be doing any drawing with this class. It's job is simply to return rectangles representing locations on the sprite sheet.

Also, we are going to implement the "ICloneable" interface for this class. All this really means is that we are going to add a function to our class to return a "copy" of the object (as opposed to a pointer to the object). In order to do that, we need to modify the declaration of the class itself by adding ": ICloneable" to the end. It should look like this :

    class FrameAnimation : ICloneable


"Interfaces" are standardized sets of methods that a class that implements them will provide to callers. When we add "ICloneable" to our class declaration we MUST define a function "object ICloneable.Clone()" in order to fulfill the requirements of the ICloneable interface.

What do we need this? In truth, I won't be doing much with it in this tutorial, but the reason we need it is that a FrameAnimation object keeps track of information specific to the animation that it is playing (current frame, etc). It is really for letting us copy animations between objects without tying them all back to a single instance. As an example, lets say you have ObjectA and ObjectB. Both of these objects have a FrameAnimation child object. If you set them up like this:

ObjectA.FA = new FrameAnimation(0,0,32,32,5,0.1f); ObjectB.FA = new FrameAnimation(0,0,32,32,5,0.1f);

everything is fine. But what if you did this:

ObjectA.FA = new FrameAnimation(0,0,32,32,5,0.1f); ObjectB.FA = ObjectA.FA;

Now you have a potential problem, in that the FA members of ObjectA and ObjectB are actually the same FrameAnimation instance in memory. Updating one updates both, so if you call ObjectA.Update and ObjectB.Update, both of which might change the frame, you could end up skipping frames, etc. Of course, there are cases where you might actually WANT to do this, but generally you wouldn't want this to happen.

If you set the objects up like this instead:

ObjectA.FA = new FrameAnimation(0,0,32,32,5,0.1f); ObjectB.FA = ObjectA.FA.Clone();

Then you actually get a copy of the ObjectA.FA object instead of a pointer to it. Now ObjectA and ObjectB have independant FA objects and everyone is happy.

Anyway! On to declarations. We are going to keep track of a small number of things in this class:

        // The first frame of the Animation.  We will calculate other
        // frames on the fly based on this frame.
        private Rectangle rectInitialFrame;
  
// Number of frames in the Animation private int iFrameCount = 1;
// The frame currently being displayed. // This value ranges from 0 to iFrameCount-1 private int iCurrentFrame = 0;
// Amount of time (in seconds) to display each frame private float fFrameLength = 0.2f;
// Amount of time that has passed since we last animated private float fFrameTimer = 0.0f;
// The number of times this animation has been played private int iPlayCount = 0;
// The animation that should be played after this animation private string sNextAnimation = null;


I've left the code comments in place (a different approach than the Star Defense tutorial where I stripped them out and explained each parameter in the text afterward) so you can see what each of the variables are for. Everything here is straightforward with the exception of the "sNextAnimation" member variable.

When we put together our higher level SpriteAnimation class that will use the FrameAnimation class, we will be keeping track of any number of Animations for a particular sprite. Each of these animations will have a name associated with it. When an animation plays completely through, we can set up our SpriteAnimation class the look at the NextAnimation property of our FrameAnimation class to decide what to do next. We'll get into the more later, but you might also notice that a FrameAnimation doesn't know what it's own name is. That's OK, since FrameAnimations won't be managing a collection of themselves. That is done at a higher level.

While the FrameAnimation class will do it's own work (via an Update method) we still need controlled access to some of it's member variables from a higher level. In C#, of course, the way we do this is to define properties. Here are the properties we need for the FrameAnimation class:

        /// 
        /// The number of frames the animation contains
        /// 
        public int FrameCount
        {
            get { return iFrameCount;}
            set { iFrameCount = value; }
        }
 
        /// 
        /// The time (in seconds) to display each frame
        /// 
        public float FrameLength
        {
            get { return fFrameLength; }
            set { fFrameLength = value; }
        }
 
        /// 
        /// The frame number currently being displayed
        /// 
        public int CurrentFrame
        {
            get { return iCurrentFrame; }
            set { iCurrentFrame = (int)MathHelper.Clamp(value, 0, iFrameCount - 1); }
        }
 
        public int FrameWidth
        {
            get { return rectInitialFrame.Width; }
        }
 
        public int FrameHeight
        {
            get { return rectInitialFrame.Height; }
        }
 
        /// 
        /// The rectangle associated with the current
        /// animation frame.
        /// 
        public Rectangle FrameRectangle
        {
            get
            {
                return new Rectangle(
                    rectInitialFrame.X + (rectInitialFrame.Width * iCurrentFrame),
                    rectInitialFrame.Y, rectInitialFrame.Width, rectInitialFrame.Height);
            }
        }
 
        public int PlayCount
        {
            get { return iPlayCount; }
            set { iPlayCount = value; }
        }
 
        public string NextAnimation
        {
            get { return sNextAnimation; }
            set { sNextAnimation = value; }
        }


Again, nothing too out of the ordinary, but a couple of things worth mentioning. You'll see that the FrameHeight and FrameWidth properties are read only. Once an animation is defined, while we can change the number of frames, the framerate, etc, we can't change the frame size, as it is dependant on the underlying graphic anyway. This doesn't mean that a single sprite sheet can't have sprites of different sizes, just that every frame of any particular animation will always be the same size.

Also in the properites is the primary purpose of the FrameAnimation class : the FrameRectangle property. The rectangle returned by this property represents the source rectangle on the sprite sheet for the current animation frame.

Next up are our Constructors. We will define a few ways to create a FrameAnimation, each specifying more parameters than the last:

        public FrameAnimation(Rectangle FirstFrame, int Frames)
        {
            rectInitialFrame = FirstFrame;
            iFrameCount = Frames;
        }
 
        public FrameAnimation(int X, int Y, int Width, int Height, int Frames)
        {
            rectInitialFrame = new Rectangle(X, Y, Width, Height);
            iFrameCount = Frames;
        }
 
        public FrameAnimation(int X, int Y, int Width, int Height, int Frames, float FrameLength)
        {
            rectInitialFrame = new Rectangle(X, Y, Width, Height);
            iFrameCount = Frames;
            fFrameLength = FrameLength;
        }
 
        public FrameAnimation(int X, int Y,
            int Width, int Height, int Frames,
            float FrameLength, string strNextAnimation)
        {
            rectInitialFrame = new Rectangle(X, Y, Width, Height);
            iFrameCount = Frames;
            fFrameLength = FrameLength;
            sNextAnimation = strNextAnimation;
        }


Our first, and simplest constructor, uses the default frame speed (0.2f per frame) and only needs the initial rectangle and the number of frames.

The second constructor is actually the same thing, except that the rectangle is built from X, Y, Width, and Height for us.

The third constructor adds a FrameLength to the second constructor.

The fourth constructor adds a "NextAnimation" specifier to the third constructor.

Note that you could, of course, always use the first constructor and then set the FrameLength and sNextAnimation values via their properties afterwards. Either way accomplishes the same goal.

Finally, we need two methods : and Update() method and an ICloneable.Clone() method to meet the requirements of the ICloneable interface:

        public void Update(GameTime gameTime)
        {
            fFrameTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;
 
            if (fFrameTimer > fFrameLength)
            {
                fFrameTimer = 0.0f;
                iCurrentFrame = (iCurrentFrame + 1) % iFrameCount;
                if (iCurrentFrame == 0)
                    iPlayCount = (int)MathHelper.Min(iPlayCount + 1, int.MaxValue);
            }
        }
 
        public object ICloneable.Clone()
        {
            return new FrameAnimation(this.rectInitialFrame.X, this.rectInitialFrame.Y,
                                      this.rectInitialFrame.Width, this.rectInitialFrame.Height,
                                      this.iFrameCount, this.fFrameLength, sNextAnimation);
        }


The "timing" code in the update method should be familiar from Star Defense. We simply accumulate elapsed game time until it is time for us to take an action. In this case, the action we take is to reset the timer and increment the value of iCurrentFrame. If we have looped back to zero, we increment iPlayCount (limiting it to the max value of an integer so we don't overflow and crash).

Our ICloneable.Clone method simply creates a new instance of the FrameAnimation class and fills in all of the current parameters in the constructor. I decided to let the iPlayCount variable come over as the default (0) since this new instance of FrameAnimation technically hasn't been played yet.

(Continued in Part 3)



































 

 
 
Site Contents Copyright 2006 Full Revolution, Inc. All rights reserved.
This site is in no way affiliated with Microsoft or any other company.
All logos and trademarks are copyright their respective companies.
RSS FEED