martes, 27 de diciembre de 2011

Un poco de colisiones

En la anterior entrada vimos un poquito de las matemáticas que usaremos en nuestros juegos. Hoy vamos a aplicar un poco de esa teoría y echar un vistazo muy básico a las colisiones, algo que se usa en casi todos los juegos (por no decir en todos). Concretamente vamos a ver 4 casos: punto contra círculo, círculo contra círculo, punto contra caja y caja contra caja. El código que se muestra es orientativo, no significa que lo vayamos a usar en los juegos sino que está a modo ayuda de la explicación.

Punto contra círculo
El caso del punto contra círculo puede sernos útil por ejemplo en un juego de vista cenital (visto desde arriba hacia abajo) donde el punto podría ser el protagonista y el círculo sea el área donde se activa una mina. Si el jugador entra dentro del área podríamos hacer explotar la mina y así dañarlo.


Veamos la siguiente imagen:

Tenemos el punto P1, que está fuera del círculo C y el punto P2 que sí esta dentro del círculo. Los puntos P1 y P2 los representamos como vectores, como vimos en la anterior entrada. El punto C representa el centro del círculo y también es un vector, mientras que r es el radio de este circulo y es un número escalar (un número "normal" vamos). Los círculos los definimos mediante estos dos valores (la posición del centro y el radio del círculo).

Bien, ¿Cómo detectamos que un punto está dentro de un círculo? Tenemos la posición del punto y también la posición del círculo así como su radio. Podemos calcular la distancia entre el centro del círculo y el punto, y si es menor o igual a r es que está dentro. ¿Cómo se hace lo de la distancia? Muy fácil, usando el módulo de un vector: calculamos el vector que va desde el punto C al P1, haciendo la resta P1 - C, y calculamos el módulo de este nuevo vector. Luego lo comparamos con r y si es igual o menor estamos dentro, sino no. En C++ quedaría por ejemplo algo así:

bool pointInsideCircle(const Circle& circle, const Vec2& point)
{
 return (circle.center() - point).length() <= circle.radius();
}

Hacemos la resta entre el centro del círculo y el punto, calculamos su módulo (el método "length()") y lo comparamos con el radio del círculo. Si el módulo es menor o igual el punto está dentro.

Con esto hay una pequeña optimización que puede ser útil en casos donde se haga esta comprobación muchísimas veces y queremos que vaya más rápido: en vez de usar la distancia en sí podemos usar el cuadrado de la distancia de forma que nos ahorramos la relativamente costosa operación de la raíz cuadrada. Cada círculo guarda además de su centro y radio el cuadrado de este último. Luego en "pointInsideCircle()" hacemos lo siguiente:

bool pointInsideCircle(const Circle& circle, const Vec2& point)
{
 return (circle.center() - point).lengthSq() <= circle.radiusSq();
}

Exactamente igual que la otra función, sólo que ahora el método del módulo es "lengthSq()" ("Sq" de "square", cuadrado) y en vez de usar "radius()" usamos "radiusSq()". Este último método devuelve el cuadrado del radio del círculo, que podemos tenerlo precalculado cuando creamos el círculo o cambiamos su radio.

El método "lengthSq()" es como "length()" pero omite el paso de calcular la raíz cuadrada. Si tenemos los valores A y B, donde A > B el cuadrado del primero también es mayor que el segundo: A * A > B * B. Para saber si el punto está dentro del círculo no nos hace falta saber la distancia en sí, sólo con saber si es menor que el radio ya es suficiente, por lo que en este caso el "truco" de usar los cuadrados nos sirve perfectamente.

Círculo contra círculo
Comprobar la colisión entre 2 círculos puede sernos útil por ejemplo para saber si el enorme proyectil circular que nos ha disparado un jefe ha colisionado o no con el escudo de nuestra nave. Consideremos la siguiente imagen:

En este caso tenemos 3 círculos: C1, C2 y C3. C2 no colisiona con C1, mientras que C3 sí lo hace. Comprobar este tipo de colisiones es como el caso del punto contra círculo pero con un pequeño cambio: teniendo 2 círculos, calculamos el vector entre los centros de ambos y calculamos el módulo de este nuevo vector. Este módulo lo comparamos con la suma de los radios de ambos círculos, si es menor existe colisión. En C++ tendríamos algo así:
bool circlesIntersect(const Circle& c1, const Circle& c2)
{
 return (c1.center() - c2.center()).length() <= c1.radius() + c2.radius();
}

Obviamente esto también admite el "truco" de los cuadrados, simplemente obtenemos la distancia cuadrada del vector y lo comparamos con la suma de los cuadrados de los radios de cada círculo.

Punto contra caja
Este caso tiene la misma utilidad que el de punto contra círculo, sólo que en algunos casos nos servirá mejor usar una caja que un círculo.

Hay  que tener en cuenta que hay 2 tipos de cajas en líneas generales: los alineados con los ejes (en inglés llamadas "Axis Aligned Bounding Box" o "AABB" usando sus iniciales) y los que son "libres" u orientados ("Oriented Bounding Box" o "OBB" en inglés), que pueden tener cualquier orientación. Obviamente los primeros son un caso especial del segundo, ya que serían OBB pero con una orientación fija de forma que los lados de la caja fuesen paralelos al eje X o Y. Sólo vamos a ver los AABB en esta entrada.

Como en los casos anteriores partimos de una imagen:
Una caja AABB la podemos definir con su origen (punto O en la imagen), su anchura y altura. Debido a que usamos este tipo de cajas podemos calcular si el punto está o no dentro de la caja con simples comparaciones. Para saber si un punto está dentro comprobamos si su coordenada X está entre los dos lados verticales de la caja y si su coordenada Y está entre los lados horizontales. Esto en C++ lo podríamos implementar así:
bool pointInsideRect(const Rect& rect, const Vec2& point)
{
 return rect.pos().x() <= point.x() &&
  point.x() <= rect.pos().x() + rect.width() &&
  rect.pos().y() <= point.y() &&
  point.y() <= rect.pos().y() + rect.height();
}


Caja contra caja
Llegamos al último caso, la colisión entre dos cajas. Primero la imagen
Tenemos 4 cajas en la imagen: B1, B2, B3 y B4. La última no colisiona con la caja B1, mientras que B2 y B3 si lo hacen. ¿Cómo se determina la colisión? La primera idea que podríamos tener sería usar el caso que acabamos de ver (el de punto contra caja) y comprobar los 4 vértices de cada caja en la otra. Es una buena idea, sobre todo la parte de usar algo que ya tenemos entre manos, pero lamentablemente no funciona. Los casos B1 con B3 y B1 con B4 por ejemplo los resolvería correctamente pero en el caso B1 con B2 fallaría miserablemente, ya que ningún vértice de B2 está dentro de B1 y viceversa. Hay que buscar otro modo.

En este caso en vez de ver si una caja colisiona con otra podemos mirar que no colisiona. Fijémonos en las cajas B1 y B4: no colisionan porque el lado horizontal de B1 más alto ("alto" en el sentido habitual, ya que el eje Y crece hacia abajo y realmente sería el más bajo respecto al sistema de coordenadas) tiene un valor superior del lado más bajo de B4. Podemos generalizar esto al resto de lados y ya tenemos la función que nos dice si dos AABB colisionan o no:

bool rectsIntersect(const Rect& r1, const Rect& r2)
{
 return !(r1.pos().x() > r2.pos().x() + r2.width() ||
         r1.pos().x() + r1.width() < r2.pos().x() ||
         r1.pos().y() > r2.pos().y() + r2.height() ||
         r1.pos().y() + r1.height() < r2.pos().y());
}

Por cada lado comprobamos si nos hemos "pasado". Si no nos "pasamos" por ningún lado es que los AABB colisionan.

Como punto final comentar que usar puntos, círculos o cajas depende de lo que queramos hacer. En algunas ocasiones nos vendrá mejor usar un círculo, otras veces una caja y otras con un punto es suficiente. 

Con esta entrada terminamos las explicaciones preliminares, en la siguiente ya empezaremos a programar Culebrilla. ¡Hasta pronto!

lunes, 5 de diciembre de 2011

Un poco de matemáticas

En la última entrada vimos cómo vamos a modelar a nivel de clases nuestro primer proyecto, pero también surgieron ciertos conceptos que pueden ser nuevos, como puede ser eso de "coordenadas de mundo" o cuestiones de cómo saber si dos cajas colisionan entre sí. Por ello en esta entrada vamos a ver un poco, de una  manera en absoluto formal, las sencillas matemáticas que vamos a usar en los proyectos de este blog.

Sistema de coordenadas
Un sistema de coordenadas permite organizar de cierta manera el espacio, para poder colocar, mover y controlar los diferentes objetos del juego. En la mayoría de los videojuegos se usan sistemas de coordenadas cartesianos. Un sistema de este tipo en un espacio 2D son 2 ejes perpendiculares. Normalmente el eje horizontal se le llama X, y al eje vertical Y. En un espacio 3D los ejes suelen ser X, Y y Z, perpendiculares entre sí también.
Volvamos al sistema de coordenadas de SFML:


Arriba a la izquierda tenemos el origen del sistema de coordenadas de SFML, que representamos con los valores X e Y (0, 0). Esta representación se llama "vector" en matemáticas (más sobre esto después). El primer número corresponde al eje X, el segundo al eje Y. El eje X en este sistema de coordenadas crece hacia la derecha, y el eje Y hacia abajo. Para representar un punto en la esquina superior derecha usamos el vector (799, 0), por ejemplo. Si quisiésemos representar un punto en el centro de la pantalla usaríamos el vector (399, 299). Esto es simplemente el sistema de SFML pero existen otros similares pero diferentes, como por ejemplo que el eje Y crezca hacia arriba en vez de hacia abajo con el punto (0, 0) en la esquina inferior izquierda, etc.

Según las necesidades también podemos definir nuestro propio sistema de coordenadas y pasar de este al de SFML (o a la librería que estamos utilizando) o viceversa según lo que toque. Un ejemplo claro de esto sería un juego isométrico, donde el mapeado (bloques, enemigos y demás elementos) están en su propio sistema de coordenadas que al pintar los gráficos 2D pasamos a las coordenadas de la librería gráfica.

Vectores
Un vector es un elemento matemático que se puede interpretar como una posición en el espacio o bien una dirección (y de cualquier otra manera según nuestras necesidades, pero aquí veremos estas dos). En un espacio 2D esto se consigue simplemente con un par de números que van "juntos". En un espacio 3D usaríamos 3, obviamente. Ya hemos visto un vector antes al definir las posiciones con la forma (X, Y). ¿Cómo se define una dirección con un vector? De la misma forma que una posición, sólo que su interpretación es diferente. Si tenemos el vector (5, 5) y lo interpretamos como posición sería un punto arriba a la izquierda de la pantalla (en el sistema de SFML). En cambio si el mismo vector lo interpretamos como una dirección, por decirlo de alguna forma tenemos una "flecha". Esta flecha la podemos imaginar como que sale de (0, 0)  y va a la posición (5, 5), pero la gracia de las direcciones es que no salen de ninguna parte sino que expresan eso, una dirección, y la podemos aplicar en cualquier parte del espacio. Ya veremos cómo.

Los vectores poseen varias operaciones matemáticas: se pueden sumar, restar, multiplicar o dividir por un valor escalar (un escalar es un valor no vectorial, es decir un número "normal", aunque también vale decir que los vectores se forman con varios números escalares), calcular su producto escalar o vectorial, su módulo, etc. Con estas operaciones podremos mover nuestros elementos del juego, calcular distancias, ángulos, etc.

La suma y resta de vectores es muy intuitiva: si tenemos los vectores V1 y V2, con los valores (x1, y1) y (x2, y2), el resultado de su suma (V3 = V1 + V2) sería (x3, y3) = (x1 + x2, y1 + y2). La resta sería igual, solo que restando: (x3, y3) = (x1 - x2, y1 - y2). Obviamente en la resta el orden es importante.

La multiplicación con un valor escalar es también muy simple: V2 = V1 * a, donde "a" es el valor escalar: (x2, y2) = (x1 * a, y1 * a). La división (V2 = V1 / a) es igual: (x2, y2) = (x1 / a, y1 / a).

El módulo de un vector representa su longitud y da como resultado un valor escalar. Teniendo el vector V1 se calcula el módulo m así: m = sqrt(x1 * x1 + y1 * y1) donde la función sqrt() calcula la raíz cuadrada. En efecto es el teorema de Pitágoras. Si por ejemplo tenemos el vector (5, 5) de antes y calculamos su módulo éste nos indicará la distancia entre (0, 0) y (5, 5), que efectivamente es la longitud de la "flecha" si interpretamos este vector como una dirección. El módulo del vector V1 se representaría como |V1|

Se dice que un vector está normalizado cuando su longitud es de una unidad. Muchas veces necesitaremos que nuestros vectores estén normalizados. Para ello se calcula su módulo y con este valor se divide el vector, de forma que mida 1 pero mantenga la dirección: V1' = V1 / |V1| donde V1' es V1 normalizado.

El producto escalar ("dot product" en ingles) es un valor que se calcula con 2 vectores. Teniendo los vectores V1 y V2 su producto escalar "d" se calcularía así: d = x1 * x2 + y1 * y2. Esto también se expresa de esta forma: V1 · V2 = x1 * x2 + y1 * y2. El punto "·" es el que le da el nombre a esta operación en inglés. El producto vectorial lo dejamos para otro día ;)

Bueno, ya hemos visto ciertas operaciones que se pueden hacer con los vectores. ¿Para qué sirven o qué sentido tienen? Vamos a ver todo esto aunque no de una forma demasiado profunda o académica (para esto podéis consultar la wikipedia o cualquier libro de matemáticas).

La suma y resta de vectores podemos usarlas para mover nuestros elementos del juego. Si tenemos nuestro sprite en (0, 0) y queremos moverlo 1 unidad a la derecha, le sumamos el vector de dirección (1, 0): (0, 0) + (1, 0) = (1, 0). Si tenemos el sprite en (60, 40) y lo queremos mover en diagonal 5 unidades en cada eje le sumamos (5, 5): (60, 40) + (5, 5) = (65, 45)

La multiplicación y división permite escalar (agrandar o reducir) el módulo de un vector. Esto es muy útil por ejemplo para controlar las velocidades: si tenemos un vector que representa la velocidad de nuestro sprite, con multiplicar por dos este vector se moverá el doble de rápido. Si lo dividimos por dos a su vez tendremos un vector con la mitad del módulo.

El cálculo del módulo de un vector nos permite saber la distancia entre dos puntos del espacio. Esto es útil para colisiones, comportamiento de los enemigos, etc.

El producto escalar de 2 vectores representa una proyección de un vector en el otro (entre otras cosas), pero también satisface esta igualdad: V1 · V2 = cos(a) * |V1| * |V2|. Esto significa que el producto escalar entre los vectores V1 y V2 es igual a la multiplicación del módulo de V1 con el módulo de V2 y cos(a), donde "a" es el ángulo entre los vectores V1 y V2. Si V1 y V2 están normalizados significa que su módulo es 1, por lo que tendríamos V1 · V2 = cos(a) * 1 * 1, es decir V1 · V2 = cos(a). Esto nos da el coseno del ángulo de los vectores V1 y V2, por lo que si hacemos arccos(V1 · V2) = a, es decir el ángulo entre V1 y V2, algo muy usado en los videjuegos y en otros muchos campos (gráficos 3D, etc etc etc).

Coordenadas "de mundo"
Ya hemos visto los sistemas de coordenadas y los vectores un poco por encima (si los ve algún matemático igual me pega y todo, ejem...) pero todavía no hemos visto eso de "coordenadas de mundo" que se habló en la anterior entrada. Coordenadas de mundo, de pantalla, de sprite, etc, es una forma de decir que nuestro sistema de coordenadas es relativo a algo. Tomemos el caso de un enemigo que tenemos en un mapa de un juego de plataformas. El mapa mide 2400x600 unidades, que al corresponder a píxeles (en este ejemplo) equivalen a 3 pantallas de ancho y uno de alto, asumiendo una resolución de pantalla de 800x600. Si tenemos el enemigo justo en la mitad del mapa, sus coordenadas por ejemplo podrían ser (1200, 300). Estas coordenadas son coordenadas mundo o de mapa. Si el jugador se acerca al enemigo y es visible, éste se deberá pintar en alguna parte de la pantalla. Tenemos que pasar de coordenadas de mundo a coordenadas de pantalla. Por ejemplo según la posición del scroll y demás el enemigo lo pintaríamos en la posición (200, 100) de la pantalla. Como se puede ver, el enemigo "tiene" varias coordenadas, ya sea la (1200, 300) o la (200, 100).

¡Pero aún hay más! Imaginemos que este enemigo es un escorpión gigante. El pincho de la cola es lo único que nos daña, el resto del enemigo lo podemos tocar sin dañarnos. De alguna manera debemos hacer esto posible. Una solución es definir una caja (de las que hablamos en la entrada anterior) que englobe esta parte de la imagen del escorpión y si colisiona con la caja del protagonista que le haga daño. Ahora viene la pregunta: ¿En qué coordenadas definimos la caja? La solución es en coordenadas de sprite: la caja tendrá sus datos relativos a la imagen del escorpión, y luego el código que gestiona estas cosas se encargará de pasar de estas coordenadas a las de mundo para comparar esta caja con el del sprite del personaje protagonista, también definida en coordenadas de sprite y pasada a coordenadas de mundo.

Veamos esto con ejemplos gráficos. Aquí tenemos el PNG donde tenemos nuestro escorpión:

La caja roja indica la zona que daña al protagonista. Lo hemos definido en coordenadas de sprite, es decir, sus coordenadas dentro del PNG que lo contiene. Asumimos que el sistema es igual que al de SFML, es decir, la coordenada (0, 0) está arriba a la izquierda y los ejes crecen de izquierda y derecha y de arriba abajo.

La siguiente imagen muestra todo el montaje. Aquí también seguimos el sistema de coordenadas de SFML, aunque para el mapa podríamos usar otro cualquiera:


La caja negra representa el tamaño del mapa, 2400x600 píxeles, y la caja naranja representa la pantalla de 800x600. El escorpión está en el mapa, con su caja roja. Partiendo de su posición en coordenadas de mundo podemos calcular sus coordenadas de pantalla. También hay que calcular, como hemos visto, la posición en coordenadas de mundo que tiene su caja para saber si colisiona o no con la del protagonista. Hemos dicho que las cajas se pasan a coordenadas de mundo para su comprobación pero también sería posible pasarlas a coordenadas de pantalla y comprobar su colisión.

La conversión entre las diferentes coordenadas (también llamado mapeo o proyección según el contexto) en este caso serían simples sumas y restas. En el caso de un juego isométrico o 3D por ejemplo habría que hacer cálculos más sofisticados, ya que las coordenadas de mundo son en 3D y las de pantalla en 2D.

Espero que esta entrada haya sido interesante. Como siempre para cualquier duda o pregunta ahí tenéis los comentarios. ¡Hasta la próxima entrega!

sábado, 3 de diciembre de 2011

[Culebrilla] Diseño general

Después de un lapso de tiempo volvemos a la carga con el taller. Ya hemos visto cierta teoría para acometer nuestro pequeño proyecto y ya va siendo hora para ponernos a hacer el juego. Tenemos la idea de cómo va a ser el juego, por lo tanto ahora nos toca diseñar "las tripas" del juego. Como vamos a usar C++ en modo orientado a objetos (mayormente) vamos a empezar modelando las clases que necesitaremos. Repasemos el cutremontaje que apareció en la entrada de la presentación del proyecto:



Bien, tenemos varios elementos en el juego:
  • El fondo, que comprende el fondo gráfico (las típicas nubes creadas con el filtro del editor gráfico) y el borde de las cajas marrones.
  • La culebrilla en sí, formada por varios círculos enganchados unos a otros
  • La comida de la culebrilla (lo que se conoce como un "item")
  • El marcador con los segundos que lleva el jugador y el record de tiempo
Aparte de estos elementos que podemos sacar de la captura tenemos que tener en cuenta el texto que mostraremos cuando el juego esté en estado "Game Over". Además necesitamos una clase que sirva de "soporte" o "contenedor" de las clases que conforman el juego.

Dicho esto podemos modelar el juego con las siguientes clases. Como siempre esta no es ni la única ni la mejor forma de hacerlo, simplemente es mi aproximación. A la hora de modelar un programa virtualmente tenemos infinidad de formas de hacerlo. El nivel de sofisticación, granularidad etc depende del que lo haga, del tiempo que dispone, de las exigencias de ampliación, si tiene en cuenta la mantenibilidad del código, etc etc. En nuestro caso esto nos bastará:
  • El fondo lo representaremos con la clase Background. Se encargará de pintar el fondo, los bordes y además calculará si la caja que le pasamos colisiona con el borde o no (ya veremos esto más adelante)
  • La culebrilla la representaremos con la clase Snake. Se encargará de pintar la culebrilla (con el número de círculos que toque), de comprobar si colisiona consigo misma, de calcular si colisiona con una caja que le pasemos y de proveer la caja de su cabeza
  • La comida la modelamos con la clase Item. Esta clase se encargará de buscar un sitio en el área de juego válido para aparecer (que no colisione con la culebrilla), de desaparecer si colisiona con la caja que le proporcionemos y de aparecer otra vez en un sitio válido
  • Para el marcador tenemos la clase HUD (Heads-Up Display), que se encargará de mostrar los datos correspondientes
  • Para mostrar el texto de "Game Over" no usaremos ninguna clase, simplemente mostraremos el texto correspondiente en este estado
  • La clase "contenedora" de las anteriores la llamaremos Game. Tendrá las instancias necesarias de las clases expuestas y se encargará de gestionar el juego
Ya hemos visto las clases, ¿Ahora qué? ¿Cómo funciona todo esto? Empecemos por el nivel de abstracción más alto, que es la clase Game. De esta clase sólo creamos una instancia en la función main() del programa, donde está el bucle principal. En este bucle comprobamos si el usuario ha pulsado alguna tecla y si es así se lo indicamos a nuestra instancia de Game, que se encarga de responder a estos eventos. También en el bucle principal llamamos al método update() de Game para que éste actualice su estado y posteriormente a draw() para mostrar todo en pantalla. Con esto ya casi que tenemos hecho el fichero main.cpp.

Dentro de Game tenemos las instancias de las clases Background, Snake, Item y HUD, una de cada una. En el método update() de Game está todo el juego: con un switch comprobamos en qué estado estamos, si en "Game Over" o jugando. Si es el primero mostramos el texto con el record, si es el segundo hay que gestionar las diferentes instancias: primero llamamos al update() de la instancia de la clase Snake e Item. Cuando llamamos al update() de Snake tenemos que preguntarle en Game si la cosa sigue o es Game Over, ya que puede haber una colisión consigo misma. Una vez hecho esto tenemos que comprobar si la culebra ha colisionado con el borde del área y si ha colisionado con la comida (si está visible).

Para comprobar si ha colisionado con el borde le pedimos a la instancia de la clase Snake la caja de la cabeza, que está en coordenadas de mundo. Esta caja se la pasamos a la instancia de Background, que nos indicará si colisiona con el borde o no. Si colisiona es Game Over, sino seguimos con los cálculos. Con comprobar la colisión entre la cabeza de la culebrilla y el borde es suficiente, no hace falta comprobar todo el cuerpo de la culebrilla. Esto es así porque por la forma en que se mueve si hay colisión siempre será la cabeza la primera en colisionar.

Si la instancia de Item está en estado "comible", le pasamos la caja de la cabeza de Snake. Si colisiona indicamos a Snake que efectivamente ha comido una pieza más, sino no hacemos nada. También en caso de colisión hay que indicar a Item que ha sido comido y que actúe en consecuencia. En este caso la instancia Item tendrá que buscar un nuevo sitio en el área de juego en función del cuerpo de la culebrilla.

Y eso es básicamente todo lo que tiene que hacer la clase Game. Quizás hemos visto demasiados conceptos nuevos de golpe en esta entrega, pero cuando veamos el código fuente todo quedará bien claro.

¡Hasta la próxima entrega!

jueves, 10 de noviembre de 2011

Pequeña introducción a las SFML


Bienvenido(s) de nuevo mi(s) querido(s) lector(es). En el capítulo de hoy vamos a echar un pequeño vistazo a las librerías SFML, Simple Fast Multimedia Library. En el primer post dijimos que no íbamos a ver cómo inicializar cosas como las SDL y tampoco es la intención de esta entrada, pero considero adecuado dar un ligero vistazo a cómo se hace para poner un sprite en pantalla, hacer sonar un efecto de sonido, etc con las SFML. Tampoco vamos a ver cómo se hace todo en sus múltiples variantes, para eso mejor mirar la sección de tutoriales de la página de las SFML.

Las librerías SFML son un equivalente de las SDL, sólo que están programadas en C++ (en contraposición al C de las SDL) y orientadas a objetos. Dispone de varios "bindings" para diferentes lenguajes de programación aparte de C++ y está estructurado en varios módulos (gráficos, sonido, etc). Para dibujar usa openGL en vez de una solución por software como hacen las SDL (aunque parece ser que en la siguiente versión estas librerías también usarán openGL). Esto implica que con las SFML tenemos efectos como rotaciones, escalados, semitransparencias y demás de serie, a diferencia de las SDL donde no podemos hacer ni un simple "flip" con lo que nos ofrecen las librerías. Las SFML también se pueden usar como las SDL como un sistema multiplataforma para inicializar openGL y lidiar con el teclado y demás, si lo estimamos oportuno. Gracias a su diseño modular podemos usar sólo lo que necesitemos.

Las SFML incluyen librerías para hilos, teclado, joystick, ratón, crear ventanas, pintar gráficos, reproducir música (en formato OGG), reproducir efectos de sonido, etc. Todo multiplataforma (Windows, Linux y Mac OS X). Aquí vamos a ver la última versión estable, la 1.6. La versión 2.0 se supone que debe salir en un futuro más o menos próximo con interesantes novedades en el aspecto gráfico, entre otros un mecanismo para hacer batching. La versión 1.6 no hace esto, aunque para un juego como Culebrilla no supone ninguna diferencia.

Bueno, empecemos: nuestra aplicación comienza con un simple main() corriente y moliente. Para ver algo debemos crear un RenderWindow, que es la clase que usan las SFML como punto de partida de su infraestructura. Al crearlo podemos indicar los típicos parámetros de tamaño, tipo de ventana, si es a pantalla completa o no, esperar el retrazo vertical, etc.

Después de instanciar un RenderWindow ya podemos entrar en nuestro famoso bucle principal. Si la instancia de RenderWindow se llama "renderWindow", el código podría quedar así:

while(renderWindow.IsOpened())
{
 sf::Event currentEvent;
 while(renderWindow.GetEvent(currentEvent))
 {
  // El usuario ha cerrado el programa?
  if(sf::Event::Closed == currentEvent.Type || 
  (sf::Event::KeyReleased == currentEvent.Type &&
   sf::Key::Escape == currentEvent.Key.Code))
  {
   renderWindow.Close(); // Cerramos la aplicacion
  }
  else
  {
   // Pasamos el evento a nuestro juego (teclado, etc)
   game.processEvent(currentEvent);
  }
 }

 // Actualizamos nuestro estado
 game.update(renderWindow);
 
 // Pintamos los graficos
 game.draw(renderWindow);
 
 // Mostramos lo que hemos pintado en el paso anterior
 renderWindow.Display();
}

Aquí vemos que tenemos un while que se ejecuta mientras tenemos abierto renderWindow. Después viene la parte donde somos buenos invitados del SO y atendemos a sus eventos: mientras RenderWindow::GetEvent() nos devuelva true tenemos un evento pendiente del SO, sino ya hemos cumplido y salimos del while para hacer nuestras cosas. En este while de los eventos miramos si el evento es el cerrar la ventana (ya sea pulsando el botón o con alguna combinación como ALT + F4) o ha pulsado la tecla ESC. Si es así cerramos renderWindow, lo cual hace que salgamos de la aplicación. Si no es uno de estos eventos se lo pasamos a nuestro juego (la instancia "game") para que pueda responder correctamente al teclado, joystick o lo que considere adecuado.

Una vez atendidos los eventos del SO pasamos a ejecutar código del juego: actualizamos el estado y pintamos los diferentes elementos del juego. A estos dos métodos le pasamos la instancia renderWindow, ya que el método update() lo usará para calcular el tiempo (usando RenderWindow::GetFrameTime()) y el método draw() lo usará como destino de los gráficos, que vamos a ver a continuación.

La parte gráfica se base en las clases Image y Sprite, aunque hay más (dibujado de formas geométricas, texto, efectos de postproceso, etc). La primera es la encargada de guardar la información de las imágenes (normalmente cargada de un fichero gráfico como puede ser un png). La segunda es la que usa la información de Image y lo pinta a un RenderTarget. En nuestro caso es la instancia de RenderWindow, que deriva de RenderTarget. Esto implica que podemos tener una instancia de Image que es usada por múltiples instancias de Sprite, pudiendo cada uno de estos presentarla de una forma diferente (con su propio escalado, rotación, etc).

Cargar un fichero gráfico es tan sencillo como lo siguiente:

sf::Image img;
img.LoadFromFile("graphics/head.png");

Hay que tener en cuenta que la clase Image no controla la carga de ficheros repetidos. Es decir, si yo creo 5 instancias de la clase Image y en todas cargo el fichero "background.png", este fichero se habrá cargado 5 veces, consumiendo 5 veces la memoria correspondiente. Evitar cargar más de una vez un fichero queda en manos del cliente de la clase.

Respecto a la clase Sprite su funcionamiento también es sencillo. Por ejemplo para dibujar lo que hemos cargado en el ejemplo anterior haríamos lo siguiente, usando también la instancia de RenderWindow que hemos visto antes en el bucle principal:

sf::Sprite sprite(img);
sprite.SetPosition(0, 0);
renderWindow.Draw(sprite);

Esto dibujaría lo que tengamos en la variable img en la esquina superior izquierda. El sistema de coordenadas de la parte gráfica de las SFML es como vimos en la entrada sobre la actualización del estado. El ejemplo es de una pantalla de 800x600 píxeles:


La clase Sprite dispone además de varios métodos que son muy útiles para programar juegos: flip (efecto espejo) tanto en X como en Y, fijar la posición del sprite o moverlo, rotarlo, escalarlo, cambiar el tipo de blending, definir un rectángulo para dibujar sólo una parte del Image del Sprite, etc. Todo bien explicado en la documentación en su página web.

Para reproducir música o efectos de sonido la cosa es más o menos la misma: instanciar la clase correspondiente e indicarle la ruta del fichero a cargar. Además de cargar los datos de un fichero, las SFML ofrecen también métodos para cargar de un buffer en memoria, por ejemplo si queremos hacer algún sistema de paquetes para proteger nuestro contenido. Esto se aplica a todas las clases que cargan de un fichero (gráficos, música, etc).

No creo que merezca la pena ahondar más en las SFML, ya iremos viendo su uso con más detalle a lo largo del desarrollo de Culebrilla. Como colofón de esta entrada tenéis un ejemplo muy sencillo para ver lo fácil que es usar las SFML en la documentación.

¡Nos vemos en la siguiente entrada!