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

Documentos relacionados