Simon (juego)

El juego Simon es originariamente un juego electrónico creado en 1978 basado en el juego Simon says («Simón dice»), en el cual uno de los participantes dice «Simón dice» y una acción, y el resto de participantes deben realizar dicha acción.

El juego electrónico Simon tenía forma de disco y estaba dividido en cuatro cuadrantes, cada uno con un botón de un color (verde, rojo, azul y amarillo). Estos botones de colores se iluminan de forma aleatoria, a la vez que emiten un sonido propio de cada uno de ellos. Una vez finalizada la secuencia aleatoria, el usuario debe reproducir dicha secuencia en el orden correcto pulsando los botones para ello. El usuario deberá emplear su memoria visual y auditiva para llevar a cabo el juego correctamente, así como su capacidad de atención. Si responde correctamente a la secuencia se reproducirá una nueva pero de mayor longitud, y así sucesivamente. La dificultad del juego vendrá determinada por la longitud de la secuencia, así como por la velocidad a la que se presenta la misma.

El juego ha evolucionado de manera que existen versiones con efectos visuales y sonoros mejorados, o con colores nuevos añadidos a mayores de los cuatro originales.

El audio es una parte muy importante del juego. Puede aumentarse la dificultad eliminándolo o disminuir la dificultad empleando sonidos más familiares (por ejemplo animales).

Actualmente existen versiones del juego en formato digital (videojuegos) en diversas plataformas: web, pc, dispositivos móviles, etc.

Nuestro juego Simon dispondrá de las siguientes variables de entrada al jugador:

  • Audio: sí o no. Esta opción activará o desactivará el audio del juego, lo cual cambiará la dificultad del juego.
  • Velocidad: 1, 2, 3,… Esta opción permitirá al jugador variar la velocidad a la que se presenta la secuencia en el juego, lo cual aumentará su dificultad.
  • Número de botones: 4-x (siendo x un número aún por determinar). Esta opción hará variar el número de botones presentes en el juego; a mayor número mayor será la dificultad.
  • Movimiento de los botones: sí o no. Esta opción permitirá indicar si deseamos que los botones se mantengan siempre en la misma posición (versión clásica) o si deseamos que vayan rotando, variando así su posición original.

Respecto a las variables de salida que mostrarán la información del juego al jugador:

  • Aciertos: mostrará el número de aciertos totales acumulados por parte del jugador.
  • Errores: mostrará el número de errores totales acumulados por parte del jugador.
  • Tiempo medio de respuesta: mostrará el tiempo medio que tarda el jugador en responder a cada elemento de la secuencia.
  • Nivel actual: mostrará el nivel en el que se encuentra en ese momento el jugador.
  • Nivel final: mostrará el nivel alcanzado por el jugador al final de la partida.

Collections

En la programación orientada a objetos, a menudo trabajaremos con grupos de objetos. Los arrays carecen de la flexibilidad que necesitamos para el rápido desarrollo de aplicaciones. Su tamaño no pueden ser cambiado. Afortunadamente, en .NET Framework posee un conjunto de interfaces y clases para trabajar con grupos de objetos de manera más fácil. Estas están incluidas en System.Collections, que contiene tipos no genéricos que más o menos han sido reemplazados por los tipos genéricos incluidos en System.Collections.Generic.

Una colección es un objeto que agrupa otros objetos. Provee métodos para almacenar, recuperar y manipular sus elementos.

Las clases más importantes de System.Collections.Generic son List, HashSet, Queue y Dictionary.

List, HashSetQueue son similares, son usadas para almacenar objetos del mismo tipo. Dictionary es bueno para almacenar key/value pairs.

La clase List

Una List es similar a un array pero con más flexibilidad. En un array debemos especificar su tamaño, que no podrá ser cambiado, y en el caso de una List especificar su tamaño es opcional. Según se van añadiendo nuevos elementos a una List, su tamaño aumentará automáticamente si ya no hay espacio disponible.

List es una clase genérica, como tal necesitas decirle al compilador que tipo de objeto quieres almacenar. Así es como creas una lista de strings:

List<string> animals = new List<string>();

Si deseamos especificar un tamaño inicial de 10:

List<string> animals = new List<string>(10);

En ambos casos la capacidad aumentará automáticamente si se necesitan almacenar nuevos elementos y ya se ha superado la capacidad existente. No obstante, si sabemos cuantos elementos serán almacenados en la List sería una buena idea asignar una capacidad inicial correspondiente, así evitaremos perder tiempo en aumentar el tamaño de la List.

Para añadir un elemento a la List se llamará al método Add. Para saber cuantos elementos contiene la List se llamará a la propiedad Count.

Para recuperar un elemento de una List usaremos la propiedad Item. Para ello le pasaremos un índice a la variable List como si fuera un array, tal que así:

myList[0]

El primer elemento estará en la posición 0, el siguiente en la 1 y así sucesivamente.

Finalmente, para recorrer todos los elementos de una List usaremos un bucle foreach.

foreach (T element in myList)
{
// do something with element
}

Ahora se mostrará un lista con algunos de los métodos más importantes en List.

public void Add(T item)

Añade un objeto a la List. El objeto podría ser null y será puesto al final de la lista.

public void Clear()

Elimina todos los objetos de la List y establece la propiedad Count a 0.

public bool Contains(T item)

Pregunta si un elemento está en la List y devuelve true si está. Si no está devuelve false.

public T Find(Predicate<T> match)

Inspecciona la List y devuelve el primer elemento que coincide con la condición especificada.

public void Insert(int index, T item)

Añade un elemento en la posición indicada por el índice.

public bool Remove(T item)

Elimina el elemento especificado de la List. Devolverá true si el elemento fue eliminado satisfactoriamente o false en caso contrario.

public void RemoveAt(int index)

Elimina el elemento especificado por el índice. Si este es 0 elimina el primer elemento de la lista.

public void Sort()

Ordena la List usando un comparador por defecto.

public T[] ToArray()

Devuelve los elementos como un array.

A continuación se muestra un ejemplo del uso de la clase List donde se almacenan strings y se recorre usando foreach. En la consola de Unity se muestra el resultado correspondiente.

ListExample

La clase HashSet

Un set es una estructura de datos que puede almacenar valores sin ningún orden en particular y no permite duplicados. Un hash set es un conjunto que está implementado usando una hash table. El término hash se refiere a la función usada para computar un índice de un elemento para que ese elemento pueda ser recuperado rápidamente.

La clase HashSet representa un hash set. HashSet posee los métodos AddClear, que funcionan del mismo modo que en List, al igual que la propiedad Count que devuelve el número de elementos en el HashSet. Sin embargo, a diferencia de ListHashSet no permite duplicados. Además, no podemos añadir o eliminar un elemento en un índice específico. No existe siquiera una propiedad Item para recuperar un elemento en una posición concreta.

Por otra parte, HashSet provee métodos que son útiles para trabajar con conjuntos, como IsSubsetOfIsSupersetOf.

Ahora se mostrará un lista con algunos de los métodos más importantes en HashSet.

public bool Add(T item)

Añade un objeto nuevo HashSet. Si el elemento añadido ya está en el HashSet, no será añadido de nuevo y el método devolverá false.

public void Clear()

Elimina todos los objetos de la HashSet y establece la propiedad Count a 0.

public bool Contains(T item)

Pregunta si un elemento está en la HashSet y devuelve true si está. Si no está devuelve false.

public bool IsSubsetOf(IEnumerable other)

Determina si el HashSet es un subconjunto de la colección especificada.

public bool IsSupersetOf(IEnumerable other)

Determina si el HashSet es un superconjunto de la colección especificada.

A continuación se muestra un ejemplo del uso de la clase HashSet donde se almacenan strings. En la consola de Unity se muestra el resultado correspondiente.

HashSetExample

La clase Queue

Una Queue es una colección como ListHashSet. Lo que hace destacar a Queue es el hecho de que podemos recuperar un elemento y eliminarlo al mismo tiempo usando el método Dequeue. Para recuperar un elemento sin eliminarlo se usará el método Peek.

Cuando añadimos un elemento a una Queue usando el método Enqueue, el elemento es añadido al final de la cola. Cuando usamos DequeuePeek, se obtiene un elemento del principio de la cola. Por tanto, Queue es un sistema first-in-first-out(FIFO), el primero que entra es el primero que sale de la cola.

Ahora se mostrará un lista con algunos de los métodos más importantes en Queue.

public void Enqueue(T item)

Añade un elemento nuevo al final de la Queue. El elemento añadido puede ser null.

public void Clear()

Elimina todos los elementos de la Queue y establece la propiedad Count a 0.

public bool Contains(T item)

Determina si un elemento está en la Queue y devuelve true de ser así. De otro modo devolverá false.

public T Dequeue()

Devuelve el elemento del principio de la Queue y lo elimina de la Queue.

public T Peek()

Devuelve el elemento del principio de la Queue sin eliminarlo de la Queue.

A continuación se muestra un ejemplo del uso de la clase Queue donde se almacenan strings. En la consola de Unity se muestra el resultado correspondiente.

QueueExample

La clase Dictionary

La clase Dictionary es un modelo para la creación de contenedores que toman key/value pairs. El Dictionary es adecuado para almacenar elementos que consisten en una clave y un valor, como un ISBN y un objeto Book o un país y una capital.

Para construir un Dictionary pasamos el tipo de la clave y el tipo del valor a su constructor. Por ejemplo, el siguiente fragmento de código crea un Dictionary que toma un string como clave y un objeto Book como valor.

Dictionary books = new Dictionary(string, Book);

Al igual que otras colecciones, tenemos un método Add para añadir un par clave/valor a un Dictionary y un método Clear para eliminar todos sus elementos.

Para recuperar un valor usamos la propiedad Item. Por ejemplo, si un Dictionary contiene un par país/capital, usamos esta sintaxis para recuperar un valor:

string selectedCountry = countryDictionary[countryName];

A continuación se muestra un ejemplo del uso de la clase Dictionary donde se almacenan pares país/capital. En la consola de Unity se muestra el resultado correspondiente.

DictionaryExample

Subir juego en WebGL a servidor Apache remoto

En este post se explicarán los pasos seguidos para transferir los archivos de nuestro juego Episodix exportado en WebGL a un servidor Apache remoto.

Nuestro equipo trabajará con Windows 7, mientras que el servidor remoto usará un sistema operativo basado en Linux. El servidor remoto ya tendrá instalado y funcionando un servidor Apache.

Para realizar la conexión remota emplearemos el protocolo SSH. Para usar dicho protocolo en Windows usaremos la aplicación WinSCP, que puede ser descargada de manera gratuita desde su página oficial aquí. La aplicación constará de dos ejecutables: uno para lanzar usando comandos por consola y otra que hará uso de una interfaz gráfica más sencilla e intuitiva de usar. En nuestro caso lanzaremos la versión con interfaz gráfica.

WinSCP ejecutables

Una vez iniciemos WinSCP nos abrirá una ventana de login para realizar la conexión con un equipo remoto usando el protocolo SSH. Podremos seleccionar el protocolo de archivos que se usará en la transferencia de datos. Los campos que debemos rellenar serán:

  • Host name : pondremos el nombre (o la direción IP) del servidor al que nos conectaremos.
  • Port numbre : el puerto mediante el que nos conectaremos al equipo remoto.
  • User name : el nombre de usuario mediante el cual realizaremos la conexión (debe existir dicha cuenta en el equipo remoto previamente).
  • Password : la contraseña para User name.

Una vez rellenados los campos, podemos guardar los datos de la sesión para no tener que introducirlos de nuevo cada vez que deseemos realizar la conexión. Para conectar haremos clic en el botón Login.

login WinSCP

Ahora miraremos en el archivo de configuración de Apache en el directorio /etc/apache2, con nombre apache2.conf, qué directorios se corresponden en el equipo remoto con el servidor.

archivo conf

La carpeta por defecto que se usará será /var/www/html. Dentro de esta carpeta crearemos una nueva para nuestro juego que se corresponderá con /var/www/html/episodix por lo que haremos la transferencia de los archivos de nuestro juego en WebGL a dicho directorio. Puesto que vamos a almacenar distintas versiones del juego en el futuro, crearemos una directorio para cada una de ellas. En este caso guardaremos la versión 0.1 de Episodix en el directorio /var/www/html/episodix/v0.1 .

directorios

Para transferir archivos de un equipo a otro bastará con arrastrarlos entre los directorios de ambos. Al hacer esto se nos abrirá una ventana donde deberemos confirmar el directorio de destino de los archivos.

Upload_window

Para acceder al juego online alojado en el servidor visitaremos la siguiente dirección: http://itec-sde.net:9007/episodix/. Ahí podremos acceder a todas las versiones de nuestro juego. En la actualidad sólo existirá la versión 0.1, a la que podremos acceder directamente mediante http://itec-sde.net:9007/episodix/v0.1/.

Publicar un videojuego de Unity en WebGL

WebGL es una especificación estándar que está siendo desarrollada actualmente para mostrar gráficos en 3D en navegadores web. El WebGL permite mostrar gráficos en 3D acelerados por hardware (GPU) en páginas web, sin la necesidad de plug-ins en cualquier plataforma que soporte OpenGL 2.0 u OpenGL ES 2.0. Nos permitirá correr nuestro juego hecho en Unity en un navegador sin necesidad de instalar ningún plug-in.

WebGL genera un código basado en JavaScript, por lo que convertirá el código de nuestros scripts C# a JavaScript.

Para compilar nuestro juego en WebGL debemos seleccionarlo, en Build Settings, como plataforma a la que será exportado.

Build Settings

Cuando compilamos un proyecto WebGL, Unity creará una carpeta con los siguientes archivos:

  • un archivo index.html que incrusta el contenido en una página web.
  • un archivo JavaScript que contiene el código para el reproductor.
  • un archivo .mem que contiene la imagen binaria para inicializar la memoria heap para el reproductor.
  • un archivo .data que contiene los datos de assets y escenas.
  • algunos archivos JavaScript de ayuda para inicializar y cargar el reproductor.

directorio_1

directorio_2

Se puede visualizar el reproductor WebGL directamente en el navegador Firefox simplemente abriendo el archivo index.html. Por razones de seguridad, la mayoría de navegadores tendrán restricciones en scripts abiertos desde los archivos locales: URLs, así que esta técnica no funcionará. Si usamos Build & Run (File > Build & Run) el archivo será almacenado temporalmente en un servidor web local y abierto desde un host local URL (que evitará las restricciones).

Actualmente, FireFox y Chrome son los únicos navegadores con soporte Unity para WebGL.

Build Player Options

WebGL permite seleccionar una nivel de optimización en la ventana Build Settings. En general, «Slow» produce compilaciones no optimizadas, pero con tiempos de compilación mucho menores que el resto de opciones, de modo que puede ser usado para probar los posibles problemas del código. «Fast» produce compilaciones optimizadas, y «Fastest» permite algunas opciones adicionales de optimización, pero hace que los tiempos de compilación sean muy grandes (debería ser usado únicamente en lanzamientos finales).

Cuando marcamos la casilla «Development Build», Unity generará una compilación de desarrollo. Además, el los JavaScripts generados podrán ser leídos y mantienen los nombres de sus funciones, pero la compilación será demasiado grande para ser distribuida.

La casilla «Autoconnect Profiler» debe ser usada cuando queramos perfilar nuestro contenido Unity WebGL. No es posible conectar al Profiler a una compilación funcional como en otras plataformas, ya que las conexiones de profiler son mantenidas usando WebSockets en WebGL, pero el navegador sólo permitirá conexiones salientes desde el contenido, por lo que la única manera de usar el Profiler en WebGL es marcar «Autoconect Profiler» para tener el contenido del Editor.

Build Settings_Build Player Options

Player Settings

WebGL tienes algunas opciones adicionales en el Player Settings ( Edit > Project Settings > Player ).

La opción Strip Engine Code en Other Settings permite activar el código desnudo (code stripping) para WebGL. Nótese que WebGL no diferencia entre niveles de desnudo, activado o no. Si se activa, Unity no incluirá código para ninguna clase que no se use (si no se usa ningún componente o método de físicas, el motor de físicas completa será excluído de la compilación).

PlayerSettings_OtherSettings

El campo WebGL memory size en Publishing Settings permite especificar cuanta memoria (en MB) el contenido debería asignar para heap. Si el contenido es demasiado bajo, se obtendrán errores de memoria cuando el contenido sea cargado y la escena podría no caber en la memoria disponible. Pero si el valor es demasiado alto, el contenido podría fallar al cargarse en algunos navegadores o máquinas, porque el navegador podría no tener suficiente memoria disponible para asignar el tamaño del heap solicitado. Este valor es escrito para una variable llamada «TOTAL_MEMORY» en el archivo html generado, así que si queremos experimentar con esta opción, puede editarse en el archivo html para evitar recompilar el proyecto entero.

PlayerSettings_PublishingSettings

El desplegable Enable Exceptions en Publishing Settings nos permite activar la excepción de soporte en WebGL. Si no necesitamos ninguna excepción de soporte, establece esta opción a «None», lo cual dará el mejor rendimiento y las menores compilaciones. Cualquier excepción arrojada causará que el contenido se detenga con un error en esa opción. Pero si necesitamos usar excepciones, se pueden establecer las siguientes:

  • Explicitly Thrown Excaptions Only (default), permitirá capturar excepciones que son explícitamente lanzadas desde una sentencia «throw», y harán bloques «finally» en nuestro código de trabajo. Esto hará que el código JavaScript generado de nuestros scripts mayor y más lento, pero no es normalmente un problema si los scripts no son el principal cuello de botella de nuestro proyecto.
  • Full, que, además de lo anterior, también generará excepciones para referencias Null y para accesos fuera de los límites de arrays. Son generados por comprobaciones incrustadas para cualquier acceso a referencias en el código generado, por lo que causarán adicionalmente incrementos del tamaño del código y ralentizaciones. Además, añadirá rastro de pilas gestionadas para excepciones. Es aconsejable sólo usar este modo cuando necesitamos depurar errores en nuestro código, ya que genera compilaciones muy grandes y muy lentas.

La casilla Data caching en Publishing Settings permite activar el almacenamiento en caché local automático para los datos del reproductor. Si está activado, nuestros assets se almacenarán en un caché local en los navegadores en bases de datos IndexedDB, por lo que no tendrán que ser re-descargados en sucesivas ejecuciones del contenido. Nótese que diferentes navegadores tienen diferentes reglas en permitir almacenamiento IndexedDB, y podrían preguntar por permisos para almacenar los datos si está activado, y las compilaciones exceder el tamaño límite definido por el navegador.

Distribución de tamaño

Cuando publicamos para WebGL, es importante mantener el tamaño de nuestra compilación lo menos posible para evitar largos tiempos de descarga a los usuarios. Para consejos generales para reducir el tamaño de los assets, mirad aquí. Con respecto a WebGL:

  • No compiléis «Development Build», están diseñadas para preservar los nombres de las funciones y ocupan demasiado.
  • Estableced el nivel de optimización en «Fastest».
  • Estableced «Enable Exceptions» en Player Settings a «None».
  • Activad «Stripping Level» en Player Settings.
  • Tened cuidado cuando uséis third-party dlls, ya que pueden arrastrar muchas dependencias e incrementar el tamaño del código significativamente.

Si hacemos una compilación de lanzamiento, Unity generará una carpeta «Compressed», que contendrá una versión gzip-comprimido de la compilación. Si el servidor web está configurado correctamente, usará estos archivos en vez de los más grandes de la carpeta «Release», puesto que el protocolo http nativamente soporta compresiones gzip (más rápido que lo que las descompresiones que Unity podría mantener por sí mismo en código JavaScript). Sin embargo, esto significa que necesitamos asegurar que el servidor http provee los datos comprimidos. Podemos comprobar eso cargando el contenido e inspeccionando la red de transferencias en las herramientas de desarrollo de nuestros navegadores. Deberíamos encontrar «Content-Encoding: gzip» en la respuesta para el js y datos descargados. Si estamos hospedando nuestros archivos en un servidor web Apache, Unity escribirá un archivo invisible .htaccess en el directorio de la compilación, que dirá a Apache que comprima las transferencias, por lo que debería funcionar sin problemas. Para otros servidores web, comprobad los manuales de cada uno.

Release1_directorio

Instalar servidor de Apache

Para hospedar nuestro juego emplearemos un servidor Apache. Lo primero será descargarlo de http://httpd.apache.org/download.cgi#apache22 , pudiendo elegir entre varias versiones. En nuestro caso seleccionamos la última versión estable para Windows, 2.4.17. Nos llevará a la siguiente página: http://httpd.apache.org/docs/current/platform/windows.html#down, que mostrará toda la información que pudiéramos necesitar detallada. Ahí se nos mostrarán varias opciones de descarga, en mi caso he empleado Apache Lounge.

A continuación descomprimimos el archivo y copiamos la carpeta Apache24 en el disco C. Ahora ejecutamos en una ventada DOS con permisos de administrador (símbolo del sistema, por ejemplo) el  archivo httpd.exe de la carpeta bin. Luego instalamos el servicio mediante httpd.exe -k install.

console

Abriremos con doble clic ApacheMonitor.exe y nos mostrará los servicios instalados de Apache (Apache2.4 en nuestro caso). Seleccionándolo y dándole a Start se iniciará el servidor. Para comprobar que funciona podemos abrir un navegador y poner la dirección http://localhost/. Si todo funciona correctamente nos mostrará el mensaje «It works».

apachemonitor

Crearemos una carpeta a la que llamaremos «servidor_web» en C. Iremos al archivo de configuración httpd.conf de Apache en la carpeta conf. En la línea 149 Document Root especificará la carpeta en la que se encontrarán las páginas y los archivos a servir. Cambiaremos la ruta a la carpeta que hemos creado:

DocumentRoot «c:/servidor_web»
<Directory «c:/servidor_web»>

A continuación copiaremos nuestro juego exportado a WebGL a la carpeta c:/servidor_web. Si ahora vamos a http://localhost/ se ejecutará nuestro juego en WebGL.

Para acceder al servidor desde otra computadora conectada en una red local solo es necesario escribir en la barra de direcciones la dirección IP de la computadora que sirve de host, es decir la que tiene el servidor Apache instalado.

Para que tu conexión a internet pase a través del servidor será necesario configurarlo como un proxy fordward para eso en el archivo de configuración httpd.conf descomenta, (quitar el signo #) las siguientes líneas:
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

Después agrega en el final del archivo la siguiente linea: ProxyRequests On
Cierra y guarda los cambios.

Accede a las Opciones de internet mediante el Panel de Control.

  • En la pestaña Conexiones pulsa el botón Configuración de LAN y marca la casilla Usar un servidor Proxy
  • Escribe en Dirección: 127.0.0.1 y en Puerto: 80 o el que vayas a usar.
  • Presiona Aceptar en todas las ventanas.
  • Reinicia el servidor.
  • Lo anterior se aplica si usas el navegador Internet Explorer y Google Chrome, si usas Firefox las opciones anteriores tienes que ingresarlas en:
    Opciones >Configuración >Configurar como Firefox se conecta a Internet.

A partir de ahora toda tu conexión pasa a través de Apache, sea direcciones locales o externas.

Release Episodix v0.1

Vamos a sacar la primera release de Episodix en BitBucket cuya versión será la 0.1. Empezaremos creando una nueva rama a la que llamaremos develop, que será la rama en la que a partir de ahora se trabajará en el desarrollo del código. Debemos crearla tanto en BitBucket (para la versión de origen) como en SourceTree (para la versión local). Esta rama partirá de la que ya teníamos, master, que a partir de ahora solo constará de los commits con las versiones estables de Episodix y HEAD estará siempre en la última de estas versiones estables.

create_branch

A continuación crearemos la versión estable del código de Episodix v0.1 y hacemos un commit en develop.

develop

Ahora uniremos la rama developmaster para pasarle el commit con la release 0.1 de Episodix.

merge

Etiquetaremos el commit con «0.1» para facilitar futuras referencias a dicha versión.

tag

Ya tendremos nuestra release de Episodix v0.1 en la rama master. A partir de ahora los commits en esta rama únicamente incluirán versiones de lanzamiento estables de Episodix, mientras que todo el desarrollo se llevará a cabo desde la rama develop.

Episodix: Iteración 2

A continuación se describirá la Iteración 2 del proyecto Episodix.

El estructura de la Iteración 2 será la siguiente:

  • Learning Phase: El usuario camina (al estilo FPS) por una calle vacía. En el lado derecho aparecen tres sólidos tridimensionales aleatorios sencillos de entre los sólidos disponibles de una lista de seis. Estos sólidos se situarán en unas posiciones llamadas Anchor Points (tres en total).
  • Recall Phase:  Cuando llega al final de la calle, el juego le presenta preguntas sí/no para comprobar si recuerda qué sólidos tridimensionales ha visto mientras caminaba por la calle. Se le preguntará por los sólidos en el orden en que aparecen en la calle y por tres sólidos aleatorios más (pudiendo ser repetidos los tres anteriores).
  • Scores: Finalmente el juego presenta la puntuación obtenida. No se almacenan los resultados.
Scene

La escena iteration_2 es la única existente y se compondrá de los siguientes objetos:

  • Scores: objeto vacío que recogerá y almacenará las respuestas del usuario mediante un script asociado.
  • Directional light: alumbrará nuestra Scene.
  • Street: será la calle vacía con los seis sólidos tridimensionales en su lado derecho desactivados y los tres Anchor Points. Al inicio del juego se asignarán de manera aleatoria tres de los sólidos a las posiciones de los Anchor Points y se activarán. Además tendrá cuatro paredes invisibles para evitar que el jugador caiga al vacío.

street

  • FPSController: personaje con vista en primera persona que manejará el usuario para moverse por la calle y observar el entorno. Está sacado de los Standard Assets de Unity. Se desplazará con las teclas WASD y empleará el ratón para mover la vista.
  • CheckEnd: pared invisible con el collider puesto en modo Trigger que se usará para detectar el momento en el que el jugador llega al final de la calle. Tendrá una marca amarilla para indicar con claridad dónde se sitúa.

checkend

  • Recall Phase Camera: será la cámara usada en todas las fases del juego que no requieran del uso del FPSController.
  • Recall Phase: es una UI (User Interface) en la que se mostrarán las preguntas al usuario, que tendrá que responder haciendo clic el el botón Sí/No. Al responder a todas las preguntas se mostrará la puntuación (incluída también en Recall Phase) y podrá elegir si quiere reiniciar la prueba o salir del juego. Esta interfaz aparecerá cuando el usuario llegue al final de la calle. Cada pregunta constará de un texto con la pregunta y dos botones (Sí/No) para responderla, además de una imagen del sólido por el cual se pregunta.

RecallPhase_1 RecallPhase_2

  • Menu: es una UI en la que se muestra el menú del juego, que incluirá tres botones: 1) Continuar, para volver al punto en el que estamos en el juego. 2) Reiniciar, para cargar el nivel de nuevo y empezar de nuevo el juego. 3) Salir, para cerrar el juego.

menu

  • All Solids: es una UI que mostrará la imagen de todos los sólidos posibles, con sus respectivos nombres, al jugador.

allsolids

  • EventSystem: se creará automáticamente al crear un Canvas ( un elemento usado para el renderizado de pantalla de la UI , el objeto Recall Phase en nuestro caso) para controlar los eventos del mismo.
Scripts

En la Iteración 2 se hará uso de un total de seis C# scripts:

  • EndMenu.cs : constará de dos funciones. Una para reiniciar el nivel y otra para cerrar el juego. Este script estará asociado a FPSController.

using UnityEngine;
using System.Collections;

public class EndMenu : MonoBehaviour
{
// Esta funcion reinicia el nivel.
public void ResetLevel ()
{
Application.LoadLevel ("iteration_1");
}

// Esta funcion cierra el juego.
public void ExitGame ()
{
Application.Quit ();
}
}

diagram_endmenu

  • Menu.cs : será el encargado de gestionar la activación/desactivación del menú (objeto Menu) mediante la tecla escape y de All Solids manteniendo la tecla tab. Este script estará asociado a Street.

using UnityEngine;
using System.Collections;

public class Menu : MonoBehaviour
{
// Se almacenarán los objetos que necesitaremos activar/desactivar
// a la hora de activar/desactivar el menú.
//
public GameObject men;
public GameObject character;
public GameObject cam;
public GameObject recallPhase;
public GameObject allSolids;


// menu indicará el estado del menú actualmente.
bool menu = false;
// rP indicará si la RecallPhase está activa.
// rP2 indicará si RecallPhase deberá activarse o desactivarse al cerrar el menú.
bool rP, rP2;


public void Update()
{
// Al pulsar la tecla "escape" se activará/desactivará el menú.
//
if (Input.GetKeyDown ("escape"))
{
rP = recallPhase.activeInHierarchy;
if(!menu)
{
menu = true;
men.SetActive(true);
if(rP)
{
recallPhase.SetActive(false);
rP2 = true;
}
else
{
character.SetActive(false);
cam.SetActive(true);
rP2 = false;
}
}
else
{
menu = false;
men.SetActive(false);
if(rP2)
{
recallPhase.SetActive(true);
}
else
{
cam.SetActive(false);
character.SetActive(true);
}
}
}


// Al mantener pulsada la tecla "tab" se mantendrá activo el objeto All Solids.
//
if (Input.GetKey ("tab"))
{
allSolids.SetActive (true);
}
else
{
allSolids.SetActive(false);
}
}


// En el caso de cerrar el menú con el botón Continuar, esta función será llamada y
// cambiará el valor de menu al valor de newMenu (que será false).
//
public void ChangeMenuValue(bool newMenu)
{
menu = newMenu;
}
}

diagram_menu

  • Scores.cs : constará de dos funciones. La primera recogerá la respuesta del usuario a una pregunta. La otra comprobará si es correcta y almacenará los resultados; si es la última pregunta calcula el porcentaje de acierto del usuario. El script estará asociado al objeto Scores.

using UnityEngine;
using System.Collections;

public class Scores : MonoBehaviour {

public float rightAnswers; // Mostrara el numero de respuestas correctas en inspector.
float rA; // Numero de respuestas correctas con las que trabajaran las funciones.
public float totalAnswers; // Mostrara el numero de respuestas totales en inspector.
float tA; // Numero de respuestas totales con las que trabajaran las funciones.
public float totalQuestions; // Numero de preguntas totales. Se establecera su valor en Inpector.
public float results; // Almacenara el porcentaje de acierto de las respuestas.

bool answer; // Almacenara la respuesta a una pregunta (Si o No)

// Recogera la respuesta del usuario a una pregunta.
//
public void CollectAnswer(bool ans)
{
answer = ans;
}

// Comprobara si el usuario ha acertado con su respuesta a la pregunta, añadira el resultado y lo almacenara con el resto.
// Si era la ultima pregunta del test, calculara el porcentaje de acierto del usuario a todas las respuestas.
//
public void AddScore (GameObject solid)
{
rightAnswers = rA;
totalAnswers = tA;

if (solid.activeInHierarchy==answer)
{
++rA;
rightAnswers = rA;
++tA;
totalAnswers = tA;
} else
{
++tA;
totalAnswers = tA;
}

if (totalQuestions == tA) {
results = (rA / tA) * 100;
rA = 0;
tA = 0;
}
}
}

diagram_scores

  • ShowScores.cs : almacenará en el componente Text del objeto al que está asociado el porcentaje de aciertos del usuario a las preguntas, basándose en el script Scores.cs que se le pase por Inspector del que tomará los resultados. El script estará asociado al objeto Scores_txt.

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

// Almacena en el componente Text los resultados obtenidos de las respuestas del test.
//
public class ShowScores : MonoBehaviour
{

Text text; // Almacenara el texto que mostrara el porcentaje de acierto a las preguntas.

float scores; // Almacenara el porcentaje de acierto del usuario.

public Scores scr; // Almacenara el componente Scores(Script) que se le pase en Inspector.

void Awake ()
{
text = GetComponent<Text> ();
}

void Update ()
{
scores = scr.results;
scores *= 100;
scores = Mathf.Round(scores);
scores /= 100;
text.text = "Porcentaje de aciertos: " + scores +"%.";
}
}

diagram_showscores

  • Solids.cs : se encargará de asignar tres sólidos aleatorios del conjunto de seis a las posiciones de los Anchor Points. Una vez asignados a dichas posiciones se activarán en la escena. El script estará asociado al objeto Solids, hijo del objeto Street, al que están asociados los sólidos.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Solids : MonoBehaviour
{
// Almacenará los objetos correspondientes a los sólidos y el componente Transform de los Anchor Points.
//
public GameObject cube, sphere, cylinder, capsule, prism, circle;
public Transform anchor1, anchor2, anchor3;

int select, count, i;

// Se crea una lista con los sólidos de la escena y se selecciona de manera aleatoria tres
// de ellos para asignarlos a la posición de los Anchor Points y activarlos.
//
void Awake ()
{
List<GameObject> solids = new List<GameObject> ()
{
cube,
sphere,
cylinder,
capsule,
prism,
circle,
};


for (i=1; i<4; i++)
{
select = Random.Range(0, solids.Count);
switch(i)
{
case 1:
solids[select].GetComponent<Transform>().position = anchor1.position;
break;
case 2:
solids[select].GetComponent<Transform>().position = anchor2.position;
break;
case 3:
solids[select].GetComponent<Transform>().position = anchor3.position;
break;
}
solids[select].SetActive(true);
solids.RemoveAt (select);
}
}
}

diagram_solids

  • StartRecallPhase.cs : comprueba si el personaje atraviesa el objeto CheckEnd que marca el fin de la calle. De ser así finaliza la Learning Phase y lanza la Recall Phase. Dentro se encuentra también la función encargada de mostrar las preguntas en la RecallPhase (empezando por las que preguntan por los sólidos presentes en la calle) y sus resultados cuando ya no haya más preguntas que mostrar. El script estará asociado al objeto CheckEnd.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class StartRecallPhase : MonoBehaviour
{
public GameObject q1,q2,q3,q4,q5,q6; // Almacenará los objetos de las posibles preguntas del test.
public GameObject cube, sphere, cylinder, capsule, prism, circle; // Almacenará todos los posibles sólidos.
public GameObject scores;
public GameObject fpsc; // Almacenará el objeto del personaje.
public GameObject cam; // Almacenará el objeto de la camara de la Recall Phase.
public GameObject recallPhase; // Almacenará el objeto Recall Phase;

public Transform anchor1, anchor2, anchor3;

int select;
int count = 3;

List<GameObject> allSolidsQuestions = new List<GameObject>(); // Lista con las preguntas de todos los sólidos.
List<GameObject> streetSolidsQuestions = new List<GameObject>(); // Lista con las preguntas de los sólidos de la calle.
List<GameObject> allSolids = new List<GameObject>(); // Lista con todos los sólidos posibles.

// Esta función añadirá a la lista streetSolidsQuestions la pregunta correspondiente al sólido
// que se encuentre en la posición del Anchor Point pasado como argumento.
//
void AddStreetSolidsQuestions(Transform anchorPoint)
{
for (int i = 0; i < allSolids.Count; i++)
{
if (allSolids[i].transform.position == anchorPoint.position)
{
switch (allSolids[i].name)
{
case "Cube":
streetSolidsQuestions.Add(q1);
break;
case "Sphere":
streetSolidsQuestions.Add(q2);
break;
case "Cylinder":
streetSolidsQuestions.Add(q3);
break;
case "Capsule":
streetSolidsQuestions.Add(q4);
break;
case "Prism":
streetSolidsQuestions.Add(q5);
break;
case "Circle":
streetSolidsQuestions.Add(q6);
break;
}
}
}
}


// Al inicio se añaden los objetos de las listas declaradas anteriormente.
//
void Start()
{
allSolids.Add(cube);
allSolids.Add(sphere);
allSolids.Add(cylinder);
allSolids.Add(capsule);
allSolids.Add(prism);
allSolids.Add(circle);

AddStreetSolidsQuestions(anchor1);
AddStreetSolidsQuestions(anchor2);
AddStreetSolidsQuestions(anchor3);

allSolidsQuestions.Add (q1);
allSolidsQuestions.Add (q2);
allSolidsQuestions.Add (q3);
allSolidsQuestions.Add (q4);
allSolidsQuestions.Add (q5);
allSolidsQuestions.Add (q6);
}


// Mostrará cual es la siguiente pregunta que de la Recall Phase. Empezará
// mostrando las correspondientes a los objetos situados en la calle.
// En caso de que ya no haya más preguntas mostrará los resultados.
//
public void nextQuestion()
{
if (streetSolidsQuestions.Count != 0)
{
streetSolidsQuestions[0].SetActive(true);
streetSolidsQuestions.RemoveAt(0);
}
else
{
if (allSolidsQuestions.Count != 0 && count != 0)
{
select = Random.Range(0, allSolidsQuestions.Count);
allSolidsQuestions[select].SetActive(true);
allSolidsQuestions.RemoveAt(select);
count--;
}
else
{
scores.SetActive(true);
}
}
}


// Comprueba si el personaje atraviesa al objeto que marca el fin de la calle y de
// ser asi comienza la Recall Phase.
//
void OnTriggerEnter(Collider other)
{
if (other.CompareTag ("Player"))
{
recallPhase.SetActive(true);
nextQuestion();
fpsc.SetActive (false);
cam.SetActive (true);
}
}
}

diagram_startrecallphase

El funcionamiento de los botones en las respuestas a las preguntas de la Recall Phase será el mismo que en la iteración 1.

Episodix: Integration Tests – Iteración 2

En cada test se añadirán los objetos, scripts y demás cosas necesarias de iteración_2 para su correcta realización. Para la realización de estos tests se hace uso de scripts (uno para cada test) que realizan las comprobaciones correspondientes. Al pasar dichas comprobaciones se utiliza la función IntegrationTest.Pass( ); para indicar que se pasa correctamente o IntegrationTest.Fail( ); para indicar que falla. A continuación se mostrarán los tests de integración que se implementaron en Unity para comprobar el correcto funcionamiento de la iteración 2:

Test_Solids: Test para comprobar que hay uno de los seis sólidos posibles en cada una de las posiciones de los Anchor Points. Partiendo del script Solids.cs se crea el script TestSolids.cs, que realizará las acciones de elección y activación en los Anchor Points de Solids.cs y comprobará que el resultado sea el esperado. La escena del test constará del objeto Street con TestSolids.cs asociado a Solids.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class TestSolids : MonoBehaviour
{
// Almacenará los objetos correspondientes a los sólidos y el componente Transform de los Anchor Points.
//
public GameObject cube, sphere, cylinder, capsule, prism, circle;
public Transform anchor1, anchor2, anchor3;

int select, count, i;

// Se crea una lista con los sólidos de la escena y se selecciona de manera aleatoria tres
// de ellos para asignarlos a la posición de los Anchor Points y activarlos.
//
void Awake ()
{
List<GameObject> solids = new List<GameObject> ()
{
cube,
sphere,
cylinder,
capsule,
prism,
circle,
};

for (i=1; i<4; i++)
{
select = Random.Range(0, solids.Count);
switch(i)
{
case 1:
solids[select].transform.position = anchor1.position;
break;
case 2:
solids[select].transform.position = anchor2.position;
break;
case 3:
solids[select].transform.position = anchor3.position;
break;
}
solids[select].SetActive(true);
solids.RemoveAt (select);
}

// Si hay un sólido en la posición de cada anchor el test es satisfactorio, en caso
// contrario se dará por fallido el test.
//
if (CheckSolid(anchor1) && CheckSolid(anchor2) && CheckSolid(anchor3))
{
IntegrationTest.Pass();
}
else
{
IntegrationTest.Fail();
}
}

// Comprueba que en la posición de anchor se encuentra alguno de los seis sólidos.
//
bool CheckSolid(Transform anchor)
{
if (anchor.position == cube.transform.position || anchor.position == sphere.transform.position || anchor.position == cylinder.transform.position
|| anchor.position == capsule.transform.position || anchor.position == prism.transform.position || anchor.position == circle.transform.position)
{
return true;
}
else
{
return false;
}
}
}

Test_Solids

Test_QuestionAnchorPoints: Test para comprobar que la primera pregunta se corresponde con el sólido del primer Anchor Point, la segunda pregunta con el sólido del segundo Anchor Point y la tercera pregunta se corresponde con el sólido del tercer Anchor Point. Partiendo del script StartRecallPhase.cs se crea el script TestStartRecallPhase.cs, que realizará las acciones de elección de las preguntas del modo que se hace en StartRecallPhase.cs y comprobará que el resultado de las tres primeras preguntas se correspondan con los objetos en los Anchor Points y manteniendo el mismo orden. La escena del test constará de los objetos Street, RecallPhase (donde se encuentran las preguntas) y CheckEnd, con TestStartRecallPhase.cs asociado.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class TestStartRecallPhase : MonoBehaviour
{
public GameObject q1, q2, q3, q4, q5, q6; // Almacenará los objetos de las posibles preguntas del test.
public GameObject cube, sphere, cylinder, capsule, prism, circle; // Almacenará todos los posibles sólidos.
public GameObject scores;
public GameObject recallPhase; // Almacenará el objeto Recall Phase;

public Transform anchor1, anchor2, anchor3;

GameObject aux;

int select;
int count = 3;

List<GameObject> allSolidsQuestions = new List<GameObject>(); // Lista con las preguntas de todos los sólidos.
List<GameObject> streetSolidsQuestions = new List<GameObject>(); // Lista con las preguntas de los sólidos de la calle.
List<GameObject> allSolids = new List<GameObject>(); // Lista con todos los sólidos posibles.

// Esta función añadirá a la lista streetSolidsQuestions la pregunta correspondiente al sólido
// que se encuentre en la posición del Anchor Point pasado como argumento.
//
void AddStreetSolidsQuestions(Transform anchorPoint)
{
for (int i = 0; i < allSolids.Count; i++)
{
if (allSolids[i].transform.position == anchorPoint.position)
{
switch (allSolids[i].name)
{
case "Cube":
streetSolidsQuestions.Add(q1);
break;
case "Sphere":
streetSolidsQuestions.Add(q2);
break;
case "Cylinder":
streetSolidsQuestions.Add(q3);
break;
case "Capsule":
streetSolidsQuestions.Add(q4);
break;
case "Prism":
streetSolidsQuestions.Add(q5);
break;
case "Circle":
streetSolidsQuestions.Add(q6);
break;
}
}
}
}

// Al inicio se añaden los objetos de las listas declaradas anteriormente.
//
void Start()
{
allSolids.Add(cube);
allSolids.Add(sphere);
allSolids.Add(cylinder);
allSolids.Add(capsule);
allSolids.Add(prism);
allSolids.Add(circle);

AddStreetSolidsQuestions(anchor1);
AddStreetSolidsQuestions(anchor2);
AddStreetSolidsQuestions(anchor3);

allSolidsQuestions.Add(q1);
allSolidsQuestions.Add(q2);
allSolidsQuestions.Add(q3);
allSolidsQuestions.Add(q4);
allSolidsQuestions.Add(q5);
allSolidsQuestions.Add(q6);

if (CheckSolidsQuestions(anchor1, nextQuestion()) && CheckSolidsQuestions(anchor2, nextQuestion()) && CheckSolidsQuestions(anchor3, nextQuestion()))
{
IntegrationTest.Pass();
}
else
{
IntegrationTest.Fail();
}
}


// Devolverá la siguiente pregunta de la Recall Phase. Empezará
// devolviendo las correspondientes a los objetos situados en la calle.
// En caso de que ya no haya más preguntas devolverá los resultados.
//
GameObject nextQuestion()
{
if (streetSolidsQuestions.Count != 0)
{
aux = streetSolidsQuestions[0];
streetSolidsQuestions.RemoveAt(0);
return aux;
}
else
{
if (allSolidsQuestions.Count != 0 && count != 0)
{
select = Random.Range(0, allSolidsQuestions.Count);
aux = allSolidsQuestions[select];
allSolidsQuestions.RemoveAt(select);
count--;
return aux;
}
else
{
aux = scores;
return aux;
}
}
}


// Comprueba si se corresponde la pregunta con el sólido de la posición
// del anchorPoint.
bool CheckSolidsQuestions(Transform anchorPoint, GameObject Qsolid)
{
switch (Qsolid.name)
{
case "Q1cube":
return CheckPosition(cube, anchorPoint);
case "Q2sphere":
return CheckPosition(sphere, anchorPoint);
case "Q3cylinder":
return CheckPosition(cylinder, anchorPoint);
case "Q4capsule":
return CheckPosition(capsule, anchorPoint);
case "Q5prism":
return CheckPosition(prism, anchorPoint);
case "Q6circle":
return CheckPosition(circle, anchorPoint);
default:
return false;
}
}

// Comprueba si la posición de sol y anch es la misma.
//
bool CheckPosition(GameObject sol, Transform anch)
{
if (sol.transform.position == anch.position)
{
return true;
}
else
{
return false;
}
}
}

Test_QuestionAnchorPoints

– Test_CheckScores: Test para comprobar que se actualiza correctamente la puntuación. Partiendo del script Scores.cs se crea el script TestScores.cs, que realizará las acciones para determinar si la respuesta dada es correcta comparándola con los sólidos de la escena que se hace en Scores.cs y comprobará que el resultado es el esperado: respuesta true si el objeto está presente en la calle añade respuesta correcta e incorrecta si no está presente; respuesta flase si el objeto no está presente en la calle añade respuesta correcta e incorrecta si está presente. La escena del test constará de los objetos Street, en el cual siempre estarán activos el cubo, la esfera y el cilindro e inactivos el resto, y Scores, con TestScores.cs asociado.

using UnityEngine;
using System.Collections;

public class TestScores : MonoBehaviour
{
public GameObject cube; // Sólido activo en la escena de testeo.
public GameObject capsule; // Sólido inactivo en la escena de testeo.
bool answer; // Almacenará la respuesta a una pregunta (Si o No)

// Recogerá la respuesta a una pregunta.
//
void CollectAnswer(bool ans)
{
answer = ans;
}

// Comprobará si la respuesta a la pregunta es correcta.
//
bool AddScore (GameObject solid)
{
if (solid.activeInHierarchy==answer) // Comprueba que la respuesta coincida con el estado del sólido en la escena.
{
return true; // Respuesta correcta.
} else
{
return false; // Respuesta incorrecta.
}
}


void Start()
{
// Se recogerá como respuesta true para el caso de que se preguntase por el cubo, que sabemos que está
// activo en la escena de testeo.
CollectAnswer(true);
// AddScore(cube) debería devolver true, en caso contrario se considera el test fallido.
if (!AddScore(cube))
{
IntegrationTest.Fail();
}
// Se recogerá como respuesta false para el caso de que se preguntase por el cubo, que sabemos que está
// activo en la escena de testeo.
CollectAnswer(false);
// AddScore(cube) debería devolver false, en caso contrario se considera el test fallido.
if (AddScore(cube))
{
IntegrationTest.Fail();
}
// Se recogerá como respuesta true para el caso de que se preguntase por la cápsula, que sabemos que no
// está activa en la escena de testeo.
CollectAnswer(true);
// AddScore(capsule) debería devolver false, en caso contrario se considera el test fallido.
if (AddScore(capsule))
{
IntegrationTest.Fail();
}
// Se recogerá como respuesta true para el caso de que se preguntase por la cápsula, que sabemos que no
// está activa en la escena de testeo.
CollectAnswer(false);
// AddScore(capsule) debería devolver true, en caso contrario se considera el test fallido.
if (!AddScore(capsule))
{
IntegrationTest.Fail();
}
IntegrationTest.Pass();
}

}

Test_CheckScores

– Test_Menu: Test para comprobar que al pulsar la tecla escape, el objeto de la interfaz del menú del juego (continuar, reiniciar, salir), así como su cámara, se activa/desactiva. Del mismo modo se comprobará que FPSController se desactiva/activa simultáneamente. Partiendo del script Menu.cs se crea el script TestMenu.cs, que realizará las acciones para activar/desactivar el objeto Menu correspondiente al menú del juego que se hace en Menu.cs y comprobará que el comportamiento sea el adecuado. Para simular la pulsación de la tecla «escape» se llamará a una función, ActivateMenu( ), que realizará las acciones que se corresponderían con la pulsación de dicha tecla. La escena del test constará de los objetos Menu (que incluye la interfaz del menú), Recall Phase Camera (la cámara correspondiente al menú), FPSController (el personaje controlado por el usuario) y Street, con TestMenu.cs asociado.

using UnityEngine;
using System.Collections;

public class TestMenu : MonoBehaviour
{
// Se almacenarán los objetos que necesitaremos activar/desactivar
// a la hora de activar/desactivar el menú.
//
public GameObject men;
public GameObject character;
public GameObject cam;

// menu indicará el estado del menú actualmente.
bool menu;

void Start()
{
menu = men.activeInHierarchy;

// Al pulsar la tecla "escape" se activará/desactivará el menú. Se simulará el caso en que se
// pulsase "escape" (Input.GetKeyDown("escape")) llamando a la función ActivateMenu().
// La primera llamada a ActiveMenu() debería activar el menú y la cámara y desactivar el FPSController...
//
ActivateMenu();
if (!men.activeInHierarchy && !cam.activeInHierarchy && character.activeInHierarchy)
{
IntegrationTest.Fail();
}

// ...mientras que la segunda llamada debería activar el FPSController y desactivar el menú y la cámara.
//
ActivateMenu();
if (men.activeInHierarchy && cam.activeInHierarchy && !character.activeInHierarchy)
{
IntegrationTest.Fail();
}
else
{
IntegrationTest.Pass();
}
}


// Activa el menú si está desactivado y lo desactiva si está activado.
//
void ActivateMenu()
{
if (!menu)
{
menu = true;
men.SetActive(true);
character.SetActive(false);
cam.SetActive(true);
}
else
{
menu = false;
men.SetActive(false);
cam.SetActive(false);
character.SetActive(true);
}
}
}

Test_Menu

– Test_AllSolids: Test para comprobar que mientras se mantiene pulsada la tecla tab el objeto de la interfaz que muestra el conjunto de todos los posibles sólidos se mantiene activado y si no se mantiene la tecla tab se mantendrá desactivado. Partiendo del script Menu.cs se crea el script TestAllSolids.cs, que realizará las acciones para activar/desactivar el objeto All Solids correspondiente a la interfaz del juego que muestra por pantalla todos los posibles sólidos del juego que se hace en Menu.cs y comprobará que el comportamiento sea el adecuado. Para simular la pulsación de la tecla «tab» se llamará a una función, ActivateAllSolids( ), que realizará las acciones que se corresponderían con la pulsación de dicha tecla. Se considerará que la tecla «tab» se mantiene pulsada hasta que se llame otra vez a la función ActivateAllSolids( ). La escena del test constará de los objetos All Solids (que incluye la interfaz con los posibles sólidos) y Street, con TestAllSolids.cs asociado.

using UnityEngine;
using System.Collections;

public class TestAllSolids : MonoBehaviour
{
// Se almacenarán los objetos que necesitaremos activar/desactivar
// a la hora de activar/desactivar AllSolids.
//


public GameObject allSolids;

// aS indicará el estado de AllSolids actualmente.
bool aS;

void Start()
{
aS = allSolids.activeInHierarchy;
// Inicialmente AllSolids debe estar desactivado.
if (allSolids.activeInHierarchy)
{
IntegrationTest.Fail();
}
// Al mantener pulsada la tecla "tab" se mantendrá activado AllSolids. Se simulará el caso en que se
// pulsase "tab" (Input.GetKey("tab")) llamando a la función ActivateAllSolids().
// La primera llamada a ActiveAllSolids() simulará que se mantiene pulsada la tecla "tab", lo que
// resultaría en mantener AllSolids activado...
//
ActivateAllSolids();
if (!allSolids.activeInHierarchy)
{
IntegrationTest.Fail();
}


// ...mientras que la segunda llamada se simulará que se deja de pulsar la tecla "tab", lo que debería
// desactivar AllSolids.
//
ActivateAllSolids();
if (allSolids.activeInHierarchy)
{
IntegrationTest.Fail();
}
else
{
IntegrationTest.Pass();
}
}


// Activa AllSolids
//
void ActivateAllSolids()
{
aS = !aS;
allSolids.SetActive(aS);
}
}

Test_AllSolids

A mayores también se mantendrán los tests realizados previamente para la iteración 1. No sería necesario añadir ningún test unitario más.

*Para más información acerca de Unity Test Tools pulse aquí.

Programación orientada a objetos

A la hora de hacer un programa, siempre es una buena idea empezar pensando qué problema resolverá. Los objetos nos permiten una estructura del código basada en el problema que se solucionará, de modo que podemos emplear el tiempo en pensar en el problema en lugar de empantanarnos en la mecánica de la escritura de código. Al usar objetos correctamente, el código a escribir será intuitivo, y fácil de leer y modificar.

Clases y métodos

Pongamos como ejemplo la navegación de un GPS. La clase que emplearemos se llamará Navigator.

Se introduciría nuestra localización actual mediante el método SetCurrentLocation() , la cual se le pasaría como un parámetro tipo string. Del mismo modo, empleando SetDestination() , se introduciría la localización objetivo. Para obtener la ruta entre las dos localizaciones se llamaría al método GetRoute() , que calcularía la ruta y la devolvería como un string. Para evitar pasar por algún lugar en concreto en nuestra ruta se emplearía ModifyRouteToAvoid() , el cual modificaría nuestra ruta original; a continuación obtendríamos la nueva ruta usando de nuevo GetRoute() .

NavigatorVS

class Navigator {
public void SetCurrentLocation(string locationName) { ... }
public void SetDestination(string destinationName) { ... }
public void ModifyRouteToAvoid(string streetName) { ... }
public string GetRoute() { ... }
}

Algunos métodos devuelven algún valor al ejecutarse, dicho valor será de un tipo en concreto. Por ejemplo, en el método GetRoute() se devuelve un valor de tipo string al finalizar su ejecución.

La sentencia return le dice al método que finalice inmediatamente su ejecución. Si el método no devuelve ningún valor (void), la sentencia return no necesitará ningún valor («return;«) y no es necesario incluirla dentro del método. Si por el contrario el método debe devolver un valor, entonces tiene que usarse return.

public int MultiplyTwoNumbers(int firstNumber, int secondNumber) {
int result = firstNumber * secondNumber;
return result;
}

Este método multiplica los dos números que se le pasan como argumentos (tipo int) y devuelve el resultado (tipo int). Si llamamos al método podremos asignarle una variable tipo int que se igualará con el valor result devuelto.

int myResult = MultiplyTwoNumbers(3, 5);

En resumen, una clase tiene métodos que contienen sentencias que realizan acciones. Los métodos pueden devolver un valor de un tipo en concreto, en cuyo caso deben incluir la sentencia return junto a una variable del tipo del valor devuelto por el método. En cuanto la sentencia return se ejecuta, el programa regresará de vuelta al punto donde ejecutó el método. Aunque el método no devuelva ningún valor, puede usarse return igualmente para finalizar el método en cualquier punto.

A modo de ejemplo, se creará un script llamado Talker.cs que contendrá la clase Talker. En esta clase incluiremos un método BlahBlahBlah( ) al cual se le pasará un string y un int. El método repetirá el string el número de veces indicado por la variable int añadiendo un salto de línea al final de cada repetición y almacenándolo todo en un nuevo string. Este nuevo string será asignado a un texto mostrado por pantalla en Unity mediante una interfaz 2D. El script estará asociado al texto en cuestión. El método devolverá la longitud del nuevo string. Al final se añadirá al texto una línea indicando la longitud del mensaje (sin incluir esta línea). El número máximo de veces que se replicará el string estará limitado a 10, para cualquier número mayor se tomará como valor 10. Los valores de los parámetros se introducirán desde Inspector.

Hello!

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class Talker : MonoBehaviour
{
[SerializeField] private Text text;
public int nTimes;
public string say;
int length;
string lengthMessage;

int BlahBlahBlah(string thingToSay, int numberOfTimes)
{
string finalString = "";
for (int count = 0; count < numberOfTimes; count++)
{
finalString = finalString + thingToSay + "\n";
}
text.text = finalString;
return finalString.Length;
}

void Update()
{
if (nTimes > 10)
{
nTimes = 10;
}
length = BlahBlahBlah(say, nTimes);
lengthMessage = "\n" + "La longitud del mensaje es " + length.ToString();
text.text = text.text + lengthMessage;
}
}

Código y estructura

A la hora de escribir el código, es importante que sea fácil de entender. Es útil añadir comentarios, pero más útil es elegir nombres intuitivos para los métodos, clases y variables.

Podríamos haber acortado los nombres de la variables. Por ejemplo, numberOfTimes podría llamarse nT para acortar el código que escribimos, pero, ¿podría alguien que ve el código por primera vez saber a qué se refiere esa variable? A simple vista sería imposible, sin embargo, numberOfTimes nos indicará solo con leerlo que la información que contiene hacer referencia al número de veces que algo debe suceder. Más importante que hacer un código compacto es hacer un código fácilmente entendible. Del mismo modo podríamos haber puesto el código del método BlahBlahBlah() dentro de Update() directamente, pero creando el método sabemos que esa parte del código hará una cosa en concreto y nuestro programa quedará mejor estructurado.

Podemos hacer nuestro código más fácil de leer y escribir pensando en el problema que se supone que debe solucionar. Si escogemos nombres que tengan sentido para alguien que entiende el problema, el código será más fácil de descifrar y desarrollar.

También es útil realizar un diagrama de la clase que vamos a crear (anteriormente se ha mostrado un diagrama de la clase Navigator con sus métodos). De este modo, podemos diseñar qué métodos tendrá nuestra clase y qué función cumplirán para tener más claro el cómo proceder. Sabremos de una manera general cual será la estructura de la clase antes de empezar con su creación. En un post anterior se expica con detalle cómo crear estos diagramas. Ahora simplemente se mostrará un ejemplo:

class CandyController {CandyController
public void DoMaintenanceTests() {
...
if (IsNougatTooHot() == true) {
DoCICSVentProcedure();
}
...
}
public void DoCICSVentProcedure() ...
public boolean IsNougatTooHot() ...
}

Objetos

Volviendo al ejemplo del GPS, podríamos crear dos nuevas clases que fuesen exactamente iguales a Navigator para tener así un total de tres posibles rutas. Las tres clases serán idénticas, por lo que se comportarán del mismo modo y si queremos modificar una tendremos que modificar las tres. Además, replicar el mismo código varias veces no es muy eficiente.

Cuando queramos trabajar con varias cosas similares (como tener varias rutas) usaremos los objetos en C#. De este modo sólo tendremos que programar una clase y la usaremos tantas veces como queramos sin necesidad de replicar el código en nuevas clases del mismo tipo.

Para crear un nuevo objeto únicamente tendremos que usar la palabra clave new y el nombre de la clase. Al usar new para crear un objeto, se reservará memoria para él y se almacenará en dicha memoria. La parte de la memoria donde se almacenan los objetos se conoce como heap. A medida que se crean objetos se reserva la memoria correspondiente en heap y se rellena con esos objetos.

Navigator navigator1 = new Navigator();
navigator1.SetDestination("Fifth Ave & Penn Ave");
string route;
route = navigator1.GetRoute();

El objeto que creamos tendrá los mismos métodos que la clase a la que corresponde. La clase vendría a ser como los planos de los objetos que puedes crear a partir de ella. Pueden crearse todos los objetos que queramos.

Navigator_objects

Cuando creamos un objeto de una clase, se le llama instancia de esa clase. Todos tendrán sus propios métodos y propiedades, pero se comportarán del mismo modo. Si quisiéramos crear tres rutas en nuestro GPS, podríamos crear tres objetos. Asignarle a cada uno la misma localización origen y destino y añadir un lugar a evitar en cada uno de los objetos. Cada objeto tendría así una ruta distinta, pero habiendo empleado los mismos métodos. Y lo más importante, sólo es necesario escribir el código de la clase Navigator una única vez.

Cuando añadimos a un método la palabra clave static, nos permite usar sus métodos sin necesidad de crear una instancia. De modo que si tuviesemos

class Talker
{
static int BlahBlahBlah(string thingToSay, int numberOfTimes)

...

podríamos llamar al método sin necesidad de crear una instancia de la clase Talker

Talker.BlahBlahBlah("Hello hello hello", 5);

Se comportarán igual los métodos de instancias y los de clase (static).

Se puede marcar toda la clase con static, de manera que todos sus métodos tendrán que  serlo también. Si intentamos añadir un método no estático no compilará.

En el ejemplo del GPS no podemos hacer static los métodos con los que trabaja, ya que cada objeto tiene que tener los datos correspondientes a su propia ruta.

Fields

Los métodos nos indican lo que un objeto puede hacer, sin embargo lo que el objeto sabe está almacenado en los campos (fields). Si tomamos de nuevo el ejemplo de Navigator, cada uno de sus objetos sabrá la dirección de destino cuando usemos SetDestination(). Lo mismo para la posición actual y la ruta, única en cada instancia. Esta información se almacena en los campos de la clase.

Navigator_fields (1)

Si el primer objeto se llamase navigator1, podríamos acceder a la información de sus campos o incluso modificarlos (siempre que fuesen públicos) del siguiente modo:

Navigator navigator1 = new Navigator();
navigator1.currentLocation = "Calle falsa 123"; // Para cambiar su valor.
string destino = navigator1.destination; // Para obtener su valor.

Construyendo una clase

Es hora de aplicar todo lo aprendido hasta ahora. Crearemos una clase Guy y crearemos dos instancias (Joe y Bob). La clase constará de dos campos: Name (nombre del objeto) y Cash (efectivo del objeto). Tendrá además dos métodos: GiveCash() (al llamarlo hará que el objeto de parte de su dinero) y ReceiveCash() (para recibir dinero). Como veis todos los nombres empleados (clase, campos, métodos) son intuitivos para el problema que afrontaremos.

Cuando creemos las dos instancias, los objetos serán almacenados en heap. A continuación se asignarán los valores de los campos  NameCash de cada objeto (cada uno tendrá los suyos propios).

Los métodos seguirán esta forma:

bob.ReceiveCash(25);

Esto haría que el Cash de Bob aumente en  25.

El diagrama de la clase será tal que así:

Guy

El código de la clase será el siguiente:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class Guy : MonoBehaviour
{
public string Name; // Estos son los campos de la clase
public int Cash;
public int GiveCash(int amount) // GiveCash() recibirá como parámetro la cantidad que se dará
{
if (amount <= Cash && amount > 0) // Se dará el dinero siempre que sea mayor que 0 y
{                                 // menor que la cantidad total que posea el sujeto.
Cash -= amount;
return amount;
}
else   // Y si no, se mostrará por consola que no se dispone de esa cantidad.
{
Debug.Log("I don’t have enough cash to give you " + amount + ", " + Name + " says...");
return 0;
}
}
public int ReceiveCash(int amount) // ReceiveCash() recibirá como parámetro la cantidad que se recibirá
{
if (amount > 0) // La cantidad recibida debe ser mayor que 0.
{
Cash += amount;
return amount;
}
else   // Y si no, se mostrará por consola que no es una cantidad aceptable.
{
Debug.Log(amount + " isn’t an amount I’ll take" + ", " + Name + " says...");
return 0;
}
}
}

Ahora para trabajar con los dos objetos de la clase Guy, crearemos la clase JoeAndBob que asociaremos al objeto vacío Joe&Bob. En ella crearemos las dos instancias de los objetos (Joe y Bob) e inicializaremos los valores de Cash de Joe a 50 y los de Bob a 100, además de sus campos Name. Añadiremos además int bank, que indicará los fondos de un banco, y se inicializará a 100. Crearemos una interfaz en Unity que mostrará la cantidad de dinero de Joe, Bob y el banco. También habrá dos botones. El primero hará que el banco le de a Joe 10€, llamando a la función GiveToJoe(), y el segundo hará que Bob le de al banco 5€, llamando a la función ReceiveFromBob(). En ambos casos se comprobará que la cantidad sea correcta.

givetojoe

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class JoeAndBob : MonoBehaviour
{
Guy joe = new Guy();
Guy bob = new Guy();
int bank;
public Text jo;
public Text bo;
public Text ba;

void Start()
{
joe.Name = "Joe";
joe.Cash = 50;
bob.Name = "Bob";
bob.Cash = 100;
bank = 100;
}


void Update()
{
jo.text = "Joe has " + joe.Cash + "€.";
bo.text = "Bob has " + bob.Cash + "€.";
ba.text = "The bank has " + bank + "€.";
}

public void GiveToJoe(int a)
{
if (bank >= a && a > 0)
{
bank -= a;
joe.ReceiveCash(a);
}
}


public void ReceiveFromBob(int b)
{
if (bob.Cash >= b && b > 0)
{
bank += b;
bob.GiveCash(b);
}
}
}

A la hora de inicializar objetos podría incluirse el valor de los campos del siguiente modo:

Guy joe = new Guy() { Cash = 50, Name = "Joe" };

De este modo se ahorrarían un par de líneas de código.

Diagrama de clases

Un diagrama de clases describe los tipos de objetos en el sistema y los distintos tipos de relaciones estáticas que existen entre ellos. Los diagramas de clases muestran también las propiedades y operaciones de una clase y de las restricciones que se aplican a la manera en que los objetos están conectados. Para crear nuestros diagramas de clase usaremos el Lenguaje Unificado de Modelado (UML, por sus siglas en inglés, Unified Modeling Language), que es un lenguaje gráfico para visualizar, especificar, construir y documentar un sistema.  El UML utiliza el término característica como un término general que cubre las propiedades y operaciones de una clase.

Propiedades

Las propiedades representan las características estructurales de una clase. Son un único concepto, pero aparecen en dos notaciones distintas: atributos y asociaciones.

Atributos

La notación de atributo describe un propiedad como una línea de texto dentro de la caja de la clase. La forma completa de un atributo es:

visibility name : type multiplicity = default {property-string}

Por ejemplo:

- name : String [1] = "Untitled" {readOnly}

Solo el nombre (name) es necesario.

  • visibility indicará si el atributo es público (+) o privado (-). Cabe destacar que para poder ser modificadas desde el editor de Unity, las variables deben ser públicas (public).
  • name se corresponde al nombre del campo en el lenguaje de programación.
  • type indica qué tipo de objeto puede ocupar el atributo. Es el tipo del campo en el lenguaje de programación.
  • multiplicity  (multiplicidad) se explicará más adelante.
  • default indica el valor por defecto para un objeto creado si el atributo no está especificado durante la creación.
  • {property-string} permite indicar propiedades adicionales para el atributo. {readOnly} indica que no puede ser modificada esta propiedad.

Usaremos como ejemplo la clase Scores de nuestro juego (se mostrará la clase completa con los métodos posteriormente):

Scores(only_fields)

Asociaciones

La otra manera para anotar una propiedad es con una asociación. Una asociación es una línea entre dos clases, dirigida desde la clase de origen hacia la clase objetivo. El nombre de la propiedad va al objetivo final de la asociación, junto con su multiplicidad. El objetivo final de la asociación se une a la clase que indica el tipo de la propiedad.

Para Scores sería:

Scores_asociaciones

Podríamos también mostrar las asociaciones entre los sólidos de Episodix y el objeto padre Solids al que están asociados.

solids_hierarchy_dia

Si abrimos nuestro script Scores.cs en Visual Studio (el cual podemos instalar con Unity) se generará de manera automática el diagrama de clases UML a partir de nuestro código. Para acceder a él vamos a Class View y hacemos clic derecho en Scores. Seleccionamos View Class Diagram y se nos mostrará el diagrama de la clase Score.

Scores

Scores2

El diagrama de la clase Score se nos mostrará de este modo. Podemos ver los detalles de los campos (Fields), los métodos (Methods), etc, en la parte de abajo del programa. A continuación se muestra Scores y sus Fields (los métodos están ocultos, se mostrarán más adelante).

Diagrama_Scores_VS_sin_metodos

También podemos crear un diagrama de una clase y obtener el código para dicha clase. Para ello creamos un diagrama de una clase de prueba a la que llamaremos Test_class. Le añadimos algunas variables (públicas y privadas) y métodos, y hacemos clic derecho en ella. Seleccionamos View Code y nos mostrará el código correspondiente al diagrama creado (lo que hará cada método lo tendremos que programar nosotros obviamente).

Test_class

Test_class2

Multiplicidad

La multiplicidad de una propiedad es una indicación de cuántos objetos podrían rellenar la propiedad. Las comunes son:

  • 1 (indica que debe haber uno)
  • 0..1 (puede haber uno o ningún)
  • * (no hay límite)

En general las multiplicidades son definidas con unos límites inferior y superior. El límite inferior puede ser cualquier número positivo o cero, mientras que el superior será cualquier número positivo o * (ilimitado). Si los límites coinciden sólo se podrá usar ese número, por lo tanto es equivalente 1 y 1..1 o * y 0..*.

En atributos, existen varios términos para referirse a la multiplicidad:

  • Optional : límite inferior de 0.
  • Mandatory : límite inferior de 1 o más.
  • Single-valued : límite superior 1.
  • Multivalued : límite superior mayor que 1, normalmente *.

Si en un sistema multivalued el orden de los objetos tiene sentido es necesario añadir {ordered} al final de la asociación. Si se quieren permitir duplicados añadimos {nonunique}. Para el valor por defecto {unorderer}{unique}. Para unorderer y nonunique se puede usar {bag}.

La multiplicidad por defecto de un atributo es [1]. Sin embargo, no se puede asumir que si la multiplicidad no está indicada esta sea necesariamente [1]. Por tanto, en caso de ser importante es mejor indicar [1] en la multiplicidad.

En el ejemplo anterior de Scores la multiplicidad de todas sus variables es 1, por tanto lo dejamos sin indicar. Puesto que no tenemos ningún ejemplo en nuestro código del juego para la multiplicidad distinta de 1, usaremos el siguiente:

ej_multiplicidad_atributos

Si este fuese el ejemplo en la notación de atributos de nuestra clase, la siguiente imagen muestra cómo sería la notación con asociaciones.

ej_multiplicidad_asociaciones

Asociaciones bidireccionales

Las asociaciones que hemos visto hasta ahora son unidireccionales. Una asociación bidireccional es un par de propiedades que están vinculadas juntas y son inversas.

person_car_bidireccional

En el ejemplo , la clase Car tiene la propiedad owner:Person[1], y la clase Person tiene la propiedad cars:Car[*].

La unión inversa entre ellos implica que si tu sigues ambas propiedades, deberías regresar a un conjunto que contenga tu punto de inicio.

Como alternativa al etiquetado de una asociación por una propiedad, podemos etiquetar la asociación usando un verbo de manera que la asociación pueda ser usada en una frase. Esto es válido y puedes agregar una flecha a la asociación para evitar ambigüedad.

person_car_bidireccional_alternativo

La naturaleza bidireccional de una asociación es obvia añadiendo flechas de navegación en ambos extremos. En el caso del uso de un verbo no existirán estas flechas.

A la hora de implementar una asociación bidireccional en un lenguaje de programación debemos asegurarnos de que ambas propiedades se mantienen sincronizadas. Se usaría el siguiente código para implementar la relación bidireccional.

class Car . . .
public Person Owner {
get {return owner ;}
set {
if (owner != null) _owner .friendCars Q .Remove(this) ;
_owner = value ;
if (owner != null) _owner .friendCars Q .Add(this) ;
}
}
private Person owner ;


class Person . . .
public IList Cars {
get {return ArrayList .ReadOnly(_cars) ;}
}
public void AddCar(Car arg) {
arg .Owner = this ;
}
private IList _cars = new ArrayList Q ;
internal IList friendCars Q {
//should only be used by Car .Owner
return _cars ;
}

Lo principal es permitir a uno de los lados de la asociación -de un único valor a ser posible- controlar la relación. La parte esclava debe pasar sus datos encapsulados a la parte maestra.

Operaciones

Las operaciones son las acciones que la clase conoce para llevar a cabo. Las operaciones más obvias se corresponden con los métodos en la clase. Normalmente no se mostrarán estas operaciones que simplemente manipulan propiedades, porque en general pueden ser deducidas.

La sintaxis completa de UML para operaciones es:

visibility name (parameter-list) : return-type {property-string}

  • visibility : pública (+) o privada (-).
  • name: el nombre, es un string.
  • parameter-list : lista de los parámetros para la operación.
  • return-type : el tipo del valor que devuelve, si hay alguno.
  • property-string : indica los valores de la propiedad que se aplican a la operación dada.

Los parámetros en la lista de parámetros son anotados de una manera similar a los atributos. La forma es:

direction name : type = default value

  • nametypedefault value son igual que para los atributos.
  • direction indica si los parámetros son de entrada (in), salida (out) o ambos (inout). Si no se indica ninguno, se asume que es in.

UML define una query (consulta) como una operación que obtiene un valor de una clase sin cambiar el estado del sistema. Puedes marcar tal operación con la property-string {query}. Aquellas operaciones que modifican el estado del sistema son los modifiers (modificadores), también llamados comandos. La diferencia entre las consultas y los modificadores es si cambian un estado observable (que se puede percibir desde fuera). Se puede cambiar el orden de las consultas sin modificar el comportamiento del sistema. Como convención se suele tratar de escribir las operaciones que son modificadores no devuelvan ningún valor, de manera que podemos confiar en que las que sí los devuelven sean consultas: Principio de separación Command-Query.

Otros términos usados son getting method, que devuelve un valor de un campo, y setting method, que pone un valor en un campo. Desde fuera no se podrá saber si una consulta o un modificador son uno u otro método, ese conocimiento es enteramente interno de la clase.

Para distinguirlos sabemos que una operación es invocada en un objeto, mientras que un método está en el cuerpo del proceso.

Ejemplo para el caso de la clase Scores:

Diagrama_Scores_VS

Generalización

Un ejemplo típico de generalización implica los clientes (Customer) personales (Personal Customer) y corporativos (Corporate Customer) de un negocio. Tienen diferencias, pero también similitudes entre ellos. Las similitudes pueden ser colocadas en una clase general Customer, con Personal Customer y Corporate Customer como subtipos.

Conceptualmente podemos decir que Corporate Customer es un subtipo de Customer si todas las instancias de Corporate Customer son también instancias de Customer. Corporate Customer es entonces un tipo especial de Customer. El concepto clave es que todo lo que digamos de un Customer -asociaciones, atributos, operaciones- es verdades para un Corporate Customer.

La interpretación de esto, desde una perspectiva de software, es la herencia: Corporate Customer es una subclase de Customer. En los principales lenguajes orientados a objetos (OO), la subclase hereda todas las características de la superclase y puede anular anular cualquier método de la superclase.

Un principio importante de usar la herencia efectivamente es la sustituibilidad. Debemos poder sustituir un Corporate Customer dentro de cualquier código que requiera un Customer y todo debería funcionar correctamente. El código creado para un Customer debería ser válido para cualquier subtipo de Customer.

En resumen, la generalización representa relaciones entre clases en lugar de entre instancias de las clases (asociaciones).

En el caso de la clase Scores, está relacionada con la clase MonoBehaviour, que es la clase de la que derivan todos los scripts en Unity. Funciones como Start( ) o Update( ) forman parte de la clase MonoBehaviour: Update( ) es llamada cada frame, mientras que Start( ) es llamada en el frame en el que un script es activado justo antes de que Update( ) sea llamado por primera vez. Además, cuando usamos C# tenemos que derivar explícitamente de Monobehaviour ( public class Scores : MonoBehaviour ).

generalización

Recordemos que el código de Scores.cs es el siguiente:

using UnityEngine;
using System.Collections;

public class Scores : MonoBehaviour {

public float rightAnswers; // Mostrara el numero de respuestas correctas en inspector.
float rA; // Numero de respuestas correctas con las que trabajaran las funciones.
public float totalAnswers; // Mostrara el numero de respuestas totales en inspector.
float tA; // Numero de respuestas totales con las que trabajaran las funciones.
public float totalQuestions; // Numero de preguntas totales. Se establecera su valor en Inpector.
public float results; // Almacenara el porcentaje de acierto de las respuestas.

bool answer; // Almacenara la respuesta a una pregunta (Si o No)

// Recogera la respuesta del usuario a una pregunta.
//
public void CollectAnswer(bool ans)
{
answer = ans;
}

// Comprobara si el usuario ha acertado con su respuesta a la pregunta, añadira el resultado y lo almacenara con el resto.
// Si era la ultima pregunta del test, calculara el porcentaje de acierto del usuario a todas las respuestas.
//
public void AddScore (GameObject solid)
{
rightAnswers = rA;
totalAnswers = tA;

if (solid.activeInHierarchy==answer) // Comprueba si el objeto esta activo en Hierarchy.
{
++rA;
rightAnswers = rA;
++tA;
totalAnswers = tA;
} else
{
++tA;
totalAnswers = tA;
}

if (totalQuestions == tA) {
results = (rA / tA) * 100;
rA = 0;
tA = 0;
}
}
}

En el caso de los objetos en Unity, todos a los que puede hacer referencia pertenecen a la clase Object. Los métodos y atributos de los objetos en Unity serán los correspondientes a los de su clase Object y a los de sus subclases asociadas a cada objeto en concreto (dependiendo de qué componentes tenga asociados), que veremos a continuación. Dos clase derivan de Object: 1) clase GameObject, clase base para todas las entidades en las escenas en Unity, y 2) clase Component, clase base para todo lo unido a GameObjects. Nótese que nuestro código nunca creará directamente un Component. En su lugar, escribimos un script y añadimos el script a un GameObject. Todas las clases de cada uno de los distintos componentes en Unity derivarán de la clase Component.

Ya hemos dicho que los scripts en Unity deben derivar de MonoBehaviour, pero éste a su vez deriva de la clase Behaviour, que son los Components que pueden ser activados o desactivados. La clase Behaviour deriva a su vez de la clase Component que hemos comentado previamente. MonoBehaviour le proporciona a Component los eventos y llamadas a métodos necesarios por el motor. Así mismo, las variables globales de dichas clases, son mostradas por el Inspector y pueden modificarse en tiempo real mientras se previsualiza el juego.

En la siguiente imagen se muestra un diagrama de la relación entre todas las clases anteriormente mencionadas (abajo de todo se muestran los 6 scripts creados hasta ahora para el proyecto Episodix):

esquema_global_clases

GUI 2D en juego 3D parte 2

Si hacemos clic en el botón se sombreará. Para modificar el tiempo que tarda en sombrearse modificamos la opción Fade Duration. Sin embargo, por mucho que pulsemos el botón no sucederá nada, ya que aún no hemos asignado nada a nuestro botón. La programación de la interacción del UI sigue una serie de pasos para cualquiera de sus elementos:

  1. Crear un objeto de la UI en la scene (por ejemplo nuestro botón).
  2. Escribir un script para llamar cuando la UI esté funcionando.
  3. Adjuntar ese script a un objeto de la scene.
  4. Unir los elementos de la UI al objeto con ese script.

Empezaremos creando un objeto controlador para unir al botón. Creamos un script llamado UIController y soltamos ese script en el objeto controlador en la escena. El código del script es el siguiente:

using UnityEngine;
using UnityEngine.UI;  // Importa el código del framework de UI
using System.Collections;

public class UIController : MonoBehaviour
{
[SerializeField] private Text scoreLabel;  // Hace referencia al objeto Text en la scene
                                           // para ajustar la propiedad del texto.
void Update()
{
scoreLabel.text = Time.realtimeSinceStartup.ToString();
}

public void OnOpenSettings() // Método llamado por el botón de ajustes
{
Debug.Log("open settings");
}
}

Ahora arrastramos el objeto Text de nuestro UI al hueco Score Label de nuestro script. Ahora mismo este texto será un contador debido al código del script.

texto_contador

A continuación añadimos una entrada OnClick al botón. Las entradas OnClick serán ejecutadas cuando se pulse el botón al que pertenecen. Cada entrada tendrá un hueco para el objeto y un menú para seleccionar la función a llamar; arrastramos el objeto controlador al hueco de la entrada y seleccionamos la función OnOpenSettings( ) de UIController. Si ahora ejecutamos el juego y pulsamos el botón se nos mostrará en Console el mensaje «open settings» (función OnOpenSettings( ) de UIController.cs).

entrada_onclick

Creamos ahora otra imagen para nuestro UI (GameObject>UI>Image) y le asignamos un sprite. Por defecto, la imagen asignada ocupará todo el espacio previamente asignado a esa objeto imagen. Para que tome sus valores originales le damos a la opción Set Native Size. La opción Image Type por defecto será Simple, pero puesto que esta imagen la usaremos como ventana pop-up la cambiaremos a Sliced. Nos aparecerá una advertencia: «This Image doesn’t have a border». En Project seleccionamos la imagen y hacemos clic en el botón Sprite Editor que aparece en Inspector y nos abrirá la ventada de Sprite Editor. Una imagen de tipo Sliced debe estar dividida en 9 secciones. Por lo tanto, en el Sprite Editor dividiremos la imagen en 9 secciones asignándole un valor de 12 píxeles a las cuatro líneas que aparecerán (L T R B). Ahora ya no aparecerá la advertencia y la imagen funcionará perfectamente. La redimensionaremos al tamaño que deseemos para la ventana pop-up (250 ancho, 200 alto y posición 0,0,0).

sprite editor

Ahora crearemos un script llamado SettingsPopup y lo añadiremos al objeto pop-up. El código del script será el siguiente:

using UnityEngine;
using System.Collections;

public class SettingsPopup : MonoBehaviour
{
public void Open()
{
gameObject.SetActive(true); // Activa el objeto para abrir la ventana.
}
public void Close()
{
gameObject.SetActive(false); // Desactiva el objeto para cerrar la ventana.
}
}

Lo siguiente será modificar el script UIController de este modo:

...

[SerializeField] private SettingsPopup settingsPopup;

void Start()
{
settingsPopup.Close (); // Cierra el pop-up cuando empieza el juego.
}

...

public void OnOpenSettings()
{
settingsPopup.Open (); // Reemplaza el texto Debug con el método de pop-up.
}

Se creará un nuevo hueco en UIController al que debemos arrastrar el objeto pop-up. Ahora el pop-up se cerrará al iniciar el juego y se abrirá cuando le demos al botón. Para cerrarlo crearemos un botón que situaremos en la esquina superior derecha del pop-up y en su texto pondremos «Cerrar» y le cambiaremos el color a blanco. Añadiremos una entrada OnClic y arrastraremos el objeto pop-up y seleccionaremos la funcion Close( ) para cerrarlo. Ahora cuando pulsemos este botón «Cerrar» el pop-up se cerrará.

Ahora crearemos un objeto Text, InputField y Slider para el UI; el texto será para identificar el slider. Todos estos objetos se crearán desde GameObject>UI. Todos serán hijos del objeto pop-up. En el texto del objeto Text pondremos «Velocidad» y en el de InputField>Placeholder escribiremos «Nombre» (este será el texto que el jugador ve antes de escribir nada). En las opciones del slider le pondremos de valor máximo 2 y marcaremos la opción Whole Numbers, de este modo no tomará decimales y se limitará a los valores [0 1 2] en este caso.

popup

Lo siguiente será escribir el código para los objetos creados. Añadiremos al script SettingsPopup lo siguiente:

...

public void OnSubmitName(string name) // Esto se lanzará cuando el usuario escriba su nombre.
{
Debug.Log(name);
}

public void OnSpeedValue(float speed) // Esto se lanzará cuando el usuario ajuste el slider.
{
Debug.Log("Speed: " + speed);
}

...

Seleccionando el inputfield podemos ver en Inspector un panel End Edit; los eventos que pongamos aquí se lanzarán cuando el jugador acabe de escribir. Añadimos una entrada, arrastramos el objeto pop-up al hueco y elegimos OnSubmitName( ) como función; la función será elegida en la sección de arriba (Dynamic strings) y no la de abajo (Static strings).

dynamic-string

Mismo proceso con el slider pero en el panel On Value Changed. Añadimos la entrada, arrastramos el objeto pop-up y elegimos OnSpeedValue( ) en la lista de valores dinámicos (sección superior). Si lanzamos el juego, introducimos un nombre en inputfield y pulsamos enter, se mostrará por Console ese nombre. Lo mismo si seleccionamos un valor en el slider.

Para alertar a la UI de las acciones de la scene, vamos a usar un broadcast messenger system. En la imagen se ve cómo funciona: los scripts pueden registrar para escuchar a un evento, otro código puede retransmitir un evento, y los listeners (los que «escuchan») serán alertados acerca de los mensajes de la retransmisión.

messenger system

El messenger system que usaremos lo encontramos en http://wiki.unity3d.com/index.php/CSharpMessenger_Extended. Crearemos un script llamado Messenger y guardaremos el código del enlace en él. También crearemos un script llamado GameEvent:

public static class GameEvent
{
public const string PRESS_BUTTON = "PRESS_BUTTON";
public const string SPEED_CHANGED = "SPEED_CHANGED";
}

Este script define una constante para un par de mensajes de evento; los mensajes están así más organizados y no necesitamos recordar y escribir el mensaje por todo el lugar.

Hasta ahora en el texto del UI se mostraba un contador para probar su funcionalidad. Pero ahora mostrará el número de veces que hacemos clic derecho con el ratón. Deberemos modificar UIController.cs para ello:

...

private int _score;

void Awake()
{                                                            // Declara qué metodo responde
Messenger.AddListener(GameEvent.PRESS_BUTTON, OnRightClick); // al evento PRESS_KEY
}
void OnDestroy()
{                                                              // Cuando un objeto es destruido,
Messenger.RemoveListener(GameEvent.PRESS_BUTTON, OnRightClick);// usamos un limpiador de listener
}                                                              // para evitar errores.

void Start()
{
_score = 0;                            // Inicializa el score a 0.
scoreLabel.text = _score.ToString ();

settingsPopup.Close ();
}

private void OnRightClick()
{
_score += 1;                           // Incrementa el score en respuesta al evento.
scoreLabel.text = _score.ToString();
}

...

El método Update( ) ha sido eliminado. Los métodos Awake( ) y OnDestroy( ) funcionarán como todo MonoBehaviour, como por ejemplo Start( ) o Update( ), de manera automática al crearse y destruirse el objeto. Un listener es añadido y eliminado en Awake( ) / OnDestroy( ). El listener es parte del broadcast messenger system, y llama a OnRightClick( ) cuando ese mensaje es recibido, que incrementará la puntuación y la mostrará en el marcador.

A continuación crearemos un script llamado IfRightClick. Este script simplemente comprobará si pulsamos el clic derecho del ratón y retransmitirá un mensaje de ser así. De este modo, cada vez que hagamos clic derecho en el juego, la puntuación de la esquina superior izquierda aumentará en 1. Este script lo añadiremos a nuestro personaje.

using UnityEngine;
using System.Collections;

public class IfRightClick : MonoBehaviour
{
void Update ()
{
if (Input.GetButtonDown ("Fire2"))
{
Messenger.Broadcast(GameEvent.PRESS_BUTTON);
}
}
}

Hemos visto cómo es recibido por el HUD un mensaje retransmitido por la scene. Veremos ahora el caso contrario, el HUD retransmite un mensaje a la scene. En nuestro ejemplo haremos que el slider sea un selector que indicará el número de veces que se llamará el evento PRESS_BUTTON si hacemos clic derecho con el ratón. Modificaremos IfRightClick.cs de la siguiente manera:

using UnityEngine;
using System.Collections;

public class IfRightClick : MonoBehaviour
{
private float speed;
int i;

void Awake()
{
Messenger<float>.AddListener(GameEvent.SPEED_CHANGED, OnSpeedChanged);
}

void OnDestroy()
{
Messenger<float>.RemoveListener(GameEvent.SPEED_CHANGED, OnSpeedChanged);
}

private void OnSpeedChanged(float value)
{
speed = value;
}

void Update ()
{
if (Input.GetButtonDown ("Fire2"))
{
for(i=(int)speed; i > 0; i--)
{
Messenger.Broadcast(GameEvent.PRESS_BUTTON);
}
}
}
}

Awake( ) y OnDestroy( ) añadirán y eliminarán, respectivamente, un event listener como en el caso anterior, pero los métodos tendrán un valor esta vez (OnSpeedChanged). También añadimos el método OnSpeedChanged que igualará el valor de speed al valor obtenido del slider (incialmente 0). Por último, hemos modificado Update( ) para que llame al evento PRESS_BUTTON las veces que indique la variable speed.

Finalmente en SettingsPopup.cs cambiaremos el contenido del método OnSpeedValue( ), añadiéndole un broadcast para enviar el mensaje del evento SPEED_CHANGED.

public void OnSpeedValue(float speed)
{
Messenger<float>.Broadcast(GameEvent.SPEED_CHANGED, speed);
}

Si ahora corremos el juego, dependiendo del valor del slider (0, 1 ó 2) el marcador se actualizará con cada clic que hagamos (añadiendo 0 si la posición del slider es 0, 1 para 1 y 2 para 2).