XNA系列教程 2D碰撞教程3:转换物体的碰撞检测

碰撞检测系列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)
{
            // AB的转换矩阵
            Matrix transformAToB = transformA * Matrix.Invert(transformB);
 
 
            //A的一个像素点转换到B的对应像素的时候,像素的移动方向是固定的。
            //下面就是用来记录在XY轴上面的位移量
            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];
 
                                                 // 如果AB两个精灵的该点均为不透明
                                                 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.
© 2007 Vincent Zhang. 转载请注明 . 如果对翻译文章有疑问发送邮件至发送邮件至 vinile@163.com
 
 
 
 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值