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.