Biblioteca libSDL

De Wikihaskell
Saltar a: navegación, buscar
Multimedia y eventos
Procesamiento y generación de elementos multimedia e interacción con diversos subsistemas
Lenguaje Haskell
Biblioteca libSDL
Autores Aarón Bueno Villares
Jose Joaquín González Maline
Lidia Lebrón Amaya

SDL es un API multiplataforma para trabajar con gráficos, sonido, video, teclado, ratón, y en general para el trabajo multimedia, que provee una serie de bibliotecas para cada uno de sus subsistemas (eventos, video, etc).

La implementación oficial está escrita en C, y es compatible para C y C++, y la biblioteca con la que trabajaremos es una implementación concreta para Haskell que respeta (hasta donde sabemos) el API SDL.

A partir de ahora, cuando nombremos SDL, nos referiremos a la implementación del API SDL para Haskell, en su versión 0.5.9 (la última).

Contenido

Utilidades de la biblioteca SDL

Las utilidades que podemos encontrar en la biblioteca SDL se encuentran divididas en diferentes subsistemas. Estos se ven reflejados en el siguiente esquema:

Subsistemas del SDL

Cada uno de estos susbsistemas es, de por sí, una biblioteca de funciones (un API) para interactuar con el hardware. Los subsistemas más usados son el de video, el de audio, y el de eventos (generalmente, ratón y teclado).

Todo el motor SDL está empaquetado en el módulo Graphics.UI.SDL, y cualquier subsistema puede cargarse importando su correspondiente módulo: Graphics.UI.SDL.Audio para audio, Graphics.UI.SDL.Video para video o Graphics.UI.SDL.Events para eventos).


Instalación

Para trabajar con SDL, debemos usar el compilador GHC, en concreto su versión 6.10. Por otro lado, la librería SDL está encapsulada en un paquete Cabal, formato de empaquetamiento oficial para las bibliotecas de Haskell.

Una descripción detalla del uso e instalación tanto del compilador GHC como de la obtención, instalación y uso de paquetes Cabal, así como de referencias adicionales, puede encontrarse aquí (grupo de trabajo 8).

Para poder desarrollar con SDL (inclusive al trabajar en otros entornos distintos a Haskell), es necesario tener instalado, por un lado, la libreria SDL, y por otro, los paquetes de desarrollo para trabajar con dicha libreria. Cada lenguaje de programación debe proveer las suyas, y Haskell tiene las propias.

Para evitar problemas a la hora de instalar la librería en sí, lo más recomendable es instalar la librería como si fueramos a instalarla para C, y de este modo conseguiriamos todas las dependencias necesarias de forma cómoda. Para instalarla de éste modo, podemos seguir las indicaciones mostradas aquí.

Lo más recomendable, siempre y cuando sea posible, es instalar estas librerias mediante un gestor de repositorios. Necesitaremos, para toda nuestra wiki, solamente SDL y SDL_image, junto con todas las dependencias posibles, que vienen indicadas en el link mostrado anteriormente, y que con un gestor de paquetes se instalan automáticamente.

Una vez conseguido esto, solo tenemos que descarganos el tar.gz de nuestra biblioteca, además de la biblioteca auxiliar para el trabajo con imágenes SDL-image. Se descomprimen, y se siguen los pasos mostrados en los READMES includos en ambos tar.gz.

Breve introducción

A modo de introducción, dejaremos este código que nos muestra una ventana del tamaño 640x480 con un rectangulo rojo de tamaño 10x10 en la posición (10,10). Comenzaremos creando un módulo al que, por ejemplo, llamaremos Prueba e importando la biblioteca que vamos a usar, la SDL.

module Prueba where

import Graphics.UI.SDL as SDL

main = do 
   -- Iniciamos los componentes.
   SDL.init [InitEverything]
   -- Damos valores a la ventana que se creará: 
   -- setVideoMode :: Int -> Int -> Int -> [SurfaceFlag] -> IO Surface
   -- El primer parámetro representa la Altura, el segundo la Anchura,
   -- el tercero los bits por pixel y el cuarto las banderas.
   setVideoMode 640 480 16 []
   -- Guardamos en una variable del tipo IO Surface la pantalla en sí.
   screen <- getVideoSurface
   -- Creamos una variable que represente el color rojo, que será el que
   -- utilizaremos para dibujar nuestro rectangulo.
   let format = surfaceGetPixelFormat screen
   red <- mapRGB format 0xFF 0 0
   -- Dibujamos nuestro rectangulo en la pantalla con tamaño 10 x 20 en
   -- en la posición 30 x 40 con color rojo.
   fillRect screen (Just (Rect 10 20 30 40)) red
   -- Actualizamos la pantalla
   SDL.flip screen

Conociendo SDL

Despues de probar este pequeño código, algunas funciones pareceran claro para lo que sirven, para otras se podrán intuir su significado, y para las restantes, quizás las vean claras pero les surgen sus dudas. Vamos a comentar los siguientes puntos conflictos, y a mejorarlos, en vista de profundizar algo más en el contenido de SDL:

  • ¿Para qué se renombra el modulo: "Graphics.UI.SDL as SDL"?
En Haskell, cuando se carga un módulo (sin renombrar, por ejemplo import Graphics.UI.SDL), todos sus tipos y funciones quedan cargados directamente y se pueden usar sin más que escribir su nombre, como por ejemplo hacemos con setVideoMode o con mapRGB. Pero en el prelude ya existen declaradas las funciones init y flip al igual que en SDL, por lo que el compilador (o interprete) muestra un fallo de ambiguedad, por no saber a qué init y a qué flip se está haciendo referencia. En esos casos es necesario indicar en el mismo código fuente a qué función concreta (es decir, a la función de qué módulo) estamos accediendo, y eso se hace indicando el nombre de la ruta del módulo en la llamada, en nuestro caso, Graphics.UI.SDL.init [InitEverything], y Graphics.UI.SDL.flip screen.
Hacer esto es tedioso para cada función que muestre ambigüedad, así que se renombra Graphics.UI.SDL como SDL y nos ahorramos escritura y ganamos simpleza y elegancia de código.
  • Antes se comentó que libSDL está organizado en subsistemas, ¿dónde se refleja eso?
Cada subsistema de SDL es un submódulo del módulo general Graphics.UI.SDL, el subsistema de video se encuentra en Graphics.UI.SDL.Video o el de audio en Graphics.UI.SDL.Audio. Aunque todo subsistema es un módulo, existen otros módulos de SDL adicionales que no son de por sí subsistemas. Por ejemplo, Graphics.UI.SDL.Rect provee el tipo rectángulo de SDL (muy común para muchos módulos), y Graphics.UI.SDL.General todas las funciones generales, como Init (que inicializa SDL), o getError (captura los errores de SDL en ejecución, como el fallo al cargar un sonido o por no encontrar una imagen que debe cargar). Aquí se encuentra la lista oficial con todos los módulos y su contenido.
Vamos a hacer una modificación del código original para cargar directamente los módulos que necesitamos, en vez de cargar a todo SDL al completo (el módulo Graphics.UI.SDL solamente es la unión de todos los submódulos -no contiene funciones propias, solo distintos imports para sus distintos submódulos). Entonces, con import Graphics.UI.SDL estamos cargando a todo SDL, incluido todo aquello que no estamos usando. Vamos a cargar, entonces, únicamente los paquetes que necesitamos:
module Prueba where

--renombrado para usar init
import Graphics.UI.SDL.General as SDL

-- renombrado para usar flip. También usamos setVideoMode, getVideoSurface, mapRGB y fillRect
import Graphics.UI.SDL.Video as SDL

-- Para usar el tipo rect
import Graphics.UI.SDL.Rect

-- Para usar el tipo surfaceGetPixelFormat
import Graphics.UI.SDL.Types

main = do 
   -- Iniciamos los componentes.
   SDL.init [InitEverything]
   -- Damos valores a la ventana que se creará: 
   -- setVideoMode :: Int -> Int -> Int -> [SurfaceFlag] -> IO Surface
   -- El primer parámetro representa la Altura, el segundo la Anchura,
   -- el tercero los bits por pixel y el cuarto las banderas.
   setVideoMode 640 480 16 []
   -- Guardamos en una variable del tipo IO Surface la pantalla en sí.
   screen <- getVideoSurface
   -- Creamos una variable que represente el color rojo, que será el que
   -- utilizaremos para dibujar nuestro rectangulo.
   let format = surfaceGetPixelFormat screen
   red <- mapRGB format 0xFF 0 0
   -- Dibujamos nuestro rectangulo en la pantalla con tamaño 10 x 20 en
   -- en la posición 30 x 40 con color rojo.
   fillRect screen (Just (Rect 10 20 30 40)) red
   -- Actualizamos la pantalla
   SDL.flip screen
Aunque con ésto ya sepamos identificar cada elemento de SDL, es muy tedioso tener que cargar los módulos independientes, ya que, normalmente, se suelen usar el 80% de los módulos. Rect, Time, Events, Audio, Video, General, Keysym y Types van a ser necesitados en prácticamente todos los nuestros programas, así que, sencillamente, cargaremos el paquete completo de una sola vez con
--carga todos los módulos de SDL, ya que la mayoría los necesitamos.
import Graphics.UI.SDL as SDL

--//Resto del código
y así ganamos en salud.
  • Al terminar la ejecución, no se cierra la ventana, y no puedo hacer nada.
Eso es por que, al terminar la ejecución, hemos inicializado SDL pero no lo hemos apagado. Esto se logra con la instrucción quit (de Graphics.UI.SDL.General):
import Graphics.UI.SDL as SDL

--// Resto del código

SDL.flip screen
quit --quit no está en el prelude, así que no hace falta hacer SDL.quit
Con esto se cierra correctamente el motor SDL (cerrando nuestra dichosa ventana), y se finaliza la ejecución.
  • Al usar el código anterior, no se ve nada, solo un leve parpadeo.
El procesador trabaja muy rápido, y desde que hace flip, hasta que cierra SDL, es decir, el tiempo entre dos instrucciones contiguas, es menor que el que el ojo necesita para percatarse de los cambios, por eso se muestra la imagen y luego se cierra antes de que podamos notar nada salvo dicho leve parpadeo. Existe una función que "detiene" la ejecución durante un pequeño intervalo de tiempo, llamado delay, en Graphics.UI.SDL.Time.
Su parámetro es un entero, que se tomará como una cantidad en milisegundos. Si queremos parar la ejecución durante medio segundo, tendremos que escribir delay 500, para 2 segundos, delay 2000, etc. Con el siguiente código podremos ver la imagen durante un segundo:
import Graphics.UI.SDL as SDL

--// Resto del código

SDL.flip screen
delay 1000
quit
Siempre es necesario usar esta función para todas nuestras aplicaciones, sobre todo si hay presente bucles. De no ser así, estariamos obligando a la tarjeta gráfica a imprimir constantemente todas las imágenes cargadas (imaginemos que dentro de un bucle hay una instrucción de flip), a capturar todos los eventos, y a realizar demasiadas instrucciones pesadas en un pequeño intervalo de tiempo que sobrecarga el sistema, lo ralentiza, y además, no sirve para nada porque el usuario va a ignorar casi el 80% de todos esos cálculos. Si por ejemplo se necesitan capturar los eventos de ratón (para tratar los movimiento de ratón que haga el usuario), el usuario no es lo suficientemente veloz como para mover el ratón o ver una secuencia de imágenes a velocidad de ciclo de reloj, y obligar al sistema a recalcular los dispositivos de entrada y salida es un completo desperdicio.
  • ¿Qué es InitEverything?, es decir, Everything significa todo, ¿qué estoy inicializando cuando inicializo 'todo'?
InitEverything es un flag, es decir, una bandera. Estas banderas se usan como 'opciones' de la función a la que estoy llamando. Las banderas usadas por init se pueden consultar aquí. En nuestro ejemplo, que solo dibujamos, solamente nos hace falta el subsistema de video (para usar delay no hace falta inicializar el subsistema times), así que con SDL.init [InitVideo] también puede servirnos. Si necesitaramos inicializar el subsistema de sonido, por ejemplo, haríamos SDL.init[InitVideo,InitAudio], y en general, separando por comas podemos inicializar todos los subsistemas que queramos. Y para finalizar, SDL.init[InitEverything] es equivalente a hacer SDL.init[InitVideo,InitAudio,InitTimer,InitCDROM,etc]
El lector podría pensar que para poder inicializar el subsistema de Audio, hace falta importar el módulo de audio, pero ésto no es cierto. Si no cargamos el módulo de audio, no tenemos la interfaz que nos hace falta para trabajar con él, pero SDL.General es el que provee el núcleo de SDL, y es capaz de inicializar cualquier subsistema (también existe en SDL.General las instrucciones initSubSystem y quitSubSystem para poder inicializar y cerrar subsistemas concretos una vez inicializado el sistema sdl con init). El inicializar el subsistema de audio y no importar su módulo es similar a comprar una tarjeta gráfica pero no instalar sus drivers: lo tienes, funciona, pero no puedes usarlo.

Módulos

Graphics
UI
Graphics.UI.SDL
Graphics.UI.SDL.Audio
Graphics.UI.SDL.CPUInfo
Graphics.UI.SDL.Color
Graphics.UI.SDL.Events
Graphics.UI.SDL.General
Graphics.UI.SDL.Joystick
Graphics.UI.SDL.Keysym
Graphics.UI.SDL.RWOps
Graphics.UI.SDL.Rect
Graphics.UI.SDL.Time
Graphics.UI.SDL.Types
Graphics.UI.SDL.Utilities
Graphics.UI.SDL.Version
Graphics.UI.SDL.Video
Graphics.UI.SDL.WindowManagement

SDL a fondo

Tras este breve acercamiento a SDL, vamos a seguir profundizando paulatinamente en los distintos subsistemas de SDL. No se pretende hacer una descripción exhaustiva de todas las funciones, pero sí dar una visión útil y autocontenida de un conjunto importante de ellas. Intentaremos hacer el texto agradable y didáctico, así, cada bloque de código será escueto para ejemplificar de forma sencilla la utilidad de la función o funciones mostradas

Generalidades

Trabajando con video

El subsistema de video de SDL es el más complejo de la librería, seguido, aunque de lejos, por el subsistema de eventos. La funcionalidad ofrecida es bastante amplia y en muchos casos poco usada.

Nos centraremos en mostrar las partes más interesantes de este subsistema así como las de mayor espectro de uso, empezando con un tratamiento sencillo de imagénes, para luego trabajar con colores e ir incrementando paulatimente la complejidad de los conceptos y funcionalidades involucradas.

Trato de imágenes

En esta sección realizaremos un ejemplo sencillo que nos permitirá cargar una imagen.

import Graphics.UI.SDL as SDL

main = do
 
 --Inicializamos el subsistema de video.
 SDL.init [InitVideo]
 
 --Definimos el formato que le queremos dar a nuestra pantalla.
 setVideoMode 640 40 16 []
 
 --Cargamos la imagen en cuestión. OJO:sólo en formato bmp.
 prueba <- loadBMP "gnu-fdl.bmp" 
 
 screen <- getVideoSurface
 blitSurface prueba Nothing screen Nothing
 
 --Por último hacemos que la imagen se pueda ver.
 SDL.flip screen
 delay 1000
 quit

En la siguiente imagen podemos apreciar el resultado del ejemplo anterior, como vemos se crea la imagen con el formato y las dimensiones que le hemos dado previamente.

Resultado del ejemplo anterior

Conviene destacar :

  • Graphics.UI.SDL.Video, utilizado para funciones empleadas en el ejemplo como loadBMP, blitSurface, getVideoSurface ...
  • loadBMP: Para cargar la imagen desde su ruta.
  • getVideoSurface, Devuelve la superficie de vídeo, produciendo una excepción en caso de error.
  • blitSurface: blitSurface :: Surface -> Maybe Rect -> Surface -> Maybe Rect -> IO Bool. Esta función realiza un "blit" de la imagen de la superficie origen a la superficie de destino, en este caso la pantalla. Como vemos, los rectángulos son instancias Maybe. Sirve para indicar cuando un argumento es, de alguna forma, opcional. Si queremos enviarle, de hecho, el argumento, se usa la variable Just seguido del constructor del tipo, y si no, ponemos sencillamente Nothing.

Blitting

La función blitSurface funciona del siguiente modo. El primer elemento es la superficie de referencia que se debe copiar, y el tercero la superficie sobre la que se va a dibujar. Si no queremos copiar la superficie origen entera, con el segundo argumento podemos indicar que sub-imagen copiaremos. A su vez, el cuarto argumento indica en qué zona de la superficie de destino dibujaremos. Si el rectángulo de destino es más pequeño que el rectángulo origen, se decarta la parte de la sub-imagen de origen que sobre.

Otro programa simple

Aquí se muestra un programa que imprime los números del 1 al 5 según el lenguaje de signos a intervalos de 1 segundo. Ésta es la imagen, que llamamos dedos.bmp con las cinco imágenes concatenadas(la imagen subida a este wiki es png, debido a que el wiki no me permite subir bmp's, recodificar con GIMP o la herramienta que desees la imagen a bmp para que el programa pueda funcionar):

Frames de una animación básica de prueba

Aquí hacemos uso del parámetro Rect de blitSurface, para dos cosas:

  • Elegir el cuadro de la imagen de origen a imprimir, que será cada vez una mano con un número distinto.
  • Imprimir cada imagen en el centro de la pantalla.
module Main where

import Graphics.UI.SDL as SDL

--Precondiciones: Recibe un 'contador', una superficie de origen que contiene
-- una lista secuencial de imagenes, una superficie de destino,
-- una coordenada x de referencia, el ancho y el alto de cada imagen
-- de la secuencia, y el rectángulo de la superficie de destino donde
-- se imprimirá.
--
--Postcondiciones: Imprime, a intervalos de un segundo, la secuencia de imágenes.
imprimirSecuencia :: Int->Surface->Surface->Int->Int->Int->Rect->IO()
imprimirSecuencia 0 _ _ _ _ _ _= return ()
imprimirSecuencia m@(n + 1) img screen x w h r = do
  blitSurface img (Just (Rect x 0 w h)) screen (Just r)
  SDL.flip screen
  delay 1000 --Mostramos la imágen durante un segundo antes de cargar la siguiente.
  imprimirSecuencia n img screen (x + w) w h r

main = do 
   SDL.init [InitVideo]

   screen <- setVideoMode 640 480 16 []

   -- Imagen que contiene, para cada número del 1 al 5, una imágen con una mano
   --  que muestra el número. Cada 'mano' ocupa 194x187.
   dedos <- loadBMP "dedos.bmp"

   imprimirSecuencia 5 dedos screen 0 194 187 (Rect 223 147 194 187)
 
   quit

La posiblidad de declarar un rectángulo concreto tanto en la imágen de origen como de destino da mucha versatilidad al comportamiento de la función. Puede servir para una gran multitud de cosas: imprimir una animación, desplazar una misma imágen por la pantalla, o incluso desplazar una secuencia de imágenes por la pantalla, mover el personaje de un juego o crear diversos efectos gráficos.

Hasta que no veámos la sección de eventos, no podremos mostrar la completa potencia de este trato de imágenes.

Otras funciones necesarias o útiles

Aunque no lo hemos comentado anteriormente, cada vez que se carga una imágen, esta luego hay que destruirla, para liberarla de la memoria, como si fuera un puntero en C.

Esto se logra con la función

lockSurface :: Surface -> IO Bool

Recibe una superficie y devuelve un booleando indicando el éxito o fracaso de la aplicación de la función (encapsulado en una mónada). La superficie principal, es decir, la pantalla, no hace falta liberarla con lockSurface, pues esta es llamada automáticamente al cerrar SDL con la instrucción quit.

En nuestro código anterior, deberiamos haberlo terminado del siguiente modo:

....
....

lockSurface dedos
--lockSurface screen
--No es necesario liberar la pantalla principal, pues esta
--ya se realiza con la instrucción quit.
quit

Otras funciones interesantes son:

surfaceGetWidth :: Surface -> Int
surfaceGetHeight :: Surface -> Int

que devuelven el ancho y el alto, en número de pixeles (resolución) de la imagen pasada como parámetro.

Trabajando con eventos

El subsistema de eventos de SDL es el que te permite detectar las ordenes del usuario, es decir, detectar los movimientos o clicks del ratón, la pulsación de teclas, o las modificaciones realizadas en la ventana de juego.

Existen varias formas de trabajar con los eventos en SDL. Bien se puede paralizar la ejecución de la aplicación hasta que el usuario realice alguna acción (técnica de waiting), o ir leyendo esporádicamente los dispositivos y actuar en el momento en que ocurra algún evento (polling).

Para todo este control de eventos tenemos los siguientes elementos básicos:

  • Tipo event: Es el tipo básico que guarda la información de cada evento particular.
  • Función waitEvent: Detiene la ejecución de la aplicación hasta que se produzca un evento.
  • Función pollEvent: Devuelve el evento más antiguo aún no procesado.

Existe una tercera función, llamada pumpEvents que actualiza manualmente la pila de eventos. Sin la llamada a esta función, no se puede obtener la información de los nuevos eventos, que por lo tanto, se perderán. Pero esta función solamente es útil cuando se desea tener un control mas directo sobre los eventos producidos en los dispositivos hardware (ratón, teclado, etc). Pero implícitamente, esta función ya es llamada cuando se realiza waiting o polling, y su uso no nos va a preocupar en esta introducción.

Primer Ejemplo sencillo

Vamos a hacer una pequeña aplicación, que muestre un uso básico del subsistema de eventos, en donde abriremos una ventana vacía, y el sistema esperará a que pulsemos una tecla para finalizar la ejecución del programa.

module Main where

import Graphics.UI.SDL as SDL 

--Función que se ejecuta continuamente, hasta
-- que una tecla sea pulsada.
esperaTecla = do
  evento <- pollEvent

  case evento of
    KeyDown e -> return ()
    otherwise -> esperaTecla

--Funcion principal
main = do 
  SDL.init [InitVideo]

  screen <- setVideoMode 640 480 16 []

  esperaTecla

  quit

Como hemos indicado, si lo ejecutamos, el sistema mostrará una pantalla negra, y no hará nada. Cuando pulsemos una tecla, finalizará la ejecución del programa. Pasemos a comentar más detenidamente las partes de la función que trata con los eventos.

Polling y tipo evento

Si nos vamos a la primera linea de ejecución de la función esperaTecla, podemos deducir que, efectivamente, hemos usado la técnica de polling, como explicamos anteriormente. Esa linea guarda en la 'variable' evento (aunque estrictamente hablando, Haskell no tiene variables), el siguiente evento en la cola de eventos sin procesar.

La función pollEvent es como sigue:

pollEvent :: IO Event

Es decir, no recibe ningún parámetro y devuelve un evento (el primero en la cola) encapsulada en una mónada (para respetar la transparencia referencial). Con:

evento <- pollEvent

se desencapsula la mónada y se guarda el 'Event' en 'evento'. A su vez, el tipo 'Event', tiene la siguiente estructura (ponemos puntos suspensivos para no mostrar todo el tipo de la unión):

data Event
= NoEvent
| GotFocus [Focus]
| LostFocus [Focus]
| KeyDown !Keysym
| KeyUp !Keysym
| ...

La lista completa de tipos unidos se puede encontrar aquí. Como veremos inmediatamente, el trato que recibe un evento es el mismo que se realiza con cualquier otro tipo unido, mediante patrones. En nuestro caso, cada 'tipo' que forma la unión corresponde a un tipo distinto de evento (click de ratón, pulsación de tecla de teclado, etc).

Tratando los eventos

En el siguiente bloque, hacemos lo que veníamos anunciando: detectamos el tipo de evento concreto producido con el uso de patrones, en este caso, con una estructura case (por comodidad).

Nos interesa la pulsación de una tecla de teclado. Existen dos eventos relacionados con ello: la pulsación de una tecla, y la liberación de una tecla previamente pulsada. Los eventos del tipo Event relacionados con estas dos situaciones son KeyDown y KeyUp (se muestra en la lista anterior).

En concreto, nos quedamos con la pulsación, y es por ello que en el case finalizamos la función con return () (una forma como otra cualquier de finalizar) cuando el patrón del evento cuadra con el tipo KeyDown.

Cuando no existen eventos en cola, el evento que se devolvería en cada llamada a PullEvent cuadrará con el tipo NoEvent (el primer del tipo enumerado). Otros eventos a destacar son:

  • MouseMotion, movimiento de ratón.
  • MouseButtonDown, análogo a KeyDown, pero para el click del ratón.
  • MouseButtonUp, análogo a KeyUp, pero para el click del ratón.

Por último, si el evento no es del tipo deseado (linea otherwise), llamamos de nuevo a la función para procesar el siguiente evento en cola, hasta encontrar el deseado.

Polling vs Waiting

En nuestro ejemplo hemos usado la técnica de polling, que permite procesar todos y cada uno de los eventos. El waiting, sin embargo, no guarda todos los eventos para consultarlos en cualquier momento, sino que procesa el primer evento que suceda desde que la función fue llamada. Si no suceden eventos, el sistema se duerme hasta que suceda. Si suceden nuevos eventos, desde que la función wait finaliza, hasta que vuelve a ser invocada, éstos se perderán, y solo serán accesibles con polling.

La técnica de waiting tiene un trato idéntico a polling, bastaría sustituir, en el código anterior, la linea de la llamada a PullEvent por:

evento <- waitEvent

Así de simple.

Ventajas y desventajas

La principal desventaja del uso de waiting es que no se puede asegurar que se procesen todos los eventos. Quizás parezca un perogrullo, pero aunque la llamada a waitEvent se realice muy frecuentemente, y usando el argumento de que el usuario jamás será tan rápido como para competir con la máquina, hay que tener en cuenta que, por muy lento o torpe que sea el usuario, un click de ratón se realizará en un instante determinado, muy concreto, en un ciclo de reloj preciso, que puede estar perfectamente situado entre dos llamadas a waitEvent (por ejemplo, en el tiempo de procesar el case, aunque sea un case muy corto), lo cual provocaría la perdida de ese importante evento (importante porque se está ignorando un deseo del usuario).

Este problema se soluciona con la técnica de polling, pues los eventos producidos se guardan en una cola independientemente de el tiempo que suceda entre llamadas (evidentemente, a mayor tiempo, mayor retardo en el procesamiento de los eventos). El problema de la técnica de polling es que el usuario leerá la cola constantemente, incluso cuando no existan nuevos eventos (la cola vacía), devolviendo reiteradamente un evento del tipo NoEvent, innecesariamente, haciendo perder tiempo inútil al sistema operativo que debe concentrarse en ejecutar dicho programa. Esto sobrecarga innecesariamente al sistema.

Una forma de solucionar este problema es durmiendo al programa (con SDL_Delay), si el tiempo entre dos llamadas a pollEvent es muy corto (por ejemplo, menor al tiempo entre dos fotogramas), ya que sabemos que un usuario, no producirá muchos eventos en ese intervalo (quizás unos 10 eventos de movimiento continuo), que se pueden procesar en un tiempo suficiente para no producir retardo aparente. De este modo, envíamos el programa a segundo plano y mayor número de veces, agilizando al sistema.

Con waiting, mientras que no haya eventos, el programa se queda en segundo plano y el sistema operativo aprovecha toda su potencia en ejecutar otras tareas, y solo cuando existe una nueva tarea que la función WaitEvent detecte, el sistema operativo pasará el control al programa de nuevo.


Combinando los métodos

Una forma de aprovechar las ventajas de ambos métodos a la vez, sin contemplar las desventajas de ambos, es combinando ambos métodos de la forma adecuada, que de hecho, es la siguiente:

siguienteEvento :: IO Event
siguienteEvento = do
  evento <- pollEvent

  case evento of
    NoEvent -> waitEvent
    otherwise -> return evento

esperaTecla = do
  evento <- siguienteEvento

  case evento of
    KeyDown _ -> return ()
    otherwise -> esperaTecla

En esta adaptacion del código hacemos lo siguiente. Diseñamos una función nueva que se encarga de buscar el siguiente evento del sistema, sea un evento pendiente, o uno que ha de llegar. En la primera linea, obtenemos el evento pendiente en cola (evento es el evento desencapsulado de la mónada). Si este es del tipo NoEvent, significa que la cola está vacia, y en vez de devolver el evento, esperamos a que suceda uno nuevo con waitEvent. En todo este trayecto hasta el siguiente evento, el sistema aprovechará todo su tiempo en realizar otras tareas útiles. Sin embargo, si este evento es útil, se encapsula de nuevo (con return) y se devuelve sin más.

Con esta pequeña y atractiva combinación de métodos, no perdemos eventos ni desaprovechamos el tiempo, que de hecho, es el comportamiento esperado y deseado de cualquier aplicación.

Eventos e imágenes

Ahora vamos a hacer un pequeño código para ver como responder 'visualmente' a las acciones requeridas del usuario. Usaremos de nuevo la imágen de los dedos de números, y creamos una miniaplicación con el siguiente comportamiento:

  • Mostramos la imagen correspondiente al número 1.
  • Si el usuario pulsa una tecla del 1 al 5, se muestra la imágen correspondiente a dicho número.
  • Si el usuario pulsa la tecla arriba o derecha, o hace un click derecho, se muestra la imágen del siguiente número al actual (del 5 pasaríamos al 1).
  • Si el usuario pulsa la tecla abajo o izquierda, o hace click izquierdo, se muestra la imágen del anterior número al actual (del 1 pasaríamos al 5).
  • Si el usuario pulsa cualquier otra tecla, mostramos durante 1 segundo una imagen de error sobre la última imagen visualizada.

Aunque aún no ha sido explicada, vamos a usar la libreria SDL.Image, por cuestiones de comodidad. Es muy sencilla y solo usamos de ella la función load. Para ver más concretamente como debe ser usada, dirigios a la sección de SDL_imagen del apartado de librerias relacionadas.

Y aquí está el código resultante, para el cual necesitamos las imagenes [1] y [2]:

module Main where

import Graphics.UI.SDL as SDL
import Graphics.UI.SDL.Image

-- Recibe la pantalla principal y la superficie a imprimir.
-- Además, considerará que la imágen esta compuesta por varias imágenes
-- del mismo tamaño en una sola fila. Así, el primer parámetro indica
-- el número de imágenes presentes en la 'fila' de imágenes, y el segundo
-- parámetro la imágen de la fila a imprimir.
-- Si el tercer parámetro es false, no imprime nada, si es true, imprime el cuadro
-- indicado por el segundo entero.
muestraImagen :: Surface->Surface->Int->Int->Bool->IO()
muestraImagen _ _ _ _ False = return ()
muestraImagen screen img n act True = do
  w <- return (div (surfaceGetWidth img) n)
  h <- return (surfaceGetHeight img)
                 
  blitSurface img (Just (Rect (w * act) 0 w h)) screen Nothing
  SDL.flip screen

--Muestra una imágen de error durante 1 segundo.
muestraError :: Surface->Surface->IO ()
muestraError screen error = do
  blitSurface error Nothing screen Nothing
  SDL.flip screen
  delay 1000

--Devuelve el siguiente evento del sistema.
siguienteEvento :: IO Event
siguienteEvento = do
  evento <- pollEvent

  --Si no existe ningún evento pendiente, espera.
  case evento of
    NoEvent -> waitEvent
    otherwise -> return evento

--Recibe la pantalla, la superficie de las manos,
--la superficie de error, el número (de dedos) actual
--y un booleano indicando si ha habido cambios.
controlaImagen :: Surface->Surface->Surface->Int->Bool->IO ()
controlaImagen screen manos error n b = do
  --Se manda a imprimir la imagen (dicha función se
  --encarga de actualizar si es necesario, con el último
  --booleano pasado como parámetro).
  muestraImagen screen manos 5 n b

  --recibimos el siguiente evento.
  evento <- siguienteEvento

  case evento of
    -- Si el evento es de pulsación de tecla.
    KeyDown e -> case (symKey e) of
                   SDLK_UP -> controlaImagen screen manos error (mod (n + 1) 5) True
                   SDLK_RIGHT -> controlaImagen screen manos error (mod (n + 1) 5) True
                   SDLK_DOWN -> controlaImagen screen manos error (mod (n - 1) 5) True
                   SDLK_LEFT -> controlaImagen screen manos error (mod (n - 1) 5) True
                   SDLK_1 -> controlaImagen screen manos error 0 True
                   SDLK_2 -> controlaImagen screen manos error 1 True
                   SDLK_3 -> controlaImagen screen manos error 2 True
                   SDLK_4 -> controlaImagen screen manos error 3 True
                   SDLK_5 -> controlaImagen screen manos error 4 True
                   SDLK_ESCAPE -> return ()
                   otherwise -> do
                                 muestraError screen error
                                 controlaImagen screen manos error n True
    --Si el evento es de pulsación del botón del ratón (izquierdo o derecho).
    MouseButtonDown _ _ ButtonLeft -> controlaImagen screen manos error (mod (n + 1) 5) True
    MouseButtonDown _ _ ButtonRight -> controlaImagen screen manos error (mod (n - 1) 5) True
    -- Si ningún evento nos interesa, llamamos de nuevo a la función (y así
    -- conseguimos el siguiente evento.
    otherwise -> controlaImagen screen manos error n False

main = do
  SDL.init [InitVideo]

  screen <- setVideoMode 640 480 16 []
  manos <- load "dedos.png"
  error <- load "error.png"

  --Función principal de la aplicación.
  controlaImagen screen manos error 0 True

  --Liberamos ambas imágenes.
  freeSurface manos
  freeSurface error

  quit

Trabajando con audio

SDL es una biblioteca para el trato multimedia. Y el audio es uno de sus pilares. El subsistema de audio de la libreria SDL tiene un conjunto rico de funciones para trabajar con audio, aunque a muy bajo nivel.

El subsistema básico de Audio, en la biblioteca para Haskell, es aún muy pobre en comparación con su equivalente en C. Para ello disponemos de la biblioteca auxiliar SDL_mixer, en su versión para Haskell, que sí está disponible (para más información, vaya a la sección correspondiente a la biblioteca auxiliar SDL_mixer).

Controlando el tiempo

En cuanto al control del tiempo, SDL nos proporciona algunas funciones para manejar y controlar el tiempo, concretamente para obtener intervalos de tiempos y detenerlo.

Por ejemplo contamos con la función:

getTicks :: IO Word32 

Dicha función nos devuelve el tiempo (en milisegundos) que ha pasado desde que se inició la biblioteca SDL hasta el momento en que la ejecutemos.

Esto es muy útil para controlar el tiempo en los juegos, controlando,por ejemplo, la velocidad del juego.

Otra función que podemos encontrar en este subsistema es:

delay :: Word32 -> IO ()

Con esta función ordenamos a la aplicación que se pause durante el tiempo indicado.

Manejando ventanas

Este subsistema de la biblioteca SDL nos proporciona funciones para interactuar con el gestor de ventanas, además de configurar aspectos de estas.

A continuación se muestran algunas de estas funciones con su significado:

setCaption :: String -> String -> IO ()

Esta función nos permite nombrar una ventana , pasándole como parámetro el título que le queremos poner a la ventana, esto es muy importante ya que nos permitirá localizar cada una de las ventanas con un simple vistazo.

getCaption :: IO (Maybe String, Maybe String)

Hace el proceso contrario, es decir, nos devuelve el título y la ruta del icono.

iconifyWindow :: IO Bool

Con esta función podemos minimizar la aplicación y dejarla en segundo plano.

Otras bibliotecas relacionadas

SDL-image

SDL-image es una biblioteca auxiliar de una gran utilidad para trabajar con imágenes, como hemos mencionado anteriormente con libSDL podemos trabajar sólo con formato bmp aquí podemos ver un ejemplo.

Con esta nueva biblioteca podremos trabajar con diferentes formatos de imagen como por ejemplo: png ,gif,jpg,tga, pnm … gracias a una misma función. Esto es una ventaja ya que el formato bmp consume un espacio considerable en disco.

Esta libreria no añade ninguna funcionalidad especialmente novedosa al motor SDL, solamente aumenta su versatilidad incrementando el número de formatos compatibles. Mientras SDL solo permite trabajar con imagenes BMP, con SDL.Image se pueden cargar imágenes en cualquier formato.

Instalación

Para obtener la libreria, descárgatela aquí. Para instalarla se procede como de constumbre (para usuarios de GNU/Linux):

>runhaskell Setup.lhs configure
>runhaskell Setup.lhs build
>sudo runhaskell Setup.lhs install

Su única dependencia es, obviamente, la libreria SDL, pues SDL.Imagen no es una libreria independiente, sino una extensión de la primera.

Ejemplo de la mano

Aquí vamos a ver lo sencillo que es el uso de esta libreria. Escogeremos el ejemplo que ya mostramos aquí, modificándolo para poder cargar directamente la imagen png que subimos al wiki -siempre es preferible usar archivos png a bmp, pues ocupan menos espacio en disco, permiten un trato más eficiente, y además es un formato de imágen libre-:


module Main where

import Graphics.UI.SDL as SDL

--Importamos esta nueva libreria.
import Graphics.UI.SDL.Image

--Precondiciones: Recibe un 'contador', una superficie de origen que contiene
-- una lista secuencial de imagenes, una superficie de destino,
-- una coordenada x de referencia, el ancho y el alto de cada imagen
-- de la secuencia, y el rectángulo de la superficie de destino donde
-- se imprimirá.
--
--Postcondiciones: Imprime, a intervalos de un segundo, la secuencia de imágenes.
imprimirSecuencia :: Int->Surface->Surface->Int->Int->Int->Rect->IO()
imprimirSecuencia 0 _ _ _ _ _ _= return ()
imprimirSecuencia m@(n + 1) img screen x w h r = do
  blitSurface img (Just (Rect x 0 w h)) screen (Just r)
  SDL.flip screen
  delay 1000 --Mostramos la imágen durante un segundo antes de cargar la siguiente.
  imprimirSecuencia n img screen (x + w) w h r

main = do 
   SDL.init [InitVideo]

   screen <- setVideoMode 640 480 16 []

   -- Imagen que contiene, para cada número del 1 al 5, una imágen con una mano
   --  que muestra el número. Cada 'mano' ocupa 194x187.
   -- Como vemos, solo hemos cambiado loadBMP por load, que es la función de SDL.Imagen
   --  para cargar imágenes de casi cualquier formato.
   dedos <- load "dedos.bmp"

   imprimirSecuencia 5 dedos screen 0 194 187 (Rect 223 147 194 187)
 
   quit

SDL-gfx

SDL-gfx, sirve de complemento para nuestra biblioteca (LibSDL). Esta biblioteca incorpora funciones para dibujar primitivas gráficas y otras funciones de apoyo. Se podría considerar como una evolución de:

  • SDL_gfxPrimitives que proveía funciones para dibujar líneas, círculos o polígonos
  • SDL_rotozoom con funciones para rotar y escalar (rotozoomer) superficies de SDL.

SDL-mixer

SDL-Mixer, esta biblioteca se diseñó para añadir soporte de música a la biblioteca SDL , gracias a ella podemos manejar diversos formatos, entre ellos mp3,ogg,xm. Por otra parte también dipone de un reproductor para sonidos que nos permite introducir además efectos de sonido, el uso de diversos canales de audio independendientes, y también distingue entre música y efectos de sonido, que reciben un trato distinto.

Todas estas características hacen de ella una biblioteca muy recomendable para el desarrollo de videojuegos. Puede descargar la biblioteca SDL-Mixer aquí

SDL-mpeg

SDL-mpeg es una biblioteca libre para la reproducción de vídeo MPEG1 además de otros formatos como MP3. Gracias a ella complementamos las funciones de la biblioteca SDL. Puede descargar la biblioteca aquí.

SDL-ttf

SDL-ttf es una biblioteca que nos permite escribir directamente texto en la pantalla gráfica, (cosa que hasta el momento era imposible con la biblioteca SDL). Gracias a ella podemos dibujar el texto que queramos en una superficie SDL eligiendo el tipo de letra que más nos guste, siempre y cuando el tipo de formato sea compatible con las fuentes true type (ttf). Puede descargar la biblioteca SD-ttf aquí.

Referencias

Herramientas personales