Hoy os traigo la segunda parte del tutorial para haceros vuestros propios minijuegos en Linux usando el lenguaje C y la biblioteca de desarrollo SDL. Para todos los que no sepais de que va, o para aquellos que se la hayan saltado, recordad que tenéis la primera parte del tutorial en este enlace si queréis poneros al tanto.
En esta segunda parte vamos a hacer algo muy interesante: vamos a construirnos nuesta propia biblioteca en C++ (tranquilos que no será nada del otro mundo) en la cual nos apoyaremos para usar sprites en nuestro humilde juego. Avisados quedais desde ahora, que esta parte del tutorial será mucho más austera (si cabe) que la primera, pues no me voy a detener mucho en explicaciones sobre C++, simplemente veremos para qué sirve cada cosa que estemos haciendo. También manejaremos los conceptos de frames (cuadros) y sprites. Si ya está todo listo vamos a comenzar.
Análisis de conceptos
Antes de nada, vamos a hacer un breve resumen de los términos que manejaremos y de las partes de la que constará nuestra biblioteca. Primeramente tenemos que saber lo que son los cuadros y los sprites. Seguramente ya os suenen a muchos: los sprites son imágenes o grupos de imágenes en 2D estáticas y que pueden estar formadas por uno o más cuadros que mostrados uno detrás de otro, causan la ilusión de movimiento. Por supuesto, un cuadro será cada una de estas imágenes individuales que conforman un sprite. Todo esto dicho así nos deja las cosas bastante claras: básicamente, un sprite será cualquier gráfico «móvil» que mostremos en pantalla (un avión, un laser, etc), y un cuadro o frame será una imágen individual que sola, o conjuntamente con otras, formará un sprite.
Ahora que tenemos claro lo que queremos, vamos a ver cómo lo obtendremos. Vamos a crear dos clases en C++, una llamada CFrame que, como su nombre indica, se encargará de cargar o eliminar frames de nuestros sprites. Constará, por tanto, de dos métodos o funciones. Por otro lado, crearemos otra llamada CSprite, que nos ayudará a manejar varios aspectos de un sprite, como son la posición en pantalla, el número de cuadros que lo conforman, si colisiona con otro sprite en pantalla, etc. Con todo ya ideado, vamos a comenzar. La primera tarea será escribir un archivo .h o cabecera donde declararemos tanto nuestras clases, como sus miembros y funciones. Comenzamos el archivo con un par de instrucciones del preprocesador y declarando la clase CFrame que quedaría así:
#ifndef CSPRITE_H_ #define CSPRITE_H_ #define TRUE 1 #define FALSE 0 class CFrame { public: SDL_Surface *img; void load(char *path); void unload(); };
Por ahora no debería haber problemas, definimos una clase llamada CFrame y declaramos su único miembro y sus dos métodos como públicos, es decir, podrán ser llamados y consultados por otras partes del programa no relacionadas con la clase. Cerrando con un cuidadoso punto y coma (cuidado con ellos, que muchos dolores de cabeza me han costado) ya tenemos definida nuestra clase encargada de los cuadros, que contendrá la imagen a mostrar, una función que cargará dicha imagen y otra que hará lo contrario, la eliminará.
La siguiente clase sí que puede dar más trabajo, así que pensemos cuidadosamente cómo deberíamos declarar o definir un sprite. Lo que tenemos que hacer es ver un sprite, como si fuese un array (vector, lista, casillero, como le llameis) de imágenes, donde cada elemento es un cuadro. Lo mejor es verlo con una imagen.
Ahora que tenemos más claro la concepción de sprite, tenemos claro que lo más obvio que se nos viene a la cabeza al momento de definir un sprite, necesitaremos asignarle una posición en pantalla. Tambien nos hará falta una variable para contabilizar de cuantos cuadros está compuesto el sprite, pues si añadimos más imágenes de los frames que permite nuestro sprite, estas no se verán.
En relación con esta limitación, necesitaremos alguna variable que nos informe de cuándo se ha alcanzado el límite de cuadros que puede alvergar el sprite. Así mismo, puede interesarnos mostrar cierto frame determinado, así que estaría bien reservar otra variable para seleccionar un frame en concreto. Dicho esto, vamos a declarar ahora los miembros privados de nuestra clase:
class CSprite{ private: int posx, posy; int state; int nframes; int cont;
Todo esto se podría haber puesto en una sola línea, pero así queda más legible. Si la función de cada variable no os queda muy clara, echadle un vistazo al código fuente que os adjunto al final del post, que viene (como siempre) comentado línea a línea.
Con esto terminamos con la parte privada de la clase, ahora vamos con la pública. Como hemos dicho, lo más básico de nuestro sprite es un array que contenga nuestros cuadros, que están representados por nuestra clase CFrame. Por supuesto, necesitaremos lo que se conoce como contructor de la clase que lo que hace es iniciar la clase, por decirlo de algún modo. Declararemos, además, una serie de métodos para trabajar con los frames del sprite, y una función especial que eliminará todo el sprite.
Declararemos los llamados setters y getters que dan o muestran valores del propio sprite. Para terminar, crearemos dos funciones más: una para dibujar el frame seleccionado en pantalla y otra para detectar colisiones entre sprites. Al finalizar el archivo lo guardaremos como csprite.h
public: CFrame *sprite; CSprite(int nf); CSprite(); void finalize(); void addframe(CFrame frame); void selframe(int nf); int frames() {return cont;} void setx(int x) {posx=x;} void sety(int y) {posy=y;} void addx(int c) {posx+=c;} void addy(int c) {posy+=c;} int getx() {return posx;} int gety() {return posy;} int getw() {return sprite[state].img->w;} int geth() {return sprite[state].img->h;} void draw(SDL_Surface *superficie); int colision(CSprite sp); }; #endif
Vale, a priori parece un montón de cosas raras pero si lo miramos con detenimiento va cobrando sentido. El primer miembro no es más que nuestro array de frames. Los dos siguientes son los constructores, que indican que puede construirse una clase CSprite con o sin parámetros. Si enviamos algún número como parámetro, dicho número será el número de frames que tendrá nuestro sprite. Si no se le envía nada, se da por hecho que tendrá un único frame.
La función finalize() eliminará todos los frames del sprite y las funciones addframe() y selframe() añadirán y seleccionarán un cuadro respectivamente. La función frames() devuelve el número de frames del sprite «ocupados». Las 8 siguientes líneas son los setters y getters, y servirán para establecer, modificar y consultar las coordenadas o el tamaño del sprite Las últimas dos funciones se encargan de dibujar el sprite y de detectar si colisiona con otro, respectivamente.
Implementación de una clase
Ya tenemos declaradas nuestras clases, pero eso no nos basta para poder usarlas, antes tenemos que implementarlas. ¿Y qué es implementar? Implementar básicamente consiste en crear las funciones que hemos declarado en nuestro archivo csprite.h. Como notaréis, los getters y los setters ya están implementados en el propio archivo de cabecera, pues son órdenes sencillas; pero hay algunas funciones que son bastante más complejas y que viene bien implementar a parte, así que crearemos un nuevo archivo llamado csprite.cpp y vamos a ello:
#include #include "csprite.h" void CFrame::load(char *path){ img = SDL_LoadBMP(path); SDL_SetColorKey(img, SDL_SRCCOLORKEY|SDL_RLEACCEL, SDL_MapRGB(img -> format, 255, 0, 0)); img =SDL_DisplayFormat(img); } void CFrame::unload(){ SDL_FreeSurface(img); }
Aquí hemos implementado las dos funciones de nuestra clase CFrame. La función load() carga un bmp con el rojo como color transparente y la función unload() libera la superficie donde está la imagen. Ahora vamos con la clase CSprite:
CSprite::CSprite(int nc){ sprite = new CFrame[nc]; nframes = nc; cont = 0; } CSprite::CSprite(){ int nc = 1; sprite = new CFrame[nc]; nframes = nc; cont = 0; }
Empecemos por los constructores. El primero se activará en caso de pasarle un número entero como parámetro y, como vemos, creará el array de frames con la longitud elegida. Posteriormente fijará el número de cuadros a la cantidad elegida y pondrá el contador de cuadros ocupados a 0. Lo mismo aplica en el segundo caso, con la diferencia de que el número de cuadros es 1 y no es especificado como un parámetro.
void CSprite::finalize(){ int i; for(i = 0; i <= nframes-1; i++){ sprite[i].unload(); } } void CSprite::addframe(CFrame frame){ if(cont < nframes){ sprite[cont] = frame; cont++; } } void CSprite::selframe(int nc){ if(nc <= cont){ state = nc; } }
La función finalize() usa un bucle for para eliminar la imagen contenida en todos los cuadros de nuestro sprite. La función addframe() que recibe un frame por parámetro añado dicho cuadro a nuestro array siempre y cuando no sobrepasemos el número máximo de cuadros. Por último, la función selframe() marca como activo el cuadro cuyo número en el array coincide con el parámetro pasado.
void CSprite::draw(SDL_Surface *superficie){ SDL_Rect dest; dest.x = posx; dest.y = posy; SDL_BlitSurface(sprite[state].img, NULL, superficie, &dest); } int CSprite::colision(CSprite sp){ int w1, h1, w2, h2, x1, x2, y1, y2; w1 = getw(); h1 = geth(); x1 = getx(); y1 = gety(); w2 = sp.getw(); h2 = sp.geth(); x2 = sp.getx(); y2 = sp.gety(); if(((x1 + w1) > x2) && ((y1 + h1) > y2) && ((x2 + w2) > x1) && ((y2 + h2) > y1)){ return TRUE; } else { return FALSE; } }
Un poco más enrevesadas son las últimas dos funciones. La función draw() recibe la superficie de destino como parámetro y dibujando posteriormente el cuadro activo o seleccionado sobre dicha superficie. La función colision() funciona en 3 pasos: Primero obtiene el tamaño y la posición del sprite que ejecuta la función. A continuación hace lo mismo con el sprite que le es pasado como parámetro. Por último, realiza una serie de comprobaciónes para verificar si los sprites se solapan. En caso de solaparse, se detecta la colisión y se devuelve el valor TRUE; en caso contrario, se devuelve FALSE.
Y con esto por fin terminamos nuestra pequeña biblioteca, ahora vamos a compilarla. Asegurándonos de tener ambos archivos (csprite.h y csprite.cpp) en la misma ubicación, abrimos un terminal y nos dirigimos hasta ahí. En mi caso sería
cd data
Anteriormente utilizamos el famoso compilador GCC del cual ya hemos hablado antes en Muy Linux, pero esta vez usaremos su versión para C++ pues GCC no puede compilar código en C++. También un punto importante, es ver que esta vez solo compilaremos nuestros archivos, obteniendo un fichero de código objeto (.o) el cual no se enlaza con ningún otro ni tampoco es transformado en un ejecutable. De esta manera, podremos linkarlo (usar las clases y funciones que acabamos de implementar) cuando queramos. Así pues volvemos a la consola y tecleamos
g++ -c csprite.cpp
Lo cual debería dejarnos un bonito fichero con extensión .o en nuestro directorio. Ya tenemos compilada con éxito nuestra primera biblioteca. Y hasta aquí la segunda parte del tutorial, estad atentos porque pronto pasaremos a la tercera y última parte: Nos apoyaremos en esta biblioteca para mostrar más de un sprite en pantalla. No olvideis bajaros el código fuente usado hoy desde este enlace. Saludos y seguid practicando.
Contenido del curso
Programación de videojuegos con SDL – Parte I: Introducción
Programación de videojuegos con SDL – Parte II: bibliotecas C++
Programación de videojuegos con SDL – Parte III: a jugar
Artículo de Garolard