Tutorial SDL - Segunda parte

(La tercera parte de este tutorial esta aquí)

Indice



Introducción

Buenas, esta es la segunda parte del tutorial de SDL, vamos a seguir de donde dejamos la vez pasada, así que si no leíste, aquí te dejo el link. Básicamente la vez pasada hablamos sobre como descargar e instalar SDL en Win y GNU/Linux y como crear una pequeña y muy básica aplicación.

En esta parte vamos a ver cómo poner una imagen en pantalla; sí, la idea es muy sencilla, primero necesitamos una imagen en un formato conocido, llámense BMP, JPG, PNG, etc y después lo que hacemos es cargarlo con SDL y decirle que parte de la imagen cargada queremos mostrar en que parte de la pantalla.

Parece fácil, ¿no? bueno... en realidad lo es. ;-)

SDL y 3rd Party Libraries

Antes de meter mano en el código vamos a aclarar algunas cosas sobre SDL.

SDL nativo solamente tiene funciones para cargar archivos de tipo BMP, entonces ¿cómo es que puedo cargar cualquier tipo de formato de imagen?

Afortunadamente, seres humanos con la misma incógnita que la nuestra, escribieron una librería llamada SDL_Image que nos permite hacer eso.

¿Qué es la SDL_Image?

La SDL_Image es lo que se llama una 3rd Party Library, esta librería se integra muy bien con SDL (por eso el prefijo SDL) y maneja los mismos tipos de datos de tal manera que parece natural el uso de la SDL_Image dentro de SDL.

SDL es una librería de bajo nivel con mucha flexibilidad que nos permite hacer muchas cosas y no usarla solamente para juegos, pero este bajo nivel hace que sea necesario crear librerías que nos permita hacer ciertas cosas comunes mas fácilmente. Por ejemplo, SDL tiene manejo de audio nativo, pero es muy complicado hacerlo funcionar, por eso crearon la SDL_Mixer, que es una librería que funciona como agregado al sistema de audio nativo y nos permite, con una API sencilla, realizar las funciones mas comunes de un juego, por ejemplo cargar un archivo que contiene la música del nivel y reproducirla infinitamente.

Para cerrar el tema de las librerías, y pasar a los que nos interesa, voy a listar las librerías mas utilizadas y mas necesarias para realizar un juego:

SDL_Mixer: Como dije antes sirve para agregar un sistema de audio, nos permite la carga de archivos tipo WAV y OGG y la reproducción de estos, también nos permite hacer algunos efectos de panning.

SDL_Net: Esta es indispensable si estamos pensando en MMO, la SDL_Net nos permite la comunicación vía red de manera TCP y UDP.

SDL_TTF: La True Type Font nos da la posibilidad de mostrar texto en la pantalla usando un archivo ttf para renderizar las fuentes.

Como último comentario les quiero decir que estas librerías son 100% multiplataforma, y si miran atentamente se habrán dado cuenta que uno de los creadores de estas librerías es el propio autor de SDL, lo cual es (de cierta forma) una garantía de calidad.

Les recomiendo que de vez en cuando visiten la página de librerías por si hay alguna librería nueva que les pueda ser útil; siempre miren si están portadas a los sistemas que quieren que su juego corra.

A Instalar

En la página de SDL_Image y al igual que en la mayoría de estas librerías van a tener la documentación para descargar, la cual es muy recomendable; el código fuente, es la elección si quieren compilarlo; y los binarios, recuerden que si quieren bajarlos para programar siempre busquen los que dicen *-devel-*.

Windows

Al abrir el archivo SDL_image-devel-1.2.6-VC8.zip van a ver 2 directorios que le deberían importar, uno es el include y el otro es el lib. Lo que les recomiendo es que copien el contenido del directorio include dentro del directorio include de la instalación de SDL y con el archivo .lib que esta dentro del directorio lib, de esta manera no van a tener que agregar directorios en los seteos del proyecto. No se preocupen que no hay archivos repetidos.
Con todas las DLLs que están en ese directorio ya les digo mas adelante que pueden hacer con esas. ;-)

DLLs

Aquí vamos a hacer un alto y explicar lo de las DLL; dentro del directorio libs verán unas cuantos archivos DLL.
¿Qué hacen acá? se preguntaran, lo que hacen ahí es lo siguiente: La SDL_image usa librerías que no son propias de la SDL_image; por ejemplo la librería zlib, (zlib1.dll) que es una librería para comprimir archivos (algo así como el zip), y otras librerías para manejar los distintos formatos de archivos de imágenes (png, jpeg, etc); pero como la SDL_image necesita usar estas estas librerías las incluye en forma de DLL.
Cuando corran un programa hecho con la SDL_image, si Win no encuentra estas librerías se va a quejar. Lo que necesitan hacer es o copiar las DLL en el mismo lugar donde esta el ejecutable que compilaron, o copiarlas en algún lugar estándar, por ejemplo C:\windows\system32, o lo que pueden hacer es incluir en su variable de entorno %PATH% la ruta en donde van a estar.
Lo recomendable es tenerlas en el mismo lugar de su ejecutable, de esta manera estas DLL no van a tener conflictos con otras DLL que puedan tener el mismo nombre y aparte es una forma de organizarse para el día que distribuyan su juego, ese día ustedes ya saben que todo lo que necesita otra PC con Win para correr su juego son esas DLL que están ahí (eso no es del todo 100% verdad pero después le contaré mas).

Linux

Siempre es recomendable que la instalación de los paquetes las maneje su propia distribución, pero ustedes eligen si quieren bajarlo y compilarlo.
Para instalarlo no tienen mas que bajar el source (yo prefiero en tar.gz) y después hacer los mismos pasos que hicimos para instalar SDL.
$su -
# cd /usr/local/
# tar -zxvf /SDL_Image-1.2.6.tar.gz
# cd SDL_image-1.2.6
# ./configure
# make && make install
# exit

Setear el entorno

windows

Si me hicieron caso con la instalación (eso espero) todo lo que tienen que agregar en el proyecto es la SDL_image.lib en las librerías adicionales.


Linux

Aquí no hay mucho que preparar, solo hay que verificar que se haya instalado el archivo de cabecera SDL_image.h y esté la librería libSDL_image.so. En general van a estar en /usr/include/SDL y /usr/lib respectivamente.
Esta comprobación es realmente innecesaria si el make install no fallo (o si lo instalaron de otra manera estándar por su distribución)..... pero por las dudas.

Manos a la obra!

Me imagino que estarán impacientes y quieren empezar a codificar, así que abran su editor preferido (seguro que es el vim) y vamos a hacer lo que dijimos: vamos a cargar una imagen y mostrarla en la pantalla.
Siempre que agreguen una nueva librería de SDL, sigan los pasos descriptos anteriores, ya que siempre tienen el mismo tipo de información.
Aquí les dejo el link para un zip el cual están todos los ejemplos y las imagenes que vamos a usar; así que descarguenlo y habran el archivo imagen.cpp, el cual es el primer ejemplo.

Windows

Solo presionen F5.

Linux

$ g++ -o happy imagen.cpp `sdl-config --libs --cflags` -lSDL_image
$ ./happy

Vamos a explicar las lineas nuevas de código:

SDL_Surface *screen, *imagen;
SDL maneja todo lo que necesita para dibujar en esta estructura llamada SDL_Surface; una vez que nosotros tenemos una estructura de este tipo podemos elegir una porción de esta superficie y 'pegarla' en otra superficie.

SDL_Rect ori, dest;
Esta estructura define un rectángulo.

screen = SDL_SetVideoMode(640, 480, 16, 0);
De esta manera al setear el modo de video, tenemos una estructura que nos define la pantalla; ahora cuando hagamos un cambio sobre la SDL_Surface screen lo vamos a estar haciendo sobre la pantalla.

imagen = IMG_Load("imagen.jpg");
De esta manera usamos la SDL_Image, fijense que en vez de tener el prefijo 'SDL' tiene el prefijo 'IMG'; esto siempre es así para las 3rd Party Libraries, cada una tiene un prefijo distinto.
Bueno noten que sencillo cargar una imagen de un tipo cualquiera desde un archivo y tener una SDL_Surface, a partir de este momento podemos hacer cualquier cosa que querramos con la imagen.
ori.x = 0;
ori.y = 0;
ori.w = imagen->w;
ori.h = imagen->h;

El siguiente paso es decidir que porción de la imagen quiero 'pegar' en la pantalla; para esto defino un rectángulo.

El eje de coordenadas de la pantalla manejado por SDL es el siguiente:
el (0,0) comienza desde la esquina superior izquierda de la pantalla, todo lo que vaya hacia la derecha, esta aumentando en el eje X y todo lo que vaya hacia abajo de la pantalla esta aumentando en el eje Y.

Entonces lo que estamos definiendo es un rectángulo con el x e y apuntando en el origen (arriba izquierda) y le decimos que de ancho (eje x) queremos todo el ancho de la imagen (imagen->w) y de alto queremos todo el alto que tiene la imagen (imagen->h).
La w y la h vienen del ingles w de ancho (width) y h de alto (height).
dest.x = 0;
dest.y = 0;

Para el destino es distinto, aquí solamente nos interesa desde donde vamos a empezar a 'pegar' la porción seleccionada; en este caso desde el (0,0).

SDL_BlitSurface(imagen, &ori, screen, &dest);
Ahora lo que hacemos es 'pegar' (Blit) desde la imagen, la porción seleccionada por ori sobre la superficie screen en el destino indicado por dest.

Si les ayuda (a mi si), pueden pensarlo como si estuviera haciendo un collage; ustedes tienen una cartulina grandota la cual sería la pantalla: la variable screen de tipo SDL_Surface; y también tienen un montón de imagenes: cualquier variable de tipo SDL_Surface que hayan creado desde un archivo con la llamada a IMG_Load().
En vez de tener tijera, lo que tienen son rectángulos con los cuales van a definir que parte de la imagen quieren y en que parte de la cartulina (pantalla) la quieren.
Y en vez de la plasticola tienen el SDL_BlitSurface().

SDL_Flip(screen);
Una vez que su collage esta listo para ser presentado solo tienen que indicar que realmente quieren mostrarlo, esto es lo que hace el SDL_Flip().

SDL_FreeSurface(imagen);
Cuando terminamos de usar un SDL_Surface hay que liberar los recursos, para eso llamamos a esta función.
Cuidado: Nunca liberen el SDL_Surface que es devuelto por la llamada a SDL_SetVideoMode, esa es manejada internamente por SDL y liberada tambien por la llamada al SDL_Quit().

Color Key

Esta bien. Eso está muy lindo para imagenes rectangulares, pero yo vi que en todos los juegos en 2D hay figuras que no son rectangulares, hay personas bien detalladas como por ejemplo el Street Fighter....
Lo que se hace en estos casos es utilizar un color al cual llamaremos 'color key' para el fondo, el cual será un color que no se utilice en la imagen que queremos mostrar; y el cual se eliminará cuando hagamos el Blit.

Rojo, Verde y Azul

Como ustedes sabrán (si no saben yo les explico), los pixels en pantalla se componen de 3 componentes (hay un cuarto pero por ahora no le demos bola); estos componentes son el Rojo (Red), el Verde (Green) y el Azul (Blue), para poder definir el color que un pixel va a tener se tiene en cuenta la cantidad que se usa de estos componentes para armarlo; por ejemplo si llenamos un pixel con todo Rojo y nada de los otros, obviamente tendremos un pixel rojo puro en pantalla, lo mismo pasa con los demás componentes.
SDL maneja la cantidad de color de pixels desde un mínimo de 0 hasta un máximo de 255, esto quiere decir que si yo quiero formar un pixel blanco voy a tener que combinar todos los componentes al máximo (255,255,255) y si quiero el negro (ausencia de color) voy a tener que usar (0,0,0) para cada componente.

Lo que ustedes tienen que hacer es ponerse de acuerdo con su artista gráfico y proponer un color que no sea usado en la imagen como fondo por ejemplo un verde puro (0,255,0) o un magenta (255,0,255) o simplemente un negro puro, como el que uso yo para el ejemplo.
Una vez que ustedes saben el color el cual NO hay que mostrar en la pantalla, hay que indicárselo a SDL de una manera muy sencilla.

Para demostrar esto vamos a ver otro ejemplo; solo explicaré lo que cambio del ejemplo anterior, así que asegúrense que lo entienden bien antes de leer este.
El código fuente es color_key.cpp.

SDL_Surface *circulo, *circulo_key;
Una va a contener el circulo sin sacarle el color key y el otro si se lo va a quitar.

SDL_SetColorKey (circulo_key, SDL_SRCCOLORKEY, SDL_MapRGB(circulo_key->format, 0, 0, 0));
Esta es la función que se lleva todos los premios (por ahora); los parámetros son:
circulo_key : es el SDL_Surface al cual le queremos aplicar el "Color Key".
SDL_SRCCOLORKEY: el 2do parámetro son unos cuantos flags, por ahora acuerdense que tienen que poner este.
El 3er parámetro es un SDL_Color, para obtener esta estructura tenemos que hacer una llamada a la función SDL_MapRGB.
Esta función toma los siguientes parámetros:
circulo_key->format: Es el tipo de formato que utiliza la imagen que cargamos para mostrar los pixels en pantalla.
lo que sigue son los componentes Rojo, Verde y Azul tal como lo estuvimos discutiendo.

Fijense que después le cambio el valor de la estructuras de origen y destino para mostrar las imagenes donde quiero que se muestren; siempre que termino de setear el x, y, ancho y alto como lo deseo hago un SDL_BlitSurface para 'pegar' la imagen origen en destino.
Una vez que termine de formar la imagen como quiero tengo que hacer una llamada a SDL_Flip().
Noten que solamente se llama una vez al SDL_Flip(), ya que solo la queremos para mostrar la imagen una vez que este lista y no cada vez que le hacemos un pequeño cambio intermedio.

Transparencias

¿Se acuerdan que les comente de un cuarto componente? bueno les presento al componente Alpha, con lo cual en vez de tener RGB ahora tenemos RGBA.

¿Qué es el componente Alpha?
El alpha se utiliza para saber cuanta transparencia tiene un pixel, un valor de 0 indica transparencia pura y un valor de 255 indica opaquez pura (para SDL); jugando con lo valores intermedios se va haciendo mas o menos transparente ese pixel.

Existe una formula para calcular la transparencia de un pixel, la cual no voy a poner acá porque la encuentran en todos lados y no tiene sentido saberla ya que SDL se encarga de hacerlo por nosotros.
Imaginense que tienen un fondo y delante del fondo están dibujando un vidrio (medio celeste) el cual quieren que tenga transparencia. Básicamente lo que hace SDL al pixel de la imagen del vidrio es combinarlo con el pixel que esta detrás, de esa manera, dependiendo del componente alpha que tenga, se combinan los 2 pixels para formar un nuevo pixel el cual es el resultado del efecto de transparencia.
Esta operación es muy costosa ya que necesita de las 2 operaciones mas lentas de la CPU (que son la multiplicación y la división).

Transparencia por pixel y transparencia por imagen

Existen 2 maneras de trabajar con componentes Alpha, una es seteando por pixel un componente alpha adecuado y la otra manera es diciéndole que toda la imagen tenga un componente alpha, igual por cada pixel.

Por Pixel

Cuando decimos que hay que setear un componente Alpha por pixel, no quiero decir que lo vamos a hacer nosotros programaticamente (aunque podríamos) sino que nuestro artista gráfico va a guardar la imagen que esta trabajando en un formato que soporte Alpha, por ejemplo PNG.
El archivo ese contendrá información sobre los cuatro componentes y nosotros solamente tendremos que indicarle a SDL que queremos hacer uso de esa información.

Por Imagen

Aquí si vamos a hacer esto programaticamente, lo que queremos hacer es tener una imagen y cambiarle esa información de forma equivalente en todos los pixels, digamos que queremos que toda la superficie de la imagen sea un 50% transparente de manera uniforme.

Entonces vamos con otro ejemplo de estas 2 cosas que recién aprendimos:

Una imagen tiene mas transparencia en los bordes que en el centro de la esfera, la otra imagen es la misma que estábamos usando antes; solamente que le aplicamos un 50% de transparencia.
Además de eso, este ejemplo de yapa tiene algunas nociones de animación.
El código esta en el archivo alpha.cpp.
Ahora viene la explicación de esas lineas de mas.
El código continua a partir del anterior ejemplo:

int lente_x, lente_y, circulo_x, circulo_y;
Para tener control sobre donde están nuestras imagenes

SDL_SetAlpha(lente, SDL_SRCALPHA, 0);
La lente es la imagen que tiene alpha distinto en sus pixeles, para indicarle a SDL que tenga en cuenta la información alpha usamos esta función;
el 1er parámetro es el SDL_Surface, el 2do unos flags, por ahora debemos saber que siempre es el mismo.
y por último la cantidad de alpha a aplicarle, en este caso el valor es 0 para indicarle que utilice la imagen tal cual es, sin ninguna modificación.

SDL_SetAlpha(circulo_key, SDL_SRCALPHA, 128);
El circulo no tiene alpha seteado en sus pixels, entonces le decimos que todos van a tener un 50% (la mitad de 256) de transparencia en toda la imagen. Los parámetros son lo mismos, solo cambia el grado de transparencia: 128.
Un valor de 255 hará que el circulo se vea exactamente igual que en el ejemplo anterior.
Fijense que primero le aplicamos el color key y después le decimos que grado de transparencia queremos, haciendo que los efectos se sumen.
for(int i=0; i<1000; i++) {
ori.w = imagen->w;
ori.h = imagen->h;

dest.x = 0;
dest.y = 0;
SDL_BlitSurface(imagen, &ori, screen, &dest);

ori.w = lente->w;
ori.h = lente->h;

lente_x++;
lente_y++;

dest.x = lente_x % 640;
dest.y = lente_y % 480;

SDL_BlitSurface(lente, &ori, screen, &dest);

circulo_x++;
circulo_y++;
dest.x = circulo_x % 640;
dest.y = circulo_y % 480;

SDL_BlitSurface(circulo_key, &ori, screen, &dest);

//Realmente lo muestra.
SDL_Flip(screen);
}

¿¿Y esto???
No desesperen, no es tan terrible como parece. Solamente estamos haciendo lo que ya sabemos, pero lo estamos repitiendo 1000 veces.
Vamos de a poco; primero tenemos que dibujar el fondo, así que seteo las estructuras correspondientes (ori y dest) para indicar esto que quiero y después bliteo el fondo.
Después vuelvo a setear el ori y el dest, pero esta vez para la lente, y además actualizo los valores de x e y de donde va a blitear (lente_x++); y bliteo la lente.
Repito pero esta vez para el circulo y bliteo.
Y por último (como ya sabíamos) hago un SDL_Flip().

Resumiendo, fijense que siempre repetimos los mismos pasos y es fácil armar funciones que nos manejen los distintos objetos de las pantallas.
Enumerando: primero actualizamos en donde queremos que este la imagen a blitear (circulo_x++), segundo preparamos los 2 SDL_Rect (ori y dest), tercero bliteamos y por último hacemos el SDL_Flip().

Optimización

En el momento de pensar en hacer un juego, uno siempre tiene que tener en mente la optimización; uno como programador siempre tiene que sacar el mayor provecho a los recursos que brinda la PC.
Afortunadamente para nosotros, SDL viene con varias funciones que nos van a ayudar a hacer nuestro código un poco mas rápido y mas hablando de imagenes.
Para demostrar esto que digo, vamos a tomar el último ejemplo que hicimos y vamos a modificarlo muy poco para hacer el Blit mas performante; y después les voy a explicar los pequeños cambios.
El archivo que contiene el código es alpha_per.cpp.

SDL_Surface *temp;
Primero necesitamos tener una superficie intermedia sobre la cual vamos a trabajar.

temp = IMG_Load("imagen.jpg");
imagen = SDL_DisplayFormat(temp);
Entonces en vez de cargar las imagenes directamente, primero la cargamos en la temporal, hacemos los checkeos y después usamos SDL_DisplayFormat() para transformar la superficie cargada en una superficie mas óptima.

SDL_FreeSurface(temp);
Una que terminamos, la liberamos.

SDL_SetAlpha(temp, SDL_SRCALPHA | SDL_RLEACCEL, 0);
lente = SDL_DisplayFormatAlpha(temp);
Aquí utilizamos un flag mas 'SDL_RLEACCEL' lo que le decimos es que use el sistema RLE de aceleración de alpha, el cual va a mejorar el calculo de las transparencias.
Existe una función para cuando queremos optimizar una superficie con Alpha: SDL_DisplayFormatAlpha, es igual a la anterior; solo que recibe una superficie que tiene alpha.

Resumiendo; hay poco a tener en cuenta, solo acuerdense de usar estos flags de mas y usar estas funciones intermedias.

¿Es el SDL_DisplayFormat mágico?
Casi, el funcionamiento es el siguiente.
Ustedes tienen en su aplicación una superficie la cual es la que maneja la pantalla con un montón de información sobre como dibujar los pixels en pantalla, y por otro lado tienen superficies cargadas desde imagenes; rara vez estas 2 superficies son compatibles, entonces en el momento de hacer un Blit, SDL tiene que transformar los pixels de la imagen cargada para que puedan trabajar con los pixels de la pantalla; esto se hace cada vez que hacemos un blit. Entonces lo que podemos hacer nosotros es decirle a SDL que transforme la imagen para que pueda trabajar bien con la pantalla, y que nos devuelva esa superficie; al estar convertida, en el momento del Blit SDL tiene menos trabajo, lo cual incrementa la velocidad.

En el tintero

Bueno, fue bastante largo, pero las imagenes son un tema largo en cualquier API y no quise cortar por la mitad una vez que nos íbamos entusiasmando.
Una cosas mas para tener en cuenta antes de despedirnos.

Alpha en vez de Color Key

Nosotros podemos elegir que nuestra imagen tenga como color de fondo transparencia pura, lo cual en vez de tener que elegir un color key para quitar en el bliteo, solamente tenemos que respetar el canal alpha de la imagen cargada.
Como la transparencia en este caso es de 100% no hay cálculos de mas ya que estos pixels son tratados de la misma manera que si fueran un color key. Simplemente no se blitean.

Ordenar

Una cosa muy importante al escribir código es ser ordenados, el código de ejemplo que tienen ahora esta muy desordenado y hacer eso en un juego real sería una locura.
Lo que hay que tener en cuenta es el código que se repite e ir armando funciones que podamos reutilizar.
Por ejemplo la carga de archivos es un ejemplo típico de una función que vamos a llamar varias veces (quizás mas adelante les muestro código sobre un manejador de recursos).
Otra cosa para tener en cuenta es sobre los 'elementos' que tenemos en nuestro juego. Aquí yo uso 2 círculos, y las variables que manejan las posiciones están sueltas en el main (nada mas asqueroso), la idea en esta situación es armarse un modulo (o clase) que maneje el circulo y ustedes deberían poder llamar a una función (o método) dibujar que se encargue de manejar las variables de posición.

Despedida

Ahora si, después de varias sugerencias y aclaraciones a tener en cuenta es el momento de terminar este articulo.
Como siempre cualquier duda, comentario o crítica que tengan las pueden enviar a nesdavid[arroba]gmail com.
Por si perdieron el link al zip con todos los ejemplos, acá esta de vuelta.
Eso es todo por ahora, así que buenas noches (al menos para mi) y Happy Hacking a todos ustedes.