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:

Miner, nivel 10

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

Miner, nivel 10 cuadriculado

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:

Colision en modo grafico

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.