Collision Series 3: 2D Collision with Transformed Objects

This article explains how to perform per-pixel collision detection on sprites by using linear transformations such as rotation or scale.
Note
This tutorial builds on the code that you wrote during the previous tutorial, "2D Collision Tutorial 2: Per-Pixel." Follow the steps in the previous tutorials before starting this one.

Introduction

In the previous tutorial, you enhanced your simple obstacle avoidance game by adding per-pixel collisions, which are more accurate than the previously existing bounding rectangle test. The per-pixel technique presented in tutorial 2 accommodates only positioned sprites, without any other transformations. For many games, this is completely sufficient. However, if your game requires objects that are rotated, scaled, or otherwise linearly transformed, you are going to need a more sophisticated per-pixel collision test.

Step 1: Create Rotating Obstacles

For starters, the game needs objects that rotate!

Add the Spinner Block Art to Your Project

  1. Make sure that you can see the Solution Explorer for your project. If you cannot see it, on the View menu, click Solution Explorer. When the Solution Explorer appears, you see files associated with your project in a tree structure.
  2. Right-click the Content project icon (two levels below the Solution icon, called "Content"), click Add, and then click Existing Item.
  3. In the dialog box that appears, browse to the path where the artwork is located, and select the SpinnerBlock.bmp texture. If you can't see the textures, change the Files of type selection box to read Content Pipeline Files.
  4. Click OK.

Modify the LoadContent Method to Use the New Art

Locate the LoadContent method in the game class. Change the blockTexture assignment to reference the SpinnerBlock texture.

blockTexture = Content.Load<Texture2D>("SpinnerBlock");

If you compile and run the project just like this, the game should still work perfectly with the big plus-sign shaped blocks. Because the blocks are larger, they are harder to dodge, so you may wish to change the BlockFallSpeed constant from 2 to 1.

Store Rotation with Each Block

Previously, blocks were represented by a Vector2 for their position, but now they also have a rotation. You need to create a structure to hold both pieces of data together for each block.

  1. Right-click the project icon in Solution Explorer, click Add, and then click New Item.
  2. In the dialog box that appears, select Class, and enter the name "Block.cs."
  3. Click OK.

This creates a Block class to which two fields must be added, one for position and one for rotation. The position field is a Vector2, as before. The rotation field is a single float that represents a clockwise rotation around the block's origin, measured in radians. The complete Block.cs should resemble the following:

C# 
using System;
using Microsoft.Xna.Framework;

namespace TransformedCollision
{
    /// <summary>
    /// A falling and spinning object to be avoided.
    /// </summary>
    class Block
    {
        public Vector2 Position;
        public float Rotation;
    }
}			

The existing code now needs to be adjusted to utilize the new Block class. Locate the Block field at the top of the game class and replace the blockPositions field with a Blocks field. Also, add a constant for the block rotation speed.

C# 
List<Block> blocks = new List<Block>();
const float BlockRotateSpeed = 0.005f;							  

At this point, the code will not compile, because the blockPositions field no longer exists. Any mention of blockPositions needs to be updated to refer to Blocks. For example, blockPositions[i] becomes blocks[i].Position, and foreach (Vector2 blockPosition in blockPositions) becomes foreach (Block block in blocks). Construction of a block also changes slightly. Instead of simply calling Vector2 blockPosition = new Vector2(x, y), a block must be constructed by calling Block newBlock = new Block() and then constructing the position by using newBlock.Position = new Vector2(x, y). Before proceeding to the next section, make all the necessary adjustments by searching for references to blockPositions. The compiler will complain if you miss any, and you can cheat by looking at the finished code sample. When the game works as expected (still without rotation), continue to the next section.

Update and Draw with Rotation

Although there is now a place to store rotation with each block, the value is always zero! For diversity, each block should start with a random rotation, but for simplicity, each block will rotate with a constant speed. Modify the block spawn code in the Update method to match the following code.

C# 
// Spawn new falling blocks
if (random.NextDouble() < BlockSpawnProbability)
{
	Block newBlock = new Block();

	// at a random position just above the screen
	float x = (float)random.NextDouble() *
		(Window.ClientBounds.Width - blockTexture.Width);
	newBlock.Position = new Vector2(x, -blockTexture.Height);

	// with a random rotation
	newBlock.Rotation = (float)random.NextDouble() * MathHelper.TwoPi;

	blocks.Add(newBlock);
}								 

Further down in the Update method, inside the block update loop, the rotation value needs to be animated. The new line appears in bold.

C# 
// Animate this block falling
blocks[i].Position += new Vector2(0.0f, BlockFallSpeed);
blocks[i].Rotation += BlockRotateSpeed;
      

Now the blocks are rotating, but the draw code does not have any knowledge of this. In the Draw method, modify the draw blocks loop to match the following code.

C# 
// Draw blocks
foreach (Block block in blocks)
{
	spriteBatch.Draw(blockTexture, block.Position, null, Color.White,
		block.Rotation, Vector2.Zero, 1.0f, SpriteEffects.None, 0.0f);
}

If you compile and run, you will notice two problems. First, as you'd expect, the collision doesn't work correctly. Second, the blocks are rotating around their top left corners! We will get to the first problem shortly, but we can solve the second problem quite easily, by moving the block origin from Vector2.Zero to the center of the block texture. Add the following field to the top of the game class.

Vector2 blockOrigin;

Immediately after loading the block texture in the LoadContent method, initialize blockOrigin to be centered.

C# 
// Calculate the block origin
blockOrigin =
	new Vector2(blockTexture.Width / 2, blockTexture.Height / 2);

And modify the Draw method to use this new field.

C# 
// Draw blocks
foreach (Block block in blocks)
{
	spriteBatch.Draw(blockTexture, block.Position, null, Color.White,
		block.Rotation, blockOrigin, 1.0f, SpriteEffects.None, 0.0f);
}

Compile and run again. The falling objects rotate correctly as they fall, but you may notice that there is yet another problem! The blocks disappear with a pop when their origin passes across the bottom edge of the window. This is also a simple fix. We need to account for the block origin when we detect removal of blocks from the screen. Modify the block update loop in the Update method. New code is in bold.

C# 
// Remove this block if it has fallen off the screen
if (block.Position.Y > Window.ClientBounds.Height + blockOrigin.Length())
						 

Finally, if you compile and run, everything looks perfect. Except, of course, the collision detection does not work correctly.

Step 2: Per-Pixel Collision with Transforms

Recall from the previous tutorial that a per-pixel collision test must compare every potentially colliding pixel from both sprites. Consider the following transformed sprites.

Both the red plus sign, called sprite A, and the blue slash, called sprite B, are transformed by more than just a simple translation. Both sprites are rotated, and sprite B is also scaled—notice how the pixel squares in sprite B are larger than in sprite A. To determine if the two sprites are colliding, we can test every pixel of A to see if it collides with a pixel of B. To do this, we need a way to take a pixel from sprite A and find the overlapping pixel in sprite B.

Note
To understand this technique, a strong understanding of matrices is helpful, but not required. For the purposes of this tutorial, you need to know only that a matrix can be used to represent a transformation such as scaling, rotation, or translation. Transformation matrices can be multiplied together to perform multiple transformations in sequence. A matrix can be used to transform a vector from one linear coordinate system to another. The inverse of the matrix transforms back to the original coordinate system. We will use the XNA Framework’s Vector2.Transform method to apply a matrix transformation to a vector.

The problem can be made much simpler by working from a perspective where sprite A is completely untransformed. This view uses a coordinate system that is local to sprite A. In other words, in this view, positive X is always one pixel directly to the right and positive Y is always one pixel directly above. In the following diagram, both sprites have the same position and orientation relative to each other, but the local coordinate system results in a much simpler algorithm.

To accomplish this, consider the following diagram.

The local coordinate systems of both sprites are relative to the world coordinate system. Points that are local to sprite A can be made local to sprite B by first passing through world space and then into the local space of sprite B. The first operation is performed by the transformation matrix of sprite A. The second operation is performed by the inverse of the tranformation matrix of sprite B.

Matrix transformAToB = transformA * Matrix.Invert(transformB);

Iterating over every pixel in A and transforming into B yields a point with fractional values that can be rounded to the nearest integer. If this integer lies within the bounds of B, the pixel is compared for collision against the original pixel in A.

The full method code follows. Add it to your game class.

C# 
/// <summary>
/// Determines if there is overlap of the non-transparent pixels between two
/// sprites.
/// </summary>
/// <param name="transformA">World transform of the first sprite.</param>
/// <param name="widthA">Width of the first sprite's texture.</param>
/// <param name="heightA">Height of the first sprite's texture.<;/param>
/// <param name="dataA">Pixel color data of the first sprite.</param>
/// <param name="transformB">World transform of the second sprite.</param>
/// <param name="widthB">Width of the second sprite's texture.</param>
/// <param name="heightB">Height of the second sprite's texture.</param>
/// <param name="dataB">Pixel color data of the second sprite.</param>
/// <returns>True if non-transparent pixels overlap; false otherwise</returns>
static bool IntersectPixels(
    Matrix transformA, int widthA, int heightA, Color[] dataA,
    Matrix transformB, int widthB, int heightB, Color[] dataB)
{
    // Calculate a matrix which transforms from A's local space into
    // world space and then into B's local space
    Matrix transformAToB = transformA * Matrix.Invert(transformB);

    // For each row of pixels in A
    for (int yA = 0; yA < heightA; yA++)
    {
        // For each pixel in this row
        for (int xA = 0; xA < widthA; xA++)
        {
            // Calculate this pixel's location in B
            Vector2 positionInB =
                Vector2.Transform(new Vector2(xA, yA), transformAToB);

            // Round to the nearest pixel
            int xB = (int)Math.Round(positionInB.X);
            int yB = (int)Math.Round(positionInB.Y);

            // If the pixel lies within the bounds of B
            if (0 <= xB && xB < widthB &&
                0 <= yB && yB < heightB)
            {
                // Get the colors of the overlapping pixels
                Color colorA = dataA[xA + yA * widthA];
                Color colorB = dataB[xB + yB * widthB];

                // If both pixels are not completely transparent,
                if (colorA.A != 0 && colorB.A != 0)
                {
                    // then an intersection has been found
                    return true;
                }
            }
        }
    }

    // No intersection found
    return false;
}

Test Application

This algorithm is likely to be dauntingly complex the first time you see it. To wrap your head around the concept of local space, take some time to try the test application that is located here. The test application presents two sprites, the black letters F and R (chosen because they have distinguishable orientation) in world space. The same two sprites are gray and represented in the local space of the F sprite. The dots represent the origins of the sprites. You will see that transforming the black R yields an analogous transform in the gray R, but transforming F yields an inverse transform in the gray R. The gray F never moves!

Test Application Controls

Action Keyboard Control Gamepad Control
Select F Hold left mouse button Hold left trigger
Select R Hold right mouse button Hold right trigger
Move selected object Move mouse Left thumb stick
Rotate selected object LEFT and RIGHT ARROW keys or scroll mouse wheel Right and left on the right thumb stick
Scale selected object UP and DOWN ARROW keys or hold CTRL and scroll mouse wheel Up and down on the right thumb stick
Select origin of F Hold ALT and left mouse button Hold left shoulder button
Select origin of R Hold ALT and right mouse button Hold right shoulder button

Step 3: Invoke the new Collision Test

To use the new intersection method, transformation matrices must be constructed for the person and for the falling blocks. The Matrix structure of the XNA Framework provides several static helper methods to create matrices for transformations that can be concatenated with multiplications. Add the bold lines to the Update method to calculate the person matrix solely from the person position.

C# 
// Move the player left and right with arrow keys or D-pad
if (keyboard.IsKeyDown(Keys.Left) ||
	gamePad.DPad.Left == ButtonState.Pressed)
{
	personPosition.X -= PersonMoveSpeed;
}
if (keyboard.IsKeyDown(Keys.Right) ||
	gamePad.DPad.Right == ButtonState.Pressed)
{
	personPosition.X += PersonMoveSpeed;
}

// Prevent the person from moving off of the screen
personPosition.X = MathHelper.Clamp(personPosition.X,
	safeBounds.Left, safeBounds.Right - personTexture.Width);

// Update the person's transform
Matrix personTransform =
	Matrix.CreateTranslation(new Vector3(personPosition, 0.0f));
		  
			  

Inside the block update loop, calculate the block transformation matrix. Be sure to account for its position, origin, and rotation. New code appears in bold.

C# 
// Build the block's transform
Matrix blockTransform =
    Matrix.CreateTranslation(new Vector3(-blockOrigin, 0.0f)) *
    Matrix.CreateRotationZ(blocks[i].Rotation) *
    Matrix.CreateTranslation(new Vector3(blocks[i].Position, 0.0f));
          
// Check collision with person
if (IntersectPixels(personTransform, personTexture.Width,
		personTexture.Height, personTextureData,
		blockTransform, blockTexture.Width,
		blockTexture.Height, blockTextureData))
{
	personHit = true;
}		

Compile and run the game. Everything should work exactly as expected now!

Going Beyond: Optimize

Two key optimizations should be performed. The first optimization is to perform a bounding rectangle collision test before invoking the per-pixel test. If the bounding rectangles do not intersect, then it is impossible for any pixels to intersect, so do not even bother to check. The second optimization is more complex, and is discussed in a later section.

Bounding Rectangles for Transformed Sprites

Sprites that are only positioned have trivially calculated bounding rectangles, but transformed sprites are slightly more complex. When a sprite is transformed, so is its bounding rectangle. Unfortunately, the new rectangle is not necessarily axis-aligned. An axis-aligned rectangle can be formed by creating a rectangle that bounds the four corners of the transformed rectangle.

Add the following method to your game class.

C# 
/// <summary>
/// Calculates an axis aligned rectangle which fully contains an arbitrarily
/// transformed axis aligned rectangle.
/// </summary>
/// <param name="rectangle">Original bounding rectangle.</param>
/// <param name="transform">World transform of the rectangle.</param>
/// <returns>A new rectangle which contains the trasnformed rectangle.</returns>
public static Rectangle CalculateBoundingRectangle(Rectangle rectangle,
										Matrix transform)
{
	// Get all four corners in local space
	Vector2 leftTop = new Vector2(rectangle.Left, rectangle.Top);
	Vector2 rightTop = new Vector2(rectangle.Right, rectangle.Top);
	Vector2 leftBottom = new Vector2(rectangle.Left, rectangle.Bottom);
	Vector2 rightBottom = new Vector2(rectangle.Right, rectangle.Bottom);

	// Transform all four corners into work space
	Vector2.Transform(ref leftTop, ref transform, out leftTop);
	Vector2.Transform(ref rightTop, ref transform, out rightTop);
	Vector2.Transform(ref leftBottom, ref transform, out leftBottom);
	Vector2.Transform(ref rightBottom, ref transform, out rightBottom);

	// Find the minimum and maximum extents of the rectangle in world space
	Vector2 min = Vector2.Min(Vector2.Min(leftTop, rightTop),
							  Vector2.Min(leftBottom, rightBottom));
	Vector2 max = Vector2.Max(Vector2.Max(leftTop, rightTop),
							  Vector2.Max(leftBottom, rightBottom));

	// Return as a rectangle
	return new Rectangle((int)min.X, (int)min.Y,
						 (int)(max.X - min.X), (int)(max.Y - min.Y));
}						   

In the Update method, check for intersection of the axis-aligned block rectangle with the player rectangle by adding the bold lines.

C# 
// Calculate the bounding rectangle of this block in world space
Rectangle blockRectangle = CalculateBoundingRectangle(
		 new Rectangle(0, 0, blockTexture.Width, blockTexture.Height),
		 blockTransform);

// The per-pixel check is expensive, so check the bounding rectangles
// first to prevent testing pixels when collisions are impossible.
if (personRectangle.Intersects(blockRectangle))
{
	// Check collision with person
	if (IntersectPixels(personTransform, personTexture.Width,
						personTexture.Height, personTextureData,
						blockTransform, blockTexture.Width,
						blockTexture.Height, blockTextureData))
	{
		personHit = true;
	}
}

Eliminate the Per-Pixel Transformation

Currently, the IntersectPixels method transforms every pixel of sprite A into the local space of sprite B. A substantial speed boost can be obtained by recognizing that each pixel in both sprites is a uniform displacement from the previous pixel. Only the first pixel of sprite A needs to be transformed into the coordinate space of sprite B. Subsequent pixels can be inferred by using only a simple translation.

Examine the following diagrams. Notice that advancing one pixel along the local x axis of sprite A results in an analogous movement along an arbitrary axis in the local coordinates of sprite B. This analogous movement vector can be determined by transforming the unit X vector from the local space of sprite A local space into the local space of sprite B without translation. Although these diagrams depict the unit X vector, the same can be done for the unit Y vector. Transformations without translation are performed by the Vector2.TransformNormal method.

For each iteration through the pixels of sprite A, the analogous position in sprite B is remembered. Each step in sprite A can be replicated in sprite B by simply adding the step vectors to the current position in sprite B.

The optimized IntersectPixels method follows.

C# 
/// <summary>
/// Determines if there is overlap of the non-transparent pixels between two
/// sprites.
/// </summary>
/// <param name="transformA">World transform of the first sprite.</param>
/// <param name="widthA">Width of the first sprite's texture.</param>
/// <param name="heightA">Height of the first sprite's texture.</param>
/// <param name="dataA">Pixel color data of the first sprite.</param>
/// <param name="transformB">World transform of the second sprite.</param>
/// <param name="widthB">Width of the second sprite's texture.</param>
/// <param name="heightB">Height of the second sprite's texture.</param>
/// <param name="dataB">Pixel color data of the second sprite.</param>
/// <returns>True if non-transparent pixels overlap; false otherwise</returns>
public static bool IntersectPixels(
				Matrix transformA, int widthA, int heightA, Color[] dataA,
				Matrix transformB, int widthB, int heightB, Color[] dataB)
{
	// Calculate a matrix which transforms from A's local space into
	// world space and then into B's local space
	Matrix transformAToB = transformA * Matrix.Invert(transformB);

	// When a point moves in A's local space, it moves in B's local space with a
	// fixed direction and distance proportional to the movement in A.
	// This algorithm steps through A one pixel at a time along A's X and Y axes
	// Calculate the analogous steps in B:
	Vector2 stepX = Vector2.TransformNormal(Vector2.UnitX, transformAToB);
	Vector2 stepY = Vector2.TransformNormal(Vector2.UnitY, transformAToB);

	// Calculate the top left corner of A in B's local space
	// This variable will be reused to keep track of the start of each row
	Vector2 yPosInB = Vector2.Transform(Vector2.Zero, transformAToB);

	// For each row of pixels in A
	for (int yA = 0; yA < heightA; yA++)
	{
		// Start at the beginning of the row
		Vector2 posInB = yPosInB;

		// For each pixel in this row
		for (int xA = 0; xA < widthA; xA++)
		{
			// Round to the nearest pixel
			int xB = (int)Math.Round(posInB.X);
			int yB = (int)Math.Round(posInB.Y);

			// If the pixel lies within the bounds of B
			if (0 <= xB && xB < widthB &&
				0 <= yB && yB < heightB)
			{
				// Get the colors of the overlapping pixels
				Color colorA = dataA[xA + yA * widthA];
				Color colorB = dataB[xB + yB * widthB];

				// If both pixels are not completely transparent,
				if (colorA.A != 0 && colorB.A != 0)
				{
					// then an intersection has been found
					return true;
				}
			}

			// Move to the next pixel in the row
			posInB += stepX;
		}

		// Move to the next row
		yPosInB += stepY;
	}

	// No intersection found
	return false;
}

Ideas to Expand

Now that you have a flexible per-pixel collision detection method, try out these ideas.

  • Make the falling blocks have varying scales.
  • Create collectable power-ups that make the person shrink or grow.