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!

domingo, 6 de noviembre de 2011

[Culebrilla] Empezando con el juego: algunas notas sobre el tiempo

En el capítulo anterior hemos visto que hay que controlar el tiempo para que el juego vaya de la misma forma independientemente del hardware, y además los dos métodos habituales para hacerlo. En el capítulo de hoy hablaremos sobre ciertos detalles relativos a esto.

En muchas librerías de ventanas u otros entornos existe la posibilidad de crear un timer (se podría decir que en casi todos). Según la librería y el lenguaje de programación hay varias formas de usarlo pero conceptualmente es el mismo mecanismo: se indica un intervalo de tiempo y la infraestructura de la librería se encarga de llamar periódicamente a una función, método de un objeto, etc.

Efectivamente, esto es lo mismo que el fixed time step que vimos en la entrada anterior pero debemos andar con cuidado al usarlo. Dependiendo del entorno y cómo está implementado es posible que no nos sirva. Puede que la resolución que ofrezca no sea la suficiente, que haya un intervalo mínimo y de esta forma no podamos aprovechar al máximo el hardware, etc. También depende de lo que necesitemos: Para Culebrilla nos serviría más o menos cualquier timer que pudiésemos encontrar pero vamos a hacerlo a mano para aprender cómo sea hace. Un juego más exigente tendría que usar el bucle que hemos estado viendo a lo largo de las últimas entradas.

Relacionado con esto está el consumo de la CPU: nuestro bucle está iterando constantemente, no espera a ningún evento sino que como hemos visto consultamos (polling) lo que necesitamos. Esto hace que se consuma el 100% de tiempo de una CPU, incluso si no calculamos nada o hacemos cálculos muy simples. No es en función de la carga de nuestros cálculos sino del bucle sin bloqueos que tenemos en nuestro juego. Esto se puede suavizar llamando a la función de bloqueo del hilo (sleep(), delay(), etc) con un tiempo muy pequeño o directamente con el valor 0. Esto hará que el SO no esté ejecutando nuestra aplicación casi todo el tiempo, bajando un poco el consumo de CPU.

En la siguiente entrada miraremos un poco la librería SFML como preludio a empezar a programar nuestro primer juego. ¡Hasta pronto!

[Culebrilla] Empezando con el juego: El problema del tiempo

En la entrada anterior vimos cómo se encarna en código (de una forma muy simple) el famoso bucle general de los videojuegos. También dijimos que era una chapuza. ¿Por qué? Muy simple: porque no tiene en cuenta el tiempo y es totalmente dependiente de la velocidad de proceso del hardware donde funciona. Recordemos el código que usamos en la última entrada:

// Inicializamos las cosas...
Img bulletImg = loadImg("bala.png");
bool done = false;
float bulletPos = 0;
while(!done)
{
 // Actualizamos el estado
 bulletPos += 1;
 if(800 <= bulletPos)
 {
  done = true;
 }
 
 // Pintamos la bala
 drawSprite(bulletImg, bulletPos, 300);
}
// Liberamos las cosas
freeImg(bulletImg);

He añadido cosas como loadImg() y freeImg(), además de declarar las variables para completar un poco el ejemplo. Estas funciones son inventadas de raíz, pero el primero se supone que cargaría el fichero indicado y el segundo liberaría lo creado por el primero. Un poco como el new/delete de C++ (o malloc/free de C).

Bien, ¿Qué problema tiene el bucle para llamarlo chapuza? Pues que la bala se moverá un píxel por frame (por cada iteración). El problema de esto es que dependiendo del hardware donde se ejecute puede tardar más o menos en ejecutar cada iteración. Esto implica que por ejemplo en una máquina pueda hacer 100 iteraciones por segundo, pero en otra menos potente puede que sólo haga 20. Si este código fuese para una consola o un hardware específico quizás podríamos dejarlo tal cual, ya que tendríamos la garantía de que todas las unidades de ese hardware tardan lo mismo en ejecutar el mismo código. Si queremos que nuestro juego vaya en PC no podemos dejarlo así ni mucho menos.

Imaginemos este caso para tener más claro el problema: tenemos un PC que puede procesar 100 veces por segundo el bucle. En este caso diríamos que el juego va a 100 FPS, fotogramas por segundo o frames per second en inglés. Tenemos otro PC que es menos potente y que sólo puede ejecutar el bucle a 50 FPS. En un segundo, en el primer caso la bala recorrería 100 píxeles, mientras que en el mismo lapso de tiempo el segundo PC lo hubiese móvido sólo 50. Es decir, en el segundo PC el juego iría a la mitad de velocidad, afectando seriamente a la jugabilidad. Lo que es más, imaginemos que se lo pasamos a un amigo con un PC mucho mas potente que mueve el juego a 1000 FPS. ¿Quien podría jugar a un juego donde todo va tan deprisa?

Con esto queda claro que no podemos usar el bucle tal cual, necesitamos que el juego vaya de la misma forma en todos los PCs. Sea cual sea el hardware (dentro de unos límites, claro) las balas del juego deben moverse a la misma velocidad, el personaje debe saltar lo mismo, etc. ¿Cómo podemos hacerlo? La clave es que el movimiento y demás cálculos sean independientes de la frecuencia de las iteraciones (framerate), de forma que en vez de expresar la velocidad de una bala en píxeles/frame lo expresemos como píxeles/segundo. Cuando en el bucle que tenemos ahí arriba hacemos bulletPos += 1 estamos indicando que la bala se mueve 1 píxel por frame, cosa que hemos visto no es aceptable. Debemos poder moverlo 1 píxel por segundo, es decir, controlar el tiempo para poder hacer los cálculos correctamente.

Hay 2 formas habituales de hacer el control del tiempo: "Variable time step" y "fixed time step". Ambos parten de la misma idea: calculamos el tiempo que nos ha llevado ejecutar el último frame y usamos este valor para actualizar el estado actual. Veamos cómo funciona cada método:

Variable time step
En cada iteración obtenemos el tiempo que ha pasado desde el último frame hasta el momento actual. Usando este valor se escalan todos los cálculos que dependan del tiempo. El bucle de nuestro ejemplo quedaría así:

float oldTime = currentTime();
while(!done)
{
 float now = currentTime();
 // Calculamos cuanto segundos han pasado desde el último frame
 float deltaTime = now - oldTime;
 oldTime = now;
 // Actualizamos el estado
 bulletPos += 1 * deltaTime;
 if(800 <= bulletPos)
 {
  done = true;
 }
 
 // Pintamos la bala
 drawSprite(bulletImg, bulletPos, 300);
}

La función currentTime() devuelve el número de segundos que han pasado desde que se inició el programa con el tipo float. Es decir, si ha pasado medio segundo por ejemplo devolverá el valor 0.5. Es una función inventada para el ejemplo como puede ser drawSprite(). Declaramos la variable oldTime y le asignamos los segundos que han pasado hasta ese momento.

En cada iteración calculamos el tiempo que ha tardado en procesarse el último frame: restamos los segundos que han pasado hasta este momento con los que obtuvimos en la iteración anterior y lo asignamos a la variable "deltaTime". Luego asignamos los segundos del momento actual (la variable "now") a la variable "oldTime" para tener este valor en la siguiente iteración.

Una vez calculado deltaTime lo usamos para escalar el desplazamiento de la bala. Como se mueve un píxel por segundo y han pasado deltaTime segundos, añadimos a bulletPos la cantidad 1 * deltaTime. Así, si ha pasado un segundo se movería 1 píxel, si han pasado 10 segundos se movería 10, y si ha pasado una décima de segundo se movería 0.1 píxeles. Como se ve la posición debe ser un valor de coma flotante, sino perdemos esta precisión de mover las cosas menos de un píxel.

Fixed time step
La idea de este método es definir un intervalo fijo para los cálculos y ejecutar tantas veces estos cálculos como sea necesario. Nuestro bucle quedaría así implementando este método:

const float LogicTime = 1.0 / 60.0; // 1/60 segundos cada paso de la lógica
float oldTime = currentTime();
float deltaTime = 0;
while(!done)
{
 float now = currentTime();
 deltaTime += now - oldTime;
 oldTime = now;

 while(LogicTime < deltaTime)
 {
  // Actualizamos el estado
  bulletPos += 1.0 / 60.0;
  if(800 <= bulletPos)
  {
   done = true;
  }

  deltaTime -= LogicTime;
 }

 // Pintamos la bala
 drawSprite(bulletImg, bulletPos, 300);
}

Primeramente definimos la frecuencia a la que queremos que se ejecute la lógica del juego. En este ejemplo lo hemos puesto a 60 veces por segundo. Después declaramos la variable oldTime como antes pero también definimos la variable deltaTime, la cual estaba dentro del while en el caso del variable time step. Esto es necesario para llevar un control preciso sobre el tiempo, como veremos ahora.

Dentro del primer while calculamos el tiempo que ha pasado del frame anterior, pero en vez de asignar este lapso de tiempo a deltaTime se lo sumamos. A continuación tenemos otro while, que es la parte fundamental del fixed time step: actualizamos el estado tantas veces como LogicTime-s contenga deltaTime. Es decir, si deltaTime es 1 segundo este while se ejecutará 60 veces. Si es 0.5 segundos se ejecutará 30 veces y si es 1/60 segundos se ejecutará 1 vez. Si deltaTime es un valor inferior a 1/60 segundos no se ejecutará nada, se pintará la bala y estaremos otra vez al principio del bucle general. Es aquí donde se ve el sentido de añadir y no asignar el lapso de tiempo a deltaTime: si el lapso de tiempo es inferior a LogicTime hay que ir acumulando los lapsos de tiempo hasta llegar a ser igual o superior a este valor para poder ejecutar la lógica del juego. De la misma forma si el framerate no es un múltiplo de LogicTime al terminar de ejecutar el segundo while deltaTime tendrá cierto tiempo residual (por decirlo de alguna forma) y este tiempo no se puede desechar, hay que tenerlo en cuenta para la siguiente iteración.

Como se puede observar a bulletPos ya no le sumamos 1 sino 1/60, que es lo que se tiene que mover en una frecuencia de 60 veces por segundo si queremos que se mueva 1 píxel por segundo.

Ya hemos visto los dos métodos típicos para controlar el tiempo. Como veis el concepto general es similar: cuanto más grande sea el lapso de tiempo más movemos la bala, ya sea haciendo más exagerado su movimiento o ejecutando más veces la lógica del juego y viceversa (a menor lapso de tiempo menos desplazamiento por frame). Además ambos métodos también sirven para hacer frente a lapsos de tiempo no constantes, ya que lo normal suele ser que varíe durante la ejecución. Es decir, en cada iteración del bucle principal podemos tener un lapso de tiempo diferente.

Dicho esto ¿Qué método escogemos? Depende un poco de las preferencia de cada uno. En mi caso prefiero el fixed time step, lo encuentro más sencillo y no hay que ir multiplicando deltaTime a cada variable dependiente del tiempo. Para el Culebrilla usaremos este método y también para los futuros juegos del blog. Los dos métodos tampoco son excluyentes: se puede usar el fixed time step para la lógica del juego y el variable time step para los gráficos, interpolando el movimiento y las animaciones con deltaTime. Por último aquí tenéis un artículo (en inglés) hablando sobre este tema, por si queréis otro enfoque: http://www.iguanademos.com/Jare/Articles.php?view=FixLoop

En la siguiente entrada hablaremos un poco más sobre el control del tiempo. ¡Hasta pronto!


viernes, 4 de noviembre de 2011

[Culebrilla] Empezado con el juego: actualización del estado

En la entrada anterior hemos visto el bucle principal que tienen todos los juegos y cómo hay un paso que es el "corazón" del juego, donde se hacen los cálculos para que todo funcione. Es decir, la parte donde se actualiza el estado del juego.

Pongamos un caso concreto muy simple para entender mejor el concepto de actualizar el estado: tenemos una bala que empieza a la izquierda del todo (X = 0) y acaba en la derecha (X = 800). El estado en este caso sería la posición 2D de la bala, o si queremos reducirlo aún más, su posición en el eje horizontal (ya que en el vertical no hay cambios). En el sistema de coordenadas del ejemplo el eje horizontal es el X y el vertical el Y. El origen (0, 0) está arriba a la izquierda y abajo a la derecha es (799, 599). Por lo tanto tenemos una pantalla de 800x600 píxeles:


Recordemos el bucle del juego:
Inicializar
Mientras la cosa sigue:
  Atender eventos del SO
  Comprobar entrada del usuario (teclado, ratón, joystick, etc)
  Actualizar el estado (enemigos, disparos, reproducir sonidos, etc etc)
  Pintar los gráficos
Desinicializar
Vamos a ver cómo sería a grandes rasgos cada paso en nuestro ejemplo:

Inicializar
Creamos una variable llamada bulletPos y fijamos su valor a 0. Cargamos el gráfico de la bala y por último creamos la variable booleana done, que fijamos a false. Como veis uso nombres en inglés en el código.

Mientras la cosa sigue
Comprobamos si done es true. Si es así salimos del bucle, sino hacemos otra pasada. Básicamente sería esto:

while(!done)
{
// ...
}
Atender eventos del SO y Comprobar entrada del usuario
En este ejemplo obviamos estos pasos

Pintar los gráficos
Aquí con la librería de gráficos que tengamos dibujamos el gráfico que hemos cargado en el paso de inicialización en la posición (bulletPos, 300)

Desinicializar
Liberamos los recursos que hemos cargado, es decir, el gráfico de la bala

Nos falta el paso Actualizar estado, que vamos a explicar ahora: tenemos que hacer que la bala se mueva y que cuando llegue a la derecha salir del bucle. Esta condición es fácil, bastaría con algo así:

if(800 <= bulletPos)
{
 done = true;
}

Con esto el while saldría del bucle. Ahora el movimiento: tenemos la posición del eje X de la bala en la variable bulletPos, y queremos que la bala vaya de izquierda a derecha. Muy fácil: le añadimos una cantidad a bulletPos en un for y listos. Algo así:

for(int i = 0; i < 800; ++ i)
{
 bulletPos += 1;
}

Ejecutamos el programa y vemos que la bala aparece directamente en la derecha. Bueno, no lo veríamos porque el programa saldría enseguida ya que al hacer la primera pasada del while bulletPos es igual a 800, en la segunda pasada done sería true y al volver a comprobar la condición del while este ya saldría del bucle.

¿Entonces cómo se hace el movimiento? La clave es entender que lo que va dentro del bucle es un pasito del juego, un frame como dijimos en la entrada anterior. En cada frame la bala se mueve un poquito, creando así la ilusión del movimiento. El bucle correcto quedaría así:

while(!done)
{
 // Actualizar estado
 bulletPos += 1;
 if(800 <= bulletPos)
 {
  done = true;
 }
 /* Pintar la bala. bulletImg es donde tenemos guardado  la imagen de la bala y que la función para pintar es drawSprite(Img, posX, posY) */
 drawSprite(bulletImg, bulletPos, 300);
}

La función drawSprite me la he inventado para el ejemplo por lo que carece de importacia. Lo importante es lo que se hace con la variable bulletPos: En cada iteración, "movemos" la bala una unidad a la derecha, comprobamos que ha llegado o no al borde de la pantalla y luego pintamos la bala. Según el valor que le añadamos a bulletPos la bala irá más o menos rápida.

En la siguiente entrada veremos porque esto es conceptualmente correcto pero una chapuza.

¡Hasta pronto!

jueves, 3 de noviembre de 2011

[Culebrilla] Empezado con el juego: bucle principal

En la última entrada vimos qué pinta va a tener nuestra versión del snake. Perfecto, ¿Pero ahora que? ¿Cómo se hace para que la culebra vaya por ahí, responda al teclado, detecte que ha conseguido la comida o que se ha estrellado contra una pared? En definitiva, ¿Cómo se implementa esto? Responder a este tipo de preguntas es el objetivo de este blog, así que vamos a empezar a ver cómo podemos montar un juego con nuestros conocimientos de programación. Por ahora explicaremos los conceptos en general, sin ceñirnos a nuestro juego para así tener una mejor comprensión de lo que estamos tratando y su porque. Más tarde ya veremos cómo traducimos este conocimiento al código del juego.

A grandes rasgos un videojuego es un programa que está en un bucle constante, en contraposición a una aplicación normal (como puede ser un editor de texto por ejemplo) que suele estar a la espera de algún evento: clics del usuarios, entrada del teclado, etc. Un videojuego normalmente no espera a estos eventos sino que consulta (lo que en inglés se llama polling) este tipo de cosas desde su bucle principal. La estructura de la ejecución de un videojuego sería la siguiente:
Inicializar
Mientras la cosa sigue:
  Comprobar entrada del usuario (teclado, ratón, joystick, etc)
  Actualizar el estado (enemigos, disparos, reproducir sonidos, etc etc)
  Pintar los gráficos
Desinicializar
En el paso "Inicializar" creamos las estructuras que necesitamos para nuestro juego: cargar ficheros, fijar la resolución de la pantalla, etc. Después de esto entramos en el bucle principal, y la primera acción que hacemos es el "polling" de los dispositivos de entrada.

El siguiente paso es donde por norma general suele estar el meollo de un videojuego, el paso de "Actualizar estado". Aquí, en función de lo que hemos obtenido en el paso de comprobar la entrada, del estado anterior y en definitiva en base a todos los factores significativos para el juego se decide qué hacer: crear la explosión de la nave, añadir puntos, quitar una vida, mover el fondo, comprobar colisiones, etc.

Después hacemos el paso "Pintar los gráficos". Cada vez que pasamos por aquí decimos que hemos pintado un "frame"  (fotograma), por lo que normalmente a cada paso del bucle se le llama frame. Efectivamente, en los videojuegos la ilusión del movimiento se consigue como en las películas o en la televisión: mostrando fotogramas ligeramente diferentes uno detrás de otro a gran frecuencia (normalmente mayor que los alrededor de 24 fotogramas por segundo de las películas, pero esto depende mucho de la carga del juego y el hardware).

Una vez hecho este paso ya tenemos (entre otras cosas) las posiciones de los elementos visuales del juego, por lo que sólo nos falta dibujarlos. Cuando esto se acaba volvemos otra vez al principio del bucle, obtenemos la entrada del usuario, hacemos todos los cálculos pertinentes y dibujamos otro frame y vuelta a empezar otra vez. En algún momento saldremos de este bucle, liberamos todo lo que hemos obtenido a lo largo del programa (normalmente memoria del sistema) y nuestro juego ya termina su ejecución.

Ya sabemos que un juego debe estar en un bucle casi todo el tiempo. ¿Cómo podemos implementarlo en C++ desde cero, por así decirlo? Es decir, ¿Sin usar un entorno ya orientado a crear videojuegos? La primera cosa que se nos ocurriría sería usar un for o un while, pero no podemos hacer esto sin más en un sistema operativo (SO) moderno. El SO asigna tiempo de CPU a cada proceso según su criterio. Si nuestro programa estuviese todo el rato en ese while no respondería a los eventos del SO, por lo que éste asumiría que se ha quedado bloqueado (el típico mensaje de "este programa ha dejado de responder" de Windows por ejemplo). Si fuese una consola tipo Mega Drive o un SO como MS-DOS donde un proceso se hace dueño de todo el sistema sí podríamos hacerlo, pero en un SO moderno debemos hacerlo de otra forma. ¿Cómo? Simplemente atendiendo a los eventos que nos pasa el SO. El bucle quedaría así:

Mientras la cosa sigue:
  Atender a eventos del SO
  Comprobar entrada del usuario (teclado, ratón, joystick, etc)
  Actualizar el estado (enemigos, disparos, reproducir sonidos, etc etc)
  Pintar los gráficos

En cada vuelta del bucle miraremos si hay eventos del SO (esto lo hacemos mediante la librería SFML) y los atenderemos si los hay. Los eventos del SO son cosas como que el usuario ha pulsado una tecla, ha movido el ratón, ha pulsado el botón de cerrar de la ventana, etc. Una vez cumplido con nuestro anfitrión, seguimos a lo nuestro.

Seguimos en la próxima entrada, ¡Hasta pronto!

Proyecto 1: Culebrilla

Empecemos con nuestro primero proyecto, y empecemos de una forma simple: haremos nuestra propia versión del conocido juego "snake". Uno de los juegos más jugados de toda la historia por cierto, ya que venía de serie en muchos terminales Nokia.

El objetivo del juego es controlar a la culebra para comer la comida que va apareciendo por la pantalla y aguantar el máximo posible de tiempo con ella viva. Cada vez que come esta comida (su cabeza colisiona con este elemento) se le añade una "bola" más a su cuerpo. Es decir, cuanto más come mas crece su cola. Si la cabeza colisiona con una esquina la culebra muere, de la misma forma que si colisiona con alguna parte de su cuerpo también muere.

La vista del juego es cenital, es decir, el observador está mirando el juego desde arriba hacia abajo. La culebra puede moverse arriba, abajo, izquierda o derecha. Nunca se para, siempre va para adelante. El cuerpo de la culebra es articulado, como una serie de bolas atadas entre sí con una cuerda.

Nuestra versión, llamada "Culebrilla", tendrá las siguientes características generales:
  • Resolución de 800x600
  • Control por el teclado
  • Sólo tendrá un modo de juego, tipo "survival" (aguantar todo lo posible)
  • Una única pantalla con 2 estados: esperando a que el usuario pulse la tecla espaciadora (mostrando un mensaje para ello, además del record del tiempo máximo) y jugando
Aquí un montaje muy simple de cómo se verá el juego en mitad de la partida:


Como veis en este blog no vamos a ver unos gráficos de última generación, quizás ni siquiera bonitos. De hecho el marcador no está ni centrado, pero para mostrar el concepto esto es más que suficiente.

Bien, tenemos ese fondo gris, que es donde se moverá nuestra culebrilla. Rodeando el fondo gris tenemos unas casillas más o menos marrones. Estas casillas representan los límites del área por donde puede moverse la culebrilla. Si su cabeza choca contra ellas hemos perdido el juego.

Nuestra culebrilla está formada por la cabeza y luego un número de bolas que representan su cuerpo. La bola roja es la comida, que irá apareciendo en lugares aleatorios a lo largo de la partida. Por último, arriba del todo tenemos los segundos que lleva el jugador jugando sin morir y a su derecha el tiempo máximo que alguien (desde que se ejecutó el juego) ha aguantado vivo.

En la siguiente entrada vamos a ver cómo diseñamos nuestro código para que podamos implementar el juego.

¡Hasta pronto!

Presentación del blog

Bienvenidos al blog "El taller de videojuegos". Este blog nace con la intención de enseñar a programar videojuegos. Si siempre has querido programar juegos pero no sabes cómo hacerlo o por donde empezar, este es tu blog. Aquí no se hablará de cómo inicializar SDL y cosas por el estilo, ya hay cientos de tutoriales que lo explican con todo lujo de detalles. Aquí se mostrará cómo programar juegos completos paso a paso, no técnicas aisladas.

El enfoque que pienso seguir es presentar un juego (por ahora clásicos sencillos), mostrar cómo lo concibo con una pequeña maqueta de sus pantallas, elementos, etc. y luego programarlo, explicando paso a paso el proceso. Obviamente este "paso a paso" no será una explicación línea a línea sino sólo de las partes significativas, pero ya se irá viendo el nivel de detalle que requiere cada caso. Por supuesto en los comentarios se podrá podrá preguntar cualquier aspecto que no haya quedado claro.

El entorno que pienso usar será Visual C++ Express 2010 con las librerías SFML. Obviamente esto implica que usaré C++ para programar los juegos. Es un lenguaje que me gusta y además permite trabajar tanto a alto como a bajo nivel. Aunque los proyectos se suban como ficheros de proyectos de VC++ también subiré el proyecto en cmake, para que se pueda compilar sin demasiados problemas en otros entornos y sistemas operativos. No descarto en absoluto usar otro lenguaje o entorno para futuros proyectos, pero por ahora usaré este.

Por último comentar que lo que se mostrará aquí no será la mejor forma ni la definitiva de hacer las cosas, se mostrará mi forma de implementar los diferentes aspectos de los juegos. Simplemente una alternativa entre otras muchas.

Lo dicho, bienvenidos de nuevo a este blog. ¡Espero que disfrutemos juntos de la programación de juegos!