27. Un mapa para un nivel
En muchos juegos de plataformas "clásicos", la memoria era un bien tan escaso que había que optimizarla como fuera posible. Una de las formas más simples es hacer que el fondo fuera una estructura repetitiva.
Vamos a ver un ejemplo real: en el juego Manic Miner, ésta era la apariencia de uno de los niveles:
Si superponemos una cuadrícula de 8x8 (en el juego original, o de 16x16 en esta imagen ampliada), vemos claramente que el fondo es una serie de casillas repetitivas (que llamaremos "tiles").
Vamos a usar esta técnica para incorporar un fondo a nuestro juego...
Ese fondo se podría plantear como un array de dos dimensiones, en el que un número 0 indicara que no hay que dibujar nada, y un 1 detallara dónde debe haber un fragmento de pared (obviamente, se podrían usar más números para indicar más tipos de casillas repetitivas en el fondo, pero nosotros no lo haremos por ahora):
static public byte[,] fondo = { {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} };
Y serían necesarios ciertos datos adicionales, como el ancho y alto de estas casillas, el margen que debemos dejar por encima y a su izquierda cuando las dibujemos (en caso de que no vayan a ocupar toda la pantalla), y las nuevas imágenes que representen esos "tiles":
static byte anchoFondo = 20; static byte altoFondo = 16; static short margenXFondo = 80; static byte margenYFondo = 30; static byte anchoCasillaFondo = 32; static byte altoCasillaFondo = 32; static Imagen imgPared;
Y esa imagen (o imágenes, si fueran varias) deberíamos cargarla desde "InicializarJuego":
public static void InicializarJuego() { ... imgPared = new Imagen("pared.png"); ...
Así, a la hora de dibujar, bastaría recorrer el array bidimensional con dos "for" anidados, dibujando el "tile" correspondiente cuando en esa posición del array no haya un 0 sino otro número:
public static void Dibujar() { // Pared de fondo for (int fila = 0; fila < altoFondo; fila++) // Fondo for (int col = 0; col < anchoFondo; col++) if (fondo[fila, col] == 1) imgPared.DibujarOculta( margenXFondo + col * anchoCasillaFondo, margenYFondo + fila * altoCasillaFondo); ...
Ahora la apariencia sería ésta:
Si tuviéramos unos cuantos tipos de casillas, usaríamos un "switch", en vez de varios "if" seguidos.
El fuente completo sería:
// Primer mini-esqueleto de juego en modo gráfico // Versión "g" using System; using System.Threading; // Para Thread.Sleep public class Juego05g { 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 byte anchoFondo = 20; static byte altoFondo = 16; static short margenXFondo = 80; static byte margenYFondo = 30; static byte anchoCasillaFondo = 32; static byte altoCasillaFondo = 32; static Imagen imgPared; static public byte[,] fondo = { {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} }; 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"); imgPared = new Imagen("pared.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 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; } 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); // Pared de fondo for (int fila = 0; fila < altoFondo; fila++) // Fondo for (int col = 0; col < anchoFondo; col++) if (fondo[fila, col] == 1) imgPared.DibujarOculta( margenXFondo + col * anchoCasillaFondo, margenYFondo + fila * altoCasillaFondo); 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 }
Esta forma de trabajar, en la que tenemos un array de "bytes" y sólo hemos cargado la imagen una vez, para irla dibujando en distintas posiciones, permite aprovechar mejor la memoria que si fuera un array de "elementos gráficos", cada uno con su imagen (repetitiva) y demás características. A cambio, complica un poco la detección de colisiones con el fondo. Por eso, como en los ordenadores actuales, la memoria no es tan escasa como en los años 80 (que es cuando se crearon la mayoría de los juegos que estamos tomando como ejemplo), veremos una versión alternativa, en la que a partir de ese array de dos dimensiones se cree una serie de elementos gráficos que representen los objetos del fondo.
Ejercicio propuesto: Amplía esta versión del juego, para que el fondo no esté formato por un único tipo de casilla, sino por dos o más.