Documente Academic
Documente Profesional
Documente Cultură
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
3.4.2 Ayuda............................................................................................................. 28
9
3.4.3 Ranking .......................................................................................................... 29
4 Diseño .......................................................................................................................... 35
1
0
4.9 Framework ........................................................................................................... 46
5 Desarrollo .................................................................................................................... 49
5.1.7.2 Proyecciones........................................................................................... 61
1
1
5.2.1 Estructura de Blocks ...................................................................................... 66
5.2.2.4.2 Vector................................................................................................. 75
1
2
5.2.3 Los elementos del Juego ................................................................................ 91
1
3
14
1 Introducción
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
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.
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.
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]
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.
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.
Un sistema Operativo para móvil está pensado para controlar un dispositivo portátil como
un SmartPhone o tableta.
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.
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.
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
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.
1.4.4 BlackBerry OS
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
20
2 Objetivos del Proyecto
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:
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.
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.
21
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.
• 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.
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.
22
3 Especificaciones del Videojuego.
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.
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
23
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.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,
24
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.
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:
25
Para salir del juego bastará con pulsar el botón atrás del dispositivo móvil en cualquier
momento.
3.4.2 Ayuda.
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 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…
26
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.
3.4.4 Preparado.
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.
Sobre el tablero veremos los bloques. En la figura 11 podremos observar una imagen de la
escena juego.
30
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.
31
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.
4.2 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.
Estos eventos sólo los manejaremos para que no se sobrecargue la memoria haciendo saltar
al colector de basura, (lo veremos más adelante).
32
4.3 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.4 Audio
El módulo:
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.
33
4.7 Gráficos
Este módulo es el que se encarga de dibujar en la pantalla las imágenes de nuestro juego.
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).
34
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.
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.
40
Figura 21. Botones del menú principal, 128x64 píxeles cada uno.
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 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.
Para crear estos recursos gráficos se ha utilizado el programa de edición Gimp [5] que tiene
licencia de distribución gratuita.
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.
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.
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.
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.
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 ();
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:
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.
También instalamos Eclipse que será nuestro entorno de trabajo, desde él compilaremos y
ejecutaremos el código de manera cómoda. [8]
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.
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]
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.
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
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.
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.
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.
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.
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.
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.
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.
En la figura 33 vemos los elementos creados inicialmente para un nuevo 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:
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.
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
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]
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]
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
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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/.
Hemos dividido el código de nuestro videojuego en cuatro paquetes. Todos empiezan por
el nombre com.games.edo.
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.
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.
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:
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.
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.).
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.
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.
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.
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.
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.
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.
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.
70
Nos indica si la tecla que le pasamos se ha presionado o no, consultando su estado en el
array de teclas pulsadas.
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.
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ó.
Nos servirá para obtener los eventos TouchEvent y así gestionar estas entradas. Deberemos
llamarlo frecuentemente para evitar que la lista se llene.
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.
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…
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.
73
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.
Aquí usaremos y clase SoundPool que nos facilita la reproducción de efectos sonoros.
Manejaremos el archivo con el identificador que asigna al cargarlo.
74
Sus métodos son:
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.
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.
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)
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.
Estos métodos nos devuelve la suma de nuestro vector con otro, o con argumentos.
Recordemos la fórmula (1)
Estos métodos nos devuelve la resta de nuestro vector con otro, o con argumentos.
Podemos ver la fórmula a continuación (2)
76
u*k = (x*k, y *k) (3)
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º.
Nos devuelve la distancia al cuadrado entre nuestro vector y el vector o coordenadas que le
pasamos.
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.
Vamos a ver la clase camara2D, que usaremos para definir la ventana de visualización y la
matriz de proyección correctamente.
78
vemos en la figura 52 está comprendida entre (0,0,1) y (frustumWidth, frustumHeight, -1)
y establecemos el zoom como 1.
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.
Figura 53. El búfer de vértices, las direcciones en que empieza a leer OpenGL, y los
saltos.
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.
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.
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.
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.
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.
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.
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.
É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.
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.
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.
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.
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.
Asocia la textura que le pasamos e reinicia los valores para empezar a trabajar con el lote.
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.
La fórmula que usamos para rotar los puntos un determinado ángulo es:
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.
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.
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.
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.
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.
88
startTime controlaremos el intervalo de tiempo y con WakeLock evitaremos que la pantalla
entre en modo ahorro de energía.
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.
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)
Nuestra clase GLJuego también implementa la interfaz juego, los métodos de ésta son:
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.
90
Este método nos sirve para consultar la pantalla activa.
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).
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.
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.
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.
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.
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.
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.)
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:
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.
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.
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.
Cuando se pause, lo único que hacemos es liberar la textura para ahorrar memoria.
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.
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.
Este método funciona igual, nos limitamos a comprobar si pulsamos el botón para volver a
la pantalla del menú principal.
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.
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.
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:
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.
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.
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.
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.
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:
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.
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.
101
Figura 61. Pantalla Juego (Fin de juego).
Se encarga de dibujar las lupas de la interfaz, para ello comprobamos cuantas lupas hay
disponibles en el tablero y dibujamos en consecuencia.
É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.
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.
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:
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.
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.
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.
public boolean update(int dir, int fila, int col, boolean cambiado)
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.
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().
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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().
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.
112
llamando a animandoGravedad(), si es así sincronizamos con el tablero para que aplique la
gravedad.
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.
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.
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.
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.
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.
116
7 Coste
El propósito de este proyecto era aprender sobre Android y videojuegos con el sub-
objetivo 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:
117
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 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.
118
120
9 Conclusiones
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.
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:
Software:
Eclipse
SDK Android
AVD
OpenGL ES 1.0
Gimp
Microsoft Office
Gantt project
123
124
11 Planning Temporal.
[12] [conferencia de Google IO “Writing Real Time Games Android”.] Diciembre 2012
http://www.google.com/events/io/2009/sessions/WritingRealTimeGamesAndroid.html
127
[17] AdMob, publicidad en las aplicaciones.] Diciembre 2012
http://www.google.com/ads/admob/
128