Aprendiendo videojuegos con la historia de las consolas: Atari (Parte IV)

En la anterior parte de este tutorial nos quedamos pendientes de que nuestro portero de madera colisionase con la pelota, así que ha llegado el momento de configurarlo para que esto sea posible y, como ya aprendimos anteriormente, esto será posible gracias a un Box Collider 2D y un RigidBody 2D. Vamos a añadirlos a nuestro muñeco de madera. (Si no recordáis como se hace volved al capítulo anterior) y a configurarlos con los valores que vemos en la imagen. Con esto tendremos un portero fuerte que no será desplazado dentro de la portería cuando la pelota le golpee.


En este caso vamos a tener (cuando lo programemos) un portero que estará en movimiento, por lo que marcaremos la casilla de verificación is Kinematic, de nuestro RigidBody 2D, ayudando así a Unity a realizar mejor los cálculos físicos necesarios y consiguiendo que solo exista colisión con otros objetos en desplazamiento, como la pelota, y que no se pueda chocar con el poste de la portería que tiene un Collider estático (Sin RigidBody). Si probamos a pulsar play y durante el juego, lanzar la pelota contra nuestro muñequito de madera veremos cómo sale rebotada, por lo que con esto ya podemos avanzar al siguiente paso.

Queremos hacer que nuestro portero de prácticas se mueva constantemente para tratar de dificultarnos un poco el meter el balón dentro de la portería. Para ello crearemos un nuevo Script en C# al que vamos a llamar GoalKeeperControl, lo arrastraremos sobre nuestro muñeco de madera, y después lo editaremos escribiendo el siguiente código:

using UnityEngine;
using System.Collections;

public class GoalKeeperControl : MonoBehaviour 
{
 public float speed;       // velocidad de movimiento
 public float limits;      // Limites del movimiento

 private Transform thisTransform;   // Referencia al transform del portero
 private int direction;        // Direccion de movimiento
 private float timePassed;     // Tiempo pasado desde el ultimo cambio de direccion
 private float changeTime;     // Tiempo para cambiar de direccion

 // Use this for initialization
 void Start () 
 {
  thisTransform = transform;
  ChangeDirection();   // damos una direccion inicial aleatoria de movimiento
 }
 
 // Update is called once per frame
 void Update () 
 {
  
  if(Time.timeSinceLevelLoad < timePassed + changeTime){   
   MoveWoodenKeeper(); // mueve el portero
  } else {
   ChangeDirection();  // mover hacia un sitio aleatorio ()
  }
 }

 /*-----------------------------------------------------------------------
  *  - ChangeDirection() -
  * 
  *  Cambia la direccion de movimiento del portero
  * ----------------------------------------------------------------------*/
 
 void ChangeDirection()
 {
  timePassed = Time.timeSinceLevelLoad;   // tiempo de juego
  direction = Random.Range(1,3);          // Numero entre 1 y 2
  changeTime = Random.Range (0.3f,0.7f);  // Rango de tiempo aleatorio para el cambio
  MoveWoodenKeeper();                     // controla el movimiento
 }

 /*-----------------------------------------------------------------------
  *  - MoveWoodenKeeper() -
  * 
  *  Mueve el portero en una direccion  despendiendo de la variable direction
  * ----------------------------------------------------------------------*/
 
 void MoveWoodenKeeper()
 {
  switch (direction)
  {
  case 1: // moving right
   
   if (thisTransform.position.x > limits)
   {
    ChangeDirection(); // si se mueve a la derecha debe ir a la izquierda
   }
   else
   {
    thisTransform.Translate(thisTransform.right * speed * Time.deltaTime);
   }
   break;
  case 2: // moving left
   if (thisTransform.position.x < -limits)
   {
    ChangeDirection(); // si se mueve a la izquierda debe ir a la derecha
   }
   else
   {
    thisTransform.Translate(thisTransform.right * -speed * Time.deltaTime);
   }
   break;
  default:
   break;
  }
 }
}

Como se puede leer en el código tenemos dos variables públicas de tipo float que posteriormente ajustaremos desde el Inspector en Unity y que se llaman speed y limits. Estas se encargarán de definir la velocidad a la que se moverá nuestro muñeco de prácticas y que limites de movimiento tiene, que no van a ser más que una distancia desde el centro donde se sitúa inicialmente hasta los palos de la portería.

Después, tenemos varias variables privadas. Por ejemplo thisTransform lo hemos usado muchas otras veces y en esta ocasión tiene el mismo objetivo, servir de referencia al transform del objeto que contiene este script (el portero) para poder usarla y acceder a dicho transform consumiendo menos recursos por tener que buscarlo cada vez. Por otro lado, la variable entera direction, se encargará de decidir si el muñeco se mueve hacia la derecha o hacia la izquierda de forma aleatoria. Nos encontraremos también timePassed, que almacenará el tiempo que llevamos de juego en cada momento que el movimiento del portero cambia de dirección y changeTime, encargada esta última de recoger un numero de segundos al azar para hacer el próximo cambio en el sentido del movimiento y que así no cambie de forma constante y repetitiva.

En Update() vamos a mover nuestro muñeco mediante MoveWoodenKeeper() si el tiempo desde que el nivel se inició (Time.timeSinceLevelLoad es menor que la suma de timePassed + changeTime. Es decir, si no ha pasado bastante tiempo desde que el portero hizo un cambio de dirección sumado al tiempo en el que tiene que cambiar de nuevo. En caso contrario, cambiaremos de dirección con ChangeDirection(), iniciando de nuevo el proceso.

La función ChangeDirection() se va a encargar de inicializar timePassed igualándolo al tiempo desde que el nivel se inició (Time.timeSinceLevelLoad) cada vez que se ejecute esta función. Además, recogerá un valor aleatorio entre 1 y 2 (uno derecha y dos izquierda) y lo almacenará en direction. Para hacer esto, usamos Random.Range(), en este caso entre 1 y 3 por que al ser enteros busca la aleatoriedad entre el número mínimo y el máximo-1 (es decir no incluye el 3, solo elige entre 1 y 2). Con esta misma operación obtenemos también un número entre 0.3 y 0.7 (a los que añadimos una f para indicar que son float, es decir, decimales) para decidir el tiempo en segundos que tardará en volver a cambiar la dirección del movimiento (volviendo a llamarse a esta función). Por último llamamos a MoveWoodeKeeper() para que el muñeco ejecute el movimiento en la nueva dirección elegida.

MoveWoodenKeeper() es una función encargada de mover el portero dependiendo hacia donde le indique la variable direction, que como ya dijimos cambiará cada changeTime segundos. Usamos pues, un condicional de tipo switch para hacer que se mueva a la derecha si direction vale 1 o a la izquierda si vale 2. Sin embargo, también tenemos que tener en cuenta si la posición x del portero de madera ha alcanzado su límite por la derecha (limits) o por la izquierda (-limits) recurriendo a condicionales if que comprueben si la posición del transform es mayor que limits o menor que -limits. Por otro lado el movimiento en sí, lo conseguiremos con thisTransform.Translate(), una función que varía la posición de un objeto cuando le indiquemos mediante sus parámetros. En este caso para ir a la derecha usamos thisTransform.right * speed * Time.deltaTime, indicando así que se mueva a la derecha a velocidad speed y teniendo en cuenta Time.deltaTime para que la velocidad de movimiento no dependa de la del dispositivo en el que se dibuje. (Esto ya se explicó un poco en anteriores capítulos).

Por cierto, en el inspector de Unity he configurado speed a 1.5 y limits a 0.5

Si observamos que el portero se dibuja por detrás de la portería podemos corregir su eje Z del transform.position desde el inspector. Yo finalmente lo he tenido que poner con un valor de -0.6.

Como vemos, el movimiento del muñeco no supone un reto para el jugador, que puede practicar el tiro a portería libremente, pero sí que molesta y le obliga a estar atento y hacer cálculos, por lo que creo que es el entrenamiento perfecto de cara al lanzamiento real en el modo de juego normal. Podemos pasar entonces a lo siguiente: Reinicio del nivel.

Resulta un poco molesto tener que parar y reiniciar nuestro juego después de cada tiro, así que vamos a crear una solución temporal para que podamos tirar penaltis infinitos hasta que decidamos detener la partida. Para conseguir esto, recurriremos a Application.LoadLevel(“NombreDeEscena”) para cargar la misma escena en la que estamos actualmente. Para esto tendremos que pensar en un evento para lanzar la acción de reiniciar el nivel, así que pudiendo elegir factores como el fin del movimiento de la pelota, pulsar una tecla u otros muchos, nos vamos a decidir por el factor tiempo, es decir, esperaremos un tiempo desde la pulsación de la tecla de disparo, o si lo preferís, desde el lanzamiento a portería, dejaremos que el usuario vea un rato como rebota la pelota o se pierde por el fondo, y restauraremos la escena actual con el ya citado Application.LoadLevel(). Descrita la explicación, veamos el código que introduciremos en el Script PlayerControl.

Primero necesitamos incluir dos variables privadas y una pública. En este caso timeAfterShoot será pública para poder decidir desde el Inspector de Unity el tiempo que queremos que pase para reiniciar el nivel después de tirar a puerta (yo he elegido 3 segundos). Las privadas serán timeSinceShoot, para guardar el tiempo que pasa desde que se tira a puerta, y un booleano llamado shooted, que nos avisará de que ya se ha efectuado el lanzamiento. Además timeSinceShoot será inicializado a 0 y shooted a false en la función Start(), ya que al inicio de juego estamos seguros de que no se ha hecho ningún disparo a puerta y necesitamos un punto de partida.

private float timeSinceShoot; // Tiempo desde que se tira a puerta
private bool shooted;
public float timeAfterShoot; // Timepo necesario para reiniciar

// Use this for initialization
void Start () 
{
        timeSinceShoot = 0;
 shooted = false;
...
}

Para evitar que el nivel se reinicie constantemente usaremos en Update() el booleano shooted, que comprobará el tiempo desde que se tiró a portería únicamente después de que se haya efectuado el tiro y lo hará mediante la función CheckLevelRestart()

if(shooted) // El jugador tira a puerta, contemos el tiempo hasta reiniciar
 CheckLevelRestart();

Pero antes, para saber cuándo se tira a puerta e iniciar el tiempo desde allí hasta el reinicio del nivel, nos vamos a la función Shoot() y añadimos las siguientes líneas al final:

timeSinceShoot = Time.timeSinceLevelLoad;
shooted = true;

Indicamos con timeSinceShoot, que tome como referencia el tiempo desde que empezó el nivel para ir contando segundos desde allí y con shooted = true, que acabamos de efectuar un tiro a portería.

Finalmente la función CheckLevelRestart() comprobará si el tiempo pasado desde el inicio del nivel es mayor que el tiempo pasado desde el inicio del nivel al tiro a puerta (timeSinceShoot) más timeSinceShoot, que es el tiempo que debe pasar hasta el reinicio del nivel después del ya mencionado disparo a puerta. (Cargamos el nivel con Application.LoadLevel(“GameScene”);)

/*-----------------------------------------------------------------------
 *  - CheckLevelRestart() -
 * 
 * Funcion que comprueba si ha pasado tiempo para reiniciar el nivel
 * --------------------------------------------------------------------*/

void CheckLevelRestart()
{
 if(Time.timeSinceLevelLoad > timeSinceShoot + timeAfterShoot)
 {
  Application.LoadLevel("GameScene");
 }
}

Si ejecutáramos ahora el juego cargaría el nivel porque es el único que tenemos, pero para hacer bien las cosas necesitamos un poco de trabajo fuera de código con Unity. Iremos a file/Build Settings y donde pone Scenes in Build, tendremos que cargar nuestra escena actual GameScene arrastrándola desde la ventana Project.


Como vemos, el juego sigue un poco después del tiro y no hay una pausa, por lo que el portero sigue moviéndose y saca el balón de la portería. Solucionaremos esto creando un nuevo script muy sencillo llamado GameManager que no vamos a asociar a ningún objeto. En este script añadiremos una variable de tipo Static para manejar estados del juego que permanecerá en la memoria durante toda la ejecución y conservará su valor. ¿Y es este el mejor método para hacer esto? No, pero es el más sencillo y rápido para nuestro pequeño proyecto, aunque los expertos programadores pueden recurrir a otro modo de hacer las cosas. Veamos el contenido de GameManager.

using UnityEngine;
using System.Collections;

public class GameManager : MonoBehaviour 
{

 public enum States { inGame, Waiting}; 
 // En juego, En espera
 public static States gameState; // Estado del juego

}

Hemos utilizado el tipo enum de C# para aclarar los estados de juego. Así decidimos que haya dos estados: “en juego” y “esperando” (inGame y Waiting), usando estos para bloquear los controles y el movimiento del portero cuando estemos en espera y para permitir continuar la partida cuando estemos en juego.

¿Cuándo vamos a poner el juego en espera? Pues en mi caso he decidido hacerlo cuando la pelota esté en una posición Y determinada. Con esto, cuando haya lanzado el jugador a puerta y el balón este subiendo hacia meta, se bloqueará en cierto momento el juego impidiendo que el muñeco de madera se siga moviendo y que el usuario continúe teniendo el control del personaje. Para hacer esto vamos a necesitar otro Script que llamaremos BallScript y que enlazaremos con la pelota (ya veremos que en un futuro próximo nos va a venir muy bien). En este script escribiremos el siguiente código.

using UnityEngine;
using System.Collections;

public class BallScript : MonoBehaviour 
{

 Transform thisTransform;

 // Use this for initialization
 void Start () 
 {
  thisTransform = transform;
 }
 
 // Update is called once per frame
 void Update () 
 {
  if(thisTransform.position.y > 0.2f)
  {
   GameManager.gameState = GameManager.States.Waiting;
  }
 }
} 

Por supuesto, para contrarrestar la activación del juego en espera, vamos a poner el estado de en juego nada más comience el juego, para ello dentro del script PlayerControl, dentro y al final de la función Start(), añadiremos la siguiente línea:

GameManager.gameState = GameManager.States.inGame;

Con esto cada vez que se reinicie el nivel y se invoque a esta función, el estado de juego se colocara como inGame.


Como último paso para bloquear el juego cuando el estado sea Waiting tendremos que colocar condicionales en PlayerControl y GoalKeeperControl antes de ejecutar las acciones encargadas del movimiento y el control. Así, solo se ejecutará esa parte del código cuando el estado de juego sea inGame, parándose durante el resto de la ejecución. (Pongo un ejemplo pero esto se pondría tanto en Update y FixedUpdate de PlayerControl, como en Update de GoalKeeperControl, dejando BallScript sin código de este tipo por que queremos ver a la bola rebotar)

if(GameManager.gameState == GameManager.States.inGame)
{
        horAxis = Input.GetAxis...
} 

Lo siguiente para acabar el capítulo de hoy va a ser comprobar si hemos marcado gol. Pero para conseguir esto tendremos que añadir un Collider (en este caso a un objeto vacío porque queremos que sea invisible) que más que Collider tal como hemos visto hasta ahora, hará de Trigger o interruptor, para chivarnos cuando el balón ha entrado en una zona concreta de juego. Este tipo de objetos no visibles se usan en juegos para activar puertas, interruptores lanzar eventos de animación y mil cosas más.

Creamos por tanto un objeto vacio y le añadimos un Box Collider 2D de una medida x = 1, y = 0.17. Con estas dimensiones podemos centrar nuestro Trigger dentro de la portería, cuando ya se ha cruzado la línea blanca de gol y sin acercarnos mucho a los postes, para evitar un falso tanto si los cálculos físicos de Unity son poco precisos y señalan gol cuando la pelota ha rebotado en uno de los postes. Por cierto, llamaremos a este objeto GoalZone y es importante marcar la casilla isTrigger del Collider, para hacer que Unity deje atravesarlo con la pelota y lo utilice como Trigger.




Una vez lo tenemos listo, en BallScript tendremos que introducir el código que detecte que la pelota está entrando en esta zona de gol e indicar de alguna forma (temporal por ahora) al jugador que ha marcado, pero para poder lograrlo también tenemos que aprender algunos conocimientos nuevos sobre nuestro motor de juegos favorito, las etiquetas.

Las Etiquetas nos permiten marcar varios objetos bajo un nombre común y realizar acciones con ellos, como comprobar colisiones. Gracias a las etiquetas podemos elegir que un personaje realice una acción o emita un sonido cuando choca con un tipo concreto de objeto y otras acciones distintas si choca con otro objeto distinto. (Por ejemplo que la lava nos quite energía y por la hierba podamos caminar libremente).

Para añadir una etiqueta seleccionaremos nuestro objeto GoalZone y desplegaremos en el Inspector, al lado de Tag, hasta seleccionar Add Tag… Después, en la casilla Element 0, introduciremos el nombre de nuestra etiqueta (en este caso también GoalZone) y pulsaremos Enter. Ahora podemos ir de nuevo al inspector, seleccionando el objeto GoalZone y elegir la etiqueta que acabamos de añadir volviendo a desplegar al lado de Tag y escogiendo esa nueva etiqueta que habrá aparecido.


Como ya tenemos una etiqueta para saber cuando el balón entre en una zona de gol, y así nos avise, vamos a editar el script BallScript y a añadir el siguiente código (una función aparte al final pero dentro de la clase.

void OnTriggerEnter2D(Collider2D col)
{
 if(col.tag == "GoalZone")
 {
  Debug.Log ("Goal!!");
 }
}

Básicamente OnTriggerEnter2D() es una función predefinida de Unity que responde cuando un objeto entra en la zona del Trigger que hicimos hace un momento. Recibe como parámetro un Collider2D que hemos llamado col y con el podemos comprobar si el objeto que ha “colisionado” cuando la bola entra en la zona tiene la etiqueta “GoalZone”. En caso de que la bola esté dentro de GoalZone significará que el jugador ha marcado, por lo que usamos Debug.Log para escribir en la consola de Unity el texto correspondiente (Que se pone entre comillas).

Con esto, veremos como cuando el balón entre en la portería aparecerá en la consola de Unity el mensaje “Goal!!” indicando que el jugador ha anotado.

En el futuro ya tendremos tiempo de hacer esto de forma mucho más bonita, pero de momento lo vamos a dejar así. :)

Comentarios

  1. Creo que este artículo es el cuarto de esta serie, no el VI.

    ResponderEliminar
    Respuestas
    1. Umm, depende como los cuentes. Es el 6º de atari si tenemos en cuenta los dos primeros de creación de gráficos. El 4º si cuentas solo la parte de Unity y el 7º de toda la sección.

      Las cosas en el Blog, por como es no están muy ordenadas, pero estoy en ello. Lo siento :P

      Eliminar

Publicar un comentario

Entradas populares