Documento - Departament d`Enginyeria Informàtica i Matemàtiques
Transcripción
Documento - Departament d`Enginyeria Informàtica i Matemàtiques
Departament d’Enginyeria Informàtica i M atemàtiques Desarrollo de un Videojuego para Android TITULACIÓN: Ingeniería Técnica en Informática de Sistemas AUTOR: Eduardo Santiso Martorell DIRECTOR: Carlos Molina Clemente FECHA: Febrero de 2013. RESUMEN La popularización de los dispositivos móviles se ha disparado en los últimos años, en parte gracias a la llegada de los smartphones o teléfonos inteligentes. Actualmente forman parte de la vida diaria de millones de personas en todo el mundo, independientemente de su edad o condición social. El sistema operativo Android es el más extendido y demandado en el mundo de los dispositivos móviles, seguramente por su adaptabilidad a todo tipo de dispositivos como por su sencillez, robustez y capacidad de personalización, logrando cubrir las necesidades de cualquier usuario. El sector de los videojuegos goza de prosperidad. Actualmente se está abriendo a nuevos mercados y se acerca a sectores de población inexplotados. El mercado de los videojuegos para móviles ya es más grande que cualquier otro mercado de videojuegos portátiles y se estima que vaya incrementando aún más. El presente proyecto busca crear un videojuego para dispositivos móviles Android desde cero. Para ello, llevaremos a cabo el diseño del juego con sus especificaciones y requisitos. Continuaremos viendo cómo construir un videojuego, creando un framework reutilizable para futuros proyectos. Después estudiaremos los fundamentos de las tecnologías que necesitaremos para, posteriormente, llevar a cabo la implementación. RESUM La popularització dels dispositius mòbils s’ha disparat en els darrers anys, en part gràcies a l’arribada dels smartphones o telèfons intel·ligents. Avui en dia, formen part de la vida diària de milions de persones a tot el món, independentment de l’edat o de la condició social. El sistema operatiu Android és el més estès i sol·licitat en el món dels dispositius mòbils, segurament per la seva adaptabilitat a tot tipus de dispositius així com també per la seva senzillesa, fiabilitat i capacitat de personalització, doncs aconsegueix cobrir les necessitats de qualsevol usuari. El sector dels videojocs gaudeix de prosperitat. Actualment, s’està obrint a nous mercats i s’apropa a sectors inexplotats de la població. El mercat dels videojocs per a mòbils és ja més gran que qualsevol altre mercat de videojocs portàtils i es preveu que augmenti encara més. Aquest projecte té la intenció de crear un videojoc per a dispositius mòbils Android des de zero. Per aconseguir-ho durem a terme tot el disseny amb les especificacions i requeriments. Continuarem veient com es construeix un videojoc, creant un framework reutilitzable per projectes futurs. Després estudiarem els fonaments de les tecnologies que necessitarem per, finalment, acabar realitzant la implementació. ABSTRACT The popularity of mobile devices has exploded in recent years, thanks in part to the arrival of smartphones. Nowadays, the smartphones are part of the daily lives of millions of people around the world, regardless of age or social status. The Android operating system is the most extended and requested in the world of mobile devices, probably due to its adaptability to all types of devices as well as for its simplicity, reliability and customization, capable of meeting the needs of any user. The video game industry is enjoying prosperity. Currently, it is opening to new markets and it approaches to unexploited sectors of the population. The mobile gaming market is already bigger than any other portable game market and is expected to increase even more. This project is intended to create a game for Android mobile devices from scratch. For this aim, the design of the game is carried out with its specifications and requirements. Then, the game is performed by creating a reusable framework for future projects. After that, the foundations for the needed technologies are studied to finish doing the implementation. INDICE 1 Introducción ................................................................................................................. 15 1.1 Dispositivos Móviles ........................................................................................... 15 1.2 Industria del Videojuego ..................................................................................... 16 1.3 Videojuegos portátiles ......................................................................................... 17 1.4 Sistemas Operativos para Móviles ...................................................................... 18 1.4.1 iOS de Apple. ................................................................................................ 18 1.4.2 Windows Phone ............................................................................................. 18 1.4.3 Symbian OS ................................................................................................... 19 1.4.4 BlackBerry OS............................................................................................... 19 1.4.5 Android .......................................................................................................... 20 1.5 2 3 ¿Por qué Android? ............................................................................................... 21 Objetivos del Proyecto ................................................................................................ 23 2.1 Requisitos Funcionales ........................................................................................ 24 2.2 Requisitos no Funcionales ................................................................................... 24 2.3 Requisitos de Rendimiento .................................................................................. 24 Especificaciones del Videojuego. ................................................................................ 25 3.1 Género i Objetivos del Videojuego. .................................................................... 25 3.2 Mecánica del Juego. ............................................................................................ 25 3.3 Control del Juego. ................................................................................................ 26 3.4 Pantallas. .............................................................................................................. 26 3.4.1 Menú Principal. ............................................................................................. 27 3.4.2 Ayuda. ........................................................................................................... 28 9 4 3.4.3 Ranking. ......................................................................................................... 29 3.4.4 Preparado. ...................................................................................................... 29 3.4.5 Juego. ............................................................................................................. 30 3.4.6 Pausa. ............................................................................................................. 31 3.4.7 Fin de Juego. .................................................................................................. 31 3.4.8 No Hay Movimientos. ................................................................................... 31 3.4.9 Diagrama entre Pantallas ............................................................................... 32 3.5 Sonido. ................................................................................................................. 34 3.6 Puntuación. .......................................................................................................... 34 Diseño .......................................................................................................................... 35 4.1 Estructura de un Videojuego ............................................................................... 35 4.2 Restricciones de Diseño....................................................................................... 35 4.3 MVC (Modelo – Vista – Controlador) ................................................................ 36 4.4 Entrada ................................................................................................................. 37 4.5 Archivo de I/O ..................................................................................................... 38 4.6 Audio ................................................................................................................... 38 4.6.1 4.7 Obteniendo los Recursos ............................................................................... 38 Gráficos ............................................................................................................... 39 4.7.1 La Pantalla ..................................................................................................... 39 4.7.2 Creando los Recursos .................................................................................... 40 4.8 4.7.2.1 Trabajar con Texto Empleando Fuentes Bitmap. ................................... 42 4.7.2.2 Mapa de Texturas. .................................................................................. 44 Gestor de Ventanas .............................................................................................. 45 10 5 4.9 Framework ........................................................................................................... 46 4.10 Lógica del Juego .................................................................................................. 46 Desarrollo. ................................................................................................................... 49 5.1 Conociendo Android y OpenGL. ........................................................................ 49 5.1.1 Preparando las Herramientas. ........................................................................ 49 5.1.2 Arquitectura del Sistema Operativo Android. ............................................... 50 5.1.3 Arquitectura Aplicación ................................................................................ 51 5.1.3.1 La pila de Actividades ............................................................................ 52 5.1.3.2 Ciclo de Vida de una Aplicación Android ............................................. 53 5.1.4 Estructura de una Aplicación Android .......................................................... 55 5.1.5 Rendimiento, el Recolector de Basura y Jit................................................... 57 5.1.6 El problema de la Fragmentación .................................................................. 58 5.1.7 OpenGL ES 1.0 ............................................................................................. 59 5.2 5.1.7.1 Como se Representa una Escena ............................................................ 60 5.1.7.2 Proyecciones........................................................................................... 61 5.1.7.3 Matrices .................................................................................................. 62 5.1.7.4 Sobre Vértices y Triángulos ................................................................... 62 5.1.7.5 Enviar Vértices a OpenGL ES ............................................................... 63 5.1.7.6 Texturas en OpenGL .............................................................................. 63 5.1.7.7 Filtrar Texturas ....................................................................................... 64 5.1.7.8 Espacio Mundo, Espacio Modelo........................................................... 64 5.1.7.9 Como Trabaja OpenGL ES 1.0 .............................................................. 65 Desarrollo del Juego ............................................................................................ 66 11 5.2.1 Estructura de Blocks ...................................................................................... 66 5.2.2 Framework ..................................................................................................... 67 5.2.2.1 Módulo Archivos Input/Output .............................................................. 67 5.2.2.2 Módulo Entrada ...................................................................................... 68 5.2.2.2.1 La clase Pool ...................................................................................... 69 5.2.2.2.2 El Controlador de Teclado ................................................................. 70 5.2.2.2.3 El Controlador Táctil ......................................................................... 71 5.2.2.2.4 Juntando las Entradas......................................................................... 72 5.2.2.3 Módulo Audio ........................................................................................ 72 5.2.2.3.1 Música ................................................................................................ 73 5.2.2.3.2 Efectos de Sonido .............................................................................. 74 5.2.2.3.3 Juntando el Audio .............................................................................. 74 5.2.2.4 Módulo Gráficos .................................................................................... 75 5.2.2.4.1 GLGraficos ........................................................................................ 75 5.2.2.4.2 Vector ................................................................................................. 75 5.2.2.4.3 La Cámara .......................................................................................... 78 5.2.2.4.4 Vértices .............................................................................................. 79 5.2.2.4.5 Texturas ............................................................................................. 81 5.2.2.4.6 Dibujando Texto ................................................................................ 83 5.2.2.4.7 Lote de Modelos ................................................................................ 84 5.2.2.4.8 Medir el Rendimiento ........................................................................ 87 5.2.2.4.9 Otras Clases ....................................................................................... 87 5.2.2.5 Módulo Gestor de Ventanas ................................................................... 88 12 5.2.3 Los elementos del Juego ................................................................................ 91 5.2.3.1 Configuración ......................................................................................... 91 5.2.3.2 Recursos ................................................................................................. 92 5.2.3.3 La Actividad Principal ........................................................................... 93 5.2.3.4 Pantallas ................................................................................................. 93 5.2.3.4.1 Menú Principal ................................................................................... 94 5.2.3.4.2 Pantallas de Ayuda............................................................................. 95 5.2.3.4.3 Pantalla Ranking ................................................................................ 96 5.2.3.4.4 Pantalla Juego .................................................................................... 97 5.2.3.5 Definir el Tablero de Juego .................................................................. 102 5.2.3.5.1 Bloque .............................................................................................. 103 5.2.3.5.2 Tablero ............................................................................................. 103 5.2.3.5.3 TableroRenderer .............................................................................. 110 6 Evaluación ................................................................................................................. 115 7 Coste .......................................................................................................................... 117 8 Trabajos Futuros ........................................................................................................ 119 9 Conclusiones.............................................................................................................. 121 10 Recursos Utilizados ................................................................................................... 123 11 Planning Temporal. ................................................................................................... 125 12 Referencias ................................................................................................................ 127 13 14 1 Introducción En el presente apartado, veremos una introducción al presente proyecto, en él discutiremos desde una perspectiva general sobre los dispositivos móviles y los videojuegos, también veremos los sistemas operativos más utilizados en móviles analizando sus puntos fuertes y débiles y finalizaremos con las razones que nos han llevado a escoger entre uno u otro para el presente proyecto. 1.1 Dispositivos Móviles El teléfono móvil nace de la necesidad de comunicación en la distancia de un lugar a otro. Los primeros sistemas están datados a finales de los años 40, eran enormes y pesados y funcionaban por radio analógica. En muy pocos años los teléfonos móviles evolucionaron pasando por varias generaciones, popularizándose a partir de los años 90, y con su éxito los fabricantes no han parado de innovar intentando destacar sobre los de la competencia. Figura 1. Smartphones El avance de la tecnología ha permitido que estos aparatos incorporen funciones que no hace mucho parecían futuristas, como juegos, reproducción de música, correo electrónico, SMS, agenda electrónica PDA, fotografía digital, video digital, videollamada, navegación por Internet, GPS… y permiten la instalación de programas incrementando así la el procesamiento de datos. Se conocen como Smarphones (en español teléfono inteligente) y funcionan como un ordenador un teléfono móvil con características cercanas a un ordenador personal. 15 Actualmente los teléfonos móviles han llegado a convertirse en un dispositivo esencial en nuestras vidas y los números indican que el mercado de los smartphones y tablets está en alza. La población demanda cada vez más este tipo de dispositivos. Según los datos de Strategy Analytics, en el tercer trimestre de 2011 se estimaba que existían 708 millones de usuarios de smartphones en todo el mundo. Sólo un año después la cifra ha alcanzado los 1.038 millones. Esto significa que uno de cada siete habitantes en el mundo tiene un smartphone. 1.2 Industria del Videojuego Los primeros videojuegos modernos aparecieron en la década de los 60, eran juegos simples que se desarrollaban completamente sobre hardware. Desde entonces el mundo de los videojuegos no ha cesado de crecer y desarrollarse con el único límite impuesto por la creatividad de los desarrolladores y la evolución tecnológica. Fue en la década de los 80 cuando se experimentó el «boom» de los videojuegos como entretenimiento, con la llegada de las consolas domésticas y títulos como Space Invaders, Pac-man, Donkey Kong, o Tetris. La expansión del videojuego es tan relevante que actualmente se trata de una industria multimillonaria capaz de rivalizar con las industrias cinematográfica y musical. [1] Figura 2. Estadísticas de ventas de videojuegos, música y cine. Por ESRB. 16 Igual que los dispositivos móviles, el videojuego ha crecido ligado a la tecnología, adaptando sus gráficos, sonido y jugabilidad, llegando a cotas inimaginables hace unos años. 1.3 Videojuegos portátiles Hoy en día con el avance de la tecnología en los teléfonos inteligentes, la industria del videojuego pasó de ser un entretenimiento de locales especializados, a la palma de la mano de cualquier persona. La reciente eclosión de los smartphones los ha convertido en la plataforma de juegos de esta nueva era, capaces de competir con las consolas portátiles como Nintendo DS o Playstation Portable de Sony. El mercado de los videojuegos para móviles ya es más grande que cualquier otro mercado de videojuegos portátiles, en 2011 los videojuegos de iOS y Android han triplicado su dominio en los EE.UU. superando a Nintendo y Sony al acaparar cerca del 60% de la cuota de mercado de los videojuegos portátiles. Figura 3. Estadísticas del videojuego portátil. iOS y Android como plataforma de juegos han incrementado sus ingresos de 2009 a 2011: de 500 millones de dólares (366,5 millones de euros) en 2009, a 800 millones de dólares (586,5 millones de euros) en 2010 y 1.900 millones de dólares (1.390 millones de euros) en 2011. 17 Existen muchos géneros de videojuegos: deportivos, arcade, estrategia… destacamos el género de los rompecabezas, ya que se adaptan perfectamente a los smartphones y gozan de gran popularidad. Títulos como Tetris o Bejeweled [2] son conocidos por todos. 1.4 Sistemas Operativos para Móviles Un sistema Operativo para móvil está pensado para controlar un dispositivo portátil como un SmartPhone o tableta. Su funcionamiento es muy similar al de cualquier otro sistema operativo de equipo de sobremesa pero con un diseño más sencillo y especialmente pensado para que el usuario interactúe con él mediante pantalla táctil, voz, teclado virtual… etc. Veremos a continuación los sistemas operativos más destacados: Android, iOS, BlackBerry OS, Symbian, Windows Mobile. 1.4.1 iOS de Apple. iOS es un sistema operativo desarrollado por la compañía Apple Inc. Para los dispositivos móviles iPod touch, iPhone e iPad. Está basado en una variante del Mach kernel de Mac OS X, que a su vez es una variante de Unix. Posee una interfaz gráfica gestual que se caracteriza por un buen diseño, funcionalidad y facilidad de uso. Su perfecta integración con servicios en la nube y equipos de sobremesa, especialmente Mac, es otro de sus puntos fuertes. Además de contar con una variedad de aplicaciones enorme. Pero el sistema operativo de Apple es cerrado, lo que significa que hay menos posibilidades de cambiar la forma de funcionar del teléfono y un control más rígido de las aplicaciones publicadas. Además los dispositivos de Apple tienen un precio bastante alto. 1.4.2 Windows Phone Es el sistema operativo móvil desarrollado por Microsoft como sucesor de la plataforma Windows Mobile. Windows Phone forma parte de los sistemas operativos con interfaz natural de usuario. Se basa en el núcleo del sistema operativo Windows CE y cuenta con un conjunto de aplicaciones básicas utilizando las API’s de Microsoft Windows. 18 Un diseño moderno, práctico, atractivo y con características innovadoras lo convierte en un sistema moderno y capaz de competir con los más grandes. Sin embargo la variedad de móviles con Windows Phone no es tan amplia como la que ofrecen Android o Symbian. Por otra parte, al llegar más tarde que sus competidores posee menor cantidad de aplicaciones disponibles. 1.4.3 Symbian OS Es un sistema operativo móvil que nace de la alianza de varias empresas de telefonía móvil, actualmente está mantenido por la consultora Accenture. Symbian ha sido siempre fiable e innovador. Con fuerte énfasis en las funciones básicas de telefonía y multimedia de sus dispositivos, también cuenta con un amplio mercado de aplicaciones externas y con una tremenda variedad de dispositivos disponibles. Se trata de una excelente opción para conseguir terminales de gama media y baja, debido a su fiabilidad, una cantidad razonable de buenas aplicaciones, posibilidades multimedia y al precio asequible de muchos de sus modelos. Lamentablemente Symbian ha perdido protagonismo con la llegada de iPhone y Android, sobre todo en los smartphones punteros. No se puede comparar la cantidad de aplicaciones con las de la competencia. Se ha confirmado de forma oficial que Symbian tendrá soporte hasta el año 2016, por no ser un competidor de Android, iOS o Windows Phone. 1.4.4 BlackBerry OS El BlackBerry OS es un sistema operativo móvil multitarea desarrollado por Research In Motion para sus dispositivos BlackBerry. El sistema permite multitarea y tiene soporte para diferentes métodos de entrada adoptados por RIM para su uso en computadoras de mano, particularmente la trackwheel, trackball, touchpad y pantallas táctiles. RIM usa un kernel propio que al igual que Android, tiene un motor Java. Con una interfaz sencilla el SO BlackBerry está claramente orientado a su uso profesional como gestor de correo electrónico y agenda. Blackberry destaca también por los aspectos de seguridad y por sus teclados QWERTY que al estilo de un teclado de PC, permiten una escritura muy rápida. 19 No obstante la tienda de aplicaciones no es comparable con las de Android o iPhone. Tampoco existen tantas posibilidades de elección en cuanto a dispositivos y el potencial multimedia no es su fuerte principal. 1.4.5 Android Android, es el nombre con el que se denomina al SO para dispositivos móviles desarrollado por la Open Handset Alliance (una alianza de empresas comandada por Google y en las que participan organizaciones tales como: Motorola, Samsung, HTC, LG, Sony Ericsson, entre otras). Figura 4. Logotipo y robot mascota de Android Originalmente creado para dispositivos móviles, posteriormente expandió su desarrollo para soportar otros dispositivos tales como tablets, reproductores MP3, netbooks, PC’s e incluso televisores. Como Android es un sistema escrito en código abierto, los fabricantes de dispositivos móviles apenas se han encontrado trabas a la hora de recurrir a esta plataforma. Pueden fabricar dispositivos para todos los bolsillos y modificar Android para ajustarlo a la potencia del procesador. Es decir Android no es un sistema exclusivo sino que puede incorporarse a muchos terminales. Pero Android es mucho más que un sistema operativo, es también un lenguaje de programación y un framework para desarrollo de aplicaciones. Es decir, que al conjunto de todos esos elementos se lo llama Android. Se trata de un SO multitarea basado en una navegación en iconos y escritorios con una interfaz agradable y sencilla. Es completamente personalizable y cuenta con un gran número de aplicaciones disponibles, destacando las de la propia Google. 20 Otro punto a favor de Android es la increíble confianza que está recibiendo de los fabricantes. Gracias a ello, la oferta de teléfonos con Android es amplia y la oferta es variada tanto en marcas como en precios. Hoy en día uno de los aspectos negativos de Android es su fragmentación, aunque va mejorando actualizar el sistema operativo a nuevas versiones no es tan fácil. 1.5 ¿Por qué Android? Al investigar las plataformas móviles para ver cuál se adapta más a las necesidades del proyecto, hemos visto que Blackbery OS está orientado a las comunicaciones sociales, que Symbian OS está en vías de extinción y que Windows Phone ha llegado tarde y aún le falta para despegar, así que dudamos entre Android e iOS. Desde la perspectiva de desarrolladores de software escoger la plataforma adecuada es de vital importancia, basando la elección normalmente en el público potencial que podría descargar el producto. Android es el sistema operativo más extendido en el mundo en los dispositivos móviles por delante de sus competidores y las cifras indican que Android crece más rápido. En la figura 5 podemos observar la cuota de mercado de los sistemas operativos móviles a fecha de Noviembre del 2011, según un estudio de Millennial Media. Figura 5. Comparativa de SO móviles. 21 Además Android nos ofrece muchas otras ventajas respecto a sus competidores: - No está atado a un único fabricante de dispositivos. - Es Open Source. - El kit y herramientas de apoyo para desarrollar son gratuitas. - Posibilidad de adaptar nuevos dispositivos. - Tiene una gran comunidad de desarrolladores detrás. - El nivel de personalización es mayor. - Posibilidad de compartir los proyectos. - Android está en alza, cada día hay más usuarios y muchas empresas buscan desarrolladores. Otro gran inconveniente es que para desarrollar para iOS se necesita un Mac, y a la hora de realizar este proyecto se persigue no invertir dinero en él. 22 2 Objetivos del Proyecto El objetivo global de este proyecto de fin de carrera es desarrollar un videojuego para entornos móviles, en este caso para el sistema operativo Android. Para ello se pretende aprender sobre diseño y desarrollo de videojuegos y cómo funciona el sistema Android. También se busca crear un framework (un marco de trabajo, es una estructura con módulos definidos para afrontar prácticas similares) estándar para Android que sirva para afrontar posibles futuros proyectos de forma más rápida y cómoda. Hay que tener en cuenta que los juegos para móviles son diferentes a los de sobremesa en varios aspectos, así que existen otros objetivos que debemos tener en cuenta: • Que tenga una mecánica sencilla. • Las partidas sean cortas. • Que gestione las interrupciones de un móvil. Tener en cuenta que es para smartphone. • Que sea multidispositivo. • Crear un framework estándar que sirva para afrontar futuros proyectos de forma más rápida y cómoda. Al tratarse de un videojuego para móvil tenemos en cuenta que los jugadores pueden sufrir déficit de atención, es decir juegan en el metro, autobús, en clase… Esto nos lleva a pensar en un juego de mecánica sencilla y creemos que lo mejor es crear un juego del tipo rompecabezas. El tiempo de juego no lo elige el usuario, buscaremos intencionadamente que las partidas tengan un ritmo rápido y que el tiempo entre partida y partida sea corto, para suscitar al jugador a intentar batir la puntuación máxima, es decir queremos que nuestro juego tenga una dinámica rápida, sencilla y adictiva. Así mismo es muy importante hacer que el videojuego se adapte y funcione en la mayoría de terminales distribuidos. Para hacer la aplicación multidispositivo, es necesario, en un principio, adaptarse al tamaño de la pantalla. Crearemos unos gráficos originales y simples para que funcione bien en todos los dispositivos. 23 Debemos tener en cuenta al jugar que puede haber interrupciones, llamadas, mensajes… queremos que nuestro juego sea capaz de interrumpirse y volver a su estado donde lo dejó. Así mismo otro objetivo que nos marcamos desde el principio es no invertir capital en este proyecto. 2.1 Requisitos Funcionales Nuestra aplicación tiene varios requisitos principales: • Permitir que el usuario interactúe con las pantallas y juegue con la aplicación. • Debe poderse salir de la aplicación en cualquier momento. • Ha de ofrecer al usuario la opción de desactivar el sonido. • Tiene que gestionar las interrupciones del móvil. 2.2 Requisitos No Funcionales • La aplicación se programará en Java, para dispositivos móviles con Android. • El juego se desarrollará para la versión 2.2 de Android. Por lo que también será compatible con versiones posteriores. • Se utilizará la versión de OpenGL ES 1.0 para los gráficos. • El idioma de la aplicación será en español. • El uso de la aplicación tiene que ser sencillo e intuitivo. • Nuestra aplicación necesita el uso de almacenamiento externo para guardar los datos de configuración. 2.3 Requisitos de Rendimiento Nuestra aplicación podemos considerarla de tiempo real blando ya que establecemos unos periodos que deben de cumplirse para una correcta funcionalidad de la aplicación pero el margen de error es flexible. Nos marcamos el objetivo de poder mostrar sesenta frames (imágenes) por segundo. La aplicación deberá estar optimizada sobre el parámetro del tiempo sacrificando el consumo de memoria principal. 24 3 Especificaciones del Videojuego. En esta sección describiremos la parte del diseño de especificaciones del juego. 3.1 Género i Objetivos del Videojuego. Blocks es un videojuego tipo rompecabezas para un jugador. Es un clon de Bejeweled [2]. Bejeweled fue el primer juego desarrollado por PopCap Games, vendió más de 5 millones de unidades y fue nombrado el Mejor Juego de Puzzle del 2001. El objetivo de nuestro juego es conseguir la mayor puntuación posible. Para ello debemos ir avanzando niveles antes que termine el tiempo, logrando eliminar un número especificado de bloques en cada nivel. Figura 6. Bejeweled. 3.2 Mecánica del Juego. Es un juego con una mecánica simple y sencilla, eliminar bloques del mismo color de un tablero, mediante la alineación de tres o más en una fila o columna. • Para eliminar los bloques deberemos intercambiar de posición dos bloques que estén en posiciones adyacentes (vertical u horizontalmente) de manera que formen una línea de al menos tres bloques del mismo color (las diagonales no cuentan). Si 25 intercambiamos dos bloques y no consiguen formar ninguna línea de tres o más, éstos volverán a su posición inicial. • Una vez eliminados, si quedan casillas libres en el tablero por debajo de un bloque este caerá hacia abajo debido a la gravedad y en las casillas libres de la parte superior del tablero aparecerán nuevos bloques con color aleatorio. Si esto da lugar a una nueva línea de tres o más, esta línea también desaparece. • El juego termina al agotarse el tiempo. Habrá en pantalla un indicador de tiempo restante. Al avanzar de nivel el tiempo se restablece, pero como más se avance, el tiempo por nivel será menor. • Para avanzar de nivel, debemos reunir el número mínimo de bloques especificado que aparece en la parte superior de la pantalla. Este crece a cada nivel que completemos. Decidimos no poner una escena de cambio de nivel para no cortar el ritmo de juego, consiguiendo así que sea más rápido. • Obtendremos puntos por cada bloque eliminado, así pues cuantos más bloques eliminemos más puntos conseguimos. • Existe un bloque comodín que aparecerá en el tablero aleatoriamente. Al pulsar sobre él elimina todos los bloques de un mismo color, éste también será aleatorio. • La Lupa es una ayuda que al pulsarla desataca un movimiento de bloques que puede formar línea. Obtendremos lupas al alcanzar cierta puntuación. Empezaremos el juego con dos y el máximo disponible serán tres. 3.3 Control del Juego. Blocks se controla única y exclusivamente mediante la pantalla táctil del dispositivo, de forma sencilla e intuitiva. Pulsando y soltando los botones de los menús y distintas pantallas y deslizando el dedo para intercambiar los bloques de posición. 3.4 Pantallas. En este apartado vamos a diseñar las pantallas y transiciones que habrá entre ellas. Sin embargo debemos comprender la finalidad de una pantalla: • Una pantalla es una unidad independiente que ocupa por completo el visor del dispositivo y que es la responsable de una parte del juego. Por ejemplo, 26 será la que se ocupe del menú principal, de la ayuda del juego o de la pantalla del juego donde tenga lugar la acción. • Una pantalla puede estar compuesta por varios componentes como por ejemplo, botones, etiquetas, imágenes y los bloques que forman el tablero. • Una pantalla permite al usuario interactuar con los elementos del juego. Estas interacciones pueden iniciar transiciones de pantalla. Por ejemplo al presionar el botón Jugar. Con estas definiciones pasamos a especificar las pantallas o escenas de nuestro juego, lo primero que mostrará al lanzar la aplicación será el menú principal. 3.4.1 Menú Principal. El juego arrancará con esta pantalla (figura 7) dónde veremos una animación del logo (rotación y escalado) hasta conseguir su posición correcta, debajo de éste unos botones táctiles rectangulares: - Jugar, que nos permitirá empezar la partida - Instrucciones, nos llevará a la pantalla que nos enseña como jugar al juego. - Ranking, que muestra las nueve mejores puntuaciones del juego. Figura 7. Pantalla Menú Principal Además debajo de éstos y en las esquinas inferior izquierda encontraremos un botón circular con un icono de sonido, que al pulsarlo activa/desactiva el sonido del juego y cambiará el icono según el estado en que se encuentre. 27 Para salir del juego bastará con pulsar el botón atrás del dispositivo móvil en cualquier momento. 3.4.2 Ayuda. En esta escena encontraremos la información para jugar al juego, la dividiremos en varias pantallas y explicada gráficamente para no saturar al jugador. En cada pantalla veremos una etiqueta que nos indique en que pantalla de ayuda nos encontramos, es decir 1/3, 2/3 o 3/3. Para movernos por las distintas pantallas habrá uno o dos botones táctiles (según en la pantalla de ayuda que nos encontremos) en la esquina inferior derecha adelante y atrás, para avanzar o retroceder. En la esquina inferior izquierda encontraremos un botón táctil volver, que al pulsarlo nos llevará al Menú principal. En la primera pantalla explica como intercambiar bloques para formar líneas y conseguir que desaparezcan, y como los superiores caen por gravedad. En la segunda pantalla, mostramos la pantalla de juego y explicamos la interfaz del juego: los indicadores, el botón de pausa y las lupas. En la tercera pantalla explicamos las reglas del juego: cuando termina el juego, cómo avanzas de nivel, si el tablero se queda sin movimientos, el bloque comodín… Figura 8. Pantalla Instrucciones 28 3.4.3 Ranking. En la pantalla de ranking, aparece el título ranking y debajo se nos mostraran las nueve mejores puntuaciones ordenadas de mayor a menor. En la figura 9 podemos ver esta pantalla. En la esquina inferior derecha encontraremos un botón volver, que al pulsarlo nos llevará al Menú principal. Figura 9. Pantalla Ranking 3.4.4 Preparado. El juego no empezará inmediatamente, se le da algo de tiempo al usuario para que se prepare. En esta escena aparece el tablero de juego, y una etiqueta en el centro de la pantalla ¿Preparado? La podemos ver en la Figura 10. Figura 10. Pantalla Preparado. 29 Esta escena espera a que el usuario toque la pantalla para empezar la partida, llevándonos a la escena Juego. 3.4.5 Juego. Una vez iniciada la partida encontraremos las etiquetas que nos informan sobre el estado del juego: en la parte inferior podremos observar el nivel en que se encuentra el jugador, y el tiempo restante. En la barra superior aparecerán las etiquetas con el número restante de bloques a eliminar y la etiqueta con la puntuación. Todas estas etiquetas irán cambiando a medida que vayamos jugando. En la barra de la derecha aparecerán los iconos de los botones lupa, transparentes si no están disponibles y pintados si están disponibles. En la esquina inferior derecha de la pantalla veremos un icono táctil con el símbolo de pausa, al pulsarlo nos llevará a la escena pausa. Sobre el tablero veremos los bloques. En la figura 11 podremos observar una imagen de la escena juego. Figura 11. Pantalla Juego Si el usuario hace una combinación y el tablero se queda sin movimientos disponibles, aparecerá la escena No hay movimientos. Cuando el juego termine porque se agote el tiempo, saltaremos a la escena fin de juego. 30 3.4.6 Pausa. Esta pantalla se mostrará al pulsar el botón de pausa durante el juego, en ella veremos dos botones táctiles: - Continuar: para continuar la partida tal en el punto en que estaba. - Salir: Que nos permite ir al menú principal, terminando la partida actual. Además en la parte inferior izquierda se encontrará el botón táctil con el icono sonido, para activar o desactivar el audio del juego. En la figura 12 vemos una imagen de la pausa del juego. Figura 12. Pantalla pausa 3.4.7 Fin de Juego. En esta escena se mostrará al finalizar el tiempo de juego. Se compone del tablero en el fondo, una imagen Fin de Juego y una etiqueta de texto con la puntuación obtenida en la partida. Si se consigue batir las anteriores mejores puntuaciones nos avisará que hemos conseguido un nuevo récord. Esta pantalla se muestra en la figura 13. Saldremos de esta escena tocando la pantalla y nos llevará a la pantalla de Ranking. 3.4.8 No Hay Movimientos. Aparece cuando no hay combinaciones posibles sobre el tablero. En esta veremos una 31 Figura 13. Pantalla Fin de Juego etiqueta en la parte superior que nos avisa de que el tablero se ha quedado sin movimientos disponibles, otra etiqueta en la parte inferior que nos avisa que se está generando un nuevo tablero, y una cuenta atrás que al llegar a cero, nos conduce otra vez a la escena juego. La podemos ver en la figura 14. Figura 14. Pantalla no hay movimientos 3.4.9 Diagrama entre Pantallas Hemos explicado las transiciones entre pantallas pero vamos a mostrarlas en un gráfico para hacernos una idea visual, lo podemos ver en la figura 15. 32 - Figura 15. Las pantallas de Blocks y sus transiciones 33 3.5 Sonido. El sonido del juego sonará la primera vez que arranque la aplicación. El usuario podrá desactivarlo y el juego recordará la preferencia del jugador para futuras ocasiones. El volumen podrá ajustarse con los botones laterales del dispositivo móvil. Si el sonido está activado sonará la música durante todas las escenas del juego. Los efectos de sonido se producirán cuando tenga lugar alguno de estos eventos. - Al pulsar sobre cualquier botón de la interfaz de usuario (jugar, pausa, sonido, continuar, lupa…). 3.6 - Al avanzar de nivel. - Al obtener una lupa extra. - Al eliminar bloques. - Al intercambiar bloques y que no formen línea. - Al combinar bloques de modo que formen una línea. - Si combinamos bloques y no forman línea. - Al pasar de nivel. Puntuación. Los puntos se obtienen asociando bloques, en concreto 10 puntos por bloque destruido, es decir si asociamos 3 bloques sumaremos 30 puntos, si asociamos 4 sumaremos 40, y así respectivamente. Cada 500 puntos el jugador obtendrá una lupa extra. 34 4 Diseño En el presente capítulo se presentarán los detalles de diseño del proyecto, estudiaremos cómo trabaja un videojuego, primero definiremos los requisitos de diseño y seguidamente nos acercaremos a un modelo de diseño muy útil. Éste lo utilizaremos para separar el funcionamiento por módulos, entendiendo las tareas que implementa cada uno y sin acercarnos aún a los detalles de la implementación. Encontraremos en este apartado la creación de los recursos necesarios para la interfaz visual y sonora de nuestro juego. 4.1 Estructura de un Videojuego Desde un punto de vista general, un videojuego es una aplicación gráfica en tiempo real en la que existe una interacción explícita mediante el usuario y el propio videojuego. En este contexto, el concepto de tiempo real se refiere a la necesidad de generar una determinada tasa de frames o imágenes por segundo, para que el usuario tenga una sensación continua de realidad. Por otra parte, con interacción se entiende a la forma de comunicación existente entre el usuario y el videojuego. Como ya hemos visto anteriormente, en nuestro juego esta interacción será táctil mediante la pantalla del dispositivo. Desde un punto de vista abstracto, una aplicación gráfica en tiempo real se basa en un bucle donde en cada iteración se realizan los siguientes pasos: • El usuario visualiza una imagen renderizada por la aplicación en la pantalla o dispositivo de visualización. • El usuario actúa en función de lo que haya visualizado, interactuando directamente con la aplicación. • En función de la acción realizada por el usuario, la aplicación gráfica genera una salida u otra. 4.2 Restricciones de Diseño Vamos a diseñar nuestro juego para una resolución de pantalla fija de 480x320. 35 En el diseño de la aplicación deben primar los tiempos de respuesta sobre el consumo de recursos de espacio como la memoria principal o secundaria. Esta es la principal restricción que tendrá el diseño de nuestro videojuego. 4.3 MVC (Modelo – Vista – Controlador) Usaremos el patrón de diseño MVC (Model-View-Controller) [3], para aislar la lógica de la parte de presentación (interfaz de usuario). Es un modelo muy útil para facilitar la evolución por separado de sus aspectos, y además los convierte en flexibles y reutilizables. La interfaz gráfica será la View (vista), el bucle de eventos hará de Controller (controlador) y las estructuras de datos internas actuaran como Model (modelo). Figura 16. Esquema MVC. Para ello crearemos un framework básico que facilite el proceso y la comunicación con el SO que se encuentra por debajo. Se divide en varios módulos como veremos a continuación: • Entrada: Gestiona los Inputs del usuario, está relacionado con el gestor de ventanas. • Archivos I/O: Para guardar, recuperar los datos de nuestro juego que almacenaremos en una unidad de disco. • Audio: carga y reproduce cualquier sonido que emita el juego. 36 • Gráficos: El responsable de cargar los gráficos y dibujarlos en pantalla. • Gestor de ventanas: es el responsable de la creación de una ventana y de copiarla en la aplicación de Android con elementos que permitan, por ejemplo, cerrarla o detener/continuar el juego. Figura 17. Esquema de un Framework básico. 4.4 Entrada Cuando el usuario toca la pantalla para interactuar con nuestro juego se produce un evento, es el módulo de entrada quien se encarga de gestionar estos eventos. Los registra y los guarda. La pantalla táctil puede generar tres tipos de eventos, en ellos se guarda además la posición relativa y un índice del puntero: • Tocar la pantalla. • Arrastrar el dedo, previamente se deberá haber tocado la pantalla. • Levantar el dedo. El teclado del dispositivo puede generar dos tipos de eventos, en éstos se guarda el código de la tecla y a veces el carácter Unicode. • Tecla presionada: cuando se presiona una tecla. • Tecla sin presionar: cuando se deja de presionar una tecla. Estos eventos sólo los manejaremos para que no se sobrecargue la memoria haciendo saltar al colector de basura, (lo veremos más adelante). 37 4.5 Archivo de I/O Este módulo nos servirá para leer y escribir datos en los archivos. En nuestro caso nos interesará leer los que empaquetaremos durante la generación de nuestro juego, como por ejemplo, imágenes y archivos de audio. Por otra parte usaremos la escritura para guardar las puntuaciones máximas, la configuración del juego y para guardar el estado del juego cuando se produzca una interrupción, de modo que el usuario pueda retomarlo allí donde lo dejó. Evidentemente el módulo debe ser capaz también de leer los datos que guardemos. 4.6 Audio El módulo de Audio se encargará de reproducir cualquier sonido de nuestro juego. Diferenciamos aquí entre la música y los efectos de sonido de nuestro juego. Al reproducir música sólo lo haremos de un elemento a la vez, accederemos una vez al disco. Usaremos un archivo comprimido en MP3 para reducir el espacio que ocupe el archivo. Cuando reproduzcamos efectos sonoros, los cargaremos en memoria para poder reproducirlos simultáneamente, procuraremos que sean archivos pequeños para que no ocupen mucho. Usaremos el formato OGG. El módulo: • Proporcionará el modo de cargar los archivos de audio para reproducirlo sobre la marcha o desde la memoria. • Controlará la reproducción de los archivos de música y sonido: inicio de reproducción, pausa, detener, bucle, volumen. 4.6.1 Obteniendo los Recursos Como música usaremos una melodía en formato mp3 que hemos descargado de internet. Se llama The Whistle Song [4], es de licencia gratuita siempre que se cite al autor de la misma. Para los efectos de sonido, cogeremos prestados unos que hemos encontrado en internet. 38 4.7 Gráficos Este módulo es el que se encarga de dibujar en la pantalla las imágenes de nuestro juego. Tareas del módulo: • Cargar las imágenes almacenadas en el disco y guardarlas en la memoria para dibujarlas más tarde. • Limpiar el la pantalla con un color, borrando cualquier contenido almacenado. • Dibujar las imágenes que hayamos cargado al inicio en la pantalla. Queremos dibujar la imagen completa o partes de ella. Vamos a crear ahora nuestros recursos gráficos, teniendo en cuenta que usaremos la librería gráfica OpenGL ES 1.0. 4.7.1 La Pantalla Una vez especificadas las pantallas de nuestro juego debemos crear los recursos para colocarlos en pantalla como los diseñamos. Ya dijimos que usaremos una resolución fija de 480x320. Pero veamos qué significa esto. La pantalla se basa en una cuadrícula, donde cada cuadro es un píxel. Su posición viene determinada por dos coordenadas en números enteros (ancho y alto). Figura 18. Sistema de coordenadas de una pantalla. El origen de coordenadas se encuentra en la esquina superior izquierda de la cuadrícula. Los valores positivos del eje x apuntan hacia la derecha y los del eje y hacia abajo. La cuadrícula de la pantalla es finita y las coordenadas que se encuentren fuera de estos 39 límites están fuera de la pantalla. El valor máximo que pueden adquirir las coordenadas serán el ancho o alto de la pantalla menos 1. Esto es así porque el origen coincide con el píxel que se encuentra al inicio del todo. Podemos ver el sistema de coordenadas de la pantalla en la figura 18. 4.7.2 Creando los Recursos En OpenGL ES las dimensiones de las texturas (alto y ancho) deben ser siempre potencia de dos (32, 64, 128, 256, 512…) Debemos tenerlo en cuenta a la hora de crear los recursos gráficos de nuestro juego. El primer elemento que generamos son los botones que necesitaremos en las diferentes pantallas. La figura 19 muestra todos los botones del juego. Tendrá un tamaño total de 196x128 píxeles. Sólo tenemos una flecha, porque luego podremos girar las imágenes. Figura 19. Botones del juego, cada uno tiene un tamaño 64x64 píxeles. Generamos ahora los elementos del menú principal, el logo, los botones del menú y el Fondo, en realidad éste lo usaremos también en la pantalla de puntuaciones, en la de juego y para generar las pantallas de ayuda y de juego. Figura 20. Logo del juego. 300x80 píxeles. El fondo tendrá un tamaño de 480x320 píxeles (consiste en un degradado de color). Nuestros botones de menú miden 128x196 píxeles (figura 21) y el logo 300x80 píxeles (figura 20). Aunque en principio no encajan en potencias de dos, más tarde veremos como los tratamos. 40 Figura 21. Botones del menú principal, 128x64 píxeles cada uno. A continuación generamos las pantallas de ayuda, en vez de recurrir a los elementos, generamos las imágenes directamente a pantalla completa en la resolución establecida, para reducir el código responsable del dibujo. Figura 22. Pantalla de Ayuda (Ayuda1.png) 512x512 píxeles. Para la pantalla de puntuaciones máximas reutilizamos el fondo y la imagen del botón del menú principal que dice Ranking. Las puntuaciones las dibujaremos con un texto que veremos más adelante. Figura 23. Imagen de juego. Es el fondo+tablero superpuesto (Juego.png). 512x512. 41 En la pantalla de juego dibujamos un tablero sobre el fondo que ya teníamos y lo guardaremos en el archivo juego.png (figura 23). Además esta pantalla incorpora más elementos de la interfaz de usuario, la etiqueta ¿Preparado?, las entradas del menú pausa Continuar y Salir, la etiqueta Fin de Juego, y las lupas. Además generaremos los textos de Nivel, Puntuación, Restantes y Tiempo que veremos más adelante. Figura 24. Etiquetas ¿Preparado? Fin de Juego y botones del menú pausa. Finalmente generamos los botones lupa de 64x64 píxeles y los bloques de colores con un tamaño de 32x32 píxeles. Y la flecha de intercambio de bloques. Figura 25. Botones Lupa 64x64píxeles cada uno, Bloques 32x32 píxeles cada uno y Flecha 64x32 píxeles. Para crear estos recursos gráficos se ha utilizado el programa de edición Gimp [5] que tiene licencia de distribución gratuita. 4.7.2.1 Trabajar con Texto Empleando Fuentes Bitmap. Para dibujar texto en pantalla emplearemos una fuente bitmap. Una fuente bitmap contiene unas imágenes que corresponden a un rango de caracteres ASCII. A cada carácter se le conoce como glifo. Todos tienen un tamaño fijo de 16x20 píxeles. Nosotros sólo usaremos los caracteres ASCII que se pueden imprimir. Los podemos ver en la figura 26. 42 Figura 26. Imagen Fuente bitmap. La primera fila contiene los caracteres comprendidos entre 32 y 47, la segunda del 48 al 63, etcétera. hasta el 127. Usaremos Bitmap Font Generator (de Codehead) [5] para generar nuestra textura de fuentes bitmap. Es una aplicación gratuita. Figura 27. Tabla con los caracteres ASCII y sus valores decimales. Crearemos 96 regiones de la textura y cada una contendrá un glifo. En java los caracteres se codifican usando Unicode, que tienen los mismos valores que ASCII. 43 Para coger la región que nos interesa en nuestro mapa de caracteres, primero obtenemos el índice en el array dónde guardamos las regiones, restando el carácter espacio (32) al carácter que queremos. Si el índice es menor de 32 o mayor de 95, tendremos un carácter que no está incluido en nuestra fuente bitmap, por tanto lo ignoraremos. 4.7.2.2 Mapa de Texturas. Ya hemos creado todos los recursos gráficos de nuestro juego. Podríamos tener muchas texturas, cada una conteniendo la imagen de un tipo de objeto, bloques, botones, etc. Pero OpenGL ES debe cambiar la textura cada vez que cambie de objeto. Para mejorar el sistema de trabajo colocaremos todas las imágenes en una, esta será el mapa de texturas. En la figura 28 vemos el mapa de texturas de nuestro juego. • Figura 28. El mapa de texturas (Items.png) 512x512 píxeles. 44 Así sólo tendremos que asociarla una vez y podremos dibujar cualquier tipo de objeto que tenga su imagen correspondiente en este mapa. Nos ahorraremos cambios de estado en OpenGL ES mejorando así el rendimiento de nuestro programa. La llamaremos Items.png y tendrá un tamaño de 512x512 píxeles, que además sí es potencia de dos. La cuadrícula no forma parte de la imagen, y los píxeles de fondo son transparentes. El tamaño de las celdas de la cuadrícula es de 32x32 píxeles. Se han colocado las imágenes en las esquinas del mapa casi todas las coordenadas son múltiplos de 32, para que sea más fácil crear regiones en la textura. 4.8 Gestor de Ventanas El sistema operativo Android emplea ventanas, la ventana en nuestro juego hará de contenedor. Pensemos en ella como un tapiz dónde dibujaremos el contenido de nuestra aplicación. Nuestro videojuego trabajará sólo con una única ventana. Esto es así para tener control absoluto sobre el aspecto de nuestro juego y poder centrarnos en la programación del juego y no en la interfaz de usuario de Android (cada vez que se cambia la orientación del dispositivo se destruye y se crea una nueva superficie de dibujo). Esto significa que en esta ventana iremos presentando las distintas pantallas de nuestro juego. Podríamos afirmar que el gestor de ventanas es el módulo principal, ya que en él enlazaremos el resto de módulos y los intercomunicaremos para que funcionen como se espera. Las tareas del módulo son: • La ventana trabaja con una interfaz de usuario (IU), para permitir a éste que interactúe con la pantalla. • Llevar a cabo la configuración de la ventana (tamaño, posición y evitará que se bloquee) y asegurarse que sólo usa un único componente IU para rellenarla. • El gestor de ventanas es el enlace con los otros módulos del juego. Debe garantizar el acceso a éstos para poder cargar recursos, hacerse cargo de las entradas del usuario, reproducir sonidos, dibujar los elementos, etcétera. • Se encarga de que la pantalla se actualice y dibuje tantas veces como sea posible. 45 • Llevar un registro de estado de la ventana e informar a la pantalla de estos eventos (si el juego está detenido o reanudado). • Lo utilizaremos también para ver el rendimiento de nuestro juego (medir los Frames por segundo). 4.9 Framework En conjunto todos estos módulos definen el framework o marco de nuestro juego. Estos son re-aprovechables para posibles trabajos futuros, pues hemos visto que están separados mediante el patrón MVC. Ahora que ya hemos examinado que tareas lleva a cabo cada módulo vamos a comprender como funciona todo esto en pseudocódigo ignorando algunos elementos que ahora no nos interesan. crearVentanaYComponenteIU (); Input entrada = new Entrada (); Graficos graficos = new Graficos (); Audio audio = new Audio (); Pantalla pantallaActual = new MenuPrincipal (); Mientras ( !usuarioNoSaleJuego () ){ pantallaActual.actualizaEstado (entrada); pantallaActual.dibuja (graficos, audio); } borrarRecursos (); 4.10 Lógica del Juego Aunque no lo consideremos un módulo, veamos aquí la lógica del juego. Establece el comportamiento del juego y se encarga de su correcto funcionamiento. Almacenaremos un tablero con los bloques, el tiempo, el nivel en que nos encontramos, los puntos y los bloques restantes a eliminar para avanzar de nivel. Además la lógica de nuestro juego deberá llevar a cabo las siguientes tareas: • Necesitaremos generar un tablero aleatoriamente. • Saber si existen combinaciones de bloques disponibles. • Intercambiar bloques • Comprobar si hay combinación, eliminando los correspondientes bloques. 46 • Aplicar la gravedad a los bloques superiores, una vez eliminados los bloques combinados y rellenar los huecos del tablero. • Actualizar el estado en que se encuentre el jugador, tiempo, puntuación… 47 48 5 Desarrollo. Una vez definidas las especificaciones y el diseño de nuestro proyecto, nos queda el paso más importante, implementarlo. Aquí surge la primera dificultad al no contar con experiencia previa programando con Android, ni OpenGL. Dado que para entender correctamente el trabajo realizado se necesitan unos conocimientos básicos, empezaremos este capítulo introduciendo aquellos conceptos indispensables para entender correctamente el desarrollo llevado a cabo. Posteriormente detallaremos el desarrollo del proyecto. Veremos cómo hemos estructurado nuestro proyecto y conoceremos en detalle el funcionamiento de los distintos módulos descritos, con sus clases y su implementación. 5.1 Conociendo Android y OpenGL. En este apartado describiremos la instalación de los recursos utilizados, la arquitectura del Sistema Operativo Android, cómo se construye una aplicación Android y la estructura de un proyecto. Luego veremos qué es y cómo trabaja OpenGL ES 1.0. 5.1.1 Preparando las Herramientas. Recordemos que el sistema operativo Android se basa en el lenguaje de programación Java. Por tanto, necesitamos el intérprete de java (JDK). [7] También instalamos Eclipse que será nuestro entorno de trabajo, desde él compilaremos y ejecutaremos el código de manera cómoda. [8] Para programar aplicaciones para el SO Android necesitamos además el kit de desarrollo Android SDK, disponible en la página web de Android en la sección de descargas. [9] Una vez instalado el SDK, veremos las carpetas de la figura 29. En SDK Manager se mostraran todos los paquetes a instalar. Aparecen todos los SDK (Kit de desarrollo de software) hasta la fecha. Descargamos los SDK necesarios y también 49 descargamos los Google USB divers, en el apartado Extras, necesario para conectar dispositivos móviles y probar la aplicación en éstos. Figura 29. Carpeta instalación herramientas SDK Android. Con AVD Manager podremos crear máquinas virtuales que nos permitirán ver la aplicación en funcionamiento. Una vez instaladas las herramientas necesarias vamos a configurar el plugin ADT Android Developer Tools, que nos permite una completa integración entre las dos herramientas. Se instala directamente desde Eclipse, añadiendo una ruta en la pestaña Help - Install New Software. [10] 5.1.2 Arquitectura del Sistema Operativo Android. La arquitectura interna de la plataforma Android, está básicamente formada por una serie de componentes. Cada uno de ellos se basa en los elementos de la capa inmediatamente inferior. La figura 30 ofrece un esquema de los principales componentes de Android. Kernel de Linux: Comenzando por la capa de abstracción más cercana al hardware se encuentra el núcleo, o kernel, de Linux. Esta parte proporciona unos servicios muy importantes relativos a la seguridad, la memoria, los procesos, la red y los modelos de los drivers (controladores de dispositivos). Runtime de Android: se encuentra encima del kernel y es el responsable de reproducir y ejecutar las aplicaciones Android. Cada una de ellas ejecuta su propio proceso a través de la máquina virtual Dalvkit. Dalvkit integra también un recolector de basuras (GC), con el que habrá que tratar más adelante. Librerías: Android incluye en su base de datos un set de librerías C/C++ , que son expuestas a todos los desarrolladores a través del framework de las aplicaciones Android Se encargan de las tareas de cálculo pesadas, como dibujo de gráficos, reproducción de audio, acceso a base de datos. 50 Figura 30. Arquitectura Android Framework de aplicaciones: asocia las librerías del sistema y el tiempo de ejecución, originando la parte del usuario de Android. También gestiona las aplicaciones y proporciona un marco de trabajo para éstas. Los desarrolladores tienen acceso total al código fuente usado en las aplicaciones base. Esto ha sido diseñado de esta forma, para que no se generen cientos de componentes de aplicaciones distintas, que respondan a la misma acción, dando la posibilidad de que los programas sean modificados o reemplazados por cualquier usuario sin tener que empezar a programar sus aplicaciones desde el principio. Aplicaciones: Todas las aplicaciones de la plataforma Android, han sido creadas con el framework. Entre ellas encontramos un cliente de email, calendario, programa de SMS, mapas, navegador, contactos, y algunos otros servicios mínimos. Todas ellas escritas en el lenguaje de programación Java. 5.1.3 Arquitectura Aplicación Debido a la filosofía utilizada por Android los elementos de una aplicación pueden ser utilizados por otras, si éstas lo permiten. Para conseguir esto, el sistema debe ser capaz de 51 ejecutar un proceso de una aplicación cuando una de sus partes lo necesite. Es por ello que al contrario que la mayoría de las aplicaciones en otros sistemas, las aplicaciones en Android no tienen un solo punto de entrada para todo, es decir no hay por ejemplo una función “main()“. En vez de ello los componentes esenciales de la aplicación pueden ser iniciados cuando se necesiten. Los bloques que pueden constituir una aplicación son los siguientes: - Activity: Representa una interfaz gráfica para una acción que el usuario puede realizar. Actúa como lo que comúnmente se conoce como “formulario”. En una Activity se colocan los elementos de la interfaz gráfica. - Services: Son lo que comúnmente se conocen como procesos. Invisibles para el usuario, carecen de interfaz gráfica. Por ejemplo para reproducir música de fondo. - Intents: Es un mecanismo para comunicar a las distintas aplicaciones y Activities. Un Intent es un mensaje de llamada asíncrono. Android está desarrollado sobre la base de reutilizar código y aplicaciones existentes, es por eso que esta característica es tan importante. - Broadcast Recivers: Se utilizan para que una aplicación responda a un evento específico del sistema. Al igual que los servicios no poseen interfaz gráfica. Por ejemplo se puede utilizar un Broadcast Reciver en un programa para que cuando el teléfono se esté quedando sin batería se muestre un mensaje advirtiendo al usuario sobre su utilización. - Content Providers: Es el mecanismo encargado de administrar la información que se pretende que perdure. Se puede utilizar para compartir información entre aplicaciones. 5.1.3.1 La pila de Actividades Como hemos explicado una actividad o activity, es el componente principal de una aplicación. Cada activity, implementada como una clase subclase de Activity, representa una pantalla de la aplicación con interfaz de usuario. Una aplicación con varias actividades tiene una pila de actividades. Cada vez que iniciemos una nueva, se colocará en dicha pila. Cuando pasamos de una a otra, se añaden en una pila de actividades (si no la eliminamos) y en caso de que se destruya la activity actual (por ejemplo si se aprieta la tecla de 52 retroceso), se realiza un pop (retira la última actividad) de la pila y se vuelve a la anterior. Podemos ver un ejemplo en la figura 31. Figura 31. Ejemplo de cómo funciona la pila de Actividades en Android. Para pasar de una activity a otra, se utiliza la clase Intent, en la que indicamos la activity origen y la activity destino. También podemos utilizar el apartado Extras del Intent para pasar información entre activities. Todas las actividades de la aplicación, tanto las que se encuentran en la pila en modo pausado como la que está activa, comparten la misma máquina virtual y la misma porción de memoria. Nuestro juego sólo tendrá una actividad, para controlar la visualización de éste, sin embargo debemos entender cómo trabaja la pila de Actividades antes de empezar a programar para Android. 5.1.3.2 Ciclo de Vida de una Aplicación Android Como se ha explicado anteriormente una aplicación no solo tiene un punto de entrada para todos los acontecimientos, por ello no se puede declarar el ciclo de vida de una aplicación en general, sino que hay que tener en cuenta el comportamiento de cada uno de sus componentes: activities, services y broadcast recivers. Una actividad puede encontrarse en tres estados diferente: 53 • Activa: cuando está en primer plano de la pantalla. Esta actividad tiene el foco e interactúa con el usuario. • Pausada: ha perdido el foco pero es aún visible para el usuario, es decir, existe una actividad por encima de ella de modo transparente o no, ocupando la pantalla entera, también cuando se bloquea la pantalla del teléfono. La actividad pausada mantiene la información pero debemos tener cuidado, pues el sistema puede decidir cerrarla sin previo aviso en caso de falta de memoria. • Parada: la actividad ha sido cubierta completamente. Mantiene su estado y la información pero el sistema puede eliminarla sin avisar cuando necesite más memoria. En la figura 32 vemos los estados de una Actividad, éstos deberán sobrescribirse para controlar la aplicación. Figura 32. Ciclo de vida de una Actividad. 54 El ciclo de vida de la Actividad empieza en onCreate, cuando se crea la activity por primera vez y finaliza en onDestroy dónde la actividad se destruida irreversiblemente. Durante este tiempo puede tener o no el foco, pasando por diferentes estados. El método onResume es llamado siempre, antes de que la activity entre en estado running. La activity puede ser destruida silenciosamente tras onPause. Aquí tendremos que guardar los datos persistentes, podemos llamar al método isFinishing para comprobar si nuestra activity va a ser eliminada. Para nuestro juego: En onCreate(): crearemos ventana, definiremos la interfaz de usuario (UI) desde donde recibiremos las entradas de éste. En onResume(): iniciamos o reiniciamos el hilo principal. En onPause(): Pausaremos el hilo principal y si el método isFinising() nos devuelve true, guardaremos los estados en el disco para no perder la información. 5.1.4 Estructura de una Aplicación Android Al crear un nuevo proyecto Android en Eclipse se genera automáticamente la estructura de carpetas necesaria para poder generar posteriormente la aplicación, vamos a echarle un vistazo para entender cómo se construye una aplicación Android. En la figura 33 vemos los elementos creados inicialmente para un nuevo proyecto Android. Figura 33. Estructura de un proyecto Android. Carpeta / src / Contiene el código fuente de la aplicación, aquí se encuentran los distintos paquetes que contienen las clases Java que forman la aplicación. 55 Carpeta / res / Contiene todos los archivos de recursos necesarios para el proyecto: imágenes, vídeos, cadenas de texto, etc. Los diferentes tipos de recursos se deberán distribuir entre las siguientes carpetas: • / Es / drawable /. Contienen las imágenes que serán dibujadas para la interfaz de usuario. Se puede dividir en / drawable-ldpi, / drawable-MDPI y / drawable-hdpi para utilizar diferentes recursos dependiendo de la resolución del dispositivo. • / Es / layout /. Contienen las plantillas de las distintas vistas en formato XML. • / Es / values /. Contiene otros recursos de la aplicación como cadenas de texto (strings.xml), estilos (styles.xml), colores (colors.xml), etc. Carpeta / gen / Esta carpeta generada automáticamente contiene las referencias de los distintos elementos creados. Se actualizan automáticamente y no deben ser modificados de forma manual. Ver la Figura34. Figura 34. Estructura carpeta gen. La clase R.java se trata de un archivo fuente para el manejo de recursos que no debemos editar. La clase R contendrá en todo momento una serie de constantes con los ID de todos los recursos de la aplicación incluidos en la carpeta / res /, de manera que podamos acceder fácilmente a estos recursos desde nuestro código a través de este dato. Así, por ejemplo, la 56 constante R.drawable.icon contendrá la identificación de la imagen “icon.png” contenida en la carpeta /nada / drawable /. Carpeta / assets / Contiene todos los archivos de recursos necesarios para la aplicación (no serán compilados), como archivos de configuración, de datos, audio, imágenes etc. Para nuestro juego crearemos tres subcarpetas fx, imágenes y music, dónde almacenaremos nuestros recursos. Archivo AndroidManifest.xml Es el archivo de configuración principal de la aplicación, contiene la definición en XML de los aspectos de la aplicación, Las principales funciones del Android Manifest son: - Describe los paquetes Java para la aplicación. Cada paquete tiene un identificador único para la aplicación. - Detalla los componentes de la aplicación: actividades, servicios, receptores de difusión y proveedores de contenido. Relaciona cada una de las clases correspondientes a cada componente. Mediante esta declaración el sistema sabe qué componentes pueden ser lanzados bajo qué condiciones. - Especifica que procesos mantendrá los componentes de la aplicación. - Declara los permisos que necesita la aplicación para acceder a partes protegidas de las API. - Se especifican también los permisos que se deben tener para interactuar con los componentes de esta aplicación. - Especifica el “level” mínimo que debe tener la API de Android para hacer funcionar la aplicación. - Describe las bibliotecas que la aplicación debe enlazar 5.1.5 Rendimiento, el Recolector de Basura y Jit. Java nos impide reservar o liberar memoria, para ello utiliza el Garbage Collector (GC). Cuando llega a una cantidad de memoria reservada decide liberarla si es posible. Esto presenta algunos problemas de rendimiento ya que a cada iteración en que se ejecute el GC detiene todo de 100 a 300 milisegundos. Esto significa que durante casi medio segundo en 57 nuestro juego no podremos dibujar en pantalla ni actualizar. Para evitarlo diseñaremos la aplicación evitando la generación de objetos y destrucción de éstos. Android 2.2 incluye JIT (Just In Time Compilation) que incrementa el rendimiento de las aplicaciones. Dalvik se comporta como un intérprete, es decir, a medida que va leyendo el código lo va ejecutando, con JIT Compilation, se convierten secciones completas de código en código ejecutable por el procesador real, que se ejecutará sin más interrupciones, acelerando así la ejecución de dicho código. [11] Figura 35. Icono JIT. En nuestro juego prima el rendimiento sobre el uso de la memoria, teniendo en cuenta esto recurriremos a métodos que en programación no están muy bien vistos. (Métodos statics, variables statics…, no llamaremos excesivamente a funciones (getter’s y setter’s) etc.) que nos permitirán ahorrar algunos milisegundos. Sobre el rendimiento de los videojuegos podemos encontrar mucha más información en las transparencias de la conferencia de Google IO 2009 de Chris Pruett de “Writing Real Time Games Android”. [12] 5.1.6 El problema de la Fragmentación Uno de los problemas de Android es la fragmentación, es decir ofrece diferentes dispositivos con distintos tamaños y densidades de pantalla. Entendemos por densidad como la cantidad de pixeles en un área, es decir una imagen de 100×100 pixeles se verá más pequeña en una pantalla con densidad mayor. Lo podemos apreciar en la figura 36. Ya hemos diseñado los recursos para una la resolución de 480x320 píxeles, nos encargaremos de que OpenGL ES modifique la escala de la salida en pantalla para ajustarla automáticamente a la ventana de visualización según el tamaño y densidad de ésta, pero 58 deberemos transformar las coordenadas del punto de contacto del dedo con la pantalla para convertirlas a nuestra resolución fija. Figura 36. Los mismos elementos con tamaño fijo en pantallas de distintas densidades 5.1.7 OpenGL ES 1.0 OpenGL ES (Open Graphics Library for Embedded Systems) es una variante simplificada de la API gráfica OpenGL diseñada para dispositivos integrados tales como teléfonos móviles, PDAs y consolas de videojuegos. Fue creada por el Grupo Khronos, un consorcio de empresas dedicadas al hardware y software gráfico. La versión 1.0 está basada en OpenGL 1.3 [13]. Todos los dispositivos Android son compatibles con OpenGL ES 1.0. OpenGL es una librería de funciones que pueden usarse para dibujar escenas 2D o 3D a partir de primitivas geométricas simples, tales como puntos, líneas y triángulos. Figura 37. Logotipo OpenGL ES. Vamos a ver cómo funciona esta librería de renderizado, y conocer la estructura básica de un motor gráfico 2D. A diferencia de usar un motor gráfico ya creado, esta opción nos permite controlar exactamente lo que necesitamos en nuestro juego, de tal manera que podríamos adaptarlo más adelante según nuestras necesidades. Antes de comenzar a ver cómo funciona OpenGL es conveniente asegurar algunos conceptos básicos acerca de los gráficos en 3D, ya que OpenGL trabaja con éstos. 59 5.1.7.1 Como se Representa una Escena Los objetos reales poseen tres dimensiones: altura, anchura y profundidad. Para que estos objetos puedan representarse en una pantalla que tiene dos dimensiones, tenemos que transformar la información visual del objeto original para producir la ilusión de ver un objeto tridimensional en un sistema bidimensional. Para posicionar los objetos OpenGL trabaja con un sistema de coordenadas cartesianas, los ejes: X, Y, Z. De tal forma que dos a dos son perpendiculares. Los ejes X e Y representan la posición horizontal y vertical respectivamente, y el eje Z representa la profundidad. Trasladándolo al monitor de un ordenador, el eje X se extiende a lo ancho, el Y a lo alto, y el Z se dirige hacia el observador desde el centro de la pantalla. Figura 38. Figura 38. Sistema de coordenadas OpenGL Pensemos en una habitación vacía con una mesa en el centro, y una pelota que va rebotando por la habitación. Nosotros sacamos fotografías con una cámara (en 2D). Aunque los objetos de la escena se estén moviendo, al presionar el botón de disparo la cámara captura una imagen estática y según en qué posición estemos (nosotros y los objetos) la fotografía tendrá una composición u otra. Ver Figura39. Figura 39. Fotogramas de una pelota rebotando. Como tenemos un espacio definido gracias al sistema de coordenadas podemos posicionar los objetos (modelos) y la cámara dentro de él, simplemente con especificar el punto en el donde lo deseamos colocar. 60 Resumiendo OpenGL traslada las coordenadas tridimensionales (de los objetos) a coordenadas bidimensionales a través de la proyección de los objetos (sus puntos) en la ventana de “trabajo” (fotografía). 5.1.7.2 Proyecciones Para nuestro juego usaremos la proyección paralela, que es útil para juegos 2D. En este tipo de proyección no se tiene en cuenta la distancia entre el objeto y la cámara, es decir, dos objetos con el mismo tamaño se ven igual en la pantalla, independientemente que de uno esté más alejado del punto de vista del observador. Por otra parte si un objeto no entra en el espacio de trabajo no se dibuja. En la Figura 40 podemos ver los planos de proyección. Figura 40. Tipos de proyecciones. Trabajaremos con un plano de proyección, podemos verlo en la figura 41. Todos los objetos que se encuentren dentro de la caja serán visibles, todos los que estén fuera, no lo serán. Es decir se lanza la proyección de los puntos (de los objetos), hacia el plano de la cámara y si están fuera de esta caja (planos de corte) no aparecen en la fotografía. El plano donde se proyectan se conoce como plano de recorte cercano o plano de proyección y tiene su propio sistema de coordenadas 2D. En realidad nuestra vista de proyección será plana, es decir no tendrá eje z, porque trabajaremos en 2D. 61 y z x Figura 41. Plano de proyección paralela. Volumen de vista y planos de corte. 5.1.7.3 Matrices OpenGL expresa las proyecciones a través de matrices, que son las que se encargan de generar los puntos que definimos en nuestra escena. Una matriz codifica las transformaciones que se deben aplicar a un punto. Puede ser una proyección, una traslación (desplazamiento), una rotación, modificación de escala o cualquier otra cosa. Al multiplicar una matriz por un punto, aplicaremos la transformación a dicho punto. • Matriz Modelo-Vista: la usaremos para mover los bloques. En vez de mover manualmente las posiciones de los vértices, empleamos esta matriz para dibujar nuestro modelo, moviendo su origen de coordenadas hasta la ubicación que especificamos. • Matriz proyección: la que actúa como cámara. 5.1.7.4 Sobre Vértices y Triángulos Como ya hemos visto OpenGL trabaja con puntos para definir los modelos (objetos). Los modelos se crean a partir de triángulos, que a su vez están definidos por 3 puntos, se les llama Vértices. A partir de estos triángulos se generan las formas geométricas que queramos representar. A nosotros nos interesa el rectángulo, que será la unión de dos triángulos. Para ello usaremos dos triángulos que tengan 2 vértices en la misma posición. Pero en vez de duplicar los vértices usaremos sólo uno de los que comparta coordenadas y a la hora de 62 dibujar indicaremos que vértices debe usar para formar cada triángulo. Esto se llama indexar vértices. Podemos verlo en la figura 42. Figura 42. Dibujar un rectángulo con seis vértices (izquierda) o con cuatro (derecha). El triángulo superior estará formado por los vértices v1, v3 y v4, y el inferior por v1, v2, v3. Indexar vértices nos permite evitar la duplicación de código y mejorar el rendimiento porque OpenGL ES no tendrá que trabajar con más vértices de los estrictamente necesarios. 5.1.7.5 Enviar Vértices a OpenGL ES OpenGL Es espera que le enviemos los vértices como un array, pero como OpenGL es una API de C no podemos recurrir a los array estándar de Java, usaremos los búfer NIO [14] de Java, que son bloques de memoria consecutivos que se asignan en la memoria del sistema, no en la máquina virtual. 5.1.7.6 Texturas en OpenGL Para convertir un mapa de bits en un rectángulo tendremos que añadir las coordenadas de la textura a cada vértice. ¿Qué son las coordenadas de las texturas? Las que especifican un punto dentro de la textura (mapa de bits) que deberá convertir en uno de los vértices del cuadrado. Las coordenadas de la textura están expresadas en dos dimensiones u / v. La coordenada u es equivalente a la x de un sistema de coordenadas convencional y la v es equivalente a la y. El eje u apunta hacia la derecha y el eje v apunta hacia abajo. El origen de coordenadas coincide con la esquina superior izquierda del mapa de bits. 63 Figura 43. Sistema de coordenadas de una textura, después de cargarla en OpenGL ES. Y el modelo (vértices) a que asociaremos cada coordenada. Así pues para convertir el mapa de bits en un rectángulo, tendremos que asociar cada coordenada de la textura a cada vértice del rectángulo. Ver figura 43. 5.1.7.7 Filtrar Texturas Cuando utilicemos las texturas en la pantalla puede suceder, que el objeto sea más grande que la textura, en este caso estaremos empleando más píxeles en la pantalla que los que hay en la textura, a estos píxeles se les llaman texels. En éste caso nos encontraremos con un efecto de ampliación. También puede que sea al contrario, que la textura sea mayor que el objeto, aquí se trata de una reducción. En ambos casos deberemos indicar a OpenGL ES cómo modificar la escala de la textura. 5.1.7.8 Espacio Mundo, Espacio Modelo El espacio mundo se refiere a la vista de nuestra proyección, como hemos visto nosotros usamos la caja de la proyección ortogonal, pero prescindiendo del eje z. Por lo tanto sólo tendremos los ejes x / y, donde el origen de coordenadas de nuestro espacio mundo, se encuentra en la esquina inferior izquierda. Los valores positivos del eje x apuntan hacia la derecha y los del eje y hacia arriba. Figura 44. Figura 44. Sistema de coordenadas OpenGL ES para nuestro juego 2D. 64 Espacio-modelo es el sistema de coordenadas en el que definimos el objeto, sin tener en cuenta nada más. Es decir las posiciones de los vértices de nuestros modelos. Este lo utilizaremos para transformar el modelo más fácilmente (rotar, mover, etc.) sin tener que aplicar las transformaciones a todos sus vértices. En la figura 45, vemos un ejemplo del espacio modelo de nuestro cuadrado. Con su sistema de coordenadas. El origen se encuentra en el centro, el eje de las x aumenta a la derecha y el de las y hacia arriba. Figura 45. Nuestro modelo con su espacio. 5.1.7.9 Como Trabaja OpenGL ES 1.0 Imaginemos un folio en blanco y un pintor. Bien pues OpenGL utiliza la clase GLSurfaceView como el folio dónde pintar, y la clase GLSurfaceView.Renderer como pintor. Utilizaremos este Renderer (renderizador) para especificar lo que queremos que aparezca en pantalla. OpenGL dibuja en un hilo independiente mediante la clase GLSurfaceView, para no sobrecargar el hilo de IU. La clase GLSurfaceView se encarga de iniciar el hilo en onResume() y lo elimina en onPause(). Aquí tenemos un problema, ya que al retomarlo crea una nueva superficie perdiendo todos los estados configurados (texturas, etc). Se conoce como pérdida de contexto, y nos obligará a volver a cargar las texturas al volver a retomar la aplicación. Los métodos que deberemos implementar de Renderer son: • onSurfaceCreated(), este método es llamado cuando la superficie es creada o es recreada. • onSurfaceChanged(), se llama a este método cuando la superficie cambia de alguna manera, por ejemplo al girar el móvil y colocarlo en posición de paisaje. En realidad no sobrescribiremos este método porque bloquearemos la orientación de la pantalla. 65 • onDrawFrame(), este método es el que utilizaremos para dibujar directamente sobre la pantalla (en realidad sobre la superficie, el “folio”). La aplicación no se ocupa de llamar a éste método, de ello se encarga automáticamente el hilo tantas veces como le es posible. Una vez pintado es GLSufaceView quien se encarga de entregar “la fotografía” a la GPU en forma de píxeles. 5.2 Desarrollo del Juego En esta sección y después de haber adquirido los conocimientos suficientes, sobre Android y OpenGL vamos a explicar la implementación del juego. Veremos la estructura de nuestro proyecto, el framework con los diferentes módulos y las clases que los forman, que tareas llevan a cabo y el modo de implementarlas. Finalmente veremos las clases que forman nuestro juego. Como complemento a la lectura de esta sección del capítulo, se recomienda tener una copia local del código del juego. 5.2.1 Estructura de Blocks La estructura de nuestro proyecto Blocks tiene la misma estructura que cualquier proyecto para Android, así que vamos a centrarnos en ver cómo hemos organizado la parte del código, carpeta /src/. En la figura 46. podemos observar la estructura de nuestro proyecto Blocks. Hemos dividido el código de nuestro videojuego en cuatro paquetes. Todos empiezan por el nombre com.games.edo. • En framework, incluimos las interfaces que utilizamos para la generación del framework. • En framework.andoidimplementado, encontramos las clases que implementan el framework para Android, al paquete lo llamamos androidimplementado para diferenciarlo de las interfaces. Aquí están las implementaciones de los módulos: Entrada, Audio, Gráficos, Archivos lectura/escritura y el Gestor de ventanas. • En framework.GL, hemos colocado las clases que necesitamos para trabajar con OpenGL. 66 • En juegoBlocks guardamos las clases con la implementación del juego: Pantallas, configuración, tablero, especificación de recursos y la clase principal JuegoBlocks.java punto de entrada de nuestro videojuego. Figura 46. Estructura del proyecto del juego Blocks En Android Manifest definimos los permisos para nuestra aplicación. Wake_Lock para impedir que la pantalla entre en modo ahorro de energía, y Write_External_Storage, para acceder a la tarjeta de memoria. Además definimos la Actividad JuegoBlocks.java como principal (punto de entrada) y la orientación como Landscape para que se vea en horizontal. Cambiamos las configuraciones para ocultar el teclado y no permitir el cambio de orientación al girar el móvil. 5.2.2 Framework Vamos a empezar viendo los elementos que componen el framework del juego, sus módulos con sus métodos. 5.2.2.1 Módulo Archivos Input/Output Recordemos que este módulo nos sirve para guardar y recuperar los datos de nuestro juego. Como vamos a usar la tarjeta de memoria, nos aseguraremos de haber pedido los permisos de acceso en Android Manifest. 67 El módulo está formado por la interfaz FileIO, y la clase AndroidFileIO que la implementa. En ella almacenamos un AssetManager y la ruta del almacenamiento externo. Vamos a ver sus métodos: • public InputStream leerAsset(String nombreArchivo) throws IOException • public InputStream leerArchivo(String nombreArchivo) throws IOException • public OutputStream escribirArchivo (String nombreArchivo) throws IOException Para leer nuestros recursos utilizamos la función leerAsset, que se vale de la clase AssetManager para abrir el archivo del cual le pasamos el nombre y nos devuelve un objeto de la clase InputStream de Java que se puede usar para leer cualquier archivo. Utilizaremos los métodos leerArchivo, escribirArchivo para leer o escribir respectivamente nuestro archivo de configuración (puntuaciones y si el sonido está activado), que devuelven objetos FileInputStream y FileOutputStream. Estos métodos se valen de la ruta de la unidad externa de almacenamiento que obtendremos con la función: getExternalStorageDirectory().getAbsolutePath() + File.separator; Figura 47. Tarjeta de memoria externa. Las excepciones IOException nos permiten identificar los errores que se puedan producir al cargar los recursos (Si no hay tarjeta SD, si el nombre del archivo es diferente, etc.). 5.2.2.2 Módulo Entrada Con el módulo de entrada accederemos a los eventos relacionados con el teclado y con la pantalla táctil. Separamos el código por labores implementando las clases que desarrollan su trabajo, tendremos un controlador de teclado y uno de pantalla. 68 Cada vez que haya una entrada del usuario se producirá un evento que obtendremos con unos listeners que suscribiremos a la IU (en nuestro caso la View donde se encuentra el foco). Así que lo que haremos será dejar que entren los eventos y registrarlos, para más tarde procesarlos. 5.2.2.2.1 La clase Pool En nuestro controlador de eventos de entrada se estarán creando instancias de TouchEvents y de KeyEvents (en el caso que el dispositivo tenga teclas) constantemente. Cada vez que presione una tecla o se toque la pantalla, el sistema de entrada de Android dispara varios de estos eventos, con lo que creamos instancias nuevas de forma continua. Éstas van a parar al poco tiempo al recolector de basura. Ya hemos visto que el Garbage Collector es un enemigo para el rendimiento de nuestro juego, así que con la clase Pool vamos a intentar evitarlo. Con Pool en vez de crear nuevas instancias constantemente y posteriormente destruirlas (con GC), nos limitaremos a guardarlas para después poder reutilizarlas. [15] Para ello implementamos una clase genérica, es decir que nos permite almacenar objetos de cualquier tipo. En ella guardaremos un ArrayList, donde guardaremos los objetos del pool, PoolObjectFactory que lo utilizaremos para generar nuevas instancias del tipo de la clase que contenga, y un miembro que almacena el número máximo de objetos que podemos incluir. En el constructor simplemente recibimos un objeto PoolObjectFactory y el número de objetos que guardaremos y los almacenamos. Sus métodos de la clase son: • public T newObject() Crea un nuevo objeto en el ArrayList en el caso que no haya ninguno, o nos devuelve la instancia de un objeto ya reciclado. • public void free(T object) Nos permite guardar los objetos que no vayamos a utilizar más, para reutilizarlos más tarde, siempre y cuando quede espacio libre. 69 5.2.2.2.2 El Controlador de Teclado La clase KeyboardHandler implementa el controlador de teclado, debemos implementarlo porque hay dispositivos con teclado que pueden usar la aplicación y generar muchos de estos eventos disparando al GC. La intención al principio del proyecto también era manejar la tecla Back del dispositivo. Se encarga de la vista a través de la cual se reciben los eventos del teclado, toma nota del estado de cada tecla guardándola en un pool, lleva un registro de instancias TouchEvent y lo sincroniza todo porque recibe los eventos desde la interfaz de usuario mientras los guardamos en el pool del juego, que se estará ejecutando desde otro hilo diferente. KeyboardHandler implementa la interfaz onKeyListener para recibir los eventos de la View. Guardaremos un array pressedKeys con el estado de cada tecla (pulsada o no), el Pool keyEventPool para reciclar las instancias de nuestra clase keyEvent. En KeysEventsBuffer guardaremos los eventos que se generen y aún no vayamos tratando. Y como doble buffer usaremos también keyEvents. En el constructor recibe como parámetro la vista dónde se suceden los eventos de teclado. Creamos la instancia a Pool y configuramos el listener para la vista. Veamos ahora sus métodos: • public boolean onKey(View v, int keyCode, android.view.KeyEvent event) Es llamado cada vez que recibamos un evento de teclado desde el hilo de IU. En él cogemos una instancia del Pool, nueva o reciclada, miramos la tecla el tipo de evento y lo añadimos a nuestra lista KeysEventsBuffer. • public List<KeyEvent> getKeyEvents() Nos devuelve una lista con los eventos de teclado que se han ido almacenando. Para ello utilizaremos el doble buffer. Primero Guardamos en el pool los eventos de KeysEvents. Luego limpiaremos este buffer para más tarde volcar los eventos de KeysEventsBuffer. Terminamos limpiando KeysEventsBuffer, para que no se llene. • public boolean isKeyPressed(int keyCode) 70 Nos indica si la tecla que le pasamos se ha presionado o no, consultando su estado en el array de teclas pulsadas. 5.2.2.2.3 El Controlador Táctil Como sólo vamos a usar un dedo para jugar, haremos nuestro controlador para pantallas mono táctiles. Está compuesto por la interfaz TouchHandler y la implementación SingleTouchHandler que a su vez implementa a onTouchListener. Guardaremos un booleano para saber si se ha tocado, dos enteros para la posición (x e y), la lista Pool para reciclar eventos y de nuevo usaremos un doble buffer, TouchEvents y TouchEventsBuffer. Usaremos los valores scaleX, scaleY para tratar diferentes resoluciones de pantalla. En el constructor recibimos como parámetros la Vista y las escalas horizontal y vertical, igual que antes registramos el Pool, configuramos el listener para la vista y acabamos almacenando los valores de escala. Figura 48. Móvil Android con teclado y pantalla táctil. Los métodos de SingleTouchHandler son: • public boolean onTouch(View v, MotionEvent event) Cada vez que se produce un evento en la pantalla desde la IU se llama a este método, en él cogemos una instancia del Pool nueva o reciclada, miramos la tecla el tipo de evento, lo añadimos a nuestra lista touchEventsBuffer y multiplicamos las coordenadas x e y por el factor de escala. • public boolean isTouchDown(int pointer) • public int getTouchX(int pointer) • public int getTouchY(int pointer) 71 Estos métodos permiten conocer el estado de la pantalla. Sólo usaremos un dedo, el identificador del primer dedo que toca la pantalla siempre es 0, así que sólo manejaremos los eventos que tengan este identificador (puntero). El primero indica si el dedo ha tocado la pantalla y los dos siguientes nos dan la coordenada del eje en que se tocó. • public List<TouchEvent> getTouchEvents() Nos servirá para obtener los eventos TouchEvent y así gestionar estas entradas. Deberemos llamarlo frecuentemente para evitar que la lista se llene. 5.2.2.2.4 Juntando las Entradas La interfaz input que está implementada por la clase AndroidInput, que es el que hace de controlador de entradas. En la interfaz input definimos nuestros tipos de eventos, touchEvent y keyEvent con sus atributos. De TouchEvent guardaremos el tipo (toca, suelta, arrastra), la posición y el identificador puntero que se le da mientras esté tocando. Para keyEvent similar, el tipo (pulsada, soltada), código y carácter. La clase AndroidInput simplemente configurará el controlador táctil y el controlador de teclado. No explicamos sus métodos porque simplemente llaman a los métodos del controlador apropiado que ya hemos visto: • public boolean isTouchDown(int pointer) • public int getTouchX(int pointer) • public int getTouchY(int pointer) • public boolean isKeyPressed(int keyCode) • public List<TouchEvent> getTouchEvents() • public List<KeyEvent> getKeyEvents() 5.2.2.3 Módulo Audio Este módulo está compuesto por las interfaces: Musica, Sonido y Audio y por las clases AndroidMusica, AndroidSonido y AndroidAudio. 72 Android tiene diferentes Streams de audio, según donde se encuentre al subir o bajar el volumen con los botones laterales veremos que hace una cosa u otra, en una llamada, en un juego, en el reproductor de audio… Primero debemos asegurarnos que controlamos el Stream de audio correcto. Utilizaremos el Stream de Android Música. 5.2.2.3.1 Música La clase AndroidMusica implementa onCompletionListener (la usamos para comprobar si se ha completado la reproducción del sonido) y nuestra interfaz Musica, con ella controlamos la reproducción del contenido del archivo de música. Lo que hace esta clase es enviar a la tarjeta de sonido del dispositivo el contenido del archivo de música almacenado en el disco. Para ello utilizamos una instancia MediaPlayer (la clase que nos reprodce el archivo de audio), junto con un booleano, para saber el estado de MediaPlayer. En el constructor creamos el MediaPlayer, lo iniciamos con el AssetDescriptor que recibimos por parámetro, e indicamos que está preparado, finalmente registramos como detector OnCompletionListener. Sus métodos son: • public void play() • public void stop() • public void pause() Sirven para controlar la reproducción de la música. En play() prepararemos Mediaplayer y lo iniciamos, en un bloque sincronizado debido a la interfaz OnCompletionListener podría hacer la verificación de si está preparado en un hilo independiente. • public void setLooping(boolean looping) Nos permite reproducir el sonido en un bucle, es decir cuando termina vuelve a empezar. • public void setVolume(float volume) • public boolean isPlaying() • public boolean isStopped() 73 Define el volumen. • public boolean isLooping() Sirven para consultar el estado de la reproducción. • public void dispose() Detiene la reproducción si está en marcha y libera el recurso. 5.2.2.3.2 Efectos de Sonido AndroidSonido implementa la interfaz Sonido, que nos permite reproducir efectos de sonido cargados en la memoria RAM. Aquí usaremos y clase SoundPool que nos facilita la reproducción de efectos sonoros. Manejaremos el archivo con el identificador que asigna al cargarlo. Los métodos son: • public void play(float volume) Para reproducir el sonido • public void dispose() Para liberar el recurso. 5.2.2.3.3 Juntando el Audio La interfaz Audio se utiliza para crear nuevas instancias tanto de música como de sonido desde los archivos de recursos. La clase AndroidAudio implementa esta interfaz. En el constructor le pasamos la actividad para definir el volumen (usando el Stream Music) y guardamos una instancia de AssetManager que nos proporcionará el directorio assets dónde guardamos nuestros ficheros y otra de SoundPool, configurándola para 20 efectos sonoros a la vez. Figura 49. Icono sonido en Android. 74 Sus métodos son: • public Musica nuevaMusica(String nombreArchivo) • public Sonido nuevoSonido(String nombreArchivo) El método nuevaMusica crea una instancia de AndroidMusica tomando el descriptor de AssetManager que nos proporcionará el directorio assets dónde guardamos nuestros ficheros. El método nuevoSonido lo usamos para cargar en la memoria un efecto sonoro que esté guardado en un archivo de audio, se vale del descriptor de AssetManager para obtener la ruta dónde está el fichero y nos devuelve el id del sonido cargado en memoria. 5.2.2.4 Módulo Gráficos Por la complejidad de este módulo, hemos agrupado todas las clases que tengan que ver con la parte gráfica en el paquete com.games.edo.framework.GL. 5.2.2.4.1 GLGraficos Usaremos esa clase para llevar un registro de GL10 (que nos permite enviar comandos a OpenGL ES) y de GLSurfaceView. Sus métodos son: • public GL10 getGL() • void setGL(GL10 gl) • public int getWidth() • public int getHeight() Que nos sirven para enviar y obtener los datos. 5.2.2.4.2 Vector Creamos la clase Vector2D que tratará con un vector en 2D. La podremos usar para representar, posición, velocidad, dirección y distancia (Figura 50). También nos servirá para rotar o modificar la escala, así como para ajustar la imagen automáticamente al volumen de la vista de la ventana. 75 Por definición un vector es: v = (x,y) (0) Figura 50. Usando un vector como posición, velocidad dirección y distancia. Guardaremos los miembros x e y, y definiremos unas constantes para poder pasar el ángulo de grados a radianes y viceversa, sólo deberemos multiplicar el ángulo por la constantes indicada. Los métodos de esta clase implementan un poco de álgebra y trigonometría: • public Vector2 cpy() Devuelve una copia el vector. • public Vector2 add(float x, float y), public Vector2 add(Vector2 vec) Estos métodos nos devuelve la suma de nuestro vector con otro, o con argumentos. Recordemos la fórmula (1) v + u = (v.x + u.x, v.y + u.y) • (1) public Vector2 sub(float x, float y), public Vector2 sub(Vector2 other) Estos métodos nos devuelve la resta de nuestro vector con otro, o con argumentos. Podemos ver la fórmula a continuación (2) v - u = (v.x - u.x, v.y + u.y) • public Vector2 mul(float scalar) Multiplica nuestro vector por un escalar. Ver fórmula (3) 76 (2) u*k = (x*k, y *k) • (3) public float len() Nos indica la longitud del vector. Fórmula (4) |u| = raíz( x^2 + y^2) • (4) public Vector2 nor() Normaliza un vector hasta su longitud unitaria. Primero calculamos su longitud y si no es cero, dividimos cada componente por ésta, obteniendo así uno de longitud unitaria. • public float angle() Nos devuelve el ángulo existente entre el eje x y nuestro vector. La fórmula la encontramos en (5) y podemos ver la representación gráfica en la figura 51. Si es menor que 0, le sumaremos 360º. angulo = arctg(y, x) (5) Figura 51. Ángulo entre un vector y el sistema de coordenadas. • public Vector2 rotate(float angulo) • public float dist(Vector2 vec), public float dist(float x, float y), Calcula la distancia entre nuestro vector y el vector o coordenadas que le pasamos. Usaremos la fórmula (6) 77 dist = Raíz( (x1-x2)^2 + (y1-y2)^2 ) • public float distSquared(Vector2 other) (6) , public float distSquared(float x, float y) Nos devuelve la distancia al cuadrado entre nuestro vector y el vector o coordenadas que le pasamos. • public Vector2 set(float x, float y), public Vector2 set(Vector2 other) Nos permite establecer los componentes x e y de nuestro vector, como argumentos o con otro vector. 5.2.2.4.3 La Cámara Recordemos que usaremos el plano de proyección paralelo y además éste será plano, es decir no tendrá eje z, porque trabajaremos en 2D. Lo podemos ver en la figura 52. Figura 52. El volumen de la vista de nuestro mundo 2D. Vamos a ver la clase camara2D, que usaremos para definir la ventana de visualización y la matriz de proyección correctamente. En esta clase almacenaremos la posición, el ancho y el alto de la vista y el factor de zoom, también necesitaremos una instancia a GLGraficos. En el constructor, almacenaremos la instancia a GLGraficos, el ancho y alto de la vista. Definiremos la posición de la cámara en el centro de la ventana de visualización, que como 78 vemos en la figura 52 está comprendida entre (0,0,1) y (frustumWidth, frustumHeight, -1) y establecemos el zoom como 1. Los métodos implementados de la clase camara2D son: • public void setVolumenvistayMatrices() Con este método establecemos el volumen de la vista, como la ventana de visualización y configuramos la matriz de proyección con los parámetros de la cámara. Primero obtenemos GL10 para poder enviar comandos a OpenGL. Después definimos el Volumen de la vista, a pantalla completa, establecemos la matriz de proyección y cargamos la identidad, configuramos la matriz de proyección definiendo las coordenadas del borde de la cámara y al final dirigimos las operaciones a la matriz modelo-vista y que las cargue en una matriz identidad. • public void toqueEnMundo(Vector2D touch) Transforma las coordenadas de un punto de contacto transformándolas al punto de contacto dentro del espacio de nuestra vista. Primero normalizamos las coordenadas x e y del punto de contacto dividiendo por ancho y alto de la pantalla, obtenemos así un rango entre 0 y 1, luego multiplicamos por ancho y alto del volumen de la vista para expresarlo en términos del espacio del mundo virtual. touch.x = (touch.x /(float) glGraficos.getWidth()) * frustumWidth * zoom; touch.y (1 - touch.y /(float)glGraficos.getHeight())*frustumHeight* zoom; Finalmente calculamos el factor de la posición del volumen de la vista y del zoom. touch.add(posicion).sub(frustumWidth * zoom / 2, frustumHeight * zoom/2); 5.2.2.4.4 Vértices Creamos la clase Vertices para almacenar los vértices e índices de los modelos que usaremos y enviarlos a OpenGL ES cuando dibujemos en pantalla. Recordemos que usaremos un búfer NIO de Java para enviar los vértices a OpenGL ES 1.0. Almacenaremos una instancia a GLGraficos, un par de booleanos para consultar si guardan color o tienen textura. Como reservaremos memoria para los vértices necesitamos saber el tamaño que ocupará cada vértice en memoria, que guardaremos en un entero. Finalmente para almacenar los vértices utilizaremos un IntBuffer y manejaremos también un 79 ShortBuffer para los índices (para usar los vértices indexados), además emplearemos un array de enteros temporal para volcar las coordenadas de los vértices que le pasaremos como array estándar. Nuestro búfer de vértices tendrá el aspecto que muestra la figura 53. Figura 53. El búfer de vértices, las direcciones en que empieza a leer OpenGL, y los saltos. En el constructor enviaremos la instancia a los gráficos, el máximo de vértices e índices que podremos almacenar y si tienen color o textura. Dentro almacenaremos estos valores, determinaremos el tamaño que ocupará cada vértice en memoria e iniciaremos los búferes. Comprobaremos si usaremos vértices indexados y si es así iniciaremos el búfer de índices. Los métodos de la clase Vertices.java son: • public void setVertices(float[] vertices, int offset, int length) Guarda los vértices en nuestra clase, recibe como parámetro un array estándar que guarda las coordenadas de los vértices, además pasamos la longitud y el desplazamiento. Primero limpiaremos nuestro búfer, calcularemos el tamaño con el desplazamiento y longitud, y volcaremos el array con los vértices en nuestro búfer temporal, para acabar transfiriéndolo al búfer de vértices, y estableciendo el tamaño de éste. • public void setIndices(short[] indices, int offset, int length) Nos permite guardar los índices de los vértices que recibe como parámetro, junto con el desplazamiento y la longitud. Limpiamos el contenido del búfer índices, copiamos los índices que recibimos como parámetro y acabamos indicando el nuevo tamaño de nuestro búfer índices. • public void draw(int primitiveType, int offset, int numVertices) 80 Nos permite dibujar, toma el tipo de primitiva, (GL10.GL_TRIANGLES), el desplazamiento y el número de vértices que vamos a dibujar. Si trabajamos con los vértices indexados calculamos el desplazamiento y dibujamos, sino los utilizamos dibujamos directamente los vértices. • public void bind() Con éste método asociamos los vértices a los atributos de color o a las texturas, si tienen. Primero cogemos la instancia de GL10 para enviar comandos a OpenGL, después le avisamos a OpenGl que pasamos los vértices y le decimos dónde encontrar los datos (posición 0 del búfer), el tamaño (2: x e y) y el salto para cada uno(vertexSize) (figura 53), luego miramos si tienen color y hacemos lo mismo: avisamos a OpenGL que pasamos el color, le decimos dónde los tiene que coger, el tamaño (4: r,g,b,a), el desplazamiento para el siguiente. Finalmente miramos si el vértice lleva textura y hacemos lo mismo. • public void unbind() Nos permite liberar los atributos de los vértices una vez hemos terminado de dibujar. Simplemente miramos si los vértices tienen color y/o textura y pedimos a OpenGL que los deshabilite. 5.2.2.4.5 Texturas Para cargar los gráficos utilizaremos dos clases, Textura y RegionTextura. Con Textura se podremos cargar un mapa de bits almacenado como recurso. La segunda RegionTextura, nos sirve para coger porciones del mapa de texturas. Veamos primero la clase Textura. Utilizaremos una instancia a GLGraficos para cargar la textura en OpenGl, una referencia FileIO y un String para leerla desde nuestro archivo e indicarle el nombre de éste. Un entero que nos guardará el identificador de la textura para que podamos trabajar con ella. Finalmente guardamos cuatro enteros más. Los dos primeros nos permiten especificar los filtros (de reducción y ampliación) y los siguientes nos guardaran el tamaño de la textura, ancho y alto. 81 En el constructor le pasaremos una referencia a glJuego y el nombre del archivo en que se encuentra la textura. Obtendremos las referencias al módulo de gráficos (GLGraficos) y al módulo ArchivoIO (FileIO) del parámetro glJuego y almacenaremos el nombre de archivo para poder cargar más tarde la textura. Vamos a ver sus métodos: • private void load() Utilizaremos este método para cargar una textura. En él lo que hacemos primero es obtener GL10 para poder enviar comandos a OpenGL ES. Después creamos el objeto textura en OpenGL que aún estará vacío, nos devolverá un identificador para la textura y éste lo utilizaremos para indicarle a OpenGL las operaciones que queramos hacer con ella. Seguidamente leeremos la imagen del archivo y la decodificamos como un Bitmap, y asociamos este Bitmap con la textura que habíamos creado en OpenGL, a partir de aquí el objeto textura y su imagen asociada estará almacenado en la RAM de vídeo, (por eso se pierde cuando se destruye OpenGL). A continuación especificamos filtros, eliminamos la asociación porque no la vamos a usar más, guardamos los datos de ancho y alto y terminamos liberando el Bitmap. En el caso de no encontrar el archivo con la textura obtendremos un error. • public void reload() Éste es el método que usaremos para recargar una textura una vez OpenGL pierda el contexto, es decir cuando se pause la aplicación. • public void setFilters(int minFilter, int magFilter) Con este método establecemos los filtros de ampliación y reducción para las texturas. Aquí los parámetros son enteros que indican el filtro que asociará OpenGL para la textura. • public void bind() Nos permite asociar la textura a OpenGL. • public void dispose() Sirve para liberar el objeto textura de la RAM de video, lo que hacemos es liberar la asociación de la textura, y la borramos de OpenGL ES. 82 Vamos a ver ahora la clase RegionTextura, que nos permite definir una región dentro de una textura. La usamos para dibujar una parte de una textura, en nuestro caso para coger los elementos de nuestro mapa de texturas. En ella almacenaremos las coordenadas de la esquina superior izquierda (la coordenada del elemento dentro del mapa de texturas), y las coordenadas de la esquina inferior izquierda, junto con la textura, de la que forma parte. Figura 54. Ejemplo de regiones de Textura, con la coordenada superior izquierda. Llamaremos al constructor enviándole la textura, la coordenada superior izquierda, y el alto y ancho de la región. En la figura 54 podemos observar algunas regiones de nuestro mapa de texturas correspondientes a los botones, con sus coordenadas superior izquierda. 5.2.2.4.6 Dibujando Texto Para escribir texto en nuestro juego, hemos visto que vamos a utilizar una fuente bitmap que cargaremos desde el mapa de texturas. Para ello nos valdremos de la clase Fuente que almacena los caracteres de la fuente bitmap. Para ello usaremos las variables textura para coger los caracteres de ésta, y el ancho y alto de los glifos (todos los caracteres tienen el mismo). Y con un array de TextureRegion almacenaremos la región de cada carácter en el constructor. El primer elemento contiene la región con el carácter espacio que tiene el código ASCII 32, el siguiente la exclamación código 33, etcétera y el último aquél cuyo código es 127. Ver la figura 55. En el constructor le pasaremos cuantos glifos tenemos por fila y la esquina superior izquierda del área de la fuente bitmap en el mapa de texturas. 83 Con el método public void drawText(SpriteBatcher batcher, String text, float x, float y) podremos escribir una línea de texto que le pasemos, en la posición que especifiquemos. Para ello obtendremos el índice del carácter comprobar si tenemos glifo para él y dibujarlo usando SpriteBatcher. Después incrementamos x para escribir el siguiente carácter. Figura 55. Tabla con los caracteres ASCII y sus valores decimales. 5.2.2.4.7 Lote de Modelos Ya hemos visto que tenemos casi todas las imágenes del juego cargadas en el mapa de texturas para no tener que ir cambiando de textura. La clase SpriteBatcher implementa un lote de modelos, es decir nos permite dibujar un grupo de sprites (modelo + textura) en una única operación. Eso sí siempre de la misma textura. 84 En la clase guardaremos: un array con las posiciones de los vértices del lote, un entero que indica el índice del búfer para empezar a escribir las posiciones de los vértices, una instancia a Vertices para enviar los vértices a OpenGL y que los dibuje, y otro entero que contendrá el número de sprites que dibujaremos en el lote. Al constructor lo llamaremos con la referencia a GLGraficos y con el número máximo de Sprites (modelos) que puede almacenar el lote. En él inicializamos el array de vértices, configuramos la instancia a Vertices (4 vértices por modelo y 6 índices), inicializamos el índice de del array vértices y el número de Sprites dibujados. Finalmente creamos un búfer para guardar los índices indexados de los modelos, e inicializamos sus valores. Para hacerlo recorremos el búfer índices generando los valores para cada modelo, el primero tendrá los índices 0,1,2,2,3,0 el segundo 4,5,6,6,7,4 el tercero 8,9,10,10,9,8 y así sucesivamente (Figura 55). Finalmente entraremos los índices en la instancia Vertices y ya no tendremos que definirlos más ya que éstos no cambiarán aunque cambien los modelos. Figura 55. Indexando índices para los modelos. Los métodos de la clase son: • public void beginBatch(Textura textura) Asocia la textura que le pasamos e reinicia los valores para empezar a trabajar con el lote. • public void drawSprite(float x, float y, float ancho, float alto, RegionTextura region) Con este método preparamos un sprite (modelo+textura) en el lote, para cuando los tengamos todos dibujarlos de una llamada. Le pasamos las coordenadas del centro del modelo, x e y, el ancho y el alto y la región RegionTextura. Calculamos las coordenadas de las esquinas del sprite (inferior derecha y superior izquierda), ver figura 56, y almacenamos en nuestro array las coordenadas de cada vértice del modelo con las 85 correspondientes a su RegionTextura. Finalizamos incrementando el valor del número de sprites en el lote. Figura 56. Calculando los vértices del modelo • public void drawSprite(float x, float y, float ancho, float alto, float angulo, RegionTextura region) Con este método también preparamos un sprite (modelo+textura) en el lote, pero lo rotaremos el angulo que le pasamos como parámetro. El resto de párametros son igual que el anterior método. Calculamos las coordenadas de las esquinas del sprite, para ello cogemos el angulo lo pasamos a radianes y calculamos el seno y el coseno para obtener los vértices (1). Y almacenamos en nuestro array las coordenadas de cada vértice del modelo con las correspondientes a su RegionTextura. Finalizamos incrementando el valor del número de sprites en el lote. La fórmula que usamos para rotar los puntos un determinado ángulo es: v.x’ = cos(angulo) * v.x – sin (angulo) * v.y (7) v.y’ = sin(angulo) * v.x + cos (angulo) * v.y • public void endBatch() Con este método indicamos que hemos completado el proceso de dibujo del lote, es decir que hemos preparado todos los sprites que vamos a dibujar con drawSprite, y lo que hace es enviar a la GPU todos los datos para presentarlos en pantalla. En él transferimos los vértices a la instancia Vertices, los asociamos, los dibujamos y finalmente los liberamos la asociación. 86 Figura 57. Rotación de un vector como posición. 5.2.2.4.8 Medir el Rendimiento Creamos la clase FPSContador para medir cuántos frames (fotogramas) por segundo consigue dibujar nuestro juego. El resultado lo visualizaremos en el Log del sistema. Es muy sencilla guardaremos el tiempo de inicio, y un contador de frames, tenemos un método public void logFrame() al que llamamos cada vez que la superficie convoque al dibujo, incrementaremos el valor de frames y el tiempo transcurrido, si éste es mayor de un segundo sacamos el resultado por el Log y reiniciamos las variables. 5.2.2.4.9 Otras Clases Con tal de dar soporte a la parte de interfaz de usuario, creamos un par de clases más que incluimos en mismo paquete con la parte de OpenGL, Boton y DentrodeBoton. La clase Boton la usaremos para definir los botones de los menús (estos serán rectangulares), en ella almacenaremos la posición de la esquina inferior izquierda utilizando Vector2D, y el alto y ancho del botón. La clase DentrodeBoton la utilizamos para saber si un toque en la pantalla se ha producido dentro de un botón definido, en ella no almacenamos nada tan sólo contiene los métodos: • public static boolean pointInRectangle(Boton r, Vector2D p) • public static boolean pointInRectangle(Boton r, float x, float y) 87 En los que comprobamos si la posición (definida como vector2D o como posición x e y) se encuentra dentro del botón recibido por parámetro. Esta clase la hemos separado para crear sus métodos como static, así al llamarlos no se crea el objeto DentrodeBoton evitando así al Recolector de basura. 5.2.2.5 Módulo Gestor de Ventanas GLJuego, es nuestro de gestor de ventanas. Recordemos que el Gestor de Ventanas hace de módulo principal, el que enlaza el resto de módulos y los comunica. Desde él trabajaremos con la interfaz de usuario, para que éste interactúe con la aplicación y dirigiremos sus eventos al resto de módulos. • La ventana trabajará con una interfaz de usuario (IU), para permitir a éste que interactúe con la pantalla. • Configura la actividad y la vista, inicia y dirige las referencias destinadas a GLGráficos, Audio, FileIO e Input a las partes seleccionadas, • Gestionar la Pantalla con el ciclo vital de la actividad, haciendo de renderizador (hace que la pantalla se muestre en cada iteración). • Lleva un registro de estado de la ventana e informará a la pantalla de los eventos (si el juego está detenido o reanudado). • Llevará cuenta del rendimiento de nuestro juego (Frames por Segundo). • Iniciará el bucle principal. Recordemos que el juego trabajará con varios hilos, así que debemos tener especial cuidado en sincronizar el hilo de ejecución con el hilo de dibujo, para que las Pantallas dependan sólo del hilo de ejecución, ya que sólo podemos acceder a OpenGL ES desde el hilo de dibujo. Almacenaremos instancias a GLSurfaceView, GLGraphics, también a los distintos módulos del framework, Audio, Input, FileIO, y Pantalla. Usamos state como el enumerador de GLState para saber el estado en que se encuentra. stateChanged lo usaremos para sincronizar los hilos de ejecución de la interfaz de usuario y de dibujo. Con 88 startTime controlaremos el intervalo de tiempo y con WakeLock evitaremos que la pantalla entre en modo ahorro de energía. Como extiende de Activity posse sus métodos: • public void onCreate(Bundle savedInstanceState) Aquí configuramos la vista (GLSurfaceView como vista), la ponemos a pantalla completa y desactivamos el ahorro de energía de pantalla (con WakeLock) e iniciamos las clases que implementan el framework (Input, Audio, FileIO). La escala de coordenadas será 1. • public void onPause() En este método lo llamaremos desde el hilo de ejecución de la IU cuando se pause la actividad, y lo usaremos para notificar a la actividad que estamos pausando la aplicación. Configuramos el estado dependiendo de si sólo se pausa o si se cierra y esperamos a que el hilo de dibujo también entre en modo pausa (lo veremos a continuación). Para finalizar liberamos WakeLock y pausamos GLSurfaceView y la propia actividad. Esto provocará la perdida de contexto de OpenGL ES. • public void onResume() Aquí dejaremos que GLSurfaceView utilice su metódo onResume() y obtendremos de nuevo el wakeLock. Además GLJuego también implementa la interfaz GLSurfaceView.Renderer de OpenGL implementando estos métodos: • public void onSurfaceCreated(GL10 gl, EGLConfig config) Este método se llama desde el hilo de dibujo, utiliza el estado para mirar si es la primera vez que inicia la aplicación, si es así llama al método getStartScreen() que nos devuelve la pantalla de inicio del juego. Cambiamos el estado a running y hacemos resume() de la pantalla. Finalmente tomamos nota de la hora para calcular más tarde el intervalo de tiempo. • public void onSurfaceChanged(GL10 gl, int width, int height) Lo llama el hilo de dibujo cuando cambia la superficie (al girar el móvil), en él no haremos nada, ya que definimos la orientación de nuestro juego en el archivo Android Manifest. 89 • public void onDrawFrame(GL10 gl) Este método es el responsable de dibujar en la pantalla, lo llama automáticamente el hilo de ejecución responsable de dibujar, tantas veces como le es posible. Primero sincronizamos el acceso al estado para asegurarnos que onPause() no está usándolo. Luego comprobamos cómo se encuentra y reaccionamos en consecuencia. Si está ejecutándose, calculamos el tiempo y actualizamos la Pantalla activa (según los eventos de IU) y la dibujamos, después medimos el rendimiento (cuántos FPS alcanza nuestro juego). Si está pausado, pausaremos la Pantalla y notificamos que hemos recibido la petición de pausa desde la IU (que estará esperando) y le avisamos que hemos terminado de dibujar y ya puede pausar la aplicación. Finalmente si se está cerrando, pararemos la Pantalla y la liberaremos, e igual que antes notificamos al hilo de IU que hemos terminado, para que pueda terminar de cerrar la aplicación. Nuestra clase GLJuego también implementa la interfaz juego, los métodos de ésta son: • public GLGraficos getGLGraficos() Nos devuelve la instancia GLGraficos para poder acceder a la interfaz GL10 con la que enviaremos comandos a OpenGL ES. • public Input getInput() • public FileIO getFileIO() • public Audio getAudio() Con estos obtendremos las instancias al resto de módulos. • public void setScreen(Pantalla pantalla) Este método pone como pantalla activa, la pantalla que le pasamos como parámetro. Primero comprobamos que no sea nula, después pausamos la pantalla que esté activa en ese momento y la liberamos, entonces activamos la pantalla que pasamos al método como parámetro, la actualizamos (para que las entradas entren en la nueva pantalla y no en la anterior) y la establecemos como pantalla actual. • public Pantalla getCurrentScreen() 90 Este método nos sirve para consultar la pantalla activa. 5.2.3 Los elementos del Juego Vamos a ver ahora los elementos que forman nuestro juego, éstos están incluidos en el paquete com.games.edo.JuegoBlocks, usan el framework para trabajar y componer nuestro juego. 5.2.3.1 Configuración Con la clase Configuraciones registraremos las puntuaciones máximas y las opciones del usuario (si el audio está activado o no). Figura 58. Icono configuración Android En ella guardaremos un booleano para indicar si el audio está activado y un array de enteros que guardará las nueve puntuaciones más altas ordenadas de mayor a menor. Veamos ahora lo métodos: • public static void save(FileIO archivo) Con este método guardaremos la configuración actual en el archivo llamado .blocks en la unidad externa de almacenamiento. En él guardaremos en líneas separadas las entradas de las puntuaciones máximas y la configuración del sonido. Si hay algún error (como que no esté disponible la tarjeta) ignoraremos el fallo y continuaremos con la configuración predeterminada. • public static void load(FileIO archivo) 91 Intentará cargar la configuración del archivo llamado .blocks que está en la unidad externa de almacenamiento, sabiendo que las entradas están en líneas separadas. Si hay algún error ignoramos el fallo y continuamos con la configuración predeterminada. • public static void anadirPuntuacion(int puntuacion) Añade la puntuación que recibe como parámetro al array de puntuaciones máximas si supera alguna de éstas, desplazando las inferiores en el array. 5.2.3.2 Recursos Con la clase Assets almacenaremos las referencias a las variables estáticas de nuestros recursos. Contendrá todos los recursos, a excepción de las texturas de la pantalla de ayuda. Serán instancias Textura, RegionTextura, Animacion, Musica y Sonido. Sus métodos son: • public static void load(GLJuego game) Lo llamaremos al iniciar el juego, cargará los recursos y creará las RegionTextura necesarias. También instancia la clase Font que utilizaremos para dibujar el texto, con la fuente bitmap incluida en el mapa de texturas. Finalmente cargará los recursos de audio y consultando la clase configuraciones arrancará la música o no. • public static void reload() Lo usaremos para recargar las texturas cuando OpenGL ES pierda el contexto, (es decir cuando la aplicación entra en modo pausa: cuando llaman o tocamos la tecla home…), en caso que el sonido esté activado también retomamos su reproducción. • public static void playSound(Sonido sonido) Este método lo usaremos para comprobar si el sonido está activado y reproducirlo, lo usaremos en el resto del código. 92 5.2.3.3 La Actividad Principal La actividad que actúa como entrada de punto del juego es JuegoBlocks, esta extiende de GLJuego en ella simplemente guardamos un booleano que nos indica si es la primera vez que se crea el juego. Sus métodos son: • public Pantalla getStartScreen() Nos devuelve la pantalla de inicio del juego, que es PantallaMenuPrincipal. • public void onSurfaceCreated(GL10 gl, EGLConfig config) Sobrescribimos el método que se llamará cada vez que se vuelva a crear el contexto de OpenGL ES, miraremos mediante el booleano si es la primera vez que carga el juego, si es así cargaremos las configuraciones y los recursos, si por el contrario ya había cargado antes deberemos recuperar el contexto recargando las texturas e iniciar la música si se encontraba activado. • public void onPause() Este método se llamará cuando se detenga la actividad, en él solamente nos encargamos de pausar la música si el sonido está activado. 5.2.3.4 Pantallas Como ya hemos visto nuestro juego se divide en pantallas, cada una tiene sus funcionalidad, y por eso difieren en los elementos que utilizan, (la pantalla menú tiene 4 botones y un logo, la pantalla de ranking posee un título, las puntuaciones y un botón, etc.) La clase GLPantalla está incluida en el paquete framework.andoidimplementado. En ella almacenamos una instancia a GLGraficos, para poder dibujar en OpenGL ES y otra instancia al módulo gestor de ventanas GLJuego para obtener el acceso al framewok. En el constructor simplemente le pasamos cómo parámetro una referencia a GLJuego, la almacenamos y obtenemos la referencia a GLGraficos. Todas las pantallas de Blocks proceden de esta clase que además hereda la interfaz Pantalla así pues todas comparten los métodos básicos especificados en esta interfaz. 93 Vamos a describir que hacen estos métodos para luego ver cada clase pantalla con sus elementos y cómo los lleva a cabo: • public abstract void dibuja(float deltaTime) Este método se encarga de dibujar directamente en la pantalla lo que le especifiquemos aquí. • public abstract void pause() Este método lo llamaremos cuando se pause la actividad, su implementación dependerá de la pantalla en la que estemos. • public abstract void resume() Nos servirán para controlar el estado de la pantalla. • public abstract void dispose() Para liberar una pantalla una vez utilizada. • public abstract void update(float deltaTime) Este método es el que se encarga de recibir los eventos de entrada y actualizar la pantalla en consecuencia. Veamos pues las clases que implementan las pantallas de nuestro juego. 5.2.3.4.1 Menú Principal PantallaMenuPrincipal, implementa el Menú del juego, es la pantalla de inicio de éste. En ella veremos el logo del juego, los botones para acceder a las distintas partes del juego, y el botón para activar/desactivar el sonido. La podemos ver en la Figura 59. Figura 59. Pantalla Menú Principal 94 En esta clase guardamos un objeto camara2D, un lote de modelos SpriteBatcher para dibujar los elementos, los botones que definen las partes táctiles (Jugar, Ranking, Ayuda, Sonido), también guardamos un vector2D que usaremos para saber en qué punto el usuario toca la pantalla, y unas variables para el efecto de animación del logo en que guardamos el angulo, y el tamaño de escala. En el constructor configuramos la cámara para nuestra resolución fija, definimos el lote de modelos, y colocamos los botones táctiles en sus posiciones, indicando el tamaño. Finalmente definimos los valores para el efecto de rotación y escalado del logo del juego. Veamos sus métodos: • public abstract void update(float deltaTime) En el método cogemos los eventos de teclado, para garantizar que no salte el garbage collector, después recorremos los eventos de toque comprobando si se ha levantado el dedo, transformamos a coordenadas del mundo virtual, y si se ha producido en alguno de los botones, Jugar, Ranking, Ayuda, iniciaremos la transición de pantalla, si se ha producido en el botón de Sonido lo activaremos/desactivaremos según su estado actual. • public abstract void dibuja(float deltaTime) Aquí, limpiamos la pantalla, definimos las matrices, preparamos en el lote de modelos el fondo y lo dibujamos, después preparamos todos los elementos de la interfaz y al acabar los dibujamos, finalmente calculamos los nuevos valores para el efecto del logo. • public abstract void pause() Aquí guardamos las configuraciones de nuestro juego en la tarjeta SD. 5.2.3.4.2 Pantallas de Ayuda Hemos dividido la ayuda en un total de tres pantallas, las clases que las implementan son PantallaAyuda, PantallaAyuda2 y PantallaAyudaFinal. En ellas cargaremos la imagen correspondiente de su pantalla de ayuda (ayuda1.png, ayuda2.png, ayuda3.png), y mostraremos los botones para avanzar/retroceder (según en qué pantalla nos encontremos) en la esquina inferior derecha y el botón para volver al menú en la esquina inferior izquierda. Las pantallas de ayuda sólo difieren de la imagen y de los botones avanzar/retroceder. 95 Los miembros de estas clases son prácticamente iguales a los del menú principal, por un lado tenemos la camara2D, el lote de modelos SpriteBatcher para dibujar los elementos, y los botones que definen las partes táctiles (botonAtras, botonSiguiente, botonSalir), el vector2D para saber en qué punto el usuario toca la pantalla, y cómo usaremos una imagen necesitamos una Textura y una RegionTextura. El constructor es igual al del menú principal, configuramos la cámara para nuestra resolución fija, colocamos los botones táctiles en sus posiciones con su tamaño, creamos el vector para el punto táctil y definimos el lote de modelos. Veamos sus métodos: • public void resume() Aquí tan sólo cargamos la Textura, y creamos su región correspondiente. • public void pause() Cuando se pause, lo único que hacemos es liberar la textura para ahorrar memoria. • public void update(float deltaTime) Este método funciona igual que el del menú principal, nos limitamos a comprobar si pulsamos algún botón para iniciar así la transición de pantalla correspondiente. • public void dibuja(float deltaTime) El método de dibujar también es igual al del menú principal, limpiamos la pantalla, definimos las matrices, dibujamos la imagen de ayuda y después los botones correspondientes. 5.2.3.4.3 Pantalla Ranking En la pantalla de Ranking utilizaremos como título parte de la etiqueta que mostramos en la pantalla principal (Ranking) y dibujaremos las puntuaciones máximas que guardamos en Configuraciones, también tendrá un botón para regresar al menú Principal. Los miembros de estas clases vuelven a ser iguales, por un lado tenemos la camara2D, el lote de modelos SpriteBatcher, el boton para volver al menú principal, el vector2D para determinar el punto en que el usuario toca la pantalla, y finalmente guardaremos un array 96 de Strings con las puntuaciones y valor con el desplazamiento con el que calcularemos la distancia para que las puntuaciones queden centradas en pantalla. En el constructor volvemos a configurar la cámara, colocamos el boton salir, creamos el vector para el punto táctil, definimos el lote de modelos, aquí el número de modelos máximo debe ser un poco alto porque no sabemos cuántos caracteres deberemos dibujar y debemos asegurarnos que podemos trabajar con todos, si calculamos que cada línea tendrá un máximo de 9 caracteres y hay 9 líneas, con el lote configurado a 100 modelos nos sobra, después configuramos las cadenas de puntuaciones colocando el valor, un punto y la puntuación. Calculamos el valor del desplazamiento mirando qué cadena es la más larga (multiplicando el número de caracteres por el ancho de glifo), y finalmente colocamos el valor en el centro de la pantalla restando el desplazamiento dividido entre 2 y sumando el ancho del glifo dividido entre dos (necesitamos sumarle este último porque emplea el centro del glifo en vez de sus esquinas). Veamos sus métodos: • public void update(float deltaTime) Este método funciona igual, nos limitamos a comprobar si pulsamos el botón para volver a la pantalla del menú principal. • public void dibuja(float deltaTime) El método de dibujar vuelve a ser igual al del menú principal, limpiamos la pantalla, definimos las matrices, dibujamos el fondo y después los elementos: la etiqueta Ranking, las líneas con las puntuaciones y el botón de volver, para las puntuaciones usamos el valor de desplazamiento que calculamos en el constructor y la altura la vamos definiendo en cada línea. 5.2.3.4.4 Pantalla Juego PantallaJuego es la clase que muestra la acción del juego. Se divide en distintas pantallas: preparado, juego corriendo, pausado, fin de juego y sin movimientos. Estas pantallas no las cargamos con distintas clases sino que desde PantallaJuego, mostramos u ocultamos los elementos que necesitemos, según en qué pantalla se encuentre el jugador. 97 Veamos los miembros de esta clase: empezamos definiendo las constantes para el estado en que se encuentra el juego: preparado, corriendo, pausado, fin de juego, o sin movimientos. Además también definimos las constantes con las direcciones en que movemos los bloques al jugar: arriba, abajo, izquierda, derecha. Por otro lado guardamos el estado del juego, la camara2D, el lote de modelos SpriteBatcher para dibujar los elementos, y los botones (botonSonido, botonPausa, botonContinuar, botonSalir, botonLupa1, botonLupa2, botonLupa3), el vector2D para saber en qué punto el usuario toca la pantalla. Guardaremos una instancia al tablero del juego, otra para el que se encarga de dibujarlo en pantalla tableroRender (lee los elementos del tablero y los dibuja con el mismo lote de modelos, lo veremos más adelante) y la instancia tableroListener que nos servirá para reproducir los efectos de sonido cuando se produzca uno de estos eventos. Figura 60. Pantalla Juego (corriendo). Usaremos varios Strings para mostrar información en la pantalla: puntosString para la puntuación, tiempoString para mostrar el tiempo restante de juego y para llevar la cuenta atrás cuando no haya movimientos, nivelString para mostrar el nivel actual y restantesString para mostrar cuantos bloques nos restan para pasar de nivel. Utilizamos un NumberFormat para que el tiempo sólo muestre dos decimales en pantalla. Con los enteros ultimaPuntuacion, ultimosRestantes y ultimoNivel comprobaremos si han cambiado los valores y necesitamos actualizar los Strings, los utilizamos para no generar cada vez los Strings y que no salte el recolector de basura. Para controlar los eventos de toque nos valdremos de las variables downX y downY que utilizaremos para controlar el arrastre del dedo en pantalla y del booleano cambiado para saber si se ha procesado el evento de toque desde el tablero. Por último tenemos las variables que controlan el tiempo en la transición entre la pantalla de No hay movimientos y la de Juego ejecutándose, tiempo que guarda el 98 tiempo a transcurrir entre estas dos pantallas (la cuenta atrás) y tiempoActual, tiempoAnterior con el que calculamos el tiempo transcurrido. En el constructor iniciamos las variables: ponemos el estado como preparado, configuramos la cámara, creamos el vector para el punto táctil, definimos el lote de modelos, con 100 modelos nos sobra (40 bloques + 3 lupas + 1 Boton pausa + los caracteres de los strings que calculamos un máximo de 50), después creamos el detector de fx tableroListener como clase interna y después configuramos tablero para que registre éste detector, (desde tablero generaremos los eventos para reproducir los efectos de sonido), iniciamos el tableroRenderer, y después los elementos de la interfaz (botones, lupas), terminamos configurando las variables para el inicio del juego. Ahora vamos a conocer los métodos de la clase, ya hemos visto que según el estado en que se encuentre el juego dibujaremos ciertos elementos u otros, así pues al actualizar las entradas del usuario también dependerá del propio estado. Veamos los métodos de actualización: • public void update(float deltaTime) El método de actualización de la pantalla hace de maestro, mira en qué estado nos encontramos (pantalla) para llamar a los métodos de actualización correspondientes. • private void updatePreparado() En éste método lo único que hacemos es esperar a que el usuario pulse la pantalla y levante el dedo para entonces poner el estado del juego a Corriendo. • private void updatePaused() En este método comprobamos si el usuario toca cualquiera de los botones: Continuar, Salir, Sonido de la interfaz de usuario para actuar en consecuencia. • private void updateNoMoves() Cuando el tablero se quede sin movimientos mostraremos la pantalla de No hay movimientos con una cuenta atrás de dos segundos para retomar el juego. Aquí generaremos la cuenta atrás para que pasen los tres segundos, y poner el estado a Corriendo. Llamaremos al método cuentaAtras(). 99 • private void cuentaAtras() Es el método que lleva a cabo la cuenta atrás para retomar el juego, en él tomamos el tiempo de entrada al método inicio, y lo vamos actualizando para restarlo a los dos segundos deseados, una vez transcurridos ponemos el estado a Corriendo y reiniciamos los valores por si volvemos a quedarnos sin movimientos, finalmente llamamos a tablero.noMoves() que generará un tablero nuevo y listo para jugar. • private void updateGameOver() Cuando acabe el juego esperaremos a que el usuario pulse la pantalla y levante el dedo para entonces cambiar la pantalla actual por la pantalla de Ranking. • private void updateRunning() En este método empezamos comprobando los eventos del usuario, primero miramos si el usuario toca el botón pausa, en caso afirmativo pondremos el estado a Pausa, después miramos si el usuario pulsa sobre las lupas, comprobando si tiene disponibles, en caso afirmativo llamamos a tablero.lupas() . Después miramos si el usuario arrastra el dedo por la pantalla y en qué dirección con el método eventoDireccion() que veremos a continuación. Una vez comprobados los eventos del usuario, actualizamos el tiempo del tablero (desde aquí para que se muestre en pantalla) y comprobamos si han cambiado los Strings en cuyo caso los actualizaremos. Acto seguido comprobamos si el estado del tablero es Fin de Juego, en cuyo caso actualizamos el estado de la pantalla a Fin de Juego y generamos la cadena de carácter de Puntos acorde si se ha establecido un nuevo récord o no y añadiremos la puntuación a Configuraciones y salvamos la configuración del juego. Finalizamos comprobando si el estado del tablero es Sin Movimientos, en cuyo caso cambiamos el estado de la pantalla a Sin Movimientos. • private void eventoDireccion(TouchEvent event) Con éste método miramos los eventos que produce el jugador dentro del tablero del juego y actualizamos el tablero en consecuencia. Para ello lo primero que hacemos es transformar las coordenadas de toque a coordenadas OpenGL, después comprobamos si el evento está dentro del tablero. Si lo está comprobamos el tipo de evento: si toca la pantalla guardamos estas coordenadas para calcular la fila y columna del tablero en la que toca, si arrastra el dedo por el tablero en este caso comprobamos que el arrastre sea entre una 100 casilla del tablero y calculamos la dirección en que lo hace. Finalmente actualizamos el tablero con los datos calculados (columna, fila y dirección). La columna y la fila los obtenemos, dividiendo la coordenada de toque por el tamaño (alto/ancho) de una casilla del tablero. Para dibujar la pantalla de Juego, lo que hacemos es dibujar el fondo con el tablero, después los elementos de la interfaz según el estado del juego y si está corriendo los bloques del tablero. Veamos los métodos: • public void dibuja(float deltaTime) Hace de método maestro igual que el método de actualización update(), miramos en qué estado se encuentra para llamar a los métodos de dibujo correspondientes, a fin de dibujar unos elementos u otros. Primero limpiamos la pantalla, definimos las matrices, preparamos en el lote de modelos el tablero y lo dibujamos, después preparamos la fusión para los elementos a dibujar y es entonces cuando miramos el estado del juego para dibujar lo que corresponda. • private void dibujaPreparado() En dibuja preparado tan sólo dibujamos la etiqueta preparado. • private void dibujaPaused() Aquí dibujamos la etiqueta del menú de pausa y el botón de sonido. • private void dibujaNoMoves() Es el método que dibuja cuando no hay movimientos, dibujamos tres strings. Uno que nos avisa de que no hay movimientos disponibles, el segundo nos informa que se está generando un tablero aleatorio, y el tercero es una cuenta atrás que al llegar a cero cambia la pantalla. Calculamos el ancho de los strings a fin de centrar su dibujo en la pantalla. • private void dibujaGameOver() En este método dibujamos la etiqueta de Fin de juego y la puntuación obtenida, ésta la centramos en la pantalla calculando el ancho del string. 101 Figura 61. Pantalla Juego (Fin de juego). • private void renderizaLupas() Se encarga de dibujar las lupas de la interfaz, para ello comprobamos cuantas lupas hay disponibles en el tablero y dibujamos en consecuencia. • private void dibujaRunning() Éste es el método que dibuja cuando el juego está en marcha, lo primero que hacemos es dibujar los elementos de la interfaz: Puntuación, Tiempo, bloques restantes, nivel y el botón de pausa. Seguidamente dibujamos las lupas con renderizaLupas() y finalmente el tablero de juego, mediante su renderizador tableroRenderer que veremos un poco más adelante. • public void pause() Aquí nos encargamos de poner el estado de juego a pausa, solamente si estaba corriendo. Si se encuentra en preparado o en Fin de Juego no hacemos nada. 5.2.3.5 Definir el Tablero de Juego El tablero de juego, es el que se encarga de la simulación de la lógica del juego. Utilizamos la clase Bloque para definir cada bloque del tablero. La clase tablero contendrá el tablero con todos los bloques y permite interactuar con ellos. Finalmente para separar el modelo de la vista (Modelo MVC) tenemos la clase tableroRenderer que se encarga de dibujar en pantalla los bloques del tablero. Ésta crea además la animación de los bloques para que parezca que se desplazan por la pantalla (cuando se intercambian o caen por gravedad), 102 actualizando la posición de éstos. Esto nos supone un problema ya que el renderizador del tablero tarda más en llevar a cabo la animación que el propio tablero en realizar las operaciones de intercambiar bloques o bajarlos por gravedad, así que deberemos sincronizar estas dos clases. También necesitamos pasar las entradas del usuario que recibimos desde la pantalla de juego al tablero, para que éste opere en consecuencia, y generar un tablero nuevo cuando no haya movimientos disponibles y termine la cuenta atrás de la pantalla Sin movimientos. Vamos a ver en pseudocódigo cómo tratamos el arrastre de Bloque para que quede claro y podamos ver quien realiza la acción. tablero_actualiza(entrada) si (entrada == arrastre){ animaBloque(); intercambiaBloque(); si ( !Linea()){ //si no forman los volvemos a su posición animaBloqueContrario(); intercambiaBloque(); } sino{ eliminaBloques(); animaGravedad(); aplicaGravedad(); rellenaTablero(); } } 5.2.3.5.1 Bloque Esta clase es muy sencilla, almacena la información que necesitamos de un bloque. Guardamos el color, las coordenadas en pantalla x e y, un booleano moviendo que nos sirve para saber si el bloque se está desplazando mientras dura la animación, un booleano cambiado que lo emplearemos para controlar en el intercambio de bloques si forman línea o no, y un entero dir que nos informará de la dirección en que se mueve el bloque. 5.2.3.5.2 Tablero La clase tablero es la que contiene todos los bloques e implementa la simulación del juego. 103 Lo primero que hacemos es definir la interfaz TableroListener que utilizamos para reproducir efectos de sonido con el listener, registramos las siguientes llamadas a los sonidos: • linea() cuando se forme una línea y se eliminen bloques. • nivelSuperado() se reproduce al superar un nivel. • noCombinacion() lo llamaremos al intercambiar dos bloques y éstos no formen línea. • LupaPulsada() al tocar el comodín de lupa. • LupaObtenida() al conseguir una puntuación que proporcione una lupa extra. Después definimos nuestras constantes, para los movimientos del dedo del jugador: toca, arriba, abajo, derecha, izquierda y levanta. Las constantes para el tamaño del tablero: tablero alto y tablero ancho y del estado del juego: corriendo, nivel siguiente, fin de juego, sin movimientos. Después definimos las constantes incremento de puntuación y tiempo por nivel. Finalmente las constantes que utilizamos para definir la posición de inicio del primer bloque en pantalla yInicio, xInicio, y las constantes offsetX offsetY como desplazamiento entre columnas y filas para los siguientes bloques. Figura 63. El Tablero de juego. Ahora vemos las variables que utilizamos: guardaremos el nivel en que nos encontremos, los puntos del jugador, los bloques restantes para pasar de nivel, el tiempo restante y el estado de juego. Utilizaremos tiempoAnterior y tiempoActual para calcular el tiempo transcurrido, y el listener TableroListener para reproducir los efectos fx. Necesitamos guardar una matriz de bloques, y utilizaremos dos arrays x_removes e y_removes para 104 guardar los índices de la matriz de los bloques a eliminar junto con un contador. Guardaremos aleatorio y rand que los utilizaremos para generar el color de los bloques aleatoriamente. Utilizaremos posX, posY para generar las posiciones de los bloques en pantalla. Y dos matrices posicionesX y posicionesY para guardar las posiciones en pantalla dónde dibujar los bloques cuando no se mueven. Finalmente usamos los booleanos aplicandoGravedad y dibujaLupa para sincronizar con el Renderizador, y después almacenamos variables que usaran los métodos de la clase, para evitar la creación y destrucción de objetos para que no se ejecute el Recolector de basura: i,j,k que usaremos para recorrer la matriz de bloques, ret que indica si hay bloques a eliminar, x e y para calcular intercambio de bloques y tmp como bloque temporal para realizar el intercambio, finalmente x1, y1, x2, y2 almacenarán posiciones de bloques intercambiables. El constructor inicia los miembros y guarda el detector TableroListener que se le pasa por parámetro, inicia los bloques con sus posiciones en pantalla, asigna los colores aleatoriamente y llama a limpiaTablero() que deja el tablero listo para jugar, lo veremos más adelante, finalmente comprueba si hay movimientos y si no es así cambia el estado de juego. Los métodos de la clase Tablero son: • public boolean update(int dir, int fila, int col, boolean cambiado) Este método lo llamaremos desde la pantalla de juego. Actualiza el tablero respondiendo al evento que produzca el jugador, dir indica la dirección en donde moveremos el bloque o si no lo moveremos (sólo se ha tocado), fila y col definen la posición del bloque dentro de la matriz de bloques con el que ha interactuado el usuario. Utilizamos el booleano cambiado para controlar que no repitamos el cambio de un bloque, ya que debido a que se generan muchos eventos de arrastre nos cambiaría el bloque de posición varias veces. El método devuelve este booleano una vez lo hemos procesado. Lo que hace es mirar si la dirección es un evento de arrastre, en cuyo caso llamamos a la función arrastraBloque() para mover el bloque a la dirección deseada e indica que se ha cambiado el bloque, si dir indica que se ha tocado la pantalla comprueba si el bloque 105 tocado es un comodín y si es así calcula aleatoriamente el color de bloques a eliminar, elimina el bloque del comodín y llama a comodin() para eliminar todos los bloques del color generado, e indica que ha procesado el evento del bloque, finalmente el devuelve el booleano cambiado. • private void comodin(int color) En este método recibimos el color de los bloques a eliminar, y recorremos la matriz de bloques eliminando los bloques que lo guarden (establecemos el color como -1), y llamando a puntua() para que actualice el estado de las variables de juego, finalmente comprobamos si no hay un cambio de nivel, en cuyo caso avisamos al renderizador que debe iniciar la animación de gravedad. • private boolean compruebaBloque(int x, int y, int color) Esta función la utilizamos para determinar si el bloque indicado en la posición que toma como parámetro contiene el color recibido. Primero comprobamos si la posición recibida está dentro del tablero y si es así comprobamos si el color es equivalente al recibido, en caso afirmativo devolveremos verdadero sino devolveremos falso. • private boolean formaLinea( int x, int y) Con este método comprobamos si el bloque con la posición que recibe por parámetro, forma línea dentro del tablero. Lo que hace es desde la posición especificada comprobar si el color es el mismo que el de los dos bloques que le rodean (en vertical y en horizontal), lo podemos ver en la figura 64, con la función compruebaBloque(). • private boolean eliminaBloques() Lo utilizaremos para eliminar los bloques que formen una línea de tres o más dentro del tablero. Lo que hace es recorrer la matriz de bloques comprobando cada uno si forman línea, en cuyo caso copia la posición del bloque dentro de la matriz en los arrays que usa para eliminar x_removes e y_removes e incrementa un contador de bloques a eliminar. Una vez terminado comprueba si hay bloques a eliminar consultando el contador y recorre los arrays de posiciones a borrar, cambiando el color de las posiciones almacenadas por un bloque vacío, es decir le damos a color el valor -1, y llama a la función puntua() si el estado de juego está en marcha. 106 Figura 64. Posibilidades de formar línea con los dos bloques adyacentes de una posición. • private void puntua() Esta función la llamamos cada vez que eliminamos un bloque, en ella simplemente incrementamos la puntuación, restamos los bloques restantes, activamos el evento para que suene el fx de bloque eliminado, y comprobamos si el jugador obtiene una lupa extra (recordemos cada 500 puntos), mirando si el resto de la división entre la puntuación y 500 es cero, en cuyo caso incrementamos las lupas siempre y cuando no tenga ya las tres, finalmente activamos el evento de fx de lupa obtenida. • private void limpiaTablero() Este método lo llamaremos cada vez que debamos generar un tablero nuevo (al pasar de nivel, al quedarnos sin movimientos). En él limpiamos el tablero y lo dejamos listo para jugar, lo que hacemos es llamar a eliminabloques() que eliminará los bloques que formen línea, y después rellenamos los bloques vacíos. • private void arrastraBloque(int dir, int fila, int col) Con esta función indicaremos al renderizador del tablero debe hacer la animación del intercambio de bloques. Recibe como parámetro la fila y columna el bloque que arrastra el jugador y la dirección en que lo hace. Primero calculamos el bloque con el que se va a intercambiar con la dirección del arrastre. Después comprobamos que ninguno de los dos bloques se esté moviendo ya, en cuyo caso activa el estado de moviendo en ambos, con esto indicamos al renderizador que debe hacer la animación de intercambio y evitamos que 107 se puedan intercambiar los bloques mientras se produce la animación. Después damos la dirección recibida al bloque original, y desactiva la dirección del bloque con el que intercambia, ya que se encargará el renderizador de mover ambos bloques a la vez. Finalmente ponemos el booleano cambiado del bloque original a falso para indicar que aún no hemos intercambiado los bloques. • public void compruebaCambioSiLinea(int x, int y , int x2, int y2) Esta función la llama el renderizador del tablero cuando ha terminado de animar los bloques que estaba moviendo, con ella intercambiaremos los dos bloques en el tablero y comprobaremos si forman línea, en el caso que no la formen volveremos a iniciar la animación a la inversa, para ello activamos el estado de moviendo activando la dirección anterior al bloque intercambiado, después indicaremos con el booleano cambiado que hemos cambiado los bloques pero no forman línea, para que sea el renderizador quien los vuelva a su posición inicial. En el caso que los bloques cambiados forman línea comprobamos si hay un cambio de nivel y si no es así avisamos al renderizador que debe aplicar la animación de gravedad con el booleano correspondiente. • private boolean cambioNivel() Sirve para avanzar de nivel si se han eliminado los bloques que se requerían en el nivel en que se encuentra el jugador. Primero comprueba si quedan bloques restantes en cuyo caso sale devolviendo un falso. En caso de que no queden bloques restantes, pone el estado de juego como siguiente nivel, incrementa el nivel actual, actualiza el número de bloques restantes y el tiempo, después genera un tablero nuevo listo para jugar, comprueba que haya movimientos disponibles y desactiva el movimiento de los bloques que pueda estar animando el renderizador y devolviéndolos a la posición original, finalmente activa el evento para el sonido de cambio de nivel y termina devolviendo un verdadero. • void intercambiaBloques(int x, int y, int x2, int y2) Este método cambia dos bloques de posición en la matriz, según los parámetros recibidos. Lo único que hace es comprobar que las posiciones se encuentren dentro de la matriz y entonces intercambia los bloques usando un temporal para la copia. • private void aplicaGravedad() 108 La función que aplica la gravedad a los bloques. Recorre la matriz de bloques comprobando si han sido eliminados, en cuyo caso desplaza los superiores hacia abajo. • public void animadaAplicaGravedad() Lo llamaremos una vez que el renderizador haya terminado de animar los bloques con la de gravedad. Aplicaremos la gravedad a los bloques y rellenaremos los bloques borrados, comprobaremos si no hay movimientos disponibles en cuyo caso cambiaremos el estado a Sin Movimientos. Continuamos comprobando si al aplicar gravedad y rellenar el tablero hay nuevas líneas a eliminar con eliminaBloques() eliminando los bloques que formen línea y comprobamos si hay un cambio de nivel, si no es así y se han eliminado nuevos bloques activamos el booleano de aplicagravedad para indicar al renderizador que genere la animación de gravedad de nuevo. • private void tableroAleatorio() Este método da colores aleatorios a todos los bloques de la matriz. Simplemente recorremos la matriz, creando un color aleatorio y asignándolo a la posición actual, si el color es un comodín volvemos a generar un color aleatorio, para reducir la probabilidad de que se produzcan. • private void rellenaTablero() Esta función lo que hace es rellenar con un color aleatorio las casillas del tablero que encuentre vacías, es decir cuando el color del bloque sea -1. Para ello recorremos la matriz y cuando encontramos una casilla vacía generamos un color aleatorio, si es un comodín volvemos a generar para reducir la probabilidad de que se produzca. • private boolean buscaMovimientos() Devuelve verdadero si existe al menos un movimiento disponible en el tablero que forme línea, también registra el valor de las posiciones de los bloques que al cambiar forman línea, por si el jugador pulsa sobre la lupa. Devuelve falso en el caso contrario. Para ello recorremos la matriz de bloques intercambiando el bloque actual con bloque superior mediante intercambiaBloques(), comprobamos si forman línea, en cuyo caso devolvemos los bloques a su posición inicial registramos las posiciones de los bloques y salimos del método devolviendo true. En caso contrario volvemos a intercambiar los bloques para 109 volverlos a su posición inicial, y hacemos exactamente lo mismo que hasta ahora con el bloque inferior, después con el izquierdo y finalmente con el de la derecha. Finalmente si ningún bloque forma línea en cruz devolvemos un false. • public void tiempoCorre() Lo llamaremos desde PantallaJuego y sirve para actualizar el tiempo. Primero comprobamos si el estado se encuentra es Sin Movimientos en cuyo caso, no trascurre el tiempo y ponemos el tiempoAnterior a 0 para indicar que hemos parado el contador de tiempo. Si hay movimientos cogemos el tiempo de inicio si es la primera vez que se inicia el juego, si ha sido pausado o si nos habíamos quedado sin movimientos, sino cogemos el tiempo actual para calcular el tiempo transcurrido. Se lo restamos al tiempo de juego y reiniciamos el tiempo anterior para la siguiente llamada. Finalmente comprobamos si el tiempo de juego ha finalizado en cuyo caso pondremos el estado a fin de juego. • public void noMoves() Este método lo llamamos desde PantallaJuego cuando termina la cuenta atrás de la pantalla Sin Movimientos. En el generamos un nuevo tablero con tableroAleatorio() y lo dejamos listo para jugar con limpiaTablero(), finalmente comprobamos que haya movimientos disponibles sino cambiamos el estado a Sin Movimientos. • public void lupa() Cuando el jugador pulsa sobre una lupa llamamos a este método. Aquí tan sólo decrementamos las lupas disponibles, activamos el listener de lupa pulsada y activamos el boolano dibujaLupa para que el renderizador sepa que debe dibujar la ayuda al usuario. 5.2.3.5.3 TableroRenderer Esta clase es la que se encarga de dibujar los bloques que son los que componen el tablero de juego. Primero definimos las constantes, para los colores de los bloques, azul, rojo, amarillo, blanco, morado, verde, comodín y borrado. Después para las animaciones de los bloques: arriba, abajo, izquierda, derecha y gravedad. Finalmente las constantes que definen las posiciones en pantalla de los bloques: xInicio e yInicio que es la posición del primer bloque 110 y offsetX y offsetY que indican el desplazamiento entre bloques, cuando están sin movimiento en el tablero. Ver figura 65. Figura 65. Posiciones (fijas) de los bloques en pantalla. Después definimos las variables: tendremos una instancia a tablero y una al lote de modelos con el que dibujaremos los bloques. Guardaremos dos matrices posicionesX y posicionesY con las que guardaremos las posiciones fijas de los bloques en la pantalla cuando no se están moviendo, las usaremos para comparar cuando animemos los bloques. Emplearemos posX y posY para calcular estas posiciones. Usaremos una matriz gravedad para crear el efecto de animación de caída de los bloques, en ella guardaremos la coordenada Y hasta dónde caerán. Usaremos fila y columna para calcular el intercambio de bloques. Finalmente utilizamos las variables i, j, a, b, y k para recorrer la matriz de bloques evitando la creación y destrucción de objetos impidiendo así que se ejecute el Recolector de basura. El constructor guarda las instancias al tablero y al lote de modelos que se recibe por parámetro, e inicia los miembros correspondientemente. Los métodos de la clase son: • public void render() Con este método dibujamos los bloques en pantalla. Primero comprobamos si desde el tablero se ha activado el booleano de aplicarGravedad, en caso afirmativo significa significa que se han eliminado bloques y debemos crear la animación de caída por gravedad, así que llamamos al método animandoGravedad() que mira que bloques deben 111 animarse y calcula la coordenada Y hasta la que deberán “caer” los bloques. Después recorremos la matriz de bloques, comprobando si tablero ha activado el movimiento de alguno con el booleano moviendo, en cuyo caso llamamos al método animandoBloques() para que se inicie la animación de intercambio de bloques, después dibujamos el bloque con la función dibujaBloque() y en último lugar comprobamos si se activó el booleano dibujaLupa que significa que el usuario tocó la ayuda, en este caso la dibujaremos con dibujaAyuda(). • public void animandoBloques(int i, int j) Es el método que usamos para crear el efecto de animación de los bloques, tanto para el intercambio como la gravedad. En él actualizamos la posición en pantalla del bloque del cual recibe la posición por parámetro. Primero mira en qué dirección vamos a animar. Si la dirección de la animación es cualquier intercambio de bloques (derecha, izquierda, arriba o abajo), calculamos la fila y columna del bloque con el que vamos hacer el intercambio. Comprobamos si la posición del bloque que se está moviendo coincide con la posición que debe alcanzar, comparando en la matriz de posiciones fijas correspondiente, si no ha llegado aún a su posición actualizamos las coordenadas correspondientes a los dos bloques que se están intercambiando. Después miramos si ha finalizado de animar comprobando la posición otra vez en cuyo caso desactivamos el booleano moviendo de los bloques, llamamos a restablecePos() para fijar los bloques en las posiciones fijas y entonces comprobamos si no los habíamos cambiado ya (cuando no forman línea) en cuyo caso sincronizamos con tablero para que los intercambie y compruebe si forman línea, si éste detecta que no forman línea volverá a indicar que deben animarse en dirección contraria e indicará con el booleano cambiado que los hemos cambiado pero no forman línea. Si por el contrario ya los habíamos intercambiado, le decimos a tablero que los intercambie para que vuelvan a su posición inicial y marcamos de nuevo el booleano cambiado a false. Si la dirección del bloque es Gravedad hacemos lo mismo, comprobamos si la posición del bloque ha alcanzado la que debe, esta vez comparando con la matriz de gravedad, si no es así decrementamos la posición. Después miramos si hemos terminado de animar, comprobando si ha llegado a la posición que debe, en cuyo caso indicamos que hemos terminado de animar el bloque, poniendo el booleano moviendo a false y la dirección a 0. Después comprobamos si hemos terminado de aplicar la gravedad a todos los bloques 112 llamando a animandoGravedad(), si es así sincronizamos con el tablero para que aplique la gravedad. • private void restablecePos() Este método lo llamamos después de hacer la animación de intercambio de bloques, en él actualizamos la posición de los bloques intercambiados a la posición fija de pantalla que tenemos almacenada en las matrices de posicionesX y posicionesY. • public boolean animandoGravedad() Esta función recorre la matriz de bloques comprobando aún se está aplicando la gravedad en algún bloque en cuyo caso nos devuelve un true y si no es así nos devuelve un false. • public void aplicandoGravedad() Este método calcula las nuevas coordenadas en pantalla que deberán alcanzar los bloques a los que se le va aplicar la animación de gravedad y las guarda en la matriz gravedad. Para ello recorre la matriz de bloques a lo ancho, comprobando las filas. Esta vez empezando por la fila de abajo y mira si hay un bloque eliminados, guarda la fila en que se encuentra en la variable k, comprueba si es la primera fila, si es así guardamos la misma coordenada Y en la matriz gravedad porque ya está en la última fila y no se desplazará, pero activa el booleano moviendo para que sincronice con el tablero desde la función animandoBloques(). Luego bajamos la posición de todos los bloques que se encuentren por encima, decrementando k hasta la fila 0 indicamos la animación de gravedad a los bloques. Guardando la posición a la que caerán, a la fila siguiente más el contador, (que indica los bloques eliminados de la columna). Finalmente cuando termina de recorrer la matriz cambia el booleano aplicandoGravedad del tablero a false. • dibujaBloque(int x, int y) El método dibuja el bloque de la matriz indicado en la posición x,y usando el lote de modelos. Para ello comprueba el color del bloque para dibujar la región de la textura correspondiente en las coordenadas de la pantalla del bloque. • public void dibujaAyuda() Con ésta función dibujamos en el tablero una flecha con un intercambio posible de bloques, cuando el usuario haya pulsado una lupa. Comparamos la posición de los bloques 113 a intercambiar para saber dónde dibujar la flecha, y la dibujamos en el medio de los dos bloques, (posición más la mitad del desplazamiento). En el caso que sea vertical dibujamos la flecha rotada noventa grados. 114 6 Evaluación Para llevar a cabo la evaluación del proyecto las pruebas se han realizado en los siguientes dispositivos: • Huawei U8650 con Sistema Operativo Android 2.3 y una pantalla de 480x320 pixeles. • Samsung Galaxy Mini, con Sistema Operativo Android 2.2 y una pantalla de 240X320 pixeles. Figura 66. Samsung Galaxy Mini y Huawei U8650. La evaluación del correcto funcionamiento de las distintas partes del framework se ha ido realizando a medida que se iban implementando. Por otra parte una vez implementadas las pantallas y las transiciones se ha comprobado que funcionen correctamente. Comprobamos también que el juego guarda la configuración cuando recibe una interrupción (pulsando la tecla home o al recibir una llamada en el teléfono…) Así pues al retomar el juego, regresa en el estado en que se encontraba (sonido, puntuaciones, las texturas se recargan) y vuelve en la pantalla dónde estábamos, si estábamos en la pantalla de juego, al retomarlo se encontrará en el menú pausa. La mecánica de juego responde como se esperaba, los bloques se intercambian, forman líneas, desaparecen y se aplica la gravedad generando nuevos bloques aleatorios. El comodín elimina todos los bloques del tablero de un color. El tiempo y los bloques restantes se restablecen al pasar de nivel y termina el juego al acabarse el tiempo. Cuando 115 el tablero se queda sin movimientos aparece la pantalla Sin Movimientos con la cuenta atrás, durante este tiempo el tiempo de juego restante no se altera. Gráficamente se aprecia la diferencia entre las resoluciones de los dos móviles. Esto es debido a que OpenGl ES escala los gráficos en el Samsung Galaxy mini, perdiendo calidad sobre todo en lo que refiere a las fuentes Bitmap. En lo que refiere al rendimiento, hemos comprobado los fps (fotogramas por segundo) gracias a la clase implementada, tanto en el Huawei como en el Samsung Galaxy mini alcanzan una tasa estable de unos 60fps. Observamos que alguna vez se activa el Colector de Basura, pero comprobamos que apenas se nota cuando jugamos. Figura 67. Figura 66. Midiendo el rendimiendo desde el log del sistema. Huawei. En cuanto a velocidad en el dispositivo Huawei apreciamos las animaciones un poco más lentas que en Samsung Galaxy Mini, esto es debido a que a menor resolución OpenGl trabaja más rápido, y no tuvimos en cuenta el tiempo de las animaciones. 116 7 Coste El propósito de este proyecto era aprender sobre Android y videojuegos con el subobjetivo de no invertir capital en él. Gracias a que las herramientas de desarrollo para Android son gratuitas, los recursos gráficos los hemos generado para el proyecto y los recursos sonoros están cogidos prestados, se ha logrado este objetivo, el capital invertido es cero. Sin embargo con ciertos cambios y una pequeña inversión, podríamos intentar hacer rentable este proyecto a través de Google Play. Google Play, anteriormente conocida como Android Market es una tienda de software en línea para dispositivos Android. A nivel de desarrollador representa una manera de publicar la aplicación e intentar rentabilizarla. Para ello se debe pagar una única tasa de 25 dólares estadounidenses (unos 18€) para obtener una cuenta de desarrollador. A partir de aquí existen dos formas de sacar rentabilidad a las aplicaciones: • Subirlas como aplicación de pago con un precio mínimo de 0,50 € el desarrollador se lleva un 70% de las ganancias. • Incluyendo banners de publicidad en la aplicaciones con librerías como las proporcionadas por AdMob[16] de la propia Google, algo parecido al servicio de publicidad por internet AdSense. Figura 67. Icono Market y ejemplo AdMob, publicidad en la aplicación. 117 Los cambios que deberíamos llevar a cabo en este proyecto son los siguientes: • La música tiene licencia libre, pero el autor exige que se le acredite en el juego. • Los efectos de sonido habría que cambiarlos, pues han sido cogidos prestados. • También habría que investigar si la fuente usada para los textos del juego, es de licencia libre, sino habría que pagarla o cambiarla. 118 8 Trabajos Futuros Teniendo en cuenta que hemos creado un framework para Android totalmente reutilizable, podríamos crear un nuevo videojuego/aplicación con sólo especificar, diseñar e implementar sus partes. Por otra parte Blocks podría mejorar con un poco más de tiempo invertido en él. Los diseñadores exponen que para que un videojuego sea bueno debe tener una buena MDA (Mecánica – Dinámica – Estética) [16]. • La mecánica hace referencia a las reglas del juego. • La dinámica a cómo se comporta el juego mientras se ejecuta. • La estética a la experiencia emocional del jugador con el juego. La mecánica está comprobada que funciona ya que el juego es un clon de Bejeweled, quizá podríamos añadir elementos de cara al futuro por ejemplo: tableros distintos, bloques especiales con efectos, incluso cambios de gravedad. En lo que refiere a la dinámica de juego podría mejorarse para tener un acabado más profesional (crear animaciones, comprobar el rendimiento en distintos dispositivos, que los bloques se generen más rápido y no aparezcan de la nada, cambiar el contador de tiempo por una barra que se vaya gastando, añadir nuevos sonidos). También podrían generarse gráficos en distintas resoluciones para que el juego se adaptase a distintas pantallas sin perder calidad. Seguramente lo que refiere a la estética mejoraría con lograr implementar una buena dinámica y quizá añadiendo algún modo de juego más como un modo historia con niveles desbloqueables. El juego está implementado la versión 2.2 de Android aunque seguramente funciona en versiones anteriores, debería verificarse. OpenGL ES 1.0 prácticamente es compatible con todas las versiones de Android. 119 120 9 Conclusiones Personalmente el proyecto me entusiasmó desde el principio, unir las nuevas tecnologías con el desarrollo de un videojuego me pareció un buen reto para el final de carrera. Durante el desarrollo han ido surgiendo muchos problemas, uno de los más remarcables es una mala planificación temporal, seguramente debida a la falta de conocimientos previos e inexperiencia que han provocado que el trabajo de documentación y sobre todo de pruebas haya sido extenso. La corrección de errores en la sincronización de las animaciones también se ha convertido en verdaderos quebraderos de cabeza, creo que fallé a la hora de diseñarlas puesto que las implementé posteriormente a la lógica del juego y no tuve en cuenta varios factores, opino que con más tiempo hubiese mejorado el rendimiento del código y de la memoria gastada. En general estoy muy satisfecho con el resultado obtenido ya que puedo decir que he logrado cumplir todos los objetivos que habíamos marcado al principio del proyecto: Se ha realizado un videojuego para móvil completamente funcional desde cero, de partidas cortas y rápidas, con mecánica sencilla pero adictiva, multidispositivo y que gestiona las interrupciones correctamente, además se ha creado un framework para posibles futuros proyectos. A nivel de conocimientos adquiridos también se han logrado los objetivos planteados: He aprendido cómo funciona el sistema Android y el desarrollo de aplicaciones para éste sistema operativo. También me ha ayudado a aprender sobre diseño y desarrollo de videojuegos, y a utilizar la tecnología de OpenGL ES. Tengo consciencia de las limitaciones del juego, sé en qué podría mejorar y cómo debería hacerlo sin embargo estoy muy contento con el resultado de mi primera aplicación Android y a la vez mi primer videojuego. El hecho de haber trabajado en un proyecto con tecnología muy actual y con un gran futuro por delante, me hace ser optimista sobre lo que personalmente puedo aportar a las empresas interesadas en este ámbito, por ello estoy convencido de que me facilitará la tarea de buscar trabajo. 121 122 10 Recursos Utilizados Hardware: • PC con CPU AMD 2Ghz, 1Gb RAM con Windows XP • Móvil Huawei U8650, CPU 600Mhz, 256MB RAM • Móvil Samsung Galaxy Mini Software: • Eclipse • J2SE 1.4.2 de Sun Microsystems • SDK Android • AVD • OpenGL ES 1.0 • Bitmap Font Generator • Gimp • Microsoft Office • Gantt project 123 124 11 Planning Temporal. El proyecto no se ha desarrollado siguiendo un calendario estricto, dado que era imposible cuantificar el tiempo que tomaría el adquirir las bases teóricas necesarias para poder afrontarlo con garantías. Hacemos un diagrama de Gantt para ver el tiempo empleado en cada una de las fases. Hemos utilizado el programa de distribución libre Gantt Project [17]. Figura 68. Diagrama de Gantt. 125 126 12 Referencias [1] [ESRB, estadísticas industria del Videojuego] Diciembre 2012 http://www.esrb.org/about/video-game-industry-statistics.jsp [2] [Wikipedia, sobre Bejeweled] Diciembre 2012 http://en.wikipedia.org/wiki/Bejeweled. [3] [Wikipedia, Patrón MVC Modelo Vista Controlador] Diciembre 2012 http://es.wikipedia.org/wiki/Modelo_Vista_Controlador [4] [The Whistle Song, Free Music] Diciembre 2012 http://www.pedroalonsopablos.com/en/free-music/ [5] [Gimp] Diciembre 2012 http://www.gimp.org/ [6] [Bitmap Font Generator] Diciembre 2012 http://www.codehead.co.uk/cbfg/ [7] [JDK Java] Enero 2012 http://www.oracle.com/technetwork/java/javase/downloads/index.html [8] [Eclipse] Enero 2012 http://www.eclipse.org/downloads/ [9] [SDK Android] Enero 2012 http://developer.android.com/sdk/index.html [10] [Plugin ADT para Eclipse] Enero 2012 http://developer.android.com/intl/es/tools/sdk/eclipse-adt.html [11] [Google, Compilador Jit para Android Dalvik] Diciembre 2012 http://www.google.com/intl/es-CL/events/io/2010/sessions/jit-compiler-androids-dalvik-vm.html [12] [conferencia de Google IO “Writing Real Time Games Android”.] Diciembre http://www.google.com/events/io/2009/sessions/WritingRealTimeGamesAndroid.html [13] [Khronos Group, OpenGL ES 1.X] Diciembre 2012 http://www.khronos.org/opengles/1_X [14] [Documentación oficial de Java] Febrero 2012 http://docs.oracle.com/javase/1.4.2/docs/api/java/nio/Buffer.html [15] [Pooling, Wikipedia] Febrero 2012 http://en.wikipedia.org/wiki/Pool_%28computer_science%29 [16] [Diseño de videojuegos, Mecánica, Dinámica, Estética] Febrero 2012 http://www.cs.northwestern.edu/%7Ehunicke/MDA.pdf 127 2012 [17] AdMob, publicidad en las aplicaciones.] Diciembre 2012 http://www.google.com/ads/admob/ [18] [Gantt Project] Febrero 2012 http://www.ganttproject.biz/ 128