GUI 2D en juego 3D

En este post hablaremos de cómo construir una GUI 2D en un juego 3D en Unity. La GUI hace referencia a la parte visual de la interfaz, como texto y botones. Este texto y botones se superponen a la imagen del propio juego, creando una especia de display denominado HUD (heads-up display).

Desde su primera versión, Unity viene con un sistema de modo inmediato de GUI, el cual hace fácil poner un botón en la pantalla que podemos clicar. Este método está basado únicamente en código. Simplemente tendremos que añadir un script con el siguiente código a uno de los objetos de la Scene (funcionamiento GUI.Button):

using UnityEngine;
using System.Collections;

public class BasicUI : MonoBehaviour
{
void OnGUI()
{
if (GUI.Button (new Rect (10, 10, 40, 20), «Test»))
{
Debug.Log («Test button»);
}
}
}

boton_GUI_inmediato

El núcleo del código en este listado está en el método OnGUI( ). Al igual que con Start( ) y Update( ), MonoBehaviour responde automáticamente a OnGUI( ). La función será llamada cada frame después de todo el renderizado. Este código dibujará un botón que responderá al hacer clic en él, devolviendo un mensaje en Console: «Test button». Con el modo inmediato es fácil crear botones en pantalla, sin embargo para todo lo demás (e incluso para botones) lo mejor es usar el método avanzado introducido en las versiones 4.6+ de Unity empleado desde el Editor. Esta UI trabajará en modo retenido, de modo que los gráficos son trazados una vez y dibujados entonces cada frame sin necesidad de estar continuamente redefiniéndolos. Al trabajar con este sistema desde el Editor tendremos dos ventajas sobre el modo inmediato: 1)podemos ver la apariencia que tendrá nuestra UI y 2)la UI puede ser personalizado con nuestras propias imágenes.

Importaremos ahora las imágenes que vayamos a usar en nuestra UI y cambiaremos su tipo de textura a Sprite (2D and UI).

sprite_type

Para crear nuestra UI tendremos que crear un Canvas, una especie de objeto especial que Unity usa como la UI del juego. Para ello vamos a GameObject>UI>Canvas. El objeto Canvas (que renombraremos como HUD) representa la extensión completa de la pantalla, y será muy grande debido a que escala con respecto a los píxeles de la pantalla (un pixel de la pantalla = un punto en la Scene). Al crear el Canvas también se creará un objeto EventSystem, requerido para la interacción del UI pero que podemos ignorar. En el ajuste de Render Mode del Canvas tendremos tres opciones:

  • Screen Space-Overlay: Renderiza la UI como gráficos 2D en la vista de la cámara (opción por defecto).
  • Screen Space-Camera: también renderiza la UI en la vista de la cámara, pero permite la rotación de sus elementos.
  • World Space: ubica el objeto Canvas en la Scene como si fuese parte de ella.

Marcando la opción de Pixel Perfect se ajustará la posición de las imágenes de manera automática.

Crearemos ahora una imagen, un texto y un botón para nuestro Canvas desde GameObject>UI. Estos elementos de nuestra UI tienen que ser hijos del Canvas (Unity lo hará por defecto). El botón tendrá un elemento de texto que eliminaremos para nuestro ejemplo. Colocaremos de manera aproximada por ahora la imagen en la esquina superior izquierda, seguida por el texto, y en la esquina superior derecha el botón.

canvas_posicion_aprox

En la imagen podemos seleccionar la imagen a usar para este elemento abriendo la ventana con los sprites disponibles en nuestro proyecto dándole al botón que aparece a la derecha del recuadro de Source Image o bien arrastrando a ese mismo recuadro el sprite que deseemos asignarle.

canvas_seleccionar_img

Si seleccionamos la opción Set Native Size la imagen tomará las medidas originales del sprite. Del mismo modo asignamos otro sprite al botón.

Pasemos ahora al texto. En el recuadro Text escribiremos el texto que queremos que muestre («Texto de prueba», por ejemplo). Podremos modificar la fuente de la letra, así como su tamaño y estilo. También podemos seleccionar el tipo de alineación que tendrá el texto dentro del recuadro que lo contiene.

canvas_texto

Por último, miraremos cómo controlar la posición de los elementos de la UI. Todos los objetos de la UI tienen un anchor (ancla), mostrado en el Editor como objetivo X. El anchor de un objeto es el punto donde el objeto se une al canvas o a la pantalla, determina con respecto a qué es medida su posición. El anchor es definido como el «centro de la pantalla» y el anchor se mantendrá centrado aun cuando la pantalla cambie de tamaño. Si, por ejemplo, ajustamos el canvas al borde derecho de la pantalla, el objeto se mantendrá fijo al borde derecho aun si se cambia el tamaño de la pantalla. Por defecto, el anchor se situará en el centro de la pantalla, pero podemos modificarlo manualmente o ajustándolo a uno de los valores de los presets.

anchor

Los anchor points pueden ajustar tanto la posición como la escala (si se sitúan más allá de la posición del objeto).

anchor_escala1 anchor_escala2

Enums and collections

A la hora de trabajar con nuestros datos necesitaremos organizarlos de algún modo. Para ello usaremos collections, que nos permitirán almacenar, ordenar y gestionar esos datos.

Supongamos que tenemos varias abejas obreras, representadas por la clase Worker. Ahora deseamos definir los trabajos permitidos en esta clase. Para esto usaremos un enum, un tipo de datos que permite ciertos valores para esa parte de datos. Lo definiremos del siguiente modo:

enum Job
{
NectarCollector,
StingPatrol,
HiveMaintenance,
BabyBeeTutoring,
EggCare,
HoneyManufacturing,
}

El nombre del enum será Job. Cada uno de sus elementos (enumerators) será uno de los valores permitidos para Job y estarán dentro de unas llaves y separados entre sí por comas. La coma del último elemento podría eliminarse.

Ahora, siempre y cuando el constructor de Worker acepte Workers.Jobs como su parámetro type, podemos hacer referencia con types como este:

Worker nanny = new Worker (Job.EggCare);

Con Job accedemos al Enum y con .EggCare accedemos al valor que deseamos dentro del enum Job. No podremos crear un nuevo valor para el enum.

Podemos asignar números a cada uno de los valores dentro del enum. A continuación se muestra con el enum TrickScore que almacenará las puntuaciones para los trucos de una competición de perros:

enum TrickScore
{
Sit = 7,
Beg = 25,
RollOver = 50,
Fetch = 10,
ComeHere = 5,
Speak = 30,
}

No tiene que haber un orden específico e incluso pueden asignarse varios nombres a un mismo número.

Aquí está un extracto de un método para usar TrickScore enum invocándolo a y desde un int.

int value = (int)TrickScore.Fetch * 3;
Debug.Log (value.ToString());
TrickScore score = (TrickScore)value;
Debug.Log (score.ToString ());

Puesto que Fetch tiene un valor de 10, se asignará 30 a int value. Luego se muestra en la Console (dentro de Unity) el valor de int value (30). A continuación invocamos un int de vuelta a TrickScore. Puesto que value es 30, TrickScore score toma el valor TrickScore.Speak. De modo que mostrando su valor en Console aparecerá «Speak».

TrickScore

Podemos invocar el enum como un número y hacer cálculos con él, o podemos usar el método ToString( ) para tratar el nombre como un string. Si no asignamos un número a los nombres, los elementos de la lista tomarán valores por defecto: primer elemento asignado a 0, segundo elemento asignado a 1, tercer elemento asignado a 2, etc.

En el siguiente ejemplo crearemos la clase Card. Esta clase Card tendrá dos propiedades públicas: Suit (el palo de la baraja: picas, tréboles, diamantes o corazones) y Value (Ace, two, three…ten, Jack, Queen, King). También tendrá una propiedad de sólo lectura Name (ejemplos de cómo será: Ace of Spades, Five of Diamonds). Para definir los valores de Suit y Value crearemos dos enum (Suits y Values). Por último se creará un objeto de la clase Card (card) pasándole como parámetros su Suit y Value correspondientes (tal como aparece en el constructor) y se mostrará en Console su Name. Para estos parámetros se usará la función Range de la clase Random, que tomará un valor aleatorio dentro del rango que le pasemos en los argumentos.

enum Suits
{
Spades,
Clubs,
Diamonds,
Hearts
}


enum Values
{
Ace = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6,
Seven = 7,
Eight = 8,
Nine = 9,
Ten = 10,
Jack = 11,
Queen = 12,
King = 13
}


class Card
{
public Suits Suit { get; set; }
public Values Value { get; set; }

public Card(Suits suit, Values value)
{
this.Suit = suit;
this.Value = value;
}

public string Name
{
get { return Value.ToString () + " of " + Suit.ToString (); }
}
}

void Start()
{
Card card = new Card ((Suits)Random.Range (0, 4), (Values)Random.Range (1, 14));
Debug.Log (card.Name);
}

Si quisiésemos crear una clase para representar la baraja de cartas, necesitaríamos que hiciese un seguimiento de todas las cartas y conocer el orden en el que están. Podría usarse un array de Card, donde la carta de arriba estaría en la posición 0 del array, la siguiente en el 1, etc.

class Deck
{
private Card[] cards =
{
new Card (Suits.Spades, Values.Ace),
new Card (Suits.Spades, Values.Two),
new Card (Suits.Spades, Values.Three),
// ...
new Card (Suits.Diamonds, Values.Queen),
new Card (Suits.Diamonds, Values.King),
};

public void PrintCards()
{
for (int i = 0; i < cards.Length; i++)
{
Debug.Log (cards[i].Name);
}
}
}

Sin embargo, con este array no podemos cambiar el orden o añadir/eliminar cartas de la baraja fácilmente. Un array está bien para almacenar una lista fija de valores o referencias. Pero una vez que necesitas cambiar de posición o añadir más elementos de los que el array puede almacenar, empiezan los problemas.

  • Todo array tiene una longitud y necesitamos conocer dicha longitud para trabajar con él. Podríamos usar refenrencias null para mantener elementos del array vacíos. En el siguiente ejemplo se muestra un array de longitud 7 que almacena 3 cartas e indexa las posiciones 3,4,5 y 6 a null (por lo que no almacenarán nada).

array1

  • Necesitaríamos hacer un seguimiento de cuántas cartas están siendo almacenadas. Por lo que necesitaríamos un int topCard que indicase el índice de la última carta en el array. Siguiendo el ejemplo anterior la longintud sería 7, mientras que topCard sería igual a 3. Todo índice por encima de topCard tendrá como referencia null.
  • Ahora las cosas se complican. Es fácil añadir un método Peek() para devolver la referencia a la última carta, de manera que podamos mirar en el tope de la baraja. Si queremos añadir una carta y topCard es menor que la longitud del array, podemos simplemente almacenar la carta en el índice de topCard y añadirle 1 a topCard. Pero si el array está lleno necesitaremos crear un nuevo array de mayor tamaño o redimensionar el que ya tenemos (método Array.Resize( ) en .NETFramework). Eliminar una carta es fácil si se trata de la última carta, sólo tendremos que eliminarla del array, poner una referencia a null en su posición y decrementar topCard. Sin embargo, si la carta que deseamos eliminar está en medio del array tendremos que reordenar todo el array corriendo los valores de la derecha a la izquierda.

Para manejar estos inconvenientes de añadir y eliminar elementos de un array tenemos .NET Framewortk, que tiene clases con colecciones para este fin. La colección más común es List<T>. Una vez creado un objeto List<T>, es fácil añadir, eliminar, observar o mover un elemento de la lista. En Unity deberemos incluir using System.Collections.Generic; . La lista funcionará del siguiente modo:

  • Primero crearemos una nueva instancia de List<T> (T será sustituido por el tipo de la lista).

Todo array tiene un tipo (int, Card, etc.). Lo mismo con las listas. Deberemos especificar su tipo entre los símbolos < > .

List<Card> cards = new List<Card> ();

  • Ahora podemos añadir a nuestra List<T>.

Una vez que tenemos un objeto List<T>, podemos añadir tantos elementos como queramos (siempre que sean asignables a su tipo). Para ello se usará el método Add( ). La lista mantendrá sus elementos en orden, como un array.

cards.Add (new Card (Suits.Diamonds, Values.King));
cards.Add (new Card (Suits.Clubs, Values.Three));
cards.Add (new Card (Suits.Hearts, Values.Ace));

Podríamos mostrar por Console esta list mediante:

for(int i = 0; i < 3; i++)
{
Debug.Log (cards[i].Name);
}

List1

Una lista será más flexible que un array. Ahora se mostrarán algunas de sus funcionalidades, para más información pulsa aquí:

  • Puedes hacer una.

List<Egg> myCarton = new List<Egg>();

Una nueva lista no contendrá nada.

  • Añadir algo a ella.

Egg x = new Egg();

myCarton.Add(x);

Ahora la lista se expande para almacenar el objeto Egg.

  • Añadir algo más a ella.

Egg y = new Egg();

myCarton.Add(y);

Se expandirá de nuevo para almacenar este segundo objeto Egg.

  • Averiguar cuántas cosas contiene.

int theSize = myCarton.Count;

  • Averiguar si contiene algo en particular.

bool isIn = myCarton.Contains(x);

Devolverá true si encuentra el objeto x dentro de myCarton.

  • Descubrir dónde está una cosa en concreto.

int idx = myCarton.IndexOf(y);

El índice para x sería 0 y para y sería 1.

  • Eliminar algo concreto dentro de ella.

myCarton.Remove(y);

Se eliminará y de la lista, por lo que myCarton ya sólo contendrá x.

Las listas encogen y crecen dinámicamente, por lo que no necesitarás saber su tamaño exacto a la hora de crearlas. A continuación se mostrará un ejemplo con unos cuantos métodos empleados para trabajar con listas.

class Shoe
{
public Style Style;
public string Color;
}


enum Style
{
Sneakers,
Loafers,
Sandals,
Flipflops,
Wingtips,
Clogs,
}

Esta será la clase Shoe y el enum Style que se usarán en el ejemplo.

void Start ()
{
List<Shoe> shoeCloset = new List<Shoe> ();
 // Declaración de la lista.

shoeCloset.Add (new Shoe ()
{ Style = Style.Sneakers, Color = "Black" });
 // Se pueden usar nuevas sentencias dentro del método List.Add( ).
shoeCloset.Add (new Shoe ()
{ Style = Style.Clogs, Color = "Brown" });
shoeCloset.Add (new Shoe ()
{ Style = Style.Wingtips, Color = "Black" });
shoeCloset.Add (new Shoe ()
{ Style = Style.Loafers, Color = "White" });
shoeCloset.Add (new Shoe ()
{ Style = Style.Loafers, Color = "Red" });
shoeCloset.Add (new Shoe ()
{ Style = Style.Sneakers, Color = "Green" });

int numberOfShoes = shoeCloset.Count;
// Devuelve el número total de objetos en la lista
foreach (Shoe shoe in shoeCloset) // El bucle foreach pasa por cada uno de los elementos Shoe de la lista shoeCloset.
{
shoe.Style = Style.Flipflops;
shoe.Color = "Orange";
}


shoeCloset.RemoveAt (4);
// El método Remove( ) eliminará el objeto; RemoveAt( ) eliminará el objeto del índice que le pasemos.

Shoe thirdShoe = shoeCloset [2];
Shoe secondShoe = shoeCloset [1];
shoeCloset.Clear();
 // El método Clear( ) elimina todos los objetos de la lista.

shoeCloset.Add (thirdShoe);
if (shoeCloset.Contains (secondShoe))
// El método Contains( ) devuelve true si el objeto está contenido en la lista y false de no ser así.
{
Debug.Log ("That's surprising.");
}
}

Capturas del código escrito usando MonoDevelop (recordad incluir System.Collections.Generics en vuestro código para poder trabajar con la clase List<T>):

ListOfShoes1

ListOfShoes2

Debemos recordar que los enums son types (tipos), mientras que las Lists (listas) son objetos. Una lista puede almacenar cualquier cosa (dependiendo del tipo de datos que le marquemos que va a almacenar) y cada elemento tendrá sus propiedades y métodos. Los enums tienen que ser asignados a uno de los tipos de valores de C#. Por otra parte, si tenemos un número fijo de elementos con los que vamos a trabajar que se mantendrá constante, podremos trabajar con arrays en vez de listas sin ningún problema. Usando ToArray( ) podemos pasar una lista a un array. Existe también un constructor para crear una lista a partir de un array.

Al inicializar una lista podemos pasarle unos objetos iniciales que serán añadidos a la lista con su creación.

List<Shoe> shoeCloset = new List<Shoe> ();
shoeCloset.Add (new Shoe () { Style = Style.Sneakers, Color = "Black" });
shoeCloset.Add (new Shoe () { Style = Style.Clogs, Color = "Brown" });
shoeCloset.Add (new Shoe () { Style = Style.Wingtips, Color = "Black" });
shoeCloset.Add (new Shoe () { Style = Style.Loafers, Color = "White" });
shoeCloset.Add (new Shoe () { Style = Style.Loafers, Color = "Red" });
shoeCloset.Add (new Shoe () { Style = Style.Sneakers, Color = "Green" });

En este código se crea la lista shoeCloset y posteriormente se le añaden mediante el método Add( ) varios objetos. Para inicializar la lista directamente con esos objetos haremos lo siguiente:

List<Shoe> shoeCloset = new List<Shoe> ()
{
new Shoe () { Style = Style.Sneakers, Color = "Black" },
new Shoe () { Style = Style.Clogs, Color = "Brown" },
new Shoe () { Style = Style.Wingtips, Color = "Black" },
new Shoe () { Style = Style.Loafers, Color = "White" },
new Shoe () { Style = Style.Loafers, Color = "Red" },
new Shoe () { Style = Style.Sneakers, Color = "Green" },
};

Creamos unas llaves donde incluiremos los elementos que se inicializarán con la lista. Seguirá el mismo esquema que en el anterior código pero sin la necesidad del método Add( ). Además, los elementos estarán separados por comas y al final de las llaves se finalizará con punto y coma. No estaremos limitados a usar únicamente new en el inicializador, podrán ser usadas variables también. Posteriormente se le podrán seguir añadiendo nuevos objetos a la lista.

En resumen, la clase List<T> representa una lista de objetos fuertemente tipados a la que se puede obtener acceso por índice. Proporciona métodos para buscar, ordenar y manipular listas. En el lugar de la T escribiremos el tipo de datos que contendrán los objetos de la lista (int, string, etc). Los métodos explicados anteriormente son los siguientes:

  • Add(T): añade un objeto al final de la lista.
  • Clear( ): elimina todos los objetos de la lista.
  • Count: devuelve el número de elementos que contiene la lista.
  • Contains(T): averigua si un objeto está en la lista.
  • IndexOf(T): busca un objeto y devuelve el índice del objeto en la lista.
  • Remove(T): se eliminará el objeto de la lista.
  • RemoveAt(int): se eliminará el objeto de la posición indicada.

Episodix: Integration Test & Unit Test – Iteración 1

A continuación se mostrarán los tests diseñados para la Iteración 1 de Episodix.

Integration Test

Se ha diseñado un Integration Test llamado Test_EndOfStreet que comprobará que FPSController (el personaje) y CheckEnd interactúan correctamente. Cuando FPSController atraviese CheckEnd, OnTriggerEnter lo detectará.

Para este test se ha creado una Scene que constará del suelo de Street, nuestro FPSController y el objeto CheckEnd. También habrá una cámara y una Directional Light. A FPSController se le ha desactivado el script FirstPersonController, encargado de mover al personaje en función de las entradas introducidas por el usuario mediante los controles de movimiento asignados, y se le ha añadido el script TestMove, que simplemente desplazará hacia adelante al personaje:

using UnityEngine;
using System.Collections;

public class TestMove : MonoBehaviour
{
// El FPSController del test se movera hacia adelante
//
void Update ()
{
transform.Translate (Vector3.forward*Time.deltaTime*10);
}
}

Al objeto CheckEnd se le añadirá un script CallTesting que dará por válido el test si otro objeto atraviesa (OnTriggerEnter) al propio CheckEnd.

IntegrationTest-CallTesting

El test tendrá una duración máxima de 5 segundos. Si en ese tiempo no se obtiene un resultado (válido o no) se tomará el test como fallido. En nuestro caso, al ejecutar el test, FPSController atravesará CheckEnd antes de que finalice la duración máxima de 5 segundos, por lo que el test tendrá un resultado positivo.

IntegrationTest-TimeOut

Unit Test

Se ha elaborado un Unit Test para comprobar que no hay errores a la hora de calcular los porcentajes de acierto en función de las respuestas acertadas y las respuestas totales contestadas para el caso de 3 preguntas. El código del Unit Test correspondiente estará en CalScoresTest.cs y su contenido es el siguiente:

using System;
using NUnit.Framework;

namespace CalScoresTest
{
public class CalScores
{
// Calcula el porcentaje de acierto dependiendo del numero de respuestas
// correctas y del numero de respuestas totales, parametros que se le
// pasaran como argumentos a la funcion.
//
public decimal GiveScores(decimal rightAnswers, decimal totalAnswers)
{
decimal scr = (rightAnswers / totalAnswers) * 100;
decimal scores = Math.Round(scr,2);
return scores;
}
}


public class TestScores
{
// Test que comprobara que los porcentajes calculados son los esperados
// dependiendo del numero de respuestas correctas y del numero de respuestas
// totales.
//
[Test]
public void Test()
{
CalScores scr = new CalScores();
Assert.AreEqual (100, scr.GiveScores (3, 3));
Assert.AreEqual (66.67M, scr.GiveScores (2, 3));
Assert.AreEqual (33.33M, scr.GiveScores (1, 3));
Assert.AreEqual (0, scr.GiveScores (0, 3));
}
}
}

Al ejecutar el test el resultado será positivo, de modo que el cálculo de los porcentajes de acierto realizado en nuestro juego es correcto.

UnitTest

Episodix: Iteración 1

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

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

  • Learning Phase: El usuario camina (al estilo FPS) por una calle vacía. En el lado derecho aparecen sólidos tridimensionales sencillos (cubo, esfera, cilindro).
  • 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.
  • Scores: Finalmente el juego presenta la puntuación obtenida. No se guarda en ningún lado.
Scene

La Scene iteration_1 es la única existente y se compondrá de los siguientes objetos:

  • Directional light: alumbrará nuestra Scene.
  • Street: será la calle vacía con los sólidos tridimensionales en su lado derecho. Además tendrá cuatro paredes invisibles para evitar que el jugador caiga al vacío. Se almacenará como prefab.

Scene-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.

Scene-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 (Question 1, Question 2, Question 3). Una vez finalizado el test podrá elegir si quiere reiniciar la prueba o salir del juego (Score_txt). 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.

Scene-Recall_Phase_pregunta

  • 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.
  • 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.

Scene-CheckEnd

  • Scores: objeto vacío que recogerá y almacenará las respuestas del usuario mediante un script asociado.
Scripts

En la Iteración 1 se hará uso de un total de cuatro 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 ();
}
}

Script-EndMenu

  • 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;
}
}
}

Script-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 +"%.";
}
}

Script-ShowScores

  • 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. El script estará asociado al objeto CheckEnd.

using UnityEngine;
using System.Collections;

public class StartRecallPhase : MonoBehaviour
{
public GameObject q1; // Almacenara el objeto de la primera pregunta del test.
public GameObject fpsc; // Almacenara el objeto del personaje.
public GameObject cam; // Almacenara el objeto de la camara de la Recall Phase.

// 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"))
{
q1.SetActive (true);
fpsc.SetActive (false);
cam.SetActive (true);
}
}
}

Script-StartRecallPhase

También debemos tener en cuenta que cuando hacemos clic en uno de los botones que saldrán en la pantalla, se llamará a ciertas funciones de los scripts para realizar su acción correspondiente. En los botones de respuesta Sí/No de las preguntas siguen siempre el mismo esquema.

  1. Cuando pulsamos uno de ellos se desactivará el objeto correspondiente a esa pregunta y se activará el objeto de la siguiente pregunta. En caso de ser la última pregunta activará el objeto que muestra el porcentaje de éxito de las respuestas del usuario.
  2. Lo siguiente que hará será llamar a la función CollectAnswer del script Scores, pasándole como parámetro el valor correspondiente de la respuesta (true para Sí, false para No), para recoger la respuesta del usuario a la pregunta.
  3. Por último, se llamará a la función AddScore, a la que se le pasará como parámetro el objeto de la Hierarchy por el cual se pregunta si ha visto el usuario.  La función comprobará si coincide la respuesta del usuario con la existencia en la Scene del objeto en cuestión (dependerá de si está activado o desactivado) y almacenará el resultado. Si es la última pregunta calculará el porcentaje de acierto final.

BotonSiPreguntaBotonNoPregunta

Cuando se muestra el porcentaje de acierto también aparecerán dos botones: «Reiniciar Test» y «Salir». El primero cargará la Scene de nuevo llamando a la función ResetLevel del script EndMenu para realizar la Iteración 1 de nuevo. El segundo cerrará el juego llamando a la función ExitGame del script EndMenu.

BotonRestartBotonExit

Coding conventions

A continuación se muestran las convenciones a tener en cuenta en el desarrollo del proyecto Episodix:

Uso de llaves

  • La llave de apertura debe situarse al principio de la línea después de la sentencia que inicia el bloque. A su contenido se le añadirá una sangría correspondiente a 1 Tab o 4 Espacios. Ejemplo:
if (someExpression)
{
   DoSomething();
}
  • En el caso de switch y sus ‘case’:
    switch (someExpression) 
    {
     
       case 0:
          DoSomething();
          break;
     
       case 1: 
          {
             int n = 1;
             DoAnotherThing(n);
          }
          break;
    }
  • Las llaves no deben ser opcionales. Deben ser usadas incluso en bloques de sentencias de una única línea.
    for (int i=0; i<100; i++) { DoSomething(i); }

Sentencias de una línea

  • Las sentencias de una línea pueden tener llaves que empiezan y terminan en la misma línea.
    public class Foo
    {
       int bar;
     
       public int Bar
       {
          get { return bar; }
          set { bar = value; }
       }
     
    }

Comentarios

  • Es aconsejable el uso de comentarios que describan el funcionamiento del código al que hacen referencia.

Estilo de los comentarios

  • El estilo de // (doble barra) será el usado en la mayoría de ocasiones, aunque también existe /*…*/ (para abarcar varias líneas). Siempre que sea posible, se situarán encima del código. Ejemplo:
    // This is required for Controller access for hit detection
    FPSController controller = hit.GetComponent<FPSController>();
  • Los comentarios también pueden ser situados al final de la línea si hay espacio suficiente.
    public class SomethingUseful 
    {
        private int          itemHash;            // instance member
        private static bool  hasDoneSomething;    // static member
    }
  • Lo comentarios empezarán por letra mayúscula.
  • Se insertará un espacio en blanco entre el comentario y  // .

Espaciado

El espaciado mejora la legibilidad mediante la disminución de la densidad del código.

  • Usar un único espaciado después de cada coma entre argumentos de una función.
  • No usar espacios después de los argumentos o paréntesis de funciones.
    Console.In.Read(myChar, 0, 1);
  • No usar espacios entre el nombre de una función y los paréntesis.
    CreateFoo()
  • No usar espacios dentro de los corchetes.
     x = dataArray[index];
  • Utilizar un único espacio antes de las sentencias de control de flujo.
    while (x == y)
  • Utilizar un espacio antes y después de los operadores de comparación.
    if (x == y)

Diseño

  • Escribir una única sentencia por línea.
  • Escribir una única declaración por línea.
  • Si las líneas de continuación no son automáticamente sangradas, añadir la sangría correspondiente mediante tab (o 4 espacios).
  • Añadir al menos una línea en blanco entre métodos de definición y propiedades de definición.
  • Usar paréntesis para hacer cláusulas en una expresión como la del siguiente ejemplo:
    if ((val1 > val2) && (val1 > val3))
    {
        // Take appropriate action.
    }
  • Al final de cada línea de código se finalizará con punto y coma (» ; «), excepto para la declaración de funciones y sentencias ( por ejemplo if ).
  • Las líneas de código que se encuentren dentro de una misma función, sentencia, etc, tendrán la misma sangría. Si existen otras función,sentencia, etc, las líneas de código correspondientes compartirán sangría, la cual será mayor o menor dependiendo de si forman parte de las anteriores o si las anteriores están dentro de ellas.

Naming

  • Se usará el inglés para los nombres de las clases, assets y demás elementos dentro del proyecto.
  • No se usarán prefijos con variables.
  • Se usará el formato camelCasing para las variables y parámetros.
  • Se usará el formato PascalCasing para funciones, propiedades, eventos y clases.
  • Se usará el prefijo «I» para los nombres de interfaces.

Organización de archivos

  • Los archivos fuente contienen un único public type, sin embargo pueden tener múltiples clases internas.
  • Los archivos fuente deben tener el nombre del public class en el archivo.
  • Clases miembro deben ser ordenadas alfabéticamente y agrupadas en secciones (Fields, Constructors, Properties, Events, Methods, Private interface implementations, Nested types).

Ejemplo

using System;
using UnityEngine;
 
public class MyClass : MonoBehavior
{
    // fields
    int foo;
 
    // properties
    public int Foo { get {} set {} }
 
    // methods
    void MyMethod(int number)
    {
        int value = number + 2;
        Debug.Log(value);
    }
}

La mayoría de estas convenciones las realizará el propio editor (MonoDevelop o VisualStudio) la primera vez que escribamos el código al finalizar cada línea. En caso de editar el código ya no será así, salvo que simulemos finalizar la línea otra vez (por ejemplo, borrar y reescribir el punto y coma de final de línea ).

Batería de pruebas

Batería de tests de aceptación para el trabajo a realizar:

1.- Test para comprobar que se presentan las preguntas cuando el personaje llega al final de la calle.

  • Given<personaje en la calle vacía con los sólidos tridimensionales (esfera, pirámide, cubo) en el lado derecho>
  • When<personaje llega al final de la calle (fin de la learning phase)>
  • Then<se presentan las preguntas para comprobar si el usuario recuerda qué figuras tridimensionales aparecían en la calle (comienzo de la recall phase)>

2.- Test para comprobar que se actualizan los puntos del usuario correctamente según su respuesta con respecto a la presencia de la pirámide.

  • Given<aparecía la pirámide en la leaning phase>
  • When<el usuario responde en la recall phase a la pregunta de si recuerda haber visto la pirámide>
  • Then<se actualiza correctamente la puntuación del usuario>

3.- Test para comprobar que se actualizan los puntos del usuario correctamente según su respuesta con respecto a la presencia de la esfera.

  • Given<aparecía la esfera en la leaning phase>
  • When<el usuario responde en la recall phase a la pregunta de si recuerda haber visto la esfera>
  • Then<se actualiza correctamente la puntuación del usuario>

4.- Test para comprobar que se actualizan los puntos del usuario correctamente según su respuesta con respecto a la presencia del cubo.

  • Given<aparecía el cubo en la leaning phase>
  • When<el usuario responde en la recall phase a la pregunta de si recuerda haber visto el cubo>
  • Then<se actualiza correctamente la puntuación del usuario>

5.- Test para comprobar que se presenta scores (los resultados) una vez el usuario ha contestado a todas las preguntas.

  • Given<se plantean las preguntas al usuario>
  • When<el usuario responde a todas las preguntas>
  • Then<se muestra la puntuación final obtenida por el usuario>

Unirse a un proyecto en Bitbucket

En este post explicaremos de manera breve cómo unirse a un grupo de trabajo de Bitbucket:

1.- Crear cuenta en Bitbucket.

2.- Facilitar la dirección de correo electrónico al administrador del grupo.

3.- Llegará una invitación por email o un aviso de que ahora pertenecéis al grupo.

4.- Conectarse a nuestra cuenta y aceptar la invitación (de ser necesario) dentro del propio Bitbucket.

Haciendo simplemente esto ya podréis entrar en un grupo de Bitbucket. Podéis ver los grupos a los que pertenecéis en la pestaña Teams.

grupos

Si en el grupo existen repositorios, podréis acceder a ellos. También podréis crear otros nuevos si disponéis de permisos de administrador (estos permisos dependerán del administrador que os haya invitado).

Git Flow

En este post se explicará el flujo de trabajo Git Flow que aplicaremos cuando usemos Git al trabajar en los proyectos con Unity. Este modelo de desarrollo que se mostrará a continuación no es más que un conjunto de procedimientos que todo miembro del equipo debe seguir en el proceso de desarrollo de un proyecto.

Empezaremos por establecer un sistema de reposiciones en el que consideraremos un repositorio central al que nos referiremos como origin. Cada desarrollador realiza operaciones de push y pull hacia origin. De igual forma puede realizar push y pull hacia otros miembros dentro de su subequipo. De este modo los miembros de ese subequipo pueden trabajar juntos sobre algo concreto antes de subirlo prematuramente a origin. Básicamente los miembros de ese subequipo se definen como Git remotos entre sí. En la siguiente imagen se describen tres subequipos (bob-alice, alice-david, david-clair) conectados entre sí además de con origin.

origin

El repositorio central tendrá dos ramas principales con tiempo de vida infinito:main-branches

  • master
  • develop

En la rama origin/master HEAD siempre estará en la última versión estable de nuestro código fuente. Por otra parte, en la rama origin/develop tendrá una versión de desarrollo con los últimos cambios de la próxima versión estable. Cuando el código fuente de develop alcanza un punto estable, estos cambios serán añadidos al código fuente en master y etiquetados con un número de versión. Se creará por lo tanto una nueva última versión en la rama master.

Junto con las ramas principales master y develop, se usarán ramas de apoyo para ayudar al desarrollo paralelo entre los miembros de equipo, facilitar el seguimiento de características, prepararse para nuevos lanzamientos y ayudar a solucionar rápidamente problemas de producción en vivo. Estas ramas de apoyo tendrán un tiempo de vida limitado y serán eliminadas llegado el momento.

Los diferentes tipos de ramas de apoyo que podríamos usar son:

feature_branch

  • Ramas de característica

Puede ramificarse o unirse a develop. Pueden tener cualquier nombre salvo master, develop, release-* o hotfix-*. Son usadas para el desarrollo de nuevas características de versiones futuras. A la hora de desarrollar una nueva característica podría no saberse la versión objetivo a la que será incorporada. Esta rama existirá mientras dure su desarrollo; hasta que se una a develop o sea descartada. Este tipo de ramas suele existir sólo en develop y no en origin.

Cuando empecemos a trabajar en una nueva característica se ramificará desde la rama develop.

git checkout -b myfeature develop
Cambiado a una nueva rama "myfeature"

Las características finalizadas podrán ser unidas a develop de manera definitiva añadiéndolas así a la próxima versión.

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557)
$ git push origin develop

La orden --no-ff causa que la unión siempre cree un nuevo objeto commit. Esto evita perder información de la existencia histórica de una rama de característica y agrupa todos los commits que juntos añaden una nueva característica. En la siguiente imagen podéis verlo de una manera gráfica:

feature_branch noff

En el segundo caso es imposible comprobar qué objetos commit forman la nueva característica. Revertir dicha característica en este caso sería muy difícil, mientra que usando --no-ff es muy sencillo.

  • Ramas de lanzamiento

Se ramifican desde develop y se unen a develop y master. Su nombre de rama será release-*. Este tipo de rama ayudará a la preparación de un nuevo lanzamiento. Permiten correcciones de bugs menores y preparación del meta-data para el lanzamiento (número de versión, fechas de elaboración, etc.). Haciendo este trabajo en la rama de lanzamiento, develop estará libre para recibir las nuevas características del próximo gran lanzamiento.

El momento clave para crear una rama de lanzamiento es cuando develop muestra interés en realizar un nuevo lanzamiento próximamente. Esto es cuando las características objetivo para ese lanzamiento están ya unidas a develop. En el instante que se crea una rama de lanzamiento se le asignará un número de versión al próximo lanzamiento. Hasta ese momento develop reflejaba los cambios para el «siguiente lanzamiento». Se seguiran las reglas del proyecto para la asignación del número de versión.

La rama de lanzamiento será creada desde la rama develop. Por ejemplo, tenemos la versión 1.1.5 actualmente y tenemos un gran lanzamiento próximo. El estado de develop es listo para «el siguiente lanzamiento» y decidimos será 1.2 (en vez de 1.1.6 o 2.0):

$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)

Después de crear una nueva rama y cambiar a ella, asignamos el número de versión. «bump-version.sh» es un script ficticio que cambia algunos archivos en la copia de trabajo para reflejar la nueva versión. Después esta versión es creada. La rama se mantendrá hasta que la nueva versión esté desplegada definitivamente. Durante ese tiempo, las correcciones de bugs pueden ser aplicadas en esta rama. No se podrán añadir nuevas características grandes, serán incluidas en develop o en el próximo gran lanzamiento.

Cunado el estado de la rama de lanzamiento está listo para un lanzamiento real, algunas acciones serán llevadas a cabo. Primero, la rama de lanzamiento se unirá a master (ya que todos los commit en master son por definición lanzamientos nuevos). A continuación, el commit en master debe ser etiquetado para futuras referencias de las versiones en el historial. Finalmente, los cambios realizados en la rama de lanzamiento deben ser añadidos de vuelta a develop, así los futuros lanzamientos también contendrán estas correcciones de bugs.

Los dos primeros pasos en Git:

$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2

El lanzamiento está ahora hecho y etiquetado para futuras referencias.

Para mantener los cambios realizados en la rama de lanzamiento, necesitamos añadirlos de vuelta en develop. En Git:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)

Este paso podría dar lugar a conflictos de unión.

Ahora la rama de lanzamiento puede ser eliminada, pues no la necesitamos más:

$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).

  • Ramas de revisiónhotfix-branches

Se ramifican desde master y se unen a develop y master. Su nombre de rama será hotfix-*. Se parecen a las ramas de lanzamiento puesto que también son para nuevos lanzamientos, aunque no planeados. Surgen de la necesidad de actuar inmediatamente debido a un estado indeseado en la versión actual. Cuando un bug crítico de la versión debe resolverse inmediatamente, una rama de revisión surgirá desde la etiqueta correspondiente en la rama master que marca la versión en cuestión. La idea es que el trabajo de los miembros del equipo pueda continuar, mientras otra persona prepara una corrección rápida.

La rama de revisión será creada desde la rama master. Por ejemplo, digamos que la versión 1.2 es el lanzamiento actual y causa problemas debido a un bug grave. Pero los cambios en develop son todavía inestables. Podríamos entonces crear una rama de revisión y empezar a solucionar el problema:

$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)

No os olvidéis de actualizar el número de versión después de crear la rama.

Luego corregid el bug y cread el commit o commits necesarios para la corrección.

$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)

Al acabar, la corrección debe unirse de vuelta a master, pero también a develop para asegurarse de que es incluida en el siguiente lanzamiento también. Es similar a como una rama de lanzamiento es finalizada.

Primero, se actualiza master y la etiqueta de lanzamiento:

$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2.1

Lo siguiente será incluir la corrección en develop también:

$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)

La única excepción a la regla es que, cuando existe una rama de lanzamiento, los cambios de la revisión deben ser añadidos a la rama de lanzamiento en lugar de a develop. La rama de lanzamiento acabará uniéndose a develop, por lo que la corrección también será añadida. Sin embargo, si la corrección es muy urgente también puede añadirse a develop además de a la rama de lanzamiento.

Finalmente eliminamos la rama:

$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).

Aunque no hay nada nuevo realmente impactante en este modelo de ramificación, la imagen global indica que el post con el que empezamos ha resultado ser tremendamente útil en nuestros proyectos. Forma un modelo elegante que es fácil de comprender y permite a los miembros del equipo desarrollar un entendimiento compartido de la ramificación y los procesos de lanzamiento.

Usando Git con Unity

En este post aprenderemos a usar Git con Unity para mantener un control de versiones de nuestros proyectos.

Debemos decidir cuál será nuestro flujo de trabajo, el cómo trabajaremos. Esto será especialmente importante si están varias personas trabajando en un mismo proyecto. Un buen flujo de trabajo sería el denominado Git Flow.

Tendremos también que elegir una Git GUI (interfaz gráfica de usuario) en caso de que deseemos utilizar alguna. Una opción altamente recomendada sería SourceTree. Podéis descargarla desde aquí y tenéis también un tutorial de uso. A mayores una vez instaléis la aplicación tendréis una pequeña guía paso a paso dentro del propio programa que se os mostrará la primera vez que lo ejecutéis.

Además habrá que crear un archivo .gitignore en nuestro directorio del proyecto de Unity que definirá los archivos y directorios que no se suben al repositorio de Git. Crearemos un archivo nuevo de texto que llamaremos gitignore y le agregaremos el siguiente contenido:

# =============== #
# Unity generated #
# =============== #
Temp/
Library/

# ===================================== #
# Visual Studio / MonoDevelop generated #
# ===================================== #
ExportedObj/
obj/
*.svd
*.userprefs
/*.csproj
*.pidb
*.suo
/*.sln
*.user
*.unityproj
*.booproj

# ============ #
# OS generated #
# ============ #
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

Ahora seleccionaremos la opción «Guardar como» y marcaremos la opción «todos los archivos». Le pondremos por nombre .gitignore y lo guardamos.

Por último nos queda configurar Unity. Vamos a Edit->Project Settings->Editor y ahí modificaremos dos opciones. La primera será cambiando en Version Control Mode el valor a Visible Meta Files. La segunda será cambiando en Asset Serialization Mode el valor a Force Text.

Unity Test Tools

En este post aprenderemos los conceptos básicos de cómo utilizar los assets de testeo de Unity. Una de las partes positivas de esta herramienta es que nos permite realizar los tests sin necesidad de tener un profundo conocimiento del código. Podéis encontrar Unity Test Tools en el assets store (https://www.assetstore.unity3d.com/en/#!/content/13802) e integrarlo en cualquier proyecto existente de Unity. Desde Unity tendréis que ir a Window->Asset Store.

utt

Se os abrirá una ventana con el Asset Store de Unity. En el buscador tendréis que poner el nombre de la herramienta «Unity Test Tools» y una vez dentro de él haced clic en el botón «Download» o «Import» si ya lo tenéis descargado. A continuación se os abrirá otra ventana para importar el asset a vuestro proyecto.

utt2

Ahora tendremos una carpeta en Project con el nombre UnityTestTools. Esta carpeta deberá ser eliminada, así como los componentes de este asset empleados, antes de exportar nuestro juego, ya que su función es únicamente de testeo. No obstante, según la documentación todo lo relacionado con el asset es desactivado cuando compilamos el juego. En dicha carpeta tendremos un enlace con toda la documentación de esta herramienta.

El primer punto que trataremos será el Assertion component. Este componente nos permitirá asegurar si algo es verdadero. Una vez importado el asset Unity Test Tools, este componente se podrá añadir a cualquier objeto en Add Components->Scripts->UnityTest->Assertion Component. Una vez configurado nos mostrará un error en caso de que la afirmación que hemos puesto no se cumpliese. Para facilitar el saber cuándo se produce dicho error podríamos activar Error Pause en Console. Esta opción pausará el juego en el momento en el que aparezca un error en la consola. Por ejemplo, imaginad que queremos asegurar que nuestro personaje se mantendrá siempre en una posición en el eje Y inferior a un valor igual a 4. En la siguiente imagen podéis observar la configuración pertinente para realizar la comprobación. Compararemos una variable tipo float en cada ciclo de Update con una frecuencia de 1 segundo. Dicha variable hará referencia al componente Transform.position.y del objeto Player y comprobará si es menor que un valor constante igual a 4.

assertion_component_1

Si ahora corremos nuestro juego y superamos esa altura con nuestro personaje, el juego se pausará y la consola mostrará un error advirtiendo de que el valor de Player.Transform.position.y ha superado el valor constante de 4 que habíamos marcado como límite.

assertion_component_2

En la documentación viene detallado el uso de Assertion Component (https://bitbucket.org/Unity-Technologies/unitytesttools/wiki/AssertionComponent) y en internet podréis encontrar más información todavía. Este complemento nos permite realizar pruebas sin necesidad de introducir código nuevo dentro de nuestros scripts que posteriormente tendríamos que eliminar.

El siguiente apartado será Integration Test. Este tipo de test comprobará que los objetos interaccionan entre ellos del modo que nosotros esperamos. Lo primero que debemos comprender es que los Integration Tests no se encontrarán en las Scenes de nuestro juego, sino que tendrán su propio «mundo». Cada Integration Test tendrá su propia Scene. Una vez creada una nueva Scene iremos al menú Unity Test Tools->Integration Test Runner (este menú se creará al importar el asset Unity Test Tools). Aparecerá una ventana nueva llamada Integration Tes que podremos integrar al igual que las ventanas Hierarchy, Inspector, Project, etc.

integration_test_runner

Crearemos un nuevo test con el botón Create. Una vez creado tendremos varias opciones. Seleccionaremos un nombre, las plataformas (windows, android, etc) incluídas en el test, cuanto tiempo debe durar (si dura más se considerará fallido), si debe ser ignorado, si deben ser ejecutadas todas las assertions o si habrá alguna excepción. Además se crearán dos objetos: un test runner y otro con el nombre de nuestro test (donde editamos las opciones anteriormente mencionadas).

integration_test_runner_2

Ahora crearemos dentro de nuestro test una esfera en la posición (0, 1.5, 0) y le añadiremos un rigidbody al que le cambiaremos el valor Drag a 20. Crearemos también un plano a partir de un cubo en la posición (0, 0, 0) al que le marcaremos la opción Is Trigger. Procederemos a continuación con un test muy sencillo. Si en un intervalo de tiempo de 3 segundos la esfera no impacta con el plano se superará el test, en caso contrario se considerará como fallido. Para ello crearemos dos Call Testing mediante Add Componet->Scripts->UnityTest->Call Testing. En el primero pondremos la condición para que el test sea un éxito. Le pondremos un valor de 3 segudos y que se dé como válido el test al cumplirse ese número de segundos (Call after seconds). En el segundo pondremos que si algo atraviesa (On trigger enter) el plano se de el test por fallido. Con estas condiciones si ejecutamos el test con clic derecho->Run se superará con éxito y pondrá un signo verde de aprobación al lado de su nombre.

test_pass

Si ahora modificamos el valor de Drag a 0 y ejecutamos el test, podremos observar que ahora el resultado será fallido y tendremos una marca roja en lugar de la verde para indicar que se ha fallado ese test.

test_fail

Existe la opción de realizar los test mediante un script que añadimos en el objeto del test. Esto nos da la posibilidad de realizar test realmente complejos de ser necesarios. Si en el script incluímos la línea IntegrationTest.Pass(gameObject); se dará el test por válido, mientras que con IntegrationTest.Pass(gameObject); se dará por fallido.

Crearemos otro integration test de ejemplo y dentro de él crearemos dos esferas a las que le añadiremos el componente Rigidbody. Si os fijáis, cuando tengáis seleccionado uno de los tests el resto de ellos no serán visibles en la escena. Debajo de la primera esfera crearemos un cubo en el que marcaremos la opción Is Trigger y le añadiremos el siguiente script llamado PowerUp:

using UnityEngine;
using System.Collections;

public class PowerUp : MonoBehaviour {

void OnTriggerEnter (Collider other)
{
var scale = other.transform.localScale;

other.transform.localScale = scale * 2;
}
}

Este script lo que hará es duplicar el tamaño de la primera esfera cuando atraviese al cubo. Ahora le añadiremos al cubo script de Assertion Component que comparará variables tipo float cuando la primera esfera entre en el cubo y se asegurará de si la componente Transform.localScale.x de esa esfera es mayor que la misma componente de la segunda esfera. Puesto que el script PowerUp duplicará el tamaño de la primera esfera cuando atraviese al cubo, el Assertion Component se debería cumplir.

assertion_integration_test_1

Iremos ahora al objeto del test y marcaremos la opción Succeed on assertion. De este modo el test será superado con éxito si se cumplen todas las assertions, mientras que será considerado fallido en el otro caso. Si ahora corremos el segundo test debería dar un resultado exitoso. Si le damos al botón Run All de la ventana de Integration Tes se ejecutarán todos los test que tengamos uno detrás de otro.

assertion_integration_test_2

Podemos incluír tantos test como queramos. Todos ellos se ejecutarán de manera independiente dando los resultados correspondientes de cada uno de ellos. Podéis comprobar los ejemplos incluidos en el asset Unity Test Tools que hay en la carpeta Examples.

El último apartado que trataremos será Unit Test. Este será como un Integration Test pero para probar nuestro código. Empezaremos yendo a Unity Test Tools->Unit Test Runner. Se nos abrirá la ventana Unit Test que podremos poner donde mejor consideremos al igual que hicimos con la ventana Integration Tes. Dentro de esta ventana existen diversos ejemplos ya hechos. Para crear nuestros propios Unit Tests necesitamos aprender NUnit. En la documentación existe más información al respecto (https://bitbucket.org/Unity-Technologies/unitytesttools/wiki/UnitTestsRunner).

unit_test

Os mostraré un ejemplo muy simple a continuación. Crearemos una carpeta a la que llamaremos Editor y dentro un Script al que llamaremos SampleTest. En dicho script borraremos todo y escribiremos el siguiente código:

using System;
using NUnit.Framework;

public class SampleTest
{
[Test]
public void SimpleAddition()
{
Assert.That (2 + 2 == 4);
}
}

Básicamente este test comprobará que 2 + 2 = 4. Obviamente será superado con éxito, pero lo comprobaremos. Creamos una escena nueva. En nuestra ventana Unit Tests tendremos ahora el que acabamos de crear, SampleTest. Si lo ejecutamos nos dirá que ha pasado favorablemente la prueba.

unit_test_2

Con esto acaban las nociones básicas del uso del asset Unity Test Tools, una herramienta muy útil para realizar cualquier tipo de prueba a nuestro juego de una manera más visual sin entrar tanto en el propio código. Recordad recurrir a la documentación para más información (https://bitbucket.org/Unity-Technologies/unitytesttools/wiki/Home).