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.
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.