CREANDO ENEMIGOS

Ahora es momento de spanear enemigos. Ya sabemos spawnear bullets así que los enemigos no deberían ser muy diferentes. Vamos a preparar a un enemigo simple que, por el momento, sólo vaya hacia abajo. Si se animan ustedes, sería genial que lo hagan antes de seguir leyendo, ya que no es muy diferente a lo que habíamos hecho con la bullet.

Empecemos con los mismos pasos que la bullet:

  • Arrastrar una imagen de un enemigo al proyecto.
  • Arrastrar la imagen al escenario.
  • Escalarlo como les guste.
  • Crear el script Enemy.
  • Arrastrarlo al GameObject del enemigo creado en la escena.
  • Arrastrar el GameObject a la carpeta “Prefabs“.

Una vez hecho todo esto, ya tenemos a nuestro enemigo listo para empezar.

 

CREANDO UN SPAWNER

 

A diferencia de con la bullet, vamos a crear ahora un objeto para spawnear enemigos. La idea de esto es poner “spawners” por distintos lugares del escenario para que creen enemigos. Estos spawners podrían también crear muchas cosas. Siempre está bueno programar de tal manera que sea todo reutilizable, pero también que no sea engorroso.

Vamos a crear un GameObject vacío en la escena, que vamos a utilizar como nuestro spawner. Para ello vamos al menú “GameObject” => “Create Empty“. Esto creará un GameObject sin nada más que un Transform.

 

 

Luego le cambiamos el nombre para que quede algo más acorde a lo que queremos lograr:

 

 

Como nuestro spawner no tiene ninguna imagen asociada, lo que podemos hacer es decirle a Unity que nos muestre un ícono sobre él, para poder encontrarlo con facilidad en el escenario. Para esto, seleccionamos el objeto y en la ventana “Inspector“, justo a la izquierda de su nombre, hacemos click y podemos elegir un ícono. Si no nos gusta ninguno de los predeterminados podemos usar también otro haciendo click en el botón “Other” y seleccionando otro del proyecto.

 

 

Ahora viene el momento del prefab. Es EXACTAMENTE IGUAL que las otras veces: Sólo lo arrastramos a la carpeta “Prefabs” que ya habíamos creado.

 

 

Oops… Nos olvidamos de ponerle un script a nuestro spawner. ¿Podemos hacerlo después de haber creado el prefab? Yeap (¿Por qué no podríamos?). Para hacerlo, podemos crear el script, como lo crearíamos siempre, y luego arrastrarlo al prefab ya creado. También podemos seleccionar el prefab y hacer click en el botón “Add Component” que figura en la ventana de “Inspector“, y luego buscar nuestro componente ya sea escribiéndolo o navegando entre los desplegables.

 

 

Ahora vamos al script de nuestro spawner y vamos a programar algo simple solamente para crear instancias de “ALGO”. Digo de “ALGO” porque decir “instancias de enemigos” suena a que SÓLO podemos crear instancias de una cosa. Pero la realidad es que como es un spanwer podría spawnear cualquier cosa.

Vamos a empezar borrando las funciones de Start() y Update() que no las vamos a usar para nada:

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Spawner : MonoBehaviour
{
    
}

 

Vamos a agregar ahora un bloque de código para crear algo. Vamos a llamarlo Spawn():

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Spawner : MonoBehaviour
{
    void Spawn()
    {
        
    }
}

 

Vamos, además, a crearle una variable para guardar el prefab de lo que querémos instanciar, de la misma manera que lo hacíamos con nuestra bullet:

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Spawner : MonoBehaviour
{
    public GameObject prefab;
    
    void Spawn()
    {
        
    }
}

 

Ahora replicamos el código que habíamos hecho en nuestra bullet para crear una copia de nuestro prefab y tirarlo a la escena, en la posición del spawner:

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Spawner : MonoBehaviour
{
    public GameObject prefab;
    
    void Spawn()
    {
        GameObject obj = GameObject.Instantiate(prefab);
        obj.transform.position = transform.position;
    }
}

 

Ahora nuestro “Spawner” puede spawnear cosas. El tema es que no va a spawnear nada hasta que se lo digamos. Podemos hacer que el mismo spawner cree algo cada X tiempo, pero lo ideal sería que cada objeto tenga una única responsabilidad. O sea… El spawner sólo se encarga de spawnear cosas, NADA MÁS, y quizás algo más debería decirle cuándo hacerlo. Ese “algo más” podría ser otro script. Quizás en principio podría ser algo engorroso tener que crear scripts para cosas diferentes, pero también es una buena forma de dividir un poco mejor el trabajo. Tampoco hay necesidad de ir al otro extremo, que es crear montones de scripts para hacer cosas demasiado nimias.

Entonces, vamos a crear otro script para que le diga al spawner que cree algo cada X tiempo. Vamos a llamarlo SpawnerByTime o el nombre creativo que se les ocurra. Este script debería encargarse de llamar al bloque Spawn() que está dentro del script Spawner cada X tiempo. No hace falta a esta altura decir cómo creamos un script, así que lo hacemos y empezamos a programar el temporizador, con una variable que indique el tiempo que queremos que tarde en hacer algo:

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpawnerByTime : MonoBehaviour
{
    public float timeToSpawn;
    
    // Use this for initialization
    void Start ()
    {
        
    }
    
    // Update is called once per frame
    void Update ()
    {
        
    }
}

 

Antes de seguir vamos a explicar un poco cuál sería la idea del temporizador. Recordemos que el bloque Update() se ejecuta frame a frame, por ende todo lo que hagamos dentro de este bloque que dependa del tiempo deberíamos usar la variable Time.deltaTime. Antes de explicar bien la idea, quiero que creen una variable para ir contando segundos. Más que nada para que vean cómo va a funcionar. Vamos a ponerle, no sé, ¿currentSeconds? Y vamos a sumarle frame a frame el valor de la variable Time.deltaTime.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpawnerByTime : MonoBehaviour
{
    public float timeToSpawn;
    public float currentSeconds;
    
    // Use this for initialization
    void Start ()
    {
        
    }
    
    // Update is called once per frame
    void Update ()
    {
        currentSeconds += Time.deltaTime;
    }
}

 

Ahora… Presionen el botón “Play” vean cómo incrementa el valor de esta variable. ¿Qué notan?

 

… Cambia con los segundos. O sea… Cuando pasa un segundo la variable vale una unidad más. Esto es también por lo que habíamos hablado sobre Time.deltaTime. Dicha variable guarda el tiempo que tardó Unity (en segundos), en pintar el último frame. Si cada vez que pasa un frame le sumamos este valor a una variable, dicha variable irá incrementando con los segundos que pasaron.

Siguiendo esta lógica, podemos usar esto como temporizador. Podríamos chequear cuándo la variable llega a tal o cual valor, y luego hacer algo en consecuencia. De la misma manera podemos hacer que la variable reste valor, hasta llegar a cero. Lo que sea más intuitivo para ustedes.

 

Así que… Podemos iniciar el valor de la variable currentSeconds en el valor que tenga la variable timeToSpawn y luego ir restando. Cuando llegue a cero, hacemos algo y reiniciamos el contador. De esta manera tenemos un temporizador. Lo que restaría ahora es decirle que spawnee un objeto, en este caso un enemigo, cuando dicho temporizador llegue a cero. Cómo lo hacemos?

El script Spawner es el que se encarga de spawnear el objeto, así que el script SpawnerByTime lo único que debería hacer es decirle que spawnee algo cada X tiempo. Dos scripts, dos tareas diferentes. Al tener esta separación, podríamos ir editando el que corresponda, y si falla una tarea, podemos encontrar el error un poco más fácil. A menos, claro, que todo esté demasiado dividido. Por eso también les decía que está bien dividir en tareas, pero no exagerar.

Entonces… Cómo quedaría nuestro código…

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpawnerByTime : MonoBehaviour
{
    public float timeToSpawn;
    public float currentSeconds;
    
    // Use this for initialization
    void Start ()
    {
        //Inicializamos el temporizador.
        currentSeconds = timeToSpawn;
    }
    
    // Update is called once per frame
    void Update ()
    {
        //Restamos TIEMPO frame a frame.
        currentSeconds -= Time.deltaTime;

        //Si el tiempo es menor o igual a cero, spawneamos ALGO.
        if (currentSeconds <= 0)
        {
            GetComponent<Spawner>().Spawn();
        }
    }
}

 

Si lo hicieron de la misma manera que yo, van a notar, al volver a Unity, que el script tira un error. Si directamente no pasó nada, chequeen si realmente arrastraron los scripts al spawner (suele pasar cuando empezamos con Unity que nos olvidemos estas cosas). Van a ver un error en la consola que dice algo como esto:

 

Error...

Assets/Scripts/SpawnerByTime.cs(22,37): error CS0122: Spawner.Spawn() is inaccessible due to its protection level

 

Unity genera este error porque el bloque de código llamado Spawn() creado dentro del script Spawner no es accesible desde afuera. ¿Qué podemos cambiar? Si tienen conocimientos algunos conocimientos medianamente avanzados de programación seguro se dieron cuenta en seguida, y sino, no importa, siempre aprender cosas nuevas.

El cambio que tenemos que hacer es agregar public antes del void Spawn(). Esto hace que el bloque de código se pueda acceder desde otro lugar. Cambienlo, y fijense cómo el error se va. Luego, presionen el botón “Play” y prueben el juego. ¿Qué notan?

Pssss… ¿Pusiste el prefab del spawner en la escena? :P.

 

¿Qué son todos esos errores que aparecen luego de darle “Play“? Si les genera este error:

 

UnassignedReferenceException: The variable prefab of Spawner has not been assigned.
You probably need to assign the prefab variable of the Spawner script in the inspector.
UnityEngine.Object.Instantiate[GameObject] (UnityEngine.GameObject original) (at /Users/builduser/buildslave/unity/build/Runtime/Export/UnityEngineObject.bindings.cs:275)
Spawner.Spawn () (at Assets/Scripts/Spawner.cs:11)
SpawnerByTime.Update () (at Assets/Scripts/SpawnerByTime.cs:22)

 

La gran mayoría de los errores debería ser bastante intuitivo solucionarlos. En este caso, lo que pasó fue que nunca arrastramos al script Spawner el prefab de nuestro enemigo. Entonces, el spawner no tiene algo para spawnear. Arrastrenlo, y también configuren el tiempo para spawnearlo, puesto que sino… Puede pasar algo un poco… Raro… Prueben!!

Otra cosa que quizás noten es que si, por ejemplo, ponemos el tiempo en 3 segundos, una vez que estos pasan el enemigo empieza a spawnear constantemente. Si quieren tomense unos segundos e intenten arreglarlo (les va a venir bien para practicar). Sino, les dejo abajo la solución.

 

Reinicien el valor de la variable currentSeconds inmediatamente después de spawnear algo!!

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpawnerByTime : MonoBehaviour
{
    public float timeToSpawn;
    public float currentSeconds;
    
    // Use this for initialization
    void Start ()
    {
        currentSeconds = timeToSpawn;
    }
    
    // Update is called once per frame
    void Update ()
    {
        currentSeconds -= Time.deltaTime;
        if (currentSeconds <= 0)
        {
            GetComponent().Spawn();
            currentSeconds = timeToSpawn;
        }
    }
}

 

 

Listo! Todo debería funcionar OK ahora. Por supuesto, el enemigo no se mueve todavía, pero ya tenemos un spawner por tiempo! Antes de seguir, vamos con algunos “detalles” que nunca están de más.

  1. Definitivamente, el script SpawnerByTime no funcionaría si no tuviéramos en el GameObject el script Spawner. Lo que podemos hacer es es que este script dependa del otro a la vista de Unity. Cosa que cuando lo agreguemos Unity agregue automáticamente todo lo que le haga falta. Para eso, vamos al script SpawnerByTime y le agregamos lo siguiente arriba de public class....
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    [RequireComponent(typeof(Spawner))]
    public class SpawnerByTime : MonoBehaviour
    {
        public float timeToSpawn;
        public float currentSeconds;
        
        // Use this for initialization
        void Start ()
        {
            currentSeconds = timeToSpawn;
        }
        
        // Update is called once per frame
        void Update ()
        {
            currentSeconds -= Time.deltaTime;
            if (currentSeconds <= 0)
            {
                GetComponent<Spawner>().Spawn();
                currentSeconds = timeToSpawn;
            }
        }
    }

    Esto lo que va a hacer es decirle a Unity que el script SpawnerByTime NECESITA del script Spawner. Así que si agregamos a cualquier GameObject SpawnerByTime agregaría automáticamente Spawner. Es más… Ni siquiera nos dejaría sacarlo, y nos lo advertiría con un mensaje de advertencia (pruebenlo!!).

  2. La variable currentSeconds no es necesario que sea visible. En tal caso, lo que pueden hacer es ponerle private en la declaración, o no poner nada (como pasaba con el bloque de código Spawn()).

 

MOVIENDO A NUESTROS ENEMIGOS

Mover a nuestros enemigos debería ser sencillo como mover las bullets. Pero vamos a hacerlo un poco diferente, para que también aprendan algo nuevo. Noten que los GameObjects tienen algo así como “direcciones“. No me refiero a direcciones de movimiento exactamente. Noten que si seleccionan un GameObject y presionan la tecla “W” (que era el atajo para editar la posición del objeto seleccionado), van a ver unas flechas. Estas flechas son básicamente las direcciones del objeto, y normalmente muestran sus “caras” (digo normalmente porque todo depende de si el botón que está arriba a la derecha dice “pivot” o “global”). Cada GameObject tiene un “lado” y ese lado es un vector dirección que indica hacia donde mira. Pero también lo podemos usar en nuestro juego para algo útil como decirle que se mueva hacia ese lado.

En el caso de nuestro enemigo (y también de nuestro héroe), podemos decirle que se mueva siempre hacia SU arriba. Digo SU arriba porque si rotamos el objeto, y hacemos que mire hacia abajo, entonces SU ARRIBA ahora es HACIA ABAJO (roten la imagen para visualizar la idea). O sea que… Siempre le voy a dar la orden a la nave de que se mueva HACIA SU ADELANTE, pero si SU ADELANTE cambia, entonces la dirección de movimiento TAMBIÉN cambiaría. Quizás en un tanto engorroso, pero vamos a visualizarlo.

En lugar de decirle a la nave que se mueva hacia abajo:

 

transform.position += Vector3.down * speed * Time.deltaTime;

 

Vamos a decirle que se mueva hacia SU ARRIBA.

 

transform.position += transform.up * speed * Time.deltaTime;

 

De este modo, si rotamos la nave enemiga, la misma cambiará su dirección. Pruebe!! Mientras la nave se mueve presionen el botón de “Pausa” y roten la nave.

 

 

Noten que cada una de las naves enemigas va en dirección diferente. Lo que hice fue que cada vez que spawnee una nave enemiga, puse pausa y la roté hacia otro lugar. Esto significa que si la nave está rotada en otra dirección, iría en ESA dirección. La propiedad transform.up puede ser también seteada para hacer esto mismo al revés, o sea: hacer que la nave esté rotada en la dirección que quiero que vaya. Por ejemplo, podríamos decirle al Spawner que luego de crear la nave, la gire hacia el ARRIBA del mismo Spawner. Esto significaría que si roto la dirección del Spawner este crearía naves enemigas en la dirección que esté “mirando”. Probemos!!

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Spawner : MonoBehaviour
{
    public GameObject prefab;
    
    public void Spawn()
    {
        GameObject obj = GameObject.Instantiate(prefab);
        obj.transform.position = transform.position;
        obj.transform.up = transform.up;
    }
}

 

 

Ahora con esto podemos crear spawners con tiempos diferentes, que creen enemigos en direcciones diferentes. Sólo nos restaría una cosa más…

 

DESTRUYENDO A LOS ENEMIGOS QUE SALEN DE PANTALLA.

Todos los enemigos que salgan de la vista de la cámara deben morir. De la misma manera que lo hacían nuestras bullets. Vamos a agregarle ese código también a nuestros enemigos para que todos los que salgan se destruyan.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour
{
    public float speed;

    // Use this for initialization
    void Start ()
    {
        
    }
    
    // Update is called once per frame
    void Update ()
    {
        transform.position += transform.up * speed * Time.deltaTime;
    }

    private void OnBecameInvisible()
    {
        GameObject.Destroy(gameObject);
    }
}

 

Y con esto ya tenemos enemigos! Aunque por el momento no hacen nada más que ir en una cierta dirección. Pero intenten poner distintos puntos de spawn para que salgan de lugares diferentes en tiempos diferentes. Como para ir armando un nivel.