Tutorial - Sprite Animation Framework
![]()
Welcome to my first tutorial here on Xna3Way.com. No, this is not another “how to create a Sprite” tutorial. I figure you can get those types of topics either from the help file or from the wealth of other tutorials that are already out there on the net. My goal in my set of tutorials is to cover topics that you might not see elsewhere. I’ll be covering all sorts of cool topics from Sprite Animation Frameworks, to Flocking algorithms, to board game AI, and many others. Basically, wherever the prevailing winds decide to take me. I warn you now though that I’m a stickler for well-designed code. So if you are looking for truly craptastic code that you can copy-and-paste and spend days trying to get work, you definitely won’t find it here. Wait, you _aren’t_ looking for that you say? I didn’t think so
. Well then, read on.
In this tutorial, we will discuss building a Sprite Animation framework that you can use to do all sorts of cool and complex animations of sprites. The framework we will implement is very simple. At the same time, it also enables some pretty complex behaviors via animation composition. So enough of me “blowing smoke where the sun don’t shine,” let’s get down to business y’all.
The End Product
For those of you that are as impatient as I am when it comes to tutorials, let’s just skip to the end and see what we will be building in this tutorial. Here is a video that shows the animation framework in action (for those of you who can’t see the embedded video player below, you can watch the video here).
This is what we will be building in this tutorial.
Our Goals
Let’s talk a little about the goals that we are wanting to achieve with this framework we will be building.
First of all, it should be simple to add animations to a Sprite. Not only that, but the individual animations should be small and easy to maintain. If animations are bulky, it becomes a pain to use them. And we don’t want that, do we? By making the individual animations very small and atomic, we can build more complex animations by bundling up smaller animations together. This compositional model for animations becomes very powerful, as we’ll see below.
Sprites
Since a Sprite Animation framework would largely be useless without a Sprite class to animate, we might as well start there. The main definition of a sprite is split among two classes, the Sprite class and the SpriteParameters class. The Sprite is the heart of our system. The SpriteParameters class defines the “essence” of a sprite like the Position, Tint, Scale, etc. By splitting this information out into its own class, it becomes easier to animate it by simply requesting the various animations to modify a SpriteParameters instance to reflect its animation results. Here’s what they look like:
![]()
The SpriteParameters class is pretty straight forward. Let’s look at the code:
1: class SpriteParameters : ICloneable
2: {
3: //
4: // Fields
5: //
6: public bool IsVisible = true;
7: public Vector2 Origin = Vector2.Zero;
8: public Vector2 Position;
9: public float Rotation;
10: public float Scale;
11: public Color Tint;
12:
13: //
14: // Constructors
15: //
16: public SpriteParameters()
17: : this(Vector2.Zero, Color.White, 0, 1) { }
18: public SpriteParameters(Vector2 position, Color tint, float rotation, float scale)
19: : this(position, tint, rotation, scale, true, Vector2.Zero) { }
20: public SpriteParameters(Vector2 position, Color tint, float rotation, float scale, bool isVisible, Vector2 origin)
21: {
22: this.Position = position;
23: this.Tint = tint;
24: this.Rotation = rotation;
25: this.Scale = scale;
26: this.IsVisible = isVisible;
27: this.Origin = origin;
28: }
29:
30: //
31: // Public Methods
32: //
33: public object Clone()
34: {
35: return new SpriteParameters(Position, Tint, Rotation, Scale, IsVisible, Origin);
36: }
37: }
The first thing you might notice is that I’m not using properties in this class. There’s a good reason for this. I don’t believe in using language features just because you “should” use them. It all depends on the situation. In this case, SpriteParameters is a very simple data structure that has no behavior. Because of this, I believe there is no reason to encapsulate the fields at this time. If at a later date I need to add behavior to this class, I can refactor the public fields into properties then.
The implementation of ICloneable is merely a convenience. This way, when we need to create a new instance of SpriteParameters, we can simply clone the existing one defined for the Sprite that we are bound to. We will leverage this Clone() method in the AnimationManager class, which we’ll take a look at in the next section.
Finally there is the heart of our system, the Sprite. Most of the Sprite class is pretty self explanatory. Let’s take a look:
1: class Sprite
2: {
3: //
4: // Fields
5: //
6: private AnimationManager animationManager;
7: private SpriteBatch batch;
8: private ContentManager contentManager;
9: private Texture2D image;
10: private SpriteParameters parameters;
11:
12: //
13: // Constructors
14: //
15: public Sprite(GraphicsDevice graphicsDevice, ContentManager contentManager, Vector2 position, string assetName)
16: {
17: this.contentManager = contentManager;
18: this.image = contentManager.Load(assetName);
19:
20: this.parameters = new SpriteParameters();
21: this.parameters.Position = position;
22:
23: this.animationManager = new AnimationManager(this);
24: this.batch = new SpriteBatch(graphicsDevice);
25: }
26:
27: //
28: // Properties
29: //
30: public AnimationManager Animations
31: {
32: get { return this.animationManager; }
33: }
34:
35: public Texture2D Image
36: {
37: get { return this.image; }
38: }
39:
40: public SpriteParameters Parameters
41: {
42: get { return this.parameters; }
43: set { this.parameters = value; }
44: }
You can see this first part of our Sprite class is pretty simple. The largest part of the Sprite class is our Draw method (which really isn’t all that large as you’ll see:
1: //
2: // Public Methods
3: //
4: public virtual void Draw(GameTime gameTime)
5: {
6: SpriteParameters animatedParameters = animationManager.Animate(gameTime);
7:
8: if (animatedParameters.IsVisible)
9: {
10: batch.Begin();
11:
12: batch.Draw(image,
13: animatedParameters.Position,
14: null,
15: animatedParameters.Tint,
16: animatedParameters.Rotation,
17: animatedParameters.Origin,
18: animatedParameters.Scale,
19: SpriteEffects.None,
20: 0);
21:
22: batch.End();
23: }
24: }
25: }
You can see here that we get the SpriteParameters that we use to draw our sprite from our actual animation framework. The main interface into our new animation framework is our AnimationManager. We’ll take a look at that in the next section. As you can see, the interface into our animation framework is very small and very simple. Remember, that is one of our goals.
Since we haven’t hit any of the “meat” yet, I will go ahead and move on to our Animation framework
. MOVING ALONG!!!
AnimationManager
Sprites are the heart of our system, that’s true. But Sprites our only a part of the larger picture. To animate our Sprites, we need the framework that will do the actual animation. This is achieved through a simple AnimationManager that will manage all the various animations that are attached to a sprite. The interface to our AnimationManager is pretty small. Let’s take a look.
![]()
So, what are animations? As you can see, they run the gamut. The key is that they are each small, and do purely atomic animations. This gives us the power of writing complex animations by composing many smaller animations together rather than creating a seperate animation for every possible combination we could have.
You may also notice that the power of the system is done through composition, not through inheritance. This is intentional. When you inherit one class from another, that newly inherited class is now tightly coupled to its parent. We want to reduce this coupling. If we used inheritance for our different animation types, we would be introducing unnecessary coupling into our system that could limit the number of behaviors we could build (or at least make them less atomic and harder to maintain). We will take a deeper look at the various animations later. For now, lets dig into our AnimationManager, the interface into our animation system.
The “brain” of our animation system is the AnimationManager. While it’s the “brain” of our system, it’s not really all that complicated itself (I know, I’m sure you’re sick and tired of me saying “it’s simple” and “it’s not complicated” already). Let’s look at the code for it.
1: public SpriteParameters Animate(GameTime gameTime)
2: {
3: SpriteParameters animatedParameters = (SpriteParameters)boundSprite.Parameters.Clone();
4:
5: if (animations.Count > 0)
6: {
7: // Process attached animations
8: SpriteAnimation currentAnimation = animations[0];
9: currentAnimation.Animate(gameTime, animatedParameters);
10:
11: if (currentAnimation.IsFinished)
12: {
13: animations.RemoveAt(0);
14: }
15: }
Here we process the first animation that we have attached. It is not a foreach of all our animations. When animations are attached, they are processed sequentially (one after the other). This means that the second attached animation won’t process until the first one is finsihed. If you need several animations to run at a single time, that is what the CompositeAnimation is for (as we’ll see in “Putting it all together” below). Once the animation is finished, we remove it from the list of animations that are currently attached to the Sprite. We know an animation is finished because it has marked itself as finished. This means that the concept of a “finished” animation is completely up to the discretion of the animation itself.
1: // Auto adjust location if we are rotating.
2: // This will force a rotation around the center of the image rather than the top left
3: if (animatedParameters.Rotation > 0)
4: {
5: Vector2 center = new Vector2(boundSprite.Image.Width / 2,
6: boundSprite.Image.Height / 2);
7:
8: animatedParameters.Origin = center;
9: animatedParameters.Position += center;
10: }
I admit, this part is kind of a “hack.” When rotating a Sprite in the draw method of a SpriteBatch in XNA, the rotation will actually occur around the Position specified, unless an Origin is specified. So, to get the image to rotate around it’s center, we need to specify the center of the image as the Origin of the image. This has an unintended consequence (well, “intended” if you are fmiliar with the XNA Framework). This consequence is that the Position specified in the draw method is assumed to be the position of the Origin on the screen. So, to have the Position still be the upper left of the image in our parameter stack, we need to offset the Position to the center of the image.
After we have accounted for the rotation of our Sprite, we simply return the animatedParameters that we have modified as part of our animation:
1:
2: return animatedParameters;
3: }
4: }
And that’s all there is for our AnimationManager. Not very interesting in of itself until you consider the various animations that we have implemented as part of our animations framework. So what do the animations shown above in the UML diagram actually do?
SpriteAnimations
The animations above are acutally not all that special. What do they do? Well, let’s discuss.
- TimedAnimation - This animation accepts as a parameter another animation that should run for a specified amount of time. Therefore, if I want the AutoRotateAnimation to run for only two seconds, I can create a TimedAnimation that lasts for two seconds and is passed the AutoRotateAnimation (or any other animation) to run during that timeframe.
- CompositeAnimation - This allows many animations to run at the same time. By default, animations are sequential. If I add two animations to a Sprite via AddAnimation(), the second one added won’t run until the first one is finished. CompositeAnimation is the way to get around this. Since CompositeAnimation is just a SpriteAnimation, an instance of it can be passed to DelayedAnimation like any other type of Animation.
- RotateAnimation - With RotateAnimation, you can specify the “speed” of the rotation (how many seconds it takes to rotate completely around a single time) you wish for a specified Sprite to have. As it exists now, it will auto rotate forever (which is why I’m currently using it below on the windowsSprite in combination with the TimedAnimation to prevent it from lasting forever).
- PathAnimation - This animation accepts two parameters: the speed at which to progress between waypoints, and an array of waypoints to animate to. In the TitleScreen.cs file, I’m only ever using a single waypoint. However, I could pass in three, five, seven, or however many waypoints as I wish and this animation would process from it’s original position to every single waypoint specified in the animation. You could very easily imagine this animation being used as output from an A* algorithm for pathfinding in NPCs and the like.
- BlinkAnimation - This animation will “blink” a Sprite at a specified frequency.
- InvisibleAnimation - If you want a Sprite to “disappear,” this is the way to go. InvisibleAnimation will set the IsVisible property of a Sprite to false every time through the animation loop. By its own definition, an InvisibleAnimation will never actually finish. If you want the Sprite to only be invisible for a specified amount of time, use this animation in combination with a TimedAnimation.
Although we won’t take a look at the code for all of these animations (you can go grab the code zip file for that), we will discuss the implementation for a couple of them. Let’s go ahead and start with the BlinkAnimation.
As mentioned above, the BlinkAnimation will “blink” a Sprite at a specified frequency. It’s pretty simple how this is done:
1: class BlinkAnimation : SpriteAnimation
2: {
3: //
4: // Fields
5: //
6: private TimeSpan blinkLength;
7: private TimeSpan timeLeft;
8: private bool isVisible = true;
9:
10: //
11: // Constructors
12: //
13: public BlinkAnimation(TimeSpan blinkLength)
14: {
15: this.blinkLength = blinkLength;
16: this.timeLeft = blinkLength;
17: }
18:
19: //
20: // Virtual Methods
21: //
22: public override void Animate(GameTime gameTime, SpriteParameters animatedParameters)
23: {
24: timeLeft -= gameTime.ElapsedGameTime;
25: if (timeLeft.TotalSeconds < 0)
26: {
27: // we have elapsed, so blink
28: isVisible = !isVisible;
29: timeLeft = blinkLength;
30: }
31:
32: animatedParameters.IsVisible = isVisible;
33: }
34: }
Yes, a good number of SpriteAnimations are acutally this simple. This is what I was talking about when I said that the compliexity of animations is derived from the composition of many smaller animations rather than complex individual animations. In the case of BlinkAnimation, each time through the Animate loop (calls to Animate) we subtract the elapsed time from the time we have been waiting. If we have reached the blink length, we toggle the visibility flag of our Sprite. This means that if I create a BlinkAnimation with the TimeSpan of two seconds, the Sprite will be visible for two seconds, invisible for two seconds, visible for two seconds, invisible for two seconds, and so on and so on.
How about the TimedAnimation?
1: class TimedAnimation : SpriteAnimation
2: {
3: //
4: // Fields
5: //
6: private SpriteAnimation animation;
7: private TimeSpan timeLeft;
8:
9: //
10: // Constructors
11: //
12: public TimedAnimation(TimeSpan length, SpriteAnimation animation)
13: {
14: this.animation = animation;
15: this.timeLeft = length;
16: }
17:
18: //
19: // Virtual Methods
20: //
21: public override void Animate(GameTime gameTime, SpriteParameters animatedParameters)
22: {
23: timeLeft -= gameTime.ElapsedGameTime;
24:
25: if (timeLeft.TotalSeconds < 0)
26: base.IsFinished = true;
27: else
28: animation.Animate(gameTime, animatedParameters);
29: }
30: }
All we do in TimedAnimation is wait for the specified amount of time, and then mark the animation complete. If the animation isn’t complete yet, we actually call the child animation that we are timing. If you want to see the implementation for the other animations that are part of this whole package, make sure to grab the code from the Downloads section below.
So, what are some examples of some other animations that you can build to augment this framework even more? Here’s a couple that I have implemented in my own games to get your mind thinking:
- DelayedAnimation - This will wait a specified amount of time before kicking off another animation. For example: if I want the sprite to wait for five seconds and then disappear, I can create a DelayedAnimation of five seconds that then kicks off an InvisibleAnimation. This becomes even more powerful when combined with other animations like CompositeAnimation and SequentialAnimation to kick off entire chunks of animations.
- SequentialAnimation - This takes a series of animations that will be executed sequentially. By default, you can do this by simply adding many animations to the Sprite via the AnimationManager. However, this allows you to create a block of animations that are managed together. With the combination of CompositeAnimation, DelayedAnimation, and TimedAnimation, it becomes a very powerful compositional tool to build complex animations out of simple ones.
- ColorAnimation - Animates the color or “tint” of a sprite. Currently, this just sets the tint to a single color (I use it for my menu items and such to emphasize which option is currently chosen). You can probably imagine this being expanded though to allow cycling through the actual color wheel in order to achieve a slightly psychadelic effect.
- PulsateAnimation - This will scale the sprite using a specified speed and scaling factor. This gives the effect of the Sprite “pulsating.” I have used this on my menu option sprites to help emphasize which menu option is chosen, although there are many other uses as well.
What animations can you think to add that would make this animation framework even cooler for your own use? Perhaps enhancing the PathAnimation to use Curves as well to allow curved paths? Perhaps a StretchAnimation that will stretch the sprite along a specified axis? It’s all up to you and your imagination
.
Putting it all together
“How does this all fit together,” you might ask. Good question! When we create our sprites, we also add any animations that we want our sprites to have. It’s really as simple as creating a new animation and adding it to the AnimationManager bound to our Sprite.
Let’s say that I want a sprite to blink every two seconds. To accomplish this, all I need to do is attach a BlinkAnimation to our ActionManager via the Sprite instance. Like this:
1: Sprite mySprite = new Sprite(…);
2: mySprite.Animations.Add(new BlinkAnimation(TimeSpan.FromSeconds(2)));
If I want to rotate a sprite around for three seconds (at a rate of one revolution per second), I could do so with the following:
1: Sprite mySprite = new Sprite(…);
2: mySprite.Animations.Add(new TimedAnimation(TimeSpan.FromSeconds(3),
3: new RotateAnimation(TimeSpan.FromSeconds(1))));
Pretty simple. Let’s see how we built the tutorial. Here is how we create the Sprites in the LoadGraphicsContent method in our sample XNA game:
1: protected override void LoadGraphicsContent(bool loadAllContent)
2: {
3: if (loadAllContent)
4: {
5: mySampleGameSprite = new Sprite(graphics.GraphicsDevice,
6: content,
7: new Vector2(200, -50),
8: ImageFiles.MySampleGame);
9: mySampleGameSprite.Animations.Add(
10: new PathAnimation(200.0f, new Vector2[] { new Vector2(200, 400), new Vector2(200, 200) }));
11:
12: xna3waySprite = new Sprite(graphics.GraphicsDevice,
13: content,
14: new Vector2(10, 500),
15: ImageFiles.Xna3Way);
16: xna3waySprite.Animations.Add(new BlinkAnimation(TimeSpan.FromSeconds(0.4)));
17:
18: windowsSprite = new Sprite(graphics.GraphicsDevice,
19: content,
20: new Vector2(600, 300),
21: ImageFiles.Windows);
22: windowsSprite.Animations.Add(
23: new TimedAnimation(TimeSpan.FromSeconds(4), new InvisibleAnimation()));
24: windowsSprite.Animations.Add(
25: new CompositeAnimation(
26: new RotateAnimation(TimeSpan.FromSeconds(1.5)),
27: new PathAnimation(400.0f,
28: new Vector2[] { new Vector2(20, 300), new Vector2(600, 300) })));
29: }
30: }
Let’s dig apart each one. mySampleGameSprite:
1: mySampleGameSprite = new Sprite(graphics.GraphicsDevice,
2: content,
3: new Vector2(200, -50),
4: ImageFiles.MySampleGame);
5: mySampleGameSprite.Animations.Add(
6: new PathAnimation(200.0f, new Vector2[] { new Vector2(200, 400), new Vector2(200, 200) }));
Here we are using a PathAnimation with two waypoints. This will start the sprite at [200, -50], and move it to [200, 400], followed by [200, 200], at a rate of 200 pixels per second.
1: xna3waySprite = new Sprite(graphics.GraphicsDevice,
2: content,
3: new Vector2(10, 500),
4: ImageFiles.Xna3Way);
5: xna3waySprite.Animations.Add(new BlinkAnimation(TimeSpan.FromSeconds(0.4)));
This will simply blink the xna3way sprite every 0.4 seconds (400 milliseconds). Once again, pretty simple. The windows sprite is where it gets a bit complicated though. Let’s take a look:
1: windowsSprite = new Sprite(graphics.GraphicsDevice,
2: content,
3: new Vector2(600, 300),
4: ImageFiles.Windows);
5: windowsSprite.Animations.Add(
6: new TimedAnimation(TimeSpan.FromSeconds(4), new InvisibleAnimation()));
7: windowsSprite.Animations.Add(
8: new CompositeAnimation(
9: new RotateAnimation(TimeSpan.FromSeconds(1.5)),
10: new PathAnimation(400.0f,
11: new Vector2[] { new Vector2(20, 300), new Vector2(600, 300) })));
Here we have two animations in sequence. First, we make the Sprite invisible for four seconds using a TimedAnimation. Once that animation is finished, we use a CompositeAnimation to perform two different animations at the same time. First of all, we rotate the Sprite at a rate of one revolution every 1.5 seconds. Second of all, we make the windows image move to the left of the screen and back to the right using a PathAnmimation. Because we create the RotateAnimation and PathAnimation within a CompositeAnimation, the image will rotate _while_ it is navigating the path. This is a good example of the type of complex animation you can achieve by simply combining several smaller animations.
In Closing
Well that’s about it for now. I hope you all enjoyed the first tutorial here on Xna3Way.com. I hope that there will be many more like it. There are many subjects I wish to talk about including flocking algorithms, tactical AI, board game AI (Minimax / Negamax), etc. There is definitely no shortage of topics to be discussed (although I’ll freely admit that I’m a slacker, so who knows how many of these subjects will wind up getting tutorials written about them). If you have any questions regarding this tutorial, feel free to contact me at jolson88 AT yahoo DOT com.
So until we meet again, remember, that’s how the cookie crumbles
.
Downloads
Code. A zip file containing the project file and code for this tutorial.