Por Nacho Cabanes
La versión 0.13 del juego todavía no hace casi nada de lo que hacía el juego original: no podemos recoger tesoros, no perdemos aire ni energía, apenas hay enemigos, no existe una tabla de las mejores puntuaciones, no hay animaciones cuando el personaje anda, ni cuando muere, ni cuando muere un enmigo...
A pesar de eso, el fuente del fichero "Juego.cs" ya ocupa 578 líneas, y empieza a ser trabajoso encontrar cada zona que queremos ampliar. A medida que lo sigamos ampliando, cada vez será más trabajoso... salvo que hagamos un rediseño.
Es el momento de pensar cómo podemos desglosar ese fuente en varios, de forma que cada uno tenga un propósito claro. Una forma de hacerlo sería replantear el problema como una serie de objetos que interaccionan. Tendríamos que pensar qué elementos forman nuestro juego, qué relaciones hay entre ellos y cómo se comunican...
Vamos a pensar primero cómo sería el diagrama de clases de todo el juego (o casi), y luego lo reduciremos para quedarnos con la parte que realmente nos interesa, y que iremos ampliando poco a poco.
En una primera aproximaci�n, podemos pensar que en el juego aparecen objetos como: un personaje, un enemigo, una pantalla de presentaci�n, un mapa...
Además, desde la pantalla principal se debería poder acceder a los créditos (lista de programadores que han realizado el programa) y a la tabla de records.
Incluso podríamos afinar un poco más lo que ocurre durante una partida, distinguiendo dos tipos de enemigos: enemigos que podemos matar (como el escorpión y la araña que avanza por el suelo) y enemigos que nos matan a nosotros pero contra los que no podemos luchar (como la araña que se descuelga del techo y el ácido). Todavía no incorporaremos todos estos enemigos, pero al menos sí los tendremos en cuenta en la primera aproximación del diagrama de clases, que podría quedar así:
En realidad, el diagrama debería incluir también las clases auxiliares que son parte del esqueleto: Hardware, Imagen, Fuente... de modo que el diagrama completo podría ser así:
Pero a nosotros todavía no nos interesa crear una estructura tan grande. De momento comenzaremos por crear sólo los objetos que ya existían en nuestro juego, aunque antes eran parte de un único fuente de gran tamaño:
Todo ello podría quedar así:
Por tanto, el trabajo de esta entrega será repartir lo que antes era un fuente y ahora estará repartido en seis.
Vamos a ver el fuente final de todos ellos...
La clase "Juego", que se limita a coordinar a las demás clases de alto nivel (por ahora, Presentación y Partida), sería:
/** * Juego: Clase "de apoyo" casi vacía que lanza la presentación * * @see Hardware * @author 1-DAI IES San Vicente 2009/10 */ /* -------------------------------------------------- Parte de Death Pit - Remake Versiones hasta la fecha: Num. Fecha Por / Cambios --------------------------------------------------- 0.01 08-Sep-2009 Nacho Cabanes Version inicial: muestra una imagen 0.02 07-Oct-2009 Nacho Cabanes Mueve el personaje hacia la derecha hasta llegar al margen 0.03 13-Oct-2009 Nacho Cabanes El personaje se mueve con flechas Un primer enemigo que se mueve a la vez 0.04 16-Oct-2009 Nacho Cabanes Primera pantalla de presentacion El primer enemigo rebota en los lados Un segundo enemigo nos persigue 0.05 20-Oct-2009 Nacho Cabanes Array de enemigos (murcielagos) que rebotan en los lados 0.06 28-Oct-2009 Nacho Cabanes Array bidimensional para imagen de fondo 0.07 04-Nov-2009 Nacho Cabanes El personaje se mueve solo por los "huecos" Eliminados los enemigos (por ahora) Renombradas variables i,j a fila,colum 0.08 10-Nov-2009 Nacho Cabanes Varias funciones para que el fuente sea mas modular 0.09 16-Nov-2009 Nacho Cabanes Varias pantallas conectadas 0.10 24-Nov-2009 Nacho Cabanes Un enemigo que se mueve "por los huecos" 0.11 02-Dic-2009 Nacho Cabanes Si chocamos con enemigo perdemos una vida 0.12 04-Dic-2009 Nacho Cabanes El personaje cambia de imagen según la dirección Si choco con un enemigo de lado, muere él y me da puntos 0.13 11-Dic-2009 Joaquín, Alejandro, Miguel; retoques por Nacho Cuando acaban las vidas o se pulsa ESC, acaba la partida, se vuelve a la presentación y se puede volver a empezar 0.14 23-Dic-2009 Nacho Cabanes Primera versión desglosada en clases. Casi todo el contenido de "Juego" pasa a "Partida". ---------------------------------------------------- */ public class Juego { private Presentacion presentacion; private Partida partida; // Inicialización al comenzar la sesión de juego public Juego() { // Inicializo modo grafico 800x600 puntos, 24 bits de color Hardware.Inicializar(800, 600, 24); // Inicializo componentes del juego presentacion = new Presentacion(); partida = new Partida(); } // --- Comienzo de un nueva partida: reiniciar variables --- public void Ejecutar() { do { presentacion.Ejecutar(); switch( presentacion.GetOpcionEscogida() ) { case Presentacion.OPC_PARTIDA: partida.BuclePrincipal(); break; } } while (presentacion.GetOpcionEscogida() != Presentacion.OPC_SALIR ); } // --- Cuerpo del programa ----- public static void Main() { Juego juego = new Juego(); juego.Ejecutar(); } } /* fin de la clase Juego */
Y la "Presentación" será:
/** * Presentacion: pantalla de presentacion * * @see Hardware ElemGrafico Juego * @author 1-DAI IES San Vicente 2009/10 */ /* -------------------------------------------------- Parte de Death Pit - Remake Versiones hasta la fecha: Num. Fecha Por / Cambios --------------------------------------------------- 0.14 23-Dic-2009 Nacho Cabanes Primera versión de Presentacion, desglosado de "Juego.cs" No añade posibilidades nuevas. ---------------------------------------------------- */ public class Presentacion { // Atributos private ElemGrafico imagen; private Fuente fuenteSans18; private int opcionEscogida; // Opciones posibles public const byte OPC_PARTIDA = 0; public const byte OPC_SALIR = 1; /// Constructor public Presentacion() // Constructor { imagen = new ElemGrafico("imagenes/present.png"); fuenteSans18 = new Fuente("FreeSansBold.ttf",18); } /// Lanza la presentacion public void Ejecutar() { //dibujo la imagen de la presentacion imagen.DibujarOculta(); // Esscribo avisos de las teclas utilizables Hardware.EscribirTextoOculta( "Pulsa Espacio para jugar", 300, 550, 0xAA, 0xAA, 0xAA, fuenteSans18); Hardware.EscribirTextoOculta( "o pulsa T para terminar", 300, 575, 0xAA, 0xAA, 0xAA, fuenteSans18); // Finalmente, muestro en pantalla Hardware.VisualizarOculta(); //hasta que se pulse espacio do { } while ((! Hardware.TeclaPulsada(Hardware.TECLA_ESP) ) && (! Hardware.TeclaPulsada(Hardware.TECLA_T))); opcionEscogida = OPC_PARTIDA; if (Hardware.TeclaPulsada(Hardware.TECLA_T)) opcionEscogida = OPC_SALIR; //borro la presentacion Hardware.BorrarPantallaOculta(0,0,0); } public int GetOpcionEscogida() { return opcionEscogida; } } /* fin de la clase Presentacion */
La "Partida", que controla la lógica de juego, será:
/** * Partida: Logica de juego para una partida * * @see Hardware * @author 1-DAI IES San Vicente 2009/10 */ /* -------------------------------------------------- Parte de Death Pit - Remake Versiones hasta la fecha: Num. Fecha Por / Cambios --------------------------------------------------- 0.01 08-Sep-2009 Nacho Cabanes Version inicial: muestra una imagen 0.02 07-Oct-2009 Nacho Cabanes Mueve el personaje hacia la derecha hasta llegar al margen 0.03 13-Oct-2009 Nacho Cabanes El personaje se mueve con flechas Un primer enemigo que se mueve a la vez 0.04 16-Oct-2009 Nacho Cabanes Primera pantalla de presentacion El primer enemigo rebota en los lados Un segundo enemigo nos persigue 0.05 20-Oct-2009 Nacho Cabanes Array de enemigos (murcielagos) que rebotan en los lados 0.06 28-Oct-2009 Nacho Cabanes Array bidimensional para imagen de fondo 0.07 04-Nov-2009 Nacho Cabanes El personaje se mueve solo por los "huecos" Eliminados los enemigos (por ahora) Renombradas variables i,j a fila,colum 0.08 10-Nov-2009 Nacho Cabanes Varias funciones para que el fuente sea mas modular 0.09 16-Nov-2009 Nacho Cabanes Varias pantallas conectadas 0.10 24-Nov-2009 Nacho Cabanes Un enemigo que se mueve "por los huecos" 0.11 02-Dic-2009 Nacho Cabanes Si chocamos con enemigo perdemos una vida 0.12 04-Dic-2009 Nacho Cabanes El personaje cambia de imagen según la dirección Si choco con un enemigo de lado, muere él y me da puntos 0.13 11-Dic-2009 Joaquín, Alejandro, Miguel; retoques por Nacho Cuando acaban las vidas o se pulsa ESC, acaba la partida, se vuelve a la presentación y se puede volver a empezar 0.14 23-Dic-2009 Nacho Cabanes Primera versión desglosada en clases. Casi todo el contenido de "Juego" pasa a "Partida". ---------------------------------------------------- */ public class Partida { // Componentes del juego private Mapa miMapa; private Escorpion enemigo; private Personaje personaje; ElemGrafico imagenPersonaje; // Imagen del personaje (se muestra al perder vidas) Fuente fuente18; // Tipo de letra para mensajes int puntos = 0; bool partidaTerminada; // Si ha terminado una partida puntual // Inicialización al comenzar la sesión de juego public Partida() { // Inicializo componentes del juego miMapa = new Mapa(); enemigo = new Escorpion(this); personaje = new Personaje(this); // Cargo imagenes y tipos de letra imagenPersonaje = new ElemGrafico("imagenes/personaje.png"); fuente18 = new Fuente("FreeSansBold.ttf", 18); } // --- Comienzo de un nueva partida: reiniciar variable --- void inicializarPartida() { puntos = 0; personaje.MoverA(70,50); personaje.SetVidas(5); miMapa.IrAHabitacion(0,0); enemigo.Recolocar(); partidaTerminada = false; mostrarVidasRestantes(); } // --- Coloca el enemigo en cualquier posición "libre" de la pantalla actual ----- void comprobarTeclas() { // Muevo si se pulsa alguna flecha del teclado if (Hardware.TeclaPulsada(Hardware.TECLA_DER) ) personaje.MoverDerecha(); if(Hardware.TeclaPulsada(Hardware.TECLA_ARR) ) personaje.MoverArriba(); if (Hardware.TeclaPulsada(Hardware.TECLA_IZQ) ) personaje.MoverIzquierda(); if (Hardware.TeclaPulsada(Hardware.TECLA_ABA) ) personaje.MoverAbajo(); if (Hardware.TeclaPulsada(Hardware.TECLA_ESC)) partidaTerminada = true; } // --- Animación de los enemigos y demás objetos "que se muevan solos" ----- void moverElementos() { enemigo.Mover(); } // --- Comprobar colisiones de enemigo con personaje, etc --- void comprobarColisiones() { if ((personaje.GetX()==enemigo.GetX()) && (personaje.GetY()==enemigo.GetY())) { if ((personaje.GetDireccion() == ElemGrafico.DERECHA) || (personaje.GetDireccion() == ElemGrafico.IZQUIERDA)) matarEnemigo(); else morir(); } } // --- Matar enemigo: aumentar puntos y recolocarlo --- void matarEnemigo() { puntos += 10; enemigo.Recolocar(); } // --- Recolocar enemigo (llamado al cambiar de habit. p.ej.) --- public void RecolocarEnemigo() { enemigo.Recolocar(); } // --- Morir personaje: mostrar vidas restantes, etc --- void morir() { personaje.Morir(); mostrarVidasRestantes(); // Vuelvo a colocar el enemigo al azar enemigo.Recolocar(); // Si no hay vidas, se acabó la partida if (personaje.GetVidas() == 0) partidaTerminada = true; } // --- Mostrar vidas restantes --- void mostrarVidasRestantes() { int i; // Rectángulo verde Hardware.RectanguloRellenoRGBA( 0,0,490,350, // Posicion, ancho y alto de la pantalla 0,255,0, // En color verde 200); // Con algo de transparencia // Dibujo las vidas restantes Hardware.EscribirTextoOculta( "Te quedan", 250, 100, 0, 0, 0, fuente18); Hardware.EscribirTextoOculta( personaje.GetVidas().ToString(), 250, 150, 0xFF, 0, 0, fuente18); Hardware.EscribirTextoOculta( "vidas", 250, 200, 0, 0, 0, fuente18); for (i=0; i<personaje.GetVidas(); i++) { imagenPersonaje.MoverA( 130 , (short) (50+ i*50) ); imagenPersonaje.DibujarOculta(); } // Aviso de la tecla para volver Hardware.EscribirTextoOculta( "Pulsa V para volver al juego", 120, 350, 0xAA, 0xAA, 0xAA, fuente18); // Y quito el aviso de "ESC para salir" Hardware.RectanguloRellenoRGBA( 160, 370, 600,550, 0,0,0, 255); // Finalmente, muestro en pantalla Hardware.VisualizarOculta(); // Espero hasta que se pulse la tecla de volver do { } while (! Hardware.TeclaPulsada(Hardware.TECLA_V) ); } // --- Dibujar en pantalla todos los elementos visibles del juego --- void dibujarElementos() { Hardware.BorrarPantallaOculta(0,0,0); miMapa.Dibujar(); personaje.DibujarOculta(); enemigo.DibujarOculta(); Hardware.EscribirTextoOculta( "Puntos", 600, 100, 0xAA, 0xAA, 0xAA, fuente18); Hardware.EscribirTextoOculta( puntos.ToString(), 600, 140, 0xAA, 0xAA, 0xAA, fuente18); Hardware.EscribirTextoOculta( "Pulsa ESC para salir", 170, 380, 0xAA, 0xAA, 0xAA, fuente18); // Finalmente, muestro en pantalla Hardware.VisualizarOculta(); } // --- Pausa tras cada fotograma de juego, para velocidad de 25 fps ----- void pausaFotograma() { Hardware.Pausa( 40 ); } // --- Bucle principal de juego ----- public void BuclePrincipal() { inicializarPartida(); do { comprobarTeclas(); moverElementos(); comprobarColisiones(); dibujarElementos(); pausaFotograma(); } while (! partidaTerminada); } // --- Devuelve el mapa, para que lo consulten el personaje // y el enemigo antes de moverse ----- public Mapa GetMapa() { return miMapa; } } /* fin de la clase Juego */
Un "Enemigo Mortal" será:
/** * EnemigoMortal: Un enemigo que podemos matar * * @see Hardware ElemGrafico Juego * @author 1-DAI IES San Vicente 2009/10 */ /* -------------------------------------------------- Parte de Death Pit - Remake Versiones hasta la fecha: Num. Fecha Por / Cambios --------------------------------------------------- 0.14 23-Dic-2009 Nacho Cabanes Primera versión del EnemigoMortal ---------------------------------------------------- */ using System; // Para numeros aleatorios: System.Random public class EnemigoMortal : ElemGrafico { short contadorFotogramas; // Contador de fotograma actual short pausaFotogramas; // Para que no se mueva en cada fotograma del juego Random numAleatorio; // Para que se mueva al azar protected Partida miPartida; // Para leer datos del mapa public EnemigoMortal() { numAleatorio = new Random( DateTime.Now.Millisecond ); contadorFotogramas = 0; pausaFotogramas = 10; ancho = 70; alto = 50; incrX = ancho; incrY = alto; } public void Morir() { } public new void Mover() { // El enemigo solo se mueve un fotograma de cada varios contadorFotogramas ++; if (contadorFotogramas == pausaFotogramas) { contadorFotogramas = 0; x += incrX; y += incrY; } // Para la siguiente posición, escojo al azar y miro si realmente se puede mover bool posibleMover = false; do { int numeroAzar = (int) numAleatorio.Next(0,100); if (numeroAzar < 25) // Intento derecha { if ((x < miPartida.GetMapa().GetMaxX()) && // Si no ha llegado al borde (miPartida.GetMapa().EsPosibleMover((short) (x+ancho),y))) { incrX = ancho; incrY = 0; posibleMover = true; } } else if (numeroAzar < 50) // Intento izquierda { if ((x > 0) && // Si no ha llegado al borde (miPartida.GetMapa().EsPosibleMover((short) (x-ancho),y))) { incrX = (short) -ancho; incrY = 0; posibleMover = true; } } else if (numeroAzar < 75) // Intento arriba { if ((y > 0) && // Si no ha llegado al borde (miPartida.GetMapa().EsPosibleMover(x,(short) (y-alto)))) { incrX = 0; incrY = (short) -alto; posibleMover = true; } } else // Intento abajo { if ((y < miPartida.GetMapa().GetMaxY()) && // Si no ha llegado al borde (miPartida.GetMapa().EsPosibleMover(x,(short) (y+alto)))) { incrX = 0; incrY = alto; posibleMover = true; } } } while (! posibleMover); } // --- Coloca el enemigo en cualquier posición "libre" de la pantalla actual ----- public void Recolocar() { do { x = (short) ((numAleatorio.Next(0,miPartida.GetMapa().GetMaxX()-2)+1)*ancho); y = (short) ((numAleatorio.Next(0,miPartida.GetMapa().GetMaxY()-2)+1)*alto); } // TODO: Falta asignar incrementos "razonables" while (! miPartida.GetMapa().EsPosibleMover(x,y)); } } /* fin de la clase EnemigoMortal */
El "Escorpión" será una subclase de "Enemigo Mortal", que sólo cambia en la imagen
/** * Escorpion: Un enemigo que podemos matar * * @see Hardware ElemGrafico EnemigoMortal Juego * @author 1-DAI IES San Vicente 2009/10 */ /* -------------------------------------------------- Parte de Death Pit - Remake Versiones hasta la fecha: Num. Fecha Por / Cambios --------------------------------------------------- 0.14 23-Dic-2009 Nacho Cabanes Primera versión del Escorpion ---------------------------------------------------- */ public class Escorpion: EnemigoMortal { public Escorpion(Partida p) { miPartida = p; CargarImagen("imagenes/escorpion.png"); } } /* fin de la clase Escorpion */
Y el personaje que nosotros controlamos quedaría:
/** * Personaje: El personaje que es controlado por el usuario * * @see Hardware ElemGrafico Juego * @author 1-DAI IES San Vicente 2009/10 */ /* -------------------------------------------------- Parte de Death Pit - Remake Versiones hasta la fecha: Num. Fecha Por / Cambios --------------------------------------------------- 0.14 23-Dic-2009 Nacho Cabanes Primera versión del Personaje, desglosado de "Juego.cs" No añade posibilidades nuevas. ---------------------------------------------------- */ public class Personaje : ElemGrafico { // Datos del personaje short vidas; protected Partida miPartida; public Personaje(Partida p) { miPartida = p; ancho =70; alto = 50; vidas = 5; x = 1; y = 1; incrX = 70; incrY = 50; //Antes era: CargarImagen("imagenes/personaje.png"); //##TODO: Falta secuencia de imágenes direccion = IZQUIERDA; CargarSecuencia( DERECHA, new string[] {"imagenes/personajed1.png"} ); CargarSecuencia( IZQUIERDA, new string[] {"imagenes/personajei1.png"} ); CargarSecuencia( ARRIBA, new string[] {"imagenes/personajea1.png"} ); CargarSecuencia( ABAJO, new string[] {"imagenes/personajea1.png"} ); } public void MoverDerecha() { direccion = DERECHA; if (x == miPartida.GetMapa().GetMaxX()) { x = 0; miPartida.GetMapa().IrAHabitacionDerecha(); miPartida.RecolocarEnemigo(); } else if (x < miPartida.GetMapa().GetMaxX()) if (miPartida.GetMapa().EsPosibleMover((short) (x+incrX),y)) x += incrX; } public void MoverIzquierda() { direccion = IZQUIERDA; if (x == 0) { x = miPartida.GetMapa().GetMaxX(); miPartida.GetMapa().IrAHabitacionIzquierda(); miPartida.RecolocarEnemigo(); } else if ( x > 0) if (miPartida.GetMapa().EsPosibleMover((short) (x-incrX),y)) x -= incrX; } public void MoverArriba() { direccion = ARRIBA; if (y == 0) { y = miPartida.GetMapa().GetMaxY(); miPartida.GetMapa().IrAHabitacionArriba(); miPartida.RecolocarEnemigo(); } else if ( y > 0) if (miPartida.GetMapa().EsPosibleMover(x, (short) (y-incrY))) y -= incrY; } public void MoverAbajo() { direccion = ABAJO; if (y == miPartida.GetMapa().GetMaxY()) { y = 0; miPartida.GetMapa().IrAHabitacionAbajo(); miPartida.RecolocarEnemigo(); } else if (y < miPartida.GetMapa().GetMaxY()) if (miPartida.GetMapa().EsPosibleMover(x, (short) (y+incrY))) y += incrY; } public int GetVidas() { return vidas; } public void SetVidas(short n) { vidas = n; } public void Morir() { vidas--; } } /* fin de la clase Personaje */
También puedes descargar todo el paquete, incluyendo todos los fuentes, el ejecutable (en la carpeta BIN/DEBUG), las imágenes, y el proyecto de SharpDevelop, en un fichero ZIP de tamaño cercano a 1 Mb.
Siguiente entrega...