Saltar a contenido

Módulo 4 — Enemigos y dificultad progresiva (25 min)

¿Qué vamos a lograr?

Dos tipos de enemigo distintos, que van a ir apareciendo desde arriba cada vez con más frecuencia:

  • Enemigo que se aproxima: avanza directo hacia la nave, en línea recta hacia donde esté en cada momento.
  • Enemigo que dispara: baja hasta cierta altura, se detiene, y desde ahí dispara balas hacia abajo a intervalos regulares, sin acercarse más.

Info

En este módulo todavía no vamos a programar qué pasa cuando algo choca con un enemigo, ni qué pasa cuando una bala enemiga o un enemigo tocan a la nave — eso lo hacemos junto con el resto del sistema de vida y puntaje en los Módulos 5 y 6, todo de una vez. Acá nos concentramos solo en que los enemigos existan, se muevan y aparezcan con el tiempo.

4.1 Crear el enemigo que se aproxima

  1. En la Hierarchy, clic derecho > 2D Object > Sprites > Square. Renómbralo a EnemyChaser.
  2. En el Sprite Renderer, asígnale un sprite de nave enemiga (por ejemplo ufoRed, o cualquiera de la carpeta Enemies).
  3. En el campo Tag del Inspector, elige Add Tag..., crea una etiqueta nueva llamada Enemy, y luego asignásela a este GameObject. Vamos a usar este tag en el Módulo 5 para reconocer a cualquier enemigo, sin importar de qué tipo sea.
  4. Add Component > Circle Collider 2D, con Is Trigger activado (mismo motivo que con la nave en el Módulo 3: queremos detectar el contacto, no que rebote físicamente).
  5. Add Component > Rigidbody 2D, con Body Type en Kinematic (nosotros lo vamos a mover por código, igual que a la nave).
  6. Arrastra el script EnemyChaser.cs sobre el GameObject.
using UnityEngine;

// Este enemigo avanza directo hacia la nave del jugador, sin importar en qué
// dirección tenga que moverse para lograrlo.
public class EnemyChaser : MonoBehaviour
{
    public float moveSpeed = 3f;

    // Guardamos una referencia al Transform de la nave, para saber hacia dónde movernos.
    private Transform jugador;

    void Start()
    {
        // GameObject.FindGameObjectWithTag busca en toda la escena el único objeto
        // que tenga el tag indicado. Por eso en el Módulo 3 le pusimos el tag "Player" a la nave.
        GameObject objetoJugador = GameObject.FindGameObjectWithTag("Player");
        if (objetoJugador != null)
        {
            jugador = objetoJugador.transform;
        }
    }

    void Update()
    {
        // Si la nave ya fue destruida (jugador quedó en null), no hay a quién perseguir.
        if (jugador == null) return;

        // Vector3.MoveTowards calcula, paso a paso, una posición que se va acercando
        // desde transform.position hacia jugador.position, sin pasarse de largo.
        transform.position = Vector3.MoveTowards(transform.position, jugador.position, moveSpeed * Time.deltaTime);
    }

    // Igual que con los meteoritos: si el enemigo sale de la pantalla sin haber chocado
    // con nada, lo destruimos para no dejar objetos de más dando vueltas.
    void OnBecameInvisible()
    {
        Destroy(gameObject);
    }
}
  1. Arrastra el GameObject EnemyChaser hasta Assets/Prefabs, y borra el que quedó en la Hierarchy.

Inspector del prefab EnemyChaser: Tag Enemy, Circle Collider 2D, Rigidbody 2D y script

4.2 Crear el enemigo que dispara desde la distancia

Primero necesitamos su bala. Es prácticamente igual a la del jugador (Módulo 2), pero viaja hacia abajo:

  1. En la Hierarchy, clic derecho > 2D Object > Sprites > Square. Renómbralo a EnemyBullet.
  2. En el Sprite Renderer, asígnale un sprite de la carpeta Lasers (elige un color distinto al de la bala del jugador, para diferenciarlas a simple vista — por ejemplo un láser rojo).
  3. Arrastra el script EnemyBullet.cs sobre el GameObject.
using UnityEngine;

// Es prácticamente igual a Bullet.cs, pero para los disparos enemigos:
// se mueve hacia abajo en vez de hacia arriba, y se destruye sola después de un tiempo.
public class EnemyBullet : MonoBehaviour
{
    [Header("Movimiento")]
    public float moveSpeed = 8f;

    [Header("Duración")]
    public float tiempoDeVida = 3f;

    void Start()
    {
        Destroy(gameObject, tiempoDeVida);
    }

    void Update()
    {
        transform.Translate(Vector3.down * moveSpeed * Time.deltaTime);
    }
}
  1. Arrastra EnemyBullet hasta Assets/Prefabs, y borra el que quedó en la Hierarchy.

Note

Por ahora, a propósito, EnemyBullet no tiene Collider2D: todavía no necesita "detectar" nada, porque el daño a la nave lo programamos recién en el Módulo 6.

Ahora el enemigo en sí:

  1. En la Hierarchy, clic derecho > 2D Object > Sprites > Square. Renómbralo a EnemyShooter.
  2. Asígnale un sprite de nave enemiga distinto al de EnemyChaser (para diferenciarlos a simple vista).
  3. Ponle el Tag Enemy (el mismo que creamos recién).
  4. Add Component > Circle Collider 2D, con Is Trigger activado.
  5. Add Component > Rigidbody 2D, con Body Type en Kinematic.
  6. Arrastra el script EnemyShooter.cs sobre el GameObject.
using UnityEngine;

// Este enemigo baja hasta una altura fija y se queda ahí quieto, disparando hacia abajo
// cada cierto tiempo, sin acercarse más a la nave.
public class EnemyShooter : MonoBehaviour
{
    [Header("Movimiento")]
    public float moveSpeed = 3f;

    // Hasta qué altura (posición Y) baja antes de detenerse a disparar.
    public float alturaDeDetencion = 2f;

    [Header("Disparo")]
    public GameObject enemyBulletPrefab;
    public float fireRate = 1.5f;

    private bool detenido = false;
    private float proximoDisparo;

    void Update()
    {
        if (!detenido)
        {
            Mover();
        }
        else if (Time.time >= proximoDisparo)
        {
            Disparar();
            proximoDisparo = Time.time + fireRate;
        }
    }

    void Mover()
    {
        transform.Translate(Vector3.down * moveSpeed * Time.deltaTime);

        // Apenas llega a la altura de detención, deja de moverse y empieza a disparar.
        if (transform.position.y <= alturaDeDetencion)
        {
            detenido = true;
            proximoDisparo = Time.time + fireRate;
        }
    }

    void Disparar()
    {
        Instantiate(enemyBulletPrefab, transform.position, Quaternion.identity);
    }

    void OnBecameInvisible()
    {
        Destroy(gameObject);
    }
}
  1. En el Inspector de EnemyShooter, arrastra el prefab EnemyBullet (desde Assets/Prefabs) al campo Enemy Bullet Prefab.
  2. Arrastra el GameObject EnemyShooter hasta Assets/Prefabs, y borra el que quedó en la Hierarchy.

Inspector del prefab EnemyShooter con Enemy Bullet Prefab asignado

4.3 Crear el spawner de enemigos, con dificultad progresiva

  1. En la Hierarchy, clic derecho > Create Empty. Renómbralo a EnemySpawner.
  2. Arrastra el script EnemySpawner.cs sobre él.
using UnityEngine;

// Crea enemigos con el tiempo, alternando al azar entre los dos tipos, y hace que
// aparezcan cada vez más seguido: así el juego se pone más difícil solo.
public class EnemySpawner : MonoBehaviour
{
    [Header("Prefabs de enemigos")]
    public GameObject enemigoQueSeAproximaPrefab;
    public GameObject enemigoQueDisparaPrefab;

    [Header("Dificultad progresiva")]
    // Cada cuánto aparece un enemigo nuevo, al empezar el juego.
    public float intervaloInicial = 2.5f;

    // El intervalo nunca va a bajar de este valor, para que el juego siga siendo jugable.
    public float intervaloMinimo = 0.6f;

    // Cuánto se reduce el intervalo cada vez que aparece un enemigo nuevo.
    public float reduccionPorAparicion = 0.05f;

    private float intervaloActual;
    private float proximaAparicion;
    private float minX, maxX, topY;

    void Start()
    {
        intervaloActual = intervaloInicial;
        CalcularLimitesDePantalla();
        proximaAparicion = Time.time + intervaloActual;
    }

    void CalcularLimitesDePantalla()
    {
        Camera camara = Camera.main;
        Vector3 esquinaInferior = camara.ViewportToWorldPoint(new Vector3(0f, 0f, 0f));
        Vector3 esquinaSuperior = camara.ViewportToWorldPoint(new Vector3(1f, 1f, 0f));

        minX = esquinaInferior.x;
        maxX = esquinaSuperior.x;
        topY = esquinaSuperior.y + 1f;
    }

    void Update()
    {
        // El mismo patrón de "cooldown" con Time.time que usamos para el disparo de la
        // nave en el Módulo 2. A diferencia del InvokeRepeating del Módulo 3, acá el
        // intervalo va cambiando, así que necesitamos controlarlo nosotros mismos.
        if (Time.time >= proximaAparicion)
        {
            CrearEnemigo();

            // Reducimos el intervalo para que el próximo enemigo tarde menos en aparecer,
            // sin bajar nunca del mínimo.
            intervaloActual = Mathf.Max(intervaloMinimo, intervaloActual - reduccionPorAparicion);
            proximaAparicion = Time.time + intervaloActual;
        }
    }

    void CrearEnemigo()
    {
        // Random.value devuelve un número al azar entre 0 y 1: lo usamos como
        // "moneda al aire" para elegir uno de los dos tipos de enemigo.
        GameObject prefabElegido = Random.value < 0.5f ? enemigoQueSeAproximaPrefab : enemigoQueDisparaPrefab;

        float posicionX = Random.Range(minX, maxX);
        Vector3 posicionDeAparicion = new Vector3(posicionX, topY, 0f);

        Instantiate(prefabElegido, posicionDeAparicion, Quaternion.identity);
    }
}
  1. En el Inspector de EnemySpawner, arrastra el prefab EnemyChaser al campo Enemigo Que Se Aproxima Prefab, y el prefab EnemyShooter al campo Enemigo Que Dispara Prefab.
  2. Presiona Play y observa unos 20-30 segundos: deberían ir apareciendo ambos tipos de enemigo desde arriba, cada vez con menos tiempo entre uno y otro.

Inspector de EnemySpawner con ambos prefabs asignados

Jugando en modo Play: enemigos de ambos tipos en pantalla

Checkpoint ✅

Al terminar este módulo deberían aparecer enemigos de los dos tipos desde arriba de la pantalla: unos avanzando directo hacia la nave, y otros bajando hasta cierta altura para quedarse ahí disparando. Con el paso del tiempo, deberían ir apareciendo más seguido.