Page 1 of 1

XNA 2D Animation Library

#1 LiberLogic969  Icon User is offline

  • D.I.C Head
  • member icon

Reputation: 28
  • View blog
  • Posts: 67
  • Joined: 03-September 12

Posted 05 March 2013 - 09:47 AM

Hello and welcome to my first tutorial! I am going to try and make this a multi-part guide for 2D sprite animations that will start with a fully functioning 2D sprite animation library and finish with an editor to create animations and save them to XML using the IntermediateSerializer.

The system I will go over in this guide is an amalgamation of my favorite features and functionality from various animation tutorials and game programming books that I have read over the years. The system will support any style of sprite sheets that you can think of!

NOTE : This tutorial assumes you know how to create projects in Visual Studio and also add files and XNA content in the solution explorer (including SpriteFonts).

Lets set up our projects. Create a new Windows Game Project in Visual Studio and name it whatever you like (I will name mine AnimationProject). Add a Windows Game Library to the your Game Project called AnimationLibrary. Don’t forget to add a reference to the AnimationLibrary in your Game Project!

Once you have the project set up we will need to add a couple of class files to the AnimationLibrary project called “KeyFrame” and “Animation“. These two classes will define exactly what an animation is and how it functions. I will post the classes and go over what each variable and method will be used for.

Starting with the KeyFrame class :


    /// <summary>
    /// Represents a single frame in an animation
    /// </summary>
    public sealed class KeyFrame
    {
        #region Fields

        // Holds a region of the sprite sheet 
        private Rectangle source;

        #endregion

        #region Properties

        public Rectangle Source
        {
            get { return source; }
        }

        //Utility Property for easy access to source.Width
        public int Width
        {
            get { return source.Width; }
        }

        //Utility Property for easy access to source.Height
        public int Height
        {
            get { return source.Height; }
        }

        #endregion

        #region Constructor

        public KeyFrame(int x, int y, int width, int height)
        {
            source = new Rectangle(x, y, width, height);
        }

        #endregion
    }



The purpose of this class is to define what a frame is in our animations. Our Animation class will manage a list of these objects. The reason we create a class to hold the source rectangle instead of just adding a List<Rectangle> field to the animation class is that we can easily add some cool functionality to a KeyFrame. For example, you have an animation of your character drawing back his sword and swinging. If you want to play a “slashing” sound effect right when the sword is swung you could add a field (int, string, enum) to the KeyFrame class that holds data for a sound effect. When the Animation class encounters a key frame it will check if that variable is initialized and if so tell your game to play the specified sound. This is a great and easy way to make smooth timings for things in your game. You could also give the KeyFrame class a Vector2 value to define its origin. This can be useful if you have frames of different sizes in the same animation.

Now onto the Animation class!


    /// <summary>
    /// Contains the data needed to animate a series of frames defined in a texture
    /// </summary>
    public sealed class Animation
    {
        #region Fields

        //Used to identify specific animations
        private string name;

        //Will this animation play in a constant loop?
        private bool shouldLoop;

        //Lets us know if the animation has reached the last frame
        private bool isComplete;

        //How many frames a second we want to display
        private float framesPerSecond;

        //The amount of seconds that have to pass before we switch frames
        private float timePerFrame;

        //We add the elapsed time to this variable each frame
        private float totalElapsedTime;

        //Holds a value that points to an element in a list of frames
        private int currentFrame;

        //The frames of this animation
        private List<KeyFrame> keyFrames;

        // Used for animations that do not loop to switch to other
        // animations when complete
        private string transitionKey;

        #endregion

        #region Properties

        public string Name
        {
            get { return name; }
            set { name = value; }
        }
        public bool ShouldLoop
        {
            get { return shouldLoop; }
            set { shouldLoop = value; }
        }
        public float FramesPerSecond
        {
            get { return framesPerSecond; }
            set { framesPerSecond = value; }
        }
        public List<KeyFrame> KeyFrames
        {
            get { return keyFrames; }
            set { keyFrames = value; }
        }
        public KeyFrame CurntKeyFrame
        {
            get { return keyFrames[currentFrame]; }
        }
        public bool IsComplete
        {
            get { return isComplete; }
            set { isComplete = value; }
        }
        public string TransitionKey
        {
            get { return transitionKey; }
        }

        #endregion

        #region Constructor

        public Animation()
        {
            name = string.Empty;
            shouldLoop = false;
            isComplete = false;
            framesPerSecond = 0;
            timePerFrame = 0;
            totalElapsedTime = 0;
            currentFrame = -1;
            keyFrames = new List<KeyFrame>();
        }
        public Animation(string name, bool shouldLoop, float framesPerSecond, string transitionKey)
        {
            this.name = name;
            this.shouldLoop = shouldLoop;
            this.framesPerSecond = framesPerSecond;
            this.transitionKey = transitionKey;

            timePerFrame = 1.0f / framesPerSecond;
            keyFrames = new List<KeyFrame>(60);
            isComplete = false;
            currentFrame = -1;
            totalElapsedTime = 0;
        }

        #endregion

        #region Methods

        /// <summary>
        /// This method will make sure this animation starts fresh
        /// </summary>
        public void Reset()
        {
            currentFrame = 0;
            totalElapsedTime = 0;
            isComplete = false;
        }

        /// <summary>
        /// Adds a KeyFrame object to our list of KeyFrames
        /// </summary>
        /// <param name="x">The x coord in texture space</param>
        /// <param name="y">The y coord in texture space</param>
        /// <param name="width">The width in pixels of out source</param>
        /// <param name="height">The height in pixels of out source</param>
        public void AddKeyFrame(int x, int y, int width, int height)
        {
            KeyFrame keyFrame = new KeyFrame(x, y, width, height);
            keyFrames.Add(keyFrame);
        }

        #endregion

        #region Update

        /// <summary>
        /// Updates the logic to advance through our list of frames
        /// </summary>
        /// <param name="gameTime">provides a snapshot of timing values</param>
        public void Update(GameTime gameTime)
        {
            totalElapsedTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
            KeyFrame keyFrame = keyFrames[currentFrame];

            if (totalElapsedTime >= timePerFrame)
            {
                if (currentFrame >= keyFrames.Count - 1)
                {
                    if (shouldLoop)
                    {
                        currentFrame = 0;
                        isComplete = false;
                    }
                    else
                    {
                        isComplete = true;
                    }
                }
                else
                {
                    currentFrame++;
                }

                totalElapsedTime -= totalElapsedTime;
            }
        }

        #endregion
    }



There is a lot going on in here that we need to go over. We will start with the fields.

[string name] This is used to identify the specific animation for various functions in this system
[bool shouldLoop] Lets us know if the animation is supposed to play in a loop.
[bool isComplete] Tells us if the animation has reached the last KeyFrame.
[float framesPerSecond] the number of KeyFrames we want to display to the screen in a second.
[float timePerFrame] This is the amount of time that needs to pass before we can switch to the next KeyFrame.
[float totalElapsedTime] acts as a timer that we compare to timePerFrame to see if we need to switch to the next KeyFrame.
[int currentFrame] stores the current frame to render and update.
[List<KeyFrame> frames] holds all the KeyFrames in this animation
[string transitionKey] used to switch to another animation when completed (only for non-looping animations)

The properties will reveal most of the private fields. We have one property, CurntKeyFrame, that acts as a utility property for easy access to the current frame. Time to go over the methods.

The Reset() method is used when changing to a new Animation. It makes sure the animation starts fresh by resetting some variables.

The AddKeyFrame() method helps us easily add KeyFrame objects to our list.

Update() is the brain of the class. This contains the logic to cycle through our KeyFrame list. We will step through it piece by piece.

       totalElapsedTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
       KeyFrame keyFrame = keyFrames[currentFrame];



We start by updating our timer (totalElapsedTime) and setting the current KeyFrame.

if (totalElapsedTime >= timePerFrame)   



Now we check if totalElapsedTime is greater than the timePerFrame value that we get from framesPerSecond. If totalElapsedTime is greater than timePerFrame we know enough time has passed to switch to the next frame… but first we need to check a few things.

            if (currentFrame >= keyFrames.Count - 1)
                {
                    if (shouldLoop)
                    {
                        currentFrame = 0;
                        isComplete = false;
                    }
                    else
                    {
                        isComplete = true;
                    }
                }
                else
                {
                    currentFrame++;
                }

                totalElapsedTime -= totalElapsedTime;



Check if this KeyFrame is the last frame in our animation, and if so we will see if this animation should loop back to the beginning. If this animation does not loop we will set the isComplete flag to true. If all else fails we know that we still have some frames left to cycle through so we update the currentFrame and reset out timer so we can go through the process all over again in the next frame of our game.

We can now add a class to the Game Project we created called AnimatedEntity. Here is the class in its entirety. I will step through all of the fields and methods.

public class AnimatedEntity
    {
        #region Fields

        // Holds all animations this entity can play
        private Dictionary<string, Animation> animations;

        // The animation we are currently playing
        private Animation currentAnimation;

        // The texture that contains all of our frames
        private Texture2D spriteSheet;

        // Positon of the sprite in our world
        private Vector2 position;

        // Tells SpriteBatch where to center our texture
        private Vector2 origin;

        // The rotation of our sprite
        private float rotation;

        // The scale of our sprite
        private float scale;

        // Tells SpriteBatch how to flip our texture
        private SpriteEffects flipEffect;

        // Tells SpriteBatch what color to tint our texture with
        private Color tintColor;

        #endregion

        #region Properties

        public Vector2 Position
        {
            get { return position; }
            set { position = value; }
        }
        public float Scale
        {
            get { return scale; }
            set { scale = value; }
        }
        public float Rotation
        {
            get { return rotation; }
            set { rotation = value; }
        }
        public SpriteEffects FlipEffect
        {
            get { return flipEffect; }
            set { flipEffect = value; }
        }
        public string CurntAnimationName
        {
            get { return currentAnimation.Name; }
        }

        #endregion

        #region Constructor

        public AnimatedEntity()
        {
            animations = new Dictionary<string, Animation>(24);
            spriteSheet = null;
            position = Vector2.Zero;
            origin = Vector2.Zero;
            rotation = 0;
            scale = 1;
            flipEffect = SpriteEffects.None;
            tintColor = Color.White;
        }
        public AnimatedEntity(Vector2 position, float scale, Color tintColor)
        {
            //Initialize the Dictionary
            animations = new Dictionary<string, Animation>(24);
            spriteSheet = null;
            origin = Vector2.Zero;
            rotation = 0;
            flipEffect = SpriteEffects.None;

            this.position = position;
            this.scale = scale;
            this.tintColor = tintColor;

            //Make sure the color is set to something otherwise we wont see
            //the texture drawn
            if (tintColor == null)
                tintColor = Color.White;

            //If the scale is less than 0 we wont see the texture drawn
            if (scale <= 0)
                scale = 0.1f;
        }

        #endregion

        #region Initialization

        /// <summary>
        /// Loads the sprite sheet texture
        /// </summary>
        /// <param name="content">ContentManager to load from</param>
        /// <param name="textureAssetName">The asset name of our texture</param>
        public void LoadContent(ContentManager content, string textureAssetName)
        {
            //Load our sprite sheet texture
            spriteSheet = content.Load<Texture2D>(textureAssetName);
        }
        public void LoadContent(Texture2D spriteSheet)
        {
            //Set our sprite sheet texture
            this.spriteSheet = spriteSheet;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Helper method to add Animations to this entity
        /// </summary>
        /// <param name="animation">The Animation we want to add</param>
        public void AddAnimation(Animation animation)
        {
            // Is this Animation already in the Dictionary?
            if (!animations.ContainsKey(animation.Name))
            {
                // If not we can safely add it
                animations.Add(animation.Name, animation);
            }
            else
            {
                // Otherwise we tell are computer to yell at us
                throw new ApplicationException("Animation Key is already contained in the Dictionary");
            }
        }

        /// <summary>
        /// Tells this entity to play an specific animation
        /// </summary>
        /// <param name="key">The name of the animation you want to play</param>
        public void PlayAnimation(string key)
        {
            if (string.IsNullOrEmpty(key) || !animations.ContainsKey(key))
                return;

            if (currentAnimation != null)
            {
                if (currentAnimation.Name == key)
                {
                    return;
                }
            }

            currentAnimation = animations[key];
            currentAnimation.Reset();
        }

        #endregion

        #region Update

        /// <summary>
        /// We call this method to Update our animation each frame
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values</param>
        public void Update(GameTime gameTime)
        {
            if (currentAnimation != null)
            {
                origin.X = currentAnimation.CurntKeyFrame.Width / 2;
                origin.Y = currentAnimation.CurntKeyFrame.Height / 2;

                currentAnimation.Update(gameTime);

                if (currentAnimation.IsComplete)
                {
                    if (!string.IsNullOrEmpty(currentAnimation.TransitionKey))
                    {
                        PlayAnimation(currentAnimation.TransitionKey);
                    }
                }
            }
        }

        #endregion

        #region Draw

        /// <summary>
        /// Draws the AnimatedEntity
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values</param>
        /// <param name="spriteBatch">The SpriteBatch object we will us to draw</param>
        public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            if (currentAnimation != null)
            {
                spriteBatch.Draw(spriteSheet, position, currentAnimation.CurntKeyFrame.Source, tintColor,
                    rotation, origin, scale, flipEffect, 0);
            }
        }

        #endregion
    }



The first field is a Dictionary<string, Animation> that will hold all the Animation objects this entity will use. The string value we pass in as the Key will be the name field we gave our animation class. The next is an Animation field to hold a reference to the currently playing animation. The rest of the fields will be passed to the SpriteBatch object we use to draw the entity.

The AnimatedEntity LoadContent methods are used to load or set our sprite sheet texture. AddAnimation is a helper method to add animations to the entity. We check to see if the Dictionary contains the key (name) of the animation we want to add and if not we are safe to add it. The PlayAnimation class is how we tell the entity what animation to update. We pass in the name of the animation we want to play and do a few checks to make sure its safe to switch animations. If we pass the tests we will set the new animation and call the Reset method to be sure it plays fresh. Next in line is the Update method. In this method we handle the logic for updating our currently playing animation and transitions. We first check if the currentAnimation field is null and if not we can start updating it. We set our origin to the center of our source rectangle contained in currentAnimation.CurntKeyFrame. Now we call the Animations Update method to begin cycling through all of the KeyFrames. If the animations IsComplete flag is true we know that it doesn’t loop so we can check if the transitionKey has a value and switch to that animation. This lets us have an animation that is only meant to transition into others. For example (this is a mouthful) if we want our player to go from an “idle” animation to a “start running” animation and finally into a “running loop” animation we would only have to pass the name given to the “running loop” animation into the “start running” animations constructor. When we set the entity to “start running“ from “idle” it will automatically switch to the running loop on the last frame of the “start running” animation. Last but not least we have the Draw method. This does what you would expect. We give it a SpriteBatch object and call spriteBatch.Draw passing the sprite sheet texture, position, the source of the current KeyFrame and the rest of the drawing related fields.

That is it! You should now have a functioning system that updates and draws spriteSheet based animation with some nifty features. To preview the system in action add this texture http://flic.kr/p/dZW96E (Which I take no credit in creating) to your XNA Game Content and a SpriteFont of your choice.

Add these fields to your Game1 class :

        private SpriteFont font;
        private AnimatedEntity entity;
        private KeyboardState prevKeyState;
        private KeyboardState curntKeyState;

        private const string idleTrans = "idleTrans";
        private const string idle = "idleLoop";
        private const string run = "runLoop";
        private const string stopRun = "stopRun";



In the LoadContent method of Game1 add this code :

// Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            font = Content.Load<SpriteFont>("font"); //<--- make sure its the name of your font

            entity = new AnimatedEntity(new Vector2(GraphicsDevice.Viewport.Width / 2, GraphicsDevice.Viewport.Height / 2),
                1, Color.White);
            entity.LoadContent(Content, "spriteSheet");

            // Each frame in the sprite sheet is 55x70
            Animation idleLoop = new Animation(idle, true, 1, string.Empty);
            idleLoop.AddKeyFrame(770, 0, 55, 70);

            Animation idleTransition = new Animation(idleTrans, false, 12, idle);
            for (int i = 0; i < 14; i++)
            {
                idleTransition.AddKeyFrame(i * 55, 0, 55, 70);
            }

            Animation runLoop = new Animation(run, true, 12, string.Empty);
            for (int i = 0; i < 15; i++)
            {
                runLoop.AddKeyFrame(i * 55, 70, 55, 70);
            }

            Animation stopRunTrans = new Animation(stopRun, false, 20, idleTrans);
            for (int i = 0; i < 13; i++)
            {
                stopRunTrans.AddKeyFrame(i * 55, 140, 55, 70);
            }

            entity.AddAnimation(idleLoop);
            entity.AddAnimation(idleTransition);
            entity.AddAnimation(runLoop);
            entity.AddAnimation(stopRunTrans);

            entity.PlayAnimation(idleTrans);




In the Game1 Update method add This above base.Update(gameTime) :

            prevKeyState = curntKeyState;
            curntKeyState = Keyboard.GetState();

            if (prevKeyState.IsKeyDown(Keys.D) && curntKeyState.IsKeyDown(Keys.D))
            {
                entity.PlayAnimation(run);
                entity.FlipEffect = SpriteEffects.None;
            }

            if (prevKeyState.IsKeyDown(Keys.A) && curntKeyState.IsKeyDown(Keys.A))
            {
                entity.PlayAnimation(run);
                entity.FlipEffect = SpriteEffects.FlipHorizontally;
            }

            if (entity.CurntAnimationName == run)
            {
                if ((prevKeyState.IsKeyUp(Keys.A) && curntKeyState.IsKeyUp(Keys.A)) &&
                    (prevKeyState.IsKeyUp(Keys.D) && curntKeyState.IsKeyUp(Keys.D)))
                {
                    if (entity.CurntAnimationName != idleTrans || entity.CurntAnimationName != idle)
                    {
                        entity.PlayAnimation(stopRun);
                    }
                }
            }

            entity.Update(gameTime);




And finally in the Game1 Draw method paste this :

            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);

            entity.Draw(gameTime, spriteBatch);
            spriteBatch.DrawString(
                font, entity.CurntAnimationName, 
                new Vector2((GraphicsDevice.Viewport.Width / 2), 20), Color.Black,
                0, font.MeasureString(entity.CurntAnimationName) / 2, 1, SpriteEffects.None, 0);

            spriteBatch.End();



Press F5 and use the A & D buttons to make the characters animations change. Thanks for reading and Enjoy!

If you have any constructive criticism directed at this Guide please let me know in the comments!

Is This A Good Question/Topic? 0
  • +

Page 1 of 1