Las Balas
Las balas van a ser representadas por puntos en la pantalla, estos saldrán desde el vértice delantero de la nave y tendrán una velocidad y una vida, lo cual definirá cuanto espacio recorren. Como no tienen aceleración la actualización de las balas será muy sencillo.Lo primero que vamos a hacer es crear dos nuevos archivos, un archivo de cabecera en la carpeta include de nuestro proyecto y otro archivo fuente en la carpeta source, tan como hicimos en la entrada anterior con los archivos de la nave. Yo los he llamado Bullet.h y Bullet.cpp
En principio la clase Bullet es similar a la clase Ship, pero el hecho que sean representados por puntos implica que no podemos usar las clases tipo shape de SFML2. En vez de eso usaremos una de las funciones de la ventana que nos permite definir vértices y dibujarlos interpretando los vértices en función de la primitiva que le pasemos.
Así pues, la bala estará representada por un punto, que se dibujara en pantalla como una primitiva point. Veamos el código de cabecera.Nota:
Cuando dibujamos en un entorno 3D, aun simulando dibujar 2D como hace SFML2, en realidad todo son dibujos hecho por polígonos, así un sprite (un objeto dibujado a partir de una textura) no es mas que un cuadrado dibujado frente a la cámara 3D.
Un circulo o una figura convexa de SFML2 no es mas que un objeto que a la hora de dibujar genera una serie de cuadrados o polígonos para que al dibujarlos tengan la forma mas aproximada a la deseada.
OpenGL (SFML2) y DirectX tiene lo que se denominan primitivas de dibujado, que no es mas que dando a la tarjeta gráfica una serie de puntos (vértices) como tiene que dibujarlos, hay principalmente las siguiente primitivas de dibujado: puntos, líneas, líneas conectadas, triángulos, triángulos conectados y abanico de triángulos, OpenGL además tiene cuadrados, cuadrados conectados y polígonos.
En esta imagen puedes ver como se relaciona los puntos dentro de una lista con como se dibujan según la primitiva que le indiquemos. SFML2 solo tiene points, line, line strip, triangle, triangle strip, triangle fan y quad.
#ifndef _GAME_BULLET_H_ #define _GAME_BULLET_H_ #include <SFML/Graphics.hpp> #include <SFML/System.hpp> //Algunas constantes que nos ayudaran a regular como se comportan las balas const float bullet_live_seconds = 4.0f; //Vida maxima de una bala es segundos const float bullet_speed = 70.0f; //Velocidad de la bala en pixel / segundo const float bullet_shoot_speed_seconds = 0.5f; //frecuencia de disparo es segundos, cuanto tiempo tiene que pasar entre dos disparos seguidos namespace game { //La clase Bullet class Bullet: public sf::Transformable, public sf::Drawable { public: //La bala se creara a partir de una posición inicial //y una dirección en la que se mueve Bullet( sf::Vector2f& initial_position, float movement_angle ); //Actualizar la posición de la bala void update( float delta_time_seconds ); //Dibujar la posición de la bala void draw ( sf::RenderTarget &target, sf::RenderStates states ) const; //Esto nos indicara si hay que destruir el objeto bala bool isAlive(); protected: //Vector dirección de la bala sf::Vector2f m_direction_of_movement; //Los segundos que le quedan a la bala antes de ser destruida float m_remaining_live; //Indicador de si la bala sigue viva bool m_is_alive; }; } #endif //_GAME_BULLET_H_
Como podéis ver es muy similar a la clase Ship. El constructor indica que para crear la bala tendremos que decirle donde empieza, y que Angulo tiene. Luego hay una variable m_is_alive que nos servirá para determinas cuando hay que destruir un objeto Bullet.
#include "Bullet.h" //Constante para convertir grados a radianes para las funciones matemáticas const float degree2radian = (3.14159f / 180.0f); namespace game { Bullet::Bullet( sf::Vector2f& initial_position, float movement_angle ) : m_direction_of_movement( std::cosf( movement_angle * degree2radian), std::sinf( movement_angle * degree2radian ) ) //Usamos la trigonometría para saber cual es el vector de movimiento de la bala , m_remaining_live( bullet_live_seconds ) //Almacenamos cuanta vida le queda a la bala , m_is_alive( true ) //Marcamos como viva { setPosition( initial_position ); //Actualizamos la posición del padre transformable } void Bullet::update( float delta_time_seconds ) { if( !m_is_alive ) return; //Si la bala no esta viva no se actualiza //Realizamos la actualización de la vida de la bala m_remaining_live -= delta_time_seconds; //Si la vida resultante es menor que cero, se indica que la bala esta muerta if( m_remaining_live < 0 ) m_is_alive = false; //Actualizamos la posición de la bala S = V * T sf::Vector2f velocity = m_direction_of_movement * bullet_speed * delta_time_seconds; move( velocity ); //Hacemos que la bala este siempre en pantalla //Si se sale por un lado, entra por el otro. //Como hicimos con la nave sf::Vector2f position = getPosition(); if( position.x <= -1.0f ) position.x = 640.0f + 1.0f; else if( position.x >= 641.0f ) position.x = - 1.0f; if( position.y <= -1.0f ) position.y = 480.0f + 1.0f; else if( position.y >= 481.0f ) position.y = - 1.0f; setPosition( position ); } void Bullet::draw( sf::RenderTarget &target, sf::RenderStates states ) const { //Como es un punto lo dibujaremos con la función que permite //Dibujar figuras geométricas usando la primitiva punto. sf::Vertex v( getPosition(), sf::Color::White ); target.draw( &v, 1, sf::Points ); } bool Bullet::isAlive() { return m_is_alive; } }
En los comentarios del archivo fuente están indicados lo pasos que se dan. En la función update, básicamente se le resta vida a la bala y se marca como muerta si es necesario, se actualiza la posición según el vector de dirección de movimiento, la constante velocidad y el tiempo transcurrido desde la ultima actualización, y se asegura que la bala este siempre en pantalla.
El dibujado es simple, se crea un vertex con la posición de la bala y se dibuja un solo punto.
NOTA:
Seria mucho mas eficiente generar los puntos de todas las balas y dibujarlas todas a la vez, pero como estamos empezando quiero primar la sencillez a la eficiencia.
Los asteroides
Los asteroides van a ser círculos, esto simplificara el calculo de las colisiones en la siguiente entrada. Por ello al igual que hicimos con la nave, yaceremos un sf::CirlceShape como representación gráfica de los asteroides.
En realidad no hay nada nuevo en la clase Rock. Tiene un indicador como la bala para saber cuando hay que destruir el objeto, tienen una serie de características como nivel que puede ser 0, 1, 2 y que tendrá influencia en como se comporta y se dibuja. Por lo demás, se crea con un origen y una dirección y se mueve en esa dirección.
Igual que con la nave y las balas crearemos dos nuevos archivos, uno de cabecera y otro fuente. Yo los he llamado Rock.h y Rock.cpp. Veamos el archivo de cabecera:
#ifndef _GAME_ROCK_H_ #define _GAME_ROCK_H_ #include <SFML/Graphics.hpp> //Algunas constantes para retocar el comportamiento de los asteroides //Hay tres niveles de asteroides Grande(0), mediano(1) y pequeño(2) //Por eso uso arrays para las constantes de control const float rock_speed[3] = { 40.0f, 60.0f, 80.0f }; const float rock_radius[3] = { 30.0f, 15.0f, 7.0f }; namespace game { //Como la nave este objeto tendrá posición y se podrá dibujar directamente //He elegido que sea un circulo para simplificar las colisiones posteriormente //Pero como en la nave podríamos haber creado un convexshape con una forma irregular class Rock:public sf::Transformable, public sf::Drawable { public: //Se crea en una posición, con una dirección de movimiento y un nivel que por defecto es Grande(0) Rock( sf::Vector2f& initial_position, float movement_angle, sf::Uint8 rock_level = 0 ); //Actualizar el asteroide void update( float delta_time_seconds ); //Dibujar el asteroide void draw ( sf::RenderTarget &target, sf::RenderStates states ) const; //Indicar si sigue vivo o hay que destruirlo bool isAlive(); protected: //Variable que indica que nivel tiene sf::Uint8 m_rock_level; //Objeto que será la representación gráfica sf::CircleShape m_rockShape; //Vector dirección del movimiento sf::Vector2f m_direction_of_movement; //Indicador de si esta vivo bool m_is_alive; }; } #endif //_GAME_ROCK_H_
Lo mas interesante es el uso de array de constantes para contener la velocidad y tamaño de los asteroides según su nivel.
Ahora veamos el código fuente:
#include "Rock.h" //Variable para convertir de grados a radianes en las funciones matemáticas const float degree2radian = (3.14159f / 180.0f); namespace game { //Creación a partir de un origen y una dirección con un nivel Rock::Rock( sf::Vector2f& initial_position, float movement_angle, sf::Uint8 rock_level ) : m_direction_of_movement( std::cosf( movement_angle * degree2radian), std::sinf( movement_angle * degree2radian ) ) , m_is_alive( true ) , m_rock_level( rock_level ) { setPosition( initial_position ); //Creamos la figura circulo y la centramos según el radio de la roca m_rockShape.setRadius( rock_radius[ m_rock_level ] ); m_rockShape.setFillColor( sf::Color( 0, 0, 0, 0xFF) ); m_rockShape.setOutlineColor( sf::Color::White ); m_rockShape.setOutlineThickness(2); //Por defecto el origen de es la esquina superior izquierda //pero nosotros queremos que este centrada, así que //movemos el origen según el radio que tiene el asteroide m_rockShape.setOrigin( rock_radius[ m_rock_level ], rock_radius[ m_rock_level ] ); } void Rock::update( float delta_time_seconds ) { if( !m_is_alive ) return; //Si el asteroide no esta vivo no se actualiza //Actualizamos la posición sf::Vector2f velocity = m_direction_of_movement * rock_speed[ m_rock_level ] * delta_time_seconds; move( velocity ); //Hacemos que siempre estén en pantalla //Usamos el radio de la roca para determinar cuando se hace el salto //de forma que el salto nunca sea visible al usuario sf::Vector2f position = getPosition(); if( position.x <= -rock_radius[ m_rock_level ] ) position.x = 640.0f + rock_radius[ m_rock_level ]; else if( position.x >= 640.0f + rock_radius[ m_rock_level ] ) position.x = - rock_radius[ m_rock_level ]; if( position.y <= -rock_radius[ m_rock_level ] ) position.y = 480.0f + rock_radius[ m_rock_level ]; else if( position.y >= 480.0f + rock_radius[ m_rock_level ] ) position.y = - rock_radius[ m_rock_level ]; setPosition( position ); } void Rock::draw( sf::RenderTarget &target, sf::RenderStates states ) const { //Dibujamos la figura en función de la transformación del objeto Rock states.transform *= getTransform(); target.draw( m_rockShape, states ); } bool Rock::isAlive() { return m_is_alive; } }
No hay nada de especial en el código que no hallamos comentado antes.
Empezando a disparar
Hasta ahora hemos creado la clase Bullet y la clase Rock, es el momento de integrarlas para que los veamos en acción en nuestro juego.
Lo primero que hay que tener en cuenta es que no sabemos con antelación cuantas balas, ni cuantos asteroides van a haber en pantalla durante el juego, así que no podemos crear una array estático. Para ello acudiremos a las librerías estadas de C++, las STL. Dentro de estas librerías hay dos contenedores que tienen la características que necesitamos, el contenedor std::vector y el contenedor std::list. Ambos pueden añadir y eliminar elementos de forma dinámica, de modo que no necesitamos saber cuanto espacio necesitamos, simplemente añadimos y eliminamos según necesitemos.
Es mejor usar la std::list por que es mas eficiente a la hora de eliminar objetos por el medio de la lista y como no sábenos a priori que bala abra que destruir o que asteroide es mejor usar la lista para mantener todos las balas y asteroides.
#include "Ship.h" #include "Bullet.h" //A- Inclusión de la clase Bullet #include "Rock.h" //B - Inclusión de la clase Rock //A - Definición para mejorar la comprensión del código typedef std::list<game::Bullet*> BulletList; typedef std::list<game::Bullet*>::iterator BulletIndex; //B - Definición para mejorar la comprensión del código typedef std::list<game::Rock*> RockList; typedef std::list<game::Rock*>::iterator RockIndex;
Lo primero que haremos será incluir los archivos de cabecera de la bala y de los asteroides. Luego , para simplificar el código vamos a definir la lista de punteros a balas como BulletList y el iterador a ese tipo de lista como BulletIndex. Lo mismo se hará con la lista de punteros a asteroides como se ve en el código.
//El objeto nave de nuestro juego game::Ship space_ship; //A - Contenedor de todas las balas disparadas BulletList bullets; float time_to_next_bullet = 0.0f; //A - Control de la frecuencia de disparo //B - Contenedor de rodas los asteroides RockList rocks;
A continuación vamos a crear dentro de la función main una lista de balas y una lista de asteroides, también se crea una variable time_to_next_bullet que nos servirá para controlar la frecuencia de disparo.
//A - Rutina de disparo cuando se pulsa espacio & se puede disparar time_to_next_bullet -= delta_time_seconds; if( sf::Keyboard::isKeyPressed( sf::Keyboard::Space ) && time_to_next_bullet < 0.0f) { time_to_next_bullet = bullet_shoot_speed_seconds; //Reset del control de frecuencia de disparo //Calculo dela posición de la bala en función de la posición y dirección de la nave sf::Vector2f position = space_ship.getPosition(); float movement_angle = space_ship.getRotation(); //Movemos la posición de la bala hasta el morro de la nave position += 10.0f * sf::Vector2f( std::cosf( movement_angle * degree2radian ), std::sinf( movement_angle * degree2radian ) ); //Creamos una nueva bala y la almacenamos game::Bullet* newBullet = new game::Bullet( position, movement_angle ); if( newBullet ) bullets.push_back( newBullet ); }
La rutina de disparo se hará después de comprobar los eventos del sistema, aquí lo que haremos será quitar tiempo a time_to_next_bullet Luego comprobaremos que se ha pulsado la tecla espacio y si se puede disparar otra bala. Si se puede disparar reseteamos la variable que controla la frecuencia de disparo, calculamos la posición del vértice delantero de la nave usando la dirección de la nave y la longitud de la misma ( 10 pixel ). Finalmente creamos la nave en la posición del morro de la nave y con la dirección que tiene la nave en ese momento y lo insertamos al final de la lista de balas.
//A - Actualización de las balas BulletIndex I = bullets.begin(); BulletIndex E = bullets.end(); while( I != E ) { game::Bullet* bullet = (*I); if( bullet->isAlive() ) //Si la bala sigue viva se actualiza y se pasa a la siguiente { bullet->update( delta_time_seconds ); ++I; } else //Si la bala ha muerto se elimina y se pasa a la siguiente { delete bullet; I = bullets.erase( I ); } };
Después de actualizar la nave, actualizaremos las balas. Para ello recorreremos toda la lista de balas comprobando si están vivas, si están vivas se actualiza según el tiempo desde la ultima actualización y se pasa al siguiente, sino esta viva se borra el objeto y se elimina de la lista.
NOTA:
Hay que asegurarse siempre que por cada new que se hace en el código hay un delete del objeto.Si no empieza a quedar agujeros de memoria (memory leaks).
//A - Recorremos la lista de balas y las dibujamos I = bullets.begin(); E = bullets.end(); while( I != E ) { game::Bullet* bullet = (*I); window.draw( *bullet ); ++I; }
Después de dibujar la nave, se dibujan las balas, recorriendo la lista y dibujando en la pantalla.
Se hará algo parecido con los asteroides.
//C - Flag de reseteado del juego bool need_reset_game = true; //Bucle principal de la aplicación //Mientras nuestra ventana sigua abierta while (window.isOpen()) { //C - Primer reseteado del juego if( need_reset_game ) { need_reset_game = false; //Reseteo de la nave space_ship.reset(); //Elimino todos los disparos BulletIndex I = bullets.begin(); BulletIndex E = bullets.end(); while( I != E ) { game::Bullet* bullet = (*I); delete bullet; ++I; }; bullets.clear(); //Elimino todos los asteroides RockIndex RI = rocks.begin(); RockIndex RE = rocks.end(); while( RI != RE ) { game::Rock* rock = (*RI); delete rock; ++RI; }; rocks.clear(); //Crea nuevos asteroides game::Rock* newRock = new game::Rock( sf::Vector2f( std::rand()%640, std::rand()%480 ), std::rand()%360 ); game::Rock* newRock2 = new game::Rock( sf::Vector2f( std::rand()%640, std::rand()%480 ), std::rand()%360 ); game::Rock* newRock3 = new game::Rock( sf::Vector2f( std::rand()%640, std::rand()%480 ), std::rand()%360 ); game::Rock* newRock4 = new game::Rock( sf::Vector2f( std::rand()%640, std::rand()%480 ), std::rand()%360 ); rocks.push_back( newRock ); rocks.push_back( newRock2 ); rocks.push_back( newRock3 ); rocks.push_back( newRock4 ); }
Vamos a usar la variable need_reset_game para controlar la creación de asteroides, esta nos servirá para cuando tengamos que reiniciar el juego mas adelante. Cuando hay que reiniciar el juego se reseteare la posición de la nave, se eliminaran todas las Balas y todos los asteroides que existan en ese momento y se crearan por el momento cuatro asteroides en puntos aleatorios y con direcciones aleatorias.
//B - Actualización de los asteroides RockIndex RI = rocks.begin(); RockIndex RE = rocks.end(); while( RI != RE ) { game::Rock* rock = (*RI); if( rock->isAlive() ) //Si esta vivo se actualiza { rock->update( delta_time_seconds ); ++RI; } else //Si no esta vivo se borra { delete rock; RI = rocks.erase( RI ); } };
Luego solo hay que actualizar como se hizo con las balas y posteriormente dibujar:
//B - Recorremos la lista de asteroides y los dibujamos RI = rocks.begin(); RE = rocks.end(); while( RI != RE ) { game::Rock* rock = (*RI); window.draw( *rock ); ++RI; }
Exactamente igual que las balas. Tenéis mi proyecto con el código fuente de la que hay hasta ahora aquí.
Con esto tenemos ya algo que se parece al juego de asteroides, aunque aun no se puede dispara ni chocar con las rocas. Eso será en mi próxima entrada, donde calcularemos las colisiones, haremos que se rompan los asteroides y crearemos unas pequeñas explosiones.
Gracias por vuestra atención, nos vemos pronto.
7 comentarios:
De nuevo, excelente tutorial y me esta siendo muy util para iniciarme en el desarrollo de videojuegos, a parte de ir acostumbrandome a la sintaxis y peculiaridades de C++, nunca he programado en C++, pero si en Java y algo de C.
Hola! Muy buenos tus tutoriales, son de mucha ayuda. Pero tengo un problema, no me reconoce las listas!
std::list
eso me da error...¿Qué librería tengo que añadir? ¿Como lo hago?
Un saludo.
Hola, gracias por tu interes.
Si ha copiado el codigo de arriba puede que te de problemas por que no esta todo el codigo completo (si te descargas el 7z de la leccion si funciona)
Faltan algunas cabeceras por incluir. Concretamente faltan
#include
#include
#include
#include
#include
en main.cpp, por eso puede que te diga que no sabe que es std::list.
Un saludo
Exacto, me faltaba uno de los includes.
#include
Ahora funciona perfecto. Muchas gracias.
Se agradecen tus tutoriales, y más en algo tan complejo como la creación de juegos.
Saludos.
El repositorio no existe mas =(((
El link te lleva a box, hay te dice que no hay acceso directo al archivo (intentare mirar porque). Aparece otro link desde donde se puede descargar el archivo 7z con el codigo y el proyecto de vs2010.
Gracias por tu interes.
Bueno compañero, lo he descargado como dices pero no lo conseguí compilar. Uso el codeblocks y tira muchos errores.
Aun así gracias.
Me quedo con las explicaciones ;)
Tu Blog esta muy bien, pena que no hagas mas tutoriales como este. :)
Saludos (y)
Publicar un comentario