这是libgdx教程的第四部分,在该教程中,我们创建了一个以Star Guard为例的2d平台游戏原型。 如果您对我们如何到达这里感兴趣,可以阅读以前的文章。
到目前为止,在完成本教程之后,我们设法拥有一个由一些街区组成的小世界,我们的英雄叫鲍勃,他可以很好地移动,但是问题是,他
与世界没有任何互动。 如果将瓷砖渲染切换回去,我们将看到鲍勃愉快地走来走去而没有障碍物阻碍他。 所有的块都会被忽略。 发生这种情况是因为我们从不检查Bob实际是否与块碰撞。 碰撞检测仅是检测两个或多个对象何时碰撞。 在我们的案例中,我们需要检测 Bob何时与块 碰撞 。 确切要检查的是Bob的边界框是否与各自块的边界框相交。 如果确实如此,我们已经检测到碰撞 。 我们注意到对象(鲍勃和块)并采取相应的行动。 在我们的案例中,我们需要阻止Bob前进,下降或跳跃,具体取决于Bob撞到块的哪一侧。
快速而肮脏的方式
一种简单快捷的方法是遍历世界上所有的块,并检查这些块是否与Bob当前的边界框发生碰撞。 这在我们很小的10×7的世界中效果很好,但是如果我们有一个拥有成千上万个块的巨大世界,那么在不影响性能的情况下进行每个帧的检测将变得不可能。
更好的方法
为了优化上述解决方案,我们将选择性地选择可能与Bob碰撞的图块。
按照设计,游戏世界由块组成,这些块的边界框沿轴向对齐,并且其宽度和高度均为1个单位。
在这种情况下,我们的世界如下图所示(所有块/平铺块都在单位块中):
红色方块代表将放置块的边界(如果有)。 黄色的被放置块。
现在,我们可以为我们的世界选择一个简单的二维数组(矩阵),每个单元将包含一个Block
或null
如果没有)。 这是地图容器。 我们总是知道Bob在哪里,因此很容易确定我们在哪个单元格中。 获取Bob可能与之碰撞的候选块的简便方法是:拾取所有周围的单元格,并检查Bob的当前边界框是否与其中一个具有块的图块重叠。
因为我们还控制Bob的运动,所以我们可以了解他的方向和运动速度。 这进一步缩小了我们的选择范围。 例如,如果鲍勃(Bob)离开,我们将遇到以下情况:
上图为我们提供了2个候选单元格(平铺),以检查这些单元格中的对象是否与Bob发生碰撞。 请记住,重力不断将Bob拉下,因此我们将始终必须检查Y轴上的图块。 根据垂直速度的符号,我们知道Bob何时跳跃或下降。 如果Bob跳跃,则候选人将是他上方的图块(单元)。 垂直速度为负数表示鲍勃正在跌落,因此我们从他下方选砖作为候选。 如果他向左行驶(他的速度<0),那么我们在他的左边选择候选人。 如果他向右行驶(速度> 0),那么我们在他的右边选择瓷砖。 如果水平速度为0,则意味着我们不需要打扰水平候选者。 我们需要使其达到最佳状态,因为我们将在每个帧中都执行此操作,并且必须对每个敌人,子弹以及游戏中任何可碰撞的实体执行此操作。
碰撞后会发生什么?
在我们的情况下,这非常简单。 鲍勃在该轴上的移动停止了。 他在该轴上的速度将设置为0。仅在单独检查2轴时才能执行此操作。 我们将首先检查水平碰撞,如果鲍勃碰撞了,那么我们将停止他的水平运动。
我们在垂直(Y)轴上执行完全相同的操作。 就这么简单。
先模拟然后渲染
我们在检查碰撞时需要小心。 我们人类倾向于在行动之前思考。 如果我们正面对一堵墙,我们不仅会走进去,还会看到并估计距离,然后在撞墙之前停下来。 想象一下,如果你是盲人。 您将需要与眼睛不同的传感器。 您会用胳膊伸手,如果您感觉到墙壁,则在进入墙壁之前会停下来。 我们可以将其翻译为Bob,但我们将使用他的边界框代替他的手臂。 首先,我们将其边界框沿Bob的移动距离移动至X轴,然后检查新位置是否会碰到墙(如果边界框与块的边界框相交)。 如果是,则检测到冲突。 鲍勃可能离墙壁有一段距离,在那个框架中,他将覆盖到墙壁的距离等等。 如果是这种情况,我们只需将Bob放在墙壁旁边,然后将其边界框与当前位置对齐。 我们还将鲍勃的速度在该轴上设置为0。 下图是尝试显示我所描述的内容。
绿框是Bob当前所在的位置。 移位的蓝色框是Bob在此框之后的位置。 紫色是鲍勃进入墙壁的数量。 那是我们需要推开Bob的距离,以便他站在墙旁边。 我们只需将他的位置设置在墙旁边即可实现此目标,而无需进行过多的计算。 冲突检测的代码实际上非常简单。 所有这些都位于BobController.java
。 我还要在控制器之前提到其他一些更改。 World.java
具有以下更改:
public class World {
/** Our player controlled hero **/
Bob bob;
/** A world has a level through which Bob needs to go through **/
Level level;
/** The collision boxes **/
Array<Rectangle> collisionRects = new Array<Rectangle>();
// Getters -----------
public Array<Rectangle> getCollisionRects() {
return collisionRects;
}
public Bob getBob() {
return bob;
}
public Level getLevel() {
return level;
}
/** Return only the blocks that need to be drawn **/
public List<Block> getDrawableBlocks(int width, int height) {
int x = (int)bob.getPosition().x - width;
int y = (int)bob.getPosition().y - height;
if (x < 0) {
x = 0;
}
if (y < 0) {
y = 0;
}
int x2 = x + 2 * width;
int y2 = y + 2 * height;
if (x2 > level.getWidth()) {
x2 = level.getWidth() - 1;
}
if (y2 > level.getHeight()) {
y2 = level.getHeight() - 1;
}
List<Block> blocks = new ArrayList<Block>();
Block block;
for (int col = x; col <= x2; col++) {
for (int row = y; row <= y2; row++) {
block = level.getBlocks()[col][row];
if (block != null) {
blocks.add(block);
}
}
}
return blocks;
}
// --------------------
public World() {
createDemoWorld();
}
private void createDemoWorld() {
bob = new Bob(new Vector2(7, 2));
level = new Level();
}
}
#09 – collisionRects
只是一个简单的数组,我将Bob碰撞的矩形放在该特定帧中。 这仅用于调试目的,并在屏幕上显示框。 它可以并且将从最终游戏中删除。
#13 –仅提供对碰撞箱的访问
#23 – getDrawableBlocks(int width, int height)
是一种方法,该方法返回相机窗口中将被渲染的Block
对象的列表。 该方法只是准备应用程序以渲染巨大的世界而不会降低性能。 这是一个非常简单的算法。 使鲍勃周围的块在一定距离内,然后将其返回进行渲染。 这是一种优化。 #61 –创建在#06行中声明的Level
。 我们希望我们的游戏拥有多个关卡,因此最好将关卡从世界上移开。 这是显而易见的第一步。 Level.java
可以在这里找到。
如前所述,实际的冲突检测在BobController.java
public class BobController {
// ... code omitted ... //
private Array<Block> collidable = new Array<Block>();
// ... code omitted ... //
public void update(float delta) {
processInput();
if (grounded && bob.getState().equals(State.JUMPING)) {
bob.setState(State.IDLE);
}
bob.getAcceleration().y = GRAVITY;
bob.getAcceleration().mul(delta);
bob.getVelocity().add(bob.getAcceleration().x, bob.getAcceleration().y);
checkCollisionWithBlocks(delta);
bob.getVelocity().x *= DAMP;
if (bob.getVelocity().x > MAX_VEL) {
bob.getVelocity().x = MAX_VEL;
}
if (bob.getVelocity().x < -MAX_VEL) {
bob.getVelocity().x = -MAX_VEL;
}
bob.update(delta);
}
private void checkCollisionWithBlocks(float delta) {
bob.getVelocity().mul(delta);
Rectangle bobRect = rectPool.obtain();
bobRect.set(bob.getBounds().x, bob.getBounds().y, bob.getBounds().width, bob.getBounds().height);
int startX, endX;
int startY = (int) bob.getBounds().y;
int endY = (int) (bob.getBounds().y + bob.getBounds().height);
if (bob.getVelocity().x < 0) {
startX = endX = (int) Math.floor(bob.getBounds().x + bob.getVelocity().x);
} else {
startX = endX = (int) Math.floor(bob.getBounds().x + bob.getBounds().width + bob.getVelocity().x);
}
populateCollidableBlocks(startX, startY, endX, endY);
bobRect.x += bob.getVelocity().x;
world.getCollisionRects().clear();
for (Block block : collidable) {
if (block == null) continue;
if (bobRect.overlaps(block.getBounds())) {
bob.getVelocity().x = 0;
world.getCollisionRects().add(block.getBounds());
break;
}
}
bobRect.x = bob.getPosition().x;
startX = (int) bob.getBounds().x;
endX = (int) (bob.getBounds().x + bob.getBounds().width);
if (bob.getVelocity().y < 0) {
startY = endY = (int) Math.floor(bob.getBounds().y + bob.getVelocity().y);
} else {
startY = endY = (int) Math.floor(bob.getBounds().y + bob.getBounds().height + bob.getVelocity().y);
}
populateCollidableBlocks(startX, startY, endX, endY);
bobRect.y += bob.getVelocity().y;
for (Block block : collidable) {
if (block == null) continue;
if (bobRect.overlaps(block.getBounds())) {
if (bob.getVelocity().y < 0) {
grounded = true;
}
bob.getVelocity().y = 0;
world.getCollisionRects().add(block.getBounds());
break;
}
}
bobRect.y = bob.getPosition().y;
bob.getPosition().add(bob.getVelocity());
bob.getBounds().x = bob.getPosition().x;
bob.getBounds().y = bob.getPosition().y;
bob.getVelocity().mul(1 / delta);
}
private void populateCollidableBlocks(int startX, int startY, int endX, int endY) {
collidable.clear();
for (int x = startX; x <= endX; x++) {
for (int y = startY; y <= endY; y++) {
if (x >= 0 && x < world.getLevel().getWidth() && y >=0 && y < world.getLevel().getHeight()) {
collidable.add(world.getLevel().get(x, y));
}
}
}
}
// ... code omitted ... //
}
完整的源代码位于github上,我已经尝试对其进行了文档记录,但在这里我将详细介绍这些重要的内容。
#03 –可collidable
数组将在每个帧中保留与Bob碰撞的候选块。
现在, update
方法更加简洁。
#07 –像往常一样处理输入内容 #08 –#09 –如果Bob不在空中,则重置他的状态。 #12 –鲍勃的加速度转化为帧时间。 这很重要,因为帧可能非常小(通常为1/60秒),并且我们只想在一帧中进行一次转换。 #13 –计算帧时的速度 #14 –高亮显示,因为这是发生碰撞检测的地方。 我将稍后介绍该方法。 #15 –#22 –将DAMP应用于Bob阻止他,并确保Bob不超过其最大速度。 #25 – checkCollisionWithBlocks(float delta)
方法,它根据Bob与关卡中的块是否碰撞来设置Bob的状态,位置和其他参数。 #26 –将速度转换为帧时间 #27 –#28 –我们使用Pool获取Rectangle,它是Bob当前边界框的副本。 该矩形将在bob应为该帧的位置处移动,并与候选块进行检查。 #29 –#36 –这些行标识了要检查碰撞的水平矩阵中的开始和结束坐标。 水平矩阵只是二维数组,每个单元代表一个单位,因此可以容纳一个块。 检查Level.java
#31 –设置了Y坐标,因为我们现在仅查找水平。 #32 –检查鲍勃是否在向左行驶,如果是,它将在他的左边识别出瓷砖。 数学运算很简单,我使用了这种方法,因此,如果我决定需要对单元格进行其他一些测量,那么仍然可以使用。 #37 –使用所提供范围内的块填充可collidable
数组。 在这种情况下,取决于Bob的方位,左边还是右边的砖块。 还要注意,如果该单元格中没有块,则结果为null。 #38 –这是我们放置Bob边界框副本的位置。 bobRec
的新职位是Bob在正常情况下应处于的位置。 但仅在X轴上。 #39 –还记得来自世界各地的冲撞人进行调试吗? 现在,我们清除该数组,以便可以用Bob碰撞的矩形填充它。 #40 –#47 –在X轴上实际发生碰撞检测的位置。 我们遍历所有候选块(在我们的示例中为1),并检查块的边界框是否与Bob的位移边界框相交。 我们使用bobRect.overlaps
方法,该方法是bobRect.overlaps
中Rectangle类的一部分,如果两个矩形重叠,则返回true。 如果存在重叠,则会发生碰撞,因此我们将Bob的速度设置为0( 第43行),将矩形添加到world.collisionRects
并中断检测。 #48 –我们重置边界框的位置,因为我们正在移动以检查X轴在Y轴上的碰撞。 #49 –#68 –与以前完全相同,但它发生在Y轴上。 还有一条附加的指令#61-#63 ,如果在Bob跌倒时检测到碰撞,则将grounded
状态设置为true
。 #69 –重置鲍勃的矩形副本 #70 –正在设置鲍勃的新速度,该速度将用于计算鲍勃的新位置。 #71 –#72 – Bob的真实边界位置已更新 #73 –我们将速度转换回基本测量单位。 这个非常重要。
这就是鲍勃与瓷砖的碰撞。 当然,随着添加更多实体,我们将对此进行改进,但目前为止已经足够了。 我们在这里作了一些欺骗,就像我说的那样,在碰撞时我将Bob放在Block旁边,但是在代码中我完全忽略了替换。 因为距离太小了,我们什至看不到,所以可以。 可以添加它,不会有太大区别。 如果您决定添加它,确保务必毗邻未来 Bob的位置来将挡,一点点远,因此重叠功能会导致
false
。 WorldRenderer.java
也有少量添加。
public class WorldRenderer {
// ... code omitted ... //
public void render() {
spriteBatch.begin();
drawBlocks();
drawBob();
spriteBatch.end();
drawCollisionBlocks();
if (debug)
drawDebug();
}
private void drawCollisionBlocks() {
debugRenderer.setProjectionMatrix(cam.combined);
debugRenderer.begin(ShapeType.FilledRectangle);
debugRenderer.setColor(new Color(1, 1, 1, 1));
for (Rectangle rect : world.getCollisionRects()) {
debugRenderer.filledRect(rect.x, rect.y, rect.width, rect.height);
}
debugRenderer.end();
}
// ... code omitted ... //
}
增加了drawCollisionBlocks()
方法,该方法将在发生碰撞的任何地方绘制一个白框。 这一切都是为了您的观看乐趣。 到目前为止,我们所做的工作应类似于以下视频:
本文应该总结基本的碰撞检测。 接下来,我们将研究扩展世界,摄像机运动,使用武器创建敌人,添加声音。 请分享您的想法,因为什么都很重要。 该项目的源代码可以在这里找到: https : //github.com/obviam/star-assault 。 您需要检出分支part4 。 要使用git进行检查: git clone -b part4 git@github.com:obviam/star-assault.git
。 您也可以将其下载为zip文件 。 libgdx tests目录中还有一个不错的平台程序。 超级Koalio 。 它展示了到目前为止我已经讲过的很多内容,并且它要简短得多,对于那些有libgdx经验的人来说,它非常有帮助。
参考: 使用libgdx进行Android游戏开发–碰撞检测, JCG合作伙伴 Impaler的第4部分,来自《 反对谷物》博客。