1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00
Files
LearnOpenGL-CN/docs/06 In Practice/2D-Game/05 Collisions/02 Collision detection.md
2017-06-26 23:50:49 +08:00

8.7 KiB
Raw Blame History

碰撞检测

当试图判断两个物体之间是否有碰撞发生时,我们通常不使用物体本身的数据,因为这些物体常常会很复杂,这将导致碰撞检测变得很复杂。正因这一点,使用重叠在物体上的更简单的外形(通常有较简单明确的数学定义)来进行碰撞检测成为常用的方法。我们基于这些简单的外形来检测碰撞,这样代码会变得更简单且节省了很多性能。这些碰撞外形例如圆、球体、长方形和立方体等,与拥有上百个三角形的网格相比简单了很多。

虽然它们确实提供了更简单更高效的碰撞检测算法,但这些简单的碰撞外形拥有一个共同的缺点,这些外形通常无法完全包裹物体。产生的影响就是当检测到碰撞时,实际的物体并没有真正的碰撞。必须记住的是这些外形仅仅是真实外形的近似。

AABB - AABB 碰撞

AABB代表的是与坐标轴对齐的边界框(bounding box)边界框是指与场景基础坐标轴2D中的是x和y轴对齐的长方形的碰撞外形。与坐标轴对齐意味着这个长方形没有经过旋转并且它的边线和场景中基础坐标轴平行例如左右边线和y轴平行。这些边界框总是和场景的坐标轴平行这使得所有的计算都变得更简单。下边是我们用一个AABB包裹一个球对象物体

Breakout中几乎所有的物体都是基于长方形的物体因此很理所应当地使用与坐标系对齐的边界框来进行碰撞检测。这就是我们接下来要做的。

有多种方式来定义与坐标轴对齐的边界框。其中一种定义AABB的方式是获取左上角点和右下角点的位置。我们定义的GameObject类已经包含了一个左上角点位置它的Position vector并且我们可以通过把左上角点的矢量加上它的尺寸Position +Size很容易地计算出右下角点。每个GameObject都包含一个AABB我们可以高效地使用它们碰撞。

那么我们如何判断碰撞呢当两个碰撞外形进入对方的区域时就会发生碰撞例如定义了第一个物体的碰撞外形以某种形式进入了第二个物体的碰撞外形。对于AABB来说很容易判断因为它们是与坐标轴对齐的对于每个轴我们要检测两个物体的边界在此轴向是否有重叠。因此我们只是简单地检查两个物体的水平边界是否重合以及垂直边界是否重合。如果水平边界垂直边界都有重叠那么我们就检测到一次碰撞。

将这一概念转化为代码也是很直白的。我们对两个轴都检测是否重叠,如果都重叠就返回碰撞:

GLboolean CheckCollision(GameObject &one, GameObject &two) // AABB - AABB collision
{
    // Collision x-axis?
    bool collisionX = one.Position.x + one.Size.x >= two.Position.x &&
        two.Position.x + two.Size.x >= one.Position.x;
    // Collision y-axis?
    bool collisionY = one.Position.y + one.Size.y >= two.Position.y &&
        two.Position.y + two.Size.y >= one.Position.y;
    // Collision only if on both axes
    return collisionX && collisionY;
}  

我们检查第一个物体的最右侧是否大于第二个物体的最左侧并且第二个物体的最右侧是否大于第一个物体的最左侧;垂直的轴向与此相似。如果您无法顺利地将这一过程可视化,可以尝试在纸上画边界线/长方形来自行判断。

为更好地组织碰撞的代码我们在Game类中加入一个额外的函数

class Game
{
    public:
        [...]
        void DoCollisions();
};

我们可以使用DoCollisions来检查球与关卡中的砖块是否发生碰撞。如果检测到碰撞就将砖块的Destroyed属性设为true,此举会停止关卡中对此砖块的渲染。

void Game::DoCollisions()
{
    for (GameObject &box : this->Levels[this->Level].Bricks)
    {
        if (!box.Destroyed)
        {
            if (CheckCollision(*Ball, box))
            {
                if (!box.IsSolid)
                    box.Destroyed = GL_TRUE;
            }
        }
    }
}  

接下来我们需要更新Game的Update函数

void Game::Update(GLfloat dt)
{
    // Update objects
    Ball->Move(dt, this->Width);
    // Check for collisions
    this->DoCollisions();
}  

If we run the code now, the ball should detect collisions with each of the bricks and if the brick is not solid, the brick is destroyed. If you run the game now it'll look something like this: 此时如果我们运行代码,球会与每个砖块进行碰撞检测,如果砖块没有被填充为实体,则表示砖块被销毁。如果运行游戏以下是你会看到的:

AABB - Circle collision detection

由于球是一个圆形的物体AABB或许不是球的最佳碰撞外形。碰撞的代码中将球视为长方形框因此常常会出现球碰撞了砖块但此时球精灵还没有接触到砖块。

使用圆形碰撞外形而不是AABB来代表球会更合乎常理。因此我们在球对象中包含了Radius变量为了定义圆形碰撞外形我们需要的是一个位置矢量和一个半径。

这意味着我们不得不修改检测算法因为当前的算法只适用于两个AABB的碰撞。检测圆和AABB碰撞的算法会稍稍复杂关键点如下我们会找到AABB上距离圆最近的一个点如果圆到这一点的距离小于它的半径那么就产生了碰撞。

难点在于获取AABB上的最近点P¯P¯。下图展示了对于任意的AABB和圆我们如何计算该点

首先我们要获取球心C¯C¯与AABB中心B¯B¯的矢量差D¯D¯。接下来用AABB的半边长(half-extents)ww和h¯h¯来限制(clamp)矢量D¯D¯。长方形的半边长是指长方形的中心到它的边的距离简单的说就是它的尺寸除以2。这一过程返回的是一个总是位于AABB的边上的位置矢量除非圆心在AABB内部

限制运算把一个值限制在给定范围内,并返回限制后的值。通常可以表示为:

float clamp(float value, float min, float max) { return std::max(min, std::min(max, value)); }

例如,值42.0f被限制到6.0f3.0f之间会得到6.0f;而4.20f会被限制为4.20f。 限制一个2D的矢量表示将其xy分量都限制在给定的范围内。

这个限制后矢量P¯P¯就是AABB上距离圆最近的点。接下来我们需要做的就是计算一个新的差矢量D¯D¯它是圆心C¯C¯和P¯P¯的差矢量。

既然我们已经有了矢量D¯D¯我们就可以比较它的长度和圆的半径以判断是否发生了碰撞。

这一过程通过下边的代码来表示:

GLboolean CheckCollision(BallObject &one, GameObject &two) // AABB - Circle collision
{
    // Get center point circle first 
    glm::vec2 center(one.Position + one.Radius);
    // Calculate AABB info (center, half-extents)
    glm::vec2 aabb_half_extents(two.Size.x / 2, two.Size.y / 2);
    glm::vec2 aabb_center(
        two.Position.x + aabb_half_extents.x, 
        two.Position.y + aabb_half_extents.y
    );
    // Get difference vector between both centers
    glm::vec2 difference = center - aabb_center;
    glm::vec2 clamped = glm::clamp(difference, -aabb_half_extents, aabb_half_extents);
    // Add clamped value to AABB_center and we get the value of box closest to circle
    glm::vec2 closest = aabb_center + clamped;
    // Retrieve vector between center circle and closest point AABB and check if length <= radius
    difference = closest - center;
    return glm::length(difference) < one.Radius;
}      

我们创建了CheckCollision的一个重载函数用于专门处理一个BallObject和一个GameObject的情况。因为我们并没有在对象中保存碰撞外形的信息因此我们必须为其计算首先计算球心然后是AABB的半边长及中心。

Using these collision shape attributes we calculate vector D¯D¯ as difference that we then clamp to clamped and add to the AABB's center to get point P¯P¯ as closest. Then we calculate the difference vector D¯D¯ between center and closestand return whether the two shapes collided or not.

Since we previously called CheckCollision with the ball object as its first argument, we do not have to change any code since the overloaded variant of CheckCollision now automatically applies. The result is now a much more precise collision detection algorithm.

It seems to work, but still something is off. We properly do all the collision detection, but the ball does not react in any way to the collisions. We need to react to the collisions e.g. update the ball's position and/or velocity whenever a collision occurs. This is the topic of the next tutorial.