30. Un primer juego completo en modo gráfico.

Tenemos ya un esqueleto de juego que hace un poco de todo, pero que todavía no es muy jugable: es "demasiado difícil" y "demasiado repetitivo", además de que tiene otras muchas carencias que de momento no nos van a preocupar:

  • Sólo hay una "pantalla" que recorrer.
  • Sólo hay un tipo de enemigos, un tipo de obstáculos, un tipo de premios...
  • No hay ningún movimiento "avanzado" (no podemos saltar, ni caer de las plataformas).
  • No hay animaciones en los movimientos.
  • No tiene sonido.
  • No se puede jugar con joystick ni gamepad.
  • ...

Vamos a ignorar esas carencias por ahora, porque las iremos solucionando más adelante, y a juntar todo lo que hemos visto, para crear un juego sencillo pero "real", con un nivel de dificultad creciente. El funcionamiento será el siguiente:

  • Al comenzar el juego, se nos mostrará una pantalla de presentación, que nos permitirá ver una pequeña ayuda, una pantalla de créditos o comenzar una nueva partida.
  • En la partida, nuestro personaje aparecerá en una primera pantalla, en la que habrá paredes que no podremos atravesar, un premio que podremos recoger, pero no habrá ningún obstáculo ni ningún enemigo.
  • Cuando recojamos ese premio, pasaremos a un segundo nivel, que tendrá las mismas paredes, pero dos premios en (nuevas) posiciones al azar, un obstáculo fijo y un enemigo móvil.
  • Cuando recojamos esos dos premios, pasaremos a un tercer nivel, también con las mismas paredes, pero tres premios, dos obstáculos fijo y dos enemigos móviles.
  • En general, cuando recojamos todos los premios de un nivel, pasaremos a un siguiente nivel, que tendrá un premio más que el anterior (hasta un máximo de 20), un enemigo móvil más que el anterior (hasta un máximo de 15) y un obstáculo fijo más que el anterior (hasta un máximo de 20).
  • Obtendremos 10 puntos por cada premio recogido.
  • Si tocamos un obstáculo o un enemigo, perderemos una vida. Cuando perdamos las tres vidas iniciales, terminará la partida y volveremos a la pantalla de presentación.

El fuente completo podría ser algo como:

// Primer mini-juego en modo gráfico: Freddy One
 
using System;
using System.Threading; // Para Thread.Sleep
 
public class FreddyOne
{
    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 = 24;
    static byte altoFondo = 17;
    static short margenXFondo = 10;
    static byte margenYFondo = 30;
    static byte anchoCasillaFondo = 32;
    static byte altoCasillaFondo = 32;
    static Imagen imgPared;
    static public byte[,] fondo =
    {
        {1,1,1,1,1,1,1,1,1,1,1,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,1,0,0,0,0,0,0,0,0,0,1},
        {1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1},
        {1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1},
        {1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1},
        {1,0,0,0,0,0,0,0,0,0,0,0,0,1,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,0,0,0,0,1},
        {1,0,0,0,0,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,0,0,0,0,1},
        {1,0,0,0,0,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,0,0,0,0,1},
        {1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,1},
        {1,0,0,0,0,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,0,0,0,0,1},
        {1,0,0,0,0,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,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,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 ElemGrafico[] fondos;
    static int elementosFondo;
 
    static bool juegoTerminado;
    static int vidas;
    static int puntos;
    static bool partidaTerminada;
    static Random generador;
 
    static Imagen fondoPresentacion;
    static Imagen fondoAyuda;
    static Imagen fondoCreditos;
    static Imagen logoPresentacion;
 
    // Cantidad de elementos que se ven en pantalla en el nivel actual
    static int premiosVisibles;
    static int enemigosVisibles;
    static int obstaculosVisibles;
 
    // Cantidad de premios que quedan para cambiar de nivel
    static int premiosRestantes;
 
 
    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 = 20;
        numEnemigos = 12;
        numObstaculos = 20;
        obstaculos = new ElemGrafico[numObstaculos];
        enemigos = new ElemGrafico[numEnemigos];
        premios = new ElemGrafico[numPremios];
        generador = new Random();
 
        // Cuento la cantidad de elementos reales en el fondo        
        elementosFondo = 0;
        for (int fila = 0; fila < altoFondo; fila++)  // Fondo
            for (int col = 0; col < anchoFondo; col++)
                if (fondo[fila, col] != 0)
                    elementosFondo++;
        // Y reservo espacio para el array que los contiene
        fondos = new ElemGrafico[elementosFondo];
        // y para cada uno de uno de ellos
        int posicFondo = 0;
        for (int fila = 0; fila < altoFondo; fila++)  // Fondo
            for (int col = 0; col < anchoFondo; col++)
                if (fondo[fila, col] != 0)
                {
                    fondos[posicFondo].x =  margenXFondo + col * anchoCasillaFondo;
                    fondos[posicFondo].y =  margenYFondo + fila * altoCasillaFondo;
                    fondos[posicFondo].imagen = new Imagen("pared.png");
                    fondos[posicFondo].ancho = anchoCasillaFondo;
                    fondos[posicFondo].alto = altoCasillaFondo;
                    fondos[posicFondo].visible =  true;
                    posicFondo++;
                }
 
        // 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");
        logoPresentacion = new Imagen("freddyRotulo.png");
        fondoAyuda = new Imagen("ayuda.jpg");
        fondoCreditos = new Imagen("creditos.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 = false;
            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].incrX = 5;
            enemigos[i].visible = false;
            enemigos[i].ancho = 36;
            enemigos[i].alto = 42;
            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));
        }
 
        for (int i = 0; i < numPremios; i++)  // Premios
            do
            {
                premios[i].x = generador.Next(50, 700);
                premios[i].y = generador.Next(30, 550);
                premios[i].visible = false;
                premios[i].ancho = 34;
                premios[i].alto = 18;
            }
            while (! EsPosibleMover(premios[i].x, premios[i].y,
                premios[i].x+premios[i].ancho, premios[i].y+premios[i].alto));
 
        premiosVisibles = 1;
        premios[0].visible = true;
        enemigosVisibles = 0;
        obstaculosVisibles = 0;
        premiosRestantes = 1;
    }
 
 
    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);
 
            logoPresentacion.DibujarOculta(60, 100);
 
            Hardware.EscribirTextoOculta("Escoja una opción:",
                     310, 300, // Coordenadas
                     200, 200, 200, // Colores
                     tipoDeLetra);
 
            Hardware.EscribirTextoOculta("J.- Jugar una partida",
                     250, 390, // Coordenadas
                     200, 200, 200, // Colores
                    tipoDeLetra);
 
            Hardware.EscribirTextoOculta("A.- Ayuda",
                     250, 430, // Coordenadas
                     200, 200, 200, // Colores
                     tipoDeLetra);
 
            Hardware.EscribirTextoOculta("C.- Créditos",
                     250, 470, // Coordenadas
                     200, 200, 200, // Colores
                     tipoDeLetra);
 
            Hardware.EscribirTextoOculta("S.- Salir",
                     250, 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",
          "Cuando recojas todos los premios",
          "  avanzarás de nivel.",
          "Comienzas con 3 vidas.",
          "Si tocas un obstáculo o un enemigo,.",
          "  perderás una de ellas."
        };
 
        // ---- Pantalla de presentación --
        Hardware.BorrarPantallaOculta(0, 0, 0);
 
        // Fondo de la presentación
        fondoAyuda.DibujarOculta(0, 0);
 
        // Marcador
        Hardware.EscribirTextoOculta("Freddy One - Ayuda",
                 300, 100, // Coordenadas
                 255, 255, 255, // Colores
                tipoDeLetra);
 
        // Textos repetitivos
        short posicYtexto = 250;
        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);
 
        // También muestro imágenes aclaradoras
        premios[0].imagen.DibujarOculta(350, 252);
        obstaculos[0].imagen.DibujarOculta(510, 280);
        enemigos[0].imagen.DibujarOculta(560, 260);
 
        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("Freddy One - Créditos",
                 300, 100, // Coordenadas
                 255, 255, 255, // Colores
                tipoDeLetra);
 
        Hardware.EscribirTextoOculta("Por Nacho Cabanes, 2011",
                  150, 200, // Coordenadas
                  200, 200, 200, // Colores
                 tipoDeLetra);
 
        Hardware.EscribirTextoOculta("(Curso 2011-2012, primer juego de ejemplo)",
                  150, 250, // Coordenadas
                  200, 200, 200, // Colores
                 tipoDeLetra);
 
        Hardware.EscribirTextoOculta("Imágenes \"in-game\", de Electro Freddy, (c) SoftSpot 1984",
                  150, 300, // Coordenadas
                  200, 200, 200, // Colores
                 tipoDeLetra);
 
        Hardware.EscribirTextoOculta("Resto de imágenes por Nacho Cabanes",
                  150, 350, // Coordenadas
                  200, 200, 200, // Colores
                 tipoDeLetra);
 
        Hardware.EscribirTextoOculta("Se puede distribuir libremente",
                  150, 400, // 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 < elementosFondo; i++)  // Imágenes del fondo
            fondos[i].imagen.DibujarOculta(fondos[i].x, fondos[i].y);
 
        for (int i = 0; i < numObstaculos; i++)  // Obstáculos
            if (obstaculos[i].visible)
                obstaculos[i].imagen.DibujarOculta(obstaculos[i].x, obstaculos[i].y);
 
        for (int i = 0; i < numEnemigos; i++)  // Enemigos
            if (enemigos[i].visible)
                enemigos[i].imagen.DibujarOculta(enemigos[i].x, 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)
            && EsPosibleMover(personaje.x + personaje.incrX, 
                  personaje.y, 
                  personaje.x + personaje.ancho + personaje.incrX,
                  personaje.y + personaje.alto))
                personaje.x += personaje.incrX;
 
        if (Hardware.TeclaPulsada(Hardware.TECLA_IZQ)
            && EsPosibleMover(personaje.x - personaje.incrX, personaje.y,
                    personaje.x + personaje.ancho - personaje.incrX, personaje.y + personaje.alto))
                personaje.x -= personaje.incrX;
        if (Hardware.TeclaPulsada(Hardware.TECLA_ARR)
            && EsPosibleMover(personaje.x, personaje.y - personaje.incrY,
                    personaje.x + personaje.ancho, personaje.y + personaje.alto - personaje.incrY))
                personaje.y -= personaje.incrY;
        if (Hardware.TeclaPulsada(Hardware.TECLA_ABA)
            && EsPosibleMover(personaje.x, personaje.y + personaje.incrY,
                    personaje.x + personaje.ancho, personaje.y + personaje.alto + personaje.incrY))
                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;
                premiosRestantes--;
                if (premiosRestantes == 0)
                    AvanzarNivel();
            }
        }
 
        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 bool EsPosibleMover(int x, int y, int xFin, int yFin)
    {
        // Veo si choca con algún elemento del fondo
        for (int i = 0; i < elementosFondo; i++)
            if ((fondos[i].x + fondos[i].ancho > x)
              && (fondos[i].x < xFin)
              && (fondos[i].y + fondos[i].alto > y)
              && (fondos[i].y < yFin))
            return false;
 
        // Si no ha chocado con ninguno, es posible moverse a esa posición
        return true;
    }
 
 
    public static void AvanzarNivel()
    {
        // Borro la pantalla y aviso del nuevo nivel
        Hardware.Pausa(200);
        Hardware.BorrarPantallaOculta();
        Hardware.EscribirTextoOculta("Nivel: "+Convert.ToString(premiosVisibles+1),
                 350, 300, /* Coordenadas */ 255, 255, 255, /* Colores */ tipoDeLetra);
        Hardware.VisualizarOculta();
        Hardware.Pausa(1000);
 
        // Activo un enemigo mas
        if (enemigosVisibles < numEnemigos)
        {
            enemigosVisibles++;
            for (int i = 0; i < enemigosVisibles; i++)
                enemigos[i].visible = true;
        }
 
        // Y un obstaculo mas
        if (obstaculosVisibles < numObstaculos)
        {
            obstaculosVisibles++;
            for (int i = 0; i < obstaculosVisibles; i++)
                obstaculos[i].visible = true;
        }
 
        // Y un premio mas, y los recoloco
        if (premiosVisibles < numPremios)
        {
            premiosVisibles++;
            for (int i = 0; i < premiosVisibles; i++)
                do
                {
                    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;
                }
                while (!EsPosibleMover(premios[i].x, premios[i].y,
                        premios[i].x + premios[i].ancho, premios[i].y + premios[i].alto));
        }
        premiosRestantes = premiosVisibles;
 
        personaje.x = personaje.xInicial;
        personaje.y = personaje.yInicial;
    }
 
 
    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
}