Skip to content

Tutorial creación de juego con SDL

alejandrov8 edited this page Nov 27, 2012 · 1 revision

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.

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.

Antes de continuar, me gustaría aclarar un asunto porque no sería la primera vez que me sucede: No pretendo hacer un juego completo, con gráficos complejos, efectos de sonido, etc; solo pretendo hacer una introducción lo bastante básica y bien explicada como para que cualquier interesado en SDL tenga una buena base para empezar y pueda seguir la documentación de SDL sin muchos problemas.

En esta última parte, y partiendo de lo que hicimos en la primera parte del tutorial, crearemos un par de enemigos y añadiremos la capacidad de disparar a nuestra nave. Para esto nos apoyaremos sobre la biblioteca en C++ que creamos en la segunda parte. Vamos a empezar, así que os convendría tener abiertos tanto el archivo fuente de nuestro primer ejemplo como el de la biblioteca, para poder ir consultando qué hace cada función que vayamos escribiendo.

Creando nuevas variables y estructuras

Antes de hacer nada, en la zona de los defines definiremos una constante para el número máximo de disparos.Ahora añadiremos algunas variables que nos harán falta. Primeramente, necesitamos un nuevo frame para nuestros enemigos, y otro más para los lasers que disparará nuestra nave. En la zona donde declaramos nuestras variables añadiremos las siguientes

CFrame fmalo; CFrame flaser;

Ahora, pasamos a los sprites que nos hacen falta. Ya tenemos el de nuestra nave así que crearemos sendos sprites por cada enemigo que que queramos mostrar y otro más para los lasers. En realidad, no nos haría falta un sprite por enemigo (como luego veremos con los disparos), pero como nos estamos basando en lo que ya tenemos hechos, haciéndolo de este modo nos será más fácil de entender el funcionamiento de SDL.

CSprite malo1(1), malo2(1); CSprite laser(1);

Para acabar con las variables, vamos a definir unas cuantas más. Una nos servirá posteriormente para controlar los fps de nuestro juego. Otra definirá la velocidad base de los enemigos, y las demás nos ayudarán a la hora de cambiar la dirección de los mismos en cuanto “choquen” contra los bordes de la pantalla.

int done = 0, frametime, vel = 5; int w1, w2, dir1 = 1, dir2 = 2;

Con esto terminamos con las variables que nos faltaban. Ahora vamos con las estructuras. En nuestro primer ejemplo definimos la estructura nave para las coordenadas de nuestra nave; la usaremos de nuevo para nuestros enemigos. También crearemos otra para los disparos de la nave. Tened en cuenta que, como seguramente querramos tener varios disparos a la vez en pantalla, usaremos un array de disparos, así que justo debajo de la estructura nave, nos quedaría algo así.

struct nave jugador; struct nave enemigo; struct nave enemigo2;

struct shot{ int x, y; } bala[MAXBALAS+1];

Hemos acabado de añadir las variables y tipos que nos hacían falta, ahora el resto será cuestión de añadir o modificar las funciones que ya teníamos hechas.

Creando nuevas funciones

Primeramente vamos a crear la función que moverá a las naves enemigas. La idea que seguiremos será la siguiente: cada vez que se llame a la función, la coordenada x de cada enemigo se le restará la velocidad multiplicada por el sentido, que siempre valdrá +1 ó -1, controlando de esta forma el sentido de avance de la nave.

Seguidamente comprobaremos si alguno de los enemigos ha chocado contra alguno de los bordes laterales de la pantalla y, en caso de chocar, se cambiará la dirección de avance. Siguiendo estas premisas, nuestra función quedaría más o menos así

void move_enemies(){ w1 = malo1.getw(); w2 = malo2.getw();

enemigo.x -= vel*dir1;
enemigo2.x += vel*dir2*2;

if(enemigo.x < 0){         enemigo.x = 0;         dir1 *= -1;     }else if(enemigo.x + w1 >= WIDTH){
    enemigo.x = (screen->w - w1) - 1;
    dir1 *= -1;
}

if(enemigo2.x < 0){         enemigo2.x = 0;         dir2 *= -1;     } else if(enemigo2.x + w2 >= WIDTH){
    enemigo2.x = (screen->w -w2) -1;
    dir2 *= -1;
}

}

Ya tenemos lista nuestra función para mover a los enemigos, ahora haremos lo mismo pero para cada disparo. Seguiremos el siguiente razonamiento: los disparos cuya coordenada x sea igual a 0 se considerarán inactivos (no disparados) y no se dibujarán. Caso contrario significará que están activos y que se dibujarán en pantalla. Este procedimiento tendrá que repetirse para cada disparo, es decir, haremos esto por cada elemento del array “bala”

void move_shot(){ int i;

for(i=0; i <= MAXBALAS; i++){
    if(bala[i].x != 0){
        bala[i].y -= 5;
    }
    if(bala[i].y < 0){
        bala[i].x = 0;
    }
}

} Seguimos con nuestros disparos, vamos a crear una función que las dibujará. Lo plantearemos así: crearemos una variable auxiliar que valdrá un número negativo. Recorreremos el array de balas buscando alguna que esté disponible (coordenada x = 0) y, si la encontramos, la dibujaremos justo delante de nuestra nave.

void draw_shot(){ int libre = -1;

for(int i = 0; i <= MAXBALAS; i++){         if(bala[i].x == 0){             libre = i;         }     }          if(libre >= 0){
    bala[libre].x = nave.getx() + 32;
    bala[libre].y = nave.gety() - 64;
}

} Con esto terminamos de crear las nuevas funciones, vamos ahora a modificar las que ya teníamos para que funcionen como es debido. Vamos a comenzar por la función draw_scene() que es la encargada de dibujar toda la escena.

Los cambios que haremos serán sencillos, ahora además de dibujar nuestra nave, dibujaremos los enemigos y los disparos que estén activos al momento de ejecutar la función. Lo haremos recorriendo el array de disparos y dibujando aquellos cuya coordenada x sea distinta a 0. Los siguientes cambios van dentro de la función. Para verlos con más detalle podeis descargar el código fuente al final del post.

int i;

malo1.setx(enemigo.x); malo1.sety(enemigo.y); malo1.draw(screen);

malo2.setx(enemigo2.x); malo2.sety(enemigo2.y); malo2.draw(screen);

for(i = 0, i <= MAXBALAS, i++){ if(bala[i].x != 0){ laser.setx(bala[i].x); laser.sety(bala[i].y); laser.draw(); } }

if(malo1.colision(laser) || malo2.colision(laser) == TRUE){ done = 1; } Con esto la función draw_scene() está preparada. Ahora modificaremos las funciones initialize(), finalize() y init_sprite(). La primera de ellas quedará de la siguiente manera

int i;

jugador.x = 400; jugador.y = 400; enemigo.x = 100; enemigo.y = 100; enemigo2.x = 250; enemigo2.y = 100;

for(i = 0; i <= MAXBALAS; i++){ bala[i].x = 0; bala[i].y = 0; } Seguimos sin detenernos mucho con la siguiente función, finalize()

nave.finalize(); malo1.finalize(); laser.finalize();

Atención aquí. Si os fijais, tenemos dos sprites enemigos, pero solo finalizamos uno ¿Por qué? Si teneis abierto el código fuente de nuestra biblioteca, vereis que la función finalize de un sprite lo que hace es liberar la superficie del frame finalizado. Nosotros hemos usado un mismo frame para ambos enemigos, por lo cual, al finalizar uno, el otro también caerá automáticamente. Vamos a acabar con los arreglos modificando la función init_sprites()

Despues de cargar el frame de nuestra nave y añadirlo a nuestro sprite)

fmalo.load("img/navemalo.bmp"); malo1.addframe(fmalo); malo2.addframe(fmalo); flaser.load("img/laser.bmp"); laser.addframe(flaser);

Aquí vemos a lo que nos referíamos. Como solo tenemos un frame para ambos sprites, basta con cargar el frame una única vez y añadirlo a ambos sprites.

Controlando el tiempo

Hemos acabado de modificar y crear las funciones necesarias, ahora solo queda usarlas dentro de nuestra función principal (main()) y un asunto muy importante de cara al desarrollo de videojuegos: controlar la tasa de frames o cuadros por segundo mostrados.

Esto lo haremos calculando cuando tiempo nos lleva mostrar un frame y, si este tiempo es menor a 30 milisegundos mandaremos el juego a dormir hasta el siguiente frame. Con esta diferencia de tiempo obtendremos una tasa de (más o menos) 33 fps, de esta manera nuestro juego irá fluido tanto en nuestro netbook como en un sobremesa de cuádruple núcleo. Esto lo haremos dentro del bucle principal del juego, y quedaría de esta manera.

(justo despues del primer while) frametime = SDL_GetTicks();

...

(justo despues de la cola de eventos, despues de cerrar el segundo while) frametime = SDL_GetTicks() - frametime; if(frametime < 30){ SDL_Delay(Uint32(30-frametime)); }

Esto funciona así: SDL_GetTicks() nos devuelve el tiempo actual, así que si tomamos el tiempo cuando comenzamos a mostrar un cuadro y se lo restamos al tiempo final despues de mostrar dicho cuadro, obtenemos el tiempo empleado en dicho cuadro. Luego le decimos al programa que, si el tiempo empleado en ese cuadro no supera los 30 milisegundos, espere el tiempo restante hasta el siguiente cuadro.

Dicho esto y realizando un par de ajustes en la función main() que podeis ver en el código fuente, ya tenemos nuestro “minijuego” listo para ser compilado. Nos aseguramos de tener los archivos csprite.h, csprite.o y el que acabamos de escribir (naves.c en mi caso) en la misma ubicación. Abrimos una terminal, nos dirigimos a dicha ubicación, y compilamos con el siguiente comando:

g++ -o naves naves.c csprite.o -lSDL

Usamos G++ porque recordad que nuestra biblioteca contiene clases de C++ que no pueden ser compiladas con GCC. Con este comando indicamos a G++ que compile el archivo naves.c linkeandolo con nuestra biblioteca y con SDL, dando como resultado un archivo ejecutable llamado naves. Lo ejecutamos con el comando

./naves

Veremos a nuestras naves en acción, pudiendo disparar o incluso chocar contra las naves enemigas. Lo sé, no es un juego digamos muy completo, pero creo que con lo que hemos aprendido aquí cualquiera que se interese puede comenzar su pequeño proyecto sin perderse entre la documentación y ejemplos que hay por internet.