Empezaremos añadiendo a las pantallas pertinentes un Widget compuesto que representa los marcadores. Para no repetir el mismo trabajo en cada pantalla, vamos a definir los marcadores una vez y reutilizarlos en todas las pantallas que queramos. Aprovecharemos para ello el CloneWidget, un tipo de widget que toma su forma de un modelo. El modelo será el grupo de marcadores. Vamos a ello:
Para separar los modelos de las auténticas pantallas crearemos una carpeta en la rama Pantallas, que podemos llamar Modelos. Dentro de ella crearemos un objeto de clase Widget que llamaremos, claro está, marcadores. Como hijos de éste crearemos tres objetos de la clase NumberWidget que llamaremos score, level y record. Este tipo de widget representa números, tiempos y fechas. Como todos los widget que presentan textos hay que asignarles un TextStyle.
Con la ventana central en Screen seleccionamos el widget marcadores. Deberíamos ver tres ceros espaciados verticalmente. Como lo que queremos es que aparenzcan organizados horizontalmente en la parte de arriba de la pantalla, editaremos algunos atributos del widget marcadores. Serán estos:
50
, más o menos. Depende de la tipografía que usemos. La anchura no importa ya que el objeto se expandirá horizontalmente.El resto de atributos se dejarán como vienen.
Cada uno de los marcadores puede ser de distinto color, o podemos usar una tipografía más pequeña que la que estamos usando hasta ahora. Como se explicó en el tutorial anterior, podemos definir una tipografía de distinta escala reutilizando la misma imagen y métrica de caracteres que ya tenemos.
El modelo así creado lo vamos a incorporar a tres pantallas: menú, juego e intermedio.
Añadiremos a la pantalla menú un objeto de la clase CloneWidget que colocaremos el primero de la lista de hijos. Cambiamos su atributo model para que contenga el widget "marcadores". Visualizaremos la pantalla para ver como queda.
El objeto CloneWidget es un poco especial y no cambia en tiempo real cuando cambia el modelo. Por razones de eficiencia sólo se actualiza al comienzo de la representación de la pantalla donde está y mantiene una copia para posteriores usos. Por ello es necesario utilizar el botón de prueba () de la pantalla para ver como queda.
Para que la pantalla quede bien organizada, insertaremos un Widget vacío entre los marcadores y el texto y añadiremos otro al final. Quedará semejante a lo que se ve en la imagen al margen.
También insertaremos un CloneWidget similar al principio de las pantallas juego e intermedio. A esta última le aplicamos el mismo tratamiento de widgets intercalados que a menú.
Para que los marcadores se sitúen en la parte superior de la pantalla de juego, debemos hacer que el atributo gravity de la pantalla juego sea start
.
En el apartado anterior hemos visto una forma de representar los marcadores, pero no tenemos la fuente de los datos, o sea, unas variables que almacenen la puntuación, cuantas pantallas hemos avanzado y el record. Estas variables deberíamos poder manipularlas mediante nuestras máquinas de estado.
La rama de objetos llamada Persistence es un poco especial. Para empezar ya viene cargada con algunos objetos obligatorios. Estos objetos tienen la peculiaridad de que muchos de sus atributos (los que tienen su nombre en negrita) son persistentes . Esto quiere decir que se conservan entre sesiones del juego. Lo que valían cuando cerramos el programa del juego es lo que valdrán cuando volvamos a abrirlo.
Hay una clase de objetos prevista sólo para mantener las variables que necesitemos. Es Data. Aunque en este objeto no tenemos los atributos que necesitamos (score, level y record). Afortunadamente disponemos de los medios para agregarlos. El procedimiento consiste en crear nuestra propia clase, derivada de Data, a la que podremos agregar los atributos que deseemos. Vamos allá.
En la ventana adosada Class Browser podemos ver la lista de todas las clases que pueden ser derivadas o cuyos objetos pueden ser referenciados en el contexto. (Tendremos que abrir dicha ventana, en el área lateral, si no la tenemos ya abierta). Aquellas clases que tienen un icono , son las que podemos extender creando otras derivadas de ellas.
Crearemos una clase derivada de Data pulsando el icono situado a la derecha del nombre de la clase. .Observemos que el nombre de la clase derivada es igual que el de la clase base con un sufijo añadido. Nosotros podremos cambiar el nombre de la clase por el que prefiramos.
La clase recién creada habrá sido cargada en el editor de clases (ClassInspector). En esta ventana tendremos siempre la clase que hayamos seleccionado en el ClassBrowser.
Observemos, también, que hay un botón para añadir atributos nuevos. Bien, pulsémoslo. Nos aparece un cuadro de diálogo para definir el nuevo atributo.
El primer dato es el nombre del atributo. Es importante elegir un nombre apropiado, porque ello nos ayudará en nuestro trabajo. Escribimos score. El siguiente campo es el tipo de dato del atributo. Elegimos int (número entero). El siguiente campo depende del tipo de dato que hayamos elegido, indica el modo en que se va a editar el dato. Elegiremos Number. Marcamos la opción Persistent para que sea recordado entre sesiones.
Para terminar pulsamos el botón de Aceptar.
Crearemos dos atributos similares más con nombres level y record.
Ahora crearemos un objeto, de la clase que acabamos de definir, en la rama Persistence/data. Le llamaremos player. Y ya tendremos las variables.
El siguiente paso es hacer que sean representadas en los widgets correspondientes. Eso se consigue construyendo una expresión en el atributo value del objeto correspondiente en los marcadores. El procedimiento es el siguiente.
Cargando uno de los NumberWidget de los marcadores en el Object Inspector vemos que hay un botón verde entre el nombre y el valor de algunos atributos. Eso quiere decir que son expresiones que se evalúan en diferentes momentos de la ejecución del programa. Hagamos que value, en cada caso, haga referencia al atributo level, record o score de la clase Data_2.
Para ver el efecto de lo que hemos hecho, mostremos la pantalla menu en el editor de pantallas en modo prueba (pulsando ). Luego, en el objeto player cambiemos el valor del atributo score, level o record. Observaremos como cambia el valor representado en la pantalla. (Si no es así algo no hemos hecho correctamente).
La clave para llevar la cuenta va a ser ProcedureAction (categoría Structural Actions). Esta es una acción que nos permite ejecutar expresiones arbitrarias que no devuelven ningún valor. Empezamos por lo fácil. Sumaremos 1 a level y 150 a score cada vez que nos carguemos una oleada de marcianos. El mejor sitio es en el estado intermedio de la máquina de estados del juego (RUN). Añadiremos un objeto de clase ProcedureAction (llamémosle actualiza vars). Trazaremos una transición vacía desde intermedio hasta actualiza vars.
En el atributo do construiremos una expresión que añade 1 a score. Para ello usaremos la función add!, (categoría Arithmetic en el desplegable). El primer parámetro es el atributo que queremos incrementar, editado como en el paso anterior. El segundo es un número, 1
en este caso.
Aparentemente no se nos permite añadir una nueva operación. Para conseguirlo debemos transformar la expresión en una secuencia de expresiones. Con la parte derecha del primer botón verde del conjunto nos muestra un menú. Debemos elegir Sequence. Ahora, con el pequeño botón podremos añadir una nueva expresión vacía. La hacemos similar a la anteror pero para record y con el incremento de 150
. Observemos la figura como referencia del resultado.
NOTA: Si pulsamos sobre el nombre de una función o atributo (la etiqueta de fondo verde claro) ocultamos o mostramos sus argumentos. Cuando están ocultos al pasar el cursor por encima de la etiqueta se muestran en formato de notación textual.
Ahora podemos probar el juego. Deberíamos poder observar que el marcador score y el level crecen cada vez que destruimos una oleada de marcianos. Aunque nos falta un pequeño detalle, las dos puntuaciones deberían empezar en 0 al comienzo del juego.
Nos falta inicializar los atributos. Esto podríamos hacerlo en el estado menu pero quedaría mal no poder leer la última puntuación conseguida, así que, es mejor, hacerlo en un estado intermedio State entre menu y juego.
La secuencia de expresiones que inicializan las variables sería como la de la imagen al margen. Cuando desplegamos el selector de métodos para la clase que contiene las variables de puntuación(Data_2 en el ejemplo), se tiene en cuenta el contexto de la expresión. En este caso nos propondrá sendas formas de modificación de las variables mediante la función set!.
Luego veremos cómo puntuar nuestra buena puntería, ahora vamos a contabilizar el record de puntos. Necesitaremos una nueva pantalla para mostrar que hemos batido el record. Como éste es un juego minimalista basta con una parecida a la que vemos en la imagen de la izquierda.
El valor numérico que aparece será un NumberWidget que represente el atributo record del objeto player.
Crearemos un estado nuevo para esta pantalla que llamaremos record.
Habrá una transición desde el estado game_over que se disparará con una condición de clase ExpressionCondition. Esta condición dispone de una expresión cuyo resultado lógico es el resultado de la condición. Ahora la usaremos para comparar score y record. Como función de comparación elegiremos >
(grupo Comparisons). Es decir, la condición se cumple cuando score es mayor que record.
La transición desde el estado record al estado menu se producirá después de un tiempo de uno o dos segundos.
El record ha sido detectado, ahora hay que copiar el valor de score en record. Volvemos a utilizar ProcedureAction enlazada al estado record.
El diagrama de la máquina de estados quedaría como la imagen siguiente.
Para puntuar la destrucción de un marciano, una bomba o cualquier otro personaje que se nos ocurra, vamos a definir un objeto reutilizable.
Crearemos un nuevo personaje que sirva para representar los puntos obtenidos en forma de globito, así como para añadirlos al score. La forma de usarlo es creando un actor de este personaje cada vez que un actor es destruido, además de crear la explosión.
Llamémosle bonus y será de la clase NumberCharacter. Esta clase de personajes permiten visualizar un número y, si se desea, un icono superpuestos a una animación. El número que se visualiza es el valor del atributo value del Actor que, a su vez es inicializado con el valor del atributo value del Character que le sirvió para crearlo.
Lo parametrizamos de la siguiente forma:
(90,60)
.Siempre podremos ajustarlo después.center
en align.1000
(1 segundo) a life, que indica el tiempo de vida en milisegundos.
Comenzamos la expresión con el atributo value de la clase Character. El argumento sería el atributo character de la clase Actor. El argumento de éste sería la función _OWNER
. Al elegir esta función en el selector, automáticamente se antepone un operador de adaptación (casting), si es necesario, para asegurarse de que es evaluado según el tipo correcto. En notación compacta, la expresión sería la siguiente:
value (character (#Actor _OWNER))
La función _OWNER
no tiene argumentos y devuelve el objeto que está siendo controlado por la máquina de estado vigente. En este caso sería la máquina de estado del actor que crea el bonus.
Probemos la creación de un bonus. En la máquina de estados del platillo, a la CompositeAction muere, agregaremos una CreateActorAction que cree un bonus. Debemos hacer que esta acción se ejecute después de la creación de la explosión, para que el globo se vea superpuesto a ella y antes de la acción ActorDeadAction, puesto que esta es el final del actor.
Si ahora probamos a matar un marciano, veremos el globo durante un segundo. Debemos de poner un valor adecuado en el atributo value del Character platillo, por ejemplo 20
. Hagamos lo propio con la bomba y probemos. La bomba debería valer el doble o más que el platillo, por ejemplo 50
, porque es más difícil acertarle.
Bien. Obviamente aún nos queda sumar los puntos al score.
Crearemos una máquina de estados para "bonus" con los siguientes estados:
add! (score (@player), value (#Actor _OWNER))
Probemos, exportemos, y ¡a matar marcianos!.