(译)LearnOpenGL实际案例Breakout:碰撞反馈
Posted 键盘春秋
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(译)LearnOpenGL实际案例Breakout:碰撞反馈相关的知识,希望对你有一定的参考价值。
1. 碰撞反馈
在教程的最后我们有一个运行的碰撞检测方案。然而,小球在碰撞检测中仍然没有任何反馈;它仅仅直接穿透了所有的砖块。我们想要小球在碰撞砖块时防水功能反弹。这个教程讨论了在AABB和圆形碰撞检测之间的碰撞反馈算法。
无论什么时候发生碰撞,我们需要发生两件事:我们需要重置小球这样它就不再进入其他对象,我们想要改变小球的方向和速度这样它看起来被对象弹开了。
碰撞重定位
将小球对象放置在在碰撞体AABB外我们必须算出小球穿透边框盒子的距离。为了达到这个目的,我们将再次图解分析之前的教程:
在这里小球稍微移动到了AABB里面并且检测到了碰撞。我们现在想要将小球移出这个形状这样它仅仅触碰了AABB就好像没发生碰撞。为了计算出我们需要将小球移出AABB多少我们需要检索穿透AABB的矢量R。为了获得这个矢量R我们从小球的半径减去矢量V。矢量V是最近点P和球心C之外的另一个矢量。
知道了R我们使用R来偏移小球的位置并且在AABB旁直接放置小球;小球现在被适当的放置了。
碰撞方向
下一步我们需要解决如何在碰撞后更新小球的速度。在Breakout中我们使用下列规则来改变小球的速度:
- 如果小球碰撞了AABB的左边或者右边,它沿着水平方向反弹。
- 如果小球碰撞了AABB的顶部或者底部,它沿着垂直方向反弹。
但是我们如何解决小球在AABB上的直接碰撞点?有几个处理这个问题的方法,它们中的一个是我们在每个砖块上使用四个放置在它每个边上的AABB来代替一个AABB。这种方法我们能够确定哪个AABB从而知道哪条边发生了碰撞。然而,一个简单的近似运算是使用点积。
你可能仍然记得在变换教程中点积在两个标准矢量之间给我们一个角度。加入我们定义四个矢量指示出北,南,西或东并且在它们和给定的矢量之间计算点积?结果是这四个向量和给定矢量之间的最大值(点积的最大值是1.0f,代表着0度角)代表着矢量的方向。
程序看起来是这样:
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
;
GLfloat max = 0.0f;
GLuint best_match = -1;
for (GLuint i = 0; i < 4; i++)
GLfloat dot_product = glm::dot(glm::normalize(target), compass[i]);
if (dot_product > max)
max = dot_product;
best_match = i;
return (Direction)best_match;
这个方法将目标和方向数组中的任何一个方向矢量进行比较。这个目标矢量指向最近的角度被返回给方法的调用者。在这里Direction是定义在game类头文件的枚举中的一个:
enum Direction
UP,
RIGHT,
DOWN,
LEFT
;
现在我们知道如何获得R向量以及如何确定小球碰撞AABB的方向,我们可以开始写碰撞反馈的代码。
AABB - Circle 碰撞反馈
为了计算出碰撞反馈的返回值我们在碰撞方法中需要比true或者false更多信息这样我们能够返回一个多元数信息,也就是说碰撞是否发生,向哪个方向发生以及差异矢量R。tuple包含在头文件中。
为了保持代码稍微有序我们将会定义碰撞反馈数据为Collision:
typedef std::tuple<GLboolean, Direction, glm::vec2> Collision;
然后我们还要改变CheckCollision方法的代码来让它不仅仅返回true或者false,还返回方向和差异矢量:
Collision CheckCollision(BallObject &one, GameObject &two) // AABB - AABB collision
[...]
if (glm::length(difference) <= one.Radius)
return std::make_tuple(GL_TRUE, VectorDirection(difference), difference);
else
return std::make_tuple(GL_FALSE, UP, glm::vec2(0, 0));
game的DoCollision方法现在不仅仅检查碰撞是否发生,还在碰撞发生时执行适当的行为。这个方法现在计算穿透的级别(在教程开始的图标中显示)并且基于方向和碰撞从小球的位置加或者减掉它。
void Game::DoCollisions()
for (GameObject &box : this->Levels[this->Level].Bricks)
if (!box.Destroyed)
Collision collision = CheckCollision(*Ball, box);
if (std::get<0>(collision)) // If collision is 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
Ball->Velocity.x = -Ball->Velocity.x; // Reverse horizontal velocity
// Relocate
GLfloat penetration = Ball->Radius - std::abs(diff_vector.x);
if (dir == LEFT)
Ball->Position.x += penetration; // Move ball to right
else
Ball->Position.x -= penetration; // Move ball to left;
else // Vertical collision
Ball->Velocity.y = -Ball->Velocity.y; // Reverse vertical velocity
// Relocate
GLfloat penetration = Ball->Radius - std::abs(diff_vector.y);
if (dir == UP)
Ball->Position.y -= penetration; // Move ball back up
else
Ball->Position.y += penetration; // Move ball back down
不要被这个方法的复杂性吓到因为他是基于到现在为止概念直接转化过来的。首先我们检查一个碰撞并且当碰撞体不是固体时我们销毁这个障碍。然后我们从tuple获得碰撞方向dir和矢量V为diff_vector并且最终执行碰撞反馈。
我们首先检查碰撞方向是水平的还是竖直的然后据此反射速度。如果是水平的,我们从diff_vector的x分量计算穿透值R并且从基于它的方向从的位置添加或者减去它。相同的操作应用到竖直碰撞,但是这次我们对所有矢量运行y分量。
运行你的程序现在会给你一个运行的碰撞方案,但是它可能很难看到真实的效果因为当你碰撞一个障碍物球将会碰撞到底边然后你会输掉游戏。我们能够通过添加玩家挡板碰撞来修复它。
2. 玩家 – 球碰撞
在小球和玩家之间的碰撞将会和我们之前讨论的稍微有一些不同,直到现在小球的水平速度将会基于中心到挡板的距离来更新。碰撞离挡板中心越远,水平速度的强度将会越强。
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;
Ball->Velocity.y = -Ball->Velocity.y;
Ball->Velocity = glm::normalize(Ball->Velocity) * glm::length(oldVelocity);
在我们在小球和每个砖块之间检查碰撞之后,我们检查小球是否碰撞到了玩家挡板。如果是(并且小球没有卡在挡板上)我们计算小球的中心和挡板的中心的距离与挡板半边长的百分比。小球的水平速度现在基于它从它中心撞击到挡板的距离进行更新。除此之外我们将会反射它的y速度。
注意旧的速度被存储为oldVelocity。存储旧速度的原因是当维持小球的y速度为常量的时候我们仅仅更新它的水平速度。这意味着矢量的速度不断发生改变,带来的影响是小球碰撞在档板的边界上比碰撞在档板的中心速度改变更大(因此更强)。因此新的速度矢量被标准化并且乘以旧速度的长度。这样,小球的速度就恒定不变,无论它撞击到档板的什么地方。
粘性档板
你可能也可能不会当你运行代码,这仍然有一个巨大的问题在玩家和小球碰撞反馈上。这个视频示范了这个问题。
这个问题叫做粘性档板问题,因为玩家挡板以高速向小球移动结果小球的中心最终位域挡板里面。由于我们不会解释小球的中心在AABB里面的位置,游戏尝试继续反馈所有的捧住哪个并且一旦他最终被释放掉他将会反射它的y速度因此它不确定它在释放之后会向上还是向下运动。
我们能够通过引入一个小技巧的简单的修复这个特性,这个小技巧是我们假定碰撞总是在档板上方。代替反射y速度我们通常简单的返回一个正y方向无论何时它被卡主,它将立即被释放。
//Ball->Velocity.y = -Ball->Velocity.y;
Ball->Velocity.y = -1 * abs(Ball->Velocity.y);
如果你足够努力尝试,这实际上仍然明显,但是我认为这是可以接受的。
底边
和经典Breakout之间仍然缺失的是一些重置关卡和玩家的失败条件。在game类的Update方法中我们想要检查小球是否到达底边,如果是,重置游戏。
void Game::Update(GLfloat dt)
[...]
if (Ball->Position.y >= this->Height) // Did ball reach bottom edge?
this->ResetLevel();
this->ResetPlayer();
ResetLevel和ResetPlayer方法简单重置了关卡并且重置了对象的值到它们的原始值。现在游戏看起来像这样。
总之你拥有它, 我们仅仅完成创建一个有着类似力学的经典Breakout游戏的拷贝。你能在这找到game类的源码:header, code.
转载请注明出处:http://blog.csdn.net/ylbs110/article/details/52830962
3. 一些备注
碰撞检测在视频游戏开发中是一个困难的主题并且可能是最有挑战性的。当下绝大多数游戏中所有的碰撞检测和反馈算法都和物理引擎结合起来。我们用在Breakout游戏中的碰撞算法是非常基础的算法并且专门用在这种类型的游戏中。
需要强调的是这种碰撞检测并不完美,它仅仅计算每一帧可能的碰撞并且仅仅只是他们在那个时间正好的位置。这意味着如果物体的速度能够在一帧穿过另一个物体,它将看起来没有碰撞到这个物体。所以如果发生掉帧或者你有足够的速度,这个碰撞检测算法就不好用了。
这几个问题仍然会发生:
- 如果小球跑得太快,它将在一帧中穿过一个物体,不会检测到碰撞
- 如果小球在一帧中碰撞到超过一个物体,它将检测到两个碰撞并且反射它的速度两次;不会影响他原来的速度
- 碰撞砖块的一个角会反射给小球错误的速度因为VectorDirection方法在一帧中只会返回一个竖直或者水平的方向
无论如何这些教程旨在教给读者物理和游戏开发的几个基础的方面。由于这个原因,碰撞算法的可理解性以及在普通环境下的效果很好让它满足这个需求。记住存在在几乎所有环境(包括可移动目标)下更好的(更复杂)的碰撞算法例如分离轴定理。
非常感激的是有大的,真实的并且常常非常高效的物理引擎(独立于时间帧的碰撞算法)被我们用在我们的游戏中。如果你希望更深入的研究这样的系统或者需要更先进的物理并且很难理解数学,Box2D是一个在你的游戏戏中实现物理和碰撞的完美的2D物理库。
以上是关于(译)LearnOpenGL实际案例Breakout:碰撞反馈的主要内容,如果未能解决你的问题,请参考以下文章
(译)LearnOpenGL实际案例Breakout:渲染文本
(译)LearnOpenGL实际案例Breakout:渲染文本