jueves, 7 de febrero de 2013

El primer juego en SFML 2.0 (II): ASTEROID con SFML 2

En la anterior entrada configuramos un proyecto modelo que nos ayudara a iniciar rápidamente un nuevo juego sin tener que hacer las repetitivas configuraciones del compilador, en este caso Visual Studio 2010. Ahora usaremos este proyecto template y crearemos nuestro primer juego, el asteroides, un juego sencillo en gráficos y diseño y ampliamente conocido por todos como para poder ponernos manos a la obra inmediatamente.


 

Diseño del Juego


El juego de asteriod es sencillo es su concepción. El jugador controla una nave que puede girar sobre si misma, puede acelerar y disparar hacia donde apunta. Tiene que ir despedazando unos asteroides, que se hacen mas pequeños, con los disparos hasta limpiar la pantalla. Cada vez que limpia la pantalla se sube un nivel y se crean nuevos asteroides con alguno mas por nivel. Si la nave choca con un asteroide se destruye, se le quita una vida y reaparece en el centro de la pantalla. Al acabar con las tres vidas pierde el juego y se reinicia
Elementos que intervienen en el juego:

  • Nave del jugador
  • Disparos del jugador
  • Asteroides
  • Contador de vidas
  • Puntuación
  • Indicador de nivel.
  • Explosión de asteroide o nave

No vamos a usar gráficos externos, sino que usaremos algunas clases internas de SFML que nos permitirán crear gráficos de líneas dentro del propio código.
Hay algunos puntos que hay que definir un poco más para que quede todo bien asentado.

  • La pantalla es de 640x480
  • Todo lo que sale por un lado de la pantalla aparece por el otro
  • El numero de asteroides será de 3 + NºNivel.
  • El nivel empieza en 1.
  • El numero de vidas será 3.
  • La velocidad de los asteroides iniciales es de 10 pixel /segundo
  • Cada asteroide se despedaza en dos nuevos con tamaño la mitad.
  • Los asteroides nuevo aumentan la velocidad en la mitad del original
  • Los asteroides nuevo tienen una trayectoria aleatoria.
  • Los asteroides con tamaño 1/4 (es decir la tercera generación ) se eliminan con el disparo
  • Los asteroides no chocan entre si.
  • La nave no choca con las balas
  • La explosión no afecta a nadie.
  • Las balas tienen una vida de 4 segundos
  • La nave pierde velocidad cuando se deja de aplicar empuje
  • La aceleración de la nave es de 5 pixel /segundo
  • La resistencia de la nave es de 2 pixel /segundo

Esto es de forma un poco desordenada un documento de diseño, donde no solo se dan unas indicaciones generales, sino que se entra hasta el ultimo detalle, para que a la hora de programar, podamos hacerlo desde el principio con seguridad. Luego según se pruebe el juego se modificaran los valores como el empuje de la nave por segundo, la velocidad de los asteroides, etc..

 

Creación del Proyecto


Para empezar copiaremos la carpeta _SFMLTemplate que hicimos antes y le cambiaremos el nombre a Asteroid. A continuación abrimos la solución, que se encuentra en Asteroid/build/build.sln, con VS2010. A continuación cambiamos el nombre del proyecto:

asteroid001

Se selecciona el nombre del proyecto, se pulsa F2, se cambia el nombre por Asteroid y se pulsa Enter. Como las propiedades usan el nombre del proyecto como nombre del ejecutable, la aplicación se llamara Asteroid. Simple y elegante.

 Anatomía de un juego


Un juego básicamente se compone de una sección de inicio donde se establece las características de nuestro juego como tamaño de pantalla inicial, controles iniciales, inicialización de la tarjeta grafica, etc.. Luego se pasa a un bucle que permanecerá repitiendo la misma secuencia hasta terminar el juego.

GameLoop

La secuencia dentro del bucle es recoger los eventos del sistema operativo y del usuario como redimensión de la ventana, pulsaciones del teclado o movimientos del ratón, luego de ejecuta la lógica del juego como mover los objetos, cambiar los contadores, comprobar las condiciones de victoria. Después se simula la física si esta esta presente.

Por ultimo se dibuja todo en su lugar, se sincroniza, es decir se espera un tiempo para que los frames por segundo sean los adecuados y no valla muy rápido y vuelta a empezar. En este bucle también estaría el procesado de los eventos que nos llegan desde la red, en un juego multi jugador.
El siguiente código lo podéis encontrar en el tutorial de SFML 2 y es la aplicación mas básica en SFML 2. Los comentarios indica que hace cada parte.

//Inclusión de las funciones graficas de SFML2
#include <SFML/Graphics.hpp>

//Punto de entrada de todo programa de ordenador
int main()
{
    //Creamos una ventana de 200x200 con el titulo "SFML works!"
    sf::RenderWindow window(sf::VideoMode(200, 200), "SFML works!");
    
    //Creamos una figura geométrica ( un circulo )
    //con las funciones internas de SFML2
    sf::CircleShape shape(100.f);
    
    //Lo rellenamos de verde
    shape.setFillColor(sf::Color::Green);

    //El programa se ejecutara mientras la ventana este abierta
    while (window.isOpen())
    {
        //Reogemos y procesamos los eventos del sistema operativo
        sf::Event event;
        while (window.pollEvent(event))
        {
            //Si el evento es cerrar la ventana, cerramos la ventana que creamos antes
            if (event.type == sf::Event::Closed)
                window.close();
        }

        //Dibujamos
        window.clear();     //Limpiamos la ventana
        window.draw(shape); //Dibujamos la figura
        window.display();   //Mostramos todo lo que se ha dibujado
    }

    return 0;   //Finalizamos la aplicación
}


Como podéis comprobar la aplicación SFML tiene todos los elementos (excepto la actualización de la lógica ) que necesitamos para hacer un juego. Copiad y pegar todo el código a vuestro main.cpp y ejecutar la aplicación ( en VS2010 pulsar F5 ).

 

Definición del campo de juego


 Vamos a empezar por hacer que nuestra pantalla sea la que queremos para nuestro juego, para ello modificaremos la línea 5 donde se crea la ventana de la aplicación:
//Definición del tamaño de la ventana
sf::RenderWindow window(sf::VideoMode(640, 480), "Asteroid by David Pulido Vargas (2012)");

Eliminad también lo referente al objeto “shape” en las líneas 12, 15 y 31 para que no tengamos molestando al circulo verde.

Lo primero que hay que saber es que las coordenadas de la pantalla tiene su origen en la esquina superior izquierda, y se incrementa la coordenada x (horizontal) hacia la derecha, u la coordenada y (vertical) hacia abajo.

sprites_5184_15_2



Creando la nave


Lo primero que vamos a hacer es crear nuestra nave. Para ello añadiremos dos nuevos archivos a nuestro proyecto. En realidad en C/C++ no es necesario, se podría escribir todo es un único archivo, pero resultaría poco practico a la hora de buscar errores y de editar el código.

Añadiremos un archivo de cabecera a la carpeta Archivos de Cabecera ( Headers Files ), Para ello pulsaremos botón derecho del ratón sobre la carpeta e iremos a Añadir > Nuevo Elemento… ( Add > New Item… ), en la ventana que aparece elegimos Archivo de Cabecera(.h) ( Header File(.h) ) cambiaremos el nombre a Ship y como Localizacion ( Localization ) nos iremos a la carpeta include de nuestro proyecto, finalmente se pulsa Añadir( Add ).

Hacemos lo mismo para crear un archivo fuente en nuestra carpeta Archivos Fuente ( Source Files )


asteroid002


Lo primero que editaremos será el archivo Ship.h donde definiremos nuestra clase para las naves. La clase nave es un elemento que tiene posición y se puede dibujar, para crearla nos ayudaremos de dos clases que tiene SFML 2 para posicionar elementos ( sf::Transformable ) y dibujar elementos en la pantalla (  sf::Drawable ), La nave tendrá como representación una figura geométrica, para definirla usaremos la clase sf::ConvexShape que nos permite definir figuras geométricas.

Ship.h
#ifndef _GAME_SHIP_H_ //Protección contra la inclusión mas de una vez
#define _GAME_SHIP_H_ //que produces errores de redefinición,
      //Este método es mucho mejor que el #pragma one
      //sobre todo hay que evitar usar #pragma pues no son estándar

#include <SFML/Graphics.hpp> //Inclusión de las clases de SFML2 graficas

const float degree2radian = (3.14159f / 180.0f);

const float m_ship_drag_force = 3.0f; //Velocidad de resistencia contra el movimiento de la nave (Esto ara que se pare poco a poco cuando se deje de acelerar )
const float m_ship_aceleration = 10.0f; //Aceleración en pixel/segundo2 de la nave cuando se aplica empuje
const float m_ship_rotation_velocity = 90.0f; //Velocidad de rotación de la nave en grados/segundo

namespace game //Un namespace para evitar cualquier colisión de nombres accidental
{
 //Clase Ship derivada de Transformable y Drawable
 //La clase transformable nos proporciona métodos para girar y mover la nave
 class Ship: public sf::Transformable, public sf::Drawable
 {
 public:
  Ship();
  ~Ship();

  //Inicializa la nave a sus valores iniciales
  void reset();

  //Función que actualizara la lógica de la nave
  void update( float delta_time_seconds );

  //Definición de la función virtual de sf::Drawable
  void draw ( sf::RenderTarget &target, sf::RenderStates states ) const;

 protected:
  sf::Vector2f m_ship_velocity; //Velocidad de la nave

  sf::ConvexShape m_shipShape; //Representación gráfica de la nave
 };
};

#endif //_GAME_SHIP_H_

Como podéis observar he puesto una cuantas constantes para que resulte luego más fácil cambiar las características del movimiento. La función reset nos servirá tanto para inicializar la nave la primera vez que se cree, como para devolver la nave a la posición inicial cuando se inicie una nueva partida.

Ship.cpp
#include "Ship.h" //Inclusión de las definición de la clase nave

namespace game
{
 Ship::Ship()
 {
  //Define la figura que representara la nave
  m_shipShape.setPointCount( 3 ); //Será un triangulo
  m_shipShape.setPoint( 0, sf::Vector2f( 10.0f, 0.0f ) );
  m_shipShape.setPoint( 1, sf::Vector2f(-10.0f, 7.5f ) );
  m_shipShape.setPoint( 2, sf::Vector2f(-10.0f,-7.5f ) );
    
  m_shipShape.setFillColor( sf::Color( 0,0,0,0 ) );
  m_shipShape.setOutlineColor(sf::Color::White);
  m_shipShape.setOutlineThickness(2);
  m_shipShape.setPosition( 0.0f, 0.0f );

  reset();
 }

 Ship::~Ship()
 {
 }

 //Inicializa la nave a sus valores iniciales
 void Ship::reset()
 {
  setPosition( 320.0f, 240.0f ); //Lo colocamos en el centro de la pantalla
  setRotation( 0.0f );   //lo ponemos con giro cero
  m_ship_velocity.x = 0.0f;  //Lo paramos para que no se mueva
  m_ship_velocity.y = 0.0f;
 }

 //Función que actualizara la lógica de la nave
 void Ship::update( float delta_time_seconds )
 {
  //Comprobación del teclado
  if( sf::Keyboard::isKeyPressed( sf::Keyboard::Left  ) )    //Si pulsamos cursor izquierda
   rotate( -1.0f * m_ship_rotation_velocity * delta_time_seconds );//Rotamos contra las agujas del reloj

  if( sf::Keyboard::isKeyPressed( sf::Keyboard::Right  ) )   //Si pulsamos cursor derecha
   rotate( m_ship_rotation_velocity * delta_time_seconds );  //Rotamos dirección a las agujas del reloj

  if( sf::Keyboard::isKeyPressed( sf::Keyboard::Up ) )    //Si pulsamos cursor arriba
  {
   //Cogemos la dirección en la que mira la nave
   float rotation = getRotation();

   //Cogemos cual es su direccion en coordenadas cartesianas
   //NOTA - SFML trabaja en grados[0...360] y las ecuaciones matemáticas estándar trabajan en radianes[0...2*Pi]
   float cosRotation = std::cosf( rotation * degree2radian );
   float sinRotation = std::sinf( rotation * degree2radian );

   //Damos un acelerón a la nave
   m_ship_velocity.x += m_ship_aceleration * delta_time_seconds * cosRotation;
   m_ship_velocity.y += m_ship_aceleration * delta_time_seconds * sinRotation;
  }

  //Comprobamos si la nave se mueve
  //La velocidad es la longitud del vector velocidad
  float ship_speed = std::sqrtf( (m_ship_velocity.x * m_ship_velocity.x) + (m_ship_velocity.y * m_ship_velocity.y) );
  if( ship_speed > 0 ) //Si la nave se esta moviendo
  {
   //Vemos cual es la direccion del movimiento
   float angle_of_velocity = std::atan2f( m_ship_velocity.y/ship_speed , m_ship_velocity.x/ship_speed );

   //Aplico la resistencia en la direccion del movimiento
   m_ship_velocity.x -= m_ship_drag_force * delta_time_seconds * std::cosf( angle_of_velocity );
   m_ship_velocity.y -= m_ship_drag_force * delta_time_seconds * std::sinf( angle_of_velocity );

   //Compruebo la nueva velocidad
   ship_speed = std::sqrtf( (m_ship_velocity.x * m_ship_velocity.x) + (m_ship_velocity.y * m_ship_velocity.y) );

   //Si es menos que cero, paro la nave para que no retroceda
   if( ship_speed < 0.0f ) m_ship_velocity = sf::Vector2f( 0.0f, 0.0f );
   
   //Actualizo la posición de la nave
   move( m_ship_velocity * delta_time_seconds );

   //Comprueba la posición
   sf::Vector2f position = getPosition();

   //Si se sale por los laterales izquierdo o derecho
   //los muevo hasta el lado contrario
   if( position.x <= -10.0f ) position.x = 640.0f + 10.0f;
   else if( position.x >= 650.0f ) position.x = - 10.0f;

   //Si se sale por los laterales superior o inferior
   //los muevo hasta el lado contrario
   if( position.y <= -10.0f ) position.y = 480.0f + 10.0f;
   else if( position.y >= 490.0f ) position.y = - 10.0f;

   //NOTA - Los 10 pixel de diferencia con los limites de la pantalla
   //  son para que no se vea que cambiamos de posición, queda mucho
   //  mejor visualmente

   //Ponemos la posición que obtenemos
   setPosition( position );
  }
 }

 //Definición de la función virtual de sf::Drawable
 void Ship::draw ( sf::RenderTarget &target, sf::RenderStates states ) const
 {
  //Aplicamos a la transformación que nos viene la transformación que tiene la nave
  states.transform *= getTransform();

  //Dibujamos la representación gráfica de la nave con la transformación calculada
  target.draw( m_shipShape, states );

  //NOTA - Una transformación contiene la información de posición, rotación y escalado.
 }

};

Lo primero que hay que aclarar es la direccion de los ángulos en pantalla. El Angulo cero u origen de los ángulos es el eje x de la pantalla, y aumenta en la direccion de las ajugas del reloj, como se muestra a continuación.

angulos

Así, para que nuestra nave este alineada con los ángulos, de forma que simplifique el calculo de direcciones la tenemos que crear mirando hacia la derecha, esto es una regla general para todos los gráficos.

nave

En la función update usamos bastantes matemáticas y física ( si, necesitas saber matemáticas y física básica para programar videojuegos ). La física que se utiliza es cinemática básica.

Cuando se pulsa el cursor arriba, aplicamos la formula  velocidad = aceleración * tiempo, luego aplicamos una pequeña velocidad en contra del movimiento para frenar si se deja de acelerar para por ultimo desplazarnos con la formula distancia = velocidad * tiempo.

main.cpp
//Inclusión de la librería de gráficos de SFML 2
#include <SFML/Graphics.hpp>
#include "Ship.h"

//Punto de entrada de nuestro programa
int main()
{
 //Definición del tamaño de la ventana
    sf::RenderWindow window(sf::VideoMode(640, 480), "Asteroid by David Pulido Vargas (2012)");

 //Reloj para sincronizar las imágenes y que no se disparen los frames
 sf::Clock syncronice_timer;

 //El objeto nave de nuestro juego
 game::Ship space_ship;


 //Bucle principal de la aplicación
 //Mientras nuestra ventana sigua abierta
    while (window.isOpen())
    {
  //Recogemos cuanto tiempo a pasado en el frame anterior
  //y volvemos a poner el contador a cero
  float delta_time_seconds = syncronice_timer.restart().asSeconds();

  //Capturamos los eventos de la ventana
  //Como cerrar, se ha pulsado una tecla,
  //se ha movido el ratón, etc.
        sf::Event event;
        while (window.pollEvent(event))
        {
   //Si se ha pulsado cerrar ventana cerramos nuestra ventana
            if (event.type == sf::Event::Closed)
                window.close();
        }  

  //Actualizamos
  space_ship.update( delta_time_seconds ); //Actualizamos la nave según el tiempo transcurrido

  //Dibujamos
        window.clear();  //Limpiar

  window.draw( space_ship ); //Dibujamos la nave

        window.display(); //Mostrar en pantalla

  //Sincronizamos para que no vaya mas rápido que 30 fps
  while( syncronice_timer.getElapsedTime().asSeconds() < 1.0f / 30.0f )
   sf::sleep( sf::seconds( 1.0f/60.0f ) );
    }

 //Finalizamos nuestra aplicación
    return 0;
}

Por ultimo, modificamos main.cpp para incluir Ship.h, creamos dos objetos, un objeto sf::Clock, para controlar la velocidad de ejecución del juego, y un objeto game::Ship que es nuestra nave. Dentro del bucle principal solo tenemos que llamar a que se actualice la nave con update teniendo en cuenta el tiempo transcurrido desde el ultimo frame, y después simplemente dibujarlo en pantalla con draw.

Eso es todo por el momento, en la próxima entrada haremos que la nave dispare y crearemos los asteroides.

Gracias por vuestra atención. Un saludo

10 comentarios:

Mayli dijo...

Muchisimas gracias esto me esta sirviendo demasiado, saludos y que tengas un excelente dia.

Forseti dijo...

Muchas gracias por esta brutal explicación. Sigue así con el blog que ya lo tengo agregado a mis favoritos !!!

Anónimo dijo...

Gracias por tus explicaciones. Sigue así. Solo un detalle:
en Ship.h << (Esto ara que se pare poco a poco cuando se deje de acelerar ) >> Esto hará...

Unknown dijo...

Gracias por tu interes en el BLog. Ah!! Hache muda yo te maldigo XD

Francisco dijo...

Muy buen tutorial, me está siendo de gran utilidad.

Gracias!

Unknown dijo...

Enhorabuena por el tutorial, me esta viniendo genial.
Pero me sale un error al compilar en la linea 51 y 52. Me dice que cosf no es un miembro de std
No se si sabrás porque es, pero me vendría de mucha ayuda.
Muchas gracias

Unknown dijo...

Gracias por leer mi blog.
Es raro, la funcion std::cosf es estandar y deveria estar incluida en algun archivo de cabezera. Haz un #include a ver si asi te compila.

aguztynbassi dijo...

#include
using name space std;
// no te das una idea como te simplifica el codigo

El Hombre Almohada dijo...

Yo también tuve ese mismo problema y lo conseguí arreglar.
En el Ship.h, arriba donde los includes añade:

#include

El Hombre Almohada dijo...

Vaya, parece que el Blogger borra el include... pues pon, después del include (menor que)cmath(mayor que)