lunes, 26 de mayo de 2008

Un ejemplo de IApplication

Con este artículo concluimos el bloque de la estructura de menus Irrlicht. Sin más preámbulos, vamos a ver el código y clases necesarias para tener una aplicación basica para nuestro proyecto.


Empezamos con el main, en la que instanciamos un objeto de tipo LSim.
#include <iostream>
#include "LSim.h"

using namespace std;

int main()
{
LSim *app = new LSim();
return app->mRun();
}


Yo no se vosotros, pero adoro los mains así de limpitos ;). La clase LSim es la encargada de instanciar el IrrlichtDevice. A su vez, instancia un objeto MainApplication (el menú principal) pasándole al constructor el device. A continuación vemos la declaracion de LSim.

class LSim
{
public:
LSim();
virtual ~LSim();
bool mRun();

protected methods:
bool mInit();
// bool Loop();

protected:
IrrlichtDevice *f_device;
MainApplication *mainApp;
};


Y su implementación. No pongo el constructor y destructor porque estan vacios.

bool
LSim::mRun()
{
if(!mInit()) return false;

mainApp = new MainApplication(f_device);
mainApp->mRun();
return true;
}

bool
LSim::mInit()
{
try
{
// create device and exit if creation failed
dimension2d<s32> resolucion(640,480);
bool fullScreen = false;
u32 bpp = 32;
f_device = createDevice(video::EDT_OPENGL, resolucion,bpp,fullScreen);
if (f_device == NULL) return false;
return true;
}
catch(std::exception &e)
{
std::cerr<<"Error: "<<e.what()<<std::endl;
}
return false;
}


Acabamos de ver cómo se llama a MainApplication. Veamos ahora la declaración de MainApplication.

class MainApplication : public IApplication
{
public:
MainApplication(IrrlichtDevice *device);
virtual ~MainApplication();
virtual bool mInit();
virtual bool mRemove();

//events
static void mButtonQuit_Click(void* o, const SEvent& event);
static void mButtonNewWindow_Click(void *o, const SEvent & event);
static void mButtonFileOpen_Click(void *o, const SEvent & event);
static void mScrollBar_Changed(void *o, const SEvent & event);

// static void mSubButton_Click(void *o, const SEvent & event);

static void mButtonAddLogo_Click(void* o, const SEvent& event);
static void mButtonRemoveLogo_Click(void *o, const SEvent & event);
static void mButtonLaunchEditor_Click(void *o, const SEvent & event);
static void mButtonLaunchSimulator_Click(void *o, const SEvent & event);

protected:
EditorApplication *f_editor;
SimulatorApplication *f_simulador;

//controls
IGUIContextMenu * f_menu;
IGUIButton *f_buttonQuit;
IGUIButton *f_buttonNewWindow;
IGUIButton *f_buttonFileOpen;
IGUIButton *f_buttonAddLogo;
IGUIButton *f_buttonRemoveLogo;
IGUIButton *f_buttonLaunchEditor;
IGUIButton *f_buttonLaunchSimulator;
IGUIStaticText *f_staticTextTransparentControl;
IGUIScrollBar *f_scrollbar;
IGUIStaticText *f_staticTextLogger;
IGUIListBox *f_listBoxLogger;
IGUISkin* f_skin;
IGUIFont* f_font;
IGUIImage* f_ImageLogo;
IGUIStaticText *f_staticTextFPS;
};


Observa como esta clase ya se asemeja a una formulario de M$Windows, con sus controles, eventos, etc. Además tiene dos objetos especialmente interesantes, el editor y el simulador, que también heredan de IApplication. De hecho, creo que tenia que haberlos declarado como IApplication, mea culpa. Veamos ahora la implementación. Sólo voy a poner los metodos importantes, mInit y mLoop, además de dos eventos importantes, los que lanzan al editor y al simulador respectivamente.

bool
MainApplication::mInit()
{
f_eventReceiver = new EventReceiver(this);
f_device->setEventReceiver(f_eventReceiver);

//Inicializacion de controles
f_menu = f_env->addMenu(0, f_genIDs->getNewID());
f_buttonQuit = f_env->addButton(rect<s32>(10,210,100,240), 0, f_genIDs->getNewID(), L"Quit");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonQuit->getID(), MainApplication::mButtonQuit_Click);
f_buttonNewWindow = f_env->addButton(rect<s32>(10,250,100,290), 0, f_genIDs->getNewID(), L"New Window");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonNewWindow->getID(), MainApplication::mButtonNewWindow_Click);
f_buttonFileOpen = f_env->addButton(rect<s32>(10,300,100,340), 0, f_genIDs->getNewID(), L"File Open");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonFileOpen->getID(), MainApplication::mButtonFileOpen_Click);

f_buttonAddLogo = f_env->addButton(rect<s32>(10,350,100,370), 0, f_genIDs->getNewID(), L"Add Logo");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonAddLogo->getID(), MainApplication::mButtonAddLogo_Click);
f_buttonRemoveLogo = f_env->addButton(rect<s32>(10,380,100,400), 0, f_genIDs->getNewID(), L"Remove Logo");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonRemoveLogo->getID(), MainApplication::mButtonRemoveLogo_Click);

f_buttonLaunchEditor = f_env->addButton(rect<s32>(10,410,100,430), 0, f_genIDs->getNewID(), L"Editor");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonLaunchEditor->getID(), MainApplication::mButtonLaunchEditor_Click);
f_buttonLaunchSimulator = f_env->addButton(rect<s32>(10,440,100,460), 0, f_genIDs->getNewID(), L"Simulator");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonLaunchSimulator->getID(), MainApplication::mButtonLaunchSimulator_Click);

f_staticTextTransparentControl = f_env->addStaticText(L"Transparent Control:", rect<s32>(150,20,350,40), true,true,0,f_genIDs->getNewID(),false);

f_scrollbar = f_env->addScrollBar(true,rect<s32>(150, 45, 350, 60), 0, f_genIDs->getNewID());
f_scrollbar->setMax(255);
f_scrollbar->setPos(127);
f_eventReceiver->mAddGuiEvent(EGET_SCROLL_BAR_CHANGED, f_scrollbar->getID(), MainApplication::mScrollBar_Changed);
f_staticTextLogger = f_env->addStaticText(L"Logging ListBox:", rect<s32>(50,80,250,100), true,true,0,f_genIDs->getNewID());

f_listBoxLogger = f_env->addListBox(rect<s32>(50, 110, 250, 180),0,f_genIDs->getNewID());

f_skin = f_env->getSkin();
f_font = f_env->getFont("media/fonthaettenschweiler.bmp");
if (f_font) f_skin->setFont(f_font);
f_ImageLogo= f_env->addImage(
f_driver->getTexture("media/irrlichtlogo.png"),
position2d<int>(f_driver->getScreenSize().Width - 128,f_driver->getScreenSize().Height- 105));

//Anyadimos el texto de los fps
f_staticTextFPS = f_env->addStaticText(L"", core::rect<s32>(10,10,100,26), true, false, 0,f_genIDs->getNewID());

// IGUIWindow* subventana = f_env->addWindow(rect<s32>(0,0,800,600), true,L"titulo de la subventana",0,2000);
// IGUIButton *subboton = f_env->addButton(rect<s32>(50,50,50+100,50+32), subventana, 2001, L"Quit");
return true;
}

bool
MainApplication::mRemove()
{
f_menu->remove();
f_buttonLaunchSimulator->remove();
f_buttonLaunchEditor->remove();
f_buttonQuit->remove();
f_buttonNewWindow->remove();
f_buttonFileOpen->remove();
f_buttonAddLogo->remove();
f_buttonRemoveLogo->remove();
f_staticTextTransparentControl->remove();
f_scrollbar->remove();
f_staticTextLogger->remove();
f_listBoxLogger->remove();
if(f_ImageLogo) f_ImageLogo->remove();
f_staticTextFPS->remove();
return true;
}

void
MainApplication::mButtonLaunchEditor_Click(void *o, const SEvent & event)
{
MainApplication* me = (MainApplication*)o;
me->mRemove();
me->f_editor = new EditorApplication(me->f_device);
me->f_editor->mRun();
me->mInit();
}

void
MainApplication::mButtonLaunchSimulator_Click(void *o, const SEvent & event)
{
MainApplication* me = (MainApplication*)o;
me->mRemove();
me->f_simulador = new SimulatorApplication(me->f_device);
me->f_simulador->mRun();
me->mInit();
}

El método mInit es el encargado de inicializar los controles y configurar a f_eventReceiver. Después de él, viene mRemove, encargado de retirar todos los controles y de asear el IrrlichtDevice. Esto lo hacemos así porque no podemos eliminar ni reasignar el objeto IGUIEnvironment del IrrlichtDevice, con lo que cada objeto IAplicación que recibe el IrrlichtDevice, pone sus controles y eventos y al terminar los quita del mismo.

Fijaos en los eventos que lanzan al editor y al simulador. Antes de instanciar, por ejemplo, el editor, se invoca a mRemove de MainApplication, para que al instanciar el editor, éste reciba el IrrlichrtDevice limpio y aseado. Tambien puede ocurrir que no sea necesario quitar todos los controles o incluso mantener algunos.

El EditorApplication y SimulatorApplication son parecidos a MainApplication, salvo que cada uno tendrá sus controles y funcionalidad.

Bueno, con esto hemos terminado el bloque de Estructura de menus Irrlicht. Conforme he ido escribiendo estas dos últimas entradas me he ido dando cuenta de algunos defetos de diseño, como el tema de IApplication como interfaz pura y otras interfaces que se podrían implementar. Será cuestion de corregislas.

Se me olvidaba deciros que en la cajita que tenemos de Box.Net hay un ejemplo que pusimos al principio de tener el blog. si lo pruebas podrás ver el menu principal y te permitira pasar al simulador o al editor. en el simulador presiona la tecla 'c' para liberar el puntero del raton y poder pulsar en el boton para volver al menú principal.

Un saludo.

Sigue leyendo >

La clase interfaz de ventana IApplication

Seguimos hoy con el bloque que trata de la estructura de ventanas con Irrlicht. Ahora nos toca ver la interfaz que nos permitirá implementar las distintas secciones de la aplicación.


Veamos la declaración de la clase.
#include <irrlicht/irrlicht.h>
#include "EventReceiver.h"
#include "GeneraID.h"
#include "FPSManager.h"

using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;

class IApplication
{
public:
IApplication(IrrlichtDevice *device);
virtual ~IApplication();
virtual bool mRun();
virtual bool mInit() = 0;
virtual bool mRemove() = 0;
virtual bool mLoop();
bool getRunLoop();
void setRunLoop(bool value);

protected:
virtual bool mPreDraw();

protected:
IrrlichtDevice *f_device;
EventReceiver *f_eventReceiver;
GeneraID *f_genIDs;
IVideoDriver *f_driver;
IGUIEnvironment *f_env;
ISceneManager *f_smgr;

bool f_runLoop;
IFPSManager *f_FPSManager;
};


He de decir que me equivoqué con el nombre, no tenía que haberse llamado IApplication, puesto que no es una interfaz pura, tiene propiedades y metodos implementados (lo siento por los puristas del diseño ;)

En su parte privada vemos que tiene punteros a objetos que ya comentamos, como el IrrlichtDevice, EventReceiver, IFPSManager; otros de Irrlicht como IVideoDriver, IGUIEnvironment e ISceneManager y uno de cosecha propia, el GeneraID.
Los objetos de irrlicht vereis que son extraidos del IrrlichtDevice para usarlos con mayor comodidad.

En cuanto a GeneraID, vereis que es necesario para, como su nombre indica, generar identificadores únicos para los controles del GUI. Recordais el ejemplo típico del EventReceiver de los tutoriales? También hay que destacar que para que los id's sean verdaderamente únicos, solo puede haber una instancia de la clase. Esto se consigue con el patron de diseño singleton. He aqui su declaración:
#include <iostream>
#include <irrlicht/irrlicht.h>

using namespace irr;

class GeneraID
{
public:
static GeneraID* getInstance();
s32 getNewID();
s32 getLastID();
virtual ~GeneraID();

protected:
GeneraID(s32 initial = 0);
s32 f_id;
static GeneraID* s_instancia;
};

Y su implementación.
#include"GeneraID.h"
GeneraID* GeneraID::s_instancia = 0;

using namespace irr;

GeneraID*
GeneraID::getInstance()
{
if(!s_instancia) s_instancia = new GeneraID();
return s_instancia;
}

GeneraID::GeneraID(s32 initial)
{
f_id = initial >= 0 ? initial : 0;
}

GeneraID::~GeneraID()
{
}

s32
GeneraID::getNewID()
{
return ++f_id;
}

s32
GeneraID::getLastID()
{
return f_id;
}


volviendo a IApplication, en la parte pública, tenemos métodos implementados como mRun, mLoop, y los métodos get y set del atributo f_runLoop. mRun y mLoop nos permiten ejecutar el bucle de ejecución de la aplicación y mediante f_runLoop, podemos parar la ejecución del mismo.

Veamos ahora la implemetación de IApplication.
#include "IApplication.h"

IApplication::IApplication(IrrlichtDevice *device)
{
f_device = device;
f_driver = f_device->getVideoDriver();
f_env = f_device->getGUIEnvironment();
f_smgr = f_device->getSceneManager();
f_genIDs = GeneraID::getInstance();
f_eventReceiver = NULL;
f_runLoop = true;
f_FPSManager = new FPSManager(f_device);
}

IApplication::~IApplication()
{
f_device = NULL;
f_driver = NULL;
f_env = NULL;
f_smgr = NULL;
f_genIDs = NULL;
f_runLoop = false;
if(f_FPSManager != NULL)
delete f_FPSManager;
f_FPSManager = NULL;
}

bool
IApplication::mRun()
{
if(!mInit()) return false;
if(!mLoop()) return false;
if(!mRemove()) return false;
return true;
}

bool
IApplication::mLoop()
{
while(f_runLoop && f_device->run() && f_driver)
{
f_FPSManager->mUpdateFPS();
if (f_device->isWindowActive())
{
f_FPSManager->mDrawFPS();
mPreDraw();

f_driver->beginScene(true, true, SColor(0,122,65,171));
f_smgr->drawAll();
f_env->drawAll();
f_driver->endScene();
}
else
f_device->yield();
}

//f_device->drop();
return true;
}

bool
IApplication::getRunLoop()
{
return f_runLoop;
}

void
IApplication::setRunLoop(bool value)
{
f_runLoop = value;
}

bool
IApplication::mPreDraw()
{
return true;
}


Podemos observar que el contructor no tiene nada especial, tan solo que recibe el IrrlichtDevice y recupera de él, el videoDriver, GUIEnvironment y SceneManager.

El metodo mLoop es el interesante. Pese a que es un bucle típico de los tutoriales de Irrlicht, le hemos agregado un par de cosillas, tales como la gestion de los FPS y en método mPreDraw(), que lo implementarán las clases sucesoras si hiciese falta. mPreDraw() surgió como necesidad en el simulador, que necesita procesar instucciones ODE antes de dibujar en ventana.

En la próxima entrega veremos ejemplos de uso de IApplication con el menú principal, el editor y el simulador.

Sigue leyendo >

Controlando la tasa de frames por segundo

En esta entrada explicaremos cómo controlar la cantidad de frames por segundo que tendrá nuestra aplicación Irrlicht.


Se que más de uno se preguntará ¿por qué limitar los FPS? Cuantos mas mejor ¿no? La respuesta es sí y no. Veréis, a partir de los 30fps ya no notamos diferencia de fluidez, luego, no es necesario que la cpu y gpu curren al 100%. Si limitamos la tasa de frames a 30, evitaremos el recalentamiento de los chips y eso, en portatiles se agradece mucho: menos consumo de bateria (si fuera el caso), menos calor, menos movimiento de ventilador y por tanto, menos acumulación de polvo, etc.

Asi pues, nuestra clase aplicacion tendrá un objeto que sea el encargado de introducir pausas limitar los fps. En caso de que el equipo no llegue al limite, no se introducirían pausas.

Para hacerlo bonito, lo haremos al estilo IS1. Implementaremos una interfaz que tendrá los métodos minimos y necesarios para que cualquier clase que implemente dicha interfaz, pueda controlar sin problemas los fps.

He aquí la clase interfaz IFPSManager. Vemos que tiene dos metodos a implementar por las clases que hereden de ésta, mUpdateFPS() y mDrawFPS().

class IFPSManager
{
public:
IFPSManager();
IFPSManager(IrrlichtDevice *device);
virtual ~IFPSManager();

virtual void mUpdateFPS() = 0;
virtual void mDrawFPS() = 0;
};



Y ahora la declaración de la clase FPSManager que implementa la interfaz.
#include "IFPSManager.h"

using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;

class FPSManager : public IFPSManager
{
public:
FPSManager();
FPSManager(IrrlichtDevice *device);
virtual ~FPSManager();

virtual void mUpdateFPS();
virtual void mDrawFPS();

protected:
IrrlichtDevice *f_device;
IVideoDriver *f_driver;

s32 fFPSMax;
f32 fFPSMaxMiliSeconds;
f32 fPauseInDraw;
};



No nos olvidemos de la implementación. La forma de calcular la pausa entre frames aún no es definitiva, pero gracias a la interfaz, podemos escribir otro gestor de fps sin tener que desechar este FPSManager.
#include "FPSManager.h"

FPSManager::FPSManager()
{
f_device = NULL;
f_driver = NULL;
fFPSMax = 0;
fFPSMaxMiliSeconds = 0.0;
fPauseInDraw = 0.0;
}

FPSManager::FPSManager(IrrlichtDevice *device)
{
f_device = device;
f_driver = f_device->getVideoDriver();
fFPSMax = 25;
fFPSMaxMiliSeconds = 1/(f32)fFPSMax;
fPauseInDraw = 0.0;
}

FPSManager::~FPSManager()
{
f_device = NULL;
fFPSMax = 0;
fFPSMaxMiliSeconds = 0.0;
fPauseInDraw = 0.0;
}

void
FPSManager::mUpdateFPS()
{
if (f_driver->getFPS() > fFPSMax)
fPauseInDraw = (fFPSMaxMiliSeconds - 1.0/(f32)f_driver->getFPS()) * 1000.0;
f_device->sleep((u32)fPauseInDraw);
}

void
FPSManager::mDrawFPS()
{
core::stringw str(core::stringw(f_driver->getFPS()));
str.append(L"fps");
f_device->setWindowCaption(str.c_str());
}



Hasta aquí el gestor de frames por segundo. En la proxima entrega hablaremos de la Interfaz de ventana IApplication.



Sigue leyendo >

domingo, 25 de mayo de 2008

Dividiendo la aplicación en ventanas

Buenas, en esta entrada vamos a empezar a explicar la forma de implementar nuestra aplicación Irrlicht de modo que podamos dividirla en varias zonas o ventanas, en nuestro caso, un menú principal, el editor y el simulador.


Una forma de hacerlo sería instanciando y cerrando el IrrlichtDevice *device para "pasar" de una ventana a otra, pero no quedaría nada bien. Sería como si las aplicaciones de ventana habituales se cerraran y abrieran en vez de cambiar su contenido. Así que es precisamente eso lo que haremos, cambiar el contenido de la ventana principal.

En Irrlicht la "ventana" es un objeto de tipo IrrlichtDevice. Lo creamos invocando al metodo createDevice(). Una vez lo tengamos creado podemos modificar sus propiedades para agregar controles, cámaras, eventos, comportamiento, etc. A continuación vemos un ejemplo de inicialización del device de Irrlicht.

        dimension2d<s32> resolucion(640,480);
bool fullScreen = false;
u32 bpp = 32;
f_device = createDevice(video::EDT_OPENGL, resolucion,bpp,fullScreen);
if (f_device == NULL) return false;
return true;


La estrategia consiste en instanciar un IrrlichtDevice y encapsular en una clase todos los elementos necesarios para administrar los controles, sus eventos, bucle de ejecución, etc. A priori ya nos podemos imaginar que nuestra clase aplicación recibirá por el constructor el device ya instanciado como en el ejemplo siguiente.

    mainApp = new MainApplication(f_device);
mainApp->mRun();


En la proxima entrega veremos, antes de entrar en detalle con la clase de aplicación, cómo gestionar la tasa de frames por segundo.

Sigue leyendo >

Formateador de código fuente

Estos días que hemos estado poniendo codigo fuente en los post, he sufrido mucho el tema del formato, indentado etc. En formatmysourcecode.blogspot.com puedes dar formato, o convertir en código html tu código fuente. Solo le falta los colores de las pabras reservadas ^_^. Lástima que eso dependa del lenguaje, pero ya se nos ocurrirá algo.

Sigue leyendo >

El nuevo EventReceiver

Terminamos hoy la parte del gestor de eventos exponiendo al detalle el nuevo EventeReceiver.


Para empezar, aquí está la declaración de la clase, que iría en su respectivo archivo de cabecera:
#include <irrlicht/irrlicht.h>
#include <map>

using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;

#define AppCB(var) void (*var)(void*, const SEvent&)

#define parGUIE std::pair< EGUI_EVENT_TYPE, int >
#define mapGUI std::map< parGUIE, AppCB() >
#define parKEYE std::pair< EKEY_CODE, bool >
#define mapKEY std::map< parKEYE, AppCB() >
#define mapMOUSE std::map< EMOUSE_INPUT_EVENT, AppCB() >
#define mapLOG std::map< ELOG_LEVEL, AppCB() >
//#define mapUSER AppCB()

class EventReceiver : public IEventReceiver
{
public:
EventReceiver(void* parent);
virtual ~EventReceiver();
virtual bool OnEvent(const SEvent& event);
bool mAddGuiEvent(EGUI_EVENT_TYPE eventType, int id, AppCB(callback));
bool mAddKeyUpEvent(EKEY_CODE code, AppCB(callback));
bool mAddKeyDownEvent(EKEY_CODE code, AppCB(callback));
bool mAddMouseEvent(EMOUSE_INPUT_EVENT input_event, AppCB(callback));
bool mAddLogEvent(ELOG_LEVEL level, AppCB(callback));

protected:
mapGUI f_guiEvents;
mapKEY f_keyEvents;
mapMOUSE f_mouseEvents;
mapLOG f_logEvents;
void* f_parent;
};


Bien, al principio vemos los includes necesarios y las definiciones, que ya explicamos, de las tablas hash y el puntero a función. Mas abajo, dentro de la declaración de la clase, que hereda de IEventReceiver, hemos añadido, además del onEvent, métodos para facilitar la adición de nuevos eventos, de modo que EventReceiver pueda gestionarlos adecuadamente. Cada uno de estos métodos recibe distintos en función del tipo de evento. Tal es el caso de mAddGuiEvent, que recibe el tipo de evento GUI y el id del control que lo provocó. Lo que sí que tienen en común, es el puntero a función o callback a invocar una vez producido el evento.

En la parte protegida podemos observar las tablas hash que contendrán los eventos y un puntero genérico (void*) al padre de la instancia de la clase. Este último campo lo veremos más abajo.

Veamos ahora la implementación. Primero, el contructor y destructor, nada de especial. Como las tablas hash no han sido creadas de manera dinámica, no es necesario ponelas en ninguno de los dos metodos.
#include "EventReceiver.h"

EventReceiver::EventReceiver(void* parent)
{
f_parent = parent;
}

EventReceiver::~EventReceiver()
{
f_parent = NULL;
}



Ahora veamos los métodos que nos permiten añadir facilmente callbacks a los eventos. Tampoco tiene ningún secreto, dado que tan sólo agregan el callback a la tabla hash pertinente con la clave recibida.
bool
EventReceiver::mAddGuiEvent(EGUI_EVENT_TYPE eventType, int id, AppCB(callback))
{
parGUIE key(eventType,id);
f_guiEvents[key] = callback;

return true;
}

bool
EventReceiver::mAddKeyUpEvent(EKEY_CODE code, AppCB(callback))
{
parKEYE key(code,false);
f_keyEvents[key] = callback;
return true;
}

bool
EventReceiver::mAddKeyDownEvent(EKEY_CODE code, AppCB(callback))
{
parKEYE key(code,true);
f_keyEvents[key] = callback;
return true;
}

bool
EventReceiver::mAddMouseEvent(EMOUSE_INPUT_EVENT input_event, AppCB(callback))
{
f_mouseEvents[input_event] = callback;

return true;
}

bool
EventReceiver::mAddLogEvent(ELOG_LEVEL level, AppCB(callback))
{
f_logEvents[level] = callback;

return true;
}



Y ahora sí, damas y caballeros, aquí viene el metodo onEvent, el cual tiene un switch que, según el evento principal, formará la clave y preguntará a la tabla hash correspondiente si tiene dicha clave, en cuyo caso recuperará el callback y lo invocará. Es aqui cuando hace uso del attributo f_parent. Recordad que el puntero a función debe apuntar a un metodo de clase (estático) y necesitábamos pasarle el objeto a manipular.
bool
EventReceiver::OnEvent(const SEvent& event)
{
switch(event.EventType)
{
case EET_GUI_EVENT:
{
s32 id = event.GUIEvent.Caller->getID();
EGUI_EVENT_TYPE eventType = event.GUIEvent.EventType;
parGUIE key(eventType,id);
if(f_guiEvents.find(key) != f_guiEvents.end())
{
AppCB(callback) = f_guiEvents[key];
callback(f_parent,event);
return true;
}
break;
}
case EET_KEY_INPUT_EVENT:
{
EKEY_CODE code = event.KeyInput.Key;
bool pressed = event.KeyInput.PressedDown;
parKEYE key(code,pressed);
if(f_keyEvents.find(key) != f_keyEvents.end())
{
AppCB(callback) = f_keyEvents[key];
callback(f_parent,event);
return true;
}
break;
}
case EET_MOUSE_INPUT_EVENT:
{
EMOUSE_INPUT_EVENT key = event.MouseInput.Event;
if(f_mouseEvents.find(key) != f_mouseEvents.end())
{
AppCB(callback) = f_mouseEvents[key];
callback(f_parent,event);
return true;
}
break;
}
case EET_LOG_TEXT_EVENT:
{
ELOG_LEVEL key = event.LogEvent.Level;
if(f_logEvents.find(key) != f_logEvents.end())
{
AppCB(callback) = f_logEvents[key];
callback(f_parent,event);
return true;
}
break;
}
case EET_USER_EVENT:
default:
{
break;
}
};
return false;
}



Para terminar, un ejemplo de uso del nuevo EventReceiver. Aquí vemos un cacho de código en el que se inicializa una instancia de tipo EventReceiver y se le agregan callbacks.
    f_eventReceiver = new EventReceiver(this);
f_device->setEventReceiver(f_eventReceiver);

//Inicializacion de controles
f_menu = f_env->addMenu(0, f_genIDs->getNewID());
f_buttonQuit = f_env->addButton(rect<s32>(10,210,100,240), 0, f_genIDs->getNewID(), L"Quit");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonQuit->getID(), MainApplication::mButtonQuit_Click);
f_buttonNewWindow = f_env->addButton(rect<s32>(10,250,100,290), 0, f_genIDs->getNewID(), L"New Window");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonNewWindow->getID(), MainApplication::mButtonNewWindow_Click);
f_buttonFileOpen = f_env->addButton(rect<s32>(10,300,100,340), 0, f_genIDs->getNewID(), L"File Open");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonFileOpen->getID(), MainApplication::mButtonFileOpen_Click);



Y a continuación vemos la implementación de los callbacks añadidos en el codigo anterior. Veis como al principio de cada evento se hace una conversión (cast) al tipo MainApplication, que es el que hace de padre (el valor del atributo f_parent) de f_eventReceiver.
void
MainApplication::mButtonQuit_Click(void *o, const SEvent & event)
{
MainApplication* me = (MainApplication*)o;
me->f_device->closeDevice();
// me->f_ImageLogo->remove();
}

void
MainApplication::mButtonNewWindow_Click(void *o, const SEvent & event)
{
MainApplication* me = (MainApplication*)o;
me->f_listBoxLogger->addItem(L"Window created");

IGUIWindow* window = me->f_env->addWindow(
rect<s32>(100, 100, 300, 200),
false, // modal?
L"Test window");
me->f_env->addStaticText(L"SubVentanica",
rect<s32>(35,35,140,50),
true, // border?,
false, // wordwrap?
window);
}

void
MainApplication::mButtonFileOpen_Click(void *o, const SEvent & event)
{
MainApplication* me = (MainApplication*)o;
me->f_listBoxLogger->addItem(L"File open");
me->f_env->addFileOpenDialog(L"Please choose a file.");
}


Bien, mediante los punteros a función hemos conseguido hacer mas mantenible el objeto EventReceiver necesario para gestionar los eventos con Irrlicht. En la proxima entrega de esta serie, empezaremos a pincelar la estructura de las ventanas en Irrlicht.

Sigue leyendo >

Colecciones de punteros a funciones

Seguimos elaborando la estructura base de la aplicación y para ello, explicaremos hoy que colecciones de objetos utilizar.


En la entrada anterior vimos que tipos de eventos tiene Irrlicht. Haremos uso de contenedores template o plantilla de la conocida familia STL de C++.

La elección del contenedor

Analicemos la situación con detenimiento. Necesitamos un contenedor de objetos que sea eficiente tanto en la inserción como en la consulta de datos, y ademas, que esten indexados por una clave, p. ej. el tipo de evento. En mi opinión, la tabla hash o array asociativo es el mas indicado (si sabes de uno mejor, por favor, dímelo ;)

Qué vamos a almacenar

Está claro, que almacenaremos punteros a funciones, pero, ¿qué declaración van a tener? Necesitamos un puntero a función que nos valga para cualquier evento. No es que me quiera copiar de .Net y Java, pero creo que uno de los parametros de la función será el propio SEvent de Irrlicht. ¿Y el retorno? en principio void, dado que el evento no suele llamase directamente, puede que me equivoque, es más, no me extrañaría, aún no conocemos Irrlicht lo sifuciente.

Los punteros a funciones tienen un inconveniente. Y es que no se puede referencia a un método de instancia. Si queremos referenciar al método de una clase, éste debe ser estático, con lo que perdemos el puntero this y cualquier atributo o campo del objeto. Pero solventar este problema es facil. Basta con que el metodo estático reciba un parametro adicional, un puntero al Objeto de la propia clase o mejor, para evitar problemas de autoreferencia, un puntero a void. Dentro de la funcion solo hay que hacer un cast al objeto de la clase y podremos acceder a todos sus atributos y campos.

He aquí el puntero a función que usaremos:

void (*puntero_a_funcion)(void*, const SEvent&)

No se tú, pero yo me canso facilmente de escribir este chorizo en cada sitio que deba usarlo, asi que me creo un define como este:

#define AppCB(var) void (*var)(void*, const SEvent&)

El define anterior es una macro que recibe el nombre del puntero a funcion, para mayor comodidad, puesto que no siempre necesito escribirlo.

Qué clave usaremos para indexar

Como ya vimos, tenemos 5 tipos de eventos, aunque, de momento el tipo de evento de usuario no lo usamos (o aún no le hemos visto utilidad). Podríamos tener una sola tabla hash para albergar a todos los eventos, pero si particularizamos en 4 contenedores separados, podremos identificar mejor los eventos. Además, de este modo no tendremos problemas con los enumerados o defines.

Si nos fijamos en el tipo de la interfaz gráfica de usuario, SGUIEvent, vemos que tiene un segundo nivel de tipo de evento, mediante gui::EGUI_EVENT_TYPE. Así que para indexar los eventos del GUI, usaremos un Pair como clave, compuesta por el gui::EGUI_EVENT_TYPE y el id del control que provocó el evento.

#define parGUIE std::pair< EGUI_EVENT_TYPE, int >

Para las teclas también usaremos un Pair que contrendrá la tecla presionada y un booleano que indicará si ha sido presionada o dejada de presionar.

#define parKEYE std::pair< EKEY_CODE, bool >

En lo referente a los eventos de raton y log, nos basta con indexarlos por el SMouseInput y SLogEvent respectivamente.

Los contenedores

Tenemos ya, todo lo necesario para definir los contenedores:


#define mapGUI std::map< parGUIE, AppCB() >
#define mapKEY std::map< parKEYE, AppCB() >
#define mapMOUSE std::map< EMOUSE_INPUT_EVENT, AppCB() >
#define mapLOG std::map< ELOG_LEVEL, AppCB() >


En la proxima entrega veremos finalmente la nueva clase que usaremos en adelante en cualquier ventana, el nuevo EventReceiver.

Sigue leyendo >

sábado, 24 de mayo de 2008

Tipos de eventos y subtipos

En esta breve entrega analizaremos los distintos tipos de eventos que aporta Irrlicht

El método OnEvent recibe un objeto SEvent. Si miramos el api de irrlicht, veremos que hay 5 tipos de eventos:

A su vez, los eventos de controles (SGUIEvent) tiene sub tipos, para poder distinguir, por ejemplo, el click de un boton de FileOpenDialog. Es por ello que cada colección de eventos depende del tipo de evento.

En la proxima entrega veremos las colecciones a emplear.

Sigue leyendo >

Ejemplo típico de los tutoriales

He aquí un ejemplo de EventReceiver extraido de uno de los tutoriales.

En él se aprecia claramente que todos los eventos estan programados de manera estática y que añadir, eliminar o modificar, es una ardua tarea.

class MyEventReceiver : public IEventReceiver
{
public:
virtual bool OnEvent(const SEvent& event)
{
// Escape swaps Camera Input
if (event.EventType == EET_KEY_INPUT_EVENT &&
event.KeyInput.Key == irr::KEY_ESCAPE &&
event.KeyInput.PressedDown == false)
{
if ( Device )
{
scene::ICameraSceneNode * camera = Device->getSceneManager()->getActiveCamera ();
if ( camera )
{
camera->setInputReceiverEnabled ( !camera->isInputReceiverEnabled() );
}
return true;
}
}

if (event.EventType == EET_GUI_EVENT)
{
s32 id = event.GUIEvent.Caller->getID();
IGUIEnvironment* env = Device->getGUIEnvironment();

switch(event.GUIEvent.EventType)
{
case EGET_FILE_SELECTED:
{
// load the model file, selected in the file open dialog
IGUIFileOpenDialog* dialog =
(IGUIFileOpenDialog*)event.GUIEvent.Caller;
loadModel(core::stringc(dialog->getFileName()).c_str());
}

case EGET_SCROLL_BAR_CHANGED:

// control skin transparency
if (id == 104)
{
s32 pos = ((IGUIScrollBar*)event.GUIEvent.Caller)->getPos();
for (s32 i=0; i<irr::gui::EGDC_COUNT ; ++i)
{
video::SColor col = env->getSkin()->getColor((EGUI_DEFAULT_COLOR)i);
col.setAlpha(pos);
env->getSkin()->setColor((EGUI_DEFAULT_COLOR)i, col);
}
}
break;

case EGET_BUTTON_CLICKED:

switch(id)
{
case 1101:
{
// set scale
gui::IGUIElement* root = env->getRootGUIElement();
core::vector3df scale;
core::stringc s;

s = root->getElementFromId(901, true)->getText();
scale.X = (f32)atof(s.c_str());
s = root->getElementFromId(902, true)->getText();
scale.Y = (f32)atof(s.c_str());
s = root->getElementFromId(903, true)->getText();
scale.Z = (f32)atof(s.c_str());

if (Model)
Model->setScale(scale);
}
break;
case 1102:
env->addFileOpenDialog(L"Please select a model file to open");
break;
case 1103:
showAboutText();
break;
case 1104:
createToolBox();
break;
case 1105:
env->addFileOpenDialog(L"Please select your game archive/directory");
break;
}

break;
}
}

return false;
}
};


Si nos fijamos en el subtipo de evento EGET_BUTTON_CLICKED vemos que distingue a cada boton por el id y con valores de enteros directamente. Imagínate que queramos añadir botones dinámicamente segun una configuracion u otra!!

En la proxima entrega examinaremos la jerarquía de eventos que tiene Irrlicht.

Sigue leyendo >

Manejo de eventos con IEventReceiver

Como ya hablamos en la introduccion, uno de los inconvenientes en los tutoriales de irrlicht, es la gestión de eventos para los controles (botones, cajas de texto, etc), cámaras, acciones de usuario, etc.

Para su gestión, Irrlicht dispone de la interfaz IEventReceiver. Cualquier Clase que herede de dicha interfaz, debe implementar el método onEvent. Si miráis en cualquiera de los tutoriales, vereis como el metodo onEvent del objeto de tipo MyEventReceiver usado en la aplicación tiene un super-switch múltiple de la muerte con todos los eventos de todos los controles, todo ello puesto a piñón fijo. Si tienes poquitos botones y posibles acciones esta bien, pero no nos sirve cuando la aplicacion crece y quieres que sea mantenible, dinámica y extensible. Lo bonito seria hacerlo al estilo de los lenguajes de nueva generación como Java y la familia .Net, esto es, yo tengo un botón, y un método que quiero que se ejecute cuando se produzca el evento onClick del botón.

Continuamos con un ejemplo típico de los tutoriales.

Sigue leyendo >

Estructura de ventanas con Irrlicht

Buenas a todos, en esta entrada trataremos de explicar como sacarle partido a las clases e interfaces que nos provee Irrlicht para montarnos una estructura de menús decente.

Si alguna vez has seguido los tutoriales de irrlicht, como por ejemplo, el User Interface y Mesh Viewer, habréis visto que presentan dos grandes inconvenientes.

El primero es que no deja claro, o no explica, cómo hacer varias ventanas, esto es, cómo tener dividido la aplicación en múltiples ventanas o formularios. Cualquier programa que se precie, tendrá una ventana de configuración, distintas secciones e incluso, subaplicaciones. Esto ocurre en [L], donde tendremos a priori, el menú principal, el editor de escenarios y el simulador en sí.

El segundo gran inconveniente es la gestión de eventos para los controles (botones, cajas de texto, etc), cámaras, acciones de usuario, etc.

Así pues, vamos a organizarnos y a poner las cosas claras. Abordaremos estos dos temas poco a poco y dividiendolo en articulos. He aqui los puntos a tratar:

Puedes ya empezar a leerlos comenzando por el primer punto: Manejo de eventos con IEventReceiver.

Sigue leyendo >

Cambios en entradas

Buenas a tod@s!!

Últimamente, a pesar de que quedamos todos los sabados para avanzar en el proyecto, carecemos de tiempo para actucalizar el blog.

A mi me ocurre que a la hora de elaborar un post sobre alguno de los bloques del proyecto, no me da tiempo a hacerlo "de una sentada", con lo que, si lo guardo como borrador hasta completarlo y despues lo publico ya como noticia visible, pasa mucho tiempo entre noticia y noticia, dando la sensación de inactividad.


Por otro lado, si publico la noticia a medias y poco a poco la voy modificando, el post no reaparece como nuevo y va quedando enterrado tras otros posts. Supongo que todo esto ya le habrá pasado a mas de uno y también supongo que es cosa de que soy un novato en el mundo de los blogs ;).

Así pues, lo que haré sera publicar una noticia con un índice en el que indicaré las secciones en las que se divide el tema a tratar, vamos, a modo de serie de articulos con el primer articulo con la introducción y el índice.

Nada mas publicar este post me pongo con el que dejé a medias: Estructura de Menús con Irrlicht

--
Un saludo.

Sigue leyendo >

Anchura del blog

Hemos "toqueteado" un poco la plantilla para aumentar el ancho general del blog. Lo sentimos por aquellos equipos que usen resoluciones de menos de 1024 x 768.

Más adelante ya buscaremos otra plantilla mejor e independiente del tamaño de la ventana.

Sigue leyendo >

domingo, 18 de mayo de 2008

Compilando el proyecto

Vamos a utilizar esta entrada para ir anotando los detalles de la compilación del proyecto en los diferentes sistemas que lo vayamos probando.
Quizás fuera más correcto utilizar un wiki para este tipo de cosas, pero como por ahora no hemos generado mucho contenido seguiremos con el blog. Además, facilitamos la comunicación con otros usuarios a través de los comentarios.


MsWindows + MinGW
- enlazar con: -lIrrlicht -lode

GNU/Linux
- enlazar con: -lIrrlicht -lode -lGL -lGLU -lX11 -lXxf86vm [-lXext]

* el proyecto incluirá todas aquellas librerías externas de las que haga uso

Sigue leyendo >

domingo, 11 de mayo de 2008

Vehículos: primeros pasos

Tras tener resuelto el tema del terreno, pasamos con la implementación de los vehículos. Hasta el momento, si miráis el código, todo está hecho muy ad hoc mientras vamos experimentando y asimilando las APIs y configuración de los motores. Cuando veamos que funciona pasaremos a la generalización y automatización.



Por ejemplo en la captura vemos un vehículo sobre el terreno. El vehículo tiene una configuración física sencilla basada en una caja para el chasis y en 4 esferas para las ruedas. Las cajas de alambres que se aprecian son la representación visual de las boundingbox de estas geometrías físicas (siempre en su perfecta horizontalidad). Se aprecia como el vehículo es posado en la inclinación del terreno, si bien no es preciso en cuanto al contacto de las ruedas con el terreno es debido a que la posición de la geometría física respecto a las ruedas está ajustada de forma numérica rápida y manualmente. La idea es que en un futuro ésto sea ajustado semi-automáticamente mediante un sencillo editor, ya que uno puede de forma sencilla automatizar la definición de una geometría física similar a la del modelo mediante malla o boundingbox, pero en el momento que deseemos mover las ruedas independientemente del chasis deberán ser representadas como un modelo a parte habiendo perdido así la posición relativa respecto al chasis. Y ésto va a suceder para cualquier elemento independiente que incorpore un objeto complejo como es el caso de un vehículo.
Por lo tanto, implementaremos un editor que nos permita cargar modelos, asignarles una configuración geométrica de su física y posicionarlos entre sí de forma relativa. Este editor deberá volcar dicha información a fichero, al igual que sucederá con la creación de los escenarios, por lo tanto en tiempo de simulación no habrán más ajustes ad hoc y todo será puesto en escena de forma automática.

El siguiente paso va a ser dotar de movimiento a las ruedas para poder pasear el cochecito por el terreno y empezar a hacer pruebas con los ajustes de "animación" (realmente con ajustes de la configuración del motor físico, ya que la animación en este caso es totalmente indirecta).

Sigue leyendo >

miércoles, 7 de mayo de 2008

Sincronizando mapa de alturas gráfico + físico

Una de nuestras primeras decisiones ha sido utilizar los mapas o campos de alturas como geometría para describir el terreno del simulador ya que los motores en los que nos apoyamos incorporan este tipo de geometrías en sus versiones más recientes. Así que vamos a realizar una mini-guía para saber como define la geometría cada uno de los motores y como podemos sincronizarlas para que la colisión proporcionada por el motor físico coincida con la representación gráfica que realiza el motor gráfico.



Introducción

Los motores que utilizamos en nuestro proyecto son: Irrlicht como motor gráfico y ODE como físico.
La terminología para referirse a los mapas de alturas es algo diversa, encontrándonos con términos como: terrain, heightfield, heightmap o sus respectivas traducciones al castellano. En general, Irrlicht los define como terrains y ODE como heightfields. Pero todos hacen referencia a exactamente el mismo tipo de geometría.

Por si alguien anda perdido sobre qué es un campo de alturas, la idea más sencilla es que se trata de un área (cuadrada normalmente) cuya altura en cada punto viene definida por una función o algún otro tipo de representación. Por eso su utilidad más directa es la de representar terrenos tridimensionales.

Irrlicht Terrain Scene Node

Para éste motor gráfico, un campo o mapa de alturas es una geometría de tipo TerrainSceneNode.
La representación de la altura es obtenida directamente de una imagen en escala de grises donde el blanco indica el punto más alto y el negro el punto más bajo. Como un color no define una altura concreta, debemos entender la representación de la altura como algo proporcional (entre el blanco y el negro, entre 0 y 1..), de forma que la altura final deseada se consigue mediante la aplicación de un escalado. Esto nos permite representar el mismo campo de alturas de forma suave o abrupta.
Recordad que al estar en entornos 3D (X,Y,Z), por defecto el terreno se representa en las coordenadas (X,Z) de forma horizontal y la altura viene dada por la componente (Y).
Algo importante a tener en cuenta es que Irrlicht define la esquina inferior izquierda del terrain como el origen de coordenadas de la geometría.

ODE Heightfield Geom

La definición del campo de alturas por parte del motor físico es algo más delicada.
El tipo de datos utilizado es un geomheightfield, y espera recibir los datos de la componente altura desde una función matemática. Por lo tanto, hay que codificar un método en nuestra aplicación que haga de callback para el campo de alturas, indicando el valor de la componente (Y) para un par (X,Z) dado.
(Si tienes problemas para utilizar un método (C++) como función de callback (C) encontraras la solución aquí.)
La principal sutileza de utilizar una función matemática como fuente de datos para las alturas es que los valores de entrada de la función son enteros, de este modo ODE construye su heightfield mediante coordenadas del tipo: { (0,0), (0,1), (0,2) ... (m,n) }, siendo independientes a las coordenadas (X,Z) del mundo tridimensional. Y algo que puede ser ahora obvio, es la necesidad de indicarle al motor físico el número de divisiones (en sus 2 dimensiones (X,Z)) de nuestro heightfield. Esas divisiones discretizan el área definida y corresponden a las coordenadas enteras utilizadas en el callback (sus valores máximos: (m,n)).
Otro detalle a tener en cuenta, sobre todo de cara a la sincronización, es que para ODE el origen de coordenadas de la geometría obtenida se sitúa en el centro (X,Z) del campo de alturas generado.

Synching Irrlicht-ODE Heighfields

La forma más fácil de afrontar el problema es generar inicialmente el terreno con Irrlicht mediante una imagen BMP que podamos modificar a nuestro antojo, y a partir de él obtener la componente altura para ODE mediante el método getHeight.

Como hemos visto, Irrlicht y ODE localizan el origen de coordenadas de la geometría en puntos distintos, con lo que habrá que corregir este desfase para que la representación de ambos por pantalla coincida.
En resumen, la posición de la geometría de ODE vendrá dada por la siguiente función:
(X,Z)_ODE = (X,Z)_IRR + (Width,Depth)/2

Irrlicht trabaja siempre con las coordenadas del mundo tridimensional, por lo que nuestro campo de alturas está correctamente localizado por su parte. Al construir el terrain definiremos su altura mediante un mapa de bits y su tamaño mediante el escalado oportuno. Y para construir el heightfield de ODE obtendremos las características directamente del terrain de Irrlicht.
El resto de parámetros del constructor por parte de Irrlicht afectan a los algoritmos de renderizado del terreno y no a su estricta definición, con lo cual no son importantes de cara a la sincronización con ODE.
En la construcción del heightfield de ODE tendremos que indicar el método callback que va a proporcionar la altura del terreno, las dimensiones del mismo que obtendremos de nuestro terrain ya construido, y las divisiones en las que queremos discretizar el área del heightfield. Estos últimos valores son independientes a Irrlicht y tendremos que sacárnoslos de la manga. Básicamente estamos definiendo el detalle y complejidad del terreno para nuestro motor físico, así que cuantas más divisiones hagamos más se ajustará la representación física del terreno a la representación gráfica, pero más cálculos físicos tendremos y más lenta irá la simulación. Clásico tradeoff de optimización, así que todo depende de la calidad de los resultados que requiramos en un momento dado. También acepta ODE un escalado en el constructor, pero como nuestro terreno ya está completamente generado por Irrlicht, debemos privarle a ODE de hacer modificaciones sobre los datos que le vamos a proporcionar, por lo tanto indicaremos siempre que NO haga escalado (lo que supone un valor real de 1.0).

Y por último, sólo quedaría definir el callback para completar la sincronía entre ambos motores.
Éste es un punto delicado ya que recordemos que Irrlicht trabaja con las coordenadas del mundo y ODE con sus componentes enteras en las que ha discretizado el terreno. Por lo tanto, la idea es transformar las coordenadas enteras que utiliza ODE en la llamada del callback en coordenadas del mundo para obtener de Irrlicht la altura correcta en el punto deseado. Teniendo esto claro y recordando los atributos que han entrado en juego no debe suponer un problema ver la solución:

anchuraDivision = (Width,Depth) / divisiones
(X,Z)_IRR = (X,Z)_ODE * anchuraDivisión + (X,Z)_POSITION

Con anchuraDivision obtenemos la distancia en coordenadas del mundo que está discretizando ODE entre entero y entero. Ya solo falta multiplicar por el entero dado y desplazar la coordenada a donde hayamos colocado nuestro terrain en el entorno tridimensional.

Demo

En el Box podéis encontrar la demo test_terrain-balls.zip en la que se muestra la sincronización explicada. Hay representado un terreno y una matriz de pelotas cae del cielo colisionando contra él. Es posible mover la cámara y desplazarse por el mundo con las teclas (W,A,S,D). También es posible modificar la forma del terreno editando el mapa de bits ./res/terrain-heightmap.bmp (debe respetarse el tamaño del BMP).

Se incluyen las fuentes, aunque éstas tienen mucho más código del necesario y quizás marean más que ayudan a entender el asunto. Así que para cualquier duda no dudéis en contactar con nosotros!

Sigue leyendo >