viernes, 24 de mayo de 2013

Segundo Proyecto: Creando La Maquina de Estados del Juego

Como primer hito de la programación de este proyecto vamos a crear un nuevo proyecto de VS2010 y ha empezar por crear una maquina de estados para el juego. La maquina de estados del juego es un poco diferente a la maquina de estados de un objeto normal.

GameStateMachine: Apilando estados

Nuestra maquina de estados del juego estará compuesta por dos clases distintas. Por una parte esta el manejador de estados que se encargara de mantener el estado actual del juego y ejecutar el código adecuado y por otra estarán cada uno de los estados en los que se puede encontrar el juego.

Los estados del juego serán:

  • Menú principal
  • Selección de nivel
  • Juego
  • Pausa
  • Opciones
  • Créditos
  • Confirmación de Salir

Como podéis comprobar estos corresponden con el esquema de la primera entrada de esta serie.

La diferencia que existirá entre esta maquina de estados (Finite State Machine – FSM) es que en la maquina de juegos vamos a apilar los estados. La principal razón es que normalmente siempre vamos a un estado (Opción de menú) y luego volvemos al estado anterior.

Manager de Estados

En la entrada anterior creamos una librería interna para extender SFML 2.0. Será ahí donde programemos la base de la maquina de estados, de forma que podamos reutilizarla en los siguientes proyectos. Según vayamos creando juegos iremos ampliando y crearemos nuevas librerías de forma que cada vez iremos mas rápido.

El sistema estará formado por dos clases. Una virtual pura servirá de base común a todos los estados del juego, mientras que la segunda será el manager que se encargue de pasar de un estado a otro y de ejecutar el código del estado que corresponda. Muy simple.

GameState.H

//----------------------------------------------------------------------------
// File: GameState.h
// Description: Interface for the GameState abstract class.
//----------------------------------------------------------------------------
#ifndef _XF_GAMESTATE_H´_
#define _XF_GAMESTATE_H_

#include <SFML/Window/Event.hpp>

namespace xf
{
class gameState
{
public:
// initialize the state
virtual void init() = 0;
// shutdown the state
virtual void destroy() = 0;

// handle window event
virtual void onEvent( sf::Event& event ) {};

// handle frame update
virtual bool update() = 0;
// handle frame rendering
virtual void draw() = 0;
};
}

#endif

Como podeis comprobar tiene cuatro metodos que son de obligada implementacion en las clases derivadas que sirver para inicializar el estado, actualizarlo, dibujarlo y al final eliminar lo que sea necesario al salir del estado.


El metodo OnEvent sirve para cuando necesitemos manejar los eventos del sistema como redimensionar la pantalla, ir a segundo plano, etc.. Como no es virtual puro ( no tiene al final = 0) no hace falta que lo implementemos si no lo necesitamos.


GameStateManager.H


//----------------------------------------------------------------------------
// File: StateManager.h
// Description: Interface for the StateManager class.
//----------------------------------------------------------------------------
#ifndef _XF_STATEMANAGER_H_
#define _XF_STATEMANAGER_H_

#include <vector>
#include <SFML/Window/Event.hpp>

namespace xf
{
// forward declaration for GameState class
class gameState;

//----------------------------------------------------------------------------
// Class: StateManager
// Description: Manages game states for the engine.
//----------------------------------------------------------------------------
class gameStateManager
{
public:

// Return the singleton StateManager instance
static gameStateManager* instance();

// Destructor
~gameStateManager();

// Push a new state onto the stack
void pushState(gameState* state);

// Pop the current state off the stack
gameState* popState();

// Remove all states from the stack
void popAll();

// Call the current state's onEvent method
void onEvent( sf::Event& event );

// Call the current state's update method
bool update();

// Call the current state's render method
void draw();

private:
// Constructor
gameStateManager();

std::vector<gameState*> _states;

// Delete all Popped State
void delPopStates();
std::vector<gameState*> _popped_states;
};
}
#endif

GameStateManager.cpp

//----------------------------------------------------------------------------
// File: GameStateManager.cpp
// Description: Implementation for the GameStateManager class.
//----------------------------------------------------------------------------
#include "GameStateManager.h"
#include "GameState.h"

namespace xf
{
//----------------------------------------------------------------------------
// Method: getInstance
// Description: Gets the singleton StateManager instance.
// Returns: A pointer to the StateManager instance.
//----------------------------------------------------------------------------
gameStateManager* gameStateManager::instance()
{
static gameStateManager _instance;
return &_instance;
}

//----------------------------------------------------------------------------
// Method: Constructor
//----------------------------------------------------------------------------
gameStateManager::gameStateManager()
{
}

//----------------------------------------------------------------------------
// Method: Destructor
//----------------------------------------------------------------------------
gameStateManager::~gameStateManager()
{
// if there's anything left in the _states vector, then we have a
// memory leak. it's up to the calling application to make sure
// the states are cleaned up and deleted.
popAll();
delPopStates();
}

//----------------------------------------------------------------------------
// Method: pushState
// Description: Push a new state onto the state stack. Calls the state's
// initialize method.
//----------------------------------------------------------------------------
void gameStateManager::pushState(gameState* state)
{
state->init();
_states.push_back(state);
}

//----------------------------------------------------------------------------
// Method: popState
// Description: Pops the current state off the state stack. Calls the state's
// shutdown method. Accumutale in temporal stack to delete.
// Returns: The state that was popped.
//----------------------------------------------------------------------------
gameState* gameStateManager::popState()
{
gameState* state = 0;

if ( !_states.empty() )
{
state = _states.back();
_states.pop_back();
state->destroy();
_popped_states.push_back( state );
}

return state;
}

//----------------------------------------------------------------------------
// Method: delPopStates
// Description: Delete the Pops state in the popped stack.
//----------------------------------------------------------------------------
void gameStateManager::delPopStates()
{
gameState* state = 0;

while ( !_popped_states.empty() )
{
state = _popped_states.back();
_popped_states.pop_back();
delete state;
}
}

//----------------------------------------------------------------------------
// Method: popAll
// Description: Pops all states from the state stack. Note that the states
//----------------------------------------------------------------------------
void gameStateManager::popAll()
{
while (popState())
;
}

//----------------------------------------------------------------------------
// Method: update
// Returns: If there is a current state, then it returns the return value
// from the current state's update method. Otherwise, it returns
// false.
//----------------------------------------------------------------------------
bool gameStateManager::update()
{
delPopStates();
if ( !_states.empty() )
{
return _states.back()->update();
}
else
{
return false;
}
}

//----------------------------------------------------------------------------
// Method: render
// Description: Calls the current state's draw method.
//----------------------------------------------------------------------------
void gameStateManager::draw()
{
if ( !_states.empty() )
{
_states.back()->draw();
}
}

//----------------------------------------------------------------------------
// Method: onEvent
// Description: Calls the current state's render method.
//----------------------------------------------------------------------------
void gameStateManager::onEvent( sf::Event& event )
{
if ( !_states.empty() )
{
_states.back()->onEvent( event );
}
}
}

Como podeis ver en el código el manager de estados es un singleton, es decir una clase que solo puede tener una copia en memoria. Existen infinidad de formas de programar un singleton, yo uso la forma que Buckland describe en su libro “Programming Game AI by example”.


Básicamente consiste en crear una clase con una metodo estático, existe siempre y de forma independiente a cualquier objeto de la clase, y el destructor publico. De forma privada se definen los constructores de la clase, de forma que no se puedan usar desde fuera de la clase (no se puede hacer new )


El metodo estático define a su vez una objeto estático de la clase, este metodo puede tener acceso al constructor porque pertenece a la clase. Una variable u objeto estático dentro de un metodo o función es como si fuera una variable global, se crea la primera vez y mantiene sus datos al salir y volver a la función o metodo.


Al terminar el programa las variable y objetos estáticos que se hayan creado se destruyen automáticamente.


Respecta a la funcionalidad, podeis comprobar que básicamente es un vector al que se le mete y sacan punteros de tipo GameState desde el final. También llama a las funciones de init antes de insertar un GameState y llama a destroy al sacarlo de la cola. También veréis que la eliminación (delete) de los punteros lo lleva a cabo el propio manager, pero no lo hace al sacarlo de la cola, eso podría dar problemas de sincronización, sino que lo hace en el siguiente update del manager.





NOTA


Todo lo que sea crear y destruir objetos dentro del juego debe hacerse de forma coordinada o podrían dar fallo de memoria (intentar consultar datos de un objeto destruido ) o de sincronización (que no se obtengan buenas colisiones). Por eso la creación y la destrucción de objetos se hace al principio o al final de la actualización siguiente a detectar que hay que destruir o crear.


 



Creando el Proyecto


Como hicimos con el proyecto de Asteroides, vamos a copiar el proyecto plantilla y a cambiar el nombre de la carpeta por VerticalShooter, un nombre de lo mas original.


Ahora vamos a incluir nuestro proyecto de SFMLX para poder programar la librería a la vez que programamos el juego.


AddIL



Desde el dialogo de archivo vamos a la carpeta build de SFMLX y añadimos el proyecto.



NOTA


Me he encontrado que tengo que renombrar el archivo de proyecto porque con el nombre build me da conflicto con el proyecto del juego. Así que he borrado de la carpeta SFMLX/build todo menos el .sln y el .vcxproj, luego he renombrado el .vcxproj de build a SFMLX y al abrir la solución me da que no encuentra el proyecto. Eliminamos el proyecto que no encuentra y añadimos el nuevo proyecto renombrado.


Os dejo todo el proyecto con la librería interna y el proyecto de VerticalShooter aquí


Gracias por vuestra atención.

No hay comentarios: