Conecta con nosotros

Hola, ¿qué estás buscando?

A fondo

Programación de videojuegos con SDL – Parte III: a jugar

Como veis ya hemos llegado hasta aquí, la tercera y última parte de nuestro tutorial para hacernos nuestro minijuego para Linux usando C y SDL. Para los que se pregunten dónde están las otras partes, aquí teneis la primera parte, y aquí tenéis la segunda, aunque también tenéis los enlaces al final de cada una de las partes del curso.

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.

Advertencia, desplázate para continuar leyendo

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.

Espero que os haya gustado este tutorial y ojalá de aquí surja algún crack que nos traiga buenos juegos para nuestro SO favorito ;). Recordad descargaros todos los archivos usados en este tutorial desde aquí y recordad que para cualquier duda, pregunta, etc, ahí están los comentarios.

El que quiera profundizar más en el uso de SDL pero no le entusiasme la web oficial, que contacte conmigo y le pasaré un excelente libro (en perfecto español, bastante largo por cierto) que explica detalladamente todos los subsistemas de SDL. Un saludo a todos y seguid visitándonos.

Actualización: El libro, ya que habéis preguntado algunos, se titula «Programación de videojuegos con
SDL
«, está escrito por Alberto García Serrano y lo mejor de todo es que lo podéis descargar libremente porque está escrito y publicado bajo licencia Creative Commons. Así que si estáis interesados, podéis encontrarlo aquí.

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

37 Comentarios
Advertencia
Advertencia

Te recomendamos

Actualidad

Oracle ha publicado JDK 22, que dependiendo del prisma por el que se mire puede ser entendido como OpenJDK 22 o Java 22. Una...

Actualidad

El uso de la línea de comandos sigue estando muy vigente en Linux, así que vamos a aprovechar la ocasión para presentar a Warp,...

Actualidad

Trece meses después, GCompris 4 ya está entre nosotros como la nueva versión mayor de la suite de software de entretenimiento educativo dirigido a...

Actualidad

Valve ha anunciado la publicación del código fuente del SDK de Steam Audio en GitHub bajo la licencia Apache 2. Este movimiento no es...