Building a Sprite



Now that we have our Rectangle Returner we can move up a level and create our next class. This class will be responsible for the following:

  • Tracking the position of the sprite on the screen
  • Holding the actual sprite sheet the sprite is drawn from
  • Managing all FrameAnimations associated with the sprite
  • Handle automatic rotation if it is turned on

Add a new class called "SpriteAnimation" to your project. This time we will need both the XNA Framework and the Framework Graphics namespaces:

using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;


As far as declarations:

        // The texture that holds the images for this sprite
        Texture2D t2dTexture;
 
        // True if animations are being played
        bool bAnimating = true;
 
        // If set to anything other than Color.White, will colorize
        // the sprite with that color.
        Color colorTint = Color.White;
 
        // Screen Position of the Sprite
        Vector2 v2Position = new Vector2(0, 0);
        Vector2 v2LastPosition = new Vector2(0, 0);
 
        // Dictionary holding all of the FrameAnimation objects
        // associated with this sprite.
        Dictionary<string, FrameAnimation> faAnimations = new Dictionary<string, FrameAnimation>();
 
        // Which FrameAnimation from the dictionary above is playing
        string sCurrentAnimation = null;
 
        // If true, the sprite will automatically rotate to align itself
        // with the angle difference between it's new position and
        // it's previous position.  In this case, the 0 rotation point
        // is to the right (so the sprite should start out facing to
        // the right.
        bool bRotateByPosition = false;
 
        // How much the sprite should be rotated by when drawn
        // Value is in Radians, and 0 indicates no rotation.
        float fRotation = 0f;
 
        // Calcualted center of the sprite
        Vector2 v2Center;
 
        // Calculated width and height of the sprite
        int iWidth;
        int iHeight;


Of particular interest here is the "Dictionary faAnimations = new Dictionary();" which will hold all of the FrameAnimations associated with this sprite. What exactly is a Dictionary? Think of it like an array or a List, but instead of using a numeric index each object in the array is accessed by a "key" or name. The "key" of each entry must be unique.

To illustrate, lets say we create a dictionary like this:

Dictionary<string, integer> ScreenWidths = new Dictionary<string, integer>();

ScreenWidths.Add("PC", 1600);
ScreenWidths.Add("XBOX", 640);
ScreenWidths.Add("XBOX360", 1280);

We could access these values by using their unique key, so:

X=ScreenWidths("XBOX360");

would return 1280 to X. We are going to use our dictionary to store FrameAnimation objects associated with an animation name.

It is also important to note here that *IF* we are going to use Automatic Rotation, our sprite graphics should be oriented so that when not rotated they are facing to the right. This is why I rotated the tanks in the image above. For fRotation, the value is in Radians, and 0 is along the X axis, for no rotation.

Finally, you will not that the iWidth and iHeight indicate that they are "calculated". When you add an animation to a sprite, the iWidth and iHeight get set depending on the size of the animation you create.

As normal, it's time for our properties:

        ///
        /// Vector2 representing the position of the sprite's upper left
        /// corner pixel.
        ///
        public Vector2 Position
        {
            get { return v2Position; }
            set
            {
                v2LastPosition = v2Position;
                v2Position = value;
                UpdateRotation();
            }
        }
 
        ///
        /// The X position of the sprite's upper left corner pixel.
        ///
        public int X
        {
            get { return (int)v2Position.X; }
            set
            {
                v2LastPosition.X = v2Position.X;
                v2Position.X = value;
                UpdateRotation();
            }
        }
 
        ///
        /// The Y position of the sprite's upper left corner pixel.
        ///
        public int Y
        {
            get { return (int)v2Position.Y; }
            set
            {
                v2LastPosition.Y = v2Position.Y;
                v2Position.Y = value;
                UpdateRotation();
            }
        }
 
        ///
        /// Width (in pixels) of the sprite animation frames
        ///
        public int Width
        {
            get { return iWidth; }
        }
 
        ///
        /// Height (in pixels) of the sprite animation frames
        ///
        public int Height
        {
            get { return iHeight; }
        }
 
        ///
        /// If true, the sprite will automatically rotate in the direction
        /// of motion whenever the sprite's Position changes.
        ///
        public bool AutoRotate
        {
            get { return bRotateByPosition; }
            set { bRotateByPosition = value; }
        }
 
        ///
        /// The degree of rotation (in radians) to be applied to the
        /// sprite when drawn.
        ///
        public float Rotation
        {
            get { return fRotation; }
            set { fRotation = value; }
        }
 
        ///
        /// Screen coordinates of the bounding box surrounding this sprite
        ///
        public Rectangle BoundingBox
        {
            get { return new Rectangle(X, Y, iWidth, iHeight); }
        }
 
        ///
        /// The texture associated with this sprite.  All FrameAnimations will be
        /// relative to this texture.
        ///
        public Texture2D Texture
        {
            get { return t2dTexture; }
        }
 
        ///
        /// Color value to tint the sprite with when drawing.  Color.White
        /// (the default) indicates no tinting.
        ///
        public Color Tint
        {
            get { return colorTint; }
            set { colorTint = value; }
        }
 
        ///
        /// True if the sprite is (or should be) playing animation frames.  If this value is set
        /// to false, the sprite will not be drawn (a sprite needs at least 1 single frame animation
        /// in order to be displayed.
        ///
        public bool IsAnimating
        {
            get { return bAnimating; }
            set { bAnimating = value; }
        }
 
        ///
        /// The FrameAnimation object of the currently playing animation
        ///
        public FrameAnimation CurrentFrameAnimation
        {
            get
            {
                if (!string.IsNullOrEmpty(sCurrentAnimation))
                    return faAnimations[sCurrentAnimation];
                else
                    return null;
            }
        }
 
        ///
        /// The string name of the currently playing animaton.  Setting the animation
        /// resets the CurrentFrame and PlayCount properties to zero.
        ///
        public string CurrentAnimation
        {
            get { return sCurrentAnimation; }
            set
            {
                if (faAnimations.ContainsKey(value))
                {
                    sCurrentAnimation = value;
                    faAnimations[sCurrentAnimation].CurrentFrame = 0;
                    faAnimations[sCurrentAnimation].PlayCount = 0;
                }
            }
        }


There are a lot of them, but we can break them down section by section. At the top, we have everything dealing with the position, size, and rotation of the sprite. Nothing fancy here. A Vector2 holds the position, the X, Y, Width, and Height are all derrived and returned for simplicity.

The AutoRotate and Rotation variables are *not* tied to each other. You can specify rotation without enabling AutoRotate (and in fact AutoRotate will override Rotation if you have it turned on).

Next, we have the properties that determine the "look" of the sprite. First the Texture2D itself, and then a Tint color. You will recall that we used this in Star Defense to create different colored barrels for Power Ups.

IsAnimating is checked in both the Update and Draw routines. If it is false, neither routine will try to do anything with the sprite.

Next, we have a couple of properties that deal our FrameAnimation dictionary. The first, CurrentFrameAnimation returns the FrameAnimation object associated with the currently playing animation for this sprite. We won't use this as often as you might think, because the next property, CurrentAnimation, returns the name of the currently playing animation which is normally all the information we need when dealing with FrameAnimations.

Note that CurrentFrameAnimation is read only, while CurrentAnimation will also let us set the current animation by name. When we do this, we reset the CurrentFrame and PlayCount properties of the FrameAnimation to 0. This means that the animation will always start playing from the beginning, and the number of times it has been played will always be relative to when it stated.

The constructor for our SpriteAnimation class is very simple:

        public SpriteAnimation(Texture2D Texture)
        {
            t2dTexture = Texture;
        }


Why so small? Because we need to do the work of setting up the FrameAnimations after the sprite object has been created. We will look at how we do that shortly.

Finally, we need some methods to manipulate our SpriteAnimations. There are a few of these, so I'll break it down into multiple sections and talk about each one.

        void UpdateRotation()
        {
            if (bRotateByPosition)
            {
                fRotation = (float)Math.Atan2(v2Position.Y - v2LastPosition.Y, v2Position.X - v2LastPosition.X);
            }
        }


If you look at the positional properties above (and the upcoming MoveBy method) you will see that whenever the position of the sprite is updated the previous position is stored in v2LastPosition and UpdateRotation is called after the change is made.

Our UpdateRotation method checks to see if AutoRotate is turned on. If it is, we use the magic of trigonometry to calculate the angle (in radians) between the old position and the current position. We store it in fRotation to be used later when the sprite is drawn (remember that I said earlier that AutoRotate will override the Rotation property when on. This is where that happens).

Next, we will deal with adding animations to our Dictionary. This isn't too difficult, as we are basically just creating a FrameAnimation object and setting it's properties:

        public void AddAnimation(string Name, int X, int Y, int Width, int Height, int Frames, float FrameLength)
        {
            faAnimations.Add(Name, new FrameAnimation(X, Y, Width, Height, Frames, FrameLength));
            iWidth = Width;
            iHeight = Height;
            v2Center = new Vector2(iWidth / 2, iHeight / 2);
        }
 
        public void AddAnimation(string Name, int X, int Y, int Width, int Height, int Frames,
           float FrameLength, string NextAnimation)
        {
            faAnimations.Add(Name, new FrameAnimation(X, Y, Width, Height, Frames, FrameLength, NextAnimation));
            iWidth = Width;
            iHeight = Height;
            v2Center = new Vector2(iWidth / 2, iHeight / 2);
        }


As with the FrameAnimation we have a couple of different constructors that just pass their parameters along to the FrameAnimation constructors. We also calculate the height, width, and center of the sprite here. For this reason, all of our animations for a particular sprite need to be the same size (for each frame) or you will end up with some definitely weird results.

This routine simply uses the Dictionary to get the FrameAnimation object associated with an animation name:

        public FrameAnimation GetAnimationByName(string Name)
        {
            if (faAnimations.ContainsKey(Name))
            {
                return faAnimations[Name];
            }
            else
            {
                return null;
            }
        }


While there are already a couple of ways (via properties) to move the sprite, this is one more to keep things simple. Pass in an X and Y offset and the sprite will be moved by that number of pixels (negatives are fine, of course)):

        public void MoveBy(int x, int y)
        {
            v2LastPosition = v2Position;
            v2Position.X += x;
            v2Position.Y += y;
            UpdateRotation();
        }


Finally we come to our Update and Draw methods:

        public void Update(GameTime gameTime)
        {
            // Don't do anything if the sprite is not animating
            if (bAnimating)
            {
                // If there is not a currently active animation
                if (CurrentFrameAnimation == null)
                {
                    // Make sure we have an animation associated with this sprite
                    if (faAnimations.Count > 0)
                    {
                        // Set the active animation to the first animation
                        // associated with this sprite
                        string[] sKeys = new string[faAnimations.Count];
                        faAnimations.Keys.CopyTo(sKeys, 0);
                        CurrentAnimation = sKeys[0];
                    }
                    else
                    {
                        return;
                    }
                }
 
                // Run the Animation's update method
                CurrentFrameAnimation.Update(gameTime);
 
                // Check to see if there is a "followup" animation named for this animation
                if (!String.IsNullOrEmpty(CurrentFrameAnimation.NextAnimation))
                {
                    // If there is, see if the currently playing animation has
                    // completed a full animation loop
                    if (CurrentFrameAnimation.PlayCount > 0)
                    {
                        // If it has, set up the next animation
                        CurrentAnimation=CurrentFrameAnimation.NextAnimation;
                    }
                }
            }
        }
 
        public void Draw(SpriteBatch spriteBatch, int XOffset, int YOffset)
        {
            if (bAnimating)
                spriteBatch.Draw(t2dTexture, (v2Position + new Vector2(XOffset, YOffset) + v2Center),
                                CurrentFrameAnimation.FrameRectangle, colorTint,
                                fRotation, v2Center,1f, SpriteEffects.None, 0);
        }


Draw, as normal, is very straightforward. Well, in the sense that it is pretty much a one-liner. There is a lot of fun math in there to take rotation into account, and it is possible to pass an X and Y offset in as well, though we usually won't be doing that.

Update is the first real chunk of code we've see but only because we need to handle two special cases (without them, CurrentFrameAnimation.Updage(gameTime) would be the only line in the method). These two cases are:

CurrentAnimationFrame == null : If this is true, we haven't set any kind of animation up for this sprite, but we are trying to play it (IsAnimating is true). In this case, we check to see if there are any defined animations. If there are, we pick the first one and make it the current animation.

The next case is if there is a NextAnimation defined for this animation. If so, we check to see if we have finished a complete play loop (all frames of the animation have been played, so PlayCount > 0) and if so, we set the CurrentAnimation to the FrameAnimation's "NextAnimation" property.

But why would you want to do that? Well, lets say you have a cannon sprite on the screen. It has an animation loop running where it is billowing smoke, or some such. Now you want the cannon to fire, so you have a nice animation where, just like in the cartoons, the cannon scrunches up and BOOM! Elongates and fires a cannon ball. Now what? Well, lets say you have defined two FrameAnimations, one called "idle" and one called "fire!". If you set the NextAnimation property of "fire!" to "idle", when you trigger "fire!" it will play the animation and then go right back to idle instead of looping the fire animation over and over again.

You can also use it to make things disappear. Lets pretend we have a target that has four animations:

"idle" : looping animation where it just sits there, maybe glinting in the light
"hit" : one-time animation where it rocks back and forth from the impact of the player's gun
"hurt" : looping animation where it looks beat up... kinda like someone shot it!
"boom" : one-time animation where it explodes
"gone" : which is a 1 frame "looping" animation on an empty portion of the texture

Now, you can set "hit"s NextAnimation property to "hurt" and "boom"s NextAnimation property to "gone". When it gets hit, you play the "hit" animation, which will fall into "hurt", showing the target in a damaged state. When it gets hit while damaged, play "boom", which will fall into "gone" when it is finished. You can even just test the CurrentAnimation property for it to read "gone" and know that you can then clean up the target object because it has finished animating it's explosion.

That's it for our SpriteAnimation class. At this point, we have expanded quite a bit on our AnimatedSprite class from Star Defense, but we're not done yet! One more class to go, and then it's time for some Sample Code Fun!

(Continued in Part 4)



































 

 
 
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