Creating Animations in XNA
For this lecture, we will discuss creating animations in XML. The first step is to create the actual animation frames themselves. We will do this using gimp (though, of course, you can use any graphics program that you like). We will then show how to create a class to handle the animation, and the put it all together. Finally, we will modify our animation to be a little more flexible, so that we could have multiple
Creating the Animation Frames
For this example, we will create a 50x50 pixel spinning coin. First, create a new 50x50 image in gimp. This time we will want to fill with the the background color (instead of a transparency)
Next, we will zoom in a bit (400% is good), and select a circle that fills the entire space
Fill this area with a nice dark brown color, by first changing the color (clicking on the forground panel, then selecting the fill tool, and then clicking anywhere within the circle:
You might want to save this brown color for later by dragging from the "current" area to one of the presets to the right (note that in this example, I've saved this color as the top-left color preset). We can then select a smaller circle inscribed in the brown circle (the ruler guides to the top and left help us line things up well), and color it a nice yellow:
If we were feeling particularly artistic, we could draw in some more detail, but this is good enough for our demo. This will be the first frame of our spinning coin. Now for the next frames. Create a new 50x50 image filled with the same background color. Then copy the original image (click anywhere in the image to bring it to the foreground, hit ctrl-c), and paste it into the new image (click anywhere inside it, and hit ctrl-v)
Next, select the perspective tool, move all 4 corners of the new image in 4 pixels (two marks on the ruler) and hit transform
Once the image is nicely transformed, repeat the process. this time shrinking the image by 8 pixels (4 ruler marks) in either direction. Repeat until you have a nice sequence. I did a special-case for the last frame -- just a rectangle filled in with the same brown color, to make the coin look a little more like a 3D object with a brown rim and a yellow face.
Now that we have all of the individual frames, we want to make a SpriteSheet -- a single images that contains all of the frames. Since we want the animation to go from the full circle, to the edge, back the the full circle, we will need a total of 12 frames, for an image of size 50*12 x 50. This time, we do want the background to be transparent:
With our final sheet in place, we can just cut and paste our images in. Since our images have backrounds, and the target is transparent, it is easy to get them lined up nicely. Be sure to copy the entire image (whole 50x50 square) before pasting:
When we have the entire sheet ready, we will want to remove the background, so that our spinning coin will work anywhere (not just on a white background). From the colors menu, select Color to Alpha, and make the color white into alpha:
Now we have our animation frames! Save them as a .png file (preserves alpha values), and we are ready for putting the animation in!
Animation Class
We are now ready to put the animation in. We will make a lightweight animation class to handle actually animating our image. We do not want this to be a GameComponent -- that's way too much overhead for a simple animation. Instead, we will create a plain C# class for an animated object. This object will contain a draw and update method. Since we don't want the overhead of each animated object owning its own spritebatch, the draw method will take as input a spritebatch parameter:
class AnimatedObject { virtual public void Update(GameTime gameTime) { } virtual public void Draw(SpriteBatch s) { } }What do we need to represent an animated object on the screen?
- Actual 2D texture that contains all animations
- Width of each animation frame (we can assume that the height of each frame is the same as the height of the texture
- Position on the screen of our animation
- We can either use the upper-right corner, or some other position, like the center of the animation. However, if we use a position other then the upper-left corner, we will need to modify our Draw method accordingly
- Time per frame of our animation
- If our animation should loop
- Current frame of the animation
- Time remaining in the current frame of animation
We will also store the number of frames of the animation (we can get that from a simple division, but we will go ahead and store that information so that we don't need to keep recalculating it.)
Our constructor needs the Texture2D and the width of each frame. Everything else can be default values. (We could also provide additional constructors that set various of these values, as a convenience). This gives us a constructor and properties:
class AnimatedObject { public int Width { get; private set; } public bool Looping { get; set; } public Vector2 Position { get; set; } public float TimePerFrame { get; set; } private Texture2D mTexture; private int mCurrentFrame; private float mCurrentFrameTime; private int mNumFrames; public AnimatedObject(int imageWidth, Texture2D texture, bool looping) { Width = imageWidth; mTexture = texture; Looping = looping; mCurrentFrame = 0; TimePerFrame = 0.1f; mCurrentFrameTime = 0; mNumFrames = texture.Width / imageWidth; Position = new Vector2(0, 0); } ... }
A note on code convetions -- for this document, I am using:
- Uppercase letters for method and property names
- lower case letters for local variables
- m<name> for instance variables
You do not need to follow this naming convention in your own code (and I admit that sometimes using m<name>, and sometimes using Properies, can be a bit confusing), but you should have your own code standards that you follow!
Note that the Width property has a public getter, but a private setter -- once we set the width of each frame of the animation, we don't really want to change it. Our update function just needs to add the elapsed game time to the time so far for this frame, incrementing the frame (and looping) as necessary:
virtual public void Update(GameTime gameTime)Note that we want this function to be virtual, just in case we want to subclass our basic animation with slighly different update logic. Our Draw method just needs to draw the correct region of the spritesheet image on the correct location of the screen:
{
mCurrentFrameTime += (float) gameTime.ElapsedGameTime.TotalSeconds;
if (mCurrentFrameTime >= TimePerFrame)
{
mCurrentFrameTime = 0;
if (mCurrentFrame == mNumFrames - 1)
{
if (Looping)
{
mCurrentFrame = 0;
}
}
else
{
mCurrentFrame++;
}
}
}
Rectangle FrameToRectangle(int frameNum) { return new Rectangle(frameNum * Width, 0, Width, Texture.Height); } virtual public void Draw(SpriteBatch s) { s.Draw(Texture, Position, FrameToRectangle(mCurrentFrame), Color.White); }
Putting it All Together
To get our animation to show up, we will need to:
- Add the animation texture to the Content directory (simple drag and drop)
- Load the image into a Texture2D
- Create at least one AnimatedObject using this texture
- Call the update and draw method on the animated object every frame
protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice); // Load our coin texture Texture2D animTexture = Content.Load<Texture2D>("coinFlip"); // Create a new coin object using the coin texture coin = new AnimatedObject(50, animTexture, true); // Set the coin's position coin.Position = new Vector2(50, 50); } protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); // Once again, low-level stuff like updating individual objects should // not be done in the main loop for a real game! coin.Update(gameTime); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); coin.Draw(spriteBatch); spriteBatch.End(); base.Draw(gameTime); }
Now we can run our example game, and see a nice rotating coin. You can do something similar to animate any other object. If you want to have some game object that runs an animation, you can subclass this animation class, and override the update method:
override public void Update(GameTime gameTime) { // Game logic for this object (change position, etc) base.Update(gameTime); // Update animation }
Extending to an Animation System
While the approach above will work, it is a little difficult to extend. What if we wanted to have an object that had multiple animations (run left, run right, idle, jump, etc)? Instead of having our object subclass an animation, we could instead have our object contain an animation -- that way we could swap out animations easily. Our animation would no longer own the position of the object (that will need to be passed in on a draw), only the information needed to draw the animation:
class Animation { public int Width { get; private set; } public bool Looping { get; set; } public float TimePerFrame { get; set; } private Texture2D Texture; private int mCurrentFrame; private float mCurrentFrameTime; private int mNumFrames; public Animation(int imageWidth, Texture2D texture, bool looping) { Width = imageWidth; Texture = texture; Looping = looping; mCurrentFrame = 0; TimePerFrame = 0.1f; mCurrentFrameTime = 0; mNumFrames = texture.Width / imageWidth; } Rectangle frameToRectangle(int frameNum) { return new Rectangle(frameNum * Width, 0, Width, Texture.Height); } public void Reset() { mCurrentFrame = 0; } virtual public void Update(GameTime gameTime) { mCurrentFrameTime += (float)gameTime.ElapsedGameTime.TotalSeconds; if (mCurrentFrameTime >= TimePerFrame) { mCurrentFrameTime = 0; if (mCurrentFrame == mNumFrames - 1) { if (Looping) { mCurrentFrame = 0; } } else { mCurrentFrame++; } } } virtual public void Draw(SpriteBatch s, Vector2 postion) { s.Draw(Texture, postion, frameToRectangle(mCurrentFrame), Color.White); } }
If we wanted to animate an object, it would contain an animation -- and each call to update would call the update method on the contained animation. We could easily extend to have multiple animations per object, storing them in a dictionary:
class AnimatedObject2 { public Vector2 Position { get; set; } private Dictionary mAnimations; private string mCurrentAnimation; public AnimatedObject2() { mAnimations = new Dictionary(); } public void AddAnimation(string name, Animation animation) { mAnimations.Add(name, animation); if (mCurrentAnimation == null) { mCurrentAnimation = name; } } public void SetAnimation(string name) { if (!mAnimations.ContainsKey(name)) { throw new InvalidOperationException(); } mCurrentAnimation = name; mAnimations[name].Reset(); } virtual public void Update(GameTime gameTime) { // Do any updating that we want here ... mAnimations[mCurrentAnimation].Update(gameTime); } virtual public void Draw(SpriteBatch s) { mAnimations[mCurrentAnimation].Draw(s, Position); } }
Once we create one of these animated objects, we will need to set one or more animations:
Texture2D flipTexture = Content.Load("coinFlip"); Texture2D shimmerTexture = Content.Load ("coinShimmer"); coin2 = new AnimatedObject2(); coin2.AddAnimation("flip", new Animation(50, flipTexture, true)); coin2.AddAnimation("shimmer", new Animation(50, shimmerTexture, true));
Further Work
There are lots of things we could do to modify this:
- We duplicated some frames, which is somehwat wasteful of memory (for a spritesheet this small, it's not a big deal). We could, however, only include the spirtes that we needed in the spritesheet. In that case, we would also need to tell the animation the order that we wanted to observe the frames. We will likely need a text file or XML file that included which frames were necessary for the animation. This would be especially useful if you had a game that had walk/run/idle animations for several characters, with many shared frames.
- We could animate the rotation as well. To look right, we would probably want to store the center of the object as the postion, and do a little math to get the drawing working correctly