Haciendo un juego en Android: lecciones aprendidas (2)

Después del primer capítulo que publiqué hace unas semanas, ya tenemos lo básico para hacer un juego en Android "a pelo". ¡Pero no nos conformamos con eso! Hoy contaré otras cosillas más que fui añadiendo a mi librería personal para el juego según iba avanzando.

Componentes gráficos

Para los que hemos trabajado con interfaces de ventanas, el diseño basado en componentes gráficos resulta muy natural. ¿Por qué no aplicarlo también a un juego?.

La idea es muy sencilla: hacer que el código de cada componente no tenga que preocuparse de coordenadas globales, ni de lo que ocurre en el resto del juego, sino que vive "en su propio mundo" y preocupado de sus cosas. Orientación a objetos pura, vaya. Así, la pantalla se va formando a base de componentes, unos dentro de otros, que acaban conformando todo lo que hay en el juego. Cada uno tiene su propia dinámica, y luego pueden reaccionar ante lo que hagan los otros.

Todas estas librerías tradicionales de componentes de ventanas, como las MVC del Visual C++, o la del Borland C++, o incluso las del más moderno SWT suelen tener un problema gordo en su diseño: abusan mucho de las subclases. Se puso de moda diseñar esto así, y como documentación de la librería aparecía, tachán!, ¡la jerarquía de clases!. O sea, todo el diseño estaba hecho en forma de jerarquía de subclasificación.

El caso es que a mi las subclases en general me dan un poco de urticaria. Los interfaces son mucho más limpios. Las subclases me gustan para reutilizar implementaciones por defecto o sencillas de los interfaces, pero poco más. Así que en lugar de hacer una jerarquía de clases como las de toda la vida (curiosamente Swing, con todo lo que se le ha criticado, está mucho mejor diseñada que todas las demás), me dediqué a darle unas vueltas más (y liar un poquito el tema, ya puestos).

¿Qué necesita cada uno de estos componentes?. Según vimos en la entrada anterior, lo mínimo que necesita cualquier entidad independiente del juego es:
  • Que sea capaz de reaccionar a eventos
  • Que sea capaz de actualizarse sola por cada unidad de tiempo
  • Que sea capaz de dibujarse
Además de esto, nuestros componentes gráficos necesitarán:
  • Una posición relativa a su componente padre
  • Un tamaño
Y por supuesto, necesitaremos entidades contenedoras, a las que podamos meter otras entidades dentro de forma relativa.

Así que hice un interfaz GameEntity para lo básico que tienen que tener todas las entidades de cualquier juego (los tres primeros puntos), y reservé dos interfaces para los otros dos: un Positionable y un Measurable. ¿Por qué usar interfaces para estos dos en lugar de beans?. Porque me puede dar más juego. Por supuesto, creo implementaciones beans que implementen esos interfaces, y un DelegateComponent que cree un componente a partir de una composición de objetos que implementen cada interfaz (y que no tienen por qué ser objetos distintos).

Una vez que te acostumbras a esta forma de trabajar, resulta comodísima, y sobre todo, muy potente. Este es el diseño resultante:




Animaciones, efectos y el estado

Cada objeto normalmente tendrá su estado e irá cambiando según las cosas que le ocurran. Eso ya dependerá de cada entidad. Sin embargo, hay algo que también es muy interesante facilitar, que son las animaciones y efectos. Aunque pueda confundirse, lo cierto es que estos se gestionan mucho mejor si se separan un poco del propio estado del objeto.

Por ejemplo, un personaje puede estar caminando. Su estado es "caminando". Sin que el estado cambie demasiado, se estará moviendo hacia la derecha. Y habrá una animación de caminar que irá cambiando sola cada X frames. Podríamos gestionar todo esto de forma explícita dentro de cada objeto, pero si lo facilitamos para que se declare desde el principio como funciona, nos evitaremos el tener que gestionar cosas incómodas como cuál es el frame actual que se debe mostrar.

Para eso, creé una clase AnimatedComponent, que decora cualquier otro componente. A este componente se le registrarán una serie de estados de transiciones o animaciones posibles, representadas con el interfaz Transition. Al registrar cada transición se indica cómo funciona: qué propiedades modificará, cada cuánto tiempo y qué ocurrirá cuando acabe (¿será cíclica?, ¿se quedará en el estado en el que estaba?, ¿desaparecerá el objeto?). Estas transiciones son las que se ocupan de hacer efectos y transiciones sin que el objeto tenga por qué preocuparse mucho por ello.

Como ejemplos de transiciones están las animaciones de gráficos, movimiento, efectos de "fade", cortinillas... A pesar del tinglado anterior del diseño de componentes por interfaces separados, lo cierto es que en la práctica casi todos los objetos utilizarán un bean para la posición. También bastantes para el tamaño, aunque no tantos, ya que muchas veces puede depender del tamaño de su bitmap o de su texto, por ejemplo. Por supuesto, también las animaciones es lógico que vayan con una propiedad explícita... en definitiva, una de las transiciones más sencillas que podemos hacer es que automáticamente se vaya incrementando o decrementando el valor de una propiedad del componente. De aquí surgió la clase BeanPropertyTransition. Para controlar de forma independiente a qué ritmo se va modificando el valor de la propiedad, se hace que utilice un interfaz IntPropertyChange, con una implementación sencilla que aumente el valor de la propiedad de forma lineal, es decir, de forma constante en el tiempo.

Así quedó el diseño:


Agrupando objetos "globales"

En las primeras versiones del juego, los constructores de los distintos objetos cada vez eran más grandes. Había una serie de objetos, digamos, prácticamente globales, que se podían utilizar casi en cualquier sitio.

Dos de ellos son clases que creé para encapsular, facilitar y/o mejorar el rendimiento de la reproducción de sonido: SoundManager para efectos de sonido, y Jukebox para música. A ellos se les suma el necesario Context de Android, con el que se cargan todos los recursos, y que face falta básicamente en... todos los sitios donde haga falta un recurso. Y otros que iban apareciendo y desapareciendo según se iba refinando el diseño, como por ejemplo una clase que facilita la carga de Bitmaps.

La solución en este caso es muy clara, encapsular todos estos objetos, que además son genéricos y valen para cualquier juego, en una sola clase GameContext, que permita acceder a ellos. Así, sólo será este objeto el que habrá que pasar a todos los demás (junto a otros objetos del propio juego que potencialmente también harán falta). Además, si en algún momento aparece otro objeto del sistema que nos venga bien de forma global, se puede añadir también sin tener que tocar todas las demás clases para que se lo pasen de unas a otras.

Esto parece una tontería, pero lo cierto es que si no se tiene un poco de cuidado, en un juego es muy fácil acabar con miles de objetitos que se van pasando a troche y moche de unos a otros, y eso se convierte en un caos. También hay que tener mucho cuidado de no mezclar churras con merinas, y no encapsular dentro del mismo objeto todo lo global que pueda necesitar el juego. Una cosa son estos objetos que pueden hacer falta en cualquier juego y son, digamos, del sistema y sus recursos, y otra muy distinta meter también por ejemplo los objetos que controlen la dinámica del juego concreto.

En nuestro caso, este es el diseño que quedó:



Acabando, que es gerundio

¡Buah, menudo coñazo de serie que me he marcao! Todavía el primer capítulo tenía su aquel, pero en este me he metido más bien en detalles de diseño e imagino que ha quedado bastante espeso. Y no he puesto chistes ni chorradinas ni ná. Así que nadie estará leyendo esto ya, ¡snif!.

El mensaje fundamental de todo esto es el primero que di: si estáis dudando en poneros o no a hacer un juego con Android, no lo dudeis: hacedlo. Es muy agradecido, con poco que hagáis van saliendo cosas. La librería de Android es genial, da muchas facilidades. Es más, yo hay cosas en las que me he complicado la vida porque he querido, porque lo cierto es que creo que algunas de las cosas que he hecho yo están ya en la propia librería estándar de Android. Y eso por no hablar del resto de librerías que existen para hacer juegos. Simplemente, me apeteció hacerlo así. Y ojo, mientras refinaba un poco la librería me dio tiempo a hacer otro juego, un Reversi (este ya no lo voy a publicar, porque Reversis hay montones y seguro que juegan mejor que el mío, que es un pobre patán...).

El secreto de todo esto de hacer juegos para móviles, lo que lo hace genial, es que es pequeño. No necesitas dedicar meses y meses de tu vida para hacer un juego. En una tarde ya ves avances. Cada tarde que te pongas haces algo, consigues algo nuevo. Es muy gratificante. Estoy convencido de que los juegos pequeños tienen su espacio, que no todo tienen que ser grandes producciones. Dan más pie a la imaginación, a la innovación, a atreverse a hacer cosas que pensabas que no iban a salir... en definitiva, ¿a qué estás esperando?, ¡¡¡ponte ya a hacer tu juego, leche!!!.



Bola extra

Has intentado entender el artículo. Realmente lo has intentado. Te lo has leído de principio a fin, has imprimido los diagramas en grande para seguir las flechitas, has buscado por ahí a ver qué leche es esa mierda de los componentes gráficos... y eso sólo te ha desorientado más aún. Finalmente, has llegado a una conclusión ineludible: ¡no hay dios que entienda este artículo!

Pues bien, aprovechando que hoy es el "día Somos", que propone darle la vuelta al mundo, he apoyado la causa y he hecho una nueva versión del mismo en la que le he dado la vuelta por completo. Jamás la Informática había sido tan sencilla, jamás algo complejo se había visto de una forma tan clara. Jamás había sido tan fácil conseguir que cualquier no iniciado puede comprenderlo con facilidad.

No te pierdas la versión revisada del artículo. De repente verás la luz y lo comprenderás en toda su extensión.