sábado, 26 de marzo de 2011

Introducción a AndEngine (Parte V)

Anteriormente en Droideando



Objetivos de hoy

  • Terminar el CatapultDetector para hacerlo funcionar bien
  • Hacer que el jugador reaccione al Detector


Con el Detector


Ya tengo terminado el detector, pongo aqui el código completo y luego vamos a verlo por partes


CatapultDetector.java
  1. package com.pruebas.andengine;
  2.  
  3. import org.anddev.andengine.input.touch.TouchEvent;
  4. import org.anddev.andengine.input.touch.detector.BaseDetector;
  5.  
  6. import android.view.MotionEvent;
  7.  
  8. public class CatapultDetector extends BaseDetector {
  9. // ===========================================================
  10. // Constants
  11. // ===========================================================
  12. private static final float ANGLE_CONSTANT = 90;
  13. private static final float DEFAULT_MAX_DISTANCE = 80;
  14. // ===========================================================
  15. // Fields
  16. // ===========================================================
  17.  
  18. // Listener for the Detector
  19. private final ICatapultDetectorListener mCatapultDetectorListener;
  20. private float mMaxDistance;
  21.  
  22. // First Touch
  23. private float mFirstX;
  24. private float mFirstY;
  25.  
  26. // ===========================================================
  27. // Constructors
  28. // ===========================================================
  29.  
  30. public CatapultDetector(
  31. final ICatapultDetectorListener pCatapultDetectorListener) {
  32. this(DEFAULT_MAX_DISTANCE, pCatapultDetectorListener);
  33. }
  34.  
  35. public CatapultDetector(final float pMaxDistance,
  36. final ICatapultDetectorListener pCatapultDetectorListener) {
  37. this.setMaxDistance(pMaxDistance);
  38. this.mCatapultDetectorListener = pCatapultDetectorListener;
  39. }
  40.  
  41. // ===========================================================
  42. // Methods for/from SuperClass/Interfaces
  43. // ===========================================================
  44.  
  45. @Override
  46. protected boolean onManagedTouchEvent(TouchEvent pSceneTouchEvent) {
  47. final float touchX = this.getX(pSceneTouchEvent);
  48. final float touchY = this.getY(pSceneTouchEvent);
  49. final int action = pSceneTouchEvent.getAction();
  50.  
  51. switch (action) {
  52. case MotionEvent.ACTION_DOWN:
  53. this.mFirstX = touchX;
  54. this.mFirstY = touchY;
  55. return true;
  56. case MotionEvent.ACTION_MOVE:
  57. case MotionEvent.ACTION_UP:
  58. // case MotionEvent.ACTION_CANCEL:
  59. final float distanceX = Math.abs(touchX - this.mFirstX);
  60. final float distanceY = Math.abs(touchY - this.mFirstY);
  61. final float distance = Math.min((float) Math.hypot(
  62. (double) distanceX, (double) distanceY), mMaxDistance);
  63. final double angleX = touchX - this.mFirstX;
  64. final double angleY = touchY - this.mFirstY;
  65. final float angle = (float) Math.toDegrees(Math.atan2(angleY,
  66. angleX))
  67. + ANGLE_CONSTANT;
  68. if (action == MotionEvent.ACTION_MOVE) {
  69. this.mCatapultDetectorListener.onCharge(this, pSceneTouchEvent,
  70. distance, angle);
  71. } else {
  72. this.mCatapultDetectorListener.onShoot(this, pSceneTouchEvent,
  73. distance, angle);
  74. }
  75. return true;
  76. default:
  77. return false;
  78. }
  79. }
  80.  
  81. // ===========================================================
  82. // Getter & Setter
  83. // ===========================================================
  84.  
  85. public void setMaxDistance(float mMaxDistance) {
  86. this.mMaxDistance = mMaxDistance;
  87. }
  88.  
  89. public float getMaxDistance() {
  90. return mMaxDistance;
  91. }
  92.  
  93. // ===========================================================
  94. // Methods
  95. // ===========================================================
  96.  
  97. protected float getX(final TouchEvent pTouchEvent) {
  98. return pTouchEvent.getX();
  99. }
  100.  
  101. protected float getY(final TouchEvent pTouchEvent) {
  102. return pTouchEvent.getY();
  103. }
  104.  
  105. // ===========================================================
  106. // Inner and Anonymous Classes
  107. // ===========================================================
  108.  
  109. public static interface ICatapultDetectorListener {
  110. // ===========================================================
  111. // Constants
  112. // ===========================================================
  113.  
  114. // ===========================================================
  115. // Methods
  116. // ===========================================================
  117.  
  118. public void onCharge(final CatapultDetector pCatapultDetector,
  119. final TouchEvent pTouchEvent, final float pDistance,
  120. final float pAngle);
  121.  
  122. public void onShoot(final CatapultDetector pCatapultDetector,
  123. final TouchEvent pTouchEvent, final float pDistance,
  124. final float pAngle);
  125. }
  126.  
  127. }
  128.  
Parsed in 0.130 seconds at 31.87 KB/s, using GeSHi 1.0.8.10


Vamos primero a profundizar un poco en el Listener que es lo primero que hay que hacer bajo mi punto de vista


Listener de la clase CatapultDetector
  1. public static interface ICatapultDetectorListener {
  2. // ===========================================================
  3. // Constants
  4. // ===========================================================
  5.  
  6. // ===========================================================
  7. // Methods
  8. // ===========================================================
  9.  
  10. public void onCharge(final CatapultDetector pCatapultDetector,
  11. final TouchEvent pTouchEvent, final float pDistance,
  12. final float pAngle);
  13.  
  14. public void onShoot(final CatapultDetector pCatapultDetector,
  15. final TouchEvent pTouchEvent, final float pDistance,
  16. final float pAngle);
  17. }
Parsed in 0.091 seconds at 7.05 KB/s, using GeSHi 1.0.8.10

Este Listener tiene que responder a dos eventos, cuando estamos cargando y cuando soltamos para realizar el disparo. Para el primero usamos un método onCharge(), que le pasa los siguientes parámetros:

  • pCatapultDetector: El propio detector.
  • pTouchEvent: El evento en pantalla que ha desencadenado la acción.
  • pDistance: la distancia desde el punto incial de pulsación hasta el punto actual.
  • pAngle: En ángulo formado desde el primer punto de pulsación al actual. Este parámetro se puede aplicar a un sprite y girará correctamente .
Los parámetros del el onShoot() son los mismos, solo que el evento implica disparo. Veamos ahora las variables del Detector.


Variables de la clase CatapultDetector
  1. // ===========================================================
  2. // Constants
  3. // ===========================================================
  4. private static final float ANGLE_CONSTANT = 90;
  5. private static final float DEFAULT_MAX_DISTANCE = 80;
  6. // ===========================================================
  7. // Fields
  8. // ===========================================================
  9.  
  10. // Listener for the Detector
  11. private final ICatapultDetectorListener mCatapultDetectorListener;
  12. private float mMaxDistance;
  13.  
  14. // First Touch
  15. private float mFirstX;
  16. private float mFirstY;
Parsed in 0.086 seconds at 6.68 KB/s, using GeSHi 1.0.8.10

Tenemos primero las constantes

  • ANGLE_CONSTANT: En las pruebas que realicé con el Detector, tenia que sumarle 90 al ángulo que calculaba para que el sprite rotara en la dirección que yo necesitaba.
  • DEFAULT_MAX_DISTANCE: Distancia máxima de carga por defecto, si no se le indica ninguna, la velocidad máxima de carga son 80 pixeles. Esto quiere decir que si alejo el dedo mas de 80 píxeles del punto inicial de toque, no carga a 81 ni a 90, el máximo es 80. Recordad que siempre son 80 pixeles relativos a la resolución deseada de trabajar, Si luego ejecutamos el programa en un terminal con una resolución mayor, AndEngine controlará ésto .


Un repaso a las variables

  • mCatapultDetectorListener: Es un ICatapultDetectorListener que hemos visto antes. Es obligado para un Detector tener un Listener que "escuche" los eventos que este Detector dispara. Se le indica en el constructor de la clase.
  • mMaxDistance: Variable donde guardamos la máxima distancia actual para este detector.
  • mFirstX,mFirstY: Aqui guardamos la posición de la primera pulsación para calcular la distancia el ángulo.


Veamos los dos constructures de la clase


Constructores de la clase CatapultDetector
  1. // ===========================================================
  2. // Constructors
  3. // ===========================================================
  4.  
  5. public CatapultDetector(
  6. final ICatapultDetectorListener pCatapultDetectorListener) {
  7. this(DEFAULT_MAX_DISTANCE, pCatapultDetectorListener);
  8. }
  9.  
  10. public CatapultDetector(final float pMaxDistance,
  11. final ICatapultDetectorListener pCatapultDetectorListener) {
  12. this.setMaxDistance(pMaxDistance);
  13. this.mCatapultDetectorListener = pCatapultDetectorListener;
  14. }
  15.  
Parsed in 0.086 seconds at 5.98 KB/s, using GeSHi 1.0.8.10

Tenemos dos constructores. Al primero se le manda solamente el ICatapultDetectorListener que va a escuchar, con este constructor se usa la máxima distancia por defecto. Éste llama al segundo con el parámetro de maxima distancia con la constante antes vista. El segundo es el más completo, que aparte del listener recibe la máxima distancia de carga.


La miga del Detector está en el método onManagedTouchEvent(). Recibe como parámetro únicamente el TouchEvent. Vamos por partes.


  1. final float touchX = this.getX(pSceneTouchEvent);
  2. final float touchY = this.getY(pSceneTouchEvent);
  3. final int action = pSceneTouchEvent.getAction();
Parsed in 0.081 seconds at 1.88 KB/s, using GeSHi 1.0.8.10

Aqui usamos variables finales para guardar datos que vamos a usar despues. En la primera línea guardamos la posición X pulsada, en la segunda guardamos la posición Y y en la tercera guardamos la acción realizada, en este caso vamos a controlar los siguientes:

  • ACTION_DOWN : Cuando se pulsa la pantalla.
  • ACTION_MOVE : Cuando ya tenemos la pantalla pulsada y lo que hacemos es mover el dedo por la pantalla.
  • ACTION_UP : Acción de levantar el dedo.


Cuando el evento es ACTION_DOWN, lo único que hacemos es guardar los puntos donde hemos pulsado para posteriormente calcular ángulos y distancia.


Para el ACTION_MOVE y ACTION_UP tenemos un mismo código que controla los dos.


ACTION_MOVE y ACTION_UP
  1. final float distanceX = Math.abs(touchX - this.mFirstX);
  2. final float distanceY = Math.abs(touchY - this.mFirstY);
  3. final float distance = Math.min((float) Math.hypot(
  4. (double) distanceX, (double) distanceY), mMaxDistance);
  5. final double angleX = touchX - this.mFirstX;
  6. final double angleY = touchY - this.mFirstY;
  7. final float angle = (float) Math.toDegrees(Math.atan2(angleY,
  8. angleX))
  9. + ANGLE_CONSTANT;
  10. if (action == MotionEvent.ACTION_MOVE) {
  11. this.mCatapultDetectorListener.onCharge(this, pSceneTouchEvent,
  12. distance, angle);
  13. } else {
  14. this.mCatapultDetectorListener.onShoot(this, pSceneTouchEvent,
  15. distance, angle);
  16. }
  17. return true;
Parsed in 0.078 seconds at 8.91 KB/s, using GeSHi 1.0.8.10

Primera y segunda linea, cogemos el número de píxeles de distancia desde el primero toque hasta el actual. En la tercera línea calculamos con la función hypot de Math la distancia real entre los dos puntos de toque, el inicial y el actual. En esa tercera línea realmente cogemos el mínimo de la distancia Real y la variable mMaxDistance, con lo cual si la distancia es mayor que la máxima nos devuelve nMaxDistance. Ya tenemos calculada la distancia, vamos a por el ángulo.


La mejor manera que encontré de hallar el ángulo entre los dos puntos es la de las siguientes lineas. hasta que guardamos en la variable angle el valor del ángulo.


Luego si la acción es ACTION_MOVE lanzamos un onCharge() y si es un ACTION_UP lanzamos un onShoot().


Con esto terminamos por completo el Detector, habrá maneras más bonitas y eficientes de hacerlo, pero a mi con esta me vale por ahora. Si alguien ve mejoras/correcciones sobre el Detector le mandaré unas cañas virtuales.


Repaso a la clase Player


Primero si miramos el sprite que hemos creado del jugador, realmente lo hemos guardado en un objeto AnimatedSprite, para poder hacer la animación del balanceo de cuando se suelta el jugador o cuando la pelota lo golpee.



Tenemos aqui los diferentes frames de la animación. Vamos al tajo. Definimos una constante para definir el frame máximo para la carga, los dos últimos son para hacer el movimiento de rebote cuando se suelta.


  1. private static final int PLAYER_CHARGE_ANIMATIONS = 5;
Parsed in 0.073 seconds at 737 B/s, using GeSHi 1.0.8.10

Ahora añadimos un procedimiento a la clase


Añadir este procedimiento a la clase Player
  1. public void setCharge(final float angle, final float distance)
  2. {
  3. final int step = Math.round(distance * PLAYER_CHARGE_ANIMATIONS / Main.MAX_CHARGE_DISTANCE);  
  4. this.stopAnimation(step);
  5. this.setRotation(angle);  
  6. }
Parsed in 0.067 seconds at 3.32 KB/s, using GeSHi 1.0.8.10

En la primera linea sacamos por una regla de 3 el paso correspondiente a la distancia que nos ha enviado el Detector. Aqui usamos una constante Main.MAX_CHARGE_DISTANCE que definiremos después. Una vez tenemos el sprite que vamos a pintar, usamos el método stopAnimation de SpriteAnimation para parar la animación en ese frame y luego lo rotamos en el ángulo que toca.


Vamos ahora al Main.java y metemos esa constante. A mi se me quedan así.


  1. // ===========================================================
  2. // Constants
  3. // ===========================================================
  4. private static final int CAMERA_WIDTH = 480;
  5. private static final int CAMERA_HEIGHT = 320;
  6. public static final float MAX_CHARGE_DISTANCE = 80;
  7.  
  8. private static final String TAG = "AndEngineTest";
  9.  
Parsed in 0.069 seconds at 4.94 KB/s, using GeSHi 1.0.8.10

Ahora en el onLoadScene() tocamos donde se crea el CatapultDetector y le pasamos esta constante.


  1. this.mScrollDetector = new SurfaceScrollDetector(this);
  2. this.mScrollDetector.setEnabled(false); 
  3.  
  4. this.mCatapultDetector = new CatapultDetector(MAX_CHARGE_DISTANCE,this);
  5. this.mCatapultDetector.setEnabled(true);
Parsed in 0.089 seconds at 2.46 KB/s, using GeSHi 1.0.8.10

Y cambiamos el método onCharge()


  1. @Override
  2. public void onCharge(CatapultDetector pCatapultDetector,
  3. TouchEvent pTouchEvent, float pDistance, float pAngle) {
  4. this.mActivePlayer.setCharge(pAngle,pDistance);  
  5. }
Parsed in 0.076 seconds at 2.42 KB/s, using GeSHi 1.0.8.10

Donde le pasamos al jugador la carga. Con esto ya funciona perfectamente el movimiento. En mi caso esto funciona bien. Si teneis algún problema con esta parte del tutorial decidmelo y aclaro ese punto. Por hoy está bien, en el siguiente voy a intentar hacer las animaciones de soltar al jugador, y poner un HUD para el marcador.

6 comentarios:

pako dijo...

He seguido las 5 entradas de este tutorial y la verdad que está muy bien. En un par de dias te haces con la api de andengine rapidamente, y lo demás investigar.

5 estrellas hamijo.

jordi dijo...

Hola,
Estoy haciendo un juego con andengine. El "main" crea dos objetos de la clase "Zombie extends Entity".
Para usar las texturas he probado 2 sistemas.
1) cargar las texturas en onLoadResources(). y luego pasárselas al objeto Zombie para que las use en el AnimatedSprite que crea.
2) que el objeto de la clase Zombie cargue sus propias texturas.

cuando creo varios zombies
en el caso 1) me encuentro que existe una sincronización entre los AnimetedSprite . No se como hacer que se muestren diferentes fotogramas en el mismo momento. Haga lo que haga parece que están bailando la misma música.

en el caso 2) consigo que se independicen y funciona como yo espero hasta que intento subir el numero de objetos tipo zombie en pantalla. Mientras que en el sistema 1) puedo hacer muchos con el 2) en cuanto hago muchos el gráfico se convierte en un cuadrado blanco.


Me gustaría saber cual es la forma mas adecuada de gestionar las texturas y poder reutilizarlas sin que se sincronicen.
Muchas gracias

edades dijo...

Primero, Gracias! :)
Segundo, Iba bien la app, hasta la parte cuando estaba el scroll con la pelota en la cancha de futbol, ahora, después del Splash se va a negro y se cae.
Ojala pudieras darme una manito.
De todas formas con mas tiempo lo revisaré en detalle.

ESPERO MAS TUTORIALES, ya que esto ha sido lo mejor que he encontrado.
Felicidades y gracias :)

Miguel Montesinos dijo...

Acabo de encontrar tu blog, como dicen arriba lo mejor que hay explicando el andengine. Grandisimo trabajo!!

Espero que sigas adelante con el proyecto por que tiene muy buena pinta. Y juegos buenos en android se cuentan con las manos. Espero novedades!

Carlos dijo...

Hola! Gran blog! Me está ayudando mucho.

Me asaltan un par de dudas que me están volviendo loco, a ver si podéis ayudarme:

He visto el video de como queda al final la aplicación. He seguido tal cual la forma de crear el mapa con el editor de mapas Tiled, y he conseguido exactamente el mismo fichero .tmx que usas en la tercera parte del tutorial. Lógicamente, el resultado de aplicar ese mapa al background (un mapa de 4x8 filas y columnas de elementos de 120x120) no es el que aparece en el video, ajustado a las dimensiones del dispositivo.

La duda es ¿El mapa finalmente se vio modificado, o es el mismo? ¿Hay alguna manera de que Android ajuste automáticamente un mapa .tmx al tamaño de la pantalla, o hay que hacerlo "a mano"?

¿El código final del proyecto puede verse? Eso ayudaría bastante jeje!

Gracias de antemano por la ayuda!

Jesus de Diego dijo...

Felicidades por el excelente tutorial. Muchisimas gracias.

Estoy intentando hacer la animación de solatar el jugador pero no me acaba de funcionar.¿Cómo sería la implementación del método setShoot de la clase Player?. No acabo de enlazar las dos animaciones (setCharge y setShoot).

De nuevo muchas gracias.

Publicar un comentario