Mejorando Tu Engine IV - GUI: Especificaciones

Tengo un nuevo proyecto entre manos para el que estoy portando mi motor de vuelta a PTK (ironías de la vida), mejorándolo en el proceso. Desde luego no estoy dispuesto a perder aquello que más me gustaba del PopCap Framework, su sistema de GUI, así que me he propuesto portarlo y mejorarlo en todo lo que pueda, pues el juego que tengo pensado es básicamente interfaz gráfica y necesito un sistema potente y flexible, con muchas características que el sistema del que ahora disponía, no tiene.

Dado que un sistema de GUI es algo bastante complejo y extenso voy a dedicarle varias entradas al mismo tema, a medida que lo voy desarrollando. En esta primera entrega discutiré las especificaciones. Es decir, los requisitos del resultado final del código, aquello que quiero que tenga y cómo debe funcionar.

Widgets de los que dispondrá

  • Label
    • Texto estático de una sóla línea
    • Soporta alineación derecha, izquierda y centrada
  • Text
    • Texto estático de varias líneas
    • Soporta scroll
    • Soporta alineación derecha, izquierda, centrada y justificada
  • TextBox
    • Control para introducir texto, de una sóla línea
    • Soporta TextBox tipo “password”
    • Soporta scrol horizontal
    • Soporta selección con ratón y teclado
    • No soporta copiar & pegar
    • Lanza estados onValueChange y onEnter
    • Teclado: cuando tiene el foco, lee las teclas alfanuméricas para introducir texto. ENTER acciona el evento onEnter
  • TextArea
    • Control para introducir texto, en varias líneas
    • Soporta scroll vertical
    • Soporta selección con ratón y teclado
    • No soporta copiar & pegar
    • Lanza estado onEnter
    • Teclado: cuando tiene el foco, lee las teclas alfanuméricas para introducir texto. ENTER acciona el evento onEnter
  • Button
    • Botón con texto centrado encima
    • Background tileable horizontalmente (el tercio central de la imagen)
    • Estado idle, over, down y disabled
    • Posibilidad de un gráfico distinto para cada estado
    • Posibilidad de un color de texto distinto para cada estado
    • Lanza estado onClick
    • Teclado: cuando tiene el foco, ENTER hace lo mismo que un click
  • AnimationButton
    • Igual que Button pero usando animaciones en vez de imágenes estáticas
    • Teclado: cuando tiene el foco, ENTER hace lo mismo que un click
  • CheckBox
    • Button sin texto y con un funcionamiento distinto (típico de checkbox, de mantenerse pulsado al soltar el mouse)
    • Estados idle, over, down y disabled tanto para cuando está checked como unchecked (luego, 8 estados reales)
    • Posibilidad de un gráfico distinto para cada estado
    • Lanza estado onCheck con el parámetro “checked”
    • Teclado: cuando tiene el foco, ENTER o ESPACIO hacen lo mismo que un click
  • Group
    • Simplemente un contenedor de Widgets, útil cuando queremos definir un mismo layout para un conjunto de widgets o para poder luego moverlos o tratarlos como un todo
  • RadioGroup
    • Prácticamente igual que un Group pero alguna funcionalidad extra para manejar los RadioButtons que tenga como hijos
    • Permite indicar el RadioButton a seleccionar fácilmente
    • Permite saber el radio button seleccionado
  • RadioButton
    • Prácticamente igual que un CheckBox pero con conocimiento de grupo
    • Cuando lee por mouse que se ha seleccionado, se llama al SetSelected de su RadioGroup padre para que deseleccione al resto
    • Lanza evento onSelect (en vez de onCheck/onUncheck del CheckBox)
  • Image
    • Imagen estática
  • Animation
    • Igual que Image pero con animaciones. Existe esta separación para evitar tener que crear animaciones cuando lo único que queremos es una imagen estática
  • Canvas
    • Como un Image pero editable proceduralmente. Útil para mostrar gráficas y demás
  • Hyperlink
    • Texto clickable (como un Button sin fondo)
    • Soporte de texto subrallado (del mismo color que el texto), definida para cada estado
    • Estados idle, over, down y disabled
    • Posibilidad de un color de texto para cada estado
    • Lanza evento onClick
    • Teclado: cuando tiene el foco, ENTER hace lo mismo que un click
  • List
    • Área con una serie de ítems que ocupan una línea cada uno
    • Soporte para scrolling vertical. Scrollbar oculta hasta que el número de ítems en la lista sea mayor al que pueden mostrarse a la vez
    • Cada elemento tiene estados idle, over y down
    • Elementos visuales personalizables: ancho del borde, color de borde, color de fondo, color de línea over, color de línea down/selected, color de texto, color de texto over, color de texto down/selected y style del scrollbar
    • No soporte de selección múltiple
    • Lanza evento onItemSelected pasando como parámetro el índice del elemento
    • Teclado: cuando tiene el foco, con los cursores arriba ya bajo puedes desplazarte por la lista
  • Slider
    • Típico deslizador utilizado en muchas ocasiones, como para ajustar el volumen de una aplicación
    • Se compone del thumb (puntero que desliza) y del track (superficie sobre la que desliza)
    • Puede ser vertical u horizontal
    • El thumb tiene estados idle, over, down y disabled, se comporta como un Button arrastrable
    • El thumb se dibujará centrado en X e Y en el punto del track que represente
    • Visualmente el track, igual que un Button, estará formado por una imagen estática cuyo tercio central se tileará en X hasta alcanzar el ancho deseado
    • Lanza evento onValueChanged pasando como parámetro un valor en el rango [0.0, 1.0]
    • Teclado: con los cursores derecha e izquierda (o arriba y abajo si es vertical) puedes aumentar o decrementar el slider en un 10%
  • Dialog
    • Facilita la creación de cuadros de diálogo
    • Se compone de varias partes:
      • Fondo: igual que un Button tilea en X su tercio central, el Dialog se compone de una imagen que divide en 9 sectores (4 esquinas, 4 lados que tilea en X o Y, y un sector central que tilea en X e Y) para redimensionar a placer
      • Cabecera:Texto centrado en la parte superior del diálogo (ejemplo: Abandonar juego)
      • Cuerpo:Parte central del diálogo. Puede estar compuesta por un texto centrado o por una imagen y un texto alineado a la izquierda situado a la derecha de la imagen
      • Botones:Soporte para uno, dos y tres botones, con nombres personalizables. Además, hay una serie de sets de botones muy utilizados que pueden activarse muy fácilmente: ok/cancel, yes/no, …
    • Posibilidad de especificar un style de Button y uno de Label para cada parte de texto (cabecera y cuerpo)
    • Lanza eventos onOpen, onClose y onClick (pasándo como parámetro el índice del botón pulsado)
    • Teclado: con los cursores izquierda y derecha se alterna el botón seleccionado
  • Scrollbar
    • Widget auxiliar para otros widgets: Text, TextArea y List
    • Se compone de varias partes:
      • Botones superior e inferior: cada uno actúa como un ImageButton. Por tanto pueden tener hasta 4 imágenes asignadas (idle, over, down, disabled)
      • Puntero: parte que se desliza por el scrollbar. Puede tener hasta 4 imágenes asignadas (lo de siempre..) y tal imagen se recortará en 3 sectores y se tileará la parte central para estirar el puntero hasta la anchura necesaria. Por lo tanto, el tamaño mínimo del puntero será la suma de los dos extremos (2/3 del ancho de esa imagen)
      • Fondo: imagen que se tileará en X o Y (según sea necesario) para rellenar el área entre un botón y otro
    • Lanza evento onThumbMoved indicando la cantidad de ítems que se ha desplazado. Es decir, el scrollbar sabe (por su widget padre) el alto o ancho que debe recorrer para que un nuevo item aparezca, y se autoregula para moverse en intérvalos de esa magnitud. Es decir, si tenemos que arrastrarlo 10px para que se muestre el siguiente item, y lo movemos 9 y soltamos, el thumb volverá a su estado anterior
    • Puede ser vertical u horizontal
  • Selector
    • Cadena de texto con una flecha a cada lado que sirven para iterar sobre una sequencia de valores de forma circular
    • Las dos flechas actúan como un ImageButton. Además son el mismo flipeado horizontalmente
    • Tiene el ancho necesario para acomodar el valor de texto mayor
    • La iteración puede ser cíclica (al pasar del último elemento, vuelve al primero) o no (simplemente no hay siguiente, la flecha aparece deshabilitada)

Soporte para UTF-8

Esto incluye a casi todos los lenguajes excepto los asiáticos: inglés, español, francés, ruso, griego, alemán… Todavía estoy pensando si vale la pena optar por Unicode y poder así ofrecer en el futuro versiones del juego en chino, japonés y demás. El problema principal es el renderizador de fuentes de PTK, que soporta UTF-8 pero no Unicode, aunque teniendo el código fuente siempre puedo añadirle soporte para esto yo mismo.

Aspecto de los widgets personalizable

Mediante el uso de estilos (o skins o themes) pretendo que el aspecto de cada widget sea totalmente personalizable, lo que será útil tanto para aportar variedad al juego, como a la hora de utilizar el sistema para próximos juegos. Prácticamente todo desde las fuentes de texto, las imágenes que se utilizarán como fondo o colores planos que se utilizarán en algunas ocasiones; será editable.

Estructura jerárquica

La clase widget tendrá funcionalidad para actuar como contenedora de otros widgets, por lo que podrán crearse relaciones jerárquicas entre widgets, de modo que podremos actuar sobre un grupo de widgets concreto para realizar ciertas tareas en vez de hacerlo individualmente: moverlos, mostrarlos, ocultarlos, deshabilitarlos, etc.

Identificación mediante cadenas de texto

El PopCap Framework utiliza IDs numéricos para identificar a los widgets, lo cual es más rápido computacionalmente pero también más engorroso ya que debes crear estos IDs en código vía enums para que sean comprensibles a un simple vistazo del código y luego asignarlos manualmente a cada widget, lo cual choca de frente con nuestro objetivo de realizar un sistema basado en definiciones externas.

Por lo tanto nosotros utilizaremos una cadena de texto para identificar inequívocamente cada widget. En nuestro sistema, un widget tendrá dos nombres (identificadores): uno relativo y otro absoluto. Pensad en ello como en un sistema de ficheros, el nombre absoluto sería la ruta desde la raíz. Así pues, el nombre relativo de un widget puede no ser único (por ejemplo, “Label”), pero el absoluto siempre debe serlo (”Options/FullscreenCheckbox/Label”). Luego dos widgets pueden tener el mismo nombre relativo si no tienen el mismo widget padre.

Definición de widgets y styles en XML

Soy un gran partidario de los sistemas data-driven, y últimamente suelo utilizar XML para definir datos estáticos en casi cada cosa que hago (animaciones, sistemas de partículas, archivos de configuración, niveles…); así que esto no será una excepción.

Habrá dos tipos de ficheros XML, uno para definir estilos (*.sty.xml) y otro para la estructura de widgets en sí (*.gui.xml).

Ejemplo de archivo de estilos:


<StyleFile>
    <LabelStyle>
        <Name>Estilo1</Name>
        <Font>FONT_VERDANA_8</Font>
        <Color r="1" g="0" b="0" />
        <LabelStyle>
            <Name>Estilo1_Azul</Name>
            <Color r="0" g="0" b="1" />
        </LabelStyle>
    </LabelStyle>
</StyleFile>

Nótese como es posible anidar estilos dentro de otros. Esto sólo puede hacerse con estilos del mismo tipo, y sirve para heredar atributos definidos anteriormente sin tener que repetirlos. Es decir, el estilo “Estilo1_Azul” del ejemplo utilizaría también la fuente FONT_VERDANA_8. Sin embargo, cabe destacar que esta estructura en la definición XML no implica que dentro de la aplicación ambos estilos tengan relación ninguna, su único propósito es ahorrar tiempo a la hora de crear las definiciones de estilos.

Ejemplo de archivo de widgets:


<GuiFile>
    <Image>
        <Name>SplashScreen</Name>
        <Rect x="0" y="0" width="800" height="640" />
        <Source>IMAGE_COMPANY_LOGO</Source>
    </Image>
    <Dialog>
        <Name>Options</Name>
        <Style>BasicDialogStyle</Style>
        <Rect width="440" height="360" />
        <Header>OPTIONS</Header>
        <Button1>APPLY</Button1>
        <Modal>true</Modal>
        <Layout>
            <HCenter />
            <VCenter />
            <RelativeTo>SplashScreen</RelativeTo>
        </Layout>
        <Slider>
            <Name>MusicVolume</Name>
            <Style>BasicSliderStyle</Style>
            <Rect y="75" width="254" height="20" />
            <Value>0.85</Value>
            <Layout>
                <HCenter />
            </Layout>
            <Label>
                <Name>Label</Name>
                <Style>LabelForDialogs</Style>
                <Rect width="10" height="10" />
                <Value>Music volume</Value>
                <Layout>
                    <Above />
                    <HCenter />
                </Layout>
            </Label>
        </Slider>
        <Checkbox>
            <Name>Fullscreen</Name>
            <Style>Checkbox</Style>
            <Rect width="24" height="25" />
            <Checked>false</Checked>
            <Layout>
                <RelativeTo>Options/MusicVolume</RelativeTo>
                <SameRight />
                <Below />
                <Padding y="20" />
            </Layout>
            <Label>
                <Name>Label</Name>
                <Style>LabelForDialogs</Style>
                <Rect width="10" height="10" />
                <Value>Full Screen:</Value>
                <Layout>
                    <SameTop />
                    <Left />
                    <Padding x="-10" />
                </Layout>
            </Label>
        </Checkbox>
    </Dialog>
</GuiFile>

En este fichero hemos definido dos jerarquías de widgets. Aquella cuya razí es el widget “SplashScreen” de tipo Image, y que está formada solamente por él; y aquella cuya raíz es el Dialog llamado “Options”. Esta última es una jerarquía más compleja varios widgets organizados en tres niveles de descendencia. Podemos apreciar también el uso de la etiqueta Layout para alinear o redimensionar widgets en relación a otros (si no se especifica ninguno se utiliza su padre).

Tooltips

Un tooltip es un pequeño mensaje que aparece en la posición del ratón cuando este se deja unos segundos sobre algún widget. Puesto que estoy preparando un juego de gestión, con una cierta complejidad al comenzar a jugar, toda ayuda integrada en el juego es bienvenida. Siempre hay que recordar los dos principios básicos del diseño de interfaces:

  1. Los usuarios no tienen el manual, y si lo tuvieran, no lo leerían.
  2. De hecho, los usuarios no pueden leer nada, y si pudieran, no querrían.

Widgets modales

Se conoce como widgets modales aquellos controles que acaparan todos los eventos de entrada del usuario. El típico ejemplo es el del diálogo modal, que no deja al usuario interactuar con ninguna aplicación hasta que este escoje entre una de las opciones que el diálogo le propone (sí/no, aceptar/cancelar, etc.).

Diálogos procedurales

En muchas ocasiones, y más en este tipo de juegos, es necesario obtener una respuesta inmediata del usuario de una manera visual, y la manera común de hacer esto en entornos gráficos es con un diálogo modal. Sin embargo, si tuviéramos que definir cada uno de ellos en XML sería una tarea tediosa. Por eso necesitamos un mecanismo para crear diálogos procedurales al vuelo. Para mayor flexibilidad, estos diálogos se compondrán de distintas partes configurables a la hora de crearlo: título de la ventana, mensaje, imagen (opcional), nombre del diálogo (para identificarlo al recibir eventos suyos) y los botones que mostrará (uno, dos o tres).

El método para crearlo podría ser algo así:

Dialog* GuiManager::CreateDialog(string theName, string theTitle, string theMessage, int theButtonsType, string theButton1="", string theButton2="", string theButton3="", string theImageId="");

Los valores posibles para el parámetro theButtonsType son YES_NO, OK_CANCEL, YES_NO_CANCEL y CUSTOM. El comportamiento de las tres primeras es bastante obvio. La tercera indica que los valores de los botones se proporcionarán con los parámetros theButton1, theButton2 y theButton3. Dejando uno vacío (”") simplemente no lo activa, por lo que se pueden tener diálogos de uno a tres botones personalizados.

Una llamada ejemplo:

Dialog* aDialog = mGuiManager->CreateDialog("QuitDlg", "Want to Quit?", "Are you sure you want to quit?", Dialog::YES_NO);

Notificación mediante estados

Hay varias formas de recibir los eventos de los widgets accionados por los usuarios (clicks, cambio de selección en una lista, introducción de texto en un textbox ,etc.) que se utilizan comunmente en las aplicaciones que hacen uso de estos elementos de interfaz: callbacks, arquitectura de eventos/casters/listeners o el más novedoso sistema de signals y slots popularizado por la librería Qt de Trolltech. Sin embargo, al menos para juegos, suelo utilizar una solución que creo que es más sencilla e intuitiva de cara a separar la funcionalidad que pertenece a cada pantalla (o estado) en el que puede encontrarse la interfaz del juego (pantalla de logín, ventana de opciones, etc.) y por tanto resulta en un código más limpio y estructurado.

La idea es muy sencilla. Tenemos una clase base GuiState, con un método para cada evento posible recibido desde un widget, y alguno más:


class GuiState
{
public:
void ButtonClick(Button* sender) {}
void CheckboxToggle(Checkbox* sender) {}
void ListSelectedChange(ListBox* sender) {}
void RadioSelectedChange(RadioGroup* sender) {}
void TextChange(TextBox* sender) {}
[...]
void Enter() {}
void Leave() {}
void Activate() {}
void Deactivate() {}
};

El método Enter() es llamado cuando se entra al estado (por ejemplo, cuando se abre la ventana de las Opciones). El método Leave() cuando se sale del estado (se cierra la ventana). Si el estado sigue abierto pero “pierde el foco” porque entramos en otro estado (por ejemplo, abrimos otra ventana dentro de las Opciones), se llama al método Deactivate(). También se llama a este método justo antes de llamar a Leave(). Cuando cerrásemos esa ventana secundaria y volviéramos a las Opciones, se llamaría al método Activate(), así como inmediatamente después de Enter().

Todos los widgets de la aplicación mandan sus eventos al gestor de GUI (clase GuiManager), que a su vez los redirecciona al estado de GUI activo en ese momento. Este gestor mantiene una pila de los estados abiertos hasta el momento. El estado en la cima de la pila, es el que recibe los eventos. El hecho de mantener una pila de todos los estados abiertos nos permite retroceder en el mismo orden en que los hemos ido abriendo, lo cual es útil para restablecer estados previos una vez salimos de otros que abrimos sólo temporalmente (por ejemplo, mostrar la ventana de opciones en medio del juego).

Ejemplo de estado:

class OptionsState : GuiState
{
public:
bool mFullscreen;
    
void ButtonClick(Button* sender)
{
  if (sender->GetName() == "Apply")
   gApp->SetFullscreenMode(mFullscreen);
  else // "Cancel"
    (Checkbox*)(mWidgetMgr->GetWidget("Options/Fullscreen"))->mChecked = mFullscreen;
  // solo hay boton Apply y Cancel, asi que salimos del estado de todos modos
  mGuiMgr->PopState();
}
void CheckboxToggle(Checkbox* sender)
{
  if (sender->GetName() == "Fullscreen")
   mFullscreen = sender->mChecked;
}
void Enter()    
{
  mWidgetManager->AddWidget("Options");    
  mFullscreen = mWidgetMgr->GetWidget("Options/Fullscreen"))->mChecked;
}
void Leave()    
{
  mWidgetManager->RemoveWidget("Options");    
}
};

Ya he descrito las características principales que quiero que tenga el sistema de GUI, que era el objetivo de este primer artículo. En el próximo artículo crearemos las clases bases principales que actuarán de pilares de todo el sistema.

Si tenéis dudas o sugerencias para mejorar el sistema no dudéis en añadir algún comentario.

2 respuestas a “Mejorando Tu Engine IV - GUI: Especificaciones”

  1. Pogacha dice:

    Es un proyecto bastante ambicioso, te deseo la mejor de las suertes.

  2. Amatar dice:

    Muy interesante, coincidimos en algunas conclusiones pero te recomiendo que le heches un vistazo a los sistemas de eventos de algunos juegos (me viene a la cabeza el wow, que hace uso intensivo de GUI, tiene buenas ideas implementadas y hay tutoriales de uso y modificacion por la red).

Deja una respuesta