(X) Hide this
    • Login
    • Join
      • Generate New Image
        By clicking 'Register' you accept the terms of use .

XNA for Silverlight developers: Part 3 - Animation (transforms)

(11 votes)
Peter Kuhn
>
Peter Kuhn
Joined Jan 05, 2011
Articles:   44
Comments:   29
More Articles
0 comments   /   posted on Feb 09, 2011
Categories:   Gaming , Windows Phone , General
Are you interested in creating games for Windows Phone 7 but don't have XNA experience?
Watch the recording of the recent intro webinar delivered by Peter Kuhn 'XNA for Windows Phone 7'.

This article is part 3 of the series "XNA for Silverlight developers".

This article is compatible with Windows Phone "Mango". 
Latest update: Aug 1, 2011.

Animation is a word of Latin origin and means "make alive/bring to live" (animare) or "spirit" (animus). It describes the technique of using a series of single images to create the illusion of movement for the beholder. We all know that concept from the movies and television where the rapid display of a sequence of frames results in the impression of natural motion. In computer graphics and especially games, we use the same technique to bring characters and other elements to life, give the player feedback about their input commands and create a more interesting overall experience. In this article, we'll learn more about animation in general and how it is done in XNA in particular.

Two kinds of animations

When I work with computer games, I like to distinguish between two kinds of animations: transformations and frame-based animation. We have already seen a few glimpses of transformations in the previous parts; when we drew the first sprite onto the screen and moved it around, we have used a translation to create a circular motion in part 1. In part 2 we talked about scaling, which is another simple transformation. These transformations will be part of this article.

The other type of animation that can frequently be found in 2D games is frame-based, which means that the actual content of an element on the screen (i.e. the bitmap that forms the sprite) changes over time. This technique is used when a certain effect cannot be achieved by using the mentioned transformations. For example, imagine a player character that moves its feet when it is walking. There is no way of achieving this animation by applying a transformation or a set of transformations like translations, rotations and scaling to the whole sprite. Instead, you use multiple different images that are shown on the screen in succession to create this animation – just like in a movie. We'll take a deeper look at those animations in the second article about animations.

Transformations Theory

From a mathematical point of view, a transformation defines how to map points from one coordinate space to another one using transformation matrices. All of the basic transformations we have shortly talked about so far are so-called affine transformations, which simply put means that after you apply such a transformation, points that were on a line before are still on a line, lines that were parallel before are still parallel afterwards, and ratios of distances are preserved. Samples for these transformations are:

  • Scale
  • Skew
  • Rotate
  • Translate

You most likely already guessed that there also are non-affine transformations that are more complex. Silverlight's perspective transforms (projections) that allow you to apply 3D-like effects to 2D elements are an example for that.

Luckily for us the frameworks we are working with make things easier when we need to use most of those common transformations, so we don't have to dig deeper into the mathematical theory at this point. They are supported by pre-configured shortcuts, helper classes and simplifications, so chances are high you won't ever see a transformation matrix or have to do the involved computations manually when you're writing a 2D game.

The situation in Silverlight

In Silverlight, there are multiple factors that determine the position and final appearance of an object on the screen. It has in fact two different concepts that influence this: the layout process and render transforms. The layout process is the mechanism that is used to position an element with respect to its size, container and siblings. Of course the key component in that process is what layout container is used to hold the element. Some containers, like the stack panel for example, position their children automatically, whereas others like the canvas don't. In addition, properties like the margin affect the final layout position too.

Separately from that, you have the possibility to change the visual appearance of an element by using render transforms. Those transformations are applied after the measure and arrange passes of Silverlight's layout process and hence do not affect the positioning of other elements on the screen. This makes them considerably cheaper to use than manipulating properties that trigger a layout update (so if you're able to achieve the same effect using both layout properties and render transforms, use the render transform). Render transforms cover pretty much all the transformations we've discussed above. They enable an extremely simple usage where you only need to set the required parameters (like an angle for a rotation), and the rest is done for you automatically behind the scene. The Silverlight documentation on MSDN actually provides a very sophisticated explanation of transforms and the involved classes and properties.

When it comes to actually animating these transforms, Silverlight has the very powerful tool of storyboards. They can contain one or more animations that are used to manipulate an object's properties over time. Of course the mentioned render transforms are suitable targets for these animations. If you combine all these tools and make use of some further helpers like triggers and actions, you can create complete animations without writing a single line of code. Especially if you're using a software like Expression Blend, that is a matter of only a few minutes. Here is an example of an image declaration in XAML:

 <Image x:Name="image"
        Source="Block.png">
     <Image.RenderTransform>
         <TranslateTransform />
     </Image.RenderTransform>
     <i:Interaction.Triggers>
         <i:EventTrigger>
            <ei:ControlStoryboardAction Storyboard="{StaticResource DemoStoryboard}" />
         </i:EventTrigger>
     </i:Interaction.Triggers>
 </Image>

What we can see here is how transforms are attached to an element in XAML (line 3-5). Lines 6-10 contain the magic of an event trigger that starts a storyboard named "DemoStoryboard" when the image is loaded. Such a storyboard can look like this:

 <Storyboard x:Name="DemoStoryboard"
             RepeatBehavior="Forever">
     <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"
                                    Storyboard.TargetName="image">
         <LinearDoubleKeyFrame KeyTime="0"
                                 Value="0" />
         <LinearDoubleKeyFrame KeyTime="0:0:1"
                                 Value="170" />
         <LinearDoubleKeyFrame KeyTime="0:0:2"
                                 Value="340" />
         <LinearDoubleKeyFrame KeyTime="0:0:3"
                                 Value="200" />
         <LinearDoubleKeyFrame KeyTime="0:0:4"
                                 Value="0" />
     </DoubleAnimationUsingKeyFrames>
     <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
                                    Storyboard.TargetName="image">
         <LinearDoubleKeyFrame KeyTime="0"
                                 Value="0" />
         <LinearDoubleKeyFrame KeyTime="0:0:1"
                                 Value="440" />
         <LinearDoubleKeyFrame KeyTime="0:0:2"
                                 Value="200" />
         <LinearDoubleKeyFrame KeyTime="0:0:3"
                                 Value="50" />
         <LinearDoubleKeyFrame KeyTime="0:0:4"
                                 Value="0" />
     </DoubleAnimationUsingKeyFrames>
 </Storyboard>

This storyboard shows several concepts: it contains two separate animations, one that targets the "X" property of the translate transform that is attached to the image, and a second one that targets the "Y" property. Both animations work with multiple values and time stamps to move the image element around on the screen. We can also see that the "RepeatBehavior" property of the storyboard is set to "Forever" so the animation starts over once it is finished.

Animation

The beauty of all this is not only that we've created an animation without writing a single line of code. The great amount of built-in animation types which in turn often have a variety of properties (for example different kinds of key frames that could be used in the above example) make it possible to tune those animations with extremely little effort. Furthermore, since version 3 Silverlight offers so-called easing functions that allow you to apply custom mathematical formulas to animations. And again it already provides a variety of built-in functions that let you create bounce or spring animations and others without custom code. The Silverlight sample code gallery on MSDN has a nice playground you can use to explore those features. All in all Silverlight's animation system in combination with render transforms is really powerful and simple to use.

... and in XNA

We've already learned that XNA has no layout engine like Silverlight does. On the one hand that means that you lose a lot of comfort which Silverlight's layout containers offer, on the other hand this kind of simplifies the process of using transformations, as you don't have to cope with multiple concepts in parallel when you're using XNA. There's basically one single place where all the parameters for the final visual appearance of a sprite on the screen can be set at once, and that is when you draw it using the sprite batch class (of course there are other factors that influence the final looks in XNA too, but for now this explanation is sufficient).

 public void Draw (
          Texture2D texture,
          Vector2 position,
          Nullable<Rectangle> sourceRectangle,
          Color color,
          float rotation,
          Vector2 origin,
          float scale,
          SpriteEffects effects,
          float layerDepth
 )

As you can see the draw method of the sprite batch class has the possibility to pass in various arguments that allow manipulation of the rendering, including the position, rotation and scale values as well as the origin for transformations, a SpriteEffects enumeration value for mirroring features and a layer depth value for z-ordering. As you can see, there's no counterpart for Silverlight's skew transform in XNA. If you want to achieve such an effect, you have to create the matrix transform for it yourself.

The other huge limitation in XNA is that there is no built-in feature like the storyboards in Silverlight. Where you create animation descriptions declaratively in Silverlight and behind the scenes all the computations are done for you at runtime automatically, you have to perform most of that work in XNA manually. It's still not that hard for simple animations, but if you try to mimic something like the easing functions of Silverlight for example, you'll quickly find yourself spending quite some time programming mathematical formulas. In the following paragraphs we'll take a look at basic and more advanced transforms and animations.

Translation

This is probably the most simple form of animation you can do in your game. It only involves manipulation of the position where an object will be drawn on the screen.

As you can see, translating an object requires changing the x- and y-coordinates of its position. By doing so you can move the object to any place on the screen and even off-screen. We had seen an example for that in Part 1 of the series already. In the Update method of our game class, we calculated the position (a 2-dimensional vector) like this:

 // take the screen width
 var screenWidth = graphics.GraphicsDevice.Viewport.Width;
  
 // calculate the new rotation agle
 angle -= gameTime.ElapsedGameTime.TotalSeconds;
  
 // update the x and y coordinates
 position.X = (float)(screenWidth / 2.0 - block.Width / 2.0 + 200 * Math.Cos(angle));
 position.Y = (float)(screenWidth / 2.0 - block.Height / 2.0 + 200 * Math.Sin(angle));

This resulted in a circular motion around a central point of the screen with the circle having a radius of 200 pixels. The important part we had learned was that you need to update the position (and every other value as we'll see in a moment) dependent on the amount of time that has elapsed since the last call to the update method. In the above sample, the elapsed seconds were added to the rotation angle. Since 2*pi describes a full circle (remember, we are using radians here) that means that the sprite ("block") finished a full turn after 2*pi ~= 6.28 seconds. If you wanted to make the sprite take 10 seconds to finish one circle, you would have calculated the angle as:

 // target a duration of 10 seconds for a full circle
 // full circle = Math.PI * 2
 // => (Math.PI * 2) / 10.0 units per second
 angle -= ((Math.PI * 2) / 10.0) * gameTime.ElapsedGameTime.TotalSeconds;

When you play around with this, you'll quickly get used to the following pattern:

  • Determine how far you want to move an object per second (here: 1/10th of a circle)
  • Multiply that by the amount of elapsed seconds (which usually will be a small fraction of one second)

In the Draw method of the game class, we used that calculated position to actually draw the sprite on the screen:

 spriteBatch.Begin();
 spriteBatch.Draw(block, position, Color.Red);            
 spriteBatch.End();

Scale

Scaling describes the method to change the size of an object. In XNA, there are two methods for scaling sprites; the more common on is a uniform scale factor you can pass to the draw method of the sprite batch (we'll learn about the second one in the next section). Let's jump right into a sample. First of all, we define the two class fields we need for this:

 float scale = 1.0f;
 float scalingPerSecond = 0.5f;

The first one holds the current scale factor (in the beginning this will be 1.0f which is the original size of the sprite). The second variable describes the change of the scale factor per second we want to achieve. Once again we determine the amount of change for the transform dependent on the elapsed time and a target value per second. To this end, I've changed the Update method of the game class to calculate the scaling:

 // calculate the scaling
 scale += scalingPerSecond * (float)gameTime.ElapsedGameTime.TotalSeconds;
 if (scale > 1.5f)
 {
     // if the block has grown to 150%,
     // reverse the scaling process
     scale = 1.5f;
     scalingPerSecond *= -1.0f;
 }
 else if (scale < 0.5f)
 {
     // if the block has shrunk to 50%,
     // reverste the scaling process
     scale = 0.5f;
     scalingPerSecond *= -1.0f;
 }

The content of line 2 is crucial. It adds the fractional change in scaling to the current scale factor, depending on the elapsed seconds. The rest of the code simply reverses the scaling (from growth to shrinking and vice versa) when the sprite size exceeds certain limits (the desired target values). When we draw the sprite, the code will look like this:

 // draw a second block that is scaled
 spriteBatch.Draw(block,
     Vector2.Zero,
     null,
     Color.Yellow,
     0.0f,
     Vector2.Zero, // this is the origin
     scale, // pass in the current scaling
     SpriteEffects.None,
     0.0f);
You can ignore most of the arguments for now, but please note how the scale factor is passed in in line 8. Also take notice of the argument on line 7 which is the origin used for all transformations. In the above code, we use the constant Vector2.Zero here. In this case this means that the upper left corner of the sprite will be the center of scaling and hence a fixed point.

You can pass in a different vector here, for example the center of the sprite, like this:

 spriteBatch.Draw(block,
     Vector2.Zero,
     null,
     Color.Yellow,
     0.0f,
     new Vector2(block.Width / 2.0f, block.Height / 2.0f), // this is the origin
     scale, // pass in the current scaling
     SpriteEffects.None,
     0.0f);

This will result in a scaling behavior that has the center of the sprite as origin:

This hopefully reminds you of similar properties we have in Silverlight's transforms, for example the CenterX and CenterY properties of the ScaleTransform. XNA's origin parameter however affects all the transforms equally; in that it behaves similar to Silverlight's RenderTransformOrigin, except that it does not use relative coordinates. This behavior is noteworthy, because the change in the above code snippet not only results in a different scaling, but it also moves the object to a different position compared to the previous code:

Before, the Vector2.Zero position described the location of the upper left corner of the sprite. Now that the origin has been set to the center of the sprite, the Vector2.Zero position describes the location of that center, thus effectively moving the sprite to the upper left by half its size.

Tip: you should be thoughtful regarding the origin parameter. In some situations it might be necessary to handle the origin differently on a case to case basis, depending on what you want to achieve in a particular situation. However my experience is that inexperienced XNA developers tend to lose track of this, especially when different developers working on the same project use different methods. The result then sometimes is very unpleasant situations where you have to track down "bugs" that surface as objects showing in the wrong places and are amazingly hard to analyze.

Using the destination rectangle

The second method you can use for scaling in XNA is to use a destination rectangle when you draw your sprite. The destination rectangle is actually a combination of both the translate and scale transforms: the X and Y properties determine the position, whereas the width and height of the rectangle determine the size of the final sprite. Using this method has two advantages:

  • You don't have to know the source size of a sprite to achieve a certain target size. That means that your code will continue to work and result in the same visual dimensions even when the size of the source texture changes at a later point.
  • You can use the rectangle to stretch the sprite in a non-uniform way, which is not possible with the scale factor method.
 // draw a block with the destination rectangle parameter
 Rectangle destination = new Rectangle(10, 40, 100, 50);
 spriteBatch.Draw(block,
     destination,
     Color.Yellow);

Here you see that the sprite is drawn at position (10,40), and has a width of 100 pixels and a height of 50 pixels. Since the source texture is square, this will squeeze the result to something like this:

And again, no matter what size the source is, these absolute dimensions will stay the same, which would be different when you use the previous scale method.

Please note that when you use the overload of the draw method that takes both a destination rectangle and an origin parameter, the result will behave as described before, i.e. the X and Y coordinates of the rectangle will determine the location of the origin.

Rotate

Rotating a sprite is pretty straight forward. The draw method of the sprite batch class takes an angle argument for this. All you have to remember is that this must be radians. In Silverlight when you are rotating an element using a rotate transform the Angle property takes degrees. If you are more comfortable using degrees then you can easily convert between both units:

radians = degrees * (pi / 180)

and

degrees = radians * (180 / pi)

XNA also has a MathHelper class which has methods to convert between degrees and radians you can use instead.

Once again the origin argument plays a crucial role in how your rotation is actually performed. The default value (Vector2.Zero) uses the upper left corner as fixed point which results in a rotation like this:

When you want to rotate around the center of an object you need to set the origin accordingly:

In our sample game, we already have calculated an angle for the movement of the block on the screen. We'll use that very same angle to rotate the block at the same time it does its circular motion, so the same side always faces the center of the circle. Since we also change the origin parameter so the block rotates around its center, we need to adjust the position calculation too - this is actually a good example of how fiddling around with the origin for your transformations can require changes in other places of your calculations. The new code looks like this:

 // take the screen width
 var screenWidth = graphics.GraphicsDevice.Viewport.Width;
  
 // target a duration of 10 seconds for a full circle
 // full circle = Math.PI * 2
 // => (Math.PI * 2) / 10.0 units per second
 angle -= ((Math.PI * 2) / 10.0) * gameTime.ElapsedGameTime.TotalSeconds;
  
 // update the x and y coordinates
 // we are now using the center of the sprite as origin,
 // which means we do not have to take the sprite size into account
 // for the position calculation
 position.X = (float)(screenWidth / 2.0 + 180 * Math.Cos(angle));
 position.Y = (float)(screenWidth / 2.0 + 180 * Math.Sin(angle));

As you can see the code to calculate the position actually got simpler in this case, as we do not need to take the sprite's size into account now that the origin is set to the center of the sprite.

The draw method now also takes the calculated angle as rotation value (line 6):

 // draw the first block that moves
 spriteBatch.Draw(block,
     position,
     null,
     Color.Red,
     (float)angle,
     Vector2.Zero,
     scale,
     SpriteEffects.None,
     0.0f);

Advanced topic: easing functions

Linear movement often isn't very pleasing to the eye and doesn't model reality very accurately. For example, when an object starts moving, then it looks a lot more natural when it starts off slow and gains speed as it goes. People are used to this kind of movement from the real world (inertia), and even if they don't know the physics behind it, they feel that something isn't quite right when your animated objects don't account for this. In this example, I'll show you how to make use of the built-in math helper class to create non-linear movement of an object. The idea is the following:

  • Take the current position of your object and use it as start position
  • Determine the target position
  • Determine the time you want to take the object to go from the start to the target position
  • Depending on the elapsed time, use a mathematical formula to interpolate the new position

This will work for a lot of easing functions, but of course there are endless possibilities, so some behave differently and may need less or more parameters to work. First of all, define the required class fields for this:

 Vector2 currentPosition;
 Vector2 sourcePosition;
 Vector2 targetPosition;
 double cycleTime = 3.0;
 double currentCycleTime = 0.0;

We keep track of the current position of the object and need the source and target position for the calculation. We also keep track of the elapsed time and how long we want the whole movement to take. The next thing we need to do is initialize the position values. I wanted to move the sprite from the upper left corner to the lower right corner, so I put the initialization code into the LoadContent method of the class, because I need the sprite dimensions for this:

 block = Content.Load<Texture2D>("Block");
  
 // initialize the source positions
 sourcePosition = new Vector2(10, 10);
  
 // initialize the target position
 var screenWidth = graphics.GraphicsDevice.Viewport.Width;
 var screenHeight = graphics.GraphicsDevice.Viewport.Height;
 targetPosition = new Vector2(screenWidth - block.Width - 10,
                              screenHeight - block.Height - 10);
  
 // initialize the current position
 currentPosition = sourcePosition;

Nothing spectacular here. We just calculate the source and target positions with some margin, and set the current position to the start point. The interesting part is the code in the Update method, which looks like this:

 // first of all we sum the elapsed time
 currentCycleTime += gameTime.ElapsedGameTime.TotalSeconds;
  
 // now we calculate the weight
 var weight = (float)(currentCycleTime / cycleTime);
   
 // the math helper provides the actual position
 var currentX = MathHelper.SmoothStep(sourcePosition.X, targetPosition.X, weight);
 var currentY = MathHelper.SmoothStep(sourcePosition.Y, targetPosition.Y, weight);
   
 // set the new position
 currentPosition = new Vector2(currentX, currentY);
   
 // check whether we've arrived at the target position yet
 if (currentPosition == targetPosition)
 {
     // swap source and target
     var oldSource = sourcePosition;
     sourcePosition = targetPosition;
     targetPosition = oldSource;
   
     // reset cycle time
     currentCycleTime = 0f;
 }

First we sum the elapsed seconds and determine how far into the targeted duration for a full cycle we are. This will usually result in a value between zero and one, but might also exceed one slightly in certain cases. You should either clamp the value to zero and one or make sure that your mathematical function takes care of this (as it does in our sample).

The crucial part is located in lines 8 and 9. We use the SmoothStep helper function to do a cubic interpolation between the source position and target position values, depending on the amount of cycle time that has passed. This will result in a motion that starts off slow and accelerates over time, just to decelerate and become slower again when it approaches the target position, much like a pendular movement. This is the place where you could also plug a custom function. The rest of the code just swaps the source and target position when the cycle has finished to make the object move back to the previous position, which results in an periodic movement. You can take a look at the final result in the included sample applications below.

Summary

In this part of the series we have learned about the three most common basic transforms: translate, rotate and scale and how to use them in XNA. We have also seen how we can animate objects using these transforms, and how this requires more manual work in XNA compared to Silverlight's really powerful animation system. The last part gave you a glimpse of more complex and advanced animations, which can become more challenging but often are well worth the effort when you look at the results they make possible. You can download the source code to the various samples above here:

Download source code

While these animations are sufficient for simple games (think Spacewar! or Tetris) the next part of the series will take the concept of animations to the next level and introduce the frame-based approach which opens the door to really unlimited possibilities and most likely better suits what you associate with "animation" these days.

About the Author

Peter Kuhn aka "Mister Goodcat" has been working in the IT industry for more than ten years. After being a project lead and technical director for several years, developing database and device controlling applications in the field of medical software and bioinformatics as well as for knowledge management solutions, he founded his own business in 2010. Starting in 2001 he jumped onto the .NET wagon very early, and has also used Silverlight for business application development since version 2. Today he uses Silverlight, .NET and ASP.NET as his preferred development platforms and offers training and consulting around these technologies and general software design and development process questions.

He created his first game at the age of 11 on the Commodore 64 of his father and has finished several hobby and also commercial game projects since then. His last commercial project was the development of Painkiller:Resurrection, a game published in late 2009, which he supervised and contributed to as technical director in his free time. You can find his tech blog here: http://www.pitorque.de/MisterGoodcat/


Subscribe

Comments

No comments

Add Comment

Login to comment:
  *      *       

From this series