Mejorando Tu Engine III - Eventos

El concepto de eventos que utilizo en mi motor es algo similar a la típica programación basada en eventos común en otros entornos de desarrollo como Visual Basic o Delphi, sólo que implementado de forma distinta. Al igual que los triggers, es un mecanismo útil y sencillo para implementar la lógica de juego de una forma no tradicional.

¿Qué es un evento?

Un evento es un mensaje que se envia de forma inmediata a todo aquel que le interese recibirlo, ya sea nadie, uno o varios destinatarios. Además tenemos la posibilidad de adjuntar datos a este mensaje creando clases derivadas personalizadas. Cada evento tiene también un filtro compuesto por dos máscaras de bits, de modo que los destinatarios puedan escoger fácilmente.

class Event
{
    static const Mask NONE = 0;
    static const Mask ANY = ~0; // todos los bits a 1
 
    Event(Mask theMask, EventSender* theSender = 0, Mask theSubMask = 1);
 
    Mask mMask;
    Mask mSubMask;
    EventSender* mSender;
}
 
class EventListener
{
    EventListener(Mask theFilter, Mask theSubFilter = 1);
 
    bool Notify(Event* theEvent) = 0;
 
    Mask mFilter;
    Mask mSubFilter;
    bool mRegistered;
}

La clase es muy sencilla pero cumple bien su cometido. Supongo que el propósito de las máscaras será obvio pero aún así diré que sirven para filtrar los eventos que llegan a los listeners. Dependiendo de la cantidad de eventos distintos que utilices en tu aplicación el subfiltro te será útil o no, en todo caso siempre puedes ignorarlo. Si tienes muchos puedes utilizar la máscara primaria para identificar una categoría y la secundaria para el tipo específico, por ejemplo:

NOTA: La macro BIT() simplemente realiza un desplazamiento de bits, no utilizo el operador porque Wordpress se empeña en cambiármelo por su correspondiente código HTML.

// Event Categories
const Mask PLAYER = BIT(2);
const Mask ENEMY  = BIT(3);
 
// Event Types
const Mask SPAWN  = BIT(2);
const Mask DIE    = BIT(3);
const Mask HIT    = BIT(4);

De este modo un listener interesado sólo en eventos relacionados con el jugador utilizaría el par de filtros [player , Event::ANY], pues la condición para recibir un evento es que se cumplan ambas máscaras:

if ((event->mMask & listener->mFilter) && (event->mSubMask & listener->mSubFilter))
    listener->Notify(event);

Por cierto, la clase EventSender es una clase vacía, es simplemente para utilizar algo más significativo semánticamente que un puntero a void, aunque luego tengas que hacer lo mismo con ambos, hacer un downcast a una clase a la que estés seguro que pertenece, si necesitas hacer uso de él.

¿Quién hace llegar los eventos a los listeners?

Seguro que si habéis leído los dos artículos anteriores de la serie ya sabéis como se llama esta clase. Efectivamente, ¡EventManager! Y por supuesto es tan sencilla como los que hemos visto hasta ahora:

class EventManager
{
    void SendEvent(Event* theEvent);
    void RegisterListener(EventListener* theListener);
    void UnregisterListener(EventListener* theListener);
 
    bool mEnabled;
    ListenerList mListeners;
}

Supongo que el código se explica por sí solo, como todo lo que hemos visto hasta el momento. La propiedad mEnabled simplemente sirve para hacer que no se envíen eventos si está a false, y por supuesto por defecto está con el valor contrario. También decir que los EventListener se registran automáticamente en su constructor y hacen lo contrario en su destructor, aunque si no te interesa que reciban eventos en un determinado momento puedes hacerlo manualmente llamando al manager o simplemente poniendo uno de sus filtros a cero.

Métodos alternativos de conseguir lo mismo

Existen otras formas de conseguir la misma funcionalidad, el envío de mensajes por parte de un emisor a una serie de destinatarios que el mismo emisor desconoce. Quizá el más antiguo sea simplemente usar callbacks, punteros a funciones, a las que el emisor simplemente se limita a llamar. El principal problema de este sistema es que es bastante limitado y se complica bastante cuando quieres hacer que el callback sea el método de un objeto en vez de una función independiente.

Una posibilidad alternativa basada en el mismo concepto de los callbacks pero muy mejorada de cara al uso es el sistema de signal y slot, popularizado por la librería Qt de Trolltech. Está fuera del propósito de este artículo discutir la creación de un sistema similar, pero hay varias librerías de código abierto disponibles a las que podéis echarles un vistazo. Para los fans de Boost hay una librería en ese mismo paquete llamado boost.signals, pero si lo de añadir dependencias mastodónticas a tu proyecto no es lo tuyo (te entiendo) también tienes una librería muy ligera de la que he oído hablar muy bien, sigslot.

Pues esto ha sido todo por hoy. Soy consciente de que el artículo me ha salido muy corto, pero la verdad es que es un tema demasiado simple como para extenderse más. De hecho tenía mis dudas sobre si daba para un artículo por sí solo pero prefiero dedicar cada uno a un tema específico independientemente de la magnitud del mismo.

Como siempre, podéis descargar el código fuente de lo visto hoy desde aquí.

Deja una respuesta