Chapter     15

Collision Detection

Collision detection is a key component to almost any game and almost every game type. In a game without collision detection, items, obstacles, characters, and weapons would move about the screen and float past each other without any consequence.

Your game code needs to be able to determine if objects that are on the screen touch or cross paths with each other. It is only after you determine that two or more objects are touching that you can then perform actions on them such as applying damage, stopping motion, powering up a character, or destroying an object.

This chapter will cover some solutions that aid you with problems in collision detection. Collision detection can be tricky, but the solutions in this chapter should help to make the process a bit easier.

15.1 Detect Obstacles

Problem

The game character can move through objects on the screen that should stop them.

Solution

Use basic collision detection to determine if the character has touched an obstacle or the edge of the screen.

How It Works

Basic collision detection is useful if you are creating a game where characters are faced with static obstacles such as floors and platforms, the edges of the screen, or steps. You can use constant values when you are testing for the location of static objects. For example, in Recipes 13.1 and 13.2 for making the character jump, I used basic collision detection to determine when the character had finished the jump and was back on the ground, as shown in Listings 15-1 (OpenGL ES 1) and 15-2 (OpenGL ES 2/3).

Listing 15-1.  Basic Jumping Collision Detection (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();

Listing 15-2.  Basic Jumping Collision Detection (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);

The bolded code in Listings 15-1 and 15-2 illustrates how, in testing that the y position of the character has reached the level of the ground, a constant of .75 is used. Since we know that the ground of the game will always be at .75 on the y axis, this simple form of collision detection can be effective.

What about running off the edge of the screen?  If the action of your game needs to be contained to a single screen, and the x axis in OpenGL ES has been scaled to range from 0 (far left) to 4 (far right), you can test your character against that to stop the image from leaving the screen.

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
}

This process requires an extra step if you are testing for a collision against the right edge of the screen. The x position of the character in OpenGL ES represents the lower left-hand corner of the image. Therefore, if you are testing whether the image of the character has encountered the right-hand side of the screen, the x position of the character, at the lower left-hand side, will not reach the right edge of the screen until the entire image has already passed off the screen.

You can compensate for this by adding the size of the character image to the if statement that tests for the collision.

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
}

The basic method of collision detection is effective for less complex game logic where there are many static objects, the size and location of which are easily known to the game loop.

What if your game logic is not that easy? The next solution helps you detect collisions between objects that are moving and whose positions are not predictable.

15.2 Detect Collisions Between Multiple Moving Objects

Problem

The game needs to detect whether two or more moving objects have collided with each other.

Solution

Use a looping method to test for collisions on the edges of all OpenGL images.

How It Works

To implement a more robust form of collision detection, create a new method that can be called from your game loop. The method will loop through all of the active items on the screen and determine whether any are colliding.

The key fields needed to implement this kind of collision detection are the x- and y-axis coordinates of the objects’ current locations, and the status of the objects. The status of an object refers to whether the object is eligible to be included in collision detection. This could include a flag that the object has already been destroyed, or perhaps the character being tested has a completed an achievement that allows them to be free of collision detection for a specific period of time.

Listings 15-3 and 15-4 depict a class for a character in a game. Three public values have been added to the class: one each for the x- and y-axis coordinates to track the character’s current position, and a Boolean to indicate whether the character has already been destroyed.

Listing 15-3.  SBGEnemy() (OpenGL ES 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();
   }

}

Listing 15-4.  SBGEnemy() (OpenGL ES 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);
}
}

Now, build a new class that can be called from the game loop. In Chapter 14, a solution was presented where the player could fire weapons. The weapons that were fired were in an array, allowing four shots to be active on the screen at one time. We are going to build upon this by looping through each of the four shots, checking whether they are actively fired, and then checking whether they have collided with the enemy character in the previous code listing.

The easiest way to accomplish the collision test is to create (in memory) a bounding box around each active object and then test whether the edge of any two objects' bounding boxes collide. Why bounding boxes? It is easier to test straight lines, such as box edges, than to try to calculate the true edges of very complex shapes. Also, objects in the game will typically collide so quickly that the eye will not be able to detect that the collision occurred a fraction of a millimeter away from the visible border of the actual object.

Create the bounding box by adding the size (in coordinates) to the current x- and y-coordinate position of the object. This means that an object that is scaled to .25 square on the coordinate axis will have a bounding box from x to (x + .25), and from y to (y + .25). Anything that crosses into that space will collide with that object. To test for a collision in this example, all you need to do is check whether another object's bounding box contains a point that is between (x to (x + .25)) and (y to (y + .25)). If so, those two objects have collided.

In Listing 15-5, the shot being fired has .25 coordinate value bounding box, and the enemy has a 1 coordinate value bounding box.

Listing 15-5.  Detecting the bounding box

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
}
}
}
}
}

This method works well when detecting a collision between a round of shots and a single enemy. To test for a collision between a round of shots and a number of enemies, you will need to modify the method slightly to loop through your array of enemies (see Listing 15-6).

Listing 15-6.  Loop through the enemies

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

}
}
}
}
}
}

This collision detection method will help you test for collisions between the bounding boxes of multiple objects in your game. Once you have detected a collision, you can then act on that collision in the commented area. One of the actions you might want to take is to change the trajectory of an object—as in the instance of a ball bouncing off of a wall.

The next recipe will cover changing an object’s trajectory.

15.3 Change Object Trajectory

Problem

A game object, such as a ball, does not change direction when it hits a wall.

Solution

Use collision detection to change the trajectory of an object when it collides with another.

How It Works

There are game types where objects do not necessarily stop or explode when they collide with other objects. Some games, such as breakout-style brick smashers, contain objects that bounce off one another when they collide.

The modification of the detectCollisions() method in this solution helps you detect a collision between two objects (in this case, a ball and a brick) and change the trajectory of the ball on contact.

The code in Listing 15-7 came directly from an old brick-smashing game I wrote, I have left in the code that cycles through the bricks to help you. In the sample, ball is an instantiation of a Ball() class that is identical to the SBGEnemy() class we saw earlier in this chapter. Also, wall is a class that contains within it a collection of rows. Rows are then a collection of instantiated bricks, the class for which is also identical to the SBGEnemy(). This creates a wall for the player to break through that is made up of rows of bricks.

Finally, Listing 15-7 not only checks for collisions between the ball and bricks, but also the ball and the edges of the screen. If the ball hits the edge of the screen, it will bounce off, causing the trajectory to change and keeping the ball in play.

Listing 15-7.  detectCollisions()

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 Damage Objects upon Collision and Remove Destroyed Objects

Problem

The game does not “damage” objects after collisions. Also, once objects are damaged, they are still visible in the game.

Solution

Use a class to track object damage and remove destroyed objects.

How It Works

In Recipe 15.2, an isDestroyed flag was set to indicate that the object has collided with another and should be destroyed, thus removing it from the game. This is one way to track whether an object has been destroyed. But what if you want to create a system whereby an object can be hit (collision) multiple times before being destroyed?

Modify the objects class. Reference the SBGEnemy() class from Listing 15-1 to include a damageCounter.

public class SBGEnemy {

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

Now, on collision increment the damage counter by one. Set the isDestroyed flag if the counter reaches a predetermined threshold.

private void detectCollisions(){

...

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

...
}

With the character “destroyed,” the final step is to remove it from the screen. The easiest way to do that is to simply not draw it. In your game loop, test that a character or object is not destroyed before drawing it.

if(!character.isDestroyed){
character.draw(gl);
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.68.197