23 octubre 2010

El bucle de juego

Un fuente con SDL puede resultar difícil de leer, y más aún si no está estructurado, sino que tiene toda la lógica dentro de "Main" y va creciendo arbitrariamente. Por eso, suele ser preferible crear nuestras propias funciones que la oculten un poco: funciones como "Inicializar", "CargarImagen", "ComprobarTeclas", "DibujarImagenes", etc.

Un "bucle de juego clásico" tendría una apariencia similar a esta:
  • Comprobar eventos (pulsaciones de teclas, clics o movimiento de ratón, uso de joystick...)
  • Mover los elementos del juego (personaje, enemigos, fondos móviles)
  • Comprobar colisiones entre elementos del juego (que pueden suponer perder vidas o energía, ganar puntos, etc)
  • Dibujar todos los elementos en pantalla en su estado actual
  • Hacer una pausa al final de cada "fotograma", para que la velocidad del juego sea la misma en cualquier ordenador, y, de paso, para ayudar a la multitarea del sistema operativo.

Por tanto, crearemos esas funciones, junto con otras auxiliares para inicializar el sistema, dibujar una imagen en pantalla oculta, escribir un texto en pantalla oculta… El resultado podría ser éste:




/*---------------------------*/
/* sdl05.cs */
/* */
/* Quinto acercamiento */
/* a SDL */
/* */
/* Por Nacho Cabanes */
/*---------------------------*/

using Tao.Sdl;
using System; // Para IntPtr (puntero: imágenes, etc)

public class Juego
{
short anchoPantalla = 800;
short altoPantalla = 600;
bool terminado = false;
short x = 400;
short y = 300;
short anchoImagen = 50;
short altoImagen = 50;
Sdl.SDL_Event suceso;
int numkeys;
byte[] teclas;
IntPtr pantallaOculta;
IntPtr tipoDeLetra;
IntPtr imagen;

void inicializar()
{
int bitsColor = 24;
int flags = Sdl.SDL_HWSURFACE | Sdl.SDL_DOUBLEBUF | Sdl.SDL_ANYFORMAT
| Sdl.SDL_FULLSCREEN;

// Inicializamos SDL
Sdl.SDL_Init(Sdl.SDL_INIT_EVERYTHING);
pantallaOculta = Sdl.SDL_SetVideoMode(
anchoPantalla,
altoPantalla,
bitsColor,
flags);
// Y SdlTTF, para escribir texto
SdlTtf.TTF_Init();

// Indicamos que se recorte lo que salga de la pantalla oculta
Sdl.SDL_Rect rect2 =
new Sdl.SDL_Rect(0,0, (short) anchoPantalla, (short) altoPantalla);
Sdl.SDL_SetClipRect(pantallaOculta, ref rect2);

// Cargamos una imagen
imagen = SdlImage.IMG_Load("personaje.png");
if (imagen == IntPtr.Zero) {
System.Console.WriteLine("Imagen inexistente: personaje.png!");
Environment.Exit(4);
}

// Y un tipo de letra, en tamano 18
tipoDeLetra = SdlTtf.TTF_OpenFont("FreeSansBold.ttf", 18);
if (tipoDeLetra == IntPtr.Zero) {
System.Console.WriteLine("Tipo de letra inexistente: FreeSansBold.ttf!");
Environment.Exit(5);
}

}

void comprobarTeclas()
{
// Comprobamos sucesos
Sdl.SDL_PollEvent(out suceso);
teclas = Sdl.SDL_GetKeyState(out numkeys);

// Miramos si se ha pulsado alguna flecha del cursor
if (teclas[Sdl.SDLK_UP] == 1)
y -= 2;
if (teclas[Sdl.SDLK_DOWN] == 1)
y += 2;
if (teclas[Sdl.SDLK_LEFT] == 1)
x -= 2;
if (teclas[Sdl.SDLK_RIGHT] == 1)
x += 2;
if (teclas[Sdl.SDLK_ESCAPE] == 1)
terminado = true;
}


void comprobarColisiones()
{
// Todavia no comprobamos colisiones con enemigos
// ni con obstaculos
}


void moverElementos()
{
// Todavia no hay ningun elemento que se mueva solo
}

void dibujarElementos()
{
// Borramos pantalla
Sdl.SDL_Rect origen = new Sdl.SDL_Rect(0,0,
anchoPantalla,altoPantalla);
Sdl.SDL_FillRect(pantallaOculta, ref origen, 0);

// Dibujamos la imagen en sus nuevas coordenadas
dibujarImagenOculta(imagen,x,y, anchoImagen, altoImagen);

// Escribimos el texto, como imagen
escribirTextoOculta("Texto de ejemplo",
/* Coordenadas */ 200,100,
/* Colores */ 50, 50, 255,
tipoDeLetra
);

// Mostramos la pantalla oculta
Sdl.SDL_Flip(pantallaOculta);
}


void pausaFotograma()
{
// Esperamos 20 ms
System.Threading.Thread.Sleep( 20 );
}


void dibujarImagenOculta(IntPtr imagen, short x, short y,
short ancho, short alto)
{
Sdl.SDL_Rect origen = new Sdl.SDL_Rect(0, 0, ancho, alto);
Sdl.SDL_Rect dest = new Sdl.SDL_Rect(x, y, ancho, alto);
Sdl.SDL_BlitSurface(imagen, ref origen, pantallaOculta, ref dest);
}


void escribirTextoOculta(string texto,
short x, short y, byte r, byte g, byte b, IntPtr fuente)
{
// Creamos una imagen a partir de ese texto
Sdl.SDL_Color color = new Sdl.SDL_Color(r, g, b);
IntPtr textoComoImagen = SdlTtf.TTF_RenderText_Solid(
fuente, texto, color);
if (textoComoImagen == IntPtr.Zero){
System.Console.WriteLine("No se puedo renderizar el texto");
Environment.Exit(6);
}
dibujarImagenOculta(textoComoImagen, x,y, (short) (800-x), (short) (600-y));
}


bool partidaTerminada()
{
return terminado;
}


private static void Main()
{
Juego j = new Juego();
j.inicializar();

// Bucle de juego
do {
j.comprobarTeclas();
j.moverElementos();
j.comprobarColisiones();
j.dibujarElementos();
j.pausaFotograma();
} while (! j.partidaTerminada() );

// Finalmente, cerramos SDL
Sdl.SDL_Quit();

}

}

19 octubre 2010

Imágenes PNG y JPG con Tao.SDL

Sabemos mostrar imágenes en formato BMP desde un programa en C# que use Tao.Sdl. Pero las imágenes en este formato ocupan mucho espacio, y no permiten características avanzadas, como la transparencia (aunque se podría imitar). Si queremos usar imágenes en formatos más modernos, como JPG o PNG, sólo tenemos que incluir unos cuantos ficheros DLL más y hacer un pequeño cambio en el programa.

Los nuevos ficheros que necesitamos son: SDL_image.dll (el principal), libpng13.dll (para imágenes PNG), zlib1.dll (auxiliar para el anterior) y jpeg.dll (si queremos usar imágenes JPG - pero cuidado, quizá SDL_image.dll provoque un mensaje de error si no está también esta DLL, así que puede ser interesante dejarla también en la carpta del proyecto, incluso si no empleaños imágenes JPG).

En el fuente, sólo cambiaría la orden de cargar cada imagen, que no utilizaría Sdl.SDL_LoadBMP sino SdlImage.IMG_Load:

imagen = SdlImage.IMG_Load("personaje.png");

13 octubre 2010

Fuentes TTF con SDL y C#

Si queremos escribir texto usando tipos de letra TrueType (los habituales en Windows y Linux), los cambios no son grandes:

Debemos incluir el fichero DLL llamado SDL_ttf.DLL en la carpeta del ejecutable de nuestro programa, así como el fichero TTF correspondiente a cada tipo de letra que queramos usar.

Tenemos que declarar un nuevo dato que será nuestro tipo de letra, del tipo genérico "IntPtr":

IntPtr tipoDeLetra;


También tenemos que inicializar SdlTtf después de la inicialización básica de SDL:

SdlTtf.TTF_Init();


Luego preparamos el tipo de letra que queremos usar, indicando a partir de qué fichero TTF y en qué tamaño:

// Un tipo de letra, en tamano 18
tipoDeLetra = SdlTtf.TTF_OpenFont("FreeSansBold.ttf", 18);
if (tipoDeLetra == IntPtr.Zero) {
System.Console.WriteLine("Tipo de letra inexistente: FreeSansBold.ttf!");
Environment.Exit(5);
}


A continuación, podemos crear una imagen a partir de una frase:

// Y creamos una imagen a partir de ese texto
Sdl.SDL_Color colorAzulIntenso = new Sdl.SDL_Color(50, 50, 255);
string texto = "Texto de ejemplo";
IntPtr textoComoImagen = SdlTtf.TTF_RenderText_Solid(
tipoDeLetra, texto, colorAzulIntenso);

if (textoComoImagen == IntPtr.Zero) {
System.Console.WriteLine("No se puedo renderizar el texto");
Environment.Exit(6);
}


Y podríamos dibujar esa imagen en cualquier parte de la pantalla, como cualquier otra imagen:

// Escribimos el texto, como imagen
origen = new Sdl.SDL_Rect(0,0,anchoPantalla,altoPantalla);
dest = new Sdl.SDL_Rect(200,100,anchoPantalla,altoPantalla);
Sdl.SDL_BlitSurface(textoComoImagen, ref origen,
pantallaOculta, ref dest);


Como esta forma de trabajar puede resultar engorrosa, dentro de poco lo mejoraremos, creando una clase "Fuente" que nos oculte todos estos detalles y nos permita escribir texto de forma sencilla.

11 octubre 2010

C#: Una imagen que se mueve con el teclado

Hemos visto lo básico de como mostrar imágenes en pantalla usando C# y SDL.

Un segundo ejemplo algo más detallado podría permitirnos mover el personaje con las flechas del teclado, a una velocidad de 50 fotogramas por segundo, así (tienes los comentarios tras el fuente):



/* Ejemplo de acceso a SDL
desde C# (2),
usando Tao.SDL

Por Nacho Cabanes
*/


using Tao.Sdl;
using System; // Para IntPtr (puntero: imágenes, etc)

public class Sdl02

{

private static void Main()
{


short anchoPantalla = 800;
short altoPantalla = 600;

int bitsColor = 24;
int flags = Sdl.SDL_HWSURFACE | Sdl.SDL_DOUBLEBUF | Sdl.SDL_ANYFORMAT
| Sdl.SDL_FULLSCREEN;

IntPtr pantallaOculta;

// Inicializamos SDL
Sdl.SDL_Init(Sdl.SDL_INIT_EVERYTHING);

pantallaOculta = Sdl.SDL_SetVideoMode(
anchoPantalla,
altoPantalla,
bitsColor,
flags);


// Indicamos que se recorte lo que salga de la pantalla oculta
Sdl.SDL_Rect rect2 =
new Sdl.SDL_Rect(0,0, (short) anchoPantalla, (short) altoPantalla);

Sdl.SDL_SetClipRect(pantallaOculta, ref rect2);

// Cargamos una imagen
IntPtr imagen;

imagen = Sdl.SDL_LoadBMP("personaje.bmp");
if (imagen == IntPtr.Zero) {

System.Console.WriteLine("Imagen inexistente!");
Environment.Exit(4);

}

// Parte repetitiva
bool terminado = false;
short x = 400;

short y = 300;
short anchoImagen = 50;

short altoImagen = 50;
Sdl.SDL_Event suceso;
int numkeys;

byte[] teclas;

do
{
// Comprobamos sucesos
Sdl.SDL_PollEvent(out suceso);
teclas = Sdl.SDL_GetKeyState(out numkeys);


// Miramos si se ha pulsado alguna flecha del cursor
if (teclas[Sdl.SDLK_UP] == 1)

y -= 2;
if (teclas[Sdl.SDLK_DOWN] == 1)

y += 2;
if (teclas[Sdl.SDLK_LEFT] == 1)

x -= 2;
if (teclas[Sdl.SDLK_RIGHT] == 1)

x += 2;
if (teclas[Sdl.SDLK_ESCAPE] == 1)

terminado = true;


// Borramos pantalla
Sdl.SDL_Rect origen = new Sdl.SDL_Rect(0,0,

anchoPantalla,altoPantalla);
Sdl.SDL_FillRect(pantallaOculta, ref origen, 0);

// Dibujamos en sus nuevas coordenadas
origen = new Sdl.SDL_Rect(0,0,anchoImagen,altoImagen);

Sdl.SDL_Rect dest = new Sdl.SDL_Rect(x,y,

anchoImagen,altoImagen);
Sdl.SDL_BlitSurface(imagen, ref origen,
pantallaOculta, ref dest);


// Mostramos la pantalla oculta
Sdl.SDL_Flip(pantallaOculta);

// Y esperamos 20 ms
System.Threading.Thread.Sleep( 20 );


} while (!terminado);

// Finalmente, cerramos SDL
Sdl.SDL_Quit();

}

}


Las diferencias de este fuente con el anterior son:

  • Al inicializar, añadimos una nueva opción, Sdl.SDL_FULLSCREEN, para que el "juego" se ejecute a pantalla completa, en vez de hacerlo en ventana.
  • Usamos un bucle "do...while" para repetir hasta que se pulse la tecla ESC.
  • SDL_Event es el tipo de datos que se usa para comprobar "sucesos", como pulsaciones de teclas, o de ratón, o el uso del joystick.
  • Con SDL_PollEvent forzamos a que se mire qué sucesos hay pendientes de analizar.
  • SDL_GetkeyState obtenemos un array que nos devuelve el estado actual de cada tecla. Luego podemos mirar cada una de esas teclas accediendo a ese array con el nombre de la tecla en inglés, así: teclas[Sdl.SDLK_RIGHT]
  • SDL_FillRect rellena un rectángulo con un cierto color. Es una forma sencilla de borrar la pantalla o parte de ésta.