Drawing with Z-Index Ordering

Prioritized Drawing

We already have the ability to perform prioritized “z-index” drawing of a sprite image with the SpriteBatch.Draw() method, and we have been using the z-index parameter all along, just set to a value of zero. This effectively gave every sprite the same priority. When that is the case, priority will be based entirely on the order at which sprites are drawn (in gameplay code). SpriteBatch.Draw() has the capability to automatically prioritize the drawing of some sprites over the top of other sprites using this z-index buffer.

What does “z-index” mean, you may be wondering? When doing 2D sprite programming, we deal only with the X and Y coordinates on the screen. The Z coordinate, then, is the position of the sprite in relation to other sprites that overlap each other. The range for the z-index goes from 0.0 to 1.0. So, a sprite with a z-index priority of 0.6 will draw over a sprite with a z-index priority of 0.3. Note that 1.0 is the highest, and 0.0 is the lowest.

Sprite Class Changes

Adding Z-Buffering to the Sprite Class

A very minor change is required to enable z-index drawing in our Sprite class. We’ll go over those changes here.

  1. Add a zindex variable to the class:
    [code]
    public float zindex;
    [/code]
  2. In the Sprite class constructor, initialize the new variable:
    [code]
    zindex = 0.0f;
    [/code]
  3. Replace the hard-coded zero with the zindex variable in the SpriteBatch.Draw() calls inside Sprite.Draw() (there are two of them):
    [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, zindex);
    }
    else
    {
    p_spriteBatch.Draw(image, position, null, color, rotation,
    origin, scaleV, SpriteEffects.None, zindex);
    }
    }
    [/code]

Adding Rendering Support for Z-Buffering

To use a z-buffer for prioritized drawing, a change must be made to the call to SpriteBatch.Begin(), which gets drawing started in the main program code. No change is made to SpriteBatch.End(). There are actually five overloads of SpriteBatch.Begin()! The version we want requires just two parameters: SpriteSortMode and BlendState. Table 16.1 shows the SpriteSortMode enumeration values.

SpriteSortMode Enumeration

The default sorting mode is Deferred, which is what SpriteBatch uses when the default Begin() is called. For z-index ordering, we will want to use either BackToFront or FrontToBack. There is very little difference between these two except the weight direction of each sprite’s z-index.

When using BackToFront, smaller z-index values have higher priority, with 0.0 being drawn over other sprites within the range up to 1.0.

When using FrontToBack, the opposite is true: Larger z-index values (such as 1.0) are treated with higher priority than lower values (such as 0.0).

It works best if you just choose one and stick with it to avoid confusion. If you think of a z-index value of 1.0 being “higher” in the screen depth, use FrontToBack. If 0.0 seems to have a higher priority, use BackToFront.

In our example, we will use FrontToBack, with larger values for the z-index having higher priority.

Z-Index Demo

To demonstrate the effect z-index ordering has on a scene, I have prepared an example that draws a screen full of sprites and then moves a larger sprite across the screen. Based on the z-index value of each sprite, the larger sprite will appear either over or under the other sprites. In this example, the screen is filled with animated asteroid sprites, and the larger sprite is an image of a shuttle. Figure 16.1 shows the demo with the shuttle sprite drawn on top of the first half of the asteroids.

The shuttle appears over the first half of the rows.
FIGURE 16.1 The shuttle appears over the first half of the rows.

Figure 16.2 shows the shuttle sprite farther across the screen, where it appears under the second half of the rows of asteroid sprites (which have a higher z-index than the shuttle sprite).

The shuttle appears under the second half of the rows.
FIGURE 16.2 The shuttle appears under the second half of the rows.

Wrapping Around the Screen Edge

To assist with this demo, a new Animation subclass has been written to automatically wrap a sprite around the edges of the screen. This completely eliminates any gameplay code that would otherwise have to be added to the Update() method. This is called synergy—when a combination of simple things produces awesome results! We’re starting to see that happen with our sprite and animation code now. Wrapping around the edges of the screen might be considered more of a behavior than an animation.

[code]
public class WrapBoundary : Animation
{
Rectangle boundary;
public WrapBoundary(Rectangle boundary)
: base()
{
animating = true;
this.boundary = boundary;
}
public override Vector2 ModifyPosition(Vector2 original)
{
Vector2 pos = original;
if (pos.X < boundary.Left)
pos.X = boundary.Right;
else if (pos.X > boundary.Right)
pos.X = boundary.Left;
if (pos.Y < boundary.Top)
pos.Y = boundary.Bottom;
else if (pos.Y > boundary.Bottom)
pos.Y = boundary.Top;
return pos;
}
}
[/code]

Z-Index Demo Source Code

Listing 16.1 contains the source code for the Z-Index Demo. Note the code highlighted in bold—these lines are relevant to our discussion of z-buffering.

LISTING 16.1 Source Code for the Z-Index Demo

[code]
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
TouchLocation oldTouch;
Random rand;
SpriteFont font;
List<Sprite> objects;
Texture2D asteroidImage;
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 shuttle sprite
Sprite shuttle = new Sprite(Content, spriteBatch);
shuttle.Load(“shuttle”);
shuttle.scale = 0.4f;
shuttle.position = new Vector2(0, 240);
shuttle.rotation = MathHelper.ToRadians(90);
shuttle.velocityLinear = new Vector2(4, 0);
Rectangle bounds = new Rectangle(-80, 0, 800 + 180, 480);
shuttle.animations.Add(new WrapBoundary(bounds));
shuttle.zindex = 0.5f;
objects.Add(shuttle);
//load asteroid image
asteroidImage = Content.Load<Texture2D>(“asteroid”);
//create asteroid sprites with increasing z-index
for (int row = 0; row < 13; row++)
{
float zindex = row / 12.0f;
for (int col = 0; col < 8; col++)
{
Sprite spr = new Sprite(Content, spriteBatch);
spr.image = asteroidImage;
spr.columns = 8;
spr.totalFrames = 64;
spr.animations.Add(new FrameLoop(0, 63, 1));
spr.position = new Vector2(30 + 60 * row, 30 + col * 60);
spr.size = new Vector2(60, 60);
spr.zindex = zindex;
objects.Add(spr);
}
}
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back ==
ButtonState.Pressed)
this.Exit();
foreach (Sprite spr in objects)
{
spr.Rotate();
spr.Move();
}
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend);
foreach (Sprite spr in objects)
{
spr.Animate();
spr.Draw();
}
spriteBatch.End();
base.Draw(gameTime);
}
}
[/code]

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.