Como programador, con frecuencia trabajará con un “grupo” de datos (como una matriz o array que presenté en la lección anterior). Tic Tac Toe, por ejemplo, tiene un tablero de 3 × 3 con nueve celdas totales. Si estuviera creando un método para operar en ese grupo de datos, como limpiar un tablero para un nuevo juego, no querría tener que aplicar manualmente los cambios a todos y cada uno de los valores de la matriz. En cambio, puede escribir algo llamado bucle y dejar que la maquina maneje el trabajo tedioso por usted. En esta lección, crearemos un Tic Tac Toe y mostraremos cómo los bucles pueden ayudar a que nuestro código sea más elegante.
La vida sin bucles
Crea un nuevo script llamado “TicTacToe”. Sin bucles, puede tratar de implementar este juego con algo como lo siguiente:
#region Librerias
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
#endregion
namespace MoonAntonio
{
public class TicTacToe : MonoBehaviour
{
[SerializeField] Text[] cells;
void Start()
{
NewGame();
}
public void NewGame()
{
cells[0].text = "";
cells[1].text = "";
cells[2].text = "";
cells[3].text = "";
cells[4].text = "";
cells[5].text = "";
cells[6].text = "";
cells[7].text = "";
cells[8].text = "";
cells[9].text = "";
}
}
}
Este código declara una matriz de componentes de texto que representan las celdas de nuestro tablero de tres en raya. También define el primer método que necesitaremos, uno que despeje el tablero para prepararlo para un nuevo juego. En ese método asignamos el texto de cada celda a una cadena vacía para que la celda no se use. Todo lo escrito aquí hasta ahora es funcional, pero no elegante, o fácilmente ampliable para juegos con tableros más grandes. ¡Imagine una configuración similar para Ajedrez, donde tiene que asignar manualmente 64 fichas en lugar de las 9 que tenemos aquí!
La magia de los bucles
Cada una de las nueve afirmaciones en el método NewGame es idéntica a una excepción, el índice de la celda en la matriz. Como programador, a menudo escuchará acerca de mantener su código “DRY”, lo que significa “No repetir”. Esto a menudo puede referirse a la necesidad de poner bits de lógica en métodos más reutilizables y más pequeños, pero también puede aplicarse aquí. Mira cómo el método NewGame podría implementarse con un nuevo vocabulario:
public void NewGame()
{
for (int n = 0; n < cells.Length; n++)
{
cells[n].text = "";
}
}
En este ejemplo, pudimos reemplazar nueve declaraciones separadas del método “NewGame” con una sola declaración envuelta en un “for loop”. Además de ser más compacto, este código recortado también es dinámico y ampliable. ¡Podríamos cambiar de un tablero de tres en raya estándar de 3 x 3 pies a un tablero de 5 x 5 y no necesitar cambiar ni agregar ninguna línea de código, mientras que la implementación anterior habría requerido 16 líneas adicionales!
La palabra clave “for” marca el comienzo de nuestro ciclo. Un inicializador, una condición y una “expresión” de iterador aparecen dentro de sus paréntesis (tenga en cuenta que están separados por punto y coma, pero el último no termina con un punto y coma), y un cuerpo (los enunciados que se ejecutarán repetidamente) aparecerán entre el abierto y cerrar corchetes ‘{’ y ‘}’.
Las declaraciones dentro del paréntesis determinan las “reglas” de cómo bucleamos y merecemos un poco más de discusión:
- Declaro una variable temporal llamada “n” en la declaración “inicializador” y asigno su valor predeterminado a 0. El alcance de esta variable está restringido al bucle en sí, y no será visible fuera de su declaración y cuerpo. El inicializador solo se ejecuta una vez, al comienzo de este bloque de código.
- La “condición” determina si se ejecuta el código en su cuerpo o no. En este ejemplo, continuaremos iterando mientras el valor de “n” sea menor que la longitud de nuestra matriz de celdas. Esta afirmación se comprueba una vez antes de cada ciclo de ciclo.
- El “iterador” nos brinda la oportunidad de modificar la variable que declaramos en el inicializador. En este ejemplo, incrementamos el valor de “n” en uno después de cada ciclo. Tenga en cuenta que “n++” es una forma abreviada de escribir “n = n + 1”. El iterador se ejecuta después de cada ciclo.
En el cuerpo de nuestro bucle, pasamos la variable “n” que definimos en el inicializador de bucle, como el índice en nuestra matriz de celdas. Al usar la variable, la celda que estamos modificando es dinámica y será diferente en cada ejecución a través del ciclo.
Interacción
Ahora que tenemos un tablero y podemos prepararla para jugar, agreguemos algo de lógica para jugar. Necesitaremos dos cosas: una variable que marque si es hora de colocar una “X” o una “O” y un método de manejo de eventos para determinar cuándo y dónde hacer un movimiento en el tablero. Agregue la siguiente declaración de variable debajo de nuestra matriz de celdas:
string marca;
Hagamos que las X siempre vayan primero. Para hacer eso, agregue la siguiente instrucción dentro del método NewGame, justo después del corchete de cierre de nuestro ciclo:
marca = "X";
Cuando el usuario haga clic en uno de los botones de nuestro tablero, necesitaremos un método para que llame. Eso se definirá de la siguiente manera:
public void SelectCell(int index)
{
if (!string.IsNullOrEmpty(cells[index].text)) return;
cells[index].text = marca;
marca = (marca == "X") ? "O" : "X";
}
Este método comienza con un control para ver si la celda ya se ha marcado o no (porque no queremos permitir que un jugador sobrescriba el movimiento de otro jugador). El signo de exclamación significa “No”, por lo que toda la afirmación se lee básicamente “si el texto de la celda no está vacío”. Cuando la condición es verdadera, el método llama a una declaración de “retorno” para que el resto del método sea ignorado. Tenga en cuenta que este ejemplo no ajusta la declaración de retorno entre corchetes. Los corchetes solo son necesarios cuando necesita más de una declaración para ser tratada como el cuerpo. Normalmente, solo usaría una declaración de devolución al final de un método, pero ocasionalmente la verá al comienzo de un método como una forma de “abortar” anticipadamente.
Cuando la condición es falsa, el resto del método se puede ejecutar normalmente. En este caso, significa que la celda está vacía y, por lo tanto, es un lugar legal para realizar un “movimiento”. Hacemos nuestro “movimiento” asignando el valor de “marca” a la etiqueta.
Finalmente, cambiamos las curvas al alternar la marca de X a O y viceversa. Esta afirmación puede considerarse como una variación de una “declaración if”. Tiene una condición (el código entre paréntesis) seguido de un signo de interrogación. Si el resultado de la condición es verdadero, se usa el valor a la izquierda de los dos puntos, de lo contrario se usa el valor a la derecha de los dos puntos. Para ver lo que tenemos hasta ahora, comencemos a construir la escena.
Configuración de escena
- Para comenzar, crea una nueva escena llamada “TicTacToe” (En el repositorio es la 05).
- Agregue un nuevo Panel (desde la barra de menú, seleccione “GameObject -> UI -> Panel”).
- Elimine los componentes “Image” y “Canvas Renderer” de ese panel (seleccione el engranaje en el inspector y luego “Eliminar componente”) porque no los necesitaremos.
- En el componente “Rect Transform” del panel, ingrese un valor de “0.5” para cada uno de los cuatro anclajes (Mín. Y Máx., X e Y), así como también el Pivot (X e Y). Establezca la Posición en cero en los tres ejes (X, Y y Z) y configure el Ancho y la Altura en “300”.
- Agregue el componente “Grid Layout Group” (en la barra de menú, seleccione “Component -> Layout -> Grid Layout Group”). Establezca su tamaño de celda en 100 para X e Y.
- Agregue un botón (desde la barra de menú elija “GameObject -> UI -> Button”) y créelo en el panel (arrástrelo y suéltelo encima del objeto Panel en el panel de jerarquía para que quede anidado debajo).
- Duplique el botón hasta que tenga nueve botones en total (desde la barra de menú, seleccione “Edit -> Duplicate” o Ctrl + D). Si ha seguido correctamente los pasos, debería ver una placa de botones 3 × 3 centrada en el centro de la cámara.
- Adjunte nuestro script de TicTacToe al canvas.
- Asegúrese de que el canvas esté seleccionado y luego bloquee el inspector (haga clic en el ícono de candado en la esquina superior derecha).
- Expanda cada uno de los botones en la jerarquía para que pueda ver las etiquetas de texto. Multi-selecciona los objetos de texto, y arrástrelos a la variable de matriz de celdas de nuestro script. Unity cambiará automáticamente el tamaño de la matriz para contener todos los valores y asignar los objetos a la matriz.
- Cuando se hayan asignado todas las celdas, desbloquee el inspector.
- Contraiga los botones en la jerarquía y luego seleccione varios botones. Use el inspector para agregar un controlador OnClick. Arrastre el objeto Canvas al campo del objeto de destino y seleccione “TicTacToe -> SelectCell (int)” como nuestro controlador de función.
- Tendrá que asignar el valor para pasar cada botón individualmente (hay formas mejores pero esto servirá por ahora) Comenzando desde arriba, establezca los valores para pasar de 0-8 (usaremos este valor como el índice en una matriz).
Reproduzca la escena y haga clic en cada uno de los botones. Debería ver cada botón establecer una X o O alternando como su etiqueta al hacer clic en ellos. Si el botón en el que hace clic hace que se actualice la etiqueta de un botón diferente, entonces ha vinculado algo incorrectamente. Verifique que la matriz de etiquetas de texto esté en orden (puede hacer clic en ellas en el inspector y resaltará la coincidencia en el panel de jerarquía) de arriba a abajo. Verifique también que el parámetro OnClick del botón esté marcado en orden según el paso 13.
Estado del juego
El último paso es hacer que nuestro juego mire para una condición de victoria/derrota. Después de cada turno necesitamos hacer este control, y cuando se encuentre, felicitar al ganador y comenzar un nuevo juego.
Agregue otra variable que indique cuando el juego está realmente terminado. Lo usaremos para asegurarnos de que no se jugarán movimientos extra cuando se encuentre una condición de victoria. También crearemos e inicializaremos una matriz multidimensional, donde cada subarray es una lista de índices de ubicación que forman una línea en el tablero (filas, columnas y diagonales) desde la cual comprobaremos las posibles victorias. Agregue estos justo debajo de la declaración de la variable “marca”:
bool gameOver;
int[,] wins = new int[,]
{
{0,1,2},
{3,4,5},
{6,7,8},
{0,3,6},
{1,4,7},
{2,5,8},
{0,4,8},
{2,4,6}
};
En el método NewGame, tendremos que asegurarnos de establecer nuestra variable gameOver en falso, o no se podrán realizar nuevos movimientos. Agregue esta declaración al final de ese método:
gameOver = false;
Determinaremos si el juego ha finalizado llamando a un nuevo método:
void CheckGameState()
{
for (int i = 0; i < wins.GetLength(0); ++i)
{
int j = wins[i, 0];
int k = wins[i, 1];
int l = wins[i, 2];
if (cells[j].text == cells[k].text &&
cells[k].text == cells[l].text &&
!string.IsNullOrEmpty(cells[j].text))
{
gameOver = true;
Debug.Log(cells[j].text + " GANAS!");
Invoke("NewGame", 3f);
break;
}
}
}
En este método, recorremos la matriz de líneas donde podría ocurrir una victoria. Dentro del bucle tenemos una “declaración if” compuesta que requiere que tres cosas sean verdaderas (esto sucede al usar “&&” que significa “y”:
- El valor de la primera celda marcada debe coincidir con el valor de la segunda celda marcada.
- El valor de la segunda celda marcada debe coincidir con el valor de la tercera celda marcada.
- El valor de la primera celda marcada no debe estar vacío.
Si esas tres condiciones se cumplen a la vez, establecemos gameOver en “verdadero”, imprimimos un mensaje que indica quién ganó, configuramos nuestro método NewGame para que se active en 3 segundos y luego llamamos “break”, que sale del bucle. No necesitamos seguir buscando victorias una vez que se ha encontrado uno.
A continuación, tenemos que modificar el método SelectCell. No queremos permitir movimientos cuando la variable gameOver es verdadera o cuando la celda ya está tomada. Podemos hacer una comprobación compuesta con O utilizando dos líneas verticales: “||”. También llamamos a nuestro CheckGameState después de aplicar un movimiento.
void CheckGameState()
{
for (int i = 0; i < wins.GetLength(0); ++i)
{
int j = wins[i, 0];
int k = wins[i, 1];
int l = wins[i, 2];
if (cells[j].text == cells[k].text &&
cells[k].text == cells[l].text &&
!string.IsNullOrEmpty(cells[j].text))
{
gameOver = true;
Debug.Log(cells[j].text + " GANAS!");
Invoke("NewGame", 3f);
break;
}
}
}
Guarde su script y regrese a Unity. Juega el juego ahora y activa una condición de victoria. Debería ver un mensaje de felicitación en la consola y luego no podrá realizar nuevos movimientos hasta que el tablero se restablezca.