十五、碰撞检测

碰撞检测是几乎所有游戏和所有游戏类型的关键组件。在一个没有碰撞检测的游戏中,物品、障碍物、角色和武器会在屏幕上四处移动,彼此漂浮而过,不会产生任何后果。

您的游戏代码需要能够确定屏幕上的对象是否相互接触或交叉。只有在你确定两个或更多的物体接触后,你才能对它们执行动作,比如施加伤害、停止运动、启动角色或摧毁一个物体。

本章将介绍一些解决碰撞检测问题的方法。碰撞检测可能很棘手,但是本章中的解决方案应该有助于使这个过程变得简单一些。

15.1 探测障碍物

问题

游戏角色可以通过屏幕上的物体来阻止他们。

解决办法

使用基本的碰撞检测来确定角色是否接触了障碍物或屏幕边缘。

它是如何工作的

如果您正在创建一个角色面临静态障碍物(如地板和平台、屏幕边缘或台阶)的游戏,则基本碰撞检测非常有用。测试静态对象的位置时,可以使用常数值。例如,在制作角色跳跃的配方 13.1 和 13.2 中,我使用了基本的碰撞检测来确定角色何时完成跳跃并回到地面,如清单 15-1 (OpenGL ES 1)和 15-2 (OpenGL ES 2/3)所示。

清单 15-1 。基本跳跃碰撞检测(OpenGL ES 1)

previousJumpPos = posJump;

posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);

if (posJump <= Math.PI)
{
goodguy.posY += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;
}else{
goodguy. posY -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.posY<= .75f){
playeraction = PLAYER_STAND;
goodguy.posY= .75f;
}
}

goodguy. posX += PLAYER_RUN_SPEED;
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glPushMatrix();
gl.glScalef(.15f, .15f, 1f);
gl.glTranslatef(goodguy. posX, goodguy. posY, 0);
gl.glPopMatrix();
gl.glLoadIdentity();

清单 15-2 。基本跳跃碰撞检测(OpenGL ES 2/3)

previousJumpPos = posJump;

posJump += (float)(((Math.PI / 2) / .5) * PLAYER_RUN_SPEED);
if (posJump <= Math.PI)
{
goodguy. posY += 1.5 / .5 * .15 * PLAYER_RUN_SPEED;

}else{
goodguy. posY -=(Math.sin((double)posJump) - Math.sin((double)previousJumpPos))* 1.5;
if (goodguy.y<= .75f){
playeraction = PLAYER_STAND;
goodguy.posY= .75f;
}
}
goodguy. posX += PLAYER_RUN_SPEED;
Matrix.translateM(RotationMatrix, 0, goodguy. posX, goodguy. posY, 0);

列表 15-1列表 15-2 中加粗的代码说明了在测试角色的 y 位置已经到达地面的水平时,如何使用常数. 75。因为我们知道游戏的地面总是在 y 轴的 0.75 处,所以这种简单的碰撞检测是有效的。

从屏幕边缘跑出来怎么办?如果您的游戏动作需要包含在一个屏幕中,并且 OpenGL ES 中的 x 轴已经被缩放到从 0(最左边)到 4(最右边)的范围内,您可以测试您的角色来阻止图像离开屏幕。

if(goodguy.posX<= 0 )
{
//the player has reached the left edge of the screen
goodguy. posX = 0; //correct the image's position and perform whatever action is necessary
}

如果您要测试与屏幕右边缘的碰撞,这个过程需要一个额外的步骤。OpenGL ES 中字符的 x 位置代表图像的左下角。因此,如果您正在测试字符的图像是否遇到了屏幕的右侧,则在整个图像已经离开屏幕之前,字符在左下角的 x 位置不会到达屏幕的右侧。

您可以通过将角色图像的大小添加到测试碰撞的if语句中来对此进行补偿。

if(goodguy. posX +.25f>= 4 )
{
//the player has reached the right edge of the screen
goodguy. posX = (4f - .25f); //correct the image's position and
                                //perform whatever action is necessary
}

碰撞检测的基本方法对于不太复杂的游戏逻辑是有效的,其中有许多静态对象,其大小和位置对于游戏循环来说是容易知道的。

如果你的游戏逻辑没那么简单怎么办?下一个解决方案帮助您检测正在移动且位置不可预测的对象之间的碰撞。

15.2 检测多个移动物体之间的碰撞

问题

该游戏需要检测两个或多个移动物体是否发生了碰撞。

解决办法

使用循环方法测试所有 OpenGL 图像边缘的碰撞。

它是如何工作的

要实现更健壮的碰撞检测,创建一个可以从游戏循环中调用的新方法。该方法将遍历屏幕上的所有活动项目,并确定是否有任何碰撞。

实现这种碰撞检测所需的关键字段是对象当前位置的 x 轴和 y 轴坐标,以及对象的状态。对象的状态是指该对象是否有资格被包括在碰撞检测中。这可以包括对象已经被破坏的标志,或者可能被测试的角色已经完成了一项成就,允许他们在特定的时间段内免于碰撞检测。

清单 15-3 和 15-4 描述了游戏中一个角色的职业。该类中添加了三个公共值:x 轴和 y 轴坐标各一个,用于跟踪角色的当前位置,还有一个布尔值,用于指示角色是否已经被破坏。

清单 15-3SBGEnemy() (OpenGL 是 1)

public class SBGEnemy {

public float posY = 0;
public float posX = 0;
public bool isDestroyed = false;

private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ByteBufferi ndexBuffer;

private float vertices[] = {
0.0, 0.0, 0.0,
1.0, 0.0, 0.0,
1.0, 1.0, 0.0,
0.0, 1.0, 0.0,
};

private float texture[] = {
0.0, 0.0,
0.25f, 0.0,
0.25f, 0.25f,
0.0, 0.25f,
};

private byte indices[] = {
0,1,2,
0,2,3,
};

public SBGEnemy () {

ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuf.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);

byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
byteBuf.order(ByteOrder.nativeOrder());
textureBuffer = byteBuf.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);

indexBuffer = ByteBuffer.allocateDirect(indices.length);
indexBuffer.put(indices);
indexBuffer.position(0);
}

public void draw(GL10gl, int[] spriteSheet) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, spriteSheet[1]);

gl.glFrontFace(GL10.GL_CCW);
gl.glEnable(GL10.GL_CULL_FACE);
gl.glCullFace(GL10.GL_BACK);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer);

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glDisable(GL10.GL_CULL_FACE);
}

public void loadTexture(GL10gl,int texture, Context context) {
InputStream imagestream = context.getResources().openRawResource(texture);
Bitmap bitmap = null;

Matrix flip = new Matrix();
flip.postScale(-1f, -1f);

try {

bitmap = BitmapFactory.decodeStream(imagestream);

}catch(Exception e){

}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}

gl.glGenTextures(1, textures, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);

gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);

GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);

bitmap.recycle();
   }

}

清单 15-4SBGEnemy() (OpenGL 是 2/3)

public class SBGEnemy {

public float posY = 0;
public float posX = 0;
public bool isDestroyed = false;

private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 TexCoordIn;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
"  gl_Position = uMVPMatrix * vPosition;" +
"  TexCoordOut = TexCoordIn;" +
"}";

private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"uniform sampler2D TexCoordIn;" +
"varying vec2 TexCoordOut;" +
"void main() {" +
"  gl_FragColor = texture2D(TexCoordIn, TexCoordOut);" +
"}";

private float texture[] = {
0, 0,
1f, 0,
1f, 1f,
0, 1f,
};

private int[] textures = new int[1];
private final FloatBuffer vertexBuffer;
private final ShortBuffer drawListBuffer;
private final FloatBuffer textureBuffer;
private final int program;
private int positionHandle;
private int matrixHandle;

static final int COORDS_PER_VERTEX = 3;
static final int COORDS_PER_TEXTURE = 2;
static float vertices[] = { -1f,  1f, 0.0,
-1f, -1f, 0.0,
1f, -1f, 0.0,
1f,  1f, 0.0 };

private final short indices[] = { 0, 1, 2, 0, 2, 3 };

private final int vertexStride = COORDS_PER_VERTEX * 4;
public static int textureStride = COORDS_PER_TEXTURE * 4;

public void loadTexture(int texture, Context context) {
InputStream imagestream = context.getResources().openRawResource(texture);
      Bitmap bitmap = null;

android.graphics.Matrix flip = new android.graphics.Matrix();
flip.postScale(-1f, -1f);

try {

bitmap = BitmapFactory.decodeStream(imagestream);

}catch(Exception e){

}finally {
try {
imagestream.close();
imagestream = null;
} catch (IOException e) {
}
}

GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);

GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);

GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

bitmap.recycle();
}

public SBGEnemy () {

ByteBuffer byteBuff = ByteBuffer.allocateDirect(
byteBuff.order(ByteOrder.nativeOrder());
vertexBuffer = byteBuff.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);

byteBuff = ByteBuffer.allocateDirect(texture.length * 4);
byteBuff.order(ByteOrder.nativeOrder());
textureBuffer = byteBuff.asFloatBuffer();
textureBuffer.put(texture);
textureBuffer.position(0);

ByteBuffer indexBuffer = ByteBuffer.allocateDirect(
indexBuffer.order(ByteOrder.nativeOrder());
drawListBuffer = indexBuffer.asShortBuffer();
drawListBuffer.put(indices);
drawListBuffer.position(0);

int vertexShader = SBGGameRenderer.loadShader(
GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = SBGGameRenderer.loadShader(
GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
}

public void draw(float[] matrix) {

GLES20.glUseProgram(program);

positionHandle = GLES20.glGetAttribLocation(program, "vPosition");

GLES20.glEnableVertexAttribArray(positionHandle);

int vsTextureCoord = GLES20.glGetAttribLocation(program, "TexCoordIn");

GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
GLES20.glVertexAttribPointer(vsTextureCoord, COORDS_PER_TEXTURE,
GLES20.GL_FLOAT, false,
textureStride, textureBuffer);
GLES20.glEnableVertexAttribArray(vsTextureCoord);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
int fsTexture = GLES20.glGetUniformLocation(program, "TexCoordOut");
GLES20.glUniform1i(fsTexture, 0);

matrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix");

GLES20.glUniformMatrix4fv(matrixHandle, 1, false, matrix, 0);

GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

GLES20.glDisableVertexAttribArray(positionHandle);
}
}

现在,构建一个可以从游戏循环中调用的新类。在第 14 章中,玩家可以使用武器。发射的武器排成一列,允许四发子弹同时出现在屏幕上。我们将在此基础上循环四个镜头中的每一个,检查它们是否被主动发射,然后检查它们是否与前面代码清单中的敌人角色发生碰撞。

完成碰撞测试最简单的方法是在每个活动对象周围创建一个边界框(在内存中),然后测试任何两个对象的边界框的边缘是否碰撞。为什么是边框?测试直线(如盒子边缘)比计算非常复杂形状的真实边缘更容易。此外,游戏中的物体通常会碰撞得如此之快,以至于眼睛无法察觉到碰撞发生在距离实际物体的可见边界不到一毫米的地方。

通过将大小(以坐标表示)添加到对象的当前 x 和 y 坐标位置来创建边界框。这意味着在坐标轴上缩放到 0.25 平方的对象将有一个从 x 到(x + .25)和从 y 到(y + .25)的边界框。任何进入那个空间的东西都会与那个物体相撞。在本例中,要测试碰撞,只需检查另一个对象的边界框是否包含介于(x 到(x + .25))和(y 到(y + .25))之间的点。如果是这样,那两个物体就相撞了。

清单 15-5 中,正在发射的子弹有. 25 坐标值包围盒,敌人有 1 坐标值包围盒。

清单 15-5 。检测边界框

private void detectCollisions(){
for (int y = 1; y < 4; y ++){ //loop through the 4 potential shots in the array
if (playerFire[y].shotFired){ //only test the shots that are currently active
if(!enemy.isDestroyed){
//only test the shot against the enemy if it is not already destroyed
//test for the collision
if (((playerFire[y].posY  >= enemy.posY
&& playerFire[y].posY <= enemy.posY + 1f )  ||
(playerFire[y].posY +.25f>= enemy.posY
&& playerFire[y].posY + .25f<= enemy.posY + 1f )) &&
((playerFire[y].posX>= enemy.posX
&& playerFire[y].posX<= enemy.posX + 1f) ||
(playerFire[y].posX + .25f>= enemy.posX
&& playerFire[y].posX + 25f<= enemy.posX + 1f ))){
//collision detected between enemy and a shot
}
}
}
}
}

这种方法在检测一轮射击和单个敌人之间的碰撞时效果很好。为了测试一轮射击和许多敌人之间的碰撞,你需要稍微修改方法来循环通过你的敌人阵列(见清单 15-6 )。

清单 15-6 。穿过敌人

private void detectCollisions(){
for (int y = 1; y < 4; y ++){
if (playerFire[y].shotFired){
for (int x = 1; x < 10; x++ ){ //assumes you have an array of 10 enemies
if(!enemies[x].isDestroyed){
if (((playerFire[y].posY  >= enemies[x].posY
&& playerFire[y].posY <= enemies[x].posY + 1f )  ||
(playerFire[y].posY +.25f>= enemies[x].posY
&& playerFire[y].posY + .25f<= enemies[x].posY + 1f ))
&&  ((playerFire[y].posX>= enemies[x].posX
&& playerFire[y].posX<= enemies[x].posX + 1f) ||
(playerFire[y].posX + .25f>= enemies[x].posX
&& playerFire[y].posX + 25f<= enemies[x].posX + 1f ))){

//collision detected between enemy and a shot

}
}
}
}
}
}

这种碰撞检测方法将帮助您测试游戏中多个对象的边界框之间的碰撞。一旦检测到冲突,您就可以在注释区域中处理该冲突。你可能想要采取的一个行动是改变一个物体的轨迹——就像一个球从墙上弹回一样。

下一个食谱将包括改变一个物体的轨迹。

15.3 改变物体轨迹

问题

一个游戏对象,比如一个球,当它撞到墙上时不会改变方向。

解决办法

当一个对象与另一个对象碰撞时,使用碰撞检测来改变其轨迹。

它是如何工作的

有些游戏类型中的物体在与其他物体碰撞时不一定会停止或爆炸。一些游戏,如突破式砖块击碎器,包含碰撞时相互弹开的物体。

本解决方案中对detectCollisions()方法的修改有助于您检测两个对象(在本例中是一个球和一块砖)之间的碰撞,并在接触时改变球的轨迹。

清单 15-7 中的代码直接来自我写的一个旧的砸砖游戏,我在代码中留下了循环砖块来帮助你。在这个示例中,ball是一个Ball()类的实例化,它与我们在本章前面看到的SBGEnemy()类相同。另外,wall是一个包含行集合的类。行则是实例化砖块的集合,砖块的类也与SBGEnemy()相同。这为玩家创造了一堵由一排排砖块组成的墙。

最后,清单 15-7 不仅检查球和砖块之间的碰撞,还检查球和屏幕的边缘。如果球碰到屏幕的边缘,它会弹开,导致轨迹改变,并保持球在比赛中。

清单 15-7detectCollisions()

private void detectCollisions(){
if(ball.posY<= 0){
}
for (int x = 0; x <wall.rows.length; x++)
{ //cycle through each brick and see if the ball has collided with it
for(int y = 0; y <wall.rows[x].bricks.length; y++)
{
if(!wall.rows[x].bricks[y].isDestroyed)
{
if (((ball.posY>wall.rows[x].bricks[y].posY - .25f)  //the height of the brick is .25
&& (ball.posY<wall.rows[x].bricks[y].posY)
&& (ball.posX + .25f>wall.rows[x].bricks[y].posX)
&& (ball.posX<wall.rows[x].bricks[y].posX + 1.50))) //the legnthof the brick
{ //there is a collision, destroy the brick and change the trajectory
//of the ball

wall.rows[x].bricks[y].isDestroyed = true;
//change the trajectory by inverting the y axis
ballTargetY = ballTargetY * -1f;

//if the ball was originally moving to the left when it collided, move it to
//the right after the bounce - otherwise move it to the left
if(ballTargetX == -2f){
ballTargetX = 5f;
}else{
ballTargetX = -2f;
}

}
}

}
}

//Now check for collisions with the player's "paddle" and bounce the ball off accordingly
if((ball.posY - .25f<= .5f)
&& (ball.posX + .25f>player.PosX ) //the paddle has the same dimensions as a brick,
                                        //keep it simple
&& (ball.posX<player.PosX  + 1.50)){
//collision detected, change the Y trajectory of the ball, and the direction on the x axis
ballTargetY = ballTargetY * -1f;
if(ballTargetX == -2f){
ballTargetX = 5f;
}else{
ballTargetX = -2f;
}
}

//check for collision with edge of the screeen, change the x axis trajectory on impact
if(ball.posX< 0 || ball.posX + .25f>3.75f)
{
ballTargetX = ballTargetX * -1f;

}

}

15.4 碰撞时损坏物体,并清除损坏的物体

问题

游戏不会在碰撞后“损坏”物体。同样,一旦物体被损坏,它们在游戏中仍然可见。

解决办法

使用一个类来跟踪对象损坏并移除被破坏的对象。

它是如何工作的

在配方 15.2 中,设置了一个isDestroyed标志来表示该物体与另一个物体发生了碰撞,应该被摧毁,从而将其从游戏中移除。这是跟踪对象是否被破坏的一种方式。但是,如果你想创建一个系统,让一个物体在被摧毁之前可以被击中(碰撞)多次呢?

修改 objects 类。引用清单 15-1 中的SBGEnemy()类来包含一个damageCounter

public class SBGEnemy {

public float posY = 0;
public float posX = 0;
public bool isDestroyed = false;
public int damageCounter = 0;
...
}

现在,碰撞时伤害计数器加 1。如果计数器达到预定阈值,则设置isDestroyed标志。

private void detectCollisions(){

...

//collision detected
character.damageCounter += 1;
if(character.damageCounter == 3){
character.isDestroyed = true;
}

...
} 

随着角色“被摧毁”,最后一步是从屏幕上删除它。最简单的方法就是不画它。在你的游戏循环中,在绘制之前测试一个角色或物体是否被破坏。

if(!character.isDestroyed){
character.draw(gl);
}