24. Colisiones simples en modo gráfico

Cuando estábamos en "modo texto", comprobábamos colisiones mirando si coincidían la X y la Y del personaje con las de algún enemigo (o premio, obstáculo...). Pero eso en modo gráfico no sirve, porque pueden "solaparse" aunque no lleguen a estar exactamente en la misma posición.

Colision en modo grafico

Si nos fijamos en la posición horizontal, para que se solapen, debe ocurrir que:

  • El rectángulo azul no debe estar demasiado a la izquierda: su posición final (posX1+anchura1) debe ser mayor que la posición inicial del rectángulo verde (posX2)
  • Además, el rectángulo azul no debe estar demasiado a la derecha: su posición inicial (posX1) debe ser menor que la posición final del rectángulo verde (posX2+anchura2)
  • Las consideraciones en vertical son las mismas, con la única diferencia de que afectan a la posición Y y a la altura de cada rectángulo.

De esta forma, aproximamos los objetos por rectángulos, de modo que puede no ser útil si dejamos "hueco" alrededor de ellos cuando recortamos las imágenes, ni tampoco si los objetos son muy irregulares, pero es una forma sencilla y que funciona "razonablemente bien" en muchos casos.

Esto equivaldría a una función "Colision", que comprobara si dos elementos gráficos chocan entre sí, de la siguiente forma:

public static bool Colision(ElemGrafico e1, ElemGrafico e2)
{
    if ((e1.x + e1.ancho > e2.x)
          && (e1.x < e2.x + e2.ancho)
          && (e1.y + e1.alto > e2.y)
          && (e1.y < e2.y + e2.alto))
        return true;
    else
        return false;
}

Una primera mejora, ya que los premios "desaparecerán" (visible=false) cuando los recojamos, es que sólo se compruebe la colisión cuando los dos objetos sean visibles:

public static bool Colision(ElemGrafico e1, ElemGrafico e2)
{
    // No se debe chocar con un elemento oculto      
    if ((e1.visible == false) || (e2.visible == false))
        return false;
    // Ahora ya compruebo coordenadas
    if ((e1.x + e1.ancho > e2.x)
          && (e1.x < e2.x + e2.ancho)
          && (e1.y + e1.alto > e2.y)
          && (e1.y < e2.y + e2.alto))
        return true;
    else
        return false;
}

Esto nos obliga a que, cada vez que definamos un elemento gráfico, indiquemos su ancho, su alto y si está visible o no (tanto para premios, como para obstáculos, enemigos o para nuestro propio personaje):

for (int i = 0; i < numPremios; i++)  // Premios
{
    premios[i].x = generador.Next(50, 700);
    premios[i].y = generador.Next(30, 550);
    premios[i].visible = true;
    premios[i].ancho = 34;
    premios[i].alto = 18;
}

Y ya que estamos, podemos hacer una pequeña mejora:

Con tantos obstáculos y enemigos en pantalla, es muy fácil que choquemos nada más comenzar la partida y que ésta termine exageradamente pronto. Podemos evitarlo en primer lugar haciendo que no pueda aparecer un obstáculo inicialmente en la misma posición que nuestro personaje:

for (int i = 0; i < numObstaculos; i++)  // Obstaculos
{
    obstaculos[i].visible = true;
    obstaculos[i].ancho = 38;
    obstaculos[i].alto = 22;
    // Al colocar un obstáculo, compruebo que no choque con el personaje, para que la partida
    // no acabe nada más empezar
    do
    {
        obstaculos[i].x = generador.Next(50, 700);
        obstaculos[i].y = generador.Next(30, 550);
    } while (Colision(obstaculos[i], personaje));
}

Y como los enemigos se mueven, no basta con que no se choquen inicialmente con nuestro personaje. Será más fiable si no permitimos que estén en la misma franja vertical que nosotros:

for (int i = 0; i < numEnemigos; i++)  // Enemigos
{
    enemigos[i].x = generador.Next(50, 700);
    // Para la Y, compruebo que no choque con el personaje, para que la partida
    // no acabe nada más empezar
    do
    {
        enemigos[i].y = generador.Next(30, 550);
    } while ((enemigos[i].y+enemigos[i].alto > personaje.y) 
        && (enemigos[i].y < personaje.y+personaje.alto) );
    enemigos[i].incrX = 5;
    enemigos[i].visible = true;
    enemigos[i].ancho = 36;
    enemigos[i].alto = 42;
}

El programa completo quedaría así:

// Primer mini-esqueleto de juego en modo gráfico
// Versión "f"
 
using System;
using System.Threading; // Para Thread.Sleep
 
public class Juego05f
{
    public struct ElemGrafico
    {
        public int x;
        public int y;
        public int xInicial;
        public int yInicial;
        public int ancho;
        public int alto;
        public int incrX;
        public int incrY;
        public Imagen imagen;
        public bool visible;
    }
 
    static ElemGrafico personaje;
    static Fuente tipoDeLetra;
 
    static int numPremios, numEnemigos, numObstaculos;
 
    static ElemGrafico[] obstaculos;
    static ElemGrafico[] enemigos;
    static ElemGrafico[] premios;
 
    static bool juegoTerminado;
    static int vidas;
    static int puntos;
    static bool partidaTerminada;
    static Random generador;
 
    static Imagen fondoPresentacion;
    static Imagen fondoAyuda;
    static Imagen fondoCreditos;
 
 
    public static void InicializarJuego()
    {
        // Entrar a modo grafico 800x600
        bool pantallaCompleta = false;
        Hardware.Inicializar(800, 600, 24, pantallaCompleta);
 
        // Resto de inicializacion
        tipoDeLetra = new Fuente("FreeSansBold.ttf", 18);
        juegoTerminado = false;
        numPremios = 10;
        numEnemigos = 10;
        numObstaculos = 20;
        obstaculos = new ElemGrafico[numObstaculos];
        enemigos = new ElemGrafico[numEnemigos];
        premios = new ElemGrafico[numPremios];
        generador = new Random();
 
        // Cargo imágenes de elementos
        personaje.imagen = new Imagen("personaje.png");
 
        for (int i = 0; i < numObstaculos; i++)  // Obstaculos
            obstaculos[i].imagen = new Imagen("obstaculo.png");
 
        for (int i = 0; i < numEnemigos; i++)  // Enemigos
            enemigos[i].imagen = new Imagen("enemigo.png");
 
        for (int i = 0; i < numPremios; i++)  // Premios
            premios[i].imagen = new Imagen("premio.png");
 
        // Y cargo las imagenes de la presentación, ayuda y créditos
        fondoPresentacion = new Imagen("present.jpg");
        fondoAyuda = new Imagen("present.jpg");
        fondoCreditos = new Imagen("present.jpg");
    }
 
 
    public static void InicializarPartida()
    {
        // En cada partida, hay que reiniciar ciertas variables
        vidas = 3;
        puntos = 0;
        partidaTerminada = false;
 
        personaje.xInicial = 400;
        personaje.yInicial = 300;
        personaje.x = personaje.xInicial;
        personaje.y = personaje.yInicial;
        personaje.visible = true;
        personaje.ancho = 32;
        personaje.alto = 30;
        personaje.incrX = 10;
        personaje.incrY = 10;
 
        // Genero las posiciones de los elementos al azar
        for (int i = 0; i < numObstaculos; i++)  // Obstaculos
        {
            obstaculos[i].visible = true;
            obstaculos[i].ancho = 38;
            obstaculos[i].alto = 22;
            // Al colocar un obstáculo, compruebo que no choque 
            // con el personaje, para que la partida
            // no acabe nada más empezar
            do
            {
                obstaculos[i].x = generador.Next(50, 700);
                obstaculos[i].y = generador.Next(30, 550);
            } while (Colision(obstaculos[i], personaje));
        }
 
        for (int i = 0; i < numEnemigos; i++)  // Enemigos
        {
            enemigos[i].x = generador.Next(50, 700);
            // Para la Y, compruebo que no sea del rango de
            // la del personaje, para que la partida
            // no acabe nada más empezar
            do
            {
                enemigos[i].y = generador.Next(30, 550);
            } while ((enemigos[i].y+enemigos[i].alto > personaje.y) 
                && (enemigos[i].y < personaje.y+personaje.alto) );
            enemigos[i].incrX = 5;
            enemigos[i].visible = true;
            enemigos[i].ancho = 36;
            enemigos[i].alto = 42;
        }
 
        for (int i = 0; i < numPremios; i++)  // Premios
        {
            premios[i].x = generador.Next(50, 700);
            premios[i].y = generador.Next(30, 550);
            premios[i].visible = true;
            premios[i].ancho = 34;
            premios[i].alto = 18;
        }
    }
 
 
    public static void MostrarPresentacion()
    {
        bool finPresentacion = false;
 
        do
        {
            // ---- Pantalla de presentación --
            Hardware.BorrarPantallaOculta(0, 0, 0);
 
            // Fondo de la presentación
            fondoPresentacion.DibujarOculta(0, 0);
 
            // Marcador
            Hardware.EscribirTextoOculta("Jueguecillo",
                     340, 200, // Coordenadas
                     255, 255, 255, // Colores
                     tipoDeLetra);
 
            Hardware.EscribirTextoOculta("Escoja una opción:",
                     310, 300, // Coordenadas
                     200, 200, 200, // Colores
                     tipoDeLetra);
 
            Hardware.EscribirTextoOculta("J.- Jugar una partida",
                     150, 390, // Coordenadas
                     200, 200, 200, // Colores
                    tipoDeLetra);
 
            Hardware.EscribirTextoOculta("A.- Ayuda",
                     150, 430, // Coordenadas
                     200, 200, 200, // Colores
                     tipoDeLetra);
 
            Hardware.EscribirTextoOculta("C.- Créditos",
                     150, 470, // Coordenadas
                     200, 200, 200, // Colores
                     tipoDeLetra);
 
            Hardware.EscribirTextoOculta("S.- Salir",
                     150, 510, // Coordenadas
                     200, 200, 200, // Colores
                     tipoDeLetra);
 
            Hardware.VisualizarOculta();
 
            Hardware.Pausa(20);
 
            if (Hardware.TeclaPulsada(Hardware.TECLA_A))
                MostrarAyuda();
 
            if (Hardware.TeclaPulsada(Hardware.TECLA_C))
                MostrarCreditos();
 
            if (Hardware.TeclaPulsada(Hardware.TECLA_J))
                finPresentacion = true;
 
            if (Hardware.TeclaPulsada(Hardware.TECLA_S))
            {
                finPresentacion = true;
                partidaTerminada = true;
                juegoTerminado = true;
            }
        } while (!finPresentacion);
    }
 
 
    public static void MostrarAyuda()
    {
        string[] textosAyuda = 
        { 
          "Recoge los premios", 
          "Evita los obstáculos y los enemigos",
          "Usa las flechas de cursor para mover" 
        };
 
        // ---- Pantalla de presentación --
        Hardware.BorrarPantallaOculta(0, 0, 0);
 
        // Fondo de la presentación
        fondoAyuda.DibujarOculta(0, 0);
 
        // Marcador
        Hardware.EscribirTextoOculta("Ayuda",
                 340, 200, // Coordenadas
                 255, 255, 255, // Colores
                tipoDeLetra);
 
        // Textos repetitivos
        short posicYtexto = 280;
        foreach (string texto in textosAyuda)
        {
            Hardware.EscribirTextoOculta(texto,
                     150, posicYtexto, // Coordenadas
                     200, 200, 200, // Colores
                    tipoDeLetra);
            posicYtexto += 30;
        }
 
        Hardware.EscribirTextoOculta("ESC- volver",
                 650, 530, // Coordenadas
                 200, 200, 200, // Colores
                tipoDeLetra);
        Hardware.VisualizarOculta();
 
        do
        {
            Hardware.Pausa(20);
        } while (!Hardware.TeclaPulsada(Hardware.TECLA_ESC));
    }
 
 
    public static void MostrarCreditos()
    {
        // ---- Pantalla de presentación --
        Hardware.BorrarPantallaOculta(0, 0, 0);
 
        // Fondo de la presentación
        fondoCreditos.DibujarOculta(0, 0);
 
        // Marcador
        Hardware.EscribirTextoOculta("Creditos",
                 250, 200, // Coordenadas
                 255, 255, 255, // Colores
                tipoDeLetra);
 
        Hardware.EscribirTextoOculta("Por Nacho Cabanes, 2011",
                  250, 300, // Coordenadas
                  200, 200, 200, // Colores
                 tipoDeLetra);
 
        Hardware.EscribirTextoOculta("ESC- volver",
                 650, 530, // Coordenadas
                 200, 200, 200, // Colores
                tipoDeLetra);
        Hardware.VisualizarOculta();
 
        do
        {
            Hardware.Pausa(20);
        } while (!Hardware.TeclaPulsada(Hardware.TECLA_ESC));
    }
 
 
    public static void Dibujar()
    {
        // -- Dibujar --
        Hardware.BorrarPantallaOculta(0, 0, 0);
 
        // Marcador
        Hardware.EscribirTextoOculta("Vidas        Puntos",
                 0, 0, // Coordenadas
                 255, 255, 255, // Colores
                tipoDeLetra);
 
        Hardware.EscribirTextoOculta(Convert.ToString(vidas),
                 70, 0, // Coordenadas
                 200, 200, 200, // Colores
                tipoDeLetra);
 
        Hardware.EscribirTextoOculta(Convert.ToString(puntos),
                 190, 0, // Coordenadas
                 200, 200, 200, // Colores
                tipoDeLetra);
 
        for (int i = 0; i < numObstaculos; i++)  // Obstáculos
        {
            obstaculos[i].imagen.DibujarOculta(
                (int)obstaculos[i].x, (int)obstaculos[i].y);
        }
 
        for (int i = 0; i < numEnemigos; i++)  // Enemigos
        {
            enemigos[i].imagen.DibujarOculta(
                (int)enemigos[i].x, (int)enemigos[i].y);
        }
 
        for (int i = 0; i < numPremios; i++)  // Premios
        {
            if (premios[i].visible)
            {
                premios[i].imagen.DibujarOculta(
                    premios[i].x, premios[i].y);
            }
        }
 
        personaje.imagen.DibujarOculta(
            personaje.x, personaje.y);
 
        // Finalmente, muestro en pantalla
        Hardware.VisualizarOculta();
    }
 
 
    public static void ComprobarTeclas()
    {
        // -- Leer teclas y calcular nueva posición --
        if (Hardware.TeclaPulsada(Hardware.TECLA_ESC))
            partidaTerminada = true;
 
        if (Hardware.TeclaPulsada(Hardware.TECLA_DER))
            personaje.x += personaje.incrX;
        if (Hardware.TeclaPulsada(Hardware.TECLA_IZQ))
            personaje.x -= personaje.incrX;
        if (Hardware.TeclaPulsada(Hardware.TECLA_ARR))
            personaje.y -= personaje.incrY;
        if (Hardware.TeclaPulsada(Hardware.TECLA_ABA))
            personaje.y += personaje.incrY;
    }
 
 
    public static void MoverElementos()
    {
        // -- Mover enemigos, entorno --
        for (int i = 0; i < numEnemigos; i++)  // Enemigos
        {
            enemigos[i].x = enemigos[i].x + enemigos[i].incrX;
            if (((int)enemigos[i].x <= 50)
                    || ((int)enemigos[i].x >= 700))
                enemigos[i].incrX = -enemigos[i].incrX;
        }
    }
 
 
    public static void ComprobarColisiones()
    {
        // -- Colisiones, perder vidas, etc --
        for (int i = 0; i < numObstaculos; i++)  // Obstáculos
        {
            if (Colision(obstaculos[i], personaje))
            {
                vidas--;
                if (vidas == 0)
                    partidaTerminada = true;
                personaje.x = personaje.xInicial;
                personaje.y = personaje.yInicial;
            }
        }
 
        for (int i = 0; i < numPremios; i++)  // Premios
        {
            if (Colision(premios[i], personaje))
            {
                puntos += 10;
                premios[i].visible = false;
            }
        }
 
        for (int i = 0; i < numEnemigos; i++)  // Enemigos
        {
            if (Colision(enemigos[i], personaje))
            {
                vidas--;
                if (vidas == 0)
                    partidaTerminada = true;
                personaje.x = personaje.xInicial;
                personaje.y = personaje.yInicial;
            }
        }
 
    }
 
 
    public static void PausaFotograma()
    {
        // -- Pausa hasta el siguiente "fotograma" del juego --
        Hardware.Pausa(20);
    }
 
 
    public static bool Colision(ElemGrafico e1, ElemGrafico e2)
    {
        // No se debe chocar con un elemento oculto      
        if ((e1.visible == false) || (e2.visible == false))
            return false;
        // Ahora ya compruebo coordenadas
        if ((e1.x + e1.ancho > e2.x)
              && (e1.x < e2.x + e2.ancho)
              && (e1.y + e1.alto > e2.y)
              && (e1.y < e2.y + e2.alto))
            return true;
        else
            return false;
    }
 
 
 
    public static void Main()
    {
 
        InicializarJuego();
 
        while (!juegoTerminado)
        {
            InicializarPartida();
            MostrarPresentacion();
 
            // ------ Bucle de juego ------
            while (!partidaTerminada)
            {
                Dibujar();
                ComprobarTeclas();
                MoverElementos();
                ComprobarColisiones();
                PausaFotograma();
            } // Fin del bucle de juego
        }     // Fin de partida
    }         // Fin de Main
}
 

Ejercicio propuesto (1): Haz que cuando se recojan todos los premios, vuelvan a aparecer otros en nuevas posiciones al azar. Crea para ello una función llamada "RecolocarPremios".

Ejercicio propuesto (2): Añade un marcador de "mejor puntuación". Al terminar una partida, si se ha superado esta "mejor puntuación", deberá actualizarse su valor.