Programación orientada a objetos

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

Clases y métodos

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

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

NavigatorVS

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

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

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

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

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

int myResult = MultiplyTwoNumbers(3, 5);

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

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

Hello!

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

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

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

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

Código y estructura

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

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

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

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

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

Objetos

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

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

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

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

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

Navigator_objects

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

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

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

...

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

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

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

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

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

Fields

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

Navigator_fields (1)

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

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

Construyendo una clase

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

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

Los métodos seguirán esta forma:

bob.ReceiveCash(25);

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

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

Guy

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

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

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

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

givetojoe

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

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

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


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

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


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

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

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

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

Diagrama de clases

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

Propiedades

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

Atributos

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

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

Por ejemplo:

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

Solo el nombre (name) es necesario.

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

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

Scores(only_fields)

Asociaciones

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

Para Scores sería:

Scores_asociaciones

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

solids_hierarchy_dia

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

Scores

Scores2

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

Diagrama_Scores_VS_sin_metodos

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

Test_class

Test_class2

Multiplicidad

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

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

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

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

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

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

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

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

ej_multiplicidad_atributos

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

ej_multiplicidad_asociaciones

Asociaciones bidireccionales

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

person_car_bidireccional

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

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

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

person_car_bidireccional_alternativo

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

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

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


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

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

Operaciones

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

La sintaxis completa de UML para operaciones es:

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

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

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

direction name : type = default value

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

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

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

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

Ejemplo para el caso de la clase Scores:

Diagrama_Scores_VS

Generalización

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

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

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

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

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

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

generalización

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

using UnityEngine;
using System.Collections;

public class Scores : MonoBehaviour {

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

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

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

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

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

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

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

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

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

esquema_global_clases

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.