Aplicación Práctica - GUI - Añadiendo una GUI y estados de juego


Nuestro simulador no es diferente de la mayoría de juegos en que tenemos dos "estados" principales: "GUI" y "Juego". Sin embargo, uno de los temas más confusos en la historia de Ogre y CEGUI es hacer que estos dos hechos fueran de la mano. Ahora es bastante directo, pero hay muchos ejemplos incompletos o desfasados en los foros y requiere 5 veces más esfuerzo del que debiera.

Primero, el ejemplo CEGUI en este wiki: olvídalo. O al menos la parte que trata con RTT (Renderizado a Textura). No se usa en el ejemplo y sirve sólo para confundir de errores no deseables. De hecho, hay sólo cuatro líneas de código necesarias para cargar un GUI en la ventana de Ogre.

Pero antes de que hagas esto, necesitas crear la GUI primero. Puedes usar el widget TaharezLook y no preocuparte. Es completo, y funciona.

Primero, un apunte sobre nuestro diseño de simulador. La primera cosa que ocurre cuando se comienza es que la GUI se muestra, en la ventana de "Bienvenida". Por ahora sólo tenemos unos pocos botones que llevan a otras páginas (Opciones) o realizan acciones de omando ("Acción instantánea", Salir). El botón de Acción Instantánea coge al usuario y lo lleva a una página en el mapa navegacional (tienes uno para tu juego, no?) Donde el o ella pueden seleccionar una misión y jugar. Como obtener la lista de niveles o misiones disponibles para tu juego es cosa tuya. Nosotros lo hacemos leyendo cada fichero en la carpeta "resource/missions" y obteniendo los datos de cabecera para llenar una lista dropdown de misiones. Simplemente usa un archivo de manifiesto que contiene los nombres de misiones y el nombre de archivo asociado, o puedes tener una solución diferente. Es cosa tuya.

Más tarde verás que usamos múltiples manejadores de escenas. ¿Por qué? Cada misión que usamos usa un manejador de escena particular, dependiendo de si el usuario está en una gran pantalla al exterior, dentro de una cueva o edificación, o sobre una nave espacial o en el espacio. La razón para la existencia del sistema de plugin de manejo de escena es que una implementación de manejador de escena no es la mejor opción para todos los tipos de escena, y veremos, como cambiar entre ellas en tiempo de ejecución y lo fácil que es.

CEGUI


CEGUI es la librería GUI oficial de Ogre. Mientras que Ogre tiene funcionalidad 2D, y el sistema de superposiciones, cuando se busca algo más sofisticado para crear interfaces usamos CEGUI. Puedes conseguir CEGUI en su sitio web.

Encuentra el directorio "datafiles" en el árbol fuente o en otro sitio en la distribución binaria (o en el directorio raíz de install del CELayoutEditor), y busca el directorio layouts/. Cada archivo que encuentres allí es una descripción de fondo (layout) para una página entera de GUI (hoja "sheet"). Es cierto que puedes crear un fondo GUI por código (y hay muchas razones para querer hacerlo así). No tienes por que hacerlo así. El 99% de los usuarios de CEGUI pueden crear archivos layout y nunca tener que crear una ventana por código. Después de todo, como verás, El fondo sólo carga en una ventana como cualquier otra ventana CEGUI y puede ser tratado de la misma forma.

El esquema referenciado en cada nombre de widget (ej: "TaharezLook/EditBox" es un EditBox en el esquema TaharezLook) y puede ser encontrado en el directorio schemes/, el cual lo convierte en las referencias como un imageset en el directorio imagesets/, el cual no es más que un concepto de imagemap de HTML: cada widget (y el estado del widget) es mapeado de una imagen TGA para ese esquema usando una notación de mapeo de cuatro esquinas. Revisa los archivos y veras como funciona todo junto. Tengo que confesar que no conozco a fondo el directorio looknfeel/, pero parece ser simple de direccionar, nada sorprendente, el look and feel del widget en el esquema.

Mientras que estas aquí, siéntete libre de eliminar los nombres relativos a rutas que encuentres (ej: "../datafiles/..); ya que proporcionamos la posición de todos nuestros recursos al ResourceManager durante la inicialización, por lo tanto no tenemos que llevar la pista de estos - El ResourceManager los encontrará por nosotros.

Si quieres, puedes cargar un layout en el CELayoutEditor (en un módulo separado CVS, en un proyecto separado, y que está disponible para su descarga desde el sitio de CEGUI). Juega con él, crea uno nuevo, lo que quieras. Este es instructivo para ver como funciona todo junto, especialmente si construyen uno desde cero.

Ten en cuenta que cada archivo .layout es la definición para una única página/hoja de GUI. Necesitarás una para cada página en tu GUI.

Finalmente, date cuenta de que la convención para la extensión de los ficheros CEGUI es (.scheme, .layout, etc.) realmente sólo es eso: una convención. CEGUI no busca una extensión específica; de hecho, tienes que proporcionarle a CEGUI el nombre del archivo completo cuando lo cargues, así que si no te gusta .layout por alguna razón, puedes cambiarlo si quieres.

Manejo de Eventos de GUI


Si estas familiarizado con la programación con MFC, tienes casi recorrido el camino para comprender el manejo de eventos que hace CEGUI. La mayor diferencia es que tu aplicación se subscribirá a los eventos proporcionando un método manejador para que CEGUI lo llame cuando un evento ocurra en el widget (control). CEGUI entonces llamará a tu método con un objeto EventArgs para que puedas devolver un puntero a la ventana que es la fuente del mensaje (ej: en el caso de un manejador de click de botón, los args te permitiran acceder a la ventana de la que es el botón). Deberías comprender que en CEGUI (mucho más que en cualquier sistema de ventanas), todo es una ventana.

Lo más difícil sobre CEGUI, pero que tiene mucha potencia una vez que lo entiendes, es que CEGUI no trata con la entrada en absoluto. No maneja dispositivos de entrada, no usa el teclado, no usa el ratón, etc... Así que cómo consigue la entrada de estos dispositivos?.

Tienes que capturar la entrada tu mismo (usando una API de entrada como OIS) y proporcionar a CEGUI la notificación de que un botón se ha pulsado o una tecla ha sido presionada. Lo mismo con el movimiento del ratón. Usando los métodos injectMouseMove(), injectKeyDown(), injectKeyUp() etc. le dirás a CEGUI que algo ha ocurrido y el te dirá en su devolución si algo fue presionado o cambió. Realmente elegante, una vez que comprendes el proceso, y eliminas completamente la necesidad de que CEGUI trate con la entrada de tu aplicación.

El archivo .scheme para el menu


Así, que estoy bastante preparado! Tengo activado Ogre y estoy aburrido de ver mi ventana de renderizado en blanco, quiero ver una GUI!. Para propósitos de demostración, usemos el fondo que he creado para nuestro proyecto:

<?xml version="1.0" ?>
 
<GUILayout>
 
    <Window Type="DefaultWindow" Name="DemoLayout">
 
        <Window Type="TaharezLook/FrameWindow" Name="Main">
 
                <Property Name="RelativeMinSize" Value="w:0.2 h:0.2" />
 
                <Property Name="RelativeMaxSize" Value="w:1.0 h:1.0" />
 
                <Property Name="Position" Value="x:0.0 y:0.0" />
 
                <Property Name="Size" Value="w:1.0 h:1.0" />
 
                <Property Name="Text" Value="MyProject" />
 
                <Property Name="CloseButtonEnabled" Value="False" />
 
 
            <Window Type="TaharezLook/Button" Name="cmdQuit">
 
                <Property Name="Text" Value="Quit" />
 
                    <Property Name="Position" Value="x:0.4 y:0.7" />
 
                    <Property Name="Size" Value="w:0.2 h:0.07" />
 
            </Window>
 
            <Window Type="TaharezLook/Button" Name="cmdOptions">
 
                    <Property Name="Position" Value="x:0.4 y:0.6" />
 
                    <Property Name="Size" Value="w:0.2 h:0.07" />
 
                <Property Name="Text" Value="Options" />
 
            </Window>
 
            <Window Type="TaharezLook/Button" Name="cmdInstantAction">
 
                    <Property Name="Position" Value="x:0.4 y:0.5" />
 
                    <Property Name="Size" Value="w:0.2 h:0.07" />
 
                <Property Name="Text" Value="InstantAction" />
 
            </Window>
 
            </Window>
 
        </Window>
 
</GUILayout>

NOTA IMPORTANTE: Para usuarios de CEGUI 0.4.x y anteriores, el anterior fondo funcionará perfectamente. Para usuarios de 0.5.x y posteriores debes cambiar el sistema de especificar las coordenadas de ventana al sistema de Dimensión Unificada (UDim). CVS HEAD tiene actualmente soporte para Position y Size. Tienes que tener cuidado. Si no estas seguro de la versión que tienes, CEGUI.log te lo dirá. Encuéntralo en el mismo sitio que el Ogre.log después de la ejecución de tu aplicación.

Este es el contenido entero del archivo de fondo para la pantalla principal que se muestra cuando comienza nuestro proyecto. Date cuenta de que a CEGUI le gusta el estilo UVW de coordenadas "normalizadas", este te permite liberarte de preocuparte sobre diferentes resoluciones que el usuario pueda usar. Tengo la ventana principal ajustada a 1.0 para el ancho y alto; esto hará que se ocupe toda la ventana de renderizado, que es lo que queremos. El scheme TaharezLook expondrá una apariencia con un conjunto de imágenes de fondo para la ventana. De hecho, puedes cortar y pegar este fondo en un archivo y cargarlo con el CELayoutEditor y ver como se ve; de la misma forma que en la aplicación.

Código para el uso de un fichero de .scheme con CEGUI


Aquí está el código para la carga de este fondo en la ventana de renderizado que hemos creado anteriormente, y lo mostramos:

    // ajuste del sistema GUI
 
        mGUIRenderer = new CEGUI::OgreCEGUIRenderer(window, Ogre::RENDER_QUEUE_OVERLAY, false, 3000, guiSceneMgr);
 
        mGUISystem = new CEGUI::System(mGUIRenderer);
 
    CEGUI::Logger::getSingleton().setLoggingLevel(CEGUI::Informative);
 
    CEGUI::SchemeManager::getSingleton().loadScheme((CEGUI::utf8*)"TaharezLookSkin.scheme");
 
    mGUISystem->setDefaultMouseCursor((CEGUI::utf8*)"TaharezLook", (CEGUI::utf8*)"MouseArrow");
 
 
    CEGUI::FontManager::getSingleton().createFont("bluehighway.font");
 
    mGUISystem->setDefaultFont((CEGUI::utf8*)"BlueHighway-12");
 
 
    // establece el cursor del raton para que aparezca inicialmente al medio de la pantalla
 
    mGUISystem->injectMousePosition((float)window->getWidth() / 2.0f, (float)window->getHeight() / 2.0f);

NOTA: El código anterior no compilará si estás usando CEGUI 0.7.1. Como esta porción de el tutorial es académica, los principios aquí descritos se mantienen. El código funcional 0.7.1 está incluido en la parte del tutorial que cubre la implementación actual de CEGUI.

Algunas notas sobre lo anterior: primero, es básicamente el código del tutorial CEGUI sin la distracción de RTT. Entonces, elijo Tahoma-12 porque, porque me gusta. Debes tener la referencia a los directorios de fuentes TTFs en Windows o Linux. (Alternativamente, puedes hacer una simple referencia al directorio C:\Windows\Fonts como otra localización de ResourceGroup. Tercero, nota la falta de cualquier directorio de información con nombres de ficheros; esto lo hará el ResourceManager, que consultaremos usando OgreCEGUIRenderer.

Recuerda el guiSceneMgr que obtuvimos anteriormente.

La última cosa a tener en cuenta es la llamada a injectMousePosition(); hacemos esto para colocar el ratón en el centro e la pantalla. Bastante útil, tomar las coordenadas absolutas en vez de el método escalado 0.0-1.0.

Subscripción a evento CEGUI


La subscripción a eventos CEGUI es directa; no te mostraré como se hace la subscripción ya que es más compleja que los propósitos de esta demostración. Sólo te diré que el código de la subscripción se parecerá mucho a este:
CEGUI::Window *win;
 
        m_win = CEGUI::WindowManager::getSingleton().getWindow(sheetName);
 
        win = m_win->getChild("cmdQuit");
 
        win->subscribeEvent(CEGUI::PushButton::EventClicked, CEGUI::Event::Subscriber(Quit_OnClick, this));

getWindow() en CEGUI conseguirá un puntero a la ventana nombrada. Antes, conseguimos un puntero a la ventana raíz de la hoja que hemos cargado.

Manejo de eventos con CEGUI


Deberías llamar a esto desde dentro de una clase implementada para el manejo de eventos GUI; en nuestro caso, hay una clase para "dialog" o "sheet" o "page" (lo que quieras llamar). El método manejador para este evento, en la clase manejadora para la hoja GUI "Main" (lo hemos llamado claramente "GuiEventHandler_Main") y se ha implementado como:

bool GuiEventHandler_Main::Quit_OnClick(const CEGUI::EventArgs &args) {
 
    // inicializa el apagado del sistema (hacemos esto para pedir el apagado
 
    // que consecuentemente parara el bucle principal)
 
        m_stateManager.requestStateChange(SHUTDOWN);
 
        return true;
 
    }

Convirtiendo Eventos de Ogre en Eventos de CEGUI


La última cosa esencial que necesitarás para trabajar con CEGUI y Ogre es la traducción del sistema de eventos de entrada en algo que CEGUI pueda usar. Nuestro sistema de entrada, está basado en DirectInput sobre Win32. Usa un estilo MFC "On*() para el esquema de procesado de eventos HID, como por ejemplo OnKeyDown() y OnMouseMove(). En el estado de la GUI, estos eventos tienen que ser manejados y entonces enviados a CEGUI, lo cual no se consigue de una manera directa. La entrada de ratón es fina e intuitiva, pero la entrada e teclado, no sólo tienes que enviar el código de teclado, tienes que enviar el carácter que se representará.

Si estas leyendo esto en un país que usa combinaciones de teclas muertas, te desearé la mejor de las suertes si estas intentando envolver tu propia entrada en Win32: El ToUnicode()/ToUnicodeEx() API en Win32 es tan horrible de hacer que los desarrolladores de MS no pueden tratar con ello sin complicarse. El resto de nosotros que usamos el conjunto de caracteres latino no tendremos problemas, pero si planeas poner soporte internacional para tu aplicación entonces tendrás que trabajar duro; te recomiendo un vistazo largo a los códigos de teclado de GTK para Win32 para saber como manejarlos. OIS maneja algo de esto, pero mientras escribo, Unicode todavía no se soporta.

Pero si en cualquier caso. Quieres ver algo más de código sobre inyección de la entrada en CEGUI, no sobre la estúpidas implementaciones de Microsoft. Mira esto:
// contenedor de datos de evento
 
        typedef struct {
 
            int x, y, z;
 
            int button;
 
        } MouseData;
 
 
        typedef struct {
 
            unsigned char keyCode;    // codigo de hardware del teclado
 
            unsigned long mbcc;    // codigo de caracter multi-byte (UNICODE)
 
            bool alt;
 
            bool ctrl;
 
            bool shift;
 
        } KeyData;
 
 
    void InputProcessor::OnMouseMove(Input::MouseData &evt) {
 
        int x = evt.x;
 
        int y = evt.y;
 
        if (m_stateManager.getCurrentState() == GUI)
 
            m_video.guiMouseEvent(x, y, 0, 0);
 
    }
 
    void InputProcessor::OnMouseMoveZ(Input::MouseData &evt) {
 
        int z = evt.z;
 
        if (m_stateManager.getCurrentState() == GUI)
 
            m_video.guiMouseEvent(0, 0, z, 0);
 
    }
 
 
    void InputProcessor::OnMouseButtonDown(Input::MouseData &evt) {
 
        int button = evt.button;
 
        if (m_stateManager.getCurrentState() == GUI)
 
            m_video.guiMouseEvent(evt.x, evt.y, 0, -button);
 
    }
 
 
    void InputProcessor::OnMouseButtonUp(Input::MouseData &evt) {
 
        int button = evt.button;
 
        if (m_stateManager.getCurrentState() == GUI)
 
            m_video.guiMouseEvent(evt.x, evt.y, 0, button);
 
    }
 
    void InputProcessor::OnKeyDown(Input::KeyData &evt) {
 
        if (m_stateManager.getCurrentState() == GUI)
 
            m_video.guiKeyboardEvent((long)evt.keyCode, evt.mbcc, true, evt.alt, evt.ctrl, evt.shift);
 
    }
 
    void InputProcessor::OnKeyUp(Input::KeyData &evt) {
 
        if (m_stateManager.getCurrentState() == GUI)
 
            m_video.guiKeyboardEvent((long)evt.keyCode, evt.mbcc, false, evt.alt, evt.ctrl, evt.shift);
 
    }

Date cuenta de que nuestro subsistema de video maneja la GUI y la entrada de comando de la aplicación de manera diferente, y por lo tanto se tienen que llamar a diferentes métodos (también date cuenta de que aquí no hay llamadas de entrada en la aplicación "todavía"). En el final de la recepción de la llamada las encontrarás:

void video::guiKeyboardEvent(long keycode, long ch, bool down, bool alt, bool ctrl, bool shift) {
 
    if (down) {
 
        mGUISystem->injectKeyDown(keycode);
 
        mGUISystem->injectChar(ch);
 
    }
 
    else
 
        mGUISystem->injectKeyUp(keycode);
 
}
 
static CEGUI::MouseButton guiButton[3] = {
 
    CEGUI::LeftButton,
 
    CEGUI::MiddleButton,
 
    CEGUI::RightButton
 
};
 
 
// x, y son 0 hasta N - 1 donde N=dimensiones de pantalla
 
// z es cualquier numero entero
 
// button indica que boton fue presionado/soltado; los numeros
 
// negativos son eventos "liberados", y cero indica ningun evento de boton
 
void video::guiMouseEvent(int x, int y, int z, int button) {
 
 
    if (button == 0)
 
        mGUISystem->injectMouseMove(x, y);
 
    else {
 
        if (button > 0)
 
            mGUISystem->injectMouseButtonUp(guiButton[button-1]);
 
        else
 
            mGUISystem->injectMouseButtonDown(guiButton[(-button)-1]);
 
    }
 
}

Es así de simple, en serio. En Win32, ToUnicodeEx() normalmente te dará el carácter Unicode que corresponde a la pulsación indicada por el código de tecla, por el correspondiente teclado de windows (no estoy seguro de como son manejados los locales en Linux), y pasas eso a CEGUI para que puedas mostrar el carácter que has tecleado.

Cambiando de -GUI a la Aplicación y Viceversa


Recuerdas el método showGui() mencionado en el artículo Inicialización? Aquí está su definición:
if (window)
 
        window->removeAllViewports();
 
 
    if (guiSceneMgr)
 
        guiSceneMgr->removeAllCameras();
 
    else
 
        guiSceneMgr = ogre->getSceneManager(ST_GENERIC);
 
 
    if (sceneMgr)
 
        sceneMgr->removeAllCameras();
 
 
    camera = guiSceneMgr->createCamera("Main");
 
        camera->setPosition(0, 0, 300);
 
        camera->lookAt(0, 0, 0);
 
        window->addViewport(camera);

Así es como cambias entre manejadores de escena en Ogre. Primero, necesitas eliminar todo de los puerto de vista de la ventana, entonces necesitas eliminar todas las cámaras que el manejador de escena este usando, entonces podrás abrir un manejador de escena alternativo y crear cámaras en él (y añadir puertos de vista usando estas cámaras). El código anterior es para el caso general, y tratará con el cambio del estado de "Aplicación" así como de comenzar desde cero. El método showScene() es así:
window->removeAllViewports();
 
    if (guiSceneMgr)
 
        guiSceneMgr->removeAllCameras();
El uso convencional de estos dos métodos es que cada uno será seguido de una llamada a cargar el contenido en la escena; para la GUI, será la cargar del código de fondo anterior, y para la escena, será cualquier código que quieras cargar de un mundo y geometría en esa escena(volveremos a esta parte más tarde). Por ahora, es bastante saber que showScene() se llama desde el código que maneja nuestro evento de presionar botón de GUI "Launch".

Y qué hacen estos eventos? No mucho, en realidad. Si has hecho programación en MFC o programación de Forms en VB, sabrás un poco sobre que se puede hacer en respuesta a algunos eventos. De hecho, el código entero que tenemos en respuesta a la presión del botón Launch es:

Mission::MissionFile mf;
 
        std::ifstream mfile("demo.xxx");
 
        mf.deserialize(mfile, 0);
 
        mfile.close();
 
 
        m_stateManager.requestStateChange(NORMAL);
 
        m_video.showScene();
 
 
        m_video.loadWorld(mf);

Este código carga nuestro formato de fichero de misión binario, lo deserializa en algo que podemos usar más tarde, pedimos un cambio de estado desde el manejador de estado, llamamos al showScene() para cambiar los manejadores de escena, y cargamos el mundo y la geometría en la escena, y seguimos. En algún punto aquí saldrá una barra de progreso que se actualizará mientras la escena y la geometría se cargan al completo, esto completará la apariencia de una aplicación profesional.

Manejo del Estado


Manejador del Estado? Qué es? Viene con Ogre?

No, es sólo una clase puente que nos permite sincronizar el bucle de administración de la entrada con los estados de juego (la entrada de la GUI es manejada de manera muy diferente y por diferentes clases que la entrada de juego; te mostraremos eso luego). Serás capaz de escribir tu propio manejador de estado, ya que cada juego tiene sus propios estados; lo que funciona en unos no funciona en otros. En esta fase, tenemos STARTUP, SHUTDOWN, GUI y NORMAL; otros deberán ser implementados posteriormente dependiendo de lo que necesitemos.