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

25 noviembre 2008

Remake (parcial) de Fruity Frank... 18 - Distintos niveles de juego

El siguiente paso es tener distintos niveles ("pantallas") en nuestro juego, de modo que cuando recojamos todos los "premios" del primer nivel, pasemos a un segundo nivel, y después a un tercero, y así sucesivamente.

En principio, la idea es sencilla: en vez de un único array para representar el mapa de un nivel, tendremos varios arrays, uno para cada para mapa (en realidad, sería una solución más razonable tener un array multidimensional, pero no lo haremos así por ahora):

  string[] mapa1 = {
"XCXXMXXX XMCXXX",
"MXXCXXXX XMXCMM",
"XMCXXXXX XCXXXC",
"XXCXCXCX XXXXCX",
" ",
"XXMCXMXX XCXXXX",
"XXXXXXXX XXXMXX",
"XXXXXCXX XXXXXX",
"XXXXXCXX XXXCXX",
"XXXXXXXX CXCXXC"
};

string[] mapa2 = {
"YCYYMY YYYMCYYY",
"MYYCYY YYYMYCMM",
"YMCYYY YYYCPYYC",
"YYCYCY CYYYYYCY",
" ",
"YYMCYM YYYCYYYY",
"YYYYYY YYYYYMYY",
"YYYPYC YYYYYYYY",
"YYPYYC YYYYYCYY",
"YYYYYY YYCYCYYC"
};

string[] mapa3 = {
"ZCZZMZZZ ZPCZZM",
"MZZCZZPZ ZMZCMM",
"ZMCZZZZZ ZCPZZC",
" ",
"ZZCZCZCZ ZZZZCZ",
"ZZMCZMZZ ZCZZZZ",
"ZZZZZZZZ ZZZMZZ",
"ZZZPZCMZ ZZZZZZ",
"ZZPZZCZZ ZZMCZZ",
"ZZZZZZZZ CZCZZC"
};


Tendremos un cuarto mapa, vacío, que representará en cada momento al nivel actual. Antes de empezar cada nivel, volcaremos el mapa de ese nivel en el mapa "en uso". Lo podemos declarar con:

  string[] mapa = new string[MAXFILAS];


Y podemos volcar los datos en él así:

  for (i=0; i<MAXFILAS; i++)
mapa[i] = mapa1[i];


Para saber cuándo pasar de un nivel a otro, podemos contar los "premios" que quedan por recoger. Si no quedan premios en el nivel actual, es el momento de pasar al siguiente nivel:

  // Si no quedan frutas, avanzo de nivel
if (numFrutas == 0)
siguienteNivel();


Y esa función "siguienteNivel" mostraría un aviso o una animación, cargaría el mapa del nuevo nivel en el mapa en uso, restauraría las posiciones iniciales de los enemigos y de nuestro personaje, etc.


En nuestro caso, tenemos varios posibles mapas de nivel, así que tendremos que asignar el que corresponda. Si fuera un array multidimensional, usaríamos como índice el número de nivel; como no es nuestro caso, tendremos que usar varios "if" encadenados. De hecho, crearemos sólo 3 niveles, pero haremos que se puedan jugar en más de 3 pantallas, haciendo que tras la tercera se vuelva a la primera, así:

  // Por ahora solo hay tres niveles, así que alterno:
if (nivel % 3 == 1)
for (i=0; i<MAXFILAS; i++)
mapa[i] = mapa1[i];
else if (nivel % 3 == 2)
for (i=0; i<MAXFILAS; i++)
mapa[i] = mapa2[i];
else for (i=0; i<MAXFILAS; i++)
mapa[i] = mapa3[i];



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

20 noviembre 2008

Remake (parcial) de Fruity Frank... 17 - Varias vidas

Ya es el momento de tener varias vidas, que se pierdan al chocar con un enemigo. Además, cuando se acaben todas las vidas, deberemos volver a la pantalla de presentación, y poder empezar una nueva partida.

Lo de perder una vida cada vez que toquemos a un enemigo supone algunos cambios:


  • Deberemos llevar un contador de vidas.

  • Además, la cantidad de vidas restantes debería aparecer en la pantalla.

  • Cuando "nos maten", habrá que llevar a nuestro personaje y a nuestros enemigos a sus posiciones iniciales: si los dejamos donde estaban, posiblemente volverán a colisionar, y perderíamos otra vida más (o incluso varias).

  • Según sea la lógica de nuestro juego, también es frecuente que cuando choquemos con enemigo, éste también "muera" (no ocurre así en el caso concreto de Fruity Frank).

  • Deberíamos avisar al usuario de que ha perdido una vida, bien sea con una breve animación o con un cartel que se mantuviera en pantalla un par de segundos.



Además, cuando se acaben las vidas deberán ocurrir varias cosas:


  • Deberá terminar la partida actual, y volver a la pantalla de presentación.

  • Al igual que antes, deberemos avisar al usuario (en este caso, de que la partida ha terminado).

  • Deberemos comparar los puntos obtenidos con la máxima puntuación hasta el momento, y actualizarla si procede.

  • Deberemos restaurar el mapa de juego inicial, con todos sus obstáculos y sus premios.

  • Todos los enemigos deberán volver a estar activos y en sus posiciones iniciales.



De paso, la pantalla de presentación deberá permitir volver a jugar una partida, o bien abandonar el juego por completo.

Esto supone unos cuantos cambios en el fuente:


  • Ahora el cuerpo del juego debe ser repetitivo, por ejemplo así:


      Juego juego = new Juego();
// Primero inicializo (variables, imágenes, etc)
juego.inicializar();
do {
// Después, pantalla de presentacion
juego.presentacion();
// Y luego, la partida en sí
if (! juego.finDelJuego)
juego.buclePrincipal();
} while (! juego.finDelJuego);


  • Es decir, desde dentro de la "presentación" anoto si el usuario ha elegido la opción de salir de la partida ("finDelJuego") o la contraria (empezar a jugar).



  • En la presentación se puede tener que entrar al juego (jugar una partida) o tener que abandonarlo. Yo uso una variable "bool" para indicar si hay que seguir repitiendo la presentación o si hay que salir de ella, y otra variable "bool" para indicar si hay que salir de todo el juego:


      while (! salirPresentacion )
{
if (TeclaPulsada (TECLA_ESP)) {
salirPresentacion = true;
nuevaPartida();
}
if (TeclaPulsada (TECLA_S)) {
salirPresentacion = true;
finDelJuego = true;
}
...


  • (Mi función "nuevaPartida" es la que prepara las variables antes de comenzar una partida: número de vidas, posición de personaje y enemigos, etc).



  • La otra dificultad posible es para regenerar el mapa: Hay que tener el mapa guardado en un array, y copiarlo en el array "de uso" antes de empezar cada partida. No se puede hacer "mapa = mapa1;", porque entonces los dos arrays están en la misma posición de memoria, y al modificar uno, se modifica el otro automáticamente, y ya no se podría regenerar el mapa original. Tenemos que hacerlo fila por fila, con un "for":


      for (i=0; i<MAXFILAS; i++)
mapa[i] = mapa1[i];


  • El resto de cambios en el fuente deberían ser sencillos...




Para más detalles, puedes ver el estado del proyecto en: code.google.com/p/fruityfrank


12 noviembre 2008

Remake (parcial) de Fruity Frank... 16 - Un fuente más modular

Nuestro fuente empieza a tener un cierto tamaño. Para ganar en legibilidad, va siendo el momento de hacer un pequeño replanteamiento, para convertirlo en algo más modular.

El "Main" del programa ya era razonablemente breve. Sólo le vamos a añadir una llamada a función "inicializar", que sea la que se encargue de cargar las imágenes, tipos de letra, etc (las variables se declararán antes, al principio del programa, como veremos un poco más adelante):

public static void Main ()
{
Juego juego = new Juego();
// Primero inicializo (variables, imágenes, etc)
juego.inicializar();
// Después, pantalla de presentacion
juego.presentacion();
// Y luego, la partida en sí
juego.buclePrincipal();
}


Por su parte, el bucle de juego (la función "buclePrincipal"), debería ser mucho más modular de lo que era, y más legible. Resultaría más fácil de mantener se pareciera más a la idea intuitiva de lo que debe hacer cada "pasada" de nuestro juego: dibujar la pantalla, comprobar qué teclas pulsa el usuario (y actuar en consecuencia), comprobar colisiones entre elementos del juego, y calcular la nueva posición de los demás elementos del juego (por ahora, sólo nuestros enemigos), por ejemplo así:

do {
dibujarPantalla();
comprobarTeclas();
comprobarColisiones();
moverEnemigos();
// Pausa de 40 ms, para velocidad de 25 fps (1000/40 = 25)
Pausa(40);
// Fin de la parte repetitiva
} while (! TeclaPulsada (TECLA_ESC));


Eso sí, estas funciones comparten información entre ellas. Podríamos hacer que esa información se pasara como parámetros, o bien que simplemente se tratase de "variables globales" (realmente, atributos de nuestra clase "Juego"), que declaremos antes que todas las funciones:

// Constantes que se usarán en el juego
const int NUMENEMIGOS = 5;
// Tipos de enemigos
const int TIPOENEMIGOPEPINO = 1;
const int TIPOENEMIGONARIZ = 2;

// Variables auxiliares para bucles
short i;
// Limites de la pantalla
short xIniPantalla = 12, xFinPantalla = 638;
short yIniPantalla = 82, yFinPantalla = 448;


A partir de ahora, los cambios que se acercan afectarán menos a otras partes del juego. Se tratará de cambios como: como perder vidas cuando choquemos con un enemigo, un movimiento más inteligente de estos enemigos, poder "disparar", que ciertos obstáculos sean móviles, que haya un tabla de records, etc.

Para más detalles, puedes ver el estado del proyecto en: code.google.com/p/fruityfrank

11 noviembre 2008

Huelga de estudiantes y profesionales de informatica

Convocada para el 19 de Noviembre (en España).

Para reclamar competencias profesionales. Nada más y nada menos que cualquier otro ingeniero o cualquier otro titulado.

A cualquiera (menos a nuestro gobierno) le parece absurdo que entre las competencias de un Ingeniero en Telecomunicaciones se incluyan redes, sistemas operativos, programación, bases de datos y otras tantas... mientras que para un Ingeniero en Informática no existen competencias definidas.

Si quieres más detalles:

http://www.huelgainformatica.es/

Y si quieres difundirlo, incluye este banner (o cualquier otro similar) en tu página web:


09 noviembre 2008

Remake (parcial) de Fruity Frank... 15 - Colisiones por coordenadas

Sabemos cómo comprobar colisiones usando nuestro "mapa" de juego. Es interesante como primera aproximación, pero generalmente querremos algo "más fino": que el movimiento vuelva a a ser suave (algo que haremos pronto) y que las colisiones se puedan comprobar antes, en cuanto un elemento "roce" al otro (eso sí que lo haremos ahora).

Vamos a mejorar las colisiones que se basan exactamente en posiciones del mapa, para ver una forma de comprobar colisiones entre dos imágenes. Aun así, lo que haremos tampoco será perfecto: comprobaremos si el rectángulo de una imagen se solapa con el rectángulo de otra imagen, pero habitualmente esto será totalmente preciso, porque la mayoría de imágenes (personaje, enemigos, frutas) no ocupan por completo su rectángulo, así que puede que consideremos que ha habido una colisión cuando realmente se estén solapando dos "zonas negras" de las imágenes.

Otras alternativas más precisas incluirían el descomponer la imagen en una serie de rectángulos que sí contengan elementos visibles (lo que complicaría la forma de comprobar colisiones), o bien mirar los píxeles que se solapan para ver si son "transparentes", y entonces no dar la colision como válida (lo que también complicaría, además de ralentizar ligeramente). Por ahora no afinaremos tanto, y nos quedaremos con una aproximación más precisa que la de antes, que funcionará cuando un enemigo no se encuentre justo en una casilla del mapa, sino pasando de una a otra, pero que tampoco es totalmente precisa, especialmente con personajes que tengan formas irregulares.

Una forma de comprobar si se solapan dos rectángulos es ver qué ocurre con sus extremos: en horizontal, si el extremo izquierdo de una imagen está más a la derecha que el extremo derecho de la otra, no se solapan; para que se solapen, el extremo derecho de una imagen (x1) debe ser estar a la izquierda del extremo izquierdo de la otra (x1 < x2+ancho2), y ocurrir lo mismo a la inversa (x2 < x1+ancho1), y algo similar debe ocurrir en vertical:

if ((x2+an2 > x1)
&& (y2+al2 > y1)
&& (x1+an1 > x2)
&& (y1+al1 > y2))
colision = true;
else
colision = false;


Pero en nuestro caso, parte del trabajo ya está hecho: esa comprobación forma parte de la clase "SdlN":

public bool Colision(int x1, int y1, int an1, int al1,
int x2, int y2, int an2, int al2)
{
if ((x2+an2 > x1)
&& (y2+al2 > y1)
&& (x1+an1 > x2)
&& (y1+al1 > y2))
return true;
else
return false;
}


De modo que si nuestro juego se apoya en SdlN, basta con que comprobemos las colisiones así:

if (Colision(enemigos[i].x, enemigos[i].y, 
enemigos[i].ancho, enemigos[i].alto,
posXPersonaje, posYPersonaje,
anchoPersonaje, altoPersonaje) )
enemigos[i].activo = false;


En este caso, como primera aproximación, pero que todavía no sigue una lógica de juego real, cada vez que tocamos un enemigo, éste desaparece (activo = false). En un juego real, nuestro enemigo perdería una vida y volveríamos a comenzar la partida en el nivel actual. Si la colisión fuera entre un disparo y un enemigo, deberíamos dibujar una explosión antes de hacerlo desaparecer. Esos detalles pertenecen a la lógica de cada juego, y nos iremos acercando a ellos poco a poco.


De paso, vamos a hacer otra pequeña mejora en esta versión: la línea superior, que muestra la puntuación, el record y la cantidad de vidas que nos quedan, va a dejar de ser "predibujada". En vez de cargar la imagen que contiene casi todo eso fijo, vamos a ser nosotros los que mostremos los puntos (ya lo hacíamos), el record (que era fijo), la cantidad de vidas (aunque todavía no nos puedan matar nuestros enemigos) y un recuadro alrededor de todo ello:

// Tabla de records y vidas restantes
RectanguloRGBA(15,45,625,80, // Marco para records
255, 109, 8, // Naranja
255); // Opaco
EscribirTextoOculta("Puntos",20,60,
0x88, 0xFF, 0xFF, fuenteSans14);
EscribirTextoOculta(puntos.ToString("000000"),70,60,
0xFF, 0xFF, 0x88, fuenteSans14);
EscribirTextoOculta("Mejor puntuac",200,60,
0x88, 0xFF, 0xFF, fuenteSans14);
EscribirTextoOculta(mejorPunt.ToString("000000"),300,60,
0xFF, 0xFF, 0x88, fuenteSans14);
EscribirTextoOculta("Vidas",450,60,
0x88, 0xFF, 0xFF, fuenteSans14);
for (i=0; i<vidas-1; i++)
DibujarImagenOculta(personajeD, (short)(490+i*30),48);


(Por supuesto, esto supone algunos pequeños cambios en el juego, como añadir la variable "vidas" o la variable "mejorPunt").


Para más detalles, puedes ver todo el proyecto en: code.google.com/p/fruityfrank

07 noviembre 2008

Remake (parcial) de Fruity Frank... 14 - Movimiento animado

Nuestro personaje ya cambia de forma: según si nos movemos hacia la derecha, la izquierda o arriba/abajo, muestra una imagen distinta. Ahora vamos a hacer que cambie ligeramente de forma mientras se mueve en una misma dirección.

Existen varias formas de hacerlo.

Una forma podría ser basarnos en su coordenada X (por ejemplo), de modo que si X es par, se muestre una imagen, y si X es impar, se muestre otra forma distinta. Esto tiene inconvenientes. Por ejemplo, si el incremento con el que se mueve nuestro personaje es 2, siempre se vería la misma imagen.

Otra forma más eficiente podría ser llevar cuenta de qué imagen acabamos de dibujar. Por ejemplo, tener dos imágenes "derecha1" y "derecha2" de nuestro personaje moviéndose a la derecha. Si la última que hemos dibujado es "derecha1", la siguiente que toca es "derecha2" y viceversa. Se podría conseguir así:

if ( TeclaPulsada (TECLA_DER) )
{
if ((xPersonaje < MAXCOLS-1)
&& (mapa1[yPersonaje][xPersonaje+1] != 'M'))
{
xPersonaje ++;
// Alterno entre dos imagenes a la derecha
if (posicion == DERECHA1)
{
personaje = personajeD;
posicion = DERECHA2;
}
else
{
personaje = personajeD2;
posicion = DERECHA1;
}
}
}


En ese ejemplo, DERECHA1 y DERECHA2 podrían ser constantes simbólicas, con valores 1 y 2, respectivamente.

Si tenemos más de dos "fotogramas" de nuestro personaje, podemos hacer que "posición" tome más valores, por ejemplo de 0 a 5 (si tenemos 6 fotogramas en cada dirección), y guardar las imágenes en un array, y entonces haríamos:

    // La posicion va rotando
posicion ++;
if (posicion == DERECHA5)
posicion = DERECHA0;
personaje = imagenPersonaje[posicion];


De la misma forma, podríamos hacer que hubiera animaciones en cada una de las posibles direcciones del movimiento, o en el movimiento de los enemigos.


Para más detalles, puedes ojear todo el proyecto en: code.google.com/p/fruityfrank

05 noviembre 2008

Remake (parcial) de Fruity Frank... 13 - Migrando a C#

Antes de seguir ampliando las posibilidades del juego, llega el momento de hacer un cambio drástico: usar otro lenguaje de programación. Vamos a usar C#, que permite despreocuparnos de ciertos detalles de bajo nivel, y además nos permitirá usar una metodología orientada a objetos para rediseñar el juego... dentro de no mucho tiempo.

A pesar de que C# deriva de C, y de que vamos a usar la librería Tao.SDL que deriva de SDL, habrá que hacer algún que otro cambio.

Como en la versión anterior, crearemos un fichero auxiliar que oculte un poco de SDL, simplifique algunos pasos, y que además nos traduzca las órdenes a español. En este caso, se llama "sdl_n.cs" y se puede ver en la página del proyecto.

Los cambios a nuestro juego, sin ser grandes, son unos cuantos:


  • Todo el programa pasará a formar parte de una clase: "public class Juego". En concreto, como se apoyará en la clase auxiliar SdlN, comenzaremos con "public class Juego: SdlN".


  • Los datos de nuestros enemigos están en un "struct"; los componentes de este "struct" ahora deben ser public.


  • Cambia la declaración del "array" que contiene nuestros enemigos: ahora deberá ser al estilo de C#: enemigo[] enemigos = new enemigo[NUMENEMIGOS];


  • El mapa del juego ahora será un "array" de "strings". Se declara casi igual, pero ahora tendremos una columna menos en MAXCOLS (ya no hace falta dejar un carácter adicional para indicar el final de cadena). Por ese mismo motivo, la comprobación de la columna en la que estamos no acaba en MAXCOLS-2, sino en MAXCOLS-1. De igual modo, como las cadenas en C# no son modificables, ya no podemos alterar el mapa con "mapa1[yPersonaje][xPersonaje] = ' ';", sino que deberemos usar un "remove" para quitar un carácter y luego un "insert" para introducir el nuevo carácter en su lugar.


  • Podemos aprovechar para cambiar el tipo de alguna variable. Por ejemplo, las coordenadas X e Y pueden ser un "short" en vez de un entero de 32 bits. Las componentes R, G y B de un color serán "byte". Por eso será necesario también cambiar algunos datos durante el desarrollo de juegos, forzando una conversión de tipos ("cast"), bien porque aprovechemos datos auxiliares que todavía sean "int" y debamos convertir a "short": datoShort = (short) datoEntero, o bien porque hagamos operaciones y el compilador no tenga la certeza de que el resultado vaya a caber en un entero corto: (short) (xIniPantalla + xPersonaje * anchoCasilla)


  • Todo el cuerpo del juego lo sacaremos de "Main" para meterlo en una nueva función, llamada (por ejemplo) "BuclePrincipal". Así nuestro "Main" contendrá apenas 3 órdenes: la declaración de un juego, su presentación y su bucle principal.


  • Como peculiaridad de la librería que usaremos (Tao.Sdl), y por no profundizar más por ahora, nuestros tipos de datos SDLA_Imagen* y SDLA_Fuente* ahora pasarán a ser un único tipo "IntPtr".


  • Todos los SDLA_xxx cambian de nombre, perdiendo ese "SDLA_" del principio, para abreviar un poco y tratar de ganar en legibilidad. Por ejemplo, en vez de "SDLA_teclaPulsada" usaremos "TeclaPulsada", y "SDLA_dibujarImagenOculta" se convertirá en "DibujarImagenOculta".



Como siempre, todos los fuentes están en: code.google.com/p/fruityfrank

03 noviembre 2008

Remake (parcial) de Fruity Frank... 12 - Una pantalla de presentacion

Nuestro juego, a pesar de estar apenas empezando a ser un borrador de lo que tendrá que ser finalmente, ocupa ya cerca de 230 líneas de código. Si esto sigue creciendo de esta manera, dentro de poco empezará a ser difícil de manejar. Va siendo el momento de empezar a hacerlo más modular.

Como primer acercamiento a esa modularidad, vamos a crear una pantalla de presentación para el juego, y esta pantalla estará en una nueva función, independiente de "main". La pantalla de presentación mostrará un animación sencilla (por ejemplo, un texto rebotando en la pantalla) hasta que pulsemos una cierta tecla (como la barra espaciadora). En ese momento será cuando realmente comience la partida.

De este modo, ahora el "main" comenzará así:


int main (int argc, char** argv)
{

// Antes de nada, pantalla de presentacion
presentacion();
...


Y esta función "presentacion" podrá ser de tipo "void" (no va a devolver ningún valor), y deberá comenzar por inicializar la pantalla y declarar sus propias variables locales (entre ellas puede estar incluso el tamaño de la pantalla, que ya estaban declaradas dentro de "main" y que se repetirán aquí a no ser que las hagamos "variables globales").

void presentacion()
{
// Limites de la pantalla
int xIniPantalla = 12, xFinPantalla = 638;
int yIniPantalla = 82, yFinPantalla = 448;

// Intentamos inicializar SDL
SDLA_inicializar(640,480,24);
...


Ahora, la llamada a "SDLA_inicializar" ya no deberá aparecer en "main", o habrá pasos que estaremos dando dos veces, y obtendremos mensajes de error.

En cuanto al contenido de esa pantalla de presentación, puede ser cualquier cosa que se repita hasta que pulsemos una tecla. Por ejemplo, el "cartel" representativo del juego rebotando en los extremos de la pantalla. Conviene que la tecla que se pulse para salir no sea ESC, que es la que utilizamos en el juego, o puede ocurrir que salgamos de la pantalla de presentación... pero también del juego, sin llegar a jugar...


Como siempre, todos los fuentes están en: code.google.com/p/fruityfrank

01 noviembre 2008

Remake (parcial) de Fruity Frank... 11 - Varios enemigos distintos

Si queremos tener varios enemigos, cada uno de los cuales tenga una posición, una velocidad distinta, etc., una forma sencilla de hacerlo es crear un "array de structs". El "struct" representará los datos que vamos a guardar de cada uno de nuestros enemigos. Por ejemplo:

struct enemigo
{
int x; // Posicion horizontal
int y; // Posicion vertical
int ancho; // Ancho en pixeles
int incrX; // Incremento horiz (velocidad)
int tipo; // Tipo de enemigo: Pepino, narizotas, etc
bool activo; // Vivo o muerto?
};


Y el array guardará esos datos para una serie de enemigos:

const int NUMENEMIGOS = 5;
enemigo enemigos[NUMENEMIGOS];


Antes preparábamos una serie de datos iniciales para un único enemigo:

    // Posicion inicial y tamaño del enemigo
int xEnemigo1=250, yEnemigo1=250;
int anchoEnemigo1=40;
int incrEnemigo=3;


Ahora tendremos que preparar datos iniciales para varios enemigos, así que lo podemos hacer de forma repetitiva, con un "for":

    // Posicion inicial y tamaño de los enemigos
enemigo enemigos[NUMENEMIGOS];
for (i=0; i<NUMENEMIGOS; i++)
{
enemigos[i].x = 250 + i*40;
enemigos[i].y = 250 + i*30;
enemigos[i].ancho = 40;
enemigos[i].incrX = 3;
if (i<3)
enemigos[i].tipo = TIPOENEMIGOPEPINO;
else
enemigos[i].tipo = TIPOENEMIGONARIZ;
enemigos[i].activo = true;
}


De igual modo, antes dibujábamos un único enemigo:

   SDLA_dibujarImagenOculta(enemigo1, xEnemigo1, yEnemigo1);


y ahora dibujaremos varios, que además pueden ser de distintos tipos, y estar activos ("vivos") o no ("muertos"):

        for (i=0; i<NUMENEMIGOS; i++)
{
if (enemigos[i].activo)
{
if (enemigos[i].tipo == TIPOENEMIGOPEPINO)
enemigo = enemigoPepino;
else
enemigo = enemigoNariz;
SDLA_dibujarImagenOculta(enemigo, enemigos[i].x,enemigos[i].y);
}
}


Y tras cada fotograma deberemos mover los enemigos, cada uno de ellos con su propia posición y velocidad (incremento de posición). Antes era así

        // Movimiento de enemigo (de momento: lado a lado)
xEnemigo1 += incrEnemigo;

if (xEnemigo1 < xIniPantalla) {
xEnemigo1 = xIniPantalla;
incrEnemigo = -incrEnemigo;
}
if (xEnemigo1 > xFinPantalla-anchoEnemigo1) {
xEnemigo1 = xFinPantalla-anchoEnemigo1;
incrEnemigo = -incrEnemigo;
}


Y ahora será parecido, pero repetitivo y accediendo a los campos de cada "struct", así:

        // Movimiento de enemigos (de momento: lado a lado)
for (i=0; i<NUMENEMIGOS; i++)
{
enemigos[i].x += enemigos[i].incrX;

if (enemigos[i].x < xIniPantalla) {
enemigos[i].x = xIniPantalla;
enemigos[i].incrX = -enemigos[i].incrX;
}
if (enemigos[i].x > xFinPantalla-enemigos[i].ancho) {
enemigos[i].x = xFinPantalla-enemigos[i].ancho;
enemigos[i].incrX = -enemigos[i].incrX;
}
}



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

29 octubre 2008

Remake (parcial) de Fruity Frank... 10 - Un personaje que cambia de forma

Nuestro personaje siempre mira hacia la derecha, nos movamos hacia donde nos movamos. Como primer acercamiento a un movimiento "más real", vamos a hacer que mire hacia el lado en que se mueve.

Existen varias formas de hacerlo. Casi todas pasan por añadir un paso adicional cada vez que el usuario pulse una tecla. Si esa tecla es la de la izquierda, memorizaremos de alguna forma que ahora se deberá dibujar el personaje que se desplaza hacia la izquierda. Se podría hacer actualizando el valor de una variable "dirección", o bien simplemente cambiando el identificador de la imagen que mostraremos. Ésta es la alternativa que usaré yo: tener 3 imágenes del personaje leídas de fichero (derecha, izquierda y arriba/abajo), junto con una cuarta imagen que no se lee de fichero, sino que toma su valor a partir de las otras, y que es la que realmente se muestra en pantalla:

   SDLA_Imagen* personajeD;
SDLA_Imagen* personajeI;
SDLA_Imagen* personajeA;
SDLA_Imagen* personaje;


Como ya he comentado, tres de esas imágenes se leen de fichero:

   personajeD=SDLA_cargarImagen("personajed1.bmp");
personajeI=SDLA_cargarImagen("personajei1.bmp");
personajeA=SDLA_cargarImagen("personajea1.bmp");



Y la cuarta cambia de valor según la dirección en que deba moverse el personaje:

        if ( SDLA_teclaPulsada (TECLA_IZQ) )
{
if ((xPersonaje > 0)
&& (mapa1[yPersonaje][xPersonaje-1] != 'M'))
{
xPersonaje --;
personaje = personajeI;
}
}



Haríamos lo mismo para las demás direcciones, y dibujaríamos siempre la imagen llamada "personaje", que en cada caso reflejará nuestro personaje mirando hacia el lado que nos interesa. Con eso bastaría para tener un personaje "cambiante" básico.

Como siempre, puedes ver todo el proyecto en: code.google.com/p/fruityfrank

28 octubre 2008

Remake (parcial) de Fruity Frank... 09 - Obstáculos, premios y texto

Con la estructura que tenemos hasta ahora, es fácil comprobar si hay un obstáculo en algún punto de la pantalla de juego.

La diferencia con lo que hemos hecho para "recoger premios" es que antes comprobábamos lo que había en una casilla cuando acabábamos de entrar a ella. Por el contrario, si se trata de un obstáculo, no deberíamos movernos a esa casilla, sino comprobar antes. Pero es fácil: ya estábamos comprobando si podíamos desplazarnos a una cierta posición de la pantalla o si estaba fuera de los márgenes, así que ahora es sólo añadir una condición más:

        if ( SDLA_teclaPulsada (TECLA_ARR) ) {
if ((yPersonaje > 0)
&& (mapa1[yPersonaje-1][xPersonaje] != 'M'))
yPersonaje --;
}


En esta expresión hay algo que puede desconcertar: si estamos en la columna 0... ¿no sería peligroso mirar en "mapa1[yPersonaje-1][xPersonaje]", es decir, en la columna "-1"? La respuesta es que NO: los compiladores de C evalúan las condiciones usando la técnica de "evaluación en cortocircuito": si se trata de dos condiciones unidas por un "Y" (&&) y la primera condición es falsa, no se molestan en comprobar la segunda condición, porque ya se sabe que el resultado de la condición global será "falso". Por eso, si estamos en la columna 0, no se llegará a comprobar qué habría en la columna -1.

Las demás comparaciones son similares a la anterior:

        if ( SDLA_teclaPulsada (TECLA_ABA) ) {
if ((yPersonaje < MAXFILAS-1)
&& (mapa1[yPersonaje+1][xPersonaje] != 'M'))
yPersonaje ++;
}



De paso, podemos hacer otra mejora: mostrar los puntos que vamos obteniendo durante el juego.

Eso supone dos pequeños cambios. El primero es declarar y cargar nuestro tipo de letra al principio del programa:

    SDLA_Fuente* SDLA_arial14;
SDLA_arial14 = SDLA_cargarFuente("arial.ttf",14);


El segundo es usar la función de "EscribirTextoOculta" para escribir textos, o "EscribirLongOculta" para escribir números (enteros largos):

        SDLA_escribirTextoOculta("Puntos",20,60,0x88FFFF, SDLA_arial14);
SDLA_escribirLongOculta(puntos,70,60,0xFFFF88, SDLA_arial14);


Por supuesto, cada vez que recojamos una cereza deberíamos obtener puntos, para que ese marcador aumente... pero eso ya no es difícil de hacer a esta altura.

(Nota: eso de poder escribir números ha supuesto hacer un cambio en los ficheros auxiliares SDL_ASI.h y sdlasi.c; tendrás que incluir en el proyecto los ficheros actualizados, o no compilará...)

24 octubre 2008

Remake (parcial) de Fruity Frank... 08 - Comprobando colisiones con el mapa

Ahora que tenemos la estructura de la pantalla de nuestro juego (fondo, enemigos, "premios") diseñada usando un mapa, es fácil comprobar colisiones entre nuestro personaje y otros elementos.

En este primer acercamiento a las colisiones, nuestro personaje podrá recoger frutas de la pantalla.

Para ello, basta con varios pequeños detalles:


  • La posición inicial de nuestro personaje ya no será un punto de la pantalla gráfica, como el (100,300), sino un punto del mapa, como el (1,1).

  • Ya no se moverá saltando varios píxeles (xPersonaje += 4), sino de una casilla en una casilla (xPersonaje ++).

  • Cuando se mueva, no comprobaremos si ha llegado al final de la pantalla gráfica, sino al final del mapa: if ( SDLA_teclaPulsada (TECLA_DER) ) { xPersonaje ++; if (xPersonaje > MAXCOLS-1) xPersonaje --; }

  • No lo dibujaremos en coordenadas de la pantalla gráfica, sino de la misma forma que dibujamos todos los elementos del fondo: SDLA_dibujarImagenOculta(personaje, xIniPantalla + xPersonaje * anchoCasilla, yIniPantalla + yPersonaje * altoCasilla);

  • Antes de redibujar la pantalla, podemos comprobar si existe alguna fruta en la posición que ahora ocupará el personaje, y, si es así, podemos borrarla (en una etapa más avanzada del juego, no sólo haríamos eso, también aumentaríamos los puntos, etc.): if (mapa1[yPersonaje][xPersonaje] == 'C') mapa1[yPersonaje][xPersonaje] = ' ';



Apenas con esos cambios, nuestro personaje ya puede "comer frutas" del recorrido. El movimiento es más brusco que antes (avanzamos de "casilla" en "casilla"), pero de momento hemos ganado en versatilidad. Más adelante comentaremos cómo hacer que el movimiento vuelva a ser suave.

Como siempre, puedes ver todo el proyecto en: code.google.com/p/fruityfrank

21 octubre 2008

Remake (parcial) de Fruity Frank... 07 - Mapa para dibujar el fondo

Ahora vamos a aplicar nuestros conocimientos de "arrays" para que el fondo no sea prefijado, sino que se pueda crear y modificar "al vuelo".

En vez de tener una imagen con todo el tamaño del fondo (por ejemplo 640x480 puntos), tendremos varias imágenes pequeñas, por ejemplo de 40x30 píxeles.

La primera ventaja evidente es el ahorro de memoria: una pantalla de 640x480 puntos con sólo 256 colores necesitará 307.200 bytes para almacenarse. Si en nuestro mapa hay cinco tipos de elementos (dos tipos de fondo, un tipo de obstáculo, dos tipos de enemigo), necesitaremos 5 x 40 x30 = 6.000 bytes; además el mapa tendría 16 columnas (640/40) y 16 filas (480/30), lo que supone 16 x 16 = 256 bytes para guardar el mapa. En total, necesitamos 6.256 bytes para una pantalla... casi 50 veces menos que antes (y esta proporción puede ser aún mayor, si en la segunda pantalla hay elementos comunes con la primera).

Pero hay muchas más ventajas. Por ejemplo:


  • Es mucho más fácil modificar ese "mapa" que modificar una imagen, en caso de que queramos cambiar la estructura de la pantalla durante el juego (por ejemplo, si nuestro personaje "recoge un premio" o destruye un obstáculo).

  • Este mapa permite una forma muy fácil de comprobar colisiones entre objetos: si nuestro personaje se mueve a la casilla (2,3) y en esa casilla hay un "premio", deberemos aumentar la cantidad de puntos, etc.



¿Y cómo creamos ese mapa? Podría ser un "array de caracteres", formado por varias filas (un array de dos dimensiones), en el que cada letra tenga un significado especial:

    char mapa[10][16]={
"XCXXMXXX XMCXXX",
"MXXCXXXX XMXCMM",
"XMCXXXXX XCXXXC",
"XXCXCXCX XXXXCX",
" ",
"XXMCXMXX XCXXXX",
"XXXXXXXX XXXMXX",
"XXXXXCXX XXXXXX",
"XXXXXCXX XXXCXX",
"XXXXXXXX CXCXXC"
};


En este ejemplo, una X indicaría una casilla del fondo que se debe dibujar, una C sería una cereza, una M sería una manzana, y un espacio en blanco sería una casilla en la que no hay que dibujar nada.

Siendo prácticos, sería mejor no usar esos valores 10 y 16 para indicar el tamaño, sino usar dos constantes, llamadas MAXFILAS y MAXCOLS (por ejemplo), de modo que podamos revisar el mapa con dos bucles "for", así:

        for (int i=0; i<MAXFILAS; i++)
for (int j=0; j<MAXCOLS; j++) {
if (mapa1[i][j] == 'X')
SDLA_dibujarImagenOculta(fondoNivel1, ...


Entonces, el primer paso, antes de este doble "for", no sería dibujar el fondo, que ya no existe como un único elemento, sino borrar la pantalla oculta, para luego dibujar sobre ella elemento a elemento:

        SDLA_borrarPantallaOculta(0,0,0); // Borro en negro


El siguiente paso que daremos será usar este mapa para comprobar "colisiones", pero eso llegará dentro de poco, en la próxima entrega...

Como siempre, puedes ver todo el proyecto en: code.google.com/p/fruityfrank

16 octubre 2008

Remake (parcial) de Fruity Frank... 06 - Transparencia y tiempo "fiable"

Vamos a hacer dos pequeñas mejoras:


  • Al mover nuestro personaje, se mueve también el recuadro negro que lo rodea. Quedaría mucho mejor si ese recuadro negro no se viera, sino que nuestro personaje pareciera estar directamente sobre el fondo. Eso es fácil de conseguir: no podemos usar imágenes en formatos que tengan transparencia, como el PNG o el GIF (al menos por ahora), porque la versión básica de la biblioteca SDL sólo soporta el formato BMP, pero sí podemos indicar que uno de los colores que forman la imagen sea tratado como transparente.

  • La velocidad de nuestro juego depende de la velocidad del ordenador en el que lo probemos, hasta el punto de que puede resultar "injugable" en un ordenador muy rápido, y eso no debería ocurrir.




Para el primero de esos puntos, la transparencia, tenemos una orden en SDL ("ocultada" dentro de las funciones en español que tenemos listas para usar) que permite indicar qué color será considerado transparente, a partir de sus componentes RGB (rojo, verde, azul, indicados entre 0 y 255). Por ejemplo, si queremos que los puntos negros de la imagen sean considerados como transparentes, usaremos el color (0,0,0), o si queremos que lo sea el rojo puro, emplearemos (255,0,0).

En el caso de Fruity Frank, nuestro personaje tiene el pelo rojo y ropa azul, así que podemos usar el verde puro (0,255,0) para indicar los puntos transparentes. Editamos nuestra imagen desde cualquier editor gráfico (incluso el propio "Paint" de Windows serviría) y pintamos en color verde intenso el contorno de nuestro personaje, que antes era negro. Después le indicamos a nuestro juego que no dibuje ese verde, con:

    personaje=SDLA_cargarImagen("personaje1.bmp");
SDLA_colorTransparente(personaje, 0,255,0);


De igual modo, nuestro primer enemigo tiene zonas rojas, blancas, negras... así que el verde volvería a ser una buena elección para hacer su contorno transparente.

Por otra parte, para que la velocidad sea "casi igual" en cualquier ordenador, añadiremos una pausa entre un "fotograma" del juego y el siguiente. Si queremos que la visualización del juego tenga la velocidad habitual, 25 fotogramas por segundo, y suponemos que el tiempo que se pierde en redibujar la pantalla y en el análisis de la lógica de juego es despreciable, deberíamos hacer una pausa de 40 milisegundos tras cada redibujado de pantalla (1 segundo = 1000 milisegundos; 1000 ms / 25 fps = 40 ms entre un "fotograma" y otro):

    // Pausa de 40 ms, para velocidad de 25 fps (1000/40 = 25)
SDLA_pausa(40);



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

13 octubre 2008

Remake (parcial) de Fruity Frank... 05 - Un primer enemigo

Vamos a añadir un primer enemigo, cuyo movimiento esté prefijado y sea sencillo. Todavía no comprobaremos si nuestro personaje y el "enemigo" colisionan.

La idea básica es sencilla: tendremos una nueva imagen, que dibujaremos en sus propias coordenadas x e y, y al final de cada pasada calcularemos cual será su nueva posición.

La imagen la extraeremos de una de las capturas del juego, igual que hicimos con la de nuestro personaje. Para cargarla y dibujarla será muy parecido a lo que ya hemos hecho:

    int xEnemigo1=250, yEnemigo1=250;
SDLA_Imagen* enemigo1;
enemigo1=SDLA_cargarImagen("enemigo1i1.bmp");
...
SDLA_dibujarImagenOculta(enemigo1, xEnemigo1, yEnemigo1);


Moverlo no es mucho más difícil: basta con cambiar su coordenada X o su coordenada Y (o ambas) en cada pasada del bucle "do..while". Por ejemplo, como primer acercamiento podríamos hacer que se moviera hacia la derecha con "xEnemigo1 += 3;"

Lo razonable es que no siempre se mueva hacia la derecha (o al menos que lo haga sólo hasta llegar al margen de la pantalla, en vez de seguir avanzando incluso entonces). Un movimiento un poco más real sería que fuera hacia un lado de la pantalla y, cuando llegara a él, "rebotara" para volver hacia el lado contrario. Hay varias formas de conseguir este efecto. Por ejemplo:


  • Tener una variable auxiliar llamada "dirección" que nos permita memorizar hacia qué lado nos movemos. Cuando la "dirección" sea hacia la derecha, aumentaremos la X del personaje; cuando la "dirección" sea hacia la izquierda, disminuiremos la X del personaje; cuando llegue al margen derecho, cambiaremos "dirección" para que sea "hacia la izquierda"; cuando llegue al margen izquierdo, cambiaremos "dirección" para que sea "hacia la derecha".

  • Una alternativa es sumar siempre un incremento a xPersonaje. Este incremento empezará siendo +3 (por ejemplo) para que se mueva hacia la derecha, y cada vez que llegue a un extremo de la pantalla, cambiaremos el signo a ese incremento, para que empiece a desplazarse en sentido contrario. Podría ser algo así:



        xEnemigo1 += incrEnemigo;

if (xEnemigo1 < xIniPantalla)
{
xEnemigo1 = xIniPantalla;
incrEnemigo = -incrEnemigo;
}

if (xEnemigo1 > xFinPantalla-anchoEnemigo1)
{
xEnemigo1 = xFinPantalla-anchoEnemigo1;
incrEnemigo = -incrEnemigo;
}


Si eso está dentro del do..while del "bucle de juego", acabamos de conseguir que nuestro enemigo se mueva en cada "fotograma" del juego, tanto si movemos nuestro personaje como si no lo hacemos.

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


(Volver al índice)

09 octubre 2008

Remake (parcial) de Fruity Frank... 04 - Movimiento limitado

En la entrega anterior podíamos mover el personaje, pero nada impedía que no saliéramos de los límites de la pantalla de juego o incluso de la pantalla del ordenador. Vamos a hacer una pequeña mejora para evitarlo.

La idea es que cada vez que cambie una coordenada, por ejemplo xPersonaje, comprobaremos que su valor sea "razonable". Por ejemplo, la x no debería ser negativa, lo que podríamos conseguir con:

  if (xPersonaje < 0)
xPersonaje = 0;


Siendo estrictos, quizá la pantalla de nuestro juego no coincida por completo con la pantalla del ordenador, y, por tanto, no empiece justo en la posición 0. Entonces, una forma más genérica de conseguir lo anterior sería:

  if (xPersonaje < xIniPantalla)
xPersonaje = xIniPantalla;


En el lado contrario, la condición no es exactamente la misma: no nos basta con
if (xPersonaje > xFinPantalla) xPersonaje = xFinPantalla;
porque si situamos nuestro personaje a partir de xFinPantalla... ¡¡¡quedará fuera de la pantalla!!! Lo que debe coincidir con xFinPantalla es el último pixel de nuestro personaje, de modo que la comparación correcta sería:

  if (xPersonaje > xFinPantalla - anchoPersonaje)
xPersonaje = xFinPantalla - anchoPersonaje;


Como se ve, estamos utilizando más de una variable, que nos permitan controlar detalles como el ancho de nuestro personaje o los límites de la pantalla, de la forma más legible posible:

  int xPersonaje=340, yPersonaje=100;
int anchoPersonaje = 32, altoPersonaje = 32;
int xIniPantalla = 2, xFinPantalla = 638;
int yIniPantalla = 82, yFinPantalla = 448;


Ahora, cada vez que el usuario pulsa una tecla deberemos comprobar que el valor de x o y sea correcto antes de mover nuestro personaje a esa posición:

  if ( SDLA_teclaPulsada (TECLA_ARR) ) {
yPersonaje -= 4;
if (yPersonaje < yIniPantalla)
yPersonaje = yIniPantalla;
}


Para ver todo el proyecto en conjunto: code.google.com/p/fruityfrank

08 octubre 2008

Remake (parcial) de Fruity Frank... 03 - Moviendo el personaje

Un juego real no se queda parado hasta que pulsemos una tecla, sino que debe proseguir continuamente: por ejemplo, aunque nosotros no hagamos nada, los enemigos deberán moverse. Normalmente, incluso en la pantalla de presentación habrá algún tipo de animación.

Por eso, lo habitual será que no usemos "SDLA_esperarTecla" sino "SDLA_teclaPulsada". La forma de emplear esta orden será indicarle entre paréntesis la tecla que queremos comprobar: if ( SDLA_teclaPulsada (TECLA_ABA) ) ..."

Hay varias constantes predefinidas, para representar las teclas más habituales:


  • TECLA_ABA para la flecha hacia abajo del teclado.

  • TECLA_ARR para la flecha hacia arriba.

  • TECLA_DER para la flecha hacia la derecha.

  • TECLA_IZQ para la flecha hacia la izquierda.

  • TECLA_ESC para la tecla de ESCAPE (Esc).

  • TECLA_ESP para la barra espaciadora.

  • ...



Lo único que quedar para que el personaje se mueva es que su posición no sean unas coordenadas fijas, como el (300,100) del ejemplo anterior, sino usar variables (por ejemplo: xPersonaje, yPersonaje) que modifiquemos cuando comprobemos que se pulsa una tecla.

Finalmente, el propio cuerpo del programa ya no deberá ejecutarse sólo una vez, sino repetirse hasta que se pulse cierta tecla (por ejemplo ESC).

La nueva apariencia de nuestro mini-juego será:

#include "SDL_ASI.h"

int main (int argc, char** argv)
{
int xPersonaje=340, yPersonaje=100;

SDLA_inicializar(640,480,24);

SDLA_Imagen* fondoPantalla;
SDLA_Imagen* personaje;

fondoPantalla=SDLA_cargarImagen("fondo.bmp");
personaje=SDLA_cargarImagen("personaje1.bmp");

do {

SDLA_dibujarImagenOculta(fondoPantalla,96,0);
SDLA_dibujarImagenOculta(personaje, xPersonaje, yPersonaje);
SDLA_visualizarOculta();

if ( SDLA_teclaPulsada (TECLA_ABA) )
yPersonaje += 4;
if ( SDLA_teclaPulsada (TECLA_ARR) )
yPersonaje -= 4;
if ( SDLA_teclaPulsada (TECLA_DER) )
xPersonaje += 4;
if ( SDLA_teclaPulsada (TECLA_IZQ) )
xPersonaje -= 4;

} while (! SDLA_teclaPulsada (TECLA_ESC));
return 0;

}


Pero si nos acercamos mucho a los extremos, veremos que el personaje se puede salir de la pantalla de juego... o incluso de la pantalla del ordenador. Ésa será la próxima mejora.

Para ver todo el proyecto en conjunto: code.google.com/p/fruityfrank

06 octubre 2008

Remake (parcial) de Fruity Frank... 02 - Mostrar fondo y personaje

En un juego real no existe sólo la imagen de fondo. Típicamente existirá también al menos un personaje que se mueva por ella. Hoy vamos a dibujar ese personaje...

En primer lugar, hacemos una copia de la imagen de fondo inicial, la que mostraba tanto la pantalla de fondo del juego como nuestro personaje (o incluso dos copias, si queremos conservar el original). En una de las imágenes borramos el personaje, de modo que sólo se vea realmente el fondo. En la otra "recortamos" el personaje, eliminando todo lo demás (usando cualquier editor gráfico, como por ejemplo XnView). Ambas imágenes deben estar en formato BMP. Por ejemplo, el fondo podría ser "fondo.bmp" y el personaje podría ser "personaje1.bmp" (el 1 es porque más adelante tendremos más de una imagen del personaje, para dar impresión de movimiento).

Una vez preparadas las imágenes, los cambios a realizar en nuestro "juego.cpp" son mínimos:.


  • Declararemos la variable que representará la imagen del personaje: SDLA_Imagen* personaje;
  • Cargaremos esa imagen desde fichero: personaje=SDLA_cargarImagen("personaje1.bmp");

  • Y la dibujaremos en la pantalla oculta, en ciertas coordenadas (por ejemplo, x=300, y=100), antes de mostrar ésta: SDLA_dibujarImagenOculta(personaje,300,100);



Es decir, el fuente sería algo como:

#include "SDL_ASI.h"

int main (int argc, char** argv)
{
SDLA_inicializar(640,480,24);

SDLA_Imagen* fondoPantalla;
SDLA_Imagen* personaje;
fondoPantalla=SDLA_cargarImagen("fondo.bmp");
personaje=SDLA_cargarImagen("personaje1.bmp");

SDLA_dibujarImagenOculta(fondoPantalla,0,0);
SDLA_dibujarImagenOculta(personaje,300,100);
SDLA_visualizarOculta();

SDLA_esperarTecla();
return 0;
}


Pero ese personaje todavía no se mueve... Por pocas horas, mañana sí lo hará.

Para ver todo el proyecto en conjunto: code.google.com/p/fruityfrank

05 octubre 2008

Remake (parcial) de Fruity Frank... 01 - Mostrar el fondo

En este primer acercamiento vamos a preparar las herramientas que usaremos para crear el juego, y las probaremos para mostrar la imagen de fondo del juego.


  • Como entorno de desarrollo usaremos CodeBlocks, que incluye el compilador GNU de C y C++, junto con entorno integrado (editor y depurador). Se podría usar cualquier otro, pero en el apartado de "Descargas" dejaré un proyecto de CodeBlocks listo para ser usado simplemente con un "doble clic" desde Windows. Quien elija usar otro entorno de desarrollo tendrá que hacer un poco más de "trabajo manual".

  • Como biblioteca que nos proporcione las funcionalidades básicas usaremos SDL (Simple DirectMedia Layer)

  • Nos apoyaremos en un par de ficheros auxiliares que se encargan de ocultar los detalles de SDL y de permitirnos usar órdenes traducidas al español, algo más sencillas.  Todas estas órdenes comenzarán por "SDLA_", para distinguirlas de las órdenes "normales" de C++ y/o de SDL.



Usando estos ficheros auxiliares, un primer fuente de prueba sería así:

#include "SDL_ASI.h"

int main (int argc, char** argv)
{
SDLA_inicializar(640,480,24);

SDLA_Imagen* fondoPantalla;
fondoPantalla=SDLA_cargarImagen("fondo.bmp");

SDLA_dibujarImagenOculta(fondoPantalla,0,0);
SDLA_visualizarOculta();

SDLA_esperarTecla();
return 0;
}


Vamos a ver línea por línea lo que hace, porque entenderlo será vital para poder irlo ampliando...

  • #include "SDL_ASI.h" - Carga uno de los ficheros auxiliares que necesitamos, el fichero de cabecera "SDL_ASI.h".

  • int main (int argc, char** argv) - Comienzo del cuerpo del programa ("main").

  • SDLA_inicializar(640,480,24);
    - Inicializa la pantalla gráfica, escogiendo el modo de 640 x 480 puntos (ancho x alto) y 24 bits de color (16 millones de colores).

  • SDLA_Imagen* fondoPantalla; - Declara una variable llamada "fondoPantalla", que será una imagen (tipo SDLA_Imagen*)

  • fondoPantalla=SDLA_cargarImagen("fondo.bmp");
    - Prepara el valor de "fondoPantalla": será una imagen que se cargue de disco, llamada "fondo.bmp".

  • SDLA_dibujarImagenOculta(fondoPantalla,0,0); - Dibuja la imagen "fondoPantalla" en las coordenadas (0,0) de la pantalla oculta, es decir en la esquina superior izquierda. Para evitar parpadeos en pantalla, siempre dibujaremos los elementos de nuestro juego uno a uno en una pantalla oculta, y finalmente haremos que toda esa pantalla oculta pase a ser visible.

  • SDLA_visualizarOculta();
    - Hace que la pantalla oculta se pueda ver.

  • SDLA_esperarTecla(); - Esperamos a que se pulse una tecla.

  • return 0; - Termina el cuerpo del programa.



Para probar todo esto, bastaría con descargar el entorno CodeBlocks, añadirle los paquetes de SDL (al menos SDL y SDL_ttf) desde el menú hacer doble clic en el fichero del proyecto: "fruity.cbp". Sólo hace falta que hayamos preparado una imagen de fondo del juego, que se llame "fondo.bmp", que podríamos haber capturado del juego original, usando un emulador (esta imagen forma parte del proyecto que se puede descargar).

Para poner en marcha el "supuesto juego" (que todavía es poco jugable), entraríamos al menú "Build" (construir) de CodeBlocks y escogeríamos la opción "Build & Run" (construir y ejecutar). Si todo es correcto (debería ser así), debería aparecer la pantalla de fondo del juego y esperar que pulsemos una tecla, momento en el que se acaba "el juego".




Para los que tengan más conocimientos técnicos, voy a dar algún detalle más de cómo son los otros dos ficheros auxiliares que forman parte de nuestro proyecto.

El primero de ellos, el que incluimos desde nuestro juego, se llama SDL_ASI.h, y es un fichero de cabecera que tiene los resúmenes de las posibilidades de nuestra "mini-biblioteca" SDL_ASI, junto con las declaraciones de constantes:

#ifndef SDL_ASI_H

#define SDL_ASI_H

#include <SDL/SDL.h>
#include <SDL/SDL_ttf.h>

/* Adaptacion de SDL, ocultando lo basico y
traduciendo al castellano */

/* =====================================
Versiones anteriores:

Numero Fecha Cambios
------- ---------- ----------------

0.04 18/05/2007 Permite escribir texto en la pantalla oculta
(necesita SDL_TTF)
Añadida una funcion para vaciar buffer de
teclado

0.03 18/05/2007 Añadido "esperaTecla", para poder esperar a
que se pulse una tecla sin necesidad de "while".
Añadidas varias constantes más para teclas habituales:
números del 0 al 5, espacio.

0.02 27/04/2007 Añadido "colision", corregido "teclaPulsada",
que usaba parametro "char" en vez de "int",
separados prototipos (.h) de codigo (.c)

0.01 26/04/2007 Versión inicial, que enmascara SDL: permite
inicializar, borrar la pantalla oculta,
cargar una imagen, dibujar una imagen en la
pantalla oculta, visualizar la pantalla
oculta, ver si se ha pulsado el ratón, ver
si se ha pulsado una tecla, y define las teclas
del cursor y ESC.


===================================== */

#define SDLA_Imagen SDL_Surface
#define SDLA_Fuente TTF_Font
#define SDLA_Color SDL_Color

#define TECLA_ESC SDLK_ESCAPE
#define TECLA_DER SDLK_RIGHT
#define TECLA_ARR SDLK_UP
#define TECLA_ABA SDLK_DOWN
#define TECLA_IZQ SDLK_LEFT

#define TECLA_J SDLK_j
#define TECLA_A SDLK_a
#define TECLA_C SDLK_c
#define TECLA_R SDLK_r
#define TECLA_V SDLK_v
#define TECLA_1 SDLK_1
#define TECLA_2 SDLK_2
#define TECLA_3 SDLK_3
#define TECLA_4 SDLK_4

#define TECLA_ESP SDLK_SPACE

/*---------------------------*/


int SDLA_inicializar(int ancho, int alto, int colores);

SDLA_Imagen* SDLA_cargarImagen(char *fichero);

void SDLA_colorTransparente(SDLA_Imagen* imagen,
int r, int g, int b);

int SDLA_borrarPantallaOculta(int r, int g, int b);

int SDLA_dibujarImagenOculta(SDLA_Imagen *imagen,
int x,int y);

int SDLA_escribirTextoOculta(char *frase,
int x,int y, int color, SDLA_Fuente *fuente);

int SDLA_visualizarOculta();

int SDLA_teclaPulsada(int c);

int SDLA_esperarTecla();

void SDLA_vaciarBufferTeclado();

int SDLA_ratonPulsado(int *x, int *y);

int SDLA_pausa(long milisegundos);

int SDLA_colision(int x1, int y1, int an1, int al1,
int x2, int y2, int an2, int al2);

#endif



El segundo fichero, que no incluimos directamente desde "juego.cpp", pero es parte del proyecto, se llama "sdlasi.c", y contiene los detalles de esas funciones:

#include "SDL_ASI.h"


/* Adaptacion de SDL, ocultando lo basico y
traduciendo al castellano */

SDLA_Fuente *SDLA_arial12;
SDLA_Fuente *SDLA_arial24;

SDLA_Imagen* pantallaOculta;
SDLA_Imagen* pantallaVisible;


int SDLA_inicializar(int ancho, int alto, int colores) {
// Inicializo SDL
if ( SDL_Init( SDL_INIT_VIDEO ) < 0 )
exit(1);

// Me aseguro de que SDL limpia al salir
atexit(SDL_Quit);

// Creo la pantalla visibl
pantallaVisible = SDL_SetVideoMode(
ancho, alto, colores,
SDL_HWSURFACE|SDL_DOUBLEBUF);
if ( !pantallaVisible )
exit(2);

// Y la oculta
pantallaOculta = SDL_CreateRGBSurface(SDL_SWSURFACE,
ancho,alto,colores, 0,0,0,0);
if ( !pantallaOculta )
exit(3);

// Inicializo SDL_TTF
if ( TTF_Init() < 0 )
exit(5);
atexit(TTF_Quit);

SDLA_arial12 = TTF_OpenFont("arial.ttf",12);
SDLA_arial24 = TTF_OpenFont("arial.ttf",24);

return 0;
}

SDLA_Imagen* SDLA_cargarImagen(char *fichero) {
SDLA_Imagen* imagen;
imagen = SDL_LoadBMP(fichero);
if (imagen == NULL)
exit(4);
return imagen;
}


void SDLA_colorTransparente(SDLA_Imagen* imagen,
int r, int g, int b){

SDL_SetColorKey(imagen,
SDL_SRCCOLORKEY|SDL_RLEACCEL,
SDL_MapRGB(imagen->format, r,g,b));
}

int SDLA_borrarPantallaOculta(int r, int g, int b) {
SDL_FillRect(pantallaOculta,
0,SDL_MapRGB(pantallaOculta->format,
r,g,b));
}

int SDLA_dibujarImagenOculta(SDLA_Imagen *imagen,
int x,int y){

SDL_Rect posicion;
posicion.x = x;
posicion.y = y;
SDL_BlitSurface(imagen, 0,
pantallaOculta, &posicion);
}

int SDLA_escribirTextoOculta(char *frase,
int x,int y, int color, SDLA_Fuente *fuente){

SDLA_Color bgcolor,fgcolor;
SDLA_Imagen *textoComoImagen;

/* El color de primer plano sera el escogido */
fgcolor.r = (color & 0xff0000)>>16;
fgcolor.g = (color & 0x00ff00)>>8;
fgcolor.b = color & 0x0000ff;

/* El fondo sera negro (transparente) */
bgcolor.r=0;
bgcolor.g=0;
bgcolor.b=0;

/* Preparo el bitmap */
textoComoImagen = TTF_RenderText_Shaded(fuente,
frase,fgcolor,bgcolor);
SDLA_colorTransparente(textoComoImagen,0,0,0);

/* Y dibujo */
SDLA_dibujarImagenOculta(textoComoImagen, x,y);
}

int SDLA_visualizarOculta() {
SDL_BlitSurface(pantallaOculta, 0,
pantallaVisible, 0);
SDL_Flip( pantallaVisible );
}

int SDLA_teclaPulsada(int c) {
SDL_PumpEvents();
Uint8 *keys = SDL_GetKeyState(NULL);
if (keys[c] == 1)
return 1;
else
return 0;
}

int SDLA_esperarTecla() {
SDL_Event suceso;

/* Imitacion de "getch()" para SDL */
while (1) { /* Repetimos indefinidamente */
while (SDL_PollEvent(&suceso)) { /* Comprobamos sucesos */
if (suceso.type == SDL_KEYDOWN) /* Si es tecla pulsada */
return suceso.key.keysym.sym; /* La devolvermos */
}
}

}

void SDLA_vaciarBufferTeclado() {
SDL_Event suceso;

while ((SDL_PollEvent(&suceso)) &&
(suceso.type == SDL_KEYDOWN))
;
}


int SDLA_ratonPulsado(int *x, int *y) {
SDL_PumpEvents();

return(SDL_GetMouseState(x,y)&SDL_BUTTON(1));
}

int SDLA_pausa(long milisegundos) {
SDL_Delay(milisegundos);
}

int SDLA_colision(int x1, int y1, int an1, int al1,
int x2, int y2, int an2, int al2) {
if ((x2+an2 > x1)
&& (y2+al2 > y1)
&& (x1+an1 > x2)
&& (y1+al1 > y2))
return 1;
else
return 0;
}


Siendo estrictos, necesitamos dos cosas más (que también se pueden descargar desde la página del proyecto): las DLL llamadas SDL.DLL y SDL_TTF.DLL, que deberían estar accesibles (al menos en el PATH, pero para distribuir nuestro juego puede ser cómodo que se encuentren en la misma carpeta que el ejecutable) y el tipo de letra ARIAL.TTF (en las msimas condiciones).

¿Y cual es la página del proyecto? code.google.com/p/fruityfrank

04 octubre 2008

Un "remake" paso a paso

Este es otro proyecto que iré dejando accesible por si a alguien más le interesara seguirlo...

Va a ser un "remake" de un juego de los 80. Sólo será un remake parcial, no pretendo reconstruir el juego original por completo, sino:

  • Crear un juego sencillo paso a paso, de forma que pueda seguirlo cualquier persona que apenas tenga unas nociones básicas de programación.

  • Usar herramientas estándar, como el lenguaje C++ y la biblioteca SDL, de forma que el juego se pueda hacer funcionar tanto en Windows como en Linux o en otros sistemas operativos.

  • Que todos los fuentes queden disponibles en algún servidor de proyectos, como Google Code, de forma que quien esté interesado pueda seguir su evolución.



El juego escogido debería tener estas características:

  • Unos gráficos sencillos, que permitan que nos podamos centrar en la lógica de juego más que en la parte visual.

  • Un personaje que el usuario deba controlar.

  • Tener "premios" que conseguir y "obstáculos" que esquivar.

  • Enemigos que se muevan de forma independiente.

  • Distintas pantallas que recorrer.

  • Una tabla con las mejores puntuaciones.

  • Una pantalla de presentación.

  • ...


Además, es deseable que se trate de un juego que esté disponible para alguna consola u ordenador doméstico, de modo que se puean capturar pantallas, para no perder tiempo en crear gráficos nuevos. También es preferible que sea un juego para el que ya no exista copyright, para que se puedan publicar esas capturas de pantallas. Por eso, en vez de elegir uno de los muchos juegos disponibles para máquinas recreativas, he optado por un juego poco conocido, que se desarrolló en el año 1984 para la gama de ordenadores Amstrad CPC, el Fruity Frank:



Aun así, los pasos que daré se podrán aplicar a otros muchos juegos, tanto nuevos como "remakes" de clásicos.

Si alguien quiere probar el juego original, lo puede descargar (junto con emuladores del Amstrad CPC) en:

www.amstrad.es/juegosamstrad/decargajuegos/fruityfrank.php

Los pasos previstos (el orden puede variar) son:

  1. Ocultar los detalles de SDL, creando ciertas funciones alternativas en español.

  2. Mostrar una primera imagen (la pantalla de fondo inicial de nuestro juego).

  3. Mostrar el personaje sobre esa imagen.

  4. Mover el personaje por encima del fondo, al pulsar las flechas del teclado.

  5. Hacer que el personaje "cambie al andar" y que no se pueda salir de los límites de la pantalla.

  6. Añadir "premios" que el personaje pueda recoger y "obstáculos" que no pueda atravesar

  7. Añadir un enemigo que se mueva al azar.

  8. La partida acaba si tocamos al enemigo.

  9. Tabla de mejores puntuaciones.

  10. Hacer que el movimiento sea más suave.

  11. Una pantalla de presentación.

  12. Añadir varios enemigos de distintos tipos.

  13. Diferentes niveles de dificultad.

  14. ...



Todo eso, muy pronto aquí mismo...   ;-)