GDE: Arranque y Parada

GDE: Arranque y Parada
Sin comentarios Facebook Twitter Flipboard E-mail

Vamos a empezar con el primer artículo técnico dedicado al proyecto GDE. Como sabéis la intención de este proyecto es la de aprender a desarrollar los elementos básicos de un Game Engine 2D o de un videojuego en general por un método práctico y colaborativo. En este primer artículo vamos a ver como vamos a gestionar el arranque y parada del motor.

La clase App

Con nuestro motor no vamos a reinventar la rueda, por lo que siempre es bueno mirar el modelo que tienen otros motores consolidados para inspirarte. Nuestra clase App representa lo que en, por ejemplo, Ogre3D sería la clase Root ó en la saga de versiones de Cocos2D sería la clase Director.

La clase App por tanto se trata de una clase con el patrón Singleton. Para los no versados, decir que se trata de una clase que tiene una única instancia en nuestro programa. Esto lo hacemos estableciendo el constructor y el destructor como privados para que no se puedan crear nuevos objetos y definiendo unos métodos que nos permitan crear u obtener el objeto (En nuestro motor se llaman instance y release).

El método intance comprueba si existe la instancia única del objeto, si no existe, lo crea y devuelve un puntero a dicha instancia. Por el contrario, si existe, devuelve el puntero a dicha instancia por lo que siempre que llamamos al método instance de la clase App nos devuelve un puntero al mismo objeto se llame desde donde se llame.

class GDE_API App : sf::NonCopyable
{
static App* uniqueInstance;
public:
static App* instance();
static void release();
private:
App();
~App();
}; // class App
view raw App.hpp hosted with ❤ by GitHub

Lo más extraño de esta simple interfaz puede ser que antes del nombre de la clase tenemos la macro GDE_API, es usada por el proyecto para crear las clases en modo exportable para Visual Studio, nada de lo que preocuparse. Por otra parte como vemos hereda de la clase sf::NonCopyable de la biblioteca SFML, esta clase no es más que una clase que tiene definidos el Constructor copia y el operador de asignación como privados, al heredar de ella el constructor copia y el operador de asignación quedan como privados en nuestra clase, algo que tiene todo el sentido tratándose de un Singleton.

Arrancando el motor: métodos básicos

Ahora que tenemos establecida nuestra clase App como un Singleton debemos empezar a aportar funcionalidad. La clase App será la encargada de arrancar y parar nuestro motor por la que su función es arrancar todos los subsistemas y contener el bucle principal del juego, el llamado Game Loop. Por último debe realizar tareas de limpieza al terminar.

Clase App

Sus tareas quedan definidas entonces, en primer lugar arranca todos los subsistemas necesarios como pueden ser el Gestor de recursos, el gestor de escenas, Logs, etc.

Luego crea la ventana ya que la clase App será la que contenga la ventana de nuestra aplicación y la gestiones. La crearemos en base a un fichero de configuración llamado window.cfg que se encontrará en el directorio del ejecutable.

Una vez creada la ventana, si todo ha ido bien entramos en el Game Loop que es un bucle que se ejecuta durante toda la vida de la aplicación y del que hablaremos en el próximo artículo cuando abordemos la integración del Scene Manager.

Por último, cuando se sale del Game Loop, se entra en un método llamado cleanup que tendrán muchas clases de nuestro Engine y se encargará de hacer las tareas de limpieza: liberar memoria, apagar los subsistemas, etc.

Un esquema general de la clase App con estas cosas sería el siguiente:

class GDE_API App : sf::NonCopyable
{
static App* uniqueInstance;
public:
/// Constante width por defecto
static const unsigned int DEFAULT_VIDEO_WIDTH = 640;
/// Constante height por defecto
static const unsigned int DEFAULT_VIDEO_HEIGHT = 480;
/// Constante bpp por defecto
static const unsigned int DEFAULT_VIDEO_BPP = 32;
static App* instance();
static void release();
sf::RenderWindow& getWindow();
bool isRunning() const;
void quit(sf::Int16 theExitCode);
void setFirstScene(Scene* scene);
sf::Int16 run();
private:
/// Ventana principal de la aplicación
sf::RenderWindow window;
/// Verdadero si la aplicación se está ejecutando
bool running;
/// Código de salida de la aplicación
sf::Int16 exitCode;
/// Puntero al SceneManager
SceneManager* sceneManager;
/// Escena inicial. Punto de entrada de la aplicación
Scene* initialScene;
App();
~App();
void createWindow();
void init();
void gameLoop();
void cleanup();
}; // class App
view raw App.hpp hosted with ❤ by GitHub

Vemos que contiene un sf::RenderWindow que será la ventana de nuestra aplicación. Otros atributos que contiene son running que será true mientras el motor deba estar activo y exitCode que almacena el valor de salida de la aplicación. Los atributos referidos a Scene y SceneManager los comentaremos en el siguiente artículo ya que tienen que ver con la gestión de escenas.

Podemos observar los métodos de los que hemos hablado: init, createWindow, gameLoop y cleanup son privados. Esto es así porque todos ellos se lanzan en el orden esperado a través del método run de la interfaz pública.

Hay otros métodos públicos que son utilidades como getWindow que devuelve una instancia a la ventana, isRunning que devuelve el valor del atributo running o quit que establace running a false y el exitCode pasado por parámetro.

En cuanto a la implementación, aún le queda mucho que revisar y cosas que añadir como gestión de subsistemas en el método init o un Fixed Time adecuado en el Game Loop, pero antes de que se complique podéis mirar como es la implementación actual con lo que aquí se explica y el SceneManager que será lo próximo que abordemos.

#include <GDE/Core/App.hpp>
#include <GDE/Core/Log.hpp>
#include <GDE/Core/StringsUtil.hpp>
#include <GDE/Core/ConfigReader.hpp>
#include <GDE/Core/ConfigCreate.hpp>
#include <fstream>
static std::ofstream logFile;
namespace GDE
{
App* App::uniqueInstance = 0;
App::App()
: window()
, exitCode(GDE::StatusNoError)
, running(false)
, initialScene(NULL)
{
// Se abre el archivo donde se guardará el loggin
logFile.open("log.txt", std::ios::app);
// Se inicializa el sistema de loggin con el archivo
GDE::Log::init(logFile);
GDE_LOG_INFO("App: constructor llamado");
}
App::~App()
{
// Termina el sistema de loggin antes de cerrar el archivo
GDE::Log::close();
GDE_LOG_INFO("App: destructor llamado");
}
App* App::instance()
{
if (uniqueInstance == 0)
{
uniqueInstance = new App();
}
return uniqueInstance;
}
void App::release()
{
if (uniqueInstance)
{
delete uniqueInstance;
}
uniqueInstance = 0;
}
sf::RenderWindow& App::getWindow()
{
return this->window;
}
bool App::isRunning() const
{
return this->running;
}
void App::quit(sf::Int16 theExitCode)
{
this->exitCode = theExitCode;
this->running = false;
}
void App::setFirstScene(Scene* scene)
{
if (this->initialScene == NULL)
{
initialScene = scene;
GDE_LOG_INFO("App::setFirstScene(): establecida escena inicial ID =" << scene->getID());
}
else
{
GDE_LOG_WARNING("App::setFirstScene(): ya se ha establecido una escena inicial");
}
}
sf::Int16 App::run()
{
// Establecemos running a true para arrancar la aplicación
this->running = true;
// Crea la ventana de la aplicación
this->createWindow();
// Arranca todos los subsistemas necesarios
this->init();
// Entra en el Bucle del juego hasta que runnig sea false
this->gameLoop();
// Se encarga de la limpieza y cerrar todos los subsistemas
this->cleanup();
// Escribimos en el log el código de salida
GDE_LOG_DEBUG("App: Código de salida: " << this->exitCode);
// Salimos con el código de salida generado
return this->exitCode;
}
void App::init()
{
// Se crea la instancia única del SceneManager
sceneManager = GDE::SceneManager::instance();
// Establecemos la escene inicial
if (initialScene != 0)
{
// Añadimos la primera escena
this->sceneManager->addScene(this->initialScene);
// La establecemos como escena activa
this->sceneManager->setActiveScene(this->initialScene->getID());
this->sceneManager->changeScene(this->sceneManager->nextScene);
}
else
{
GDE_LOG_ERROR("App::init(): no se ha establecido escena inicial. LLamar a App::setFirstScene() primero");
// Salimos con código -2
quit(GDE::StatusAppInitFailed);
}
GDE_LOG_DEBUG("App::init(): completado");
}
void App::createWindow()
{
// Creamos un modo de video con los valores por defecto
sf::VideoMode videoMode(this->DEFAULT_VIDEO_WIDTH, this->DEFAULT_VIDEO_HEIGHT, this->DEFAULT_VIDEO_BPP);
// Creamos un estilo por defecto
sf::Int32 style = sf::Style::Default;
// Establecemos fullscreen a false
bool fullscreen = false;
// Valor inicial del redisionamiento de la ventana
bool resize = true;
// Activamos la sincronización vertical por defecto
bool vsync = true;
GDE::ConfigReader conf;
// Si existe un archivo de configuración lo usamos para cargar los datos
if(conf.loadFromFile("window.cfg")) // FIX: Deberá ser argado con el gestor de recursos
{
// Comprobamos si esta activado el modo fullscreen
if (conf.getBool("window", "fullscreen", false))
{
fullscreen = true;
// Si esta en modo fullscreen obtenemos la resolución del escritorio
videoMode = sf::VideoMode::getDesktopMode();
// Establecemos el estilo de fullScreen
style = sf::Style::Fullscreen;
}
else
{
// Establecemos la configuración obtenida del archivo
videoMode.width = conf.getUint32("window", "width", this->DEFAULT_VIDEO_WIDTH);
videoMode.height = conf.getUint32("window", "height", this->DEFAULT_VIDEO_HEIGHT);
videoMode.bitsPerPixel = conf.getUint32("window", "bpp", this->DEFAULT_VIDEO_BPP);
// Comprobamos si la ventana se puede redimensionar
if (!conf.getBool("window", "resize", true))
{
resize = false;
style = sf::Style::Close | sf::Style::Titlebar;
}
}
// Comprobamos si vsync está activado
vsync = (conf.getBool("this->window", "vsync", true));
}
// Creamos la ventana con los valores resulantes
this->window.create(videoMode, "Genbeta Dev Engine", style);
// FIX: Hay que dar opción también a usar setFrameLimit
this->window.setVerticalSyncEnabled(vsync);
// Escribimos la configuración en el archivo
GDE::ConfigCreate newConf;
newConf.open("window.cfg");
newConf.putSection("window");
newConf.putValue("width", GDE::convertInt32(videoMode.width));
newConf.putValue("height", GDE::convertInt32(videoMode.height));
newConf.putValue("bpp", GDE::convertInt32(videoMode.bitsPerPixel));
newConf.putValue("fullscreen", GDE::convertBool(fullscreen));
newConf.putValue("resize", GDE::convertBool(resize));
newConf.putValue("vsync", GDE::convertBool(vsync));
newConf.close();
// Escribimos en el log
GDE_LOG_INFO("App::createWindow(): ventana creada");
GDE_LOG_INFO("Modo de Video:" << GDE::Log::nospace
<< videoMode.width << "x" << videoMode.height << "x" << videoMode.bitsPerPixel);
GDE_LOG_INFO("Vsync:" << vsync);
GDE_LOG_INFO("Fullscreen:" << fullscreen);
}
void App::gameLoop()
{
// Bucle mientras se está ejecutando y la ventana está abierta
while (this->isRunning() && this->window.isOpen())
{
// Gestionamos los eventos de la aplicación
sf::Event event;
while (window.pollEvent(event))
{
switch (event.type)
{
case sf::Event::Closed: // La ventana es cerrada
quit(StatusAppOK); // FIX: Dar la opción de parar el evento a la escena
break;
case sf::Event::GainedFocus: // La ventana obtiene el foco
this->sceneManager->resumeScene();
break;
case sf::Event::LostFocus: // La ventana pierde el foco
this->sceneManager->pauseScene();
break;
default: // Otros eventos se los pasamos a la ecena activa
this->sceneManager->eventScene(event);
} // switch (event.Type)
} // while (window.GetEvent(event))
// Llamamos al método Update() de la escena activa
this->sceneManager->updateScene();
// Establecemos el color de fondo de limpieza
this->window.clear(sceneManager->getActiveScene()->getBackgroundColor());
// Llamamos al método Draw() de la escena activa
this->sceneManager->drawScene();
// Actualizamos la ventana
this->window.display();
// Comprobamos cambios de escena
if (this->sceneManager->handleChangeScene())
{
// Cambiamos el puntero de la escena activa
this->sceneManager->changeScene(this->sceneManager->nextScene);
}
}
}
void App::cleanup()
{
// Se elimina todas las escenas
sceneManager->removeAllScene();
// Eliminamos el SceneManager
sceneManager->release();
sceneManager = NULL;
GDE_LOG_DEBUG("App::cleanup(): completado");
}
} // namespace GDE
view raw App.cpp hosted with ❤ by GitHub

Como vemos la implementación ya integra el sistema de Log, La creación de la ventana desde el fichero, creándolo si no existiera y la inicialización del SceneManager.

Como ya hemos dicho en el siguiente artículo técnico explicaremos la integración del SceneManager y como funciona el GameLoop.

En Genbeta Dev | Proyecto GDE En Github | Genbeta Dev Engine

Comentarios cerrados
Inicio
×

Utilizamos cookies de terceros para generar estadísticas de audiencia y mostrar publicidad personalizada analizando tu navegación. Si sigues navegando estarás aceptando su uso. Más información