Sprite Frame Animation

Drawing Animation Frames

Let’s dig into the nitty-gritty of sprite animation. Most beginners create an animation sequence by loading each frame from a separate bitmap file and storing the frames in an array or a list. This approach has the distinct disadvantage of requiring many files for even a basic animation for a walking character or some other game object, which often involves 50 to 100 or more frames. Although it can be done, that is a slow and error-prone way to handle animation. Instead, it is preferable to use an animation sheet.

Preparing the Animation

A sprite animation sheet is a single bitmap containing many frames arranged in rows and columns, as shown in Figure 14.1. In this sprite sheet (of an animated asteroid), there are eight columns across and 64 frames overall. The animation here was rendered from a 3D model, which is why it looks so great when animated on the screen! One question that might come up is, where do you get animations? Most game art is created by a professional artist specifically for one game, and then it is never used again (usually because the game studio owns the assets). There are, however, several good sources of free artwork online, such as Reiner’s Tilesets (http:// www.reinerstilesets.de).

Sprite sheet of animation frames for an asteroid.
FIGURE 14.1 Sprite sheet of animation frames for an asteroid.

SpriteBatch doesn’t care whether your sprite’s source image uses a color key or an alpha channel for transparency; it just renders the image. If you have an image with an alpha channel, like a TGA or PNG, then it will be rendered with any existing alpha, with translucent blending of the background. This is the technique used to render a sprite with transparency in XNA. Looking at sprite functionality at a lower level, you can tell the sprite renderer (SpriteBatch) what color you want to use when drawing the image, which was the focus of the preceding two hours on performing color and transform animations.

The bitmap file should have an alpha channel if you want to use transparency (which is almost always the case). Most artists prefer to define their own translucent pixels for best results rather than leaving it to chance in the hands of a programmer. The main reason to use alpha rather than color-key transparency is better quality. An alpha channel can define pixels with shades of translucency. In contrast, a color key is an all-or-nothing, on/off setting with a solid edge, because such an image will have discrete pixels. You can do alpha blending at runtime to produce special effects (such as a particle emitter), but for maximum quality, it’s best to prepare artwork in advance. See Figure 14.2.

A sprite animation sheet of an explosion showing the alpha channel.
FIGURE 14.2 A sprite animation sheet of an explosion showing the alpha channel.

Rather than using a black border around a color-keyed sprite (the old-school way of highlighting a sprite), an artist will usually blend a border around a sprite’s edges using an alpha level for partial translucency. To do that, you must use a file format that supports 32-bit RGBA images. TGA and PNG files both support an alpha channel and XNA supports them. The PNG format is a good choice that you may consider using most of the time because it has wide support among all graphic editing tools.

Calculating Frame Position

Assuming that we have an animation sheet like the asteroid animation shown in Figure 14.2, we can begin exploring ways to draw a single frame. This will require some changes to the Sprite class, which currently just uses the dimensions of the loaded image. That will have to change to reflect the dimensions of a single frame, not the whole image. To calculate the position of a frame in the sheet, we have to know the width and height of a single frame. Then, beginning at the upper left, we can calculate how to move right and down the correct number of frames. Figure 14.3 shows a typical sheet with the columns and rows labeled for easy reference.

This animation sheet has eight columns and eight rows, for 64 total frames.
FIGURE 14.3 This animation sheet has eight columns and eight rows, for 64 total frames.

The more important of the two is the row calculation, so we’ll do that one first. To make this calculation, you need to know how many frames there are across from left to right. These are the columns. (See Figure 14.4.) Here is the formula for calculating the row or Y position of a frame number on the sprite sheet:

[code]
Y = ( Frame_Number / Columns ) * Frame_Height
[/code]

To calculate the column or X position of a frame number on the sprite sheet, a similar- looking calculation is done, but the result is quite different:

[code]
X = ( Frame_Number % Columns ) * Frame_Width
[/code]

Calculating the position of a frame in the animation sheet.
FIGURE 14.4 Calculating the position of a frame in the animation sheet.

Note that the math operator is not division. The percent symbol (%) is the modulus operator in C#. Modulus is similar to division, but instead of returning the quotient (or answer), it returns the remainder! Why do we care about the remainder? That represents the X position of the frame! Here’s the answer: because X is the extra or leftover amount after the division. Recall that the formula for calculating Y gave us a distinct integer quotient. We want to use the same variables, but modulus rather than division gives us the partial column in the row, which represents the X value.

Drawing One Frame

Equipping ourselves with these formulas, we can write the code to draw a frame from a sprite sheet onto the screen. First, we’ll create a Rectangle to represent the source frame:

[code]
Rectangle source = new Rectangle();
source.X = (frame % columns) * width;
source.Y = (frame / columns) * height;
source.Width = width;
source.Height = height;
[/code]

Next, we’ll use the Rectangle when calling SpriteBatch.Draw(), using one of the overloads of the method that allows use of a source rectangle. We can retain the existing rotation, origin, and scale parameters while still drawing just a single frame.

[code]
spriteBatch.Draw( image, //source Texture2D
position, //destination position
source, //source Rectangle
Color.White, //target color
rotation, //rotation value
origin, //pivot for rotation
scale, //scale factor
SpriteEffects.None, //flip or mirror effects
0.0f ); //z-index order
[/code]

Creating the Frame Animation Demo

The only way to really get experience with animation is to practice by writing code. One fairly common mistake that results in an animated sprite not showing up is to forget the frame size property. This must be set after a bitmap file is loaded or the image property is set to an outside Texture2D object. The Sprite.size property is a Vector2 that must be set to the width and height of a single frame. Forgetting to do this after loading the bitmap will result in the animation not showing up correctly.

Sprite Class Changes

Some rather dramatic changes must be made to the Sprite class to support frame animation. Now, the reason for most of the changes involves the sprite sheet image. Previously, the whole image was used, for drawing, for calculating scale, and so on. Now, that code has to be changed to account for the size of just one frame, not the whole image.

Modifying the Sprite Class

  1. First up, we have some new variables in the Sprite class. These can be added to the top of the class with the other variables:
    [code]
    private double startTime;
    public Vector2 size;
    public int columns, frame, totalFrames;
    [/code]
  2. In the Sprite constructor, the new variables are initialized after all the others. The columns and totalFrames variables are crucial to drawing simple sprites when no animation is being used. In other words, they’re needed to preserve compatibility with code that used the Sprite class before this point. By setting columns to 1, we tell the Draw() method to treat the image as if there is just one column. Likewise, setting totalFrames to 1 ensures that just that one frame is drawn, even if no animation is used. A flag will be used in the Draw() method just to make sure null errors don’t occur, but these initialized values should take care of that as well.
    [code]
    size.X = size.Y = 0;
    columns = 1;
    frame = 0;
    totalFrames = 1;
    startTime = 0;
    [/code]
  3. Next up are two helper properties that make using Sprite a bit easier, by exposing the X and Y properties of position. This is a convenience rather than a required change, but it is very helpful in the long term.
    [code]
    public float X
    {
    get
    {
    return position.X;
    }
    set
    {
    position.X = value;
    }
    }
    public float Y
    {
    get
    {
    return position.Y;
    }
    set
    {
    position.Y = value;
    }
    }
    [/code]
  4. Next, we need to review the Load() method again for reference, just to note what is being initialized at this point. Pay special attention to origin and size, because they are involved in a single frame being drawn correctly. Notice that origin is initialized with the full size of the image. When using a sprite sheet, origin must be reset after the image is loaded for drawing to work correctly!
    [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;
    return true;
    }
    [/code]
  5. Next, make the required changes to the Sprite.Draw() method. Quite a dramatic change has come over Draw(), transforming it into a fully featured animation rendering routine with support for single images or sprite sheet animations. This is a frame animation workhorse—this is where all the “good stuff” is happening.
    [code]
    public void Draw()
    {
    if (!visible) return;
    if (totalFrames > 1)
    {
    Rectangle source = new Rectangle();
    source.X = (frame % columns) * (int)size.X;
    source.Y = (frame / columns) * (int)size.Y;
    source.Width = (int)size.X;
    source.Height = (int)size.Y;
    p_spriteBatch.Draw(image, position, source, color,
    rotation, origin, scale, SpriteEffects.None, 0.0f);
    }
    else
    {
    p_spriteBatch.Draw(image, position, null, color, rotation,
    origin, scaleV, SpriteEffects.None, 0.0f);
    }
    }
    [/code]
  6. Next, make a minor improvement to the Rotate() method to speed it up. If no rotation is happening, the calculations are skipped.
    [code]
    public void Rotate()
    {
    if (velocityAngular != 0.0f)
    {
    rotation += velocityAngular;
    if (rotation > Math.PI * 2)
    rotation -= (float)Math.PI * 2;
    else if (rotation < 0.0f)
    rotation = (float)Math.PI * 2 – rotation;
    }
    }
    [/code]
  7. Next, we must make minor modifications to the Boundary() method to account for the size of a single frame, rather than using the whole image. The old lines have been commented out; note the new calculations for halfw and halfh.
    [code]
    public Rectangle Boundary()
    {
    //int halfw = (int)((float)(image.Width / 2) * scaleV.X);
    //int halfh = (int)((float)(image.Height / 2) * scaleV.Y);
    int halfw = (int)((float)(size.X / 2) * scaleV.X);
    int halfh = (int)((float)(size.Y / 2) * scaleV.Y);
    return new Rectangle(
    (int)position.X – halfw,
    (int)position.Y – halfh,
    halfw * 2,
    halfh * 2);
    }
    [/code]
  8. Next, we’ll add two new overloads of Animate() to support frame animation. It might be less confusing to call these FrameAnimate() if you want, because they share the same name as the previous version of Animate() that did color and transform animations. The difference between those and frame animation is that the latter requires parameters, either the elapsed time or the actual frame range, time, and animation speed. First, let’s review the existing method (no changes required):
    [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]

Okay, now here are the new methods for frame animation that you can add to the source code. The elapsedTime parameter helps the animation code to run at the correct speed. Note that the simple version calls the more complex version with default values for convenience. If you want to draw just a subset of an animation set, this second version of Animate() will do that.

[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]

That concludes the changes to the Sprite class, so now we can go into the example.

Sample Program

The example for this hour draws a bunch of animated asteroid sprites that move across the screen (in the usual landscape mode). But this is no simple demo—there is rudimentary gameplay. A small spaceship has been added. Tap above the ship to move it up, or below the ship to move it down, and avoid the asteroids! (See Figure 14.5.)

LISTING 14.1 Source Code for the Frame Animation Demo Program

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
TouchLocation oldTouch;
Random rand;
SpriteFont font;
List<Sprite> objects;
Sprite fighter;
int score = 0;
int hits = 0;
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 fighter sprite
fighter = new Sprite(Content, spriteBatch);
fighter.Load(“fighter”);
fighter.position = new Vector2(40, 240);
fighter.rotation = MathHelper.ToRadians(90);
//create asteroid sprites
for (int n = 0; n < 20; n++)
{
Sprite ast = new Sprite(Content, spriteBatch);
ast.Load(“asteroid”);
ast.size = new Vector2(60, 60);
ast.origin = new Vector2(30, 30);
float x = 800 + (float)rand.Next(800);
float y = (float)rand.Next(480);
ast.position = new Vector2(x, y);
ast.columns = 8;
ast.totalFrames = 64;
ast.frame = rand.Next(64);
x = (float)(rand.NextDouble() * rand.Next(1, 10));
y = 0;
ast.velocityLinear = new Vector2(-x, y);
objects.Add(ast);
}
}
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)
{
if (touch.Position.Y < fighter.Y )
{
fighter.velocityLinear.Y -= 1.0f;
}
else if (touch.Position.Y >= fighter.Y)
{
fighter.velocityLinear.Y += 1.0f;
}
}
oldTouch = touch;
}
//gradually reduce velocity
if (fighter.velocityLinear.Y < 0)
fighter.velocityLinear.Y += 0.05f;
else if (fighter.velocityLinear.Y > 0)
fighter.velocityLinear.Y -= 0.05f;
//keep fighter in screen bounds
if (fighter.Y < -32)
{
fighter.Y = -32;
fighter.velocityLinear.Y = 0;
}
else if (fighter.Y > 480-32)
{
fighter.Y = 480-32;
fighter.velocityLinear.Y = 0;
}
fighter.Move();
//update all objects
foreach (Sprite spr in objects)
{
spr.Rotate();
spr.Move();
//wrap asteroids around screen
if (spr.X < -60)
{
spr.X = 800;
score++;
}
//look for collision with fighter
if (fighter.Boundary().Intersects(spr.Boundary()))
{
hits++;
spr.X = 800;
}
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
foreach (Sprite spr in objects)
{
spr.Animate(gameTime.ElapsedGameTime.Milliseconds);
spr.Draw();
}
fighter.Draw();
spriteBatch.DrawString(font, “Score:” + score.ToString() +
“, Hits:” + hits.ToString(), Vector2.Zero, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]

The Frame Animation Demo is a mini-game of dodging the asteroids.
FIGURE 14.5 The Frame Animation Demo is a mini-game of dodging the asteroids.

 

When Objects Collide

Boundary Collision Detection

The first type of collision test we’ll examine uses the boundaries of two rectangles for the test condition. Also called “bounding box” collision detection, this technique is simple and fast, which makes it ideal in situations in which a lot of objects are interacting on the screen at once. Not as precise as the radial technique (coming up next), boundary collision detection does return adequate results for a high-speed arcade game. Interestingly, this is also useful in a graphical user interface (GUI), comparing the boundary of the mouse cursor with GUI objects for highlighting and selection purposes. On the WP7, however, we can’t actually track mouse movement because a “mouse” does not even exist on Windows Phone, only touch points.

XNA provides a useful struct called Rectangle. Among the many methods in this struct is Intersects(), which accepts a single parameter—another Rectangle. The return value of Intersects() is just a bool (true/false). The trick to using Rectangle.Intersects() effectively is creating a bounding rectangle around a sprite at its current location on the screen, taking into account the sprite’s width and height. When we get into animation a couple of hours from now, we’ll have to adapt this code to take into account the width and height of individual frames. In the meantime, we can use the width and height of the whole texture since we’re just working with simple static sprite images at this point. Figure 10.1 illustrates the boundary around a sprite image.

The image dimensions are used as the boundary.
FIGURE 10.1 The image dimensions are used as the boundary.

As you can see in this illustration, boundary collision does not require that the image have an equal width and height, since the bounding rectangle can handle a nonuniform aspect ratio. However, it is usually better to store game artwork in square images for best results.

Accounting for Pivot

Remember that Sprite.position references the pivot point of the object, also known as the origin. Normally, the origin is at the center of the sprite. If the origin is changed from the center of the sprite, the boundary will return an incorrect result! Therefore, if you change the origin, it’s up to you to take that into account when calculating boundaries. The Sprite class does not account for such changes. The calculation for the boundary might be done like so at this point:

[code]
int halfw = image.Width / 2;
int halfh = image.Height / 2;
return new Rectangle(
(int)position.X – halfw,
(int)position.Y – halfh,
image.Width,
image.Height);
[/code]

Accounting for Scaling

That should work nicely, all things being equal. But another important factor that must be considered is scaling. Has the scale been changed from 1.0, or full image size? If so, that will affect the boundary of the sprite, and we have to account for that or else false results will come back while the game is running, causing a sprite to collide when it clearly did not touch another sprite (due to scaling errors). When the boundary is calculated, not only the origin must be accounted for, but the scaling as well, which adds a bit of complexity to the Boundary() method. Not to worry, though—it’s calculated by the method for us. Here is a new version that takes into account the scaling factor:

[code]
int halfw = (int)( (float)(image.Width / 2) * scaleV.X );
int halfh = (int)( (float)(image.Height / 2) * scaleV.Y );
return new Rectangle(
(int)position.X – halfw,
(int)position.Y – halfh,
halfw * 2,
halfh * 2);
[/code]

What is happening in this code is that the width and height are each divided by two, and the scaling is multiplied by these halfw and halfh values to arrive at scaled dimensions, which are then multiplied by two to get the full width and height when returning the Rectangle.

Sprite Class Changes

To simplify the code, we can add a new method to the Sprite class that will return the boundary of a sprite at its current location on the screen. While we’re at it, it’s time to give Sprite a new home in its own source code file. The new file will be called Sprite.cs. The namespace is a consideration, because the class needs a home. I propose just calling it GameLibrary, and in any program that uses Sprite, a simple using statement will import it.

The new version of this class includes the Boundary() method discussed earlier. An additional new method is also needed for debugging purposes. Every class has the capability to override the ToString() method and return any string value. We can override ToString() and have it return information about the sprite. This string can be logged to a text file if desired, but it is easier to just print it on the screen. The code in ToString() is quite messy due to all the formatting codes used, but the result looks nice when printed. It can be very helpful to add a ToString() to a custom class for this purpose, for quick debugging. For example, it is helpful to see the position and boundary of a sprite to verify whether collision is working correctly. See the demo coming up shortly to see an example of this in action. Our Sprite class, the source code for which is shown in Listing 10.1, sure has grown since its humble beginning!

LISTING 10.1 Revised source code for 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;
public bool alive;
public bool visible;
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;
alive = true;
visible = true;
}
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);
}
catch (Exception) { return false; }
return true;
}
public void Draw()
{
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;
else if (rotation < 0.0f)
rotation = (float)Math.PI * 2 – rotation;
}
public Rectangle Boundary()
{
int halfw = (int)( (float)(image.Width / 2) * scaleV.X );
int halfh = (int)( (float)(image.Height / 2) * scaleV.Y );
return new Rectangle(
(int)position.X – halfw,
(int)position.Y – halfh,
halfw * 2,
halfh * 2);
}
public override string ToString()
{
string s = “Texture {W:” + image.Width.ToString() +
“ H:” + image.Height.ToString() + “}n” +
“Position {X:” + position.X.ToString(“N2”) + “ Y:” +
position.Y.ToString(“N2”) + “}n” +
“Lin Vel “ + velocityLinear.ToString() + “n” +
“Ang Vel {“ + velocityAngular.ToString(“N2”) + “}n” +
“Scaling “ + scaleV.ToString() + “n” +
“Rotation “ + rotation.ToString() + “n” +
“Pivot “ + origin.ToString() + “n”;
Rectangle B = Boundary();
s += “Boundary {X:” + B.X.ToString() + “ Y:” +
B.Y.ToString() + “ W:” + B.Width.ToString() +
“ H:” + B.Height.ToString() + “}n”;
return s;
}
}
[/code]

Boundary Collision Demo

Let’s put the new Sprite method to use in an example. This example has four asteroids moving across the screen, and our spaceship pilot must cross the asteroid belt without getting hit by an asteroid. This example is automated—there’s no user input. So just watch it run this time. The source code is found within Listing 10.2, while Figure 10.2 shows a screenshot of the program running. When the ship collides with an asteroid, it simply stops until the asteroid goes on by. But to give the ship more survivability, additional intelligence code has to be added.

The ship tries to dodge asteroids as it crosses the asteroid field.
FIGURE 10.2 The ship tries to dodge asteroids as it crosses the asteroid field.

When the demo is completed later in the hour, it will also have dodging capability. If an asteroid is directly above or below the ship when a “grazing” collision occurs, the ship can simply stop to avoid being destroyed. But if an asteroid is coming from the left or right, destruction is imminent! When this happens, the pilot has to use emergency thrusters to quickly move forward to get out of the way of the asteroid. This is also automated, so again, just watch it run, as input is ignored. We’ll start by just having the ship stop when a collision occurs, and add the capability to speed up in the section coming up on collision response.

Note that the using statements are included here just to show that GameLibrary must be included (the namespace containing the Sprite class), but they will again be omitted in future listings.

LISTING 10.2 Source code for the boundary collision demo program.

[code]
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
using GameLibrary;
namespace Bounding_Box_Collision
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Random rand;
Viewport screen;
SpriteFont font;
Sprite[] asteroids;
Sprite ship;
const float SHIP_VEL = -1.5f;
int collisions = 0;
int escapes = 0;
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;
rand = new Random();
font = Content.Load<SpriteFont>(“WascoSans”);
//create asteroids
asteroids = new Sprite[4];
for (int n=0; n<4; n++)
{
asteroids[n] = new Sprite(Content, spriteBatch);
asteroids[n].Load(“asteroid”);
asteroids[n].position.Y = (n+1) * 90;
asteroids[n].position.X = (float)rand.Next(0, 760);
int factor = rand.Next(2, 12);
asteroids[n].velocityLinear.X = (float)
((double)factor * rand.NextDouble());
}
//create ship
ship = new Sprite(Content, spriteBatch);
ship.Load(“ship”);
ship.position = new Vector2(390, screen.Height);
ship.scale = 0.2f;
ship.velocityLinear.Y = SHIP_VEL;
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//move asteroids
foreach (Sprite ast in asteroids)
{
ast.Move();
if (ast.position.X < 0 – ast.image.Width)
ast.position.X = screen.Width;
else if (ast.position.X > screen.Width)
ast.position.X = 0 – ast.image.Width;
}
//move ship
ship.Move();
if (ship.position.Y < 0)
{
ship.position.Y = screen.Height;
escapes++;
}
//look for collision
int hit = 0;
foreach (Sprite ast in asteroids)
{
if (BoundaryCollision(ship.Boundary(), ast.Boundary()))
{
//oh no, asteroid collision is imminent!
EvasiveManeuver(ast);
collisions++;
hit++;
break;
}
//if no collision, resume course
if (hit == 0)
ship.velocityLinear.Y = SHIP_VEL;
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
foreach (Sprite ast in asteroids)
ast.Draw();
ship.Draw();
string text = “Collisions:” + collisions.ToString();
spriteBatch.DrawString(font, text, new Vector2(600, 0),
Color.White);
text = “Escapes:” + escapes.ToString();
spriteBatch.DrawString(font, text, new Vector2(600, 25),
Color.White);
spriteBatch.DrawString(font, ship.ToString(), new Vector2(0, 0),
Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
void EvasiveManeuver(Sprite ast)
{
//for now, just stop the ship
ship.velocityLinear.Y = 0.0f;
}
bool BoundaryCollision(Rectangle A, Rectangle B)
{
return A.Intersects(B);
}
}
}
[/code]

Radial Collision Detection

The term “radial” refers to rays or radii (plural for radius), implying that this form of collision detection uses the radii of objects. Another common term is “distance” or “spherical” collision testing. In some cases, a bounding sphere is used to perform 3D collision testing of meshes in a rendered scene, the 3D version of a bounding circle. (Likewise, a bounding cube represents the 3D version of rectangular boundary collision testing.) Although radial collision can be done with an image that has an unbalanced aspect ratio (of width to height), best results will be had when the image has uniform dimensions. The first figure here, Figure 10.3, shows how a bounding circle will not correspond to the image’s width and height correctly, and this radius would have to be set manually.

A bounding circle around a nonsquare image.
FIGURE 10.3 A bounding circle around a nonsquare image.

But if the image’s width and height are uniform, as illustrated in Figure 10.4, then the width or height of the image can be used for the radius, simplifying everything. The beauty of radial collision testing is that only a single float is needed—the radius, along with a method to calculate the distance between two objects. This illustration also shows the importance of reducing the amount of empty space in an image. The transparent background pixels in this illustration (the area within the box) should be as close to the edges of the visible pixels of the shuttle as possible to improve collision results. The radius and distance factors will never be perfect, so reducing the overall radii of both sprites is often necessary. I have found in testing that 80% produces good results. Due to the shape of some images, reducing the radius by 50% might even be helpful, especially in a high-speed arcade-style game in which the velocity would exceed the collision error anyway.

Here is a method that will be helpful when radial collision detection is being used:

[code]
bool RadialCollision(Vector2 A, Vector2 B, float radius1, float radius2)
{
double diffX = A.X – B.X;
double diffY = A.Y – B.Y;
double dist = Math.Sqrt(Math.Pow(diffX, 2) + Math.Pow(diffY, 2));
return (dist < radius1 + radius2);
}
[/code]

An overload with Sprite properties will make using the method even more useful in a game:

[code]
bool RadialCollision(Sprite A, Sprite B)
{
float radius1 = A.image.Width / 2;
float radius2 = B.image.Width / 2;
return RadialCollision(A.position, B.position, radius1, radius2);
}
[/code]

The bounding circle shows the radius at all points from the center.
FIGURE 10.4 The bounding circle shows the radius at all points from the center.

To test radial collision, just make a change to the Boundary Collision program so that RadialCollision() is called instead of BoundaryCollision() in the program’s Update() method.

Assessing the Damage

We now have two algorithms to test for sprite collisions: boundary and radial. Detecting collisions is the first half of the overall collision-handling code in a game. The second half involves responding to the collision event in a meaningful way.

There are as many ways to respond to the collision are there are games in the computer networks of the world—that is, there is no fixed “right” or “wrong” way to do this; it’s a matter of gameplay. I will share one rather generic way to respond to collision events by figuring out where the offending sprite is located in relation to the main sprite.

First, we already know that a collision occurred, so this is post-collision response, not predictive response. The question is not whether a collision occurred, but where the offending sprite is located. Remember that the center of a sprite represents its position. So if we create four virtual boxes around our main sprite, and then use those to test for collision with the offender, we can get a general idea of where it is located: above, below, left, or right, as Figure 10.5 shows.

Four collision response boxes are used to quickly determine colliding object position.
FIGURE 10.5 Four collision response boxes are used to quickly determine colliding object position.

There will be cases in which the offender is within two of these virtual boxes at the same time, but we aren’t concerned with perfect results, just a general result that is “good enough” for a game. If you are doing a slow-paced game in which pixel-perfect collision is important, it would be vital to write more precise collision response code, perhaps a combination of boundary and radial collision values hand-coded for each sprite’s unique artwork. It might even be helpful to separate appendages into separate sprites and move them in relation to the main body, then perform collision testing on each item of the overall “game object.” Figure 10.6 shows a screenshot of the new boundary collision example, which now has some collision-avoidance intelligence built in. The source code for the program is contained in Listing 10.3.

The ship now has a rudimentary intelligence to avoid collisions.
FIGURE 10.6 The ship now has a rudimentary intelligence to avoid collisions.

LISTING 10.3 Source code for the new boundary collision program with collision-avoidance A.I.

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Random rand;
Viewport screen;
SpriteFont font;
Sprite[] asteroids;
Sprite ship;
int collisions = 0;
int escapes = 0;
int kills = 0;
int hit = 0;
const float SHIP_VEL = 1.5f;
const float SHIP_ACCEL = 0.2f;
const float ESCAPE_VEL = 2.5f;
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;
rand = new Random();
font = Content.Load<SpriteFont>(“WascoSans”);
//create asteroids
asteroids = new Sprite[4];
for (int n=0; n<4; n++)
{
asteroids[n] = new Sprite(Content, spriteBatch);
asteroids[n].Load(“asteroid”);
asteroids[n].position.Y = (n+1) * 90;
asteroids[n].position.X = (float)rand.Next(0, 760);
int factor = rand.Next(2, 12);
asteroids[n].velocityLinear.X = (float)
((double)factor * rand.NextDouble());
}
//create ship
ship = new Sprite(Content, spriteBatch);
ship.Load(“ship”);
ship.position = new Vector2(390, screen.Height);
ship.scale = 0.2f;
ship.velocityLinear.Y = -SHIP_VEL;
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
//move asteroids
foreach (Sprite ast in asteroids)
{
ast.Move();
if (ast.position.X < 0 – ast.image.Width)
ast.position.X = screen.Width;
else if (ast.position.X > screen.Width)
ast.position.X = 0 – ast.image.Width;
}
//move ship
ship.Move();
if (ship.position.Y < 0)
{
ship.position.Y = screen.Height;
escapes++;
}
//look for collision
foreach (Sprite ast in asteroids)
{
if (RadialCollision(ship, ast))
{
//oh no, asteroid collision is imminent!
EvasiveManeuver(ast);
collisions++;
hit++;
}
}
//accelerate
if (ship.velocityLinear.Y >= -SHIP_VEL)
{
ship.velocityLinear.Y -= SHIP_ACCEL;
if (ship.velocityLinear.Y < -SHIP_VEL)
ship.velocityLinear.Y = -SHIP_VEL;
}
else if (ship.velocityLinear.Y < SHIP_VEL)
{
ship.velocityLinear.Y += SHIP_ACCEL;
if (ship.velocityLinear.Y > SHIP_VEL)
ship.velocityLinear.Y = SHIP_VEL;
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
foreach (Sprite ast in asteroids)
{
ast.Draw();
ast.color = Color.White;
}
ship.Draw();
string text = “Intersections:” + collisions.ToString();
spriteBatch.DrawString(font, text, new Vector2(600, 0), Color.White);
text = “Kills:” + kills.ToString();
spriteBatch.DrawString(font, text, new Vector2(600, 25), Color.White);
text = “Escapes:” + escapes.ToString();
spriteBatch.DrawString(font, text, new Vector2(600, 50), Color.White);
float survival = 100.0f – (100.0f / ((float)collisions
/ (float)kills));
text = “Survival:” + survival.ToString(“N2”) + “%”;
spriteBatch.DrawString(font, text, new Vector2(600, 75), Color.White);
spriteBatch.DrawString(font, ship.ToString(), new Vector2(0, 0),
Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
void EvasiveManeuver(Sprite ast)
{
ast.color = Color.Red;
//shortcuts for ship
int SW = (int)(float)(ship.image.Width * ship.scale);
int SH = (int)(float)(ship.image.Height * ship.scale);
int SX = (int)ship.position.X – SW/2;
int SY = (int)ship.position.Y – SH/2;
//create boundary boxes around the ship
Rectangle[] boxes = new Rectangle[4];
boxes[0] = ship.Boundary(); //upper
boxes[0].Y -= SH;
boxes[1] = ship.Boundary(); //lower
boxes[1].Y += SH;
boxes[2] = ship.Boundary(); //left
boxes[2].X -= SW;
boxes[3] = ship.Boundary(); //right
boxes[3].X += SW;
if (boxes[0].Intersects(ast.Boundary()))
ship.velocityLinear.Y = SHIP_VEL * ESCAPE_VEL;
else if (boxes[1].Intersects(ast.Boundary()))
ship.velocityLinear.Y = -SHIP_VEL * ESCAPE_VEL;
else if (boxes[2].Intersects(ast.Boundary()))
ship.velocityLinear.Y = -SHIP_VEL * ESCAPE_VEL;
else if (boxes[3].Intersects(ast.Boundary()))
ship.velocityLinear.Y = -SHIP_VEL * ESCAPE_VEL;
//check for a “kill,” intersection with small inner box
Rectangle kill = ship.Boundary();
int shrinkh = kill.Width / 4;
int shrinkv = kill.Height / 4;
kill.Inflate(-shrinkh, -shrinkv);
if (kill.Intersects(ast.Boundary()))
kills++;
}
bool BoundaryCollision(Rectangle A, Rectangle B)
{
return A.Intersects(B);
}
bool RadialCollision(Sprite A, Sprite B)
{
float radius1 = A.image.Width / 2;
float radius2 = B.image.Width / 2;
return RadialCollision(A.position, B.position, radius1, radius2);
}
bool RadialCollision(Vector2 A, Vector2 B, float radius1, float radius2)
{
double diffX = A.X – B.X;
double diffY = A.Y – B.Y;
double dist = Math.Sqrt(Math.Pow(diffX, 2) + Math.Pow(diffY, 2));
return (dist < radius1 + radius2);
}
}
[/code]

This is perhaps our longest sample program so far, even taking into account that that Sprite class is now found in a different source code file. Game A.I. is a challenging subject. By studying the way the algorithm responds to asteroid collisions, you could use this technique for any number of game scenarios to improve gameplay and/or increase the difficulty by making your A.I. sprites more intelligent.

We learned about collision detection via boundary and radial collision methods, and used these algorithms to study collision response in gameplay code. When a collision algorithm is available, it really opens up our ability to make a game for the first time. As we saw in the final example of this hour, the level of response to collisions can be basic or advanced, rudimentary or quite intelligent, as the spaceship in this example demonstrated.

Advanced Linear and Angular Velocity

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]