Advanced Linear and Angular Velocity

0
215

Calculating Angular Velocity

We have done a lot of work already with sprite transforms, so now it’s time to put some of these new features to the test in a real-world situation that often comes up in game projects. We’ll have a sprite move on the screen based on user input, and move in the direction it is facing. This requires some familiar trigonometry functions used in a creative way.

To begin, we need to understand the starting point for trigonometric calculations. The artwork in a game is often oriented with the “front” or “nose” pointing upward. But in our math calculations, that starting point will always be to the right, or 90 degrees clockwise from the up direction, as shown in Figure 9.1.

Trigonometry functions assume that angle 0 is right, not up.
FIGURE 9.1 Trigonometry functions assume that angle 0 is right, not up.

Math functions dealing with rotation angles and velocities always work with radians, not degrees. Using degrees in code is fine, but angles must be converted to radians during calculations. This can be done with MathHelper.ToDegrees() and MathHelper.ToRadians().

We use cosine() to calculate the X component, and sine() to calculate the Y component for the velocity of an object. In XNA, we can use the Math.Cos() and Math.Sin() methods to perform these calculations. The sole parameter passed to both of these methods is the angle that an object is facing or moving toward.

The angle will be any value from 0 to 360 degrees, including decimal values for partial degrees. When the calculations are made, the angle must be converted to radians. Suppose that the angle is 10 degrees. We convert this to radians with the following:

[code]
float radians = MathHelper.ToRadians( 10 );
// answer: radians = 0.174532925
[/code]

The angular velocity is calculated using this radian value, rounded to 0.1745 for our purposes (although the complete floating-point value is used with all decimal places in memory):

[code]
Velocity X = Cos( 0.1745 )
Velocity Y = Sin( 0.1745 )
[/code]

Figure 9.2 shows a circle with the angle and calculated values.

Calculating angular velocity.
FIGURE 9.2 Calculating angular velocity.

The results are X = 0.9848 and Y = 0.1736, as shown in the illustration. Consider the direction the arrow is facing in the illustration (10 degrees). The X and Y velocity values make sense, given that angle. Considering pixel movement on the screen, at this angle a sprite will move in the X axis much more than in the Y axis, a ratio of about five-and-a-half to one (5.5:1). So, when a sprite is moving across the screen at an angle of 10 degrees, it will move 5.5 pixels to the right (+X) for every 1 pixel down (+Y). If the angle were 180, for instance, the arrow would be pointing to the left, which would result in a negative X velocity.

Updating the Sprite Class

Some changes are needed for the Sprite class to work with the upcoming sample program. There are some new variables and some improvements to the Draw() and Rotate() methods. To make rotation more versatile, the origin variable (a float) has been moved out of Draw() and into the class’s public declarations so that it can be modified as a public variable. The Rotate() method has some improvements to make its boundary checking more accurate. The changes are included in Listing 9.1.

LISTING 9.1 Yet even more changes to the 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 Vector2 scaleV;
public Vector2 origin; //new
public bool alive; //new
public bool visible; //new
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);
origin = Vector2.Zero; //new
alive = true; //new
visible = true; //new
}
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);
origin = new Vector2(image.Width / 2, image.Height / 2); //new
}
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 -= (float)Math.PI * 2; //change
else if (rotation < 0.0f)
rotation = (float)Math.PI * 2 – rotation; //change
}
}
[/code]

There’s a great tutorial lesson on all the functions of trigonometry on Wikipedia here: http://en.wikipedia.org/wiki/Trigonometric_functions.

Apache Helicopter Demo

The example for this section is included in the hour resource files, so you may open the project while studying the code in Listing 9.2. This demo draws a small sprite of an Apache helicopter firing bullets at whatever angle it is facing. Touching the top of the screen will cause the chopper’s nose to rotate upward. Likewise, touching the bottom of the screen will rotate the chopper’s nose downward. Touching the center of the screen will cause the chopper to fire its bullets in its current facing angle. Figure 9.3 shows the program running, and it looks almost like the start of a game! It could be, with a little work! As we continue to improve the Sprite class, the source code for our example programs continue to shrink!

The Apache helicopter demo.
FIGURE 9.3 The Apache helicopter demo.

LISTING 9.2 Source code to the Apache helicopter demo utilizing the improved Sprite class.

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Viewport screen;
SpriteFont font;
Sprite chopper;
Texture2D bullet;
Sprite[] bullets;
float rotation;
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 helicopter sprite
chopper = new Sprite(Content, spriteBatch);
chopper.Load(“apache”);
chopper.position = new Vector2(120, 240);
chopper.origin = new Vector2(100, 22);
//load bullet image
bullet = Content.Load<Texture2D>(“bullet”);
//create bullet sprites
bullets = new Sprite[10];
for (int n = 0; n < 10; n++)
{
bullets[n] = new Sprite(Content, spriteBatch);
bullets[n].image = bullet;
bullets[n].alive = false;
}
}
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();
//get rotation
rotation = MathHelper.ToDegrees(chopper.rotation);
//look at all touch points
foreach (TouchLocation touch in touchInput)
{
if (touch.Position.Y < 180) //top of screen
rotation -= 1.0f;
else if (touch.Position.Y > 300) //bottom
rotation += 1.0f;
else
Fire(); //middle
}
//keep rotation in bounds
if (rotation < 0.0f)
rotation = 360.0f – rotation;
else if (rotation > 360.0f)
rotation = 360.0f – rotation;
//save rotation
chopper.rotation = MathHelper.ToRadians(rotation);
//move the bullets
for (int n = 0; n < 10; n++)
{
if (bullets[n].alive)
{
bullets[n].Move();
if (bullets[n].position.X > 800)
bullets[n].alive = false;
}
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
//draw the chopper
chopper.Draw();
//draw the bullets
for (int n = 0; n < 10; n++)
{
if (bullets[n].alive)
bullets[n].Draw();
}
string text = “Angle: “ + rotation.ToString(“N4”);
spriteBatch.DrawString(font, text, new Vector2(200, 440),
Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
void Fire()
{
//look for an unused bullet
for (int n = 0; n < 10; n++)
{
if (!bullets[n].alive)
{
bullets[n].alive = true;
bullets[n].position = chopper.position;
bullets[n].rotation = chopper.rotation;
//calculate angular velocity
float x = (float)Math.Cos(bullets[n].rotation) * 10.0f;
float y = (float)Math.Sin(bullets[n].rotation) * 10.0f;
bullets[n].velocityLinear = new Vector2(x,y);
break;
}
}
}
}
[/code]

“Pointing” a Sprite in the Direction of Movement

You might be thinking, Didn’t we just do this? That’s an astute question! In fact, in the preceding example, the bullet sprites were pointed in a certain direction, and we just added the angular velocity code to make them move in that direction. Now we’re going to do the reverse: Given the direction a sprite is already moving, we want it to “point” in that direction so that it looks right. To demonstrate, we’ll cause the spaceship sprite to “orbit” around a planet and rotate while moving. To more accurately describe this situation, we want a sprite to move and point toward a target.

The trigonometry (“circular”) functions we’ve been using can be considered elementary physics. In a sense, then, we’re working on our own simple physics engine here, which is a bit of a stretch but still compelling!

Have you ever played an RTS (real-time strategy) game in which you can select units, then right-click somewhere on the map, and they would move toward that location? That is the basic way most RTS games work. Along the path, if your units encounter enemy units, they will usually fight or shoot at the enemy, unless you tell them to target a specific enemy unit with a similar right-click on it.

Well, we can do something like that with the concept covered here. Oh, there’s a lot more involved in an RTS game than just moving toward a destination, but at the core of the game is code similar to what we’re going to learn about here.

Calculating the Angle to Target

In the preceding section, we used Math.Cos() and Math.Sin() to calculate the respective X and Y components of velocity (a Vector2). These values could then be used to move a sprite in any desired direction. There’s a related calculation we can perform to do the opposite: Given a sprite’s current location, and a target location, we can calculate the angle needed to get there.

We won’t be using sine and cosine to calculate the angle. Those trig functions are useful only if you know the angle already. The reverse is, knowing where we are already headed, what is that angle?

This concept is powerful in terms of gameplay! Let’s say you do know the angle and use it to calculate velocity, then send a bullet on its way, as we did in the preceding example. Okay, that’s great. But what if you wanted to slow down that sprite, make it stop, and even begin moving in reverse? Not in terms of a bullet, but any sprite, like a spaceship or a car? We can do these things.

This code could be used to cause one sprite to continually point at another sprite. Instead of using a target screen coordinate, use the location of a moving target sprite instead!

The secret behind all this advanced velocity code is another trig function called arctangent. Arctangent is an inverse trigonometric function—specifically, the inverse of tangent, which itself is opposite over adjacent (side a divided by side b), as shown in Figure 9.4. Since I don’t want to get into deriving these trig functions, let’s just jump to the function name in XNA. There are two versions: Math.Atan(), which takes one parameter, and Math.Atan2(), which takes two parameters (double y, double x). This math function returns the angle whose tangent is the quotient of two specified numbers.

A right triangle is the basis for trigonometry. Illustration courtesy of Wikipedia.
FIGURE 9.4 A right triangle is the basis for trigonometry. Illustration courtesy of Wikipedia.

We can’t just pass the position (X and Y) of a target screen coordinate to this function, because the two parameters are actually delta values (the difference between the X and Y values of two points). That’s not as bad as it sounds, as any math major will tell you. Delta is just the difference between the two points: X2 – X1 and Y2 – Y1.

[code]
double deltaX = x2 – x1;
double deltaY = y2 – y1;
[/code]

Having these variables ready, we can calculate the angle toward a target with arctangent in XNA like so:

[code]
double angle = Math.Atan2(deltaY,deltaX);
[/code]

Shuttle Orbit Demo

So now we know how to calculate the angle to a target location. What now? Let’s put this new knowledge to use in another sample program. This one will simulate a spaceship orbiting a planet. While it’s rotating around the planet, the nose of the ship will rotate so it is oriented in the direction of movement. This isn’t exactly moving toward a target on the screen, but given the ship’s previous position and the current position, we can figure out what in direction the nose should be pointing in reverse. Figure 9.5 shows the program running, showing the space shuttle not just rotating around a planet, but rotating to keep the nose pointing forward. This isn’t exactly realistic, because while in orbit, it doesn’t matter which direction a ship or satellite is facing—it will continue to orbit the planet. But it looks cool this way in a game, especially to a younger audience. Listing 9.3 contains the source code for the program.

The spaceship points toward its path while rotating around the planet.
FIGURE 9.5 The spaceship points toward its path while rotating around the planet.

LISTING 9.3 Source code for the orbiting spaceship program.

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Viewport screen;
SpriteFont font;
Sprite shuttle, planet;
float orbitRadius, orbitAngle;
Vector2 oldPos;
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 planet sprite
planet = new Sprite(Content, spriteBatch);
planet.Load(“planet1”);
planet.scale = 0.5f;
planet.position = new Vector2(400, 240);
//create the ship sprite
shuttle = new Sprite(Content, spriteBatch);
shuttle.Load(“shuttle”);
shuttle.scale = 0.2f;
orbitRadius = 200.0f;
orbitAngle = 0.0f;
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//remember position for orientation
oldPos = shuttle.position;
//keep angle within 0-360 degs
orbitAngle += 1.0f;
if (orbitAngle > 360.0f)
orbitAngle -= 360.0f;
//calculate shuttle position
float x = 400 + (float)Math.Cos(MathHelper.ToRadians(orbitAngle))
* orbitRadius;
float y = 240 + (float)Math.Sin(MathHelper.ToRadians(orbitAngle))
* orbitRadius;
//move the position
shuttle.position = new Vector2(x, y);
//point shuttle’s nose in the right direction
float angle = TargetAngle(shuttle.position, oldPos);
//subtract 180 degrees to reverse the direction
angle = MathHelper.WrapAngle(angle – MathHelper.ToRadians(180));
//adjust for artwork pointing up
angle += MathHelper.ToRadians(90);
//update shuttle’s rotation
shuttle.rotation = angle;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
planet.Draw();
shuttle.Draw();
string text = “Angle: “ + shuttle.rotation.ToString(“N4”);
spriteBatch.DrawString(font, text, new Vector2(200, 440),
Color.White);
text = “Radius: “ + orbitRadius.ToString(“N0”);
spriteBatch.DrawString(font, text, new Vector2(450, 440),
Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
float TargetAngle(Vector2 p1, Vector2 p2)
{
return TargetAngle(p1.X, p1.Y, p2.X, p2.Y);
}
float TargetAngle(double x1,double y1,double x2,double y2)
{
double deltaX = (x2-x1);
double deltaY = (y2-y1);
return (float)Math.Atan2(deltaY,deltaX);
}
}
[/code]