Programación de Juegos en Android 3. Game Loop y Animación.

En este tutorial vamos a ver el concepto "Game Loop" y un poco sobre las técnicas de animación.

El "game loop" es la repetición de dos actividades principales;

  1. Actualización de la física; se actualizan los datos del juego, como por ejemplo la posición "x" e "y" para un caracter (posiciones de los sprites, puntuación ...)
  2. Dibujo; el dibujo de la imagen que se ve en la pantalla. Cuando este método es llamado repetidamente produce la percepción de ser una película o una animación.

Vamos a ejecutar el "game loop" en un thread diferente. En un hilo, hacemos las actualizaciones y los dibujos y en el hilo principal, manejamos los eventos como hacemos en una aplicación normal. El código que tenemos a continuación nos muestra esta implementación:

package com.edu4java.android.killthemall;

import android.graphics.Canvas;

 

public class GameLoopThread extends Thread {

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

             while (running) {

                    Canvas c = null;

                    try {

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

                           synchronized (view.getHolder()) {

                                  view.onDraw(c);

                           }

                    } finally {

                           if (c != null) {

                                  view.getHolder().unlockCanvasAndPost(c);

                           }

                    }

             }

       }

}  

 

El campo "running" es un "flag" que hace que se pare el "game loop". Dentro de este loop, llamamos al método onDraw, como aprendimos en el tutorial anterior. En este caso, por simplicidad, hacemos el update y las actividades de dibujo en el método onDraw. Utilizamos la sincronización para evitar que cualquier otro hilo nos produzca conflicto cuando estemos dibujando.

 

En el SurfaceView añadimos un campo int "x" para mantener la coordena "x" para dibujar la imagen en el método onDraw. En el método onDraw también incrementamos la posición "x" si no ha llegado al borde derecho.

 

package com.edu4java.android.killthemall;

import android.content.Context;

import android.graphics.Bitmap;

import android.graphics.BitmapFactory;

import android.graphics.Canvas;

import android.graphics.Color;

import android.view.SurfaceHolder;

import android.view.SurfaceView;

 

public class GameView extends SurfaceView {

       private Bitmap bmp;

       private SurfaceHolder holder;

       private GameLoopThread gameLoopThread;

       private int x = 0; 

      

       public GameView(Context context) {

             super(context);

             gameLoopThread = new GameLoopThread(this);

             holder = getHolder();

             holder.addCallback(new SurfaceHolder.Callback() {

 

                    @Override

                    public void surfaceDestroyed(SurfaceHolder holder) {

                           boolean retry = true;

                           gameLoopThread.setRunning(false);

                           while (retry) {

                                  try {

                                        gameLoopThread.join();

                                        retry = false;

                                  } catch (InterruptedException e) {

                                  }

                           }

                    }

 

                    @Override

                    public void surfaceCreated(SurfaceHolder holder) {

                           gameLoopThread.setRunning(true);

                           gameLoopThread.start();

                    }

 

                    @Override

                    public void surfaceChanged(SurfaceHolder holder, int format,

                                  int width, int height) {

                    }

             });

             bmp = BitmapFactory.decodeResource(getResources(), R.drawable.icon);

       }

 

       @Override

       protected void onDraw(Canvas canvas) {

             canvas.drawColor(Color.BLACK);

             if (x < getWidth() - bmp.getWidth()) {

                    x++;

             }

             canvas.drawBitmap(bmp, x, 10, null);

       }

}


Vemos el resultado que obtenemos con la imagen animada que tenemos a continuación. Si lo miramos con atención podemos ver que la velocidad de la animación no es constante. Esto es porque cuando el "game loop" obtiene más tiempo de la CPU, la animación es más rápida. Podemos arreglar esto definiendo cuantos FPS (frames per second; imágenes por segundo) queremos en la aplicación.


 

Limitamos el dibujo a 10 FPS, que es 100 ms (milisegundos). Utilizamos el método sleep para que el tiempo restante sea 100ms. Si el bucle tarda más de 100ms, lo dormimos 10ms igualmente para evitar que la aplicación utilice demasiada CPU.

 

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

             }

       }

}

 

En SurfaceView añadimos un campo xSpeed para mantener la dirección de la animación y en onDraw cambiamos la dirección cuando se llega a los bordes. Ahora la imagen va y viene del borde derecho al izquierdo indefinidamente.

 

package com.edu4java.android.killthemall;

import android.content.Context;

import android.graphics.Bitmap;

import android.graphics.BitmapFactory;

import android.graphics.Canvas;

import android.graphics.Color;

import android.view.SurfaceHolder;

import android.view.SurfaceView;

 

public class GameView extends SurfaceView {

       private Bitmap bmp;

       private SurfaceHolder holder;

       private GameLoopThread gameLoopThread;

       private int x = 0; 

       private int xSpeed = 1;

      

       public GameView(Context context) {

             super(context);

             gameLoopThread = new GameLoopThread(this);

             holder = getHolder();

             holder.addCallback(new SurfaceHolder.Callback() {

 

                    @Override

                    public void surfaceDestroyed(SurfaceHolder holder) {

                           boolean retry = true;

                           gameLoopThread.setRunning(false);

                           while (retry) {

                                  try {

                                        gameLoopThread.join();

                                        retry = false;

                                  } catch (InterruptedException e) {

                                  }

                           }

                    }

 

                    @Override

                    public void surfaceCreated(SurfaceHolder holder) {

                           gameLoopThread.setRunning(true);

                           gameLoopThread.start();

                    }

 

                    @Override

                    public void surfaceChanged(SurfaceHolder holder, int format,

                                  int width, int height) {

                    }

             });

             bmp = BitmapFactory.decodeResource(getResources(), R.drawable.icon);

       }

 

       @Override

       protected void onDraw(Canvas canvas) {

             if (x == getWidth() - bmp.getWidth()) {

                    xSpeed = -1;

             }

             if (x == 0) {

                    xSpeed = 1;

             }

             x = x + xSpeed;

             canvas.drawColor(Color.BLACK);

             canvas.drawBitmap(bmp, x , 10, null);

       }

}

 

<< Dibujo de una imagen utilizando SurfaceView Nuestro primer Sprite >>