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:
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.
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.
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 )
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.
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.
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:
Muchisimas gracias esto me esta sirviendo demasiado, saludos y que tengas un excelente dia.
Muchas gracias por esta brutal explicación. Sigue así con el blog que ya lo tengo agregado a mis favoritos !!!
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á...
Gracias por tu interes en el BLog. Ah!! Hache muda yo te maldigo XD
Muy buen tutorial, me está siendo de gran utilidad.
Gracias!
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
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.
#include
using name space std;
// no te das una idea como te simplifica el codigo
Yo también tuve ese mismo problema y lo conseguí arreglar.
En el Ship.h, arriba donde los includes añade:
#include
Vaya, parece que el Blogger borra el include... pues pon, después del include (menor que)cmath(mayor que)
Publicar un comentario