martes, 11 de junio de 2013

Segundo Proyecto: Programando los Estados del Juego

Voy a ver si aceleramos las cosas que empiezo a meter demasiada prosa en este proyecto. En esta entrada voy a programar por encima todos los estados del juego y a mostrar como se hace con la librería que empezamos en la entrada anterior.

Refrescando la memoria

Este es el diagrama de flujo de los estados del juego que ya comentamos en una entrada anterior. Lo pongo aquí de nuevo simplemente para recordad que es lo que vamos ha hacer.
Como todavía no tenemos gráficos para la interfaz gráfica usaremos texto para mostrar los datos.
MenuFlow
Empecemos por el menú principal. Crearemos un nuevo filtro dentro de la carpeta Archivos de Cabecera (Header Files), para ello pulsamos botón derecho del ratón y seleccionamos “"añadir > Nuevo Filtro” (Add > New Filter). Esto crea una nueva carpeta que llamaremos States.

Propiedades del proyecto Verticalshooter

En la anterior entrada me quedaron algunas cosas en el tintero de la configuración del proyecto VerticalShooter para que funcione bien con SFMLX.
Hay que ir a C/C++ > General > Directorio de inclusión adicional (C/C++ > General > Additional include directories) y añadir la ruta a la carpeta include de SFMLX (../../../InternalLib/SFMLX/include).
En Linkado > General > Directorio de librerías adicional (Linker > General > Additional library directories) hay que poner la ruta a la carpeta lib de SFMLX (../../../InternalLib/SFMLX/lib)
En Linkado > Entrada > Dependencias adicionales (Linker > Input > Additional dependencies) hay que añadir la librería de SFMLX ( SFMLX_d.lib en DEBUG y SFMLX.lib en RELEASE )

gameState

SOBRE LOS FILTROS

Las carpetas que se crean dentro del explorador del proyecto en Visual Studio no son carpetas reales dentro del disco duro. Esta es una forma de organizar los ficheros del proyecto para hacerlos mas manejables.
Si se quiere además tener mejor organizado los archivos en el disco duro tendrás que hacerlo desde el explorados de Windows.
Añadiremos siete archivos de cabecera a al nuevo filtro de cabecera, uno por cada estado de nuestro diagrama. Para eso pulsamos el botón derecho del ratón (BDR) sobre la carpeta y elegimos añadir > nuevo archivo (add > new item.) En el dialogo que aparece elegimos archivo de cabezera (Header File) y cambiamos la ruta a la carpeta include d nuestro proyecto.
Los archivos tendrán los nombres:
  • Maistate
  • SelectState
  • CreditState
  • ExitState
  • PlayState
  • PauseState
Y básicamente serán todos iguales  excepto por la macro del principio del archivo y el nombre de la clase, todos los estados son parecidos. Según programemos unos u otros iremos cambiando la programación para introducir gráficos y elementos de la interfaz gráficas.

MainState y su menú

MainState.h
#ifndef _GAME_MAINSTATE_H_
#define _GAME_MAINSTATE_H_

#include "GameState.h"
#include <SFML\Graphics.hpp>

namespace game
{
 class MainState: public xf::GameState
 {
 public:
  MainState();

  void init();
  void destroy();

  void onEvent( sf::Event& event );

  bool update();
  void draw();

 protected:
  sf::Text m_title;
  sf::Text m_menu[4];
  unsigned m_menuOption;
  bool m_execute;
 };
}

#endif

Es muy simple definir un nuevo estado. Es una simple clase con todas las funciones virtuales implementadas. Las variables miembro protegidas son el meollo de la cuestión. Como podéis comprobar son textos, un titulo y cuatro opciones, así cono un “cursor” que es m_menuOption que llevara el control de que opción de menu estamos seleccionando, así mismo hay un booleano m_execute que le dirá a update si tiene que ejecutar o no la opción seleccionada.

MainState.cpp
#include "MainState.h"
#include "GameStateManager.h"
#include "main.h"

namespace game
{
 MainState::MainState()
  : m_menuOption(0)
  , m_execute(false)
 {
  //Titulo del estado actual
  //Si miráis la documentación 
  //de SFML podréis ver que hace cada función
  m_title.setFont(*g_mainFont);
  m_title.setCharacterSize(30);
  m_title.setStyle(sf::Text::Regular);
  m_title.setString( "Main Menu" );

  //Esta parte es el centrado del texto
  sf::FloatRect textSize = m_title.getLocalBounds();

  m_title.setOrigin( textSize.width/2.f, textSize.height/2.f);
  m_title.setPosition( g_screenWidth/2.f, 10.f );

  // ---------------------------------------------------------------
  //Primera opción del menú, seleccionar un nivel
  m_menu[0].setFont(*g_mainFont);
  m_menu[0].setCharacterSize(20);
  m_menu[0].setStyle(sf::Text::Regular);
  m_menu[0].setString( "Select Level" );

  textSize = m_menu[0].getLocalBounds();

  m_menu[0].setOrigin( textSize.width/2.f, textSize.height/2.f);
  m_menu[0].setPosition( g_screenWidth/2.f, 100.f );

  //-------------------------------------------------------------------
  //Segunda opción del menú, opciones del programa
  m_menu[1].setFont(*g_mainFont);
  m_menu[1].setCharacterSize(20);
  m_menu[1].setStyle(sf::Text::Regular);
  m_menu[1].setString( "Options" );

  textSize = m_menu[1].getLocalBounds();

  m_menu[1].setOrigin( textSize.width/2.f, textSize.height/2.f);
  m_menu[1].setPosition( g_screenWidth/2.f, 130.f );

  //-------------------------------------------------------------------
  //Tercera opción del menú, mostrar los créditos
  m_menu[2].setFont(*g_mainFont);
  m_menu[2].setCharacterSize(20);
  m_menu[2].setStyle(sf::Text::Regular);
  m_menu[2].setString( "Credits" );

  textSize = m_menu[2].getLocalBounds();

  m_menu[2].setOrigin( textSize.width/2.f, textSize.height/2.f);
  m_menu[2].setPosition( g_screenWidth/2.f, 160.f );

  //-------------------------------------------------------------------
  //Cuarta opción del menú, salir
  m_menu[3].setFont(*g_mainFont);
  m_menu[3].setCharacterSize(20);
  m_menu[3].setStyle(sf::Text::Regular);
  m_menu[3].setString( "Quit" );

  textSize = m_menu[3].getLocalBounds();

  m_menu[3].setOrigin( textSize.width/2.f, textSize.height/2.f);
  m_menu[3].setPosition( g_screenWidth/2.f, 190.f );  
 }

 void MainState::init()
 {
  //Al iniciar el estado se pone la opción a 0 (que es la primera opción)
  m_menuOption = 0;
 }

 void MainState::destroy()
 {
  //Como no se han creado punteros ni se han cargado recursos
  //Aquí no hace falta nada de momento
 }

 void MainState::onEvent( sf::Event& event )
 {
  //Se comprueba el teclado y se actúa en consecuencia
  //Otra opción seria tener un event como variable global
  //que se leyera desde update y se actuara en consecuencia
  m_execute = false;
  if(event.type == sf::Event::KeyReleased)
  {
   switch( event.key.code )
   {
   case sf::Keyboard::Up:
    --m_menuOption;
    break;
   case sf::Keyboard::Down:
    ++m_menuOption;
    break;
   case sf::Keyboard::Return:
    m_execute = true;
    break;
   };
  }
 }

 bool MainState::update()
 {
  //Para resaltar la opción actual se le pone estilo subrayado
  //al m_menu que corresponda
  for( unsigned i = 0; i < 4; ++i)
  {
   m_menu[i].setStyle( i!=m_menuOption ? sf::Text::Regular : sf::Text::Underlined );
  }

  //Si se ha pulsado enter hay que actuar
  if( m_execute )
  {
   switch(m_menuOption)
   {
   case 0:
    //Ir al estado SelectState
    //xf::GameStateManager::instance()->pushState( new SelectState );
    break;
   case 1:
    //Ir al estado OptionState
    //xf::GameStateManager::instance()->pushState( new OptionState );
    break;
   case 2:
    //Ir al estado CreditState
    //xf::GameStateManager::instance()->pushState( new CreditState );
    break;
   case 3:
    //Ir al estado ExitState
    //xf::GameStateManager::instance()->pushState( new ExitState );
    return false; //De momento devolvemos false en update que hace que se cierre el programa
    break;
   }
  }

  return true;
 }

 void MainState::draw()
 {
  //Se manda dibujar en la ventana
  g_mainWindow->draw( m_title );
  for( unsigned i = 0; i < 4; ++i)
  {
   g_mainWindow->draw( m_menu[i] );
  }
 }
}

Esta bastante comentado así que podréis ver que se hace. Lo único que aquí no veis es como queda el main.cpp y de donde salen g_mainWindow y g_mainFont.

Main.h, una cabecera para incluirlas a todas.


En un intento de simplificar las cosas he creado un archivo de cabecera main.h que incluirá todas las variables globales, macros, y archivos de cabecera que vallamos necesitando, de forma que con incluirlo en los archivos será suficiente para tener acceso a todo lo que necesitemos de nuestro juego. Es romper un poco las reglas, pero en videojuegos a veces nos tenemos que pasar las reglas por la papelera de reciclaje (XD)
#ifndef _GAME_MAIN_H_
#define _GAME_MAIN_H_

//Aquí pondremos todas las variables globales
//que se puedan necesitar.
namespace game
{
 extern sf::Font* g_mainFont;
 extern sf::RenderWindow* g_mainWindow;

 const int g_screenWidth = 800;
 const int g_screenHeight = 600;
}

//Esto agiliza la inclusion de los distintos archivos de cabezera
//que son necesarios para poder hacer la transición entre estados
//del juego
#include "CreditState.h"
#include "ExitState.h"
#include "MainState.h"
#include "OptionState.h"
#include "PauseState.h"
#include "PlayState.h"
#include "SelectState.h"

#endif

main.cpp

#include <SFML/Graphics.hpp>

#include "main.h"
#include "GameStateManager.h"

//Declaración de las variables globales
namespace game
{
 sf::Font* g_mainFont;
 sf::RenderWindow* g_mainWindow;
}

int main()
{
 //Iniciamos la ventana principal y conservamos un puntero a ella para usarla en otros sitios
 sf::RenderWindow window(sf::VideoMode(game::g_screenWidth, game::g_screenHeight), "Vertical Shooter");
 game::g_mainWindow = &window;

 //Creamos la fuente (he usado un puntero en vez de hacer moco antes porque existe un fallo
 //en SFML si creas la fuente como global al salir del programa se produce una excepción
 game::g_mainFont = new sf::Font;

 //Se trata de cargar una fuente, el archivo arial.ttf debe estar en la carpeta data
 if (!game::g_mainFont->loadFromFile("arial.ttf")) 
 {
  //Si no puedes cargar la fuente se acaba el programa
  delete game::g_mainFont;
  return 1;
 }

 //Iniciamos el manager de estados poniendo el estado mainstate
 xf::GameStateManager::instance()->pushState( new game::MainState );
 
 //El bucle principal de la ventana
    while (window.isOpen())
    {
  //Recogemos los eventos del sistema operativo
        sf::Event event;
        while (window.pollEvent(event))
        {
   //Y se los pasamos al manager que se los pasara al estado que corresponda
   xf::GameStateManager::instance()->onEvent( event );

   //Si recibimos cerrar la ventana, pues la cerramos
            if (event.type == sf::Event::Closed)
                window.close();
        }

  //Si al ejecutar el update del estado
  if( xf::GameStateManager::instance()->update() )
  {
   //En caso de ser cierto pintamos
   window.clear();
   xf::GameStateManager::instance()->draw();        
   window.display();
  }
  else
  {
   //Si es falso cerramos la ventana
   window.close();
  }
    }

 //Liberamos la memoria de la fuente
 delete game::g_mainFont;

 //Y salimos con normalidad
    return 0;
}

Aunque todavía no haga mucho, los principios básicos ya están mostrados, ahora toca ir estado por estado creándolos y completando la librería interna con las clases que nos irán ayudando a crear nuestros juegos.

Gracias por vuestro tiempo. Un saludo

Actulizacion: Este es el link al proyecto con lo ultimo

2 comentarios:

Francisco dijo...

Buen artículo, me aclara un poco como van funcionando lo de los estados de un juego.

Me surge una duda, más que nada por curiosidad, ¿por qué es necesario declarar g_mainFont y g_mainWindow como punteros?. En un principio no me parecían objetos que fuera necesario guardar su referencia en memoria.

Cuando declaras un objeto, en caso de que no sepas si vas a tener que actualizar su valor en otras funciones del código, ¿es aconsejable declararlas como punteros ante la duda?


Unknown dijo...

Hola Francisco, gracias por tu interes en mi blog.
Sobre los punteros.
El puntero a g_mainWindow es para poder hacer referencia a el en otros sitios del programa, es decir solo una referencia.
Con respecto a g_mainFont es en parte para tener una referencia en otras partes del codigo, y, si lees el comentario, es porque el objeto sf::Font tiene un fallo y si lo creo como objeto al salir salta una excepcion (un error).

Espero haber aclarado tus dudas.