Eventos táctiles y detección de colisiones de sprites. Programación de Juegos en Android 7.

En este tutorial vamos a gestionar los eventos táctiles en la pantalla y ver si el eje de coordenadas (x,y) colisiona con alguno de nuestros sprites.

 

Lo primero que hacemos es añadir el siguiente método en el view para gestionar cada toque en la pantalla.

Para cada sprite de la lista de sprites vamos a preguntar si existe una colisión con las coordenas (x,y). Si existe la colisión, eliminamos el sprite de la lista de sprites. Iteramos la lista de sprites hacia atrás para evitar errores en la siguiente iteración, cuando hayamos eliminado el 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);

       }

La siguiente vez que el método onDraw dibuja la lista de sprites, los sprites eliminados de la lista, no serán dibujados. El efecto final es que el caracter desaparece debajo de nuestro dedo.

Para hacerlo funcionar, tenemos que implementar el método isCollition en la clase Sprite. Este método tiene que devolver true si las coordenadas (x,y) están dentro del área cubierta por el sprite.

       public boolean isCollition(float x2, float y2) {

             return x2 > x && x2 < x + width && y2 > y && y2 < y + height;

       }

Si x2 no es mayor que x, esto significa que el toque en pantalla ha sido fuera del sprite. Si x2 es mayor que x+ancho, esto significa que el toque está fuera a la derecha del sprite. Si If x2 > x && x2 < x + width es verdadero, significa que el toque ha sido en la misma columna pero hay que chequear si ha sido en la misma fila. y2 > y && y2 < y + height chequea si hay colisión en la fila de la misma manera.

Ejecutamos y veamos que pasa;

Como se ve cuando se hace click (o en el teléfono se da un toque) todos los sprites de la pantalla que coinciden con la posición, desaparecen. Un comportamiento más correcto para nuestra aplicación sería que sólo el sprite más arriba desaparezca. Podemos hacer esto añadiendo un break en

             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;

                    }

             }

Por último hacemos la sincronización. Los eventos son gestionados en un hilo diferente al del dibujo y la actualización. Esto puede producir un conflicto si actualizamos los datos que están siendo dibujados al mismo tiempo.

La solución es sencilla; envolvemos todo el código dentro del método onTouchEvent con el mismo objeto holder que usamos en el game loop. Para recordarlo hemos utilizado;

                           c = view.getHolder().lockCanvas();

                           synchronized (view.getHolder()) {

                                  view.onDraw(c);

                           }

y vamos a añadir

             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;

                           }

                    }

             }

Con esto evitamos un error que aparece randómicamente pero que es bastante molesto.

Si lo ejecutamos otra vez, veremos que el problema persiste. En este caso el problema no es el loop.

Lo que ocurre es que los eventos son tan seguidos que parece que uno mata a varios. La solución es hacer que vaya más lento para no permitir más de un click cada 300 milisegundos.

             if (System.currentTimeMillis() - lastClick > 300) {

                    lastClick = System.currentTimeMillis();

Conseguimos esto envolviendo al evento handler con este if. Este utiliza una propiedad lastClick que evita que haya mas de un click en 0.3 segundos.

Si lo ejecutamos ahora, vemos la diferencia.

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<Sprite> sprites = new ArrayList<Sprite>();

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

             }

       }

}

<< Trabajar con múltiples Sprites Sprites Temporales >>