碰撞检测系列3:转换物体的2D碰撞检测
这篇文章讲述了如何对线性变换的物体,如物体的旋转和大小变换,使用基于像素的碰撞检测。
Note
注意
|
这个教程的代码是建立在之前教程的基础上的,请确保学习过前面的教程之后再开始本教程的学习。
|
绪论
在前面的教程当中,你已经为你的躲避落体游戏添加了可以使游戏中的碰撞更精确的基于像素级别的碰撞检测。而在教程
2
中所讲述的像素碰撞检测只是对于没有产生变换的图像精灵而言。而对于其他游戏而言,或许需要物体进行旋转,大小变换或者其他的线性变换,所以你就需要一个更加聪明的检测方法。
第一步:建立旋转的落体
首先,你的游戏需要一个可以旋转的落体。在
Content
文件夹下面的
SpinnerBlock.bmp
文件就是我们所需要的旋转下落的物体。
将
Spinner Block
文件添加到你的工程当中
1.
首先确定你打开了
Solution Exploer
并且可以看到你的工程项目。
2.
右击
Content
文件夹,单击
Add
,通过对话框选择
SpinnerBlock.bmp
纹理文件。
3.
单击
OK.
在
LoadGraphicsContent
方法中修改代码加载新的美术资源
在
game
类中找到
LoadGraphicsContent
方法,修改
blockTexture
,
blockTexture = content.Load<Texture2D>("Content/SpinnerBlock");
如果你编译运行工程,你会发现一个新的十字形的物体下落。如果你觉得物体下落太快没有办法躲避的话可以将
BlockFallSpeed
的值由
2
修改为
1
。
为每一个下落物体建立旋转元素
在之前的教程当中,我们使用一个
Vector2
型变量存储下落物体的位置,但是现在他们需要旋转。所以你需要为下落的物体建立一个类。
1.
右击
Solution Explorer
的工程图标,单击
Add
,单击
New Item
。
2.
在显示出的对话窗口中选择
Class
,输入名称
’Block.cs”
3.
单击
OK
。
建立了
Block
类,需要添加物体的位置和旋转两个元素。位置采用
Vector2
类型,而旋转需要一个
float
型来表示物体顺时针旋转的速度。
Block.cs
类应如下建立:
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; } } |
而现有的代码必须因为
Block
类的存在而进行改变,我们需要将所有的
blockPositions
替换为
Blocks
。
C#
|
List<Block> blocks = new List<Block>(); const float BlockRotateSpeed = 0.005f; |
现在,你的代码是无法编译通过的,因为
blockPositions
已经不存在了,而所有的
blockPositions
的需要由
Blocks
代替,例如需要将
blockPositions[i]
替换为
blocks[i].Position
,
将
foreach (Vector2 blockPosition in blockPositions)
替换为
foreach (Block block in blocks)
.
建立
block
的代码同样需要一些修改
.
需要将
Vector2 blockPosition = new Vector2(x, y)
,
修改为
Block newBlock = new Block()
同时添加
newBlock.Position = new Vector2(x, y)
.
在进行下面的学习之前请确保完成所有的替换。否则程序是无法编译的。
更新并绘制旋转精灵
尽管我们建立了
block
的类,但是没有进行初始化,下落物体以一个随机的速度进行匀速的旋转,在
Update
方法中修改下面代码:
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); } |
在
Update
方法中再进行修改。
C#
|
// Animate this block falling blocks[i].Position += new Vector2(0.0f, BlockFallSpeed); blocks[i].Rotation += BlockRotateSpeed; |
现在,下落的物体可以进行旋转了,但还需要在
Draw
方法中添加以下代码。
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); } |
如果现在进行编译运行,你会发现两个错误,第一个就是你所想像的那样,碰撞检测并没有起作用。第二个就是下落的物体只依照图片精灵的左上角为中心进行旋转。我们通过下面代码来修改这两个错误。
Vector2 blockOrigin;
在
LoadGraphicsContent
方法中加载
block
的纹理后,初始
blockOrigin
为图像精灵的中间。
C#
|
// Calculate the block origin blockOrigin = new Vector2(blockTexture.Width / 2, blockTexture.Height / 2); |
修改
Draw
方法:
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); } |
编译运行之后你会发现物体按照他们下落的路径进行旋转,但与此同时你会发现另一个错误,下落的物体在中心经过窗口下边的时候就消失了。现在我们在
Update
方法中修改代码如下:
C#
|
// Remove this block if it has fallen off the screen if (block.Position.Y > Window.ClientBounds.Height + blockOrigin.Length()) |
最后,物体的下落和旋转完成了。
第二步:转换物体的2D像素碰撞检测
在我们前面教程中讲到过对可能存在碰撞的两个图像精灵都要进行像素级别的碰撞检测。那么我们看一下下面的这种情况。
红色的十字图片,我们成为精灵
A
,蓝色斜杠的图片,我们称之为精灵
B
。我们可以看到这两个精灵都有部分的旋转,而且精灵
B
在大小上也有变化。而我们要判断这两个图片精灵是否产生碰撞,我们就要用精灵
A
的每一个像素与精灵
B
的进行比较。所以我们需要一个方法得到精灵
A
重合在精灵
B
上的像素位置。
注意
|
为了理解这个技术,对于矩阵的运算的理解是有帮助的,但不是必要的。现在你只需要知道一个矩阵可以用来实现精灵的大小,位置以及旋转等变换。多个变换矩阵有序的连乘可以表示多个变换。同样一个矩阵可以将一个向量通过一个坐标系转换为另外一个坐标系。而这个矩阵的逆矩阵可以完成反向的变换。我们使用
XNA
框架中的
Vector2.Transform
方法来实现对一个向量的变换。
|
如果我们将精灵
A
看作是没有经过变换的话,那么问题就会简单很多。我们现在使用精灵
A
的坐标系,如下图。现在这两个精灵之间的关系就比较好进行计算了。
下面这个图也许会帮助你理解是怎样实现坐标系的变换的。
每一个图像精灵的坐标系都与世界坐标相关,那么我们可以通过世界坐标来判断判断精灵
A
的像素在精灵
B
的像素的位置。首先计算出精灵
A
的变换矩阵,其次计算出精灵
B
的逆矩阵。
Matrix transformAToB = transformA * Matrix.Invert(transformB);
对精灵
A
的每一个像素点转换到精灵
B
所对应的区域内,如果出现小数的话近似为整数。如果这个整数在
B
的区域内,则将此像素点与原来精灵
A
的像素进行碰撞检测。
所有的代码如下,将他们添加到你的
Game
类中。
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; } |
测试程序
第一次看到这个算法也许有些令人恐惧,在
TransformedCollisionTest
的文件夹下有一个程序使你更好的理解上述的一些概念。这个程序呈现出两个图像精灵
F
和
R,
与此同时有两个相似的灰色精灵在与之对应的地方。其中原点是图像精灵的原始位置,你可以看到灰色的
R
会随着黑色
R
的变换而变换,但是
F
的旋转是灰色
R
旋转的反方向。灰色的
F
是不会移动的。
程序的控制
动作
|
键盘鼠标
|
手柄
|
选择
F
|
按下鼠标左键
|
按下左按键
|
选择
R
|
按下鼠标右键
|
按下右按键
|
选择移动
|
移动鼠标
|
左控制键
|
旋转物体
|
LEFT
和
RIGHT
键
或者鼠标滚轮
|
左右控制键
|
缩放物体
|
UP
和
DOWN
键或者
CTRL
加鼠标滚轮
|
上下控制键
|
选择原始
F
|
ALT
和鼠标左键
|
左上方按键
|
选择原始
R
|
ALT
和鼠标右键
|
右上方按键
|
第三步:应用新的碰撞算法
在新的算法当中,我们需要为游戏角色和下落物体都建立一个转换矩阵。我们可以使用
XNA
框架自带的
Matrix
结构来建立矩阵,并且通过多个矩阵的乘法完成转换。我们可以将下面的代码添加到
Update
方法当中来完成对游戏角色的转换。
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)); |
在
Update
循环当中,变换矩阵表示了下落物体的旋转,位置等转换。
C#
|
// Check collision with person if (IntersectPixels(personTransform, personTexture.Width, personTexture.Height, personTextureData, blockTransform, blockTexture.Width, blockTexture.Height, blockTextureData)) { personHit = true; } |
编译并运行游戏。一切都正常了!
扩展:优化
优化这个算法有以下两种方式,第一种是在调用像素检测之前使用包围盒检测来判断两个物体是否相交。如果包围盒都不想交的话我们就不用使用像素检测了。第二个优化方法比较复杂,我们在后面进行讨论。
变换物体的矩形包围盒
一般的图像精灵的包围盒比较好计算,而变换后的物体的包围盒计算就比较复杂了。当一个图像精灵变换之后,他的包围盒也随之变换。但是这个新的包围盒并不是在正坐标系下的。所以我们需要选择变换后的精灵的角作为包围盒的边界。如下图所示。
我们将下面的代码添加到你的
Game
类当中。
C#
|
/// <summary> /// 为变换后的图像精灵建立包围盒 /// </summary> /// <param name="rectangle">原始包围盒</param> /// <param name="transform">转换的矩形</param> /// <returns>一个新的包围盒</returns> public static Rectangle CalculateBoundingRectangle(Rectangle rectangle, Matrix transform) { // 在本地坐标下取得四个角的坐标 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); // 将四个角的坐标在世界坐标系当中变换 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); // 在世界坐标系当中选择最小和最大的坐标 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 new Rectangle((int)min.X, (int)min.Y, (int)(max.X - min.X), (int)(max.Y - min.Y)); } |
在
Update
方法当中,为相交的矩形进行像素级别的检测。
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; } } |
减少像素的转换
到目前为止,我们的
IntersectPixels
方法会将精灵
A
的每一个像素转换到精灵
B
的坐标当中,在像素的转换当中,我们如果知道第一个像素转换后的位置,那么我们就可以根据原先像素之间的位置关系来确定转换后的像素位置,而不需要再将这些像素进行矩阵乘法了。这样,速度就会提高很多。
我们来看下面这两幅图,可以看到在精灵
A
的坐标系当中,像素是按照
X
轴横向计算的,而在精灵
B
的坐标系当中,相似的像素按照另外一个方向排列。我们可以通过精灵
A
的坐标系和精灵
B
的坐标系的关系,不需要变换就可以求得在精灵
B
的坐标系中
X
轴方向每个像素的移动距离。同样,我们也可以求出
Y
轴方向的像素移动距离,我们主要通过
Vector2.TransformNormal
方法实现。
在精灵
A
的每个像素在精灵
B
当中的相对应的像素可以通过一个步向量(
Step Vector
)来计算。也就是说确定精灵
A
的第一个对应像素之后,后面的像素只通过步向量的计算既可以得到。比起通过矩阵计算要减少很多资源消耗。
下面是在
IntersectPixels
方法中添加的优化代码
C#
|
/// <summary> /// 如果两精灵有重叠的地方则运行以下程序 /// </summary> /// <param name="transformA">第一个精灵的年换矩阵.</param> /// <param name="widthA">第一个精灵贴图的宽.</param> /// <param name="heightA">第一个精灵贴图的高</param> /// <param name="dataA">第一个精灵的像素颜色数据</param> /// <param name="transformB">第二个精灵的年换矩阵.</param> /// <param name="widthB">第二个精灵贴图的宽.</param> /// <param name="heightB">第二个精灵贴图的高</param> /// <param name="dataB">第二个精灵的像素颜色数据</param> /// <returns>如果相交返回真否则假</returns> public static bool IntersectPixels( Matrix transformA, int widthA, int heightA, Color[] dataA, Matrix transformB, int widthB, int heightB, Color[] dataB) { // A到B的转换矩阵 Matrix transformAToB = transformA * Matrix.Invert(transformB); //当A的一个像素点转换到B的对应像素的时候,像素的移动方向是固定的。 //下面就是用来记录在X和Y轴上面的位移量 Vector2 stepX = Vector2.TransformNormal(Vector2.UnitX, transformAToB); Vector2 stepY = Vector2.TransformNormal(Vector2.UnitY, transformAToB); // 从左上角的像素开始进行转换 // 该变量同样用来记录每一行的开始位置 Vector2 yPosInB = Vector2.Transform(Vector2.Zero, transformAToB); for (int yA = 0; yA < heightA; yA++) { Vector2 posInB = yPosInB; for (int xA = 0; xA < widthA; xA++) { // 近似到临近像素点 int xB = (int)Math.Round(posInB.X); int yB = (int)Math.Round(posInB.Y); // 确定在B的区域内 if (0 <= xB && xB < widthB && 0 <= yB && yB < heightB) { // 取得相覆盖的颜色信息 Color colorA = dataA[xA + yA * widthA]; Color colorB = dataB[xB + yB * widthB]; // 如果A,B两个精灵的该点均为不透明 if (colorA.A != 0 && colorB.A != 0) { // 相交 return true; } } // 移到下一行 posInB += stepX; } // 移到下一列 yPosInB += stepY; } // 没有相交 return false; } 下载附带源码 |
© 2007 Microsoft Corporation. All rights reserved.
Send feedback to xna@microsoft.com.
Send feedback to xna@microsoft.com.