La clase Gráfico

El juego que estamos desarrollando va a desplazar muchos tipos de gráficos por pantalla: asteroides, nave, misiles,… Dado que el comportamiento de todos estos elementos es muy similar, con el fin de reutilizar el código y mejorar su comprensión, vamos a crear una clase que represente un gráfico a desplazar por pantalla. A esta nueva clase le llamaremos Grafico y presentará las siguientes características. El elemento a dibujar será representado en un objeto Drawable. Como hemos visto, esta clase presenta una gran versatilidad, lo que nos permitirá trabajar con gráficos en bitmap (BitmapDrawable), vectoriales (ShapeDrawable), gráficos con diferentes representaciones (StateListDrawable), gráficos animados (AnimationDrawable),… Además un Grafico dispondrá de posición, velocidad de desplazamiento, ángulo de rotación y velocidad de rotación. Para finalizar, un gráfico que salga por uno de los extremos de la pantalla reaparecerá por el extremo contrario, tal y como ocurría en el juego original de Asteroides. 

Ejercicio paso a paso: La clase Gráfico

1.     Crea una nueva clase Grafico en el proyecto Asteroides y copia el siguiente código

class Grafico {
       private Drawable drawable;   //Imagen que dibujaremos
       private int cenX, cenY;   //Posición el centro el gráfico
       private int ancho, alto;     //Dimensiones de la imagen
       private double incX, incY;   //Velocidad desplazamiento
       private double angulo, rotacion;//Ángulo y velocidad rotación
       private int radioColision;   //Para determinar colisión
       private int XAnterior, YAnterior;   //Posición anterior
       private int radioInval;   //Radio usado en invalidate()
       private View view;   //Usada en view.invalidate()
              
       public Grafico(View view, Drawable drawable){
             this.view = view;
             this.drawable = drawable;
             ancho = drawable.getIntrinsicWidth();  
             alto = drawable.getIntrinsicHeight();
             radioColision = (alto+ancho)/4;
             radioInval = (int) Math.hypot(ancho/2,alto/2);
       }
      
       public void dibujaGrafico(Canvas canvas){
             int x= cenX - ancho/2);
             int y= cenY - alto/2);
             drawable.setBounds(x, y, x+ancho, y+alto);   
             canvas.save();
             canvas.rotate((float)angulo,cenX,cenY);                                        drawable.draw(canvas);
             canvas.restore();
             view.invalidate(cenX-radioInval, cenY-radioInval, 
                            cenX+radioInval, cenY+radioInval);
             view.invalidate(xAnterior -radioInval, YAnterior-radioInval, 
                            xAnterior+radioInval, YAnterior+radioInval);
             xAnterior= cenX;
             yAnterior= cenY;
       }
 
       public void incrementaPos(double factor){
             cenX += incX * factor;
             cenY += incY* factor;
             angulo += rotacion * factor;
             // Si salimos de la pantalla, corregimos posición
             if(cenX<0)                   cenX=view.getWidth();
             if(cenX=view.getWidth())     cenX=0;
             if(cenY<0)                   cenY=view.getHeiht();
             if(cenY=view.getHeiht())     cenY=0;
      }
            
       public double distancia(Grafico g) {
             return Math.hypot(cenX-g.cenX, cenY-g.cenY-g.cenY);
       }
 
       public boolean verificaColision(Grafico g) {
             return (distancia(g) < (radioColision + g.radioColision));
       }
}

Cada objeto de la clase Grafico se caracteriza por situarse en unas coordenadas centrales (cenX, cenY). Por lo tanto su esquina superior derecha estará en las coordenadas (x, y) = (cenX - ancho/2, cenY - alto/2). Aunque un graficos pueden ser alargados (si alto diferente a ancho), a efecto de las colisiones, vamos a considerar que son círculos.El radio de este círculo se podría calcular como ancho/2  ó  alto/2, según tomáramos el radio mayor o el radio menor. Lo que hacemos es tomar una media de estos dos posibles radios: (ancho/2+alto/2)/2= (ancho+alto)/4. El método verificaColision()comprueba si hemos colisionado con otro gráfico. Para ello se comprueba si la distancia al otro Graficoes menor a la suma de los dos radios de colisión.

El método dibujaGrafico()se encarga de dibujar el Drawable del Grafico en un Canvas. Comienza indicando los límites donde se situará el Drawable, utilizando setBounds(). Luego, guarda la matriz de transformación del Canvas. A continuación, aplica una transformación de rotación según lo indicado en la variable anguloutilizando como eje de rotación (cenX, cenY). Se dibuja el Drawable en el Canvas y se recupera la matriz de transformación, para que la rotación introducida no se aplique en futuras operaciones con este Canvas.

Para finalizar hacemos dos llamadas al método invalidate() de la vista donde estamos dibujando el Grafico. Con este método informamos a la vista que tiene que ser redibujada. Para mejorar la eficiencia indicaremos solo el rectángulo que hemos modificado. De esta forma, la vista no tendrá que redibujarse en su totalidad. En un primer momento podríamos pensar que el rectángulo de invalidación coincide con el utilizado en setBounds(). Pero, hay que recordar que hemos realizado una rotación sobre el Drawable y, como puede verse en la ilustración de la izquierda, posiblemente el Drawable se salga de este rectángulo. Para resolver este problema vamos a aumentar el área de invalidación a un cuadrado con la mitad de su lado igual a la mitad de la diagonal de gráfico. Este valor es precalculado en la variable radioInval.

 

Hay que tener en cuenta que hemos desplazado el Drawable desde una posición anterior. Por lo tanto, también es necesario indicar a la vista que redibuje el área donde estaba antes el Grafico. Con este fin vamos a utilizar las varibles xAnterior y yAnterior.

Otro método interesante es incrementaPos() que es utilizado  para modificar la posición y ángulo del Grafico según la velocidad de translación (incX, incY) y la velocidad de rotación (rotacion). Este método tiene el párametro, factor, que permite ajustar esta velocidad. Con un valor igual a 1, tendremos una velocidad normal; si vale 2, el grafico se mueve al doble de velocidad. En el juego original de Asteroides, si un grafico salía por un lado de la pantalla, aparecía de nuevo por el lado opuesto. Este comportamiento es implementado en las últimas líneas del método

2.     Al principio de la clase hemos definido varios campos con el modificador private. Vamos a necesitar acceder a estos campos desde fuera de la clase, por lo que resulta necesario declarar los métodos get yset correspondientes. Vamos a realizar esta tarea de forma automática utilizando una herramienta de Eclipse. Sitúa el cursor al final de la clase (justo antes de la última llave) y pulsa con el botón derecho.Selecciona en el menú desplegable Source/Generate Getters and Setters… En la ventana de diálogo marca todos los campos (botón Sellect All) y pula OK. Eclipse hará el trabajo por nosotros.

Nota sobre Java: En el tutorial Java Esencial / Encapsulamiento y visibilidad puedes aprender más sobre los métodos get y set. Lo encontrarás en la Web www.androidcurso.com.

La clase VistaJuego

Pasemos a describir la creación de VistaJuego, que como hemos indicado es la responsable de la ejecución el juego. En una primera versión solo se representarán los asteroides de forma estática:

Ejercicio paso a paso: La clase VistaJuego

1. Crea una nueva clase VistaJuego en el proyecto Asteroides y copia el siguiente código:

public class VistaJuego extends View {

      // //// ASTEROIDES //////

      private Vector Asteroides; // Vector con los Asteroides

      private int numAsteroides= 5; // Número inicial de asteroides

      private int numFragmentos= 3; // Fragmentos en que se divide



      public VistaJuego(Context context, AttributeSet attrs) {

            super(context, attrs);

            Drawable drawableNave, drawableAsteroide, drawableMisil;

            drawableAsteroide = context.getResources().getDrawable(
                                                                                  R.drawable.asteroide1);

            Asteroides = new Vector();

            for (int i = 0; i < numAsteroides; i++) {

                  Grafico asteroide = new Grafico(this, drawableAsteroide);

                  asteroide.setIncY(Math.random() * 4 - 2);

                  asteroide.setIncX(Math.random() * 4 - 2);

                  asteroide.setAngulo((int) (Math.random() * 360));

                  asteroide.setRotacion((int) (Math.random() * 8 - 4));

                  Asteroides.add(asteroide);

            }

      }



      @Override protected void onSizeChanged(int ancho, int alto,
                                                           int ancho_anter, int alto_anter) {

            super.onSizeChanged(ancho, alto, ancho_anter, alto_anter);

            // Una vez que conocemos nuestro ancho y alto.

            for (Grafico asteroide: Asteroides) {

                  asteroide.setCenX((int) Math.random()ancho);

                  asteroide.setCenY((int) Math.random()alto);

            }

      }



      @Override protected void onDraw(Canvas canvas) {

            super.onDraw(canvas);

            for (Grafico asteroide: Asteroides) {

                asteroide.dibujaGrafico(canvas);

            }

      }

}

Como ves se han declarado tres métodos. En el constructor creamos los asteroides e inicializamos su velocidad, ángulo y rotación. Sin embargo, resulta imposible iniciar su posición, dado que no conocemos el alto y ancho de la pantalla. Esta información será conocida cuando se llame a onSizeChanged(). Observa cómo en esta función los asteroides están situados de forma aleatoria. El último método, onDraw(), es el más importante de la clase View, dado que es el responsable de dibujar la vista.

2. Hemos creado una vista personalizada. No tendría demasiado sentido, pero podrá ser utilizada en cualquier otra aplicación que desarrolles. Visualiza el Layout juego.xml y observa como la nueva vista se integra perfectamente en el entorno de desarrollo.

 

 

3. Registra la actividad Juego en AndroidManifest.xml.

4. Ejecuta la aplicación. Has de ver cinco asteroides repartidos al azar por la pantalla

 

Introduciendo la nave en VistaJuego

El siguiente paso consiste en dibujar la nave que controlará el usuario para destruir los asteroides.

Práctica: Introduciendo la nave en VistaJuego

1. Declara las siguientes variables al comienzo de la clase VistaJuego:

// //// NAVE //////

   private Grafico nave;// Gráfico de la nave

   private int giroNave; // Incremento de dirección

   private float aceleracionNave; // aumento de velocidad

   // Incremento estándar de giro y aceleración

   private static final int PASO_GIRO_NAVE = 5;

   private static final float PASO_ACELERACION_NAVE = 0.5f;

Algunas de estas variables serán utilizadas en el siguiente capítulo.

 

2. En el constructor de la clase instancia la variable drawableNave de forma similar como se ha hecho en drawableAsteroide.

3. Inicializa también en el constructor la variable navede la siguiente forma:

            nave = new Grafico(this, drawableNave);

4. En el método onSiceChange() posiciona la nave justo en el centro de la vista.

5. En el método onDraw() dibuja la nave en el Canvas.

6. Ejecuta la aplicación. La nave ha de aparecer centrada:

 

7. Si cuando situamos los asteroides, alguno coincide con la posición de la nave, el jugador no tendrá ninguna opción de sobrevivir. Sería más interesante asegurarnos de que al posicionar los asteroides estos se encuentran a una distancia adecuada a la nave, y en caso contrario tratar de obtener otra posición. Para conseguirlo puedes utilizar el siguiente código en sustitución de las dos líneas asteroide.setPosX(…) y asteroide.setPosY(…).

do{

      asteroide.setPosX(Math.random()*(ancho-asteroide.getAncho()));

      asteroide.setPosY(Math.random()*(alto-asteroide.getAlto()));

} while(asteroide.distancia(nave) < (ancho+alto)/5);

Ejercicio paso a paso: Evitando que VistaJuego cambie su representación con el dispositivo en horizontal y en vertical

 

 

1. Ejecuta la aplicación

2. Cambia de orientación la pantalla del dispositivo. En el emulador se consigue pulsando la tecla Ctrl-F11.

3. Observa cómo cada vez, se reinicializa la vista, regenerando los asteroides. Esto nos impediría jugar de forma adecuada. Para solucionarlo edita AndroidManifet.xml. En la lengüeta Application selecciona la actividad Juego. En los parámetros de la derecha selecciona en Screen orientation: landscape.

4. Ejecuta de nuevo la aplicación. Observa como la actividad Juego será siempre representada en modo horizontal, de forma independiente a la posición del teléfono.

5. Abre de nuevo las propiedades de la actividad Juego. En Theme selecciona el valor @android:style/Theme.NoTitleBar.Fullscreen. Este tema visualizará la vista ocupando toda la pantalla, sin la barra de notificaciones ni el nombre de la aplicación.

6. Si en Theme pulsas el botón Browse… y seleccionas el botón circular System Resources puedes ver una lista de estilos definidos en el sistema.

NOTA. En algunas instalaciones esta lista puede que no te funcione.

7. Ejecuta la aplicación en un terminal real y verifica el resultado.

NOTA: en un emulador si cambias la orientación (Crtl-F11) esta cambiará igualmente. Se trata de un error de simulación, al no soportar esta configuración.

{jcomments on}