Sprite Transform Animation

0
248

The preceding hour saw the introduction of a powerful new Animation class that added color animation effect capabilities to Sprite objects. By subclassing Animation, we can create specific animation effects in reusable classes, such as the CycleColor class that demonstrated color cycling over time. There are many more possibilities with the Animation class that have not been explored yet, so we will spend this hour delving into new aspects of animation that are easy to implement and that produce great results for relatively little effort. Specifically, we’ll look at animating the transforms that can be applied with existing properties in the Sprite class: position, rotation, and scaling. These properties are important as they are currently implemented in Sprite, so we won’t mess up what already works. Instead, variations of the Animation class will set up modifiers for these Sprite properties, with changes applied to the Sprite.Animate() method to make it work.

Adding Transform Support to the Animation Class

To perform transforms on a sprite, we need to add some new code to the Sprite.Animate() method. Currently, the method supports only color animation, and we want transforms as well. So, here are the changes:

[code]
public void Animate()
{
if (p_animation != null)
{
if (p_animation.animating)
{
color = p_animation.ModifyColor(color);
position = p_animation.ModifyPosition(position);
rotation = p_animation.ModifyRotation(rotation);
scaleV = p_animation.ModifyScale(scaleV);
}
}
}
[/code]

This is all that is needed to give Animation access to Sprite properties, and we will take full advantage of it!

Position Transforms

When it comes to “animating” the position of a sprite with a custom animation class, we are really getting into custom behaviors, and the term “animation” may not be as appropriate—but it is what it is, so let’s just work with it. If you want to use the term “behavior” in the name for your own transform animation classes, that might seem more appropriate. Since we can do anything with the position, a really dumb but technically valid translation “animation” could be to simply position a sprite to one hard-coded location and never let it leave. That’s silly but it can be done, because the translation modifier added to the Sprite class makes it possible.

The important thing to remember when writing a translation class is to override the ModifyPosition() method (coming from the base Animation class). Any of these base methods that are not overridden will return the passed parameter back, so that no changes are made. Here is just one possible translation class called OrbitalMovement, shown in Listing 13.1. This class inherits from Animation, and exposes a number of properties (which will usually just be passed to the constructor to make initialization simple). ModifyPosition() is where all the work is done. Using some trig, the incoming position is completely ignored, while a calculated orbital position for the object is returned. In other words, it doesn’t matter where a Sprite is located; when this method runs, it calculates position based on sine and cosine calculations, using the properties provided.

LISTING 13.1 Source Code for the OrbitalMovement Class

[code]
public class OrbitalMovement : Animation
{
public int radius;
public Vector2 center;
public double angle;
public float velocity;
public OrbitalMovement(Vector2 center, int radius, double angle,
float velocity)
: base()
{
animating = true;
this.center = center;
this.radius = radius;
this.angle = angle;
this.velocity = velocity;
}
public override Vector2 ModifyPosition(Vector2 original)
{
Vector2 modified = original;
angle += velocity;
modified.X = center.X + (float)(Math.Cos(angle) * radius);
modified.Y = center.Y + (float)(Math.Sin(angle) * radius);
return modified;
}
}
[/code]

Figure 13.1 shows the output of the Transform Animation Demo program, which is found in Listing 13.2. There are a lot of asteroids orbiting a black hole in this small simulation! The really great thing about it is that no orbit code is found anywhere in the main program—it’s all stuffed in the OrbitalMovement class! Let this be just one example of what you can do on your own with similar results. I don’t know about you, but I enjoy producing interesting results with relatively small amounts of code. It is a challenge! Consider how to combine several simple rules to produce interesting results, rather than taking the brute force approach and coding a solution one step at a time. You may be surprised by how this synergy of code works. Following is the code for the program so that you can see how the black hole and asteroids are created and drawn.

The Transform Animation Demo uses a custom animation class to simulate an orbit.
FIGURE 13.1 The Transform Animation Demo uses a custom animation class to simulate an orbit.

LISTING 13.2 Source Code for the Transform Animation Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Random rand;
SpriteFont font;
Texture2D asteroidImg;
List<Sprite> objects;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
rand = new Random();
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
objects = new List<Sprite>();’
Sprite blackhole = new Sprite(Content, spriteBatch);
blackhole.Load(“blackhole”);
blackhole.position = new Vector2(400, 240);
blackhole.scale = 0.5f;
blackhole.origin = new Vector2(64, 64);
blackhole.velocityAngular = 1.0f;
objects.Add(blackhole);
asteroidImg = Content.Load<Texture2D>(“asteroid”);
for (int n = 0; n < 500; n++)
{
Sprite ast = new Sprite(Content, spriteBatch);
ast.image = asteroidImg;
ast.origin = new Vector2(32, 32);
ast.scale = 0.25f + (float)(rand.NextDouble() * 0.5);
ast.velocityAngular = (float)(rand.NextDouble() * 0.1);
Vector2 pos = new Vector2(380 + rand.Next(40),
220 + rand.Next(40));
double angle = rand.NextDouble() * 6.0;
int radius = rand.Next(60, 400);
float velocity = (float)(rand.NextDouble() * 0.01 *
((400-radius) * 0.1) );
ast.SetAnimation(new OrbitalMovement(pos, radius,
angle, velocity));
objects.Add(ast);
}
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
foreach (Sprite spr in objects)
{
spr.Move();
spr.Rotate();
}
base.Update(gameTime);
}’
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
foreach (Sprite spr in objects)
{
spr.Animate();
spr.Draw();
}
spriteBatch.DrawString(font, “Transform Animation Demo”,
new Vector2(0, 0), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]

Rotation and Scaling Transforms

We don’t need a custom Animation subclass just to rotate a Sprite object, so what is the purpose of a so-called rotation animation? Don’t think of rotation in terms of absolute rotation value being set and used to draw. Consider rotation instead in terms of rotation over time. We can transform an animation in small increments over time for interesting results. For example, a sprite could rotate back and forth between 180 degrees to look like it’s wobbling, or it could rotate around at one-second increments like the hand of a clock. It’s all based on the code in your own Animation subclass. To demonstrate, I’ve created an analog clock example. The clock will be based around a class called ClockHand, found in Listing 13.3.

LISTING 13.3 Source Code for the ClockHand Class

[code]
public class ClockHand : Animation
{
public int direction;
public ClockHand(int direction) //0 to 59
: base()
{’’
animating = true;
this.direction = direction;
}
public override float ModifyRotation(float original)
{
float angle = (float)(direction / 60.0f) *
(float)(2.0f * Math.PI);
return angle;
}
}
[/code]

This class is on the simple side so it can be used for hours, minutes, and seconds. Assuming you supply the project with an arrow for the “clock hands,” the ClockHand.direction property represents the time value. For minutes and seconds, this comes to a value from 0 to 59. For hours, we can use the same range by just multiplying hours by 5 (because 60 / 12 = 5). The time values are passed to ClockHand.direction at regular intervals in the program’s Update() method:

[code]
hours.direction = DateTime.Now.Hour * 5;
minutes.direction = DateTime.Now.Minute;
seconds.direction = DateTime.Now.Second;
[/code]

Figure 13.2 shows the output of the Rotate/Scale Animation Demo, but we can just call it the “Clock Demo” for short. This example does scale the clock-hand sprites during initialization, but the scale factor is not being actively used in this example. Given the ease with which rotation was modified in this example, I’m sure you will see how easy scaling would be as well. The source code is found in Listing 13.4.

LISTING 13.4 Source Code for the Clock Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game’’
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Random rand;
SpriteFont font;
List<Sprite> objects;
ClockHand hours, minutes, seconds;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
rand = new Random();
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
objects = new List<Sprite>();
Sprite clock = new Sprite(Content, spriteBatch);
clock.Load(“clock”);
clock.position = new Vector2(400, 240);
objects.Add(clock);
hours = new ClockHand(0);
minutes = new ClockHand(0);
seconds = new ClockHand(0); ’’
Sprite hourHand = new Sprite(Content, spriteBatch);
hourHand.Load(“arrow200”);
hourHand.position = new Vector2(400, 240);
hourHand.origin = new Vector2(30, 199);
hourHand.scaleV = new Vector2(1.5f, 0.7f);
hourHand.color = new Color(250, 50, 250);
hourHand.SetAnimation(hours);
objects.Add(hourHand);
Sprite minuteHand = new Sprite(Content, spriteBatch);
minuteHand.Load(“arrow200”);
minuteHand.position = new Vector2(400, 240);
minuteHand.origin = new Vector2(30, 199);
minuteHand.scaleV = new Vector2(1.0f, 0.9f);
minuteHand.color = new Color(250, 100, 150);
minuteHand.SetAnimation(minutes);
objects.Add(minuteHand);
Sprite secondHand = new Sprite(Content, spriteBatch);
secondHand.Load(“arrow200”);
secondHand.position = new Vector2(400, 240);
secondHand.origin = new Vector2(30, 199);
secondHand.scaleV = new Vector2(0.25f, 1.0f);
secondHand.color = new Color(120, 80, 150);
secondHand.SetAnimation(seconds);
objects.Add(secondHand);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
foreach (Sprite spr in objects)
{
spr.Rotate();
spr.Move();
}
hours.direction = DateTime.Now.Hour * 5;
minutes.direction = DateTime.Now.Minute;
seconds.direction = DateTime.Now.Second;
base.Update(gameTime);
}’’
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
foreach (Sprite spr in objects)
{
spr.Animate();
spr.Draw();
}
spriteBatch.DrawString(font, “Rotate/Scale Animation Demo”,
new Vector2(0, 0), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}’’
[/code]

The Clock Demo actually shows how to use an animation class that modifies rotation.
The Clock Demo actually shows how to use an animation class that modifies rotation.

Code Consistency

An interesting thing has happened since we started using the Animation class and all of its children. Have you noticed that much of the source code in our examples changes very little from one project to the next? Oh, there are often different variables in use, but LoadContent() seems to be where most of the code is being written at this point. Have you really noticed much of a change to Update() or Draw() in quite some time? This is a good sign! When standard code begins to remain unchanged, that means we have replaced the core logic of the program with a statedriven, property-based game engine. Sure, it’s a very, very simple game engine, but that is the path we are now on. The end result is some very solid code, easy to debug, easy to modify, easy to understand. Strive for this type of code in your own projects!

Combining Multiple Animations

We have quite a bit of good animation code now with the ability to add new effects fairly easily using the techniques learned in the preceding two chapters. The next step is quite revolutionary: doing more than one animation at a time! The current animation system will apply a behavior to a game sprite using a single method, Sprite.SetAnimation(). This is going to be replaced with a more powerful mechanism that will support many animations stored in a list.

Although it is technically possible to remove an animation from the list, we’re not to be concerned with seldom-used features like that. Our sprite animation system currently supports just one animation, but it can be replaced at any time with the SetAnimation() method just mentioned. The same will be true when multiple animation support is added, because the list container will have public scope.

Remember the 80/20 rule! Focus most of your efforts on writing code that will be used 80% of the time, not on features rarely used (unless you have time to kill).

Sprite Class Modifications

Adding Multiple Animation Support

We need to make a few changes to the Sprite class to support multiple animations.

  1. Remove the old Animation variable, p_animation, and replace it with a List:
    [code]
    //private Animation p_animation;
    public List<Animation> animations;
    [/code]
  2. Completely remove the SetAnimation() method, which is no longer used:
    [code]
    //public void SetAnimation(Animation animation)
    //{
    // p_animation = animation;
    //}
    [/code]
  3. Make the following changes to the Animate() method. This is the most dramatic change to the class required at this time, adding support for multiple animations. Now, when an animation has completed (by setting the animating property to false), it is actually removed!
    [code]
    public void Animate()
    {
    if (animations.Count == 0) return;
    foreach (Animation anim in animations)
    {
    if (anim.animating)
    {
    color = anim.ModifyColor(color);
    position = anim.ModifyPosition(position);
    rotation = anim.ModifyRotation(rotation);
    scaleV = anim.ModifyScale(scaleV);
    }
    else
    {
    animations.Remove(anim);
    return;
    }
    }
    }
    [/code]

New Animations

To support the upcoming example, here are a couple of new animations to examine. They are called Spin and Throb, affecting the rotation and scaling, respectively. First up is the Spin class, which does a 360-degree spin and then stops. Note that this class overrides only ModifyRotation(), but none of the other modification methods. The source code is found in Listing 13.5.

LISTING 13.5 Source Code for the Spin Class

[code]
public class Spin : Animation
{
private float angleDist, velocity;
public Spin(float velocity)
: base()
{
animating = true;
this.velocity = velocity;
angleDist = 0.0f;
}
public override float ModifyRotation(float original)
{
if (animating)
{
float fullCircle = (float)(2.0 * Math.PI);
angleDist += velocity;
if (angleDist > fullCircle)
animating = false;
original += velocity;
}
return original;
}
}t
[/code]

Next up is the Throb class, which performs a scaling “throb” of a sprite by cycling between a start and an end scale value. This class implements only ModifyScale(), because it does not need to touch any other property. The source code is found in Listing 13.6.

LISTING 13.6 Source Code for the Throb Class

[code]
public class Throb : Animation
{
public float startScale, endScale, speed;
private bool p_started;
public Throb(float startScale, float endScale, float speed)
: base()
{t
p_started = false;
animating = true;
this.startScale = startScale;
this.endScale = endScale;
this.speed = speed;
}
public override Vector2 ModifyScale(Vector2 original)
{
if (!animating) return original;
Vector2 modified = original;
if (!p_started)
{
modified.X = startScale;
modified.Y = startScale;
p_started = true;
}
modified.X += speed;
modified.Y += speed;
if (modified.X >= endScale)
speed *= -1;
else if (modified.X <= startScale)
animating = false;
return modified;
}
}t
[/code]

Multiple Animation Demo

With all of these enhancements, we can now do multiple animations per sprite. Remember, the behavior of the animations is entirely up to you, so if you don’t like how an animation is automatically removed when it is done, then change it! Figure 13.3 shows the sample program. The Throb animation is automatically renewed anytime the animations have all completed. Also, you can tap the screen to add a Spin animation. The fun thing about this demo is that you can tap the screen repeatedly to give the sprite a superfast spin! That is, you can do so until each Spin has finished its 360-degree rotation and is removed. Listing 13.7 has the source code.

Testing multiple animations with the Spin and Throb animations simultaneously.
FIGURE 13.3 Testing multiple animations with the Spin and Throb animations simultaneously.

LISTING 13.7 Source Code for the Multiple Animation Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
TouchLocation oldTouch;
Random rand;
SpriteFont font;
Sprite ship;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
oldTouch = new TouchLocation();
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
rand = new Random();
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>(“WascoSans”);
ship = new Sprite(Content, spriteBatch);
ship.Load(“ship”);
ship.position = new Vector2(400, 240);
ship.rotation = (float)rand.NextDouble();
ship.animations.Add(new Spin(0.1f));
ship.animations.Add(new Throb(0.5f, 3.0f, 0.2f));
}f’
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//get state of touch input
TouchCollection touchInput = TouchPanel.GetState();
if (touchInput.Count > 0)
{
TouchLocation touch = touchInput[0];
if (touch.State == TouchLocationState.Pressed &&
oldTouch.State == TouchLocationState.Released)
{
ship.animations.Add(new Spin(0.1f));
}
oldTouch = touch;
}
ship.Rotate();
ship.Move();
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
ship.Animate();
//reset throb
if (ship.animations.Count == 0)
ship.animations.Add(new Throb(0.5f, 3.0f, 0.2f));
ship.Draw();
int anims = ship.animations.Count;
spriteBatch.DrawString(font, “Tap screen to add animation…”,
new Vector2(0,0), Color.White);
spriteBatch.DrawString(font, “Animations:” + anims.ToString(),
new Vector2(600,0), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}f’
}
[/code]