More Sprite Transforms: Rotation and Scaling

0
241

Rotating a Sprite

XNA performs sprite rotation for us. That, in a nutshell, is the gist of this section on the subject. Digging into the trigonometry reveals that rotation calculations are in relation to the origin of the coordinate system at (0,0). It is possible to rotate a sprite the “hard way” by rotating each pixel of the sprite using the trigonometry functions sine() and cosine() the way they were used in the previous hour to rotate planets around the sun in the Solar System Demo. The same basic principle is used to rotate a sprite, but it is done very quickly in XNA thanks to a very fast SpriteBatch. Draw() method that can handle rotation like a cinch.

Since we have full control over how fast a sprite can rotate or revolve in a scene (such as the planets in the Solar System Demo), it is possible to use rotation with just partial circles to give a sprite complex movements by linking arclike movements together in interesting ways.

Rotation Angles

Not too many years ago, artists on a game project would prerotate all sprites in order to preserve quality. Back in the early days of game development, graphics hardware was very limited—which is why game programming in the “early years” was such a black art, because it required extensive knowledge of the hardware in order to eke out every possible CPU cycle to the fullest. Most games for Nintendo’s original NES fit on a 512Kb ROM cartridge. Compare that with games today, in which a single sprite animation stored as a texture might take up a few megabytes.

Figure 8.1 shows an example of the rotation frames often used in early games. The first direction is usually 0 degrees (north), and each successive frame is 45 degrees clockwise, as listed in Table 8.1.

A sprite pointing in the most common eight directions.
FIGURE 8.1 A sprite pointing in the most common eight directions.

 

Sprite Rotation Frame Directions and Angles

As the number of frames increases, so does the quality of the rotated animation, at which point we might end up with the 32 frames of rotation shown in Figure 8.2. Despite the large number of animation frames here in the form of a sprite sheet, this is not truly animation; it is just a prerendered rotation sequence. Each angle in the 32 frames of rotation represents 11 degrees of rotation. In this situation, compared to the 8-frame rotation, with 45 degrees per angle, you can see that there are four times as many frames for much higher quality.

A 32-frame prerotated sprite sheet.
FIGURE 8.2 A 32-frame prerotated sprite sheet.

Rotation actually takes place around the origin, and in trigonometric terms, we’re still working with the Cartesian coordinate system. Figure 8.3 shows an illustration of a point being rotated around the origin a certain interval.

Rotation takes place around the origin (0,0) in Cartesian coordinates.
FIGURE 8.3 Rotation takes place around the origin (0,0) in Cartesian coordinates.

Sprite Class Changes

We have been slowly adding new features to the Sprite class over the past two hours, and now the class will receive some much-needed rotation know-how. First, to differentiate between the old velocity and what we will need for rotational velocity, I have renamed the old velocity variable to velocityLinear, which is still a Vector2. A new variable has been added to the Sprite class called velocityAngular. A support method has been added, called Rotate(). Assuming that the velocityAngular property variable is set to a value other than zero, the Rotate() method will cause the sprite to rotate automatically (as the Move() method did for linear velocity in the preceding hour). Here is our new Sprite class:

LISTING 8.1 Source code for the even further improved Sprite class!

[code]
public class Sprite
{
private ContentManager p_content;
private SpriteBatch p_spriteBatch;
public Texture2D image;
public Vector2 position;
public Vector2 velocityLinear;
public Color color;
public float rotation;
public float velocityAngular;
public Sprite(ContentManager content, SpriteBatch spriteBatch)
{
p_content = content;
p_spriteBatch = spriteBatch;
image = null;
position = Vector2.Zero;
velocityLinear = Vector2.Zero;
color = Color.White;
rotation = 0.0f;
velocityAngular = 0.0f;
}
public bool Load(string assetName)
{
try
{
image = p_content.Load<Texture2D>(assetName);
}
catch (Exception) { return false; }
return true;
}
public void Draw()
{
p_spriteBatch.Draw(image, position, color);
}
public void Move()
{
position += velocityLinear;
}
public void Rotate()
{
rotation += velocityAngular;
if (rotation > Math.PI * 2)
rotation = 0.0f;
else if (rotation < 0.0f)
rotation = (float)Math.PI * 2;
}
}
[/code]

Drawing with Rotation

Besides the rotation code, some huge changes have had to be made to the Draw() method to accommodate rotation. We’re using the seventh and final overload of the SpriteBatch.Draw() method, which looks like this:

[code]
public void Draw( Texture2D texture,
Vector2 position,
Rectangle? sourceRectangle,
Color color,
float rotation,
Vector2 origin,
float scale,
SpriteEffects effects,
float layerDepth );
[/code]

There are nine parameters in this version of Draw(), which is capable of servicing all of our sprite needs for the next few chapters. But first things first: the rotation factor. The fifth parameter requires the rotation value as a float, so this is where we will pass the new rotation property (added to the preceding class).

For rotation to work correctly, we need to pass the right value for the origin parameter. The origin defines the point at which rotation will occur. If we pass Vector2.Zero, as in the sample call

[code]
Vector2 origin = new Vector2(0, 0);
float scale = 1.0f;
p_spriteBatch.Draw(image, position, null, color, rotation, origin,
scale, SpriteEffects.None, 0.0f);
[/code]

then the origin is set to the upper-left corner of the sprite (0,0), causing the result to look as shown in Figure 8.4, with the image rotation around the upper left.

The origin parameter defines the point at which rotation takes place.
FIGURE 8.4 The origin parameter defines the point at which rotation takes place.

We need to calculate the center of the image so that it will rotate correctly from the center of the image rather than the upper-left corner. This can be calculated easily enough using the width and height of the image:

[code]
Vector2 origin = new Vector2(image.Width/2,image.Height/2);
float scale = 1.0f;
p_spriteBatch.Draw(image, position, null, color, rotation,
origin, scale, SpriteEffects.None, 0.0f);
[/code]

This is the new and final code for Draw() at this point. We will also make minor changes to it again in the next section on scaling. The new version is shown in Figure 8.5.

Changing the origin to the center of the sprite’s image affects its position as well as rotation center—the origin becomes the focus for the position, even if the sprite is not rotated (with rotation set to 0.0f).

The sprite now rotates correctly, with the origin set to the center of the image.
FIGURE 8.5 The sprite now rotates correctly, with the origin set to the center of the image.

Sprite Rotation Demo

For this rotation demo, we will use user input to rotate the sprite either right or left, depending on where the screen is touched, on the right side or left side of the screen. The second screenshot of this demo is shown in Figure 8.5, referenced earlier. Now the sprite is centered on the screen and rotating from the center. Listing 8.2 contains the source code for the test program.

LISTING 8.2 Test program for the new rotation capability of our Sprite class.

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Viewport screen;
SpriteFont font;
Sprite ship;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
screen = GraphicsDevice.Viewport;
//create the font
font = Content.Load<SpriteFont>(“WascoSans”);
//create the ship sprite
ship = new Sprite(Content, spriteBatch);
ship.Load(“ship”);
ship.position = new Vector2(screen.Width/2, screen.Height/2);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//get state of touch inputs
TouchCollection touchInput = TouchPanel.GetState();
//look at all touch points (usually 1)
foreach (TouchLocation touch in touchInput)
{
if (touch.Position.X > screen.Width / 2)
ship.velocityAngular = 0.05f;
else if (touch.Position.X < screen.Width / 2)
ship.velocityAngular = -0.05f;
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
ship.Rotate();
ship.Draw();
string text = “Rotation: “ + ship.rotation.ToString(“N4”);
Vector2 size = font.MeasureString(text);
float x = (screen.Width – size.X) / 2;
spriteBatch.DrawString(font, text, new Vector2(x, 440), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]

Scaling a Sprite

Sprite scaling is also handled by SpriteBatch.Draw(), but it doesn’t hurt to learn a little bit about how scaling happens. Like rotation, scaling occurs in relation to the origin of the Cartesian coordinate system, used for many trigonometry functions, although the most common we use in game programming are sine() and cosine(). Figure 8.6 shows a before-and-after result of an object (represented with just two points, which might be a line). The points are scaled by a factor of 0.5 (50%), resulting in the new positions shown.

Scaling also occurs in relation to the origin (0,0).
FIGURE 8.6 Scaling also occurs in relation to the origin (0,0).

In our previous example on rotation, the scale factor was hard-coded in the Sprite.Draw() method like so:

[code]
public void Draw()
{
Vector2 origin = new Vector2(image.Width / 2, image.Height / 2);
float scale = 1.0f;
p_spriteBatch.Draw(image, position, null, color, rotation,
origin, scale, SpriteEffects.None, 0.0f);
}
[/code]

We need to delete this out of Draw() and add a new variable to the class’s public declaration, with a new initializer in the constructor.

[code]
private ContentManager p_content;
private SpriteBatch p_spriteBatch;
public Texture2D image;
public Vector2 position;
public Vector2 velocityLinear;
public Color color;
public float rotation;
public float velocityAngular;
public Vector2 scale;
public Sprite(ContentManager content, SpriteBatch spriteBatch)
{
p_content = content;
p_spriteBatch = spriteBatch;
image = null;
position = Vector2.Zero;
velocityLinear = Vector2.Zero;
color = Color.White;
rotation = 0.0f;
velocityAngular = 0.0f;
scale = new Vector2(1.0f);
}
[/code]

The scale variable is no longer just a mere float, but has been upgraded to a Vector2. This will allow us to scale the sprite horizontally and vertically at different rates, if desired. This can be annoying, however. Every time you need to change the scale value, a Vector2 has to be set. I would much rather just set the scale as a float by default, and have the option of setting the scale individually for the horizontal or vertical axes. This is not because setting a Vector2 is time-consuming, but because the scale will most often be uniform on both axes. We want the most often-used properties and methods to reflect the most common need, not unusual needs.

We need to rename the scale variable to scaleV, and add a new property called scale. After making the modification, scaleV is initialized in the constructor, which sets both the X and the Y values. Anytime Sprite.scale is changed, both the X and the Y properties are changed.

[code]
scaleV = new Vector2(1.0f);
[/code]

The new scale property looks like this:

[code]
public float scale
{
get { return scaleV.X; }
set {
scaleV.X = value;
scaleV.Y = value;
}
}
[/code]

In the Sprite Scaling Demo coming up shortly, we can scale the sprite very large or very small by simply modifying the Sprite.Draw() method to use Sprite.scaleV (which is a Vector2). It is also now possible to scale the width and height of a sprite. See Figure 8.7.

We’re not going to add a scale velocity because that is so rarely needed that if the need does arise, it can be done with a global variable outside of the Sprite class. Or you can go ahead and add that capability to your version of the Sprite class if you want! In fact, let’s see the complete new version of the Sprite class (in Listing 8.3), just to be thorough, since that was a lot of new information to sort out.

Scaling a sprite up to 5× the normal size.
FIGURE 8.7 Scaling a sprite up to 5× the normal size.

LISTING 8.3 Source code for the updated Sprite class with new scaling abilities.

[code]
public class Sprite
{
private ContentManager p_content;
private SpriteBatch p_spriteBatch;
public Texture2D image;
public Vector2 position;
public Vector2 velocityLinear;
public Color color;
public float rotation;
public float velocityAngular;
public Vector2 scaleV;
public Sprite(ContentManager content, SpriteBatch spriteBatch)
{
p_content = content;
p_spriteBatch = spriteBatch;
image = null;
position = Vector2.Zero;
velocityLinear = Vector2.Zero;
color = Color.White;
rotation = 0.0f;
velocityAngular = 0.0f;
scaleV = new Vector2(1.0f);
}
public float scale
{
get { return scaleV.X; }
set {
scaleV.X = value;
scaleV.Y = value;
}
}
public bool Load(string assetName)
{
try
{
image = p_content.Load<Texture2D>(assetName);
}
catch (Exception) { return false; }
return true;
}
public void Draw()
{
Vector2 origin = new Vector2(image.Width / 2, image.Height / 2);
p_spriteBatch.Draw(image, position, null, color, rotation,
origin, scaleV, SpriteEffects.None, 0.0f);
}
public void Move()
{
position += velocityLinear;
}
public void Rotate()
{
rotation += velocityAngular;
if (rotation > Math.PI * 2)
rotation = 0.0f;
else if (rotation < 0.0f)
rotation = (float)Math.PI * 2;
}
}
[/code]

The complete Sprite Scaling Demo is found in Listing 8.4. A view of the program with the scale set very small is shown in Figure 8.8.

Scaling a sprite down to 10% of normal size.
FIGURE 8.8 Scaling a sprite down to 10% of normal size.

LISTING 8.4 Source code for the updated Sprite class with new scaling abilities.

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Viewport screen;
SpriteFont font;
Sprite ship;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = “Content”;
TargetElapsedTime = TimeSpan.FromTicks(333333);
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
screen = GraphicsDevice.Viewport;
//create the font
font = Content.Load<SpriteFont>(“WascoSans”);
//create the ship sprite
ship = new Sprite(Content, spriteBatch);
ship.Load(“ship”);
ship.position = new Vector2(screen.Width / 2, screen.Height / 2);
//rotate the sprite to the right
ship.rotation = MathHelper.ToRadians(90.0f);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//get state of touch inputs
TouchCollection touchInput = TouchPanel.GetState();
foreach (TouchLocation touch in touchInput)
{
if (touch.Position.X > screen.Width / 2)
ship.scale += 0.05f;
else if (touch.Position.X < screen.Width / 2)
ship.scale -= 0.05f;
}
//keep the scaling within limits
if (ship.scale < 0.10f)
ship.scale = 0.10f;
else if (ship.scale > 5.0f)
ship.scale = 5.0f;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
ship.Rotate();
ship.Draw();
string text = “Scale: “ + ship.scale.ToString(“N4”);
Vector2 size = font.MeasureString(text);
float x = (screen.Width – size.X) / 2;
spriteBatch.DrawString(font, text, new Vector2(x, 440),
Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]

That concludes our study of the three basic transforms: translation, rotation, and scaling. These techniques, along with SpriteBatch and some good math formulas, can produce any type of special effect or animated movement that we need for a game, on small game sprites, or even whole backgrounds (give that a try!).