28 diciembre 2008

Remake (parcial) de Fruity Frank... 23 - Un movimiento del personaje más suave

Para hacer que el movimiento del personaje sea más suave, la solución puede ser no sumar 40 a su coordenada X cada ver que se pulse la tecla "derecha" (por ejemplo), sino aumentar poco a poco hasta llegar a esos 40. Aun así, cuando pulsemos la tecla "derecha", su posición deberá seguir aumentando durante varios "fotogramas" del juego, por lo que podemos usar una variable booleana para comprobar si estamos en medio de un movimiento:

Algunos de los atributos, que antes eran
    // El personaje se moverá de 1 en 1 casilla,
// cuando se pulse cada tecla
incrX = 41;
incrY = 34;

ahora podrían ser
    // El personaje se moverá de varios en varios pixeles
// cuando se pulse cada tecla
anchoCasilla = 41;
altoCasilla = 34;
incrX = 4;
incrY = 4;
enMovimiento = false;

Y la rutina de mover a la derecha, que antes era
    public  void MoverDerecha()
{
if (miJuego.GetNivelActual().EsPosibleMover( (short) (x+incrX), y) )
{
CambiarDireccion(DERECHA);
SiguienteFotograma();
x += incrX;
miJuego.GetMarcador().IncrPuntuacion(
miJuego.GetNivelActual().PuntosMover( x, y ));
}
}

ahora podría modificarse así
    public  void MoverDerecha()
{
if (enMovimiento) // Si ya está en movimiento, salgo
return;
if (miJuego.GetNivelActual().EsPosibleMover( (short) (x+anchoCasilla), y) )
{
CambiarDireccion(DERECHA);
enMovimiento = true;
incrXActual = incrX; incrYActual = 0;
xFinal = (short)(x+anchoCasilla); yFinal = y;
miJuego.GetMarcador().IncrPuntuacion(
miJuego.GetNivelActual().PuntosMover( (short) (x+anchoCasilla), y ));
}
}

Es decir, pone en marcha el movimiento (enMovimiento = true), calcula los incrementos que se van a usar para desplazarse y la posición final, suma puntos... pero deja los movimientos intermedios a otra función, que podría llamarse "Mover", que se llamaría desde "SiguienteFotograma" (en el juego), y que podría ser así:
    public new void Mover()
{
if (!enMovimiento) // Si no está en movimiento, no hay que hacer nada
return;
SiguienteFotograma();
x += incrXActual; // Aumento otro poco la posición
y += incrYActual;
// Compruebo si me paso (ancho y salto pueden no ser proporcionales)
if ((incrXActual > 0 ) && (x >= xFinal))
x = xFinal;
if ((incrXActual < 0 ) && (x <= xFinal))
x = xFinal;
if ((incrYActual > 0 ) && (y >= yFinal))
y = yFinal;
if ((incrYActual < 0 ) && (y <= yFinal))
y = yFinal;
// Compruebo si ya he avanzado toda la casilla, para dejar de mover
if ((x == xFinal) && (y == yFinal))
enMovimiento = false;
}

Esta función aumenta la posición según los incrementos previstos, comprueba que no nos hayamos pasado (porque el incremento puede no ser un divisor exacto del ancho de la casilla), y marca el movimiento como terminado cuando corresponda.


De paso, en esta versión he mejorado un poco los gráficos, conservando la estética inicial y los 16 colores originales, pero con puntos algo más pequeños para que las imágenes sean más nítidas. Las imágenes originales siguen estando disponibles en una subcarpeta, de forma que en la versión definitiva del juego se podría permitir incluso al usuario escoger si quiere usar los gráficos originales del juego o una estética ligeramente revisada.

Como es habitual, todo el fuente del proyecto está en: code.google.com/p/fruityfrank

20 diciembre 2008

Remake (parcial) de Fruity Frank... 22 - Rediseñando usando clases (3 - Personajes animados)

Todavía queda una cosa por hacer para que el juego rediseñado usando clases tenga las mismas funcionalidades que la versión anterior: personajes animados.

Habíamos comentado que lo ideal sería que la clase "ElementoGráfico" nos permitiera tanto manejar elementos con una única imagen estática como elementos que tengan varias imágenes, que actuarían como distintos fotogramas para dar una sensación de movimiento más real. Incluso sería deseable que pidiéramos tener distintas secuencias de imágenes según el sentido en que se muestra el personaje (izquierda, derecha, arriba o abajo).

Tampoco es especialmente difícil. Vamos con ello...

  • Por una parte, ahora tendremos un array de imágenes dentro de la clase ElemGrafico. Nos interesará no declarar el array como "Imagen[,]", sino como "Imagen[][]", para permitir que haya distinto número de fotogramas en un sentido o en otro:


    // La secuencia de imagenes, si es animada
protected Imagen[][] secuencia;


  • Necesitaremos variables para saber en qué dirección nos encontramos, y en qué número de fotograma. Para que el fuente sea más legible, podemos crear variables (o incluso constantes) llamadas ARRIBA, ABAJO, etc.


    protected byte fotogramaActual;
protected byte direccion;
public const byte ABAJO = 0;
public const byte ARRIBA = 1;
public const byte DERECHA = 2;
public const byte IZQUIERDA = 3;


  • Ahora la rutina de Dibujar deberá mostrar una imagen estática o una imagen animada, según lo que hayamos cargado:


    public  void Dibujar()
{
if (contieneSecuencia)
secuencia[direccion][fotogramaActual].Dibujar(x, y);
else if (contieneImagen)
miImagen.Dibujar(x, y);
else
Hardware.ErrorFatal("Se ha intentado dibujar una imagen no cargada!");
}


  • Donde la variable "contieneSecuencia" se declara con valor "false" en el constructor, y recibe el valor "true" cuando cargamos.


    public  ElemGrafico()
{
contieneImagen = false;
contieneSecuencia = false;
direccion = ABAJO;
fotogramaActual = 0;
secuencia = new Imagen[4][];
}


  • Cuando cargamos, recibiremos un "array" de nombres de imágenes. Deberemos reservar espacio para todas ellas, y cargarlas una por una:


    public void CargarSecuencia(byte direcc, string[] nombres)
{
contieneImagen = true;
contieneSecuencia = true;
byte tamanyo = (byte) nombres.Length;
secuencia[direcc] = new Imagen[tamanyo];
for (byte i=0; i< nombres.Length; i++) {
secuencia[direcc][i] = new Imagen(nombres[i]);
}
}


  • Cuando cambiemos de dirección, deberemos volver al fotograma número 0, por si acaso en una dirección hubiera más fotogramas que en otra. Si la dirección que nos piden es en la que ya estamos, no debería hacer falta volver al fotograma 0:


    public  void CambiarDireccion(byte nuevaDir)
{
if (direccion != nuevaDir)
{
direccion = nuevaDir;
fotogramaActual = 0;
}
}


  • En el personaje, cambiará el constructor, que ahora no carga una imagen sino una secuencia:


    public  Personaje(Juego j)  // Constructor
{
miJuego = j;
...
CargarSecuencia( ARRIBA,
new string[] {"imagenes/personajea1.png",
"imagenes/personajea2.png"} );
CargarSecuencia( DERECHA,
new string[] {"imagenes/personajed1.png",
"imagenes/personajed2.png"} );
...
}


  • Además, debemos decidir cuando queremos que cambie la apariencia del personaje. Podría ser en cada fotograma del juego, y entonces incluiríamos "miPersonaje.SiguienteFotograma" dentro del método "SiguienteFotograma" de Juego, pero en el Fruity Frank original, el personaje sólo cambia de forma cuando se mueve, así que lo haremos dentro de "MoverDerecha" y los métodos similares:


    public  void MoverDerecha()
{
if (miJuego.GetNivelActual().EsPosibleMover( (short) (x+incrX), y) )
{
CambiarDireccion(DERECHA);
SiguienteFotograma();
x += incrX;
miJuego.GetMarcador().IncrPuntuacion(
miJuego.GetNivelActual().PuntosMover( x, y ));
}
}


  • Con eso ya hemos igualado casi todas las funcionalidades de la versión "no orientada a objetos", excepto el disparo. Pronto volveremos a incorporarlo, pero ahora será un disparo un poco más "real", en vez de moverse siempre hacia la derecha, haya obstáculos o no los haya.


Si quieres ver el fuente completo, todo el fuente del proyecto está en: code.google.com/p/fruityfrank

14 diciembre 2008

Remake (parcial) de Fruity Frank... 21 - Rediseñando usando clases (2 - Escribir textos, marcador, varios niveles, colisiones)

Como el rediseño usando clases ha supuesto muchos cambios, en la entrega anterior habíamos creado una primera versión, que todavía tenía muchas carencias:

  • No se podía escribir textos. Es fácil crear una función "EscribirTextoOculta", similar a la de las versiones anteriores, e incluso una clase "Fuente" para que en nuestro programa "normal" no aparezca ninguna referencia a "IntPtr". Así, normalmente para escribir daremos estos pasos:


    private Fuente fuenteSans18;  // Declaramos la variable como atributo
fuenteSans18 = new Fuente("FreeSansBold.ttf",18); // Leemos, en el constructor
Hardware.EscribirTextoOculta( // Usamos:
"Hola", // texto
110,440, // posición
0xFF, 0xAA, 0xAA, // color (R,G,B)
fuenteSans18); // fuente


  • En cuanto podamos escribir textos, ya podemos hacer que el marcador nos muestra la puntuación actual. Para que también nos muestre el nivel actual, deberíamos añadir algo como "GetNumeroNivel" a la clase "Juego". Y para saber el número de vidas, el juego deberá tener un "GetPersonaje" que permita acceder al personajes, y éste tendrá un "GetNumeroVidas" que indique cuantas vidas tenemos. Así, la rutina de dibujar el marcador en pantalla terminaría con:


    public  void Dibujar()
{
...
Hardware.EscribirTextoOculta("Nivel",380,60,
0x88, 0xFF, 0xFF, fuenteSans14);
Hardware.EscribirTextoOculta(miJuego.GetNumeroNivel().ToString(),420,60,
0xFF, 0xFF, 0x88, fuenteSans14);
Hardware.EscribirTextoOculta("Vidas",480,60,
0x88, 0xFF, 0xFF, fuenteSans14);
for (byte i=0; i<miJuego.GetPersonaje().GetNumVidas()-1; i++)
iconoVida.Dibujar( (short)(520+i*30),48);
}


  • Ahora el marcador necesita comunicarse con el juego. Una forma de conseguirlo es que el constructor del "Marcador" reciba como parámetro el juego que lo está manipulando, algo que ya hicimos con el "personaje" en la primera entrega:


    public class Marcador
{
// Atributos
private Juego miJuego; // Para comunicar con el resto de elementos
...

// Constructor
public Marcador(Juego j)
{
miJuego = j;
...


  • Y cuando el juego cree el marcador, lo hará así:


    miMarcador = new Marcador(this);


  • Sigamos ampliando... No se podía cambiar de nivel. Esto supone un par de cambios: por una parte, deberemos crear otra clase "Nivel2" (similar a Nivel1), que se apoye en un "Mapa2" (parecido a Mapa1), y que puede tener un distinto número de enemigos. Para cambiar de un nivel a otro, antes era el juego el que comprobaba la cantidad de frutas desde "ComprobarColisiones"; ahora podría ser el propio nivel, el que tuviera una variable booleana de control, llamada "completo" (a la que accederíamos con "GetCompleto") para saber si se ha completado un nivel, lo que además nos da la versatilidad de que un nivel pueda terminarse por otros motivos, no sólo porque se acaben las frutas. Así, por ejemplo, podríamos crear una función "siguienteFotograma", que se encargara de mover todos los enemigos y demás elementos del nivel, y de cambiar al siguiente nivel cuando corresponda. Esta función se llamaría en cada pasada del bucle principal:


    // Anima los enemigos y demás elementos del nivel.
// Cambia el nivel si corresponde.
private void SiguienteFotograma()
{
miNivel.SiguienteFotograma();
if (miNivel.GetCompleto())
SiguienteNivel();
}

// Bucle principal del juego
public void BuclePrincipal()
{
// Parte repetitiva ("bucle de juego")
NuevaPartida();
do {
DibujarPantalla();
ComprobarTeclas();
ComprobarColisiones();
SiguienteFotograma();
// Pausa de 40 ms, para velocidad de 25 fps (1000/40 = 25)
Hardware.Pausa(40);
// Fin de la parte repetitiva
} while (! partidaTerminada); // Hasta tecla ESC
}


  • La versión anterior no comprobaba colisiones: la función "ColisionCon" que comprueba si dos "ElementosGraficos" coinciden, siempre devolvía "false". El esqueleto de la función ya lo teníamos antes en "sdl_n.cs". También teníamos la idea de cómo comprobar colisiones desde la clase "Juego", recorriendo todos los enemigos con un "for." Podemos reescribirlo ligeramente usando un "foreach", de modo que ambas funciones quedarían como sigue:


    // ColisionCon, en "ElemGrafico"
public bool ColisionCon(ElemGrafico otroElem)
{
if ((otroElem.x+otroElem.ancho > x)
&& (otroElem.y+otroElem.alto > y)
&& (x+ancho > otroElem.x)
&& (y+alto > otroElem.y))
return true;
else
return false;
}

// ComprobarColisiones, en "Juego"
private void ComprobarColisiones()
{
foreach (Enemigo e in miNivel.GetEnemigos() )
if ( e.ColisionCon(miPersonaje) )
{
miPersonaje.PerderVida();
if (miPersonaje.GetNumVidas() == 0)
PartidaTerminada();
break; // Para no perder 2 vidas si se choca con 2
}
}


  • Eso sí, sigue faltando una cosa: animaciones en el movimiento de los personajes. Pero lo ideal sería que la clase "ElementoGráfico" nos permitiera tanto manejar elementos con una única imagen estática como elementos que tengan varias imágenes, que actuarían como distintos fotogramas para dar una sensación de movimiento más real. Incluso sería deseable que pidiéramos tener distintas secuencias de imágenes según el sentido en que se muestra el personaje (izquierda, derecha, arriba o abajo). Esto supone varios cambios que merecen una entrega aparte, así que queda para la versión 0.22...



Como siempre, todo el fuente del proyecto está en: code.google.com/p/fruityfrank

10 diciembre 2008

Remake (parcial) de Fruity Frank... 20 - Rediseñando usando clases (1)

LLega el momento de hacer cambios drásticos. La última ampliación que hemos hecho al juego era sencilla: un único disparo que se movía en una única dirección, y aun así nos ha supuesto hacer modificaciones en cinco partes distintas del programa. Cada vez que queramos hacer una ampliación, o bien una corrección, tendremos que pelear con más y más zonas de código dispersas.

Parece más razonable cambiar un poco el planteamiento "estructurado" que estábamos usando por uno "orientado a objetos": expresar el problema como una serie de objetos que interaccionan.

Esto supondrá hacer varios cambios profundos en nuestro fuente. La mayoría de los bloques podremos "copiarlos y pegarlos" del fuente antiguo a la nueva estructura, pero aun así va a ser trabajoso...

A cambio de esta trabajo, será más fácil localizar cualquier parte de código para realizar correcciones o ampliaciones, y además sería más fácil repartir el trabajo entre distintas personas. De hecho, la nueva forma de pensar debería ser "más natural"...

Ahora nuestro programa será una serie de objetos interrelacionados, que se pasan "mensajes" unos a otros. Por ejemplo, tenemos los siguientes objetos:


  • Existe un "personaje" que nosotros manejamos.

  • Existen dos tipos de "enemigos": los señores "pepino" y los señores "nariz", que nos matan si nos tocan.

  • Todos ellos son "elementos gráficos": objetos que tienen una imagen representativa, que se pueden dibujar en pantalla, mover a una cierta posición, etc.

  • En el juego debemos superar varios "niveles", cada uno de ellos con una serie de premios y obstáculos, representados en un "mapa". Además, en cada nivel existirá un número y tipo de enemigos distinto.

  • En el mapa aparecerán distintos elementos, como "obstáculos" que no podemos atravesar (manzanas) y "premios" que podemos recoger (cerezas, plátanos). Al mapa de un nivel le podremos preguntar también si es posible mover a una cierta posición, o incluso los puntos que se obtienen al mover a cierta posición (si había un premio en ella).

  • Nos vendrá bien tener objetos auxiliares, que nos oculten la librería gráfica que estamos empleando, lo que permitirá simplificar algunas operaciones y además nos dará la posibilidad de cambiar la librería gráfica por otra distinta si fuera necesario. Podemos tener al menos una clase llamada "Hardware", que centralice casi todas las operaciones, y también nos puede interesar una clase "Imagen" y una clase "Fuente" (tipo de letra).



Este planteamiento nos permitirá hacer cambios "globales" con una cierta facilidad. Por ejemplo, cuando ampliemos las posibilidades de un "elemento gráfico" para que pueda mostrar una sucesión de imágenes en vez una imagen estática, todos los elementos gráficos del programa podrán mostrar animaciones sin necesidad de cambios en el resto del programa.

Una primera aproximación a los objetos que podemos emplear se podría representar con este diagrama de clases, que refinaremos más adelante:



De este diagrama se podrían "leer" detalles como:


  • La clase "Juego" es la que coordina todo.

  • Tenemos "elementos gráficos", como nuestro personaje, o los enemigos, o el disparo.

  • Nuestro personaje puede moverse a la derecha, izquierda, arriba o abajo, como respuesta a cuando el usuario pulse una tecla (o intentarlo: realmente llamará antes a "EsPosibleMover", del mapa actual, para comprobar si es posible moverse en esa dirección).
  • El nivel podrá por ahora poco más que dibujar su mapa y los enemigos que le corresponden.

  • Los enemigos, además de "dibujarse" (como cualquier otro elemento gráfico), se podrán "mover" con un movimiento prefijado.

  • El marcador se puede "dibujar", pero también se puede leer su puntuación, o cambiar el valor de ésta, o simplemente incrementarlo (que será lo más habitual durante el juego, a medida que recojamos "premios").



La comunicación se realiza entre clases que estén directamente relacionadas (por ejemplo, un "nivel" puede pedirle a su "mapa" que se dibuje); si las clases están más alejadas, se hace acudiendo a la clase "Juego" como intermediaria. Por ejemplo, para mover a la derecha, el personaje pide antes al juego que mire en su nivel para ver si la siguiente casilla está disponible, y después le pide al juego que aumente su marcador en la puntuación correspondiente a esa casilla, así:

  public  void MoverDerecha()
{
if (miJuego.GetNivelActual().EsPosibleMover( (short) (x+incrX), y) )
{
x += incrX;
miJuego.GetMarcador().IncrPuntuacion(
miJuego.GetNivelActual().PuntosMover( x, y ));
}
}



En cuanto a funcionalidades, esta versión es un paso atrás comparado con las anteriores: todavía no permite escribir textos (la clase "fuente" no está lista), ni cambiar de nivel (no tenemos más que un nivel), ni comprueba colisiones, ni tiene animaciones en el movimiento e los personajes, pero ya tenemos un esqueleto que mejoraremos en la siguiente entrega hasta conseguir imitar lo que ya teníamos, y poder ampliar con más facilidad a partir de entonces.

Si quieres más detalles, todo el fuente del proyecto está en: code.google.com/p/fruityfrank

06 diciembre 2008

Remake (parcial) de Fruity Frank... 19 - Un personaje que dispara

Ahora vamos a añadir la posibilidad de que nuestro personaje "dispare". La idea básica es la misma que con los enemigos: el disparo será otro elemento que podrá estar activo o no estarlo, y que deberá irse moviendo en cada fotograma.

En el juego Fruity Frank original, el disparo era una "pelota" que salía en la dirección en la que miraba nuestro personaje, que rebotaba con las paredes y que podía volver a nuestras manos. En esta primera aproximación vamos a hacer algo mucho más sencillo: el "disparo" siempre saldrá hacia la derecha, y avanzará hasta que choque con un enemigo o se salga de la pantalla. Además, sólo podrá haber un disparo activo en cada momento (si pudiera haber varios, deberíamos guardarlos en alguna estructura repetitiva, como un Array).

Esto supone varios cambios en el fuente:

  • Habrá que declarar ciertas variables adicionales al principio del programa, que necesitaremos para el control del disparo:


      // Datos del disparo
bool disparoActivo = false;
short xDisparo, yDisparo;
short anchoDisparo=28, altoDisparo=24;


  • Cuando dibujamos cosas en pantalla (función dibujarPantalla), también habrá que dibujar el disparo:


      // Disparo
if (disparoActivo)
DibujarImagenOculta(disparo, xDisparo, yDisparo);


  • Igual que teníamos una función para mover los enemigos, necesitaremos otra para mover el disparo:


      void moverDisparo()
{
// De momento el disparo se mueve solo a la derecha, 5 pixeles
if (disparoActivo)
xDisparo += 5;
if (xDisparo > xFinPantalla)
disparoActivo = false;
}


  • Cuando comprobemos colisiones (función comprobarColisiones), habrá que mirar también si la pelota choca con algún enemigo; si es así, eliminaremos el enemigo y la pelota. (Entonces la pelota pasaría a estar disponible nuevamente, algo que tampoco ocurría en el Fruity original, en el que la pelota tardaba un cierto tiempo en volver a ser utilizable):


      // Veo si el disparo mata algun enemigo
for (i=0; i<NUMENEMIGOS; i++)
if (enemigos[i].activo)
{
if (Colision(enemigos[i].x, enemigos[i].y,
enemigos[i].ancho, enemigos[i].alto,
xDisparo, yDisparo,
anchoDisparo, altoDisparo))
{
enemigos[i].activo = false; // Desaparece el enemigo
disparoActivo = false; // Y el disparo
}
}


  • Incluso, al cambiar de nivel, o cuando nos mate un enemigo, habrá que indicar que no hay disparo activo: quedaría "feo" que disparásemos, cambiáramos de nivel y se viera todavía la pelota del nivel anterior moviéndose por ahí. Esto debería hacerse en "prepararNivel" y en "perderVida".



  • Y, por supuesto, habrá que buscar una imagen que represente ese disparo.



Como siempre, todo el fuente del proyecto está en: code.google.com/p/fruityfrank