Introducción a C#
Scripting en Unity
- Incluso los juegos más básicos necesitan algo de código.
- Unity se encarga de todo lo “complicado”.
- Para cada GameObject permite incluir código de control (un script)
- Ejemplos de uso:
- Responder a los controles del jugador
- Definir el comportamiento de los objetos de juego
- Controlar eventos (por ejemplo, colisiones)
Incluso el juego más básico necesita algo de código. Unity se encarga de todo lo «complicado», pero lo que ocurre en el juego lo decidimos nosotros, y eso se hace programando.
En Unity el código se organiza en scripts: a cada GameObject (cada objeto del juego) se le puede añadir un script que lo controla. Con ellos podemos, por ejemplo, responder a los controles del jugador, definir cómo se comportan los objetos de juego o reaccionar a eventos como las colisiones.
Lenguajes soportados
- C#
- UnityScript (parecido a JavaScript) (obsoleto)
- Boo (parecido a python) (obsoleto)
Unity ha admitido tres lenguajes para escribir scripts: C#, UnityScript (parecido a JavaScript) y Boo (parecido a Python). Los dos últimos están obsoletos, así que el lenguaje que vamos a aprender en esta unidad es C#.
C#
- Es un lenguaje .NET
- Basado en C++, pero más sencillo de utilizar
- Recolector de basura automático (gestión de memoria más sencilla y segura)
- Es un estándar ISO → Microsoft → Mono → Xamarin → Microsoft
- Orientado a objetos
- Tipado fuerte
- Flexible, seguro, productivo, multiplataforma
C# es un lenguaje de la plataforma .NET. Está basado en C++, pero es más sencillo de utilizar: por ejemplo, cuenta con un recolector de basura automático, que hace la gestión de la memoria más sencilla y segura.
Es un lenguaje orientado a objetos y de tipado fuerte: cada dato tiene un tipo, y el compilador comprueba que se usa de forma coherente con él. Además es flexible, seguro, productivo y multiplataforma.
C# nació en Microsoft, pero la definición del lenguaje se convirtió en un estándar internacional: primero de Ecma (2001) y después de ISO (2003). Lo que se estandarizó fue el lenguaje, no la implementación de Microsoft. A partir de esa definición se creó Mono, una implementación libre y multiplataforma que Miguel de Icaza puso en marcha en 2001 desde la empresa Ximian. En 2011, él mismo fundó Xamarin, donde el equipo original siguió desarrollando Mono y lo llevó a los móviles: con Xamarin se podían crear en C# aplicaciones para iOS y Android. Finalmente, en 2016, Microsoft compró Xamarin y, con ello, respaldó oficialmente esa implementación.
Esta historia nos toca de cerca: Unity utiliza su propia versión de Mono para ejecutar los scripts.
POO
- C# es un lenguaje orientado a objetos (paradigma POO)
- Modela los problemas de programación como objetos de la vida real
- Un objeto tiene propiedades y métodos para modificar las propiedades
- Ejemplo: RadioFM
- Concepto de clase (modelo del objeto), e instancias de una clase (objetos que siguen el modelo)
C# sigue el paradigma de la programación orientada a objetos (POO), que consiste en modelar los problemas de programación como si fueran objetos de la vida real. Un objeto tiene propiedades (sus datos) y métodos (las acciones que consultan o modifican esas propiedades).
Piensa en una radio FM: tiene propiedades, como la emisora sintonizada o el volumen, y métodos, como cambiar de emisora o subir el volumen.
Para crear objetos necesitamos antes una clase, que es el modelo del objeto. Cada objeto que sigue ese modelo es una instancia de la clase: con un mismo modelo de radio se pueden fabricar muchas radios, cada una con su emisora y su volumen.
Importancia de la sintaxis
- Error de sintaxis → No compila
- Llaves, paréntesis, comillas, ...ámbitos. Cada apertura debe tener su cierre
- Todas las sentencias acaban con
; - Formas de comentar el código (
//y/* */)
El compilador es muy estricto con la forma de escribir el código: basta un error de sintaxis para que el programa no compile y, por tanto, no se pueda ejecutar. Al empezar, casi todos los errores vienen de tres despistes.
El primero, olvidar un cierre: cada llave, paréntesis o comilla que se abre debe cerrarse. Las llaves, además, marcan dónde empieza y dónde acaba cada ámbito (una clase, un método, un if…). El segundo, olvidar el punto y coma con el que acaban las sentencias.
El tercero tiene que ver con los comentarios, que son texto para las personas y que el compilador ignora. Hay dos formas de escribirlos: // convierte en comentario el resto de la línea, y /* */ todo lo que quede entre la apertura y el cierre, aunque ocupe varias líneas.
Coding Standards
- Indentación
- Variables en
camelCase(empiezan en minúscula):numeroDeVidas - Métodos, clases y propiedades en
PascalCase(empiezan en mayúscula):CrearEnemigo - Poner nombres con sentido (nada de
variable1, niaux, nikk) - Utilizar alfabeto incluido en ASCII (sin ñ, sin acentos, diéresis, etc.)
- Refactorizar cuando podamos
Además de las reglas que impone el lenguaje, hay normas de estilo (coding standards) que el compilador no exige, pero que hacen el código mucho más fácil de leer, tanto para ti como para quien trabaje contigo.
La primera es la indentación: el contenido de cada bloque se escribe un nivel más a la derecha que el bloque que lo contiene, de modo que la estructura del programa se ve de un vistazo.
La segunda es la forma de escribir los nombres. Las variables van en camelCase: empiezan en minúscula y cada palabra siguiente, en mayúscula, como numeroDeVidas. Los métodos, las clases y las propiedades van en PascalCase, igual pero empezando en mayúscula, como CrearEnemigo. En todos los casos, el nombre debe explicar para qué sirve aquello que nombra (nada de variable1, aux o kk) y usar solo caracteres ASCII: sin ñ, sin acentos y sin diéresis.
Por último, conviene refactorizar cuando podamos: revisar el código que ya funciona para dejarlo más claro y ordenado.
Variables (y constantes)
- Crean un espacio en memoria para guardar un dato (un número, un texto, un objeto…).
- Pueden ser de cualquier tipo (básico o compuesto).
- Deben ser declaradas, con o sin asignación inicial:
Una variable crea un espacio en memoria para guardar un dato: un número, un texto, un objeto… Tiene un nombre, con el que nos referimos a ella, y un tipo, que indica qué clase de dato guarda. Ese tipo puede ser básico o compuesto, como veremos enseguida.
Antes de usar una variable hay que declararla, escribiendo su tipo y su nombre. En la misma línea se le puede dar, si queremos, un valor inicial:
// [tipo] nombre;
int numeroDeVidas;
// [tipo] nombre = valor;
int numeroDeVidas = 3;
- Para convertir en constante: añadir
constantes de la declaración
Si el dato no va a cambiar nunca, la variable se puede convertir en una constante añadiendo const antes de la declaración.
Tipos de datos básicos (más comunes)
Los tipos básicos guardan un único valor sencillo, como un número o una letra. Estos son los más comunes:
| Tipo | Qué guarda | Ejemplo |
|---|---|---|
int |
Números enteros | int vidas = 5; |
long |
Números enteros más grandes | long puntos = 3000000000L; |
float |
Números reales (los más usados en Unity) | float velocidad = 1.5f; |
double |
Números reales con más precisión | double pi = 3.14159265; |
bool |
Verdadero o falso | bool vivo = true; |
char |
Un único carácter, entre comillas simples | char letra = 'A'; |
Tipos de datos compuestos
string- Array
List- Clases propias
- Algunas clases de Unity:
Vector3GameObjectTransform,Rigidbody,Collidery otros componentesTexture,Materialo cualquier otro tipo de asset (recurso)
Los tipos compuestos se construyen a partir de otros. Algunos vienen con el lenguaje: las cadenas de texto (string), los arrays y las listas (List), que veremos más adelante. Otros los crearemos nosotros, definiendo nuestras propias clases.
Unity aporta además muchos tipos que usaremos constantemente: Vector3, GameObject, los componentes (Transform, Rigidbody, Collider…) y los distintos tipos de asset o recurso (Texture, Material…).
Inicialización de variables
- Enteros:
int vidas = 5;
- Números reales:
- Separamos los decimales con
. - En un
floatañadimos unafdespués del número (sin ella, no compila) float velocidad = 1.5f;
- Separamos los decimales con
- Cadenas de texto:
- Siempre entre comillas
string saludo = "It's me, Mario!";
Al dar un valor a una variable hay que escribirlo de la forma que corresponde a su tipo.
Los enteros se escriben tal cual: int vidas = 5;.
En los números reales, los decimales se separan con un punto, no con una coma. Si la variable es de tipo float, hay que añadir además una f después del número, como en float velocidad = 1.5f;. Sin ella, C# entiende que el número es un double y el código no compila.
Las cadenas de texto van siempre entre comillas dobles: string saludo = "It's me, Mario!";.
Variables. Ámbito de uso.
- Por defecto una variable sólo está presente en el ámbito donde ha sido creada
- Dentro de una clase (variable miembro)
- Dentro de un método o función (variable local)
- Dentro de un bloque
{ }, como unifo un bucle
- Una variable local sólo se puede utilizar después de haber sido declarada.
Una variable no existe en todo el programa: sólo está presente en el ámbito donde ha sido creada. Hay tres casos:
- Si se declara dentro de una clase, es una variable miembro y se puede usar en toda la clase.
- Si se declara dentro de un método o función, es una variable local y sólo existe dentro de ese método.
- Si se declara dentro de un bloque entre llaves, como el de un
ifo un bucle, deja de existir en cuanto se cierra la llave.
Además, una variable local sólo se puede utilizar después de la línea en la que se declara.
Variables. Visibilidad.
- En una clase, podemos modificar la visibilidad de sus variables (o constantes):
private: Es la visibilidad por defecto. Sólo se puede usar dentro de la misma clase o struct (parecido a una clase, pero de tipo valor: se copia al asignarlo).public: Se puede usar en cualquier ámbito, incluso desde otras librerías (assemblies).protected: Como la privada, pero además da acceso a las subclases (clases que heredan de una clase).internal: Como pública, pero sólo dentro de la misma librería.- Tabla resumen de Microsoft
Propiedades
- Un campo es una variable de una clase: guarda un dato
- Una propiedad se usa desde fuera como un campo, pero ejecuta código al leerla (
get) o al asignarle un valor (set) - Permite controlar el acceso al dato: por ejemplo, validarlo
Las variables que se declaran en una clase se llaman campos: cada uno guarda un dato. Una propiedad es algo distinto: desde fuera se usa igual que un campo, pero en realidad ejecuta código cada vez que se lee (el bloque get) o cada vez que se le asigna un valor (el bloque set). Eso permite controlar el acceso al dato; por ejemplo, validarlo antes de guardarlo.
private int vidas = 3; // campo: guarda el dato
public int Vidas // propiedad: controla el acceso
{
get { return vidas; }
set { if (value >= 0) vidas = value; }
}
valuees el valor que se intenta asignar
En este ejemplo, el dato se guarda en el campo privado vidas, al que no se puede acceder desde fuera de la clase. La propiedad pública Vidas es la puerta de entrada: get devuelve el valor del campo y set sólo lo cambia si el valor nuevo no es negativo. Dentro de set, la palabra value representa el valor que se intenta asignar.
Propiedades. Forma abreviada y uso.
- Si no hace falta código propio, C# crea el campo por nosotros (propiedad automática):
Muchas veces no hace falta ningún código especial en get y set. Para esos casos existe una forma abreviada, la propiedad automática, en la que C# crea el campo por nosotros:
public int Puntos { get; set; }
public int Nivel { get; private set; } = 1; // sólo se modifica desde la clase
- Se usan igual que una variable:
Puntos se puede leer y modificar desde cualquier sitio. Nivel empieza valiendo 1 y, al tener private set, se puede leer desde fuera, pero sólo se puede modificar desde dentro de la clase.
Las propiedades se usan igual que una variable:
Vidas = 5; // ejecuta set
Vidas = -1; // set no lo acepta: sigue valiendo 5
Debug.Log(Vidas); // ejecuta get y muestra 5
- ¡Ojo! El editor de Unity muestra los campos públicos, pero no las propiedades
Un detalle importante en Unity: el editor muestra los campos públicos de un script, pero no sus propiedades.
Operadores
- Calcular valores a partir de otros
- Ejemplos:
Los operadores sirven para calcular valores a partir de otros. Por ejemplo:
a = a + b;
a += b;
c = a * 6;
La primera línea suma a y b y guarda el resultado en a. La segunda hace exactamente lo mismo, con la forma abreviada +=. La tercera multiplica a por 6 y guarda el resultado en c.
Conversiones
- Implícitas: ocurren solas al asignar un valor a una variable de un tipo más amplio (por ejemplo, de
intafloat).
A veces necesitamos pasar un valor de un tipo a otro. Las conversiones implícitas ocurren solas, sin escribir nada especial, cuando asignamos un valor a una variable de un tipo más amplio; por ejemplo, de int a float:
float velocidad = 5; // el entero 5 se convierte en 5.0f
- Explícitas: se fuerza la conversión a un tipo específico. Son obligatorias en sentido contrario (de
floataint): sin ellas, el código no compila.
En sentido contrario, de float a int, C# no convierte por su cuenta. Hay que pedirlo con una conversión explícita, escribiendo delante del valor, entre paréntesis, el tipo al que queremos convertirlo. Sin ella, el código no compila.
int resultado = (int)3.7f; // 3: se pierde la parte decimal
- También hay métodos específicos para convertir:
Fíjate en que esta conversión no redondea: descarta la parte decimal.
También hay métodos específicos para convertir. Convert.ToInt32 sí redondea al entero más cercano, e Int32.Parse convierte en número un texto que contiene cifras:
Convert.ToInt32(3.7f); // 4: redondea al entero más cercano
Int32.Parse("52"); // 52: convierte un texto en número
Para hacer los primeros ejercicios
- Ejercicios de C# · Parte 1
- Crear un proyecto "Universal 2D" en Unity
- Para los datos de entrada: declarar variables locales a la función
- Por ejemplo:
int numero = 5;
- Por ejemplo:
- Para mostrar un dato:
Debug.Log- Por ejemplo:
Debug.Log("Hola mundo");
- Por ejemplo:
Con lo visto hasta aquí ya puedes hacer la primera tanda: Ejercicios de C# · Parte 1.
Para hacerlos, crea en Unity un proyecto con la plantilla «Universal 2D». Los datos de entrada de cada ejercicio se declaran como variables locales dentro de la función, por ejemplo int numero = 5;, y los resultados se muestran con Debug.Log, por ejemplo Debug.Log("Hola mundo");.
Control de flujo
- Para tomar decisiones, necesitamos instrucciones de control de flujo
- Ej. Si Mario alcanza las 100 monedas → sumar una vida
- También podemos utilizar instrucciones para repetir una tarea
- Ej. Para crear 10 enemigos:
- Realizar 10 veces: Crear enemigo
- Ej. Para crear 10 enemigos:
- En ambos casos, necesitamos operadores relacionales y lógicos
- Sirven para determinar si se cumple una condición
Hasta ahora, nuestros programas ejecutan sus instrucciones una detrás de otra, siempre de la misma forma. Para que un juego reaccione a lo que ocurre necesitamos instrucciones de control de flujo, que son de dos tipos.
Las primeras sirven para tomar decisiones: si Mario alcanza las 100 monedas, se le suma una vida. Las segundas, para repetir una tarea: para crear 10 enemigos no escribimos diez veces la misma instrucción, sino que pedimos que «crear enemigo» se realice 10 veces.
En los dos casos hay que determinar si se cumple una condición, y para eso sirven los operadores relacionales y los lógicos.
Operadores relacionales
Los operadores relacionales comparan dos valores. El resultado de la comparación es un valor de tipo bool: true si se cumple y false si no.
| Operador | Nombre | Resultado |
|---|---|---|
a > b |
mayor | true si a es mayor que b |
a >= b |
mayor o igual | true si a es mayor o igual que b |
a < b |
menor | true si a es menor que b |
a <= b |
menor o igual | true si a es menor o igual que b |
a == b |
igual | true si a es igual a b |
a != b |
distinto | true si a es distinto de b |
Cuidado con no confundir ==, que compara dos valores, con =, que asigna un valor a una variable.
Operadores lógicos
Los operadores lógicos combinan varias condiciones en una sola. Los dos más usados son && («y») y || («o»). La tabla muestra el resultado para cada combinación de a y b:
a | b | a && b (y) | a || b (o) |
|---|---|---|---|
| true | true | true | true |
| true | false | false | true |
| false | true | false | true |
| false | false | false | false |
&&es true solo si los dos son true;||es true si al menos uno lo es.
Es decir, a && b sólo es true si se cumplen las dos condiciones, mientras que a a || b le basta con que se cumpla al menos una.
Condicional básico: if
La instrucción if ejecuta una instrucción sólo si se cumple una condición, que se escribe entre paréntesis. Con else podemos indicar, además, qué hacer cuando no se cumple:
public int numeroDeVidas = 5;
void Start() {
if (numeroDeVidas > 0) Debug.Log("Estoy vivo");
else Debug.Log("Estoy muerto");
}
- El editor de Unity puede modificar el valor de las variables públicas de un script (DEMO)
- Modo abreviado de if-else: operador ternario:
En este ejemplo, si numeroDeVidas es mayor que 0 se muestra «Estoy vivo» y, en caso contrario, «Estoy muerto». Como numeroDeVidas es una variable pública, su valor se puede modificar desde el editor de Unity, sin tocar el código.
Cuando el if-else sólo sirve para elegir entre dos valores, existe una forma abreviada, el operador ternario: se escribe la condición, un ?, el valor que se usa si se cumple, : y el valor que se usa si no se cumple.
Debug.Log(numeroDeVidas > 0 ? "Estoy vivo" : "Estoy muerto");
Condicionales anidados
Detrás de un else se puede poner otro if, y encadenar así varias comprobaciones. Se evalúan en orden y sólo se ejecuta la primera que se cumple:
public int velCoche;
public int velMaxima = 120;
public int velMinima = 60;
void Update() {
if (velCoche > velMaxima) Debug.Log("Velocidad máxima superada");
else if (velCoche < velMinima) Debug.Log("No se alcanza la velocidad mínima");
else Debug.Log("Tu velocidad es correcta");
}
Aquí se comprueba primero si el coche supera la velocidad máxima. Sólo si no es así, se comprueba si va por debajo de la mínima. Y si tampoco, es que su velocidad es correcta.
Condiciones complejas
- Usando operadores lógicos podemos evaluar expresiones más complejas
Con los operadores lógicos podemos evaluar condiciones más complejas, combinando varias comparaciones en un mismo if:
public int num1, num2, num3;
void Start() {
if (num1 > num2 && num1 > num3) Debug.Log("El número mayor es " + num1);
if (num1 < num2 || num1 < num3) Debug.Log(num1 + " puede ser el número menor");
if (num1 == num2 && num1 == num3) Debug.Log("Los tres números son iguales");
}
El primer if sólo se cumple si num1 es mayor que los otros dos a la vez (&&). Al segundo le basta con que sea menor que uno de ellos (||). El tercero comprueba que los tres son iguales.
Fíjate también en que, al mostrar los mensajes, el operador + une un texto con un número.
Control de casos: switch-case
- Permite ejecutar instrucciones distintas según el valor de la condición (“el caso”)
Cuando hay que hacer cosas distintas según el valor de una misma variable, la instrucción switch resulta más cómoda que encadenar muchos if: compara el valor con cada uno de los casos (case) y ejecuta las instrucciones del que coincide.
public int playerNumber = 1;
void Start() {
switch (playerNumber) {
case 1:
Debug.Log("Bienvenido jugador 1");
break;
case 2:
Debug.Log("Bienvenido jugador 2");
break;
default:
Debug.Log("Error, este juego sólo permite dos jugadores");
break;
}
}
Si playerNumber vale 1, se da la bienvenida al jugador 1; si vale 2, al jugador 2. El caso default recoge cualquier otro valor.
Particularidad de switch-case
- Cada caso debe terminar con
break(oreturn): si falta, el código no compila - En C#, la ejecución no puede «caer» de un caso al siguiente
- Sí se pueden agrupar varios casos que comparten las mismas instrucciones:
En C#, cada caso debe terminar con break (o con return): si falta, el código no compila. La ejecución no puede «caer» de un caso al siguiente.
Lo que sí se puede hacer es agrupar varios casos que comparten las mismas instrucciones, escribiendo sus etiquetas seguidas:
switch (dificultad) {
case 0:
case 1:
CrearEnemigo();
break;
case 2:
CrearEnemigo();
CrearEnemigo();
break;
}
Con dificultad 0 o 1 se crea un enemigo; con dificultad 2, dos.
Bucles. for
- Los bucles nos permiten repetir una tarea mientras se cumple una condición
- Bucle
for:
Los bucles nos permiten repetir una tarea mientras se cumple una condición. El primero que veremos es el bucle for:
for (int i = 0; i < 5; i++) {
CrearEnemigo();
}
int i = 0→ Declara una variable y la inicializa a 0i < 5→ Repetirá el bucle mientrasisea menor que 5i++→ Tras cada repetición, sumará uno a la variablei
Entre los paréntesis del for hay tres partes, separadas por punto y coma:
int i = 0declara una variable y la inicializa a 0.i < 5es la condición: el bucle se repetirá mientrasisea menor que 5.i++suma uno a la variableitras cada repetición.
Así, i va valiendo 0, 1, 2, 3 y 4, y CrearEnemigo() se ejecuta cinco veces.
Bucles. while
- Igual que
for, pero sólo establece la condición para seguir repitiendo. El resto (inicialización y actualización) se realiza aparte.
El bucle while funciona igual que el for, pero entre paréntesis sólo lleva la condición para seguir repitiendo. La inicialización y la actualización de la variable se escriben aparte:
int i = 0;
while (i < 5) {
CrearEnemigo();
i++;
}
- Variante:
do–while(comprueba la condición al final: realiza al menos una iteración)
Este bucle hace lo mismo que el for anterior. Si olvidáramos el i++, la condición se cumpliría siempre y el bucle no terminaría nunca.
Existe una variante, do-while, que comprueba la condición al final en lugar de al principio, por lo que realiza al menos una iteración.
Segunda tanda de ejercicios
- Ejercicios de C# · Parte 2
- Mismas instrucciones que para la primera tanda de ejercicios
Es el momento de practicar el control de flujo: Ejercicios de C# · Parte 2. Las instrucciones son las mismas que para la primera tanda.
Colecciones
- Una colección es un tipo de variable (o constante) que posee más de un elemento.
- Por ejemplo, puede resultar conveniente guardar todos los enemigos creados juntos, en una colección.
- La colección “básica” se llama array, pero hay muchos tipos de colecciones: Listas, Colas, Pilas, Diccionarios…
- Nada impide que los elementos de una colección sean… más colecciones (arrays de arrays, listas de listas…)
Una colección es una variable (o constante) que guarda más de un elemento. Resulta útil, por ejemplo, para tener juntos todos los enemigos creados, en lugar de usar una variable para cada uno.
La colección más básica es el array, pero hay muchas otras: listas, colas, pilas, diccionarios… Y nada impide que los elementos de una colección sean, a su vez, colecciones: arrays de arrays, listas de listas…
Array. Declaración.
- Tamaño fijo: se establece al crearlo y no se puede modificar
Un array guarda varios elementos del mismo tipo. Su tamaño es fijo: se establece al crearlo y ya no se puede modificar. Se declara añadiendo [] al tipo de sus elementos y se crea con new, indicando cuántos tendrá:
// Array de 12 cadenas de texto
public string[] meses = new string[12];
// Array de 5 números enteros
public int[] numeros = new int[5];
- También se pueden inicializar sus valores en la creación:
Si ya conocemos los valores, se pueden indicar entre llaves al crear el array. Las tres líneas siguientes son formas equivalentes de hacerlo (en un programa usaríamos sólo una de ellas):
public string[] jugadores = new string[] {"Mario", "Luigi"};
public string[] jugadores = new string[2] {"Mario", "Luigi"};
public string[] jugadores = {"Mario", "Luigi"};
Array multidimensional.
- Tiene más de una dimensión: se puede entender como una tabla (filas y columnas)
- Cada elemento se localiza con un índice por dimensión
- Ejemplo de dos dimensiones:
Un array puede tener más de una dimensión. Uno de dos dimensiones se puede entender como una tabla, con filas y columnas, en la que cada elemento se localiza con dos índices: uno por dimensión.
// Tabla de 2 filas y 3 columnas
int[,] multiDimensionalArray1 = new int[2, 3];
int[,] multiDimensionalArray2 = { { 1, 2, 3 }, { 4, 5, 6 } };
Debug.Log(multiDimensionalArray2[1, 1]); // Muestra 5 (fila 1, columna 1)
La primera línea crea una tabla de 2 filas y 3 columnas. La segunda crea otra del mismo tamaño, indicando ya sus valores. Como los índices empiezan en 0, [1, 1] es la segunda fila y la segunda columna: el 5.
Array. Usos.
- Se puede consultar el tamaño con la propiedad
Length:
El tamaño de un array se consulta con su propiedad Length:
int numJugadores = jugadores.Length;
- Se puede consultar el valor de una posición con su índice (empezando en 0):
A cada elemento se accede con su índice, es decir, su posición dentro del array, escrita entre corchetes. La primera posición es la 0, no la 1, así que en un array de dos elementos los índices válidos son 0 y 1:
string jugador1 = jugadores[0];
string jugador2 = jugadores[1];
- Para modificar un elemento del array:
De la misma forma se puede modificar un elemento:
jugadores[0] = "Mario Bros";
Array. Recorrido.
- Se pueden procesar todos los elementos de un array con un bucle.
Lo habitual es procesar todos los elementos de un array con un bucle. Con un for, la variable i hace de índice y va desde 0 hasta la última posición:
for (int i = 0; i < jugadores.Length; i++) Debug.Log(jugadores[i]);
- Instrucción de bucle especial para colecciones:
foreach
Para recorrer colecciones existe además un bucle especial, foreach, que nos entrega los elementos uno a uno, sin tener que manejar el índice:
foreach (var jugador in jugadores) Debug.Log(jugador);
Listas
- Tamaño dinámico, se puede modificar en cualquier momento
- Métodos de utilidad
- Añadir elemento (
Add) - Quitar elemento (
Remove) - Ordenar (
Sort) - Invertir el orden (
Reverse)
- Añadir elemento (
Una lista (List) es una colección de tamaño dinámico: se le pueden añadir y quitar elementos en cualquier momento. Además incluye métodos de utilidad para las operaciones más habituales, como añadir un elemento (Add), quitarlo (Remove), ordenar la lista (Sort) o invertir su orden (Reverse).
Listas. Declaración.
- Requiere importar su espacio de nombres (namespace):
Para usar listas hay que importar su espacio de nombres (namespace), con esta línea al principio del fichero:
using System.Collections.Generic;
- Declaraciones permitidas:
Al declarar una lista se indica, entre < y >, el tipo de sus elementos. Hay varias formas de crearla:
// Vacía
public List<string> jugadores = new List<string>();
// Vacía, con sitio reservado para 2 elementos
public List<string> jugadores = new List<string>(2);
// Con los elementos de un array
public List<string> jugadores = new List<string>(new string[] {"Mario", "Luigi"});
// Con dos elementos iniciales
public List<string> jugadores = new List<string>() {"Mario", "Luigi"};
Atención a la segunda: el 2 sólo reserva sitio por adelantado; la lista sigue estando vacía.
Listas. Uso.
Estas son las operaciones más habituales con una lista:
jugadores.Add("Wario"); // Añade un elemento a la lista
Debug.Log(jugadores[0]); // "Mario"
Debug.Log(jugadores.Count); // Count en lugar de Length
jugadores.RemoveAt(2); // Elimina el tercer jugador
jugadores.Remove("Luigi"); // Elimina a Luigi
jugadores.Clear(); // Borra todos los elementos
jugadores.Contains("Mario"); // Devuelve true si Mario está en la lista
Fíjate en que el número de elementos se consulta con Count, y no con Length como en los arrays. Y en la diferencia entre RemoveAt, que elimina el elemento que está en una posición, y Remove, que elimina un elemento concreto.
Listas. Uso.
Insert→ Inserta en una posición específicaIndexOf→ Devuelve la posición de un elemento concretoSort→ Ordena la lista. Se puede especificar el criterio.Reverse→ Invierte el orden de la lista- Se recorren igual que los arrays:
Hay más métodos útiles: Insert inserta un elemento en una posición específica, IndexOf devuelve la posición de un elemento concreto, Sort ordena la lista (se puede especificar el criterio) y Reverse invierte su orden.
Las listas se recorren igual que los arrays:
foreach (var jugador in jugadores) Debug.Log(jugador);
Otros tipos de colecciones
ArrayList- Permite guardar elementos de distinto tipo
- Comparte muchos métodos con
List(Add,Remove, etc.) - ¡Ojo! Los elementos no aparecen en el editor de Unity
- Es una clase antigua: Microsoft recomienda usar
List<object>en su lugar
Dictionary,HashSet…- Estructuras de datos optimizadas para la consulta
- Los estudiaremos cuando los necesitemos
Existen muchas más colecciones. Una de ellas es ArrayList, que permite guardar elementos de distinto tipo y comparte muchos métodos con List (Add, Remove…). Tiene dos inconvenientes: sus elementos no aparecen en el editor de Unity y es una clase antigua, en cuyo lugar Microsoft recomienda usar List<object>.
Otras, como Dictionary o HashSet, son estructuras de datos optimizadas para la consulta. Las estudiaremos cuando las necesitemos.
Métodos/funciones
- Conforman una agrupación de instrucciones
- Nos ayudan a reutilizar código
- En C# siempre pertenecen a una clase, y su nombre formal es método
- «Función» es el término general en programación: mucha gente utiliza ambos indistintamente
Un método es una agrupación de instrucciones a la que damos un nombre. Nos ayuda a reutilizar código: en lugar de repetir las mismas instrucciones en varios sitios, las escribimos una vez y las llamamos por su nombre cada vez que hacen falta.
En C# siempre pertenecen a una clase, y su nombre formal es método. «Función» es el término general en programación, y mucha gente utiliza los dos indistintamente.
Declaración
- Al igual que una variable, la función tiene un tipo, que corresponde al tipo de información que devuelve
- Si no devuelve nada, usamos el tipo
void
Al igual que una variable, un método tiene un tipo, que corresponde al tipo de información que devuelve y se escribe delante del nombre. Si no devuelve nada, se usa el tipo void:
void ResetPosition()
{
transform.position = new Vector3(0, 0, 0);
}
ResetPosition no devuelve ningún dato: se limita a colocar el objeto en la posición (0, 0, 0).
Argumentos
- Un método o función puede recibir argumentos
- Se añaden en los paréntesis después del nombre
- Para devolver un valor se utiliza
return - Ejemplo:
Un método puede recibir datos para hacer su trabajo: son los argumentos, que se añaden en los paréntesis que siguen al nombre, cada uno con su tipo. Para devolver un valor se utiliza return:
public int Suma(int sumando1, int sumando2)
{
return sumando1 + sumando2;
}
Suma recibe dos números enteros y devuelve otro entero (por eso su tipo es int): el resultado de sumarlos.
Llamada a un método
- Para llamar (o invocar) un método o función debemos utilizar el nombre de la función, seguido de paréntesis (y pasar dentro los argumentos si la función los requiere)
Para llamar (o invocar) a un método se escribe su nombre seguido de paréntesis y, dentro de ellos, los argumentos, si el método los requiere:
int resultado = Suma(5, 7);
Debug.Log(resultado); // 12
- Dentro de la función
Suma, 5 se convierte ensumando1y 7 se convierte ensumando2
Durante esta llamada, dentro de Suma, sumando1 vale 5 y sumando2 vale 7. El valor que devuelve el método, 12, se guarda en la variable resultado.
Tercera tanda de ejercicios
- Ejercicios de C# · Parte 3
- Mismo tipo de proyecto que en el ejercicio anterior
- Trabajar el uso de Arrays
Con las colecciones y los métodos ya puedes hacer la tercera tanda: Ejercicios de C# · Parte 3. El tipo de proyecto es el mismo que en los ejercicios anteriores, y en ellos trabajarás el uso de arrays.
Clases
- Es la forma de definir objetos en C# (común en POO)
- Encapsula:
- Datos del objeto (campos y propiedades)
- Métodos para trabajar con esos datos
- Funciones de utilidad
Al principio de la unidad vimos que una clase es el modelo que siguen los objetos. Una clase encapsula, es decir, reúne en un mismo sitio:
- Los datos del objeto: sus campos y propiedades.
- Los métodos que trabajan con esos datos.
- Funciones de utilidad.
Declaración
- Visibilidad, constructor/es, campos y métodos
En la declaración de una clase encontramos su visibilidad, uno o varios constructores, sus campos y sus métodos. Como ejemplo, esta clase representa una posición en el espacio:
public class Posicion
{
public Posicion(float x, float y, float z)
{
xPos = x;
yPos = y;
zPos = z;
}
public float xPos;
public float yPos;
public float zPos;
// (continúa)
La clase es pública y se llama Posicion. Lo primero que contiene es su constructor: un método especial, con el mismo nombre que la clase, que se ejecuta al crear cada objeto y que aquí da su valor inicial a los tres campos. Después vienen los campos xPos, yPos y zPos, que guardan las coordenadas.
Declaración (continuación)
La clase continúa con sus métodos:
public void Add(float xDelta, float yDelta, float zDelta)
{
xPos += xDelta;
yPos += yDelta;
zPos += zDelta;
}
public override string ToString()
{
return "X:" + xPos + " Y:" + yPos + " Z:" + zPos;
}
}
Add desplaza la posición, sumando a cada coordenada la cantidad indicada. ToString devuelve un texto con las tres coordenadas; lleva override porque todas las clases de C# tienen ya un método ToString, y aquí lo estamos reescribiendo.
Creación/uso de objetos de una clase
- Palabra reservada:
new - Uso de los constructores para la creación
.para acceder a sus campos, propiedades y métodos públicos- Ejemplo:
Para crear un objeto de una clase se utiliza la palabra reservada new, seguida de una llamada a su constructor. Una vez creado, con el punto (.) se accede a sus campos, propiedades y métodos públicos:
var initialPosition = new Posicion(0f, 0f, 0f);
initialPosition.xPos = 5.0f;
initialPosition.Add(-2.0f, 1.5f, 0.0f);
Debug.Log(initialPosition.ToString());
La primera línea crea una posición en (0, 0, 0). La segunda cambia directamente su campo xPos a 5. La tercera llama al método Add, que la desplaza hasta (3, 1.5, 0). La última muestra las coordenadas.
Herencia
- Las clases en C# pueden heredar de otras
- Al heredar, la clase “hija” obtiene automáticamente sus campos, propiedades y métodos (
publicoprotected) - Permite reutilizar código cómodamente
- La clase “hija” puede reescribir los métodos que la clase padre marque como
virtual, usandooverride(como hicimos conToString)
Las clases en C# pueden heredar de otras. Al heredar, la clase «hija» obtiene automáticamente los campos, propiedades y métodos de la clase padre que sean public o protected. Eso permite reutilizar código cómodamente: la hija sólo tiene que escribir lo que la hace diferente.
Además, la clase hija puede reescribir los métodos que la clase padre marque como virtual, usando override, como hicimos con ToString.
Herencia
En Unity usamos la herencia constantemente: los scripts que añadimos a los GameObjects son clases que heredan de MonoBehaviour. Para indicar de qué clase se hereda, se escribe su nombre detrás de dos puntos:
public class Laser : MonoBehaviour
{
[SerializeField]
private float speed = 10.0f;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
transform.Translate(Vector3.up * speed * Time.deltaTime);
if (transform.position.y > 6)
{
Destroy(this.gameObject);
}
}
}
Herencia
Laserhereda deMonoBehaviour: por eso puede usartransform,gameObjectoDestroyStartyUpdateson métodos que son llamados automáticamente cuando ocurren eventos- Es Unity quien los llama:
Startantes del primerUpdate, yUpdateen cada fotograma
Como Laser hereda de MonoBehaviour, puede usar transform, gameObject o Destroy sin haberlos declarado. Start y Update son métodos que son llamados automáticamente cuando ocurren eventos, y es Unity quien los llama: Start, antes del primer Update; y Update, en cada fotograma.
Así, este láser se desplaza hacia arriba en cada fotograma y, cuando su posición y pasa de 6, se destruye.
Composición vs Herencia
- Es otra forma definida en la POO para ampliar la funcionalidad de una clase.
- En lugar de heredar, una clase se puede componer de otras. Es otro modo de reutilizar código.
- Ejemplo:
- Radio tradicional → Radio IP (IP hereda de tradicional)
- Radio tradicional → Radio IP (Dispositivo con dos componentes: Radio y Conector IP)
La herencia no es la única forma definida en la POO para ampliar la funcionalidad de una clase. La otra es la composición: en lugar de heredar, una clase se compone de otras. Es otro modo de reutilizar código.
Supón que partimos de una radio tradicional y queremos una radio IP. Con herencia, la radio IP sería una clase hija de la radio tradicional. Con composición, sería un dispositivo con dos componentes: una radio y un conector IP.
Cuarta tanda de ejercicios
- Ejercicios de C# · Parte 4
- Mismo tipo de proyecto que en los ejercicios anteriores
- Trabajar las clases, la herencia y la composición
- Para practicar por tu cuenta: Refuerzo 1, 2 y 3, con soluciones
Para terminar la unidad, la cuarta tanda: Ejercicios de C# · Parte 4. El tipo de proyecto es el mismo que en los ejercicios anteriores, y en ellos trabajarás las clases, la herencia y la composición.
Si quieres seguir practicando por tu cuenta, tienes además tres tandas de ejercicios de refuerzo, que combinan todo lo visto en la unidad y traen la solución de cada ejercicio: Refuerzo 1, Refuerzo 2 y Refuerzo 3.