Transforming Frame Animations

0
270

Drawing Frames with Color Mods

We will first test the frame animation system with color modifications, then get into transform mods, and then create a new frame animation mod system as well.

Pivot Optimization

The sprite’s pivot point used for rotation, called origin, is a potentially serious source of difficulty and head scratching. When recurring events or calculations can be automated, they should be. Anytime the sprite frame size is changed, the origin should be modified along with it. This calls for either a method or a property. Presently, both size and origin are defined as public Vector2 variables in Sprite:

[code]
public Vector2 size;
public Vector2 origin;
[/code]

We want to make a change so that it doesn’t wreck any previous code that relies on the Sprite class, although those prior examples could just use an earlier version of the class if necessary. Perhaps the easiest way to do this is with a property, with size made private. This will require several changes in the Sprite class, as size is used quite a bit.

Sprite Class Modifications to Support, Well, Modifications

We will make changes to the Sprite class from the preceding chapter to support color and transform animation of a sprite with frame animation support.

  1. First, we’ll make a variable change in Sprite:
    [code]
    //public Vector2 size;
    private Vector2 p_size;
    [/code]
  2. Next, we’ll take care of initialization in the constructor:
    [code]
    //size.X = size.Y = 0;
    size = Vector2.Zero;
    [/code]
  3. Now we’ll work on the new property, with the changes made to the pivot/origin being done automatically:
    [code]
    public Vector2 size
    {
    get { return p_size; }
    set
    {
    p_size = value;
    origin = new Vector2(p_size.X / 2, p_size.Y / 2);
    }
    }
    [/code]
  4. One of the additional locations in the class where size is used is in Load(), which must be modified as follows. Note that with the property now being used, the new line of code causes origin to be changed as well! Now we’re already starting to see the benefit of this property right inside the Sprite class, and it will be equally helpful outside. Now, we can still manually change the origin whenever needed (for instance, recall the clock example from Hour 13, which needed to set the origin for the clock hands manually for it to rotate correctly). Just remember that changing size automatically changes the pivot point (origin).
    [code]
    public bool Load(string assetName)
    {
    try
    {
    image = p_content.Load<Texture2D>(assetName);
    origin = new Vector2(image.Width / 2, image.Height / 2);
    }
    catch (Exception) { return false; }
    //size.X = image.Width;
    //size.Y = image.Height;
    size = new Vector2(image.Width, image.Height);
    return true;
    }
    [/code]

Revisiting CycleColorBounce

The CycleColor class was introduced back during Hour 12 as a way to fade any color in a desired direction, going to fully red, or full whiteout, or fade to black, or any combination in between. A subclass was then developed, called CycleColorBounce, that could do the same color transforms, but would not automatically stop animating when the limits were reached. Instead, it would continue to cycle. We also considered a class called SolidColor that did no real color changes, but that would be helpful in some cases when just a solid color is needed without changing the base Sprite.color property manually. In other words, without creating an additional “old color” style property in the class, SolidColor allowed a sprite to change color temporarily and then return.

Despite the relative simplicity of color modification in principle, it turns out that— ironically—our color mod classes are the most complex. Here again in Listing 15.1 are the Animation subclasses for reference, since we are using them in the example.

LISTING 15.1 Review of CycleColor Class for Reference (Not New Code)

[code]
public class CycleColor : Animation
{
public int red, green, blue, alpha;
public CycleColor(int red, int green, int blue, int alpha)
: base()
{
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
animating = true;
}
public override Color ModifyColor(Color original)
{
Color modified = original;
if (animating)
{
int R = original.R + red;
int G = original.G + green;
int B = original.B + blue;
int A = original.A + alpha;
if (R < 0 || R > 255 || G < 0 || G > 255 ||
B < 0 || B > 255 || A < 0 || A > 255)
{
animating = false;
}
modified = new Color(R, G, B, A);
}
return modified;
}
}
public class CycleColorBounce : CycleColor
{
public int rmin, rmax, gmin, gmax, bmin, bmax, amin, amax;
public CycleColorBounce(int red, int green, int blue, int alpha)
: base(red,green,blue,alpha)
{
rmin = gmin = bmin = amin = 0;
rmax = gmax = bmax = amax = 255;
}
public override Color ModifyColor(Color original)
{
Color modified = original;
if (animating)
{
int R = original.R + red;
int G = original.G + green;
int B = original.B + blue;
int A = original.A + alpha;
if (R < rmin)
{
R = rmin;
red *= -1;
}
else if (R > rmax)
{
R = rmax;
red *= -1;
}
if (G < gmin)
{
G = gmin;
green *= -1;
}
else if (G > gmax)
{
G = gmax;
green *= -1;
}
if (B < bmin)
{
B = bmin;
blue *= -1;
}
else if (B > bmax)
{
B = bmax;
blue *= -1;
}
if (A < amin)
{
A = amin;
alpha *= -1;
}
else if (A > amax)
{
A = amax;
alpha *= -1;
}
modified = new Color(R, G, B, A);
}
return modified;
}
}
[/code]

Listing 15.2 shows some source code showing how we will test color modification in the upcoming example; we will delay going into the example until the next section, which features a combined example.

LISTING 15.2 Sample Source Code Demonstrating the Latest Sprite Changes

[code]
//create skeleton sprite
skelly = new Sprite(Content, spriteBatch);
skelly.Load(“skeleton_attack”);
skelly.position = new Vector2(425, 240);
skelly.size = new Vector2(96,96);
skelly.scale = 2.0f;
skelly.columns = 10;
skelly.totalFrames = 80;
skelly.animations.Add(new CycleColorBounce(1, 2, 2, 0));
objects.Add(skelly);
[/code]

We haven’t tried to do anything this complex yet. The question is, will it work—will color and frame animations work correctly “out of the box” or will changes be needed?

The particular sprite sheet used for the color mod example is shown in Figure 15.1. As you can see, it is an animated skeleton with a sword (courtesy of Reiner Prokein via his website, http://www.reinerstilesets.de). We will see this awesome skeleton sprite in the upcoming example.

Animated skeleton warrior used as an example with color modification.
FIGURE 15.1 Animated skeleton warrior used as an example with color modification.

Drawing Frames with Transform Mods

Now we will test the frame animation system with transform modifications. Color modification is rather trivial compared to transform modifications, which involve gradually affecting movement, rotation, or scaling over time, very much in parallel with how frame animation changes the image over time. When we’re dealing with transforms, though, the image size and position are crucial for these mods to work correctly. If an image is loaded in from a bitmap file that has dimensions of 1024×1024, and then transforms are applied to the sprite based on this image size, most likely the sprite will not even show up on the WP7 screen, because frame animation is also being done, and the image is being thrown out of the range of the screen, in all likelihood. So, the initial setup of the sprite’s properties is essential to make transformed frame animation work correctly.

Figure 15.2 shows the sprite sheet for the animated swordsman also featured in the example (also courtesy of Reiner Prokein, http://www.reinerstilesets.de).

Animated swordsman used to demo two combined animations.
FIGURE 15.2 Animated swordsman used to demo two combined animations.

Custom Frame Animation

We have been using the simple version of the Sprite.Animate() method so far, passing just the time value to cause the animation to run at a consistent frame rate. But there is a second version of Sprite.Animate() that is called from this first one, giving us the ability to fine-tune or manually intervene in the way the animation runs. Here are the two methods again for reference (covered in the preceding hour):

[code]
public void Animate(double elapsedTime)
{
Animate(0, totalFrames-1, elapsedTime, 30);
}
public void Animate(int startFrame, int endFrame,
double elapsedTime, double speed)
{
if (totalFrames <= 1) return;
startTime += elapsedTime;
if (startTime > speed)
{
startTime = 0;
if (++frame > endFrame) frame = startFrame;
}
}
[/code]

The second method allows us to pass the start frame and end frame for a specific range of frames to be animated. By default, the first Animate() method just draws all frames (0 to totalFrames-1). This is a simple looped animation: When the last frame is reached, the animation system loops back around to the first frame again. But there are more advanced forms of frame animation that can be performed as well, such as a frame bounce rather than a loop: When the last frame is reached, the animation direction reverses downward toward the first frame again, and then reverses again in the positive direction. This can be actually quite useful if the artwork has only half of the total frames needed to create a complete loop, and instead the bounce technique must be used.

Now we come to the dilemma: The game loop does not have room for custom, manual method calls! Here is the code in Update() that updates the sprite list:

[code]
//update all objects
foreach (Sprite spr in objects)
{
spr.Rotate();
spr.Move();
}
And here is the code in Draw() that animates and draws all the sprites:
foreach (Sprite spr in objects)
{
spr.Animate(); //mods
spr.Animate(gameTime.ElapsedGameTime.Milliseconds); //frames
spr.Draw();
}
[/code]

It is true that we can jump into these foreach loops and make manual changes to a specific sprite in the list. Or we can just leave a sprite out of the list and handle it separately. That works and there’s nothing wrong with that approach. But it kind of defeats the benefits of having an automated game loop, in which sprites are updated and drawn automatically based on properties. This describes a data-driven game loop, and it is simply unmatched in versatility and power when it comes to building a complex game. In other words, we want this to work correctly; we really don’t want to bypass it. If a new feature is needed, it’s better to work that into the loop than to make exceptions.

What we need is a new modification method in the Animation class. Do you see where we’re going with this?

Updating the Animation Class

The Sprite class will retain the two new Animate() methods from the preceding hour, and that form of frame animation can be used when it will suffice. But we will now add a more versatile frame animation system that uses the Animation class, fully merging the functionality of color and transform animation with frame animation. This calls for some changes to the Animation class, but no changes to the Sprite class are needed because it already supports multiple animations.

Add the following new method called ModifyFrame() to the Animation class as shown:

[code]
public class Animation
{
. . . some portions skipped
public virtual int ModifyFrame(int current)
{
return current;
}
}
[/code]

New Modifications to the Sprite Class

To make the new frame animation work with Sprite, some things must be modified.

  1. A new variable, frameDir, must be added to the class:
    [code]
    public int columns, frame, totalFrames, frameDir;
    [/code]
  2. It is initialized in the constructor:
    [code]
    columns = 1;
    frame = 0;
    totalFrames = 1;
    frameDir = 1;
    [/code]
  3. The Animate() method can be updated like so (note the line in bold). Remember, this call to ModifyFrame() is the default or base method call, and the current frame is passed as the sole parameter. Any class that inherits from Animate and overrides ModifyFrame() will have the animation range set to specific values.
    [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);
    frame = anim.ModifyFrame(frame);
    }
    else
    {
    animations.Remove(anim);
    return;
    }
    }
    }
    [/code]

New Animation Subclass Examples

Strangely enough, that’s it! That’s all we had to do to add frame animation support to the Animation class. Now we can create custom subclasses that inherit from Animation to create any kind of special frame animation we need. Listing 15.3 gives two sample animations involving frame modifications that you can study as a reference.

LISTING 15.3 New Animations

[code]

public class FrameLoop : Animation
{
public FrameLoop(int start, int end, int current, int direction)
: base()
{
animating = true;
}
public override int ModifyFrame(int start, int end, int current,
int direction)
{
current += direction;
if (current > end)
current = start;
else if (current < start)
current = end;
return current;
}
}
public class FrameBounce : Animation
{
public FrameBounce(int start, int end, int current, int direction)
: base()
{
animating = true;
}
public override int ModifyFrame(int start, int end, int current,
int direction)
{
current += direction;
if (current > end)
{
direction *= -1;
current = end;
}
else if (current < start)
{
direction *= -1;
current = start;
}
return current;
}
}
[/code]

If you want frame animation to continue to function as it did before, one option is to automatically add a FrameLoop object to the animation list when a sprite is created (from the Sprite constructor). But it’s probably better to just work with the more advanced system now in place.

The FrameLoop class duplicates the existing functionality of parameterized Sprite.Animate(), which updates frame animation. Those two methods could now be removed since FrameLoop replicates that functionality, but I will just leave them alone since they might be useful. Next, the FrameBounce class handles the situation in which an animation will go from start frame to end frame, and then reverse direction down toward start frame again, rather than looping.

Figure 15.3 shows an animated dragon that will also be used in the example.

 Animated dragon used to demo OrbitalMovement and FrameBounce.
FIGURE 15.3 Animated dragon used to demo OrbitalMovement and FrameBounce.

As of the preceding hour, we had an updated Draw() that handles sprite frame animation like so:

[code]
foreach (Sprite spr in objects)
{
spr.Animate(); //mods
spr.Animate(gameTime.ElapsedGameTime.Milliseconds); //frames
spr.Draw();
}
[/code]

The second line in the method, the second call to Animate(), can now be removed, after FrameLoop or FrameBounce has been added as an animation. But if you do remove that line, you must be sure to add FrameLoop or FrameBounce (or some variation thereof) to the animation list after a sprite has been created. For instance:

[code]
//create dragon sprite
dragon = new Sprite(Content, spriteBatch);
dragon.Load(“dragon”);
dragon.animations.Add(new FrameLoop(48, 56, 48, 1));
[/code]

Animation Mods Demo

We need a complete example to demonstrate how the new improvements to the animation system work. Figure 15.4 shows the final output of the demo for this hour. On the left is the swordsman character animated with FrameLoop and ThrobBounce.

The final Animation Mods Demo program animates three sprites with several animations at once.
FIGURE 15.4 The final Animation Mods Demo program animates three sprites with several animations at once.

The New ThrobBounce Class

The ThrobBounce class inherits from Throb and perpetuates the “throb” animation by overriding the animating property (making it continue when the animation normally ends). For reference, the original Throb class is shown here in Listing 15.4 as well since it is inherited.

LISTING 15.4 New ThrobBounce Class (Throb Repeated for Reference)

[code]
public class Throb : Animation
{
public float startScale, endScale, speed;
private bool p_started;
public Throb(float startScale, float endScale, float speed)
: base()
{
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;
}
}
public class ThrobBounce : Throb
{
public ThrobBounce(float startScale, float endScale, float speed)
: base(startScale, endScale, speed)
{
}
public override Vector2 ModifyScale(Vector2 original)
{
//keep it going
Vector2 scale = base.ModifyScale(original);
if (!animating)
{
animating = true;
speed *= -1;
}
return scale;
}
}
[/code]

The Animation Mods Demo Source Code

Here in Listing 15.5 is the complete source code for the Animation Mods Demo that wraps up all the techniques of this hour in one short example.

LISTING 15.5 Source Code for the Animation Mods Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
TouchLocation oldTouch;
Random rand;
SpriteFont font;
List<Sprite> objects;
Sprite swordsman, skelly, dragon;
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”);
//create object list
objects = new List<Sprite>();
//create swordsman sprite
swordsman = new Sprite(Content, spriteBatch);
swordsman.Load(“swordsman_walking”);
swordsman.position = new Vector2(150, 240);
swordsman.size = new Vector2(96, 96);
swordsman.columns = 8;
swordsman.totalFrames = 64;
swordsman.animations.Add(new FrameLoop(0, 63, 1));
swordsman.animations.Add(new ThrobBounce(0.5f, 4.0f, 0.1f));
objects.Add(swordsman);
//create skeleton sprite
skelly = new Sprite(Content, spriteBatch);
skelly.Load(“skeleton_attack”);
skelly.position = new Vector2(425, 240);
skelly.size = new Vector2(96,96);
skelly.scale = 2.0f;
skelly.columns = 10;
skelly.totalFrames = 80;
skelly.animations.Add(new FrameLoop(0, 79, 1));
skelly.animations.Add(new CycleColorBounce(1, 2, 2, 0));
objects.Add(skelly);
//create dragon sprite
dragon = new Sprite(Content, spriteBatch);
dragon.Load(“dragon”);
dragon.position = new Vector2(700, 240);
dragon.size = new Vector2(128, 128);
dragon.scale = 1.5f;
dragon.columns = 8;
dragon.totalFrames = 64;
dragon.animations.Add(new FrameBounce(48, 55, 1));
dragon.animations.Add(new OrbitalMovement(
dragon.position, 50, 0, 0.1f));
objects.Add(dragon);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//update all objects
foreach (Sprite spr in objects)
{
spr.Rotate();
spr.Move();
}
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, ““, Vector2.Zero, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]

The sprite animation system is now completely functional, with lots of room for growth and with support for custom Animation subclasses. In a professional game, these subclasses would be programmed by designers with Lua script code, allowing designers to make their own custom animations, and the job of the programmers essentially being done at this point with regard to the Animation and Sprite classes. But we aren’t getting into scripting, so feel free to create your own awesome new animation classes to see what interesting effects you can come up with! In the following hour, we will study one last subject related to sprites: z-index ordering. This will allow our sprites to draw over each other with some having higher priority than others.