mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-22 20:25:28 +08:00
finish translation
This commit is contained in:
@@ -6,20 +6,20 @@
|
||||
| 翻译 | [aillieo](https://github.com/aillieo) |
|
||||
| 校对 | 暂无 |
|
||||
|
||||
当试图判断两个物体之间是否有碰撞发生时,我们通常不使用物体本身的数据,因为这些物体常常会很复杂,这将导致碰撞检测变得很复杂。正因这一点,使用**重叠**在物体上的更简单的外形(通常有较简单明确的数学定义)来进行碰撞检测成为常用的方法。我们基于这些简单的外形来检测碰撞,这样代码会变得更简单且节省了很多性能。这些碰撞外形例如圆、球体、长方形和立方体等,与拥有上百个三角形的网格相比简单了很多。
|
||||
当试图判断两个物体之间是否有碰撞发生时,我们通常不使用物体本身的数据,因为这些物体常常会很复杂,这将导致碰撞检测变得很复杂。正因这一点,使用**重叠**在物体上的更简单的外形(通常有较简单明确的数学定义)来进行碰撞检测成为常用的方法。我们基于这些简单的外形来检测碰撞,这样代码会变得更简单且节省了很多性能。这些<def>碰撞外形</def>例如圆、球体、长方形和立方体等,与拥有上百个三角形的网格相比简单了很多。
|
||||
|
||||
虽然它们确实提供了更简单更高效的碰撞检测算法,但这些简单的碰撞外形拥有一个共同的缺点,这些外形通常无法完全包裹物体。产生的影响就是当检测到碰撞时,实际的物体并没有真正的碰撞。必须记住的是这些外形仅仅是真实外形的近似。
|
||||
|
||||
|
||||
## AABB - AABB 碰撞
|
||||
|
||||
AABB代表的是与坐标轴对齐的边界框(bounding box),边界框是指与场景基础坐标轴(2D中的是x和y轴)对齐的长方形的碰撞外形。与坐标轴对齐意味着这个长方形没有经过旋转并且它的边线和场景中基础坐标轴平行(例如,左右边线和y轴平行)。这些边界框总是和场景的坐标轴平行,这使得所有的计算都变得更简单。下边是我们用一个AABB包裹一个球对象(物体):
|
||||
AABB代表的是<def>与坐标轴对齐的边界框(bounding box)</def>,边界框是指与场景基础坐标轴(2D中的是x和y轴)对齐的长方形的碰撞外形。与坐标轴对齐意味着这个长方形没有经过旋转并且它的边线和场景中基础坐标轴平行(例如,左右边线和y轴平行)。这些边界框总是和场景的坐标轴平行,这使得所有的计算都变得更简单。下边是我们用一个AABB包裹一个球对象(物体):
|
||||
|
||||

|
||||
|
||||
Breakout中几乎所有的物体都是基于长方形的物体,因此很理所应当地使用与坐标系对齐的边界框来进行碰撞检测。这就是我们接下来要做的。
|
||||
|
||||
有多种方式来定义与坐标轴对齐的边界框。其中一种定义AABB的方式是获取左上角点和右下角点的位置。我们定义的GameObject类已经包含了一个左上角点位置(它的Position vector)并且我们可以通过把左上角点的矢量加上它的尺寸(Position` +`Size)很容易地计算出右下角点。每个GameObject都包含一个AABB我们可以高效地使用它们碰撞。
|
||||
有多种方式来定义与坐标轴对齐的边界框。其中一种定义AABB的方式是获取左上角点和右下角点的位置。我们定义的<fun>GameObject</fun>类已经包含了一个左上角点位置(它的Position vector)并且我们可以通过把左上角点的矢量加上它的尺寸(<fun>Position</fun> + <fun>Size</fun>)很容易地计算出右下角点。每个<fun>GameObject</fun>都包含一个AABB我们可以高效地使用它们碰撞。
|
||||
|
||||
那么我们如何判断碰撞呢?当两个碰撞外形进入对方的区域时就会发生碰撞,例如定义了第一个物体的碰撞外形以某种形式进入了第二个物体的碰撞外形。对于AABB来说很容易判断,因为它们是与坐标轴对齐的:对于每个轴我们要检测两个物体的边界在此轴向是否有重叠。因此我们只是简单地检查两个物体的水平边界是否重合以及垂直边界是否重合。如果水平边界**和**垂直边界都有重叠那么我们就检测到一次碰撞。
|
||||
|
||||
@@ -28,26 +28,26 @@ Breakout中几乎所有的物体都是基于长方形的物体,因此很理所
|
||||
将这一概念转化为代码也是很直白的。我们对两个轴都检测是否重叠,如果都重叠就返回碰撞:
|
||||
|
||||
|
||||
```
|
||||
```c++
|
||||
GLboolean CheckCollision(GameObject &one, GameObject &two) // AABB - AABB collision
|
||||
{
|
||||
// Collision x-axis?
|
||||
// x轴方向碰撞?
|
||||
bool collisionX = one.Position.x + one.Size.x >= two.Position.x &&
|
||||
two.Position.x + two.Size.x >= one.Position.x;
|
||||
// Collision y-axis?
|
||||
// y轴方向碰撞?
|
||||
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类中加入一个额外的函数:
|
||||
为更好地组织碰撞的代码,我们在<fun>Game</fun>类中加入一个额外的函数:
|
||||
|
||||
|
||||
```
|
||||
```c++
|
||||
class Game
|
||||
{
|
||||
public:
|
||||
@@ -56,11 +56,9 @@ class Game
|
||||
};
|
||||
```
|
||||
|
||||
我们可以使用DoCollisions来检查球与关卡中的砖块是否发生碰撞。如果检测到碰撞,就将砖块的Destroyed属性设为`true`,此举会停止关卡中对此砖块的渲染。
|
||||
我们可以使用<fun>DoCollisions</fun>来检查球与关卡中的砖块是否发生碰撞。如果检测到碰撞,就将砖块的<fun>Destroyed</fun>属性设为<var>true</var>,此举会停止关卡中对此砖块的渲染。
|
||||
|
||||
|
||||
|
||||
```
|
||||
```c++
|
||||
void Game::DoCollisions()
|
||||
{
|
||||
for (GameObject &box : this->Levels[this->Level].Bricks)
|
||||
@@ -77,15 +75,15 @@ void Game::DoCollisions()
|
||||
}
|
||||
```
|
||||
|
||||
接下来我们需要更新Game的Update函数:
|
||||
接下来我们需要更新<fun>Game</fun>的<fun>Update</fun>函数:
|
||||
|
||||
|
||||
```
|
||||
```c++
|
||||
void Game::Update(GLfloat dt)
|
||||
{
|
||||
// Update objects
|
||||
// 更新对象
|
||||
Ball->Move(dt, this->Width);
|
||||
// Check for collisions
|
||||
// 检测碰撞
|
||||
this->DoCollisions();
|
||||
}
|
||||
```
|
||||
@@ -102,7 +100,7 @@ void Game::Update(GLfloat dt)
|
||||
|
||||

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

|
||||
|
||||
@@ -114,18 +112,23 @@ void Game::Update(GLfloat dt)
|
||||

|
||||
|
||||
|
||||
首先我们要获取球心\(\bar{C}\)与AABB中心\(\bar{B}\)的矢量差\(\bar{D}\)。接下来用AABB的半边长(half-extents)\(w\)和\(\bar{h}\)来限制(clamp)矢量\(\bar{D}\)。长方形的半边长是指长方形的中心到它的边的距离;简单的说就是它的尺寸除以2。这一过程返回的是一个总是位于AABB的边上的位置矢量(除非圆心在AABB内部)。
|
||||
首先我们要获取球心\(\bar{C}\)与AABB中心\(\bar{B}\)的矢量差\(\bar{D}\)。接下来用AABB的半边长(half-extents)\(w\)和\(\bar{h}\)来<def>限制(clamp)</def>矢量\(\bar{D}\)。长方形的半边长是指长方形的中心到它的边的距离;简单的说就是它的尺寸除以2。这一过程返回的是一个总是位于AABB的边上的位置矢量(除非圆心在AABB内部)。
|
||||
|
||||
|
||||
!!! Important
|
||||
|
||||
限制运算把一个值**限制**在给定范围内,并返回限制后的值。通常可以表示为:
|
||||
|
||||
`
|
||||
```
|
||||
float clamp(float value, float min, float max) {
|
||||
return std::max(min, std::min(max, value));
|
||||
return std::max(min, std::min(max, value));
|
||||
}
|
||||
`
|
||||
```
|
||||
|
||||
例如,值<var>42.0f</var>被限制到<var>6.0f</var>和<var>3.0f</var>之间会得到<var>6.0f</var>;而<var>4.20f</var>会被限制为<var>4.20f</var>。
|
||||
限制一个2D的矢量表示将其<var>x</var>和<var>y</var>分量都限制在给定的范围内。
|
||||
|
||||
|
||||
例如,值`42.0f`被限制到`6.0f`和`3.0f`之间会得到`6.0f`;而`4.20f`会被限制为`4.20f`。
|
||||
限制一个2D的矢量表示将其`x`和`y`分量都限制在给定的范围内。
|
||||
|
||||
这个限制后矢量\(\bar{P}\)就是AABB上距离圆最近的点。接下来我们需要做的就是计算一个新的差矢量\(\bar{D'}\),它是圆心\(\bar{C}\)和\(\bar{P}\)的差矢量。
|
||||
|
||||
@@ -137,34 +140,34 @@ float clamp(float value, float min, float max) {
|
||||
|
||||
这一过程通过下边的代码来表示:
|
||||
|
||||
```
|
||||
```c++
|
||||
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)
|
||||
// 计算AABB的信息(中心、半边长)
|
||||
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
|
||||
// AABB_center加上clamped这样就得到了边界框上距离圆最近的点closest
|
||||
glm::vec2 closest = aabb_center + clamped;
|
||||
// Retrieve vector between center circle and closest point AABB and check if length <= radius
|
||||
// 获得圆心center和最近点closest的矢量并判断是否 length <= radius
|
||||
difference = closest - center;
|
||||
return glm::length(difference) < one.Radius;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
我们创建了CheckCollision的一个重载函数用于专门处理一个BallObject和一个GameObject的情况。因为我们并没有在对象中保存碰撞外形的信息,因此我们必须为其计算:首先计算球心,然后是AABB的半边长及中心。
|
||||
我们创建了<fun>CheckCollision</fun>的一个重载函数用于专门处理一个<fun>BallObject</fun>和一个<fun>GameObject</fun>的情况。因为我们并没有在对象中保存碰撞外形的信息,因此我们必须为其计算:首先计算球心,然后是AABB的半边长及中心。
|
||||
|
||||
使用这些碰撞外形的参数,我们计算出矢量差\(\bar{D}\)然后得到限制后的值,并与AABB中心相加得到最近的点\(\bar{P}\)。然后计算出圆心和最近点的矢量差\(\bar{D'}\)并返回两个外形是否碰撞。
|
||||
使用这些碰撞外形的参数,我们计算出<var>difference</var>\(\bar{D}\)然后得到限制后的值<var>clamped</var>,并与AABB中心相加得到<var>closest</var>\(\bar{P}\)。然后计算出<var>center</var>和<var>closest</var>的矢量差\(\bar{D'}\)并返回两个外形是否碰撞。
|
||||
|
||||
之前我们调用CheckCollision时将球对象作为其第一个参数,因此现在CheckCollision的重载变量会自动生效,我们无需修改任何代码。现在的结果会比之前的碰撞检测算法更准确。
|
||||
之前我们调用<fun>CheckCollision</fun>时将球对象作为其第一个参数,因此现在<fun>CheckCollision</fun>的重载变量会自动生效,我们无需修改任何代码。现在的结果会比之前的碰撞检测算法更准确。
|
||||
|
||||
<video src="../../../../img/06/Breakout/05/02/collisions_circle.mp4" controls="controls"></video>
|
||||
|
||||
|
@@ -6,12 +6,11 @@
|
||||
| 翻译 | [aillieo](https://github.com/aillieo) |
|
||||
| 校对 | 暂无 |
|
||||
|
||||
上个教程的最后,我们得到了一种有效的碰撞检测方案。但是球对检测到的碰撞不会有反作用;它仅仅是径直穿过所有的砖块。我们希望球会从撞击到的砖块**反弹**。此教程将讨论如何使用AABB-圆碰撞方案实现这项称为碰撞处理 (collision resolution)的功能。
|
||||
上个教程的最后,我们得到了一种有效的碰撞检测方案。但是球对检测到的碰撞不会有反作用;它仅仅是径直穿过所有的砖块。我们希望球会从撞击到的砖块**反弹**。此教程将讨论如何使用AABB-圆碰撞方案实现这项称为<def>碰撞处理 (collision resolution)</def>的功能。
|
||||
|
||||
当碰撞发生时,我们希望出现两个现象:重新定位球,以免它进入另一个物体,其次是改变球的速度方向,使它看起来像是物体的反弹。
|
||||
|
||||
|
||||
### Collision repositioning
|
||||
### 碰撞重定位
|
||||
|
||||
为了把球对象定位到碰撞的AABB的外部,我们必须明确球侵入碰撞框的距离。为此我们要回顾上一节教程中的示意图:
|
||||
@@ -25,34 +24,33 @@
|
||||
有了\(\bar{R}\)之后我们将球的位置偏移\(\bar{R}\)就将球直接放置在与AABB紧邻的位置;此时球已经被重定位到合适的位置。
|
||||
|
||||
|
||||
### Collision direction
|
||||
### 碰撞方向
|
||||
|
||||
|
||||
下一步我们需要确定碰撞之后如何更新球的速度。对于Breakout我们使用以下规则来改变球的速度:
|
||||
|
||||
|
||||
1. 如果球撞击AABB的右侧或左侧,它的水平速度(`x`)将会反转。
|
||||
1. 如果球撞击AABB的右侧或左侧,它的水平速度(<var>x</var>)将会反转。
|
||||
|
||||
2. 如果球撞击AABB的上侧或下侧,它的垂直速度(`y`)将会反转。
|
||||
2. 如果球撞击AABB的上侧或下侧,它的垂直速度(<var>y</var>)将会反转。
|
||||
|
||||
|
||||
但是如何判断球撞击AABB的方向呢?解决这一问题有很多种方法,其中之一是对于每个砖块使用4个AABB而不是1个AABB,并把它们放置到砖块的每个边上。使用这种方法我们可以确定被碰撞的是哪个AABB和哪个边。但是有一种使用点乘(dot product)的更简单的方法。
|
||||
|
||||
您或许还记得[transformations](../../../01 Getting started/07 Transformations.md)教程中点乘可以得到两个正交化的矢量的夹角。如果我们定义指向北、南、西和东的四个矢量,然后计算它们和给定矢量的夹角会怎么样?由这四个方向矢量和给定的矢量点乘积的结果中的最高值(点乘积的最大值为`1.0f`,代表`0`度角)即是矢量的方向。
|
||||
您或许还记得[变换](../../../01 Getting started/07 Transformations.md)教程中点乘可以得到两个正交化的矢量的夹角。如果我们定义指向北、南、西和东的四个矢量,然后计算它们和给定矢量的夹角会怎么样?由这四个方向矢量和给定的矢量点乘积的结果中的最高值(点乘积的最大值为`1.0f`,代表`0`度角)即是矢量的方向。
|
||||
|
||||
|
||||
这一过程如下代码所示:
|
||||
|
||||
|
||||
```
|
||||
```c++
|
||||
Direction VectorDirection(glm::vec2 target)
|
||||
{
|
||||
glm::vec2 compass[] = {
|
||||
glm::vec2(0.0f, 1.0f), // up
|
||||
glm::vec2(1.0f, 0.0f), // right
|
||||
glm::vec2(0.0f, -1.0f), // down
|
||||
glm::vec2(-1.0f, 0.0f) // left
|
||||
glm::vec2(0.0f, 1.0f), // 上
|
||||
glm::vec2(1.0f, 0.0f), // 右
|
||||
glm::vec2(0.0f, -1.0f), // 下
|
||||
glm::vec2(-1.0f, 0.0f) // 左
|
||||
};
|
||||
GLfloat max = 0.0f;
|
||||
GLuint best_match = -1;
|
||||
@@ -70,11 +68,9 @@ Direction VectorDirection(glm::vec2 target)
|
||||
```
|
||||
|
||||
|
||||
此函数比较了target矢量和compass数组中各方向矢量。compass数组中与target角度最接近的矢量,即是返回给函数调用者的Direction。这里的Direction是一个game类的头文件中定义的枚举类型:
|
||||
此函数比较了<var>target</var>矢量和<var>compass</var>数组中各方向矢量。<var>compass</var>数组中与<var>target</var>角度最接近的矢量,即是返回给函数调用者的<var>Direction</var>。这里的<var>Direction</var>是一个<fun>Game</fun>类的头文件中定义的枚举类型:
|
||||
|
||||
|
||||
|
||||
```
|
||||
```c++
|
||||
enum Direction {
|
||||
UP,
|
||||
RIGHT,
|
||||
@@ -87,24 +83,22 @@ enum Direction {
|
||||
既然我们已经知道了如何获得\(\bar{R}\)以及如何判断球撞击AABB的方向,我们开始编写碰撞处理的代码。
|
||||
|
||||
|
||||
### AABB - Circle collision resolution
|
||||
### AABB - 圆碰撞检测
|
||||
|
||||
|
||||
为了计算碰撞处理所需的数值我们要从碰撞的函数中获取更多的信息而不只只是一个`true`或`false`,因此我们要返回一个包含更多信息的tuple,这些信息即是碰撞发生时的方向及差矢量(\(\bar{R}\))。你可以在头文件`<tuple>`中找到`tuple`。
|
||||
为了计算碰撞处理所需的数值我们要从碰撞的函数中获取更多的信息而不只只是一个<var>true</var>或<var>false</var>,因此我们要返回一个包含更多信息的<def>tuple</def>,这些信息即是碰撞发生时的方向及差矢量(\(\bar{R}\))。你可以在头文件<var><tuple></var>中找到<var>tuple</var>。
|
||||
|
||||
|
||||
为了更好组织代码,我们把碰撞相关的数据使用typedef定义为Collision:
|
||||
为了更好组织代码,我们把碰撞相关的数据使用typedef定义为<fun>Collision</fun>:
|
||||
|
||||
```
|
||||
```c++
|
||||
typedef std::tuple<GLboolean, Direction, glm::vec2> Collision;
|
||||
```
|
||||
|
||||
接下来我们还需要修改CheckCollision函数的代码,使其不仅仅返回`true`或`false`而是还包含方向和差矢量:
|
||||
接下来我们还需要修改<fun>CheckCollision</fun>函数的代码,使其不仅仅返回<var>true</var>或<var>false</var>而是还包含方向和差矢量:
|
||||
|
||||
|
||||
|
||||
```
|
||||
Collision CheckCollision(BallObject &one, GameObject &two) // AABB - AABB collision
|
||||
```c++
|
||||
Collision CheckCollision(BallObject &one, GameObject &two) // AABB - AABB 碰撞
|
||||
{
|
||||
[...]
|
||||
if (glm::length(difference) <= one.Radius)
|
||||
@@ -114,12 +108,10 @@ Collision CheckCollision(BallObject &one, GameObject &two) // AABB - AABB collis
|
||||
}
|
||||
```
|
||||
|
||||
<fun>Game</fun>类的<fun>DoCollision</fun>函数现在不仅仅只检测是否出现了碰撞,而且在碰撞发生时会有适当的动作。此函数现在会计算碰撞侵入的程度(如本教程一开始计时的示意图中所示)并且基于碰撞方向使球的位置矢量与其相加或相减。
|
||||
|
||||
|
||||
Game类的DoCollision函数现在不仅仅只检测是否出现了碰撞,而且在碰撞发生时会有适当的动作。此函数现在会计算碰撞侵入的程度(如本教程一开始计时的示意图中所示)并且基于碰撞方向使球的位置矢量与其相加或相减。
|
||||
|
||||
|
||||
```
|
||||
```c++
|
||||
void Game::DoCollisions()
|
||||
{
|
||||
for (GameObject &box : this->Levels[this->Level].Bricks)
|
||||
@@ -127,33 +119,33 @@ void Game::DoCollisions()
|
||||
if (!box.Destroyed)
|
||||
{
|
||||
Collision collision = CheckCollision(*Ball, box);
|
||||
if (std::get<0>(collision)) // If collision is true
|
||||
if (std::get<0>(collision)) // 如果collision 是 true
|
||||
{
|
||||
// Destroy block if not solid
|
||||
// 如果砖块不是实心就销毁砖块
|
||||
if (!box.IsSolid)
|
||||
box.Destroyed = GL_TRUE;
|
||||
// Collision resolution
|
||||
// 碰撞处理
|
||||
Direction dir = std::get<1>(collision);
|
||||
glm::vec2 diff_vector = std::get<2>(collision);
|
||||
if (dir == LEFT || dir == RIGHT) // Horizontal collision
|
||||
if (dir == LEFT || dir == RIGHT) // 水平方向碰撞
|
||||
{
|
||||
Ball->Velocity.x = -Ball->Velocity.x; // Reverse horizontal velocity
|
||||
// Relocate
|
||||
Ball->Velocity.x = -Ball->Velocity.x; // 反转水平速度
|
||||
// 重定位
|
||||
GLfloat penetration = Ball->Radius - std::abs(diff_vector.x);
|
||||
if (dir == LEFT)
|
||||
Ball->Position.x += penetration; // Move ball to right
|
||||
Ball->Position.x += penetration; // 将球右移
|
||||
else
|
||||
Ball->Position.x -= penetration; // Move ball to left;
|
||||
Ball->Position.x -= penetration; // 将球左移
|
||||
}
|
||||
else // Vertical collision
|
||||
else // 垂直方向碰撞
|
||||
{
|
||||
Ball->Velocity.y = -Ball->Velocity.y; // Reverse vertical velocity
|
||||
// Relocate
|
||||
Ball->Velocity.y = -Ball->Velocity.y; // 反转垂直速度
|
||||
// 重定位
|
||||
GLfloat penetration = Ball->Radius - std::abs(diff_vector.y);
|
||||
if (dir == UP)
|
||||
Ball->Position.y -= penetration; // Move ball back up
|
||||
Ball->Position.y -= penetration; // 将球上移
|
||||
else
|
||||
Ball->Position.y += penetration; // Move ball back down
|
||||
Ball->Position.y += penetration; // 将球下移
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,34 +153,32 @@ void Game::DoCollisions()
|
||||
}
|
||||
```
|
||||
|
||||
不要被函数的复杂度给吓到,因为它仅仅是我们目前为止的概念的直接转化。首先我们会检测碰撞如果发生了碰撞且砖块不是实心的那么就销毁砖块。然后我们从tuple中获取到了碰撞的方向dir以及表示\(\bar{V}\)的差矢量diff_vector,最终完成碰撞处理。
|
||||
不要被函数的复杂度给吓到,因为它仅仅是我们目前为止的概念的直接转化。首先我们会检测碰撞如果发生了碰撞且砖块不是实心的那么就销毁砖块。然后我们从tuple中获取到了碰撞的方向<var>dir</var>以及表示\(\bar{V}\)的差矢量<var>diff_vector</var>,最终完成碰撞处理。
|
||||
|
||||
|
||||
我们首先检查碰撞方向是水平还是垂直,并据此反转速度。如果是水平方向,我们从diff_vector的x分量计算侵入量RR并根据碰撞方向用球的位置矢量加上或减去它。垂直方向的碰撞也是如此,但是我们要操作各矢量的y分量。
|
||||
我们首先检查碰撞方向是水平还是垂直,并据此反转速度。如果是水平方向,我们从<var>diff_vector</var>的x分量计算侵入量RR并根据碰撞方向用球的位置矢量加上或减去它。垂直方向的碰撞也是如此,但是我们要操作各矢量的y分量。
|
||||
|
||||
|
||||
现在运行你的应用程序,应该会向你展示一套奏效的碰撞方案,但可能会很难真正看到它的效果,因为一旦球碰撞到了一个砖块就会弹向底部并永远丢失。我们可以通过处理玩家挡板的碰撞来修复这一问题。
|
||||
|
||||
|
||||
|
||||
## Player - ball collisions
|
||||
## 玩家 - 球碰撞
|
||||
|
||||
|
||||
球和玩家之间的碰撞与我们之前讨论的碰撞稍有不同,因为这里应当基于撞击挡板的点与(挡板)中心的距离来改变球的水平速度。撞击点距离挡板的中心点越远,则水平方向的速度就会越大。
|
||||
|
||||
|
||||
```
|
||||
```c++
|
||||
void Game::DoCollisions()
|
||||
{
|
||||
[...]
|
||||
Collision result = CheckCollision(*Ball, *Player);
|
||||
if (!Ball->Stuck && std::get<0>(result))
|
||||
{
|
||||
// Check where it hit the board, and change velocity based on where it hit the board
|
||||
// 检查碰到了挡板的哪个位置,并根据碰到哪个位置来改变速度
|
||||
GLfloat centerBoard = Player->Position.x + Player->Size.x / 2;
|
||||
GLfloat distance = (Ball->Position.x + Ball->Radius) - centerBoard;
|
||||
GLfloat percentage = distance / (Player->Size.x / 2);
|
||||
// Then move accordingly
|
||||
// 依据结果移动
|
||||
GLfloat strength = 2.0f;
|
||||
glm::vec2 oldVelocity = Ball->Velocity;
|
||||
Ball->Velocity.x = INITIAL_BALL_VELOCITY.x * percentage * strength;
|
||||
@@ -201,10 +191,10 @@ void Game::DoCollisions()
|
||||
|
||||
在我们完成了球和各砖块的碰撞检测之后,我们来检测球和玩家挡板是否发生碰撞。如果有碰撞(并且球不是被固定在挡板上)我们要计算球的中心与挡板中心的距离和挡板的半边长的百分比。之后球的水平速度会依据它撞击挡板的点到挡板中心的距离来更新。除了更新水平速度之外我们还需要反转它的y方向速度。
|
||||
|
||||
注意旧的速度被存储为oldVelocity。之所以要存储旧的速度是因为我们只更新球的速度矢量中水平方向的速度并保持它的y速度不变。这将意味着矢量的长度会持续变化,其产生的影响是如果球撞击到挡板的边缘则会比撞击到挡板中心有更大(也因此更强)的速度矢量。为此新的速度矢量会正交化然后乘以旧速度矢量的长度。这样一来,球的力量和速度将总是一一致的,无论它撞击到挡板的哪个地方。
|
||||
注意旧的速度被存储为<var>oldVelocity</var>。之所以要存储旧的速度是因为我们只更新球的速度矢量中水平方向的速度并保持它的y速度不变。这将意味着矢量的长度会持续变化,其产生的影响是如果球撞击到挡板的边缘则会比撞击到挡板中心有更大(也因此更强)的速度矢量。为此新的速度矢量会正交化然后乘以旧速度矢量的长度。这样一来,球的力量和速度将总是一一致的,无论它撞击到挡板的哪个地方。
|
||||
|
||||
|
||||
### Sticky paddle
|
||||
### 粘板
|
||||
|
||||
|
||||
无论你有没有注意到,但当运行代码时,球和玩家挡板的碰撞处理仍旧有一个大问题。以下的视频清楚地展示了将会出现的现象:
|
||||
@@ -212,13 +202,13 @@ void Game::DoCollisions()
|
||||
<video src="../../../../img/06/Breakout/05/03/collisions_sticky_paddle.mp4" controls="controls"></video>
|
||||
|
||||
|
||||
这种问题称为粘板问题(sticky paddle issue),出现的原因是玩家挡板以较高的速度移向球,导致球的中心进入玩家挡板。由于我们没有考虑球的中心在AABB内部的情况,游戏会持续试图对所有的碰撞做出响应,当球最终脱离时,已经对`y`向速度翻转了多次,以至于无法确定球在脱离后是向上还是向下运动。
|
||||
这种问题称为<def>粘板问题(sticky paddle issue)</def>,出现的原因是玩家挡板以较高的速度移向球,导致球的中心进入玩家挡板。由于我们没有考虑球的中心在AABB内部的情况,游戏会持续试图对所有的碰撞做出响应,当球最终脱离时,已经对`y`向速度翻转了多次,以至于无法确定球在脱离后是向上还是向下运动。
|
||||
|
||||
|
||||
我们可以引入一个小的特殊处理来很容易地修复这种行为,这个处理之所以成为可能是基于我们可以假设碰撞总是发生在挡板顶部的事实。我们总是简单地返回正的`y`速度而不是反转`y`速度,这样当它被卡住时也可以立即脱离。
|
||||
我们可以引入一个小的特殊处理来很容易地修复这种行为,这个处理之所以成为可能是基于我们可以假设碰撞总是发生在挡板顶部的事实。我们总是简单地返回正的<var>y</var>速度而不是反转<var>y</var>速度,这样当它被卡住时也可以立即脱离。
|
||||
|
||||
|
||||
```
|
||||
```c++
|
||||
//Ball->Velocity.y = -Ball->Velocity.y;
|
||||
Ball->Velocity.y = -1 * abs(Ball->Velocity.y);
|
||||
```
|
||||
@@ -228,14 +218,14 @@ Ball->Velocity.y = -1 * abs(Ball->Velocity.y);
|
||||
|
||||
### 底部边界
|
||||
|
||||
与经典的Breakout内容相比唯一缺少的就是失败条件了,失败会重置关卡和玩家。在Game类的Update函数中,我们要检查球是否接触到了底部边界,如果接触到就重置游戏。
|
||||
与经典的Breakout内容相比唯一缺少的就是失败条件了,失败会重置关卡和玩家。在<fun>Game</fun>类的<fun>Update</fun>函数中,我们要检查球是否接触到了底部边界,如果接触到就重置游戏。
|
||||
|
||||
|
||||
```
|
||||
```c++
|
||||
void Game::Update(GLfloat dt)
|
||||
{
|
||||
[...]
|
||||
if (Ball->Position.y >= this->Height) // Did ball reach bottom edge?
|
||||
if (Ball->Position.y >= this->Height) // 球是否接触底部边界?
|
||||
{
|
||||
this->ResetLevel();
|
||||
this->ResetPlayer();
|
||||
@@ -244,16 +234,14 @@ void Game::Update(GLfloat dt)
|
||||
```
|
||||
|
||||
|
||||
ResetLevel和ResetPlayer函数直接重新加载关卡并重置对象的各变量值为原始的值。现在游戏看起来应该是这样的:
|
||||
<fun>ResetLevel</fun>和<fun>ResetPlayer</fun>函数直接重新加载关卡并重置对象的各变量值为原始的值。现在游戏看起来应该是这样的:
|
||||
|
||||
<video src="../../../../img/06/Breakout/05/03/collisions_complete.mp4" controls="controls"></video>
|
||||
|
||||
就是这样了,我们创建完成了一个有相似机制的经典Breakout游戏的复制版。这里你可以找到Game类的源代码:[header](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_collisions.h),[code](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_collisions)。
|
||||
|
||||
## A few notes
|
||||
## 一些注意事项
|
||||
|
||||
|
||||
在视频游戏的发展过程中,碰撞检测是一个困难的话题甚至可能是最大的挑战。大多数的碰撞检测和处理方案是和物理引擎合并在一起的,正如多数现代的游戏中看到的那样。我们在Breakout游戏中使用的碰撞方案是一个非常简单的方案并且是专门给这类游戏所专用的。
|
||||
|
||||
|
||||
@@ -268,10 +256,10 @@ ResetLevel和ResetPlayer函数直接重新加载关卡并重置对象的各变
|
||||
- 如果球在一帧内同时撞击了一个以上的物体,它将会检测到两次碰撞并两次反转速度;这样不改变它的原始速度。
|
||||
|
||||
|
||||
- 撞击到砖块的角时会在错误的方向反转速度,这是因为它在一帧内穿过的距离会引发VectorDirection返回水平方向还是垂直方向的差别。
|
||||
- 撞击到砖块的角时会在错误的方向反转速度,这是因为它在一帧内穿过的距离会引发<fun>VectorDirection</fun>返回水平方向还是垂直方向的差别。
|
||||
|
||||
|
||||
但是,本教程目的在于教会读者们图形学和游戏开发的基础知识。因此,这里的碰撞方案可以服务于此目的;它更容易理解且在正常的场景中可以较好地运作。需要记住的是存在有更好的(更复杂)碰撞方案,在几乎所有的场景中都可以很好地运作(包括可移动的物体)如分离轴定理(separating axis theorem)。
|
||||
但是,本教程目的在于教会读者们图形学和游戏开发的基础知识。因此,这里的碰撞方案可以服务于此目的;它更容易理解且在正常的场景中可以较好地运作。需要记住的是存在有更好的(更复杂)碰撞方案,在几乎所有的场景中都可以很好地运作(包括可移动的物体)如<def>分离轴定理(separating axis theorem)</def>。
|
||||
|
||||
|
||||
值得庆幸的是,有大量实用并且常常很高效的物理引擎(使用时间步无关的碰撞方案)可供您在游戏中使用。如果您希望在这一系统中有更深入的探索或需要更高级的物理系统又不理解其中的数学机理,[Box2D](http://box2d.org/about/)是一个实现了物理系统和碰撞检测的可以用在您的应用程序中的完美的2D物理库。
|
||||
|
Reference in New Issue
Block a user