Thursday, May 22, 2014

Touch Events and Sprite Collision Detection. Android Game Programming

In this tutorial we are going to handle the touch event in the screen and check if the (x,y) coordinates collision with any of our characters sprites.


The first thing to do is add this method in the view to handle each touch in the screen. For each sprite in the sprites list we are going to ask if the (x,y) touch coordinates have a collision. If the collision exists, we remove the sprite from the sprites list. We iterate backwards the sprite list to avoid errors in the next iteration when we remove a sprite.


@Override
public boolean onTouchEvent(MotionEvent event) {
 for (int i = sprites.size()-1; i >= 0; i--) {
  Sprite sprite = sprites.get(i);
  if (sprite.isCollition(event.getX(),event.getY())) {
   sprites.remove(sprite);
  }
             }
             return super.onTouchEvent(event);
 }

The next time the onDraw method draws the sprite list, the removed sprites will be not drawn. Then the final effect is that the character disappears under our finger.

To make it work, we have to implement the isCollition method in the Sprite class. This method has to return true if the (x,y) coordinates are inside the present area covered by the sprite.
public boolean isCollition(float x2, float y2){
 return x2 > x && x2 < x + width && y2 > y && y2 < y + height;
}

if x2 is not bigger than x, this means that the touch was outside left from the sprite. if x2 is bigger than x + width, this means that the touch was outside right from the sprite. If x2 > x && x2 < x + width is true that means that the touch is in the same column but still we have to check if it is in the same row. y2 > y && y2 < y + height checks for row collision in the same way.

Let's run it and see what happens


As you can see when you click (in the phone is a touch) all sprites in the screen that match with its position disappear. For our application a better behaviour could be that only the sprite in the top disappears. We can get this by adding a break in

for (int i = sprites.size()-1; i >= 0; i--) {
Sprite sprite = sprites.get(i);
if (sprite.isCollition(event.getX(),event.getY())) {
 sprites.remove(sprite);
 break;
 }
}

The last thing that I almost forgot is the synchronization. The events are handled in a different thread than the drawing and update. This can produce conflict if we update the data that is been drawn at the same time. The solution is easy; just wrap all the code inside the onTouchEvent method with the same holder object we used in the game loop. To help you to remember in the run method we used

c = view.getHolder().lockCanvas();
synchronized (view.getHolder()) {
 view.onDraw(c);
 }
and now we are going to add

synchronized (getHolder()) {
 for (int i = sprites.size()-1; i > 0; i--) {
  Sprite sprite = sprites.get(i);
  if (sprite.isCollition(event.getX(),event.getY())) {
   sprites.remove(sprite);
   break;
   }
                    }
             }

with this we avoid an error that appears randomically but is really annoying. If we run it again we are going to find that the problem persist. This time the problem is not the loop. It is that the events are triggered so fast that we fill like is one that kill many. The solution is makes it slow not letting more than one click in 300 milliseconds.
if (System.currentTimeMillis() - lastClick > 300) {
 lastClick = System.currentTimeMillis();

we get this wrapping the event handler with this if. This use a lastClick property to avoid click often than 0.3 second. Now run it again and you are going to see the difference.
package com.edu4java.android.killthemall;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;

public class GameView extends SurfaceView {
       private Bitmap bmp;
       private SurfaceHolder holder;
       private GameLoopThread gameLoopThread;
       private List sprites = new ArrayList();
       private long lastClick;

       public GameView(Context context) {
             super(context);
             gameLoopThread = new GameLoopThread(this);
             holder = getHolder();
             holder.addCallback(new Callback() {

                    @Override
                    public void surfaceDestroyed(SurfaceHolder holder) {
                    }

                    @Override
                    public void surfaceCreated(SurfaceHolder holder) {
                           createSprites();
                           gameLoopThread.setRunning(true);
                           gameLoopThread.start();
                    }

                    @Override
                    public void surfaceChanged(SurfaceHolder holder, int format,

                                  int width, int height) {
                    }
             });
       }

       private void createSprites() {
             sprites.add(createSprite(R.drawable.bad1));
             sprites.add(createSprite(R.drawable.bad2));
             sprites.add(createSprite(R.drawable.bad3));
             sprites.add(createSprite(R.drawable.bad4));
             sprites.add(createSprite(R.drawable.bad5));
             sprites.add(createSprite(R.drawable.bad6));
             sprites.add(createSprite(R.drawable.good1));
             sprites.add(createSprite(R.drawable.good2));
             sprites.add(createSprite(R.drawable.good3));
             sprites.add(createSprite(R.drawable.good4));
             sprites.add(createSprite(R.drawable.good5));
             sprites.add(createSprite(R.drawable.good6));
       }

       private Sprite createSprite(int resouce) {
             Bitmap bmp = BitmapFactory.decodeResource(getResources(), resouce);
             return new Sprite(this, bmp);
       }

       @Override
       protected void onDraw(Canvas canvas) {
             canvas.drawColor(Color.BLACK);
             for (Sprite sprite : sprites) {
                    sprite.onDraw(canvas);
             }
       }

       @Override
       public boolean onTouchEvent(MotionEvent event) {
             if (System.currentTimeMillis() - lastClick > 500) {
                    lastClick = System.currentTimeMillis();
                    synchronized (getHolder()) {
                        for (int i = sprites.size() - 1; i >= 0; i--) {
                            Sprite sprite = sprites.get(i);
                            if (sprite.isCollition(event.getX(), event.getY())) {
                                  sprites.remove(sprite);
                                  break;
                            }
                        }
                    }
             }
             return true;
       }
}
package com.edu4java.android.killthemall;

import java.util.Random;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;

public class Sprite {
       // direction = 0 up, 1 left, 2 down, 3 right,
       // animation = 3 back, 1 left, 0 front, 2 right
       int[] DIRECTION_TO_ANIMATION_MAP = { 3, 1, 0, 2 };
       private static final int BMP_ROWS = 4;
       private static final int BMP_COLUMNS = 3;
       private static final int MAX_SPEED = 5;
       private GameView gameView;
       private Bitmap bmp;
       private int x = 0;
       private int y = 0;
       private int xSpeed;
       private int ySpeed;
       private int currentFrame = 0;
       private int width;
       private int height;

       public Sprite(GameView gameView, Bitmap bmp) {
             this.width = bmp.getWidth() / BMP_COLUMNS;
             this.height = bmp.getHeight() / BMP_ROWS;
             this.gameView = gameView;
             this.bmp = bmp;

             Random rnd = new Random();
             x = rnd.nextInt(gameView.getWidth() - width);
             y = rnd.nextInt(gameView.getHeight() - height);
             xSpeed = rnd.nextInt(MAX_SPEED * 2) - MAX_SPEED;
             ySpeed = rnd.nextInt(MAX_SPEED * 2) - MAX_SPEED;
       }

       private void update() {
             if (x >= gameView.getWidth() - width - xSpeed || x + xSpeed <= 0) {
                    xSpeed = -xSpeed;
             }
             x = x + xSpeed;
             if (y >= gameView.getHeight() - height - ySpeed || y + ySpeed <= 0) {
                    ySpeed = -ySpeed;
             }
             y = y + ySpeed;
             currentFrame = ++currentFrame % BMP_COLUMNS;
       }

       public void onDraw(Canvas canvas) {
             update();
             int srcX = currentFrame * width;
             int srcY = getAnimationRow() * height;
             Rect src = new Rect(srcX, srcY, srcX + width, srcY + height);
             Rect dst = new Rect(x, y, x + width, y + height);
             canvas.drawBitmap(bmp, src, dst, null);
       }

       private int getAnimationRow() {
             double dirDouble = (Math.atan2(xSpeed, ySpeed) / (Math.PI / 2) + 2);
             int direction = (int) Math.round(dirDouble) % BMP_ROWS;
             return DIRECTION_TO_ANIMATION_MAP[direction];
       }

       public boolean isCollition(float x2, float y2) {
             return x2 > x && x2 < x + width && y2 > y && y2 < y + height;
       }
}
package com.edu4java.android.killthemall;

import android.graphics.Canvas;

public class GameLoopThread extends Thread {

       static final long FPS = 10;
       private GameView view;
       private boolean running = false;

       public GameLoopThread(GameView view) {
             this.view = view;
       }

       public void setRunning(boolean run) {
             running = run;
       }

       @Override
       public void run() {
             long ticksPS = 1000 / FPS;
             long startTime;
             long sleepTime;
             while (running) {
                    Canvas c = null;
                    startTime = System.currentTimeMillis();
                    try {
                           c = view.getHolder().lockCanvas();
                           synchronized (view.getHolder()) {
                                  view.onDraw(c);
                           }

                    } finally {
                           if (c != null) {
                                  view.getHolder().unlockCanvasAndPost(c);
                           }
                    }
                    sleepTime = ticksPS - (System.currentTimeMillis() - startTime);
                    try {
                           if (sleepTime > 0)
                                  sleep(sleepTime);
                           else
                                  sleep(10);
                    } catch (Exception e) {}
             }
       }
}

No comments:

Post a Comment