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

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>