Advertisement
  1. Game Development
  2. Game Engine Development
Gamedevelopment

Cómo implementar y utilizar una cola de mensajes en su juego

by
Difficulty:IntermediateLength:LongLanguages:

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

Un juego generalmente se hace de varias entidades diferentes que interactúan entre sí. Esas interacciones tienden a ser muy dinámicas y profundamente conectadas con el juego. Este tutorial cubre el concepto y la implementación de un sistema de cola de mensajes que puede unificar las interacciones de las entidades, haciendo que su código sea manejable y fácil de mantener a medida que crece en complejidad.

Introducción

Una bomba puede interactuar con un personaje explotando y causando daño, un kit médico puede sanar a una entidad, una llave puede abrir una puerta, y así sucesivamente. Las interacciones en un juego son interminables, pero ¿cómo podemos mantener el código del juego manejable mientras somos capaces de manejar todas esas interacciones? ¿Cómo podemos garantizar que el código puede cambiar y seguir funcionando cuando surgen nuevas e inesperadas interacciones?

Interactions in a game tend to grow in complexity very quickly
Las interacciones en un juego tienden a crecer en complejidad muy rápidamente.

A medida que se agregan interacciones (especialmente las inesperadas), su código se verá cada vez más desordenado. Una implementación ingenua le llevará rápidamente a hacer preguntas como:

"Esta es la entidad A, por lo que debería llamar al método damage() en él, ¿verdad? o ¿es damageByItem()? Tal vez este método damageByWeapon() es el correcto?"

Imagínese que el caos desordenado se extiende a todas sus entidades de juego, porque todos interactúan entre sí de maneras diferentes y peculiares. Por suerte, hay una manera mejor, más simple y más manejable de hacerlo.

Cola de mensajes

Introduzca la cola de mensajes. La idea básica detrás de este concepto es implementar todas las interacciones del juego como un sistema de comunicación (que todavía está en uso hoy en día): el intercambio de mensajes. Las personas se han comunicado a través de mensajes (cartas) durante siglos porque es un sistema eficaz y simple.

En nuestros servicios reales, el contenido de cada mensaje puede diferir, pero la forma en que se envían y reciben físicamente sigue siendo la misma. Un remitente pone la información en un sobre y la dirige a un destino. El destino puede responder (o no) siguiendo el mismo mecanismo, simplemente cambiando los campos "desde / hacia" en el sobre.

Interactions made using a message queue system
Interacciones realizadas utilizando un sistema de cola de mensajes.

Al aplicar esa idea a tu juego, todas las interacciones entre entidades pueden ser vistas como mensajes. Si una entidad de juego desea interactuar con otra (o con un grupo de ellas), todo lo que tiene que hacer es enviar un mensaje. El destino se ocupará o reaccionará al mensaje basándose en su contenido y en quién es el remitente.

En este enfoque, la comunicación entre entidades de juego se vuelve unificada. Todas las entidades pueden enviar y recibir mensajes. No importa cuán compleja o peculiar sea la interacción o mensaje, el canal de comunicación siempre permanece igual.

A lo largo de las siguientes secciones, describiré cómo puedes implementar realmente este enfoque de cola de mensajes en tu juego.

Diseño de una Envoltura (Mensaje)

Comencemos diseñando la envoltura, que es el elemento más básico en el sistema de cola de mensajes.

Una envoltura puede describirse como en la siguiente figura:

Structure of a message
Estructura de un mensaje.

Los dos primeros campos (sender y destination) son referencias a la entidad que creó ya la entidad que recibirá este mensaje, respectivamente. Usando esos campos, tanto el remitente como el receptor pueden decir a dónde va el mensaje y de dónde vino.

Los otros dos campos (type y data) trabajan juntos para asegurar que el mensaje se maneja correctamente. El campo type describe de qué se trata este mensaje; Por ejemplo, si el tipo es "damage", el receptor manejará este mensaje como un orden para disminuir sus puntos de salud; Si el tipo es "pursue", el receptor lo tomará como una instrucción para perseguir algo—y así sucesivamente.

El campo data se conecta directamente al campo type. Utilizando los ejemplos anteriores, si el tipo de mensaje es "damage", entonces el campo data contendrá un número—digamos, 10—que describe la cantidad de daño que el receptor debe aplicar a sus puntos de salud. Si el tipo de mensaje es "pursue", data contendrá un objeto que describa el objetivo que se debe perseguir.

El campo data puede contener cualquier información, lo que hace que el sobre sea un medio versátil de comunicación. Cualquier cosa se puede colocar en ese campo: números enteros, flotadores, cadenas, e incluso otros objetos. La regla es que el receptor debe saber cuál está en el campo data basado en cuál está en el campo type.

Toda esa teoría se puede traducir en una clase muy simple llamada Message. Contiene cuatro propiedades, una para cada campo:

Como ejemplo de esto en uso, si una entidad A quiere enviar un mensaje de "damage" a la entidad B, todo lo que tiene que hacer es instanciar un objeto de la clase Message, establecer la propiedad to a B, establecer la propiedad from (Entidad A), establecer type a "damage" y, finalmente, establecer data a algún número (10, por ejemplo):

Ahora que tenemos una manera de crear mensajes, es hora de pensar en la clase que los almacenará y entregará.

Implementación de una cola

La clase responsable de almacenar y entregar los mensajes se llamará MessageQueue. Funcionará como una oficina de correos: todos los mensajes se entregan a esta clase, lo que garantiza que serán enviados a su destino.

Por ahora, la clase MessageQueue tendrá una estructura muy simple:

La propiedad messages es un array. Almacena todos los mensajes que están a punto de ser entregados por MessageQueue. El método add() recibe un objeto de la clase Message como un parámetro, y agrega ese objeto a la lista de mensajes.

He aquí cómo nuestro ejemplo anterior de la entidad A de la entidad de mensajería B sobre el daño funcionaría usando la clase MessageQueue:

Ahora tenemos una manera de crear y almacenar mensajes en una cola. Es hora de que lleguen a su destino.

Entrega de mensajes

Para que la clase MessageQueue realmente despache los mensajes publicados, primero debemos definir cómo las entidades manejarán y recibirán mensajes. La forma más fácil es agregar un método denominado onMessage() a cada entidad capaz de recibir mensajes:

La clase MessageQueue invocará el método onMessage() de cada entidad que debe recibir un mensaje. El parámetro pasado a ese método es el mensaje que está siendo entregado por el sistema de colas (y que está siendo recibido por el destino).

La clase MessageQueue enviará los mensajes en su cola de una vez, en el método dispatch():

Este método itera sobre todos los mensajes de la cola y, para cada mensaje, el campo to se utiliza para buscar una referencia al receptor. El método onMessage() del receptor se invoca, con el mensaje actual como un parámetro, y el mensaje entregado se quita de la lista MessageQueue. Este proceso se repite hasta que se envíen todos los mensajes.

Uso de una cola de mensajes

Es hora de ver todos los detalles de esta implementación trabajando juntos. Utilizemos nuestro sistema de colas de mensajes en una demo muy simple compuesta por unas pocas entidades móviles que interactúan entre sí. En aras de la simplicidad, vamos a trabajar con tres entidades: Healer, Runner y Hunter.

El Runner tiene una barra de salud y se mueve alrededor aleatoriamente. El Healer curará a cualquier Runner que pase cerca; Por otro lado, el Hunter infligirá daño a cualquier Runner cercano. Todas las interacciones serán manejadas usando el sistema de cola de mensajes.

Agregar la cola de mensajes

Empecemos por crear el PlayState que contiene una lista de entidades (curanderos, corredores y cazadores) y una instancia de la clase MessageQueue:

En el bucle de juego, representado por el método update(), se invoca el método dispatch() de colas de mensajes, por lo que todos los mensajes se entregan al final de cada marco de juego.

Añadiendo los Runners

La clase Runner tiene la siguiente estructura:

La parte más importante es el método onMessage(), invocado por la cola de mensajes cada vez que hay un mensaje nuevo para esta instancia. Como se explicó anteriormente, el campo type en el mensaje se utiliza para decidir de qué se trata esta comunicación.

Basándose en el tipo de mensaje, se realiza la acción correcta: si el tipo de mensaje es "damage", los puntos de salud se reducen; Si el tipo de mensaje es "heal", los puntos de salud se incrementan. El número de puntos de salud para aumentar o disminuir por es definido por el remitente en el campo data del mensaje.

En PlayState, añadimos algunos corredores a la lista de entidades:

El resultado son cuatro corredores que se mueven al azar:

Añadiendo el Cazador

La clase Hunter tiene la siguiente estructura:

Los cazadores también se moverán, pero causarán daño a todos los corredores que están cerca. Este comportamiento se implementa en el método update(), donde todas las entidades del juego son inspeccionadas y los corredores reciben mensajes sobre el daño.

El mensaje de daño se crea de la siguiente manera:

El mensaje contiene la información sobre el destino (entity, en este caso, que es la entidad que se analiza en la iteración actual), el remitente (this, que representa al cazador que está realizando el ataque), el tipo del mensaje ("damage") y la cantidad de daño (2, en este caso, asignado al campo data del mensaje).

El mensaje se contabiliza en el destino mediante el comando this.getMessageQueue().add(msg), que agrega el mensaje recién creado a la cola de mensajes.

Finalmente, agregamos Hunter a la lista de entidades en el PlayState:

El resultado es que unos cuantos corredores se mueven, recibiendo mensajes del cazador a medida que se acercan:

He añadido los sobres volantes como una ayuda visual para ayudar a mostrar lo que está pasando.

Añadiendo el Curador

La clase Healer tiene la siguiente estructura:

El código y la estructura son muy similares a la clase Hunter, a excepción de algunas diferencias. De forma similar a la implementación del cazador, el método update() del curandero itera sobre la lista de entidades en el juego, enviando mensajes a cualquier entidad dentro de su alcance de curación:

El mensaje también tiene un destino (entity), un remitente (this, que es el sanador que realiza la acción), un tipo de mensaje ("heal") y el número de puntos de curación (2, asignados en el campo data del mensaje).

Añadimos el Healer a la lista de entidades en el PlayState de la misma manera que hicimos con el Hunter y el resultado es una escena con corredores, un cazador y un curandero:

¡Y eso es! Tenemos tres entidades diferentes que interactúan entre sí intercambiando mensajes.

Discusión sobre flexibilidad

Este sistema de colas de mensajes es una forma versátil de gestionar interacciones en un juego. Las interacciones se realizan a través de un canal de comunicación que está unificado y tiene una única interfaz que es fácil de usar e implementar.

A medida que su juego crece en complejidad, nuevas interacciones podrían ser necesarias. Algunos de ellos pueden ser completamente inesperados, por lo que debe adaptar su código para hacer frente a ellos. Si está utilizando un sistema de cola de mensajes, se trata de agregar un nuevo mensaje en algún lugar y manejarlo en otro.

Por ejemplo, imagina que quieres hacer que el Hunter interactúe con el Healer; Sólo tienes que hacer que el Hunter envíe un mensaje con la nueva interacción—por ejemplo, "flee"—y asegúrate de que el Healer pueda manejarlo en el método onMessage:

¿Por qué no sólo enviar mensajes directamente?

Aunque el intercambio de mensajes entre las entidades puede ser útil, podrías estar pensando por qué el MessageQueue es necesario después de todo. ¿No puede simplemente invocar el método onMessage() del receptor usted mismo en lugar de depender de MessageQueue, como en el código de abajo?

Definitivamente podría implementar un sistema de mensajes como ese, pero el uso de un MessageQueue tiene algunas ventajas.

Por ejemplo, al centralizar el envío de mensajes, puede implementar algunas funciones interesantes como mensajes retrasados, la capacidad de enviar mensajes a un grupo de entidades e información de depuración visual (como los sobres volantes utilizados en este tutorial).

Hay espacio para la creatividad en la clase MessageQueue, depende de usted y de los requisitos de su juego.

Conclusion

Manejar las interacciones entre las entidades del juego usando un sistema de cola de mensajes es una manera de mantener su código organizado y listo para el futuro. Las nuevas interacciones pueden agregarse fácil y rápidamente, incluso sus ideas más complejas, siempre y cuando se encapsulen como mensajes.

Como se explica en el tutorial, puede ignorar el uso de una cola de mensajes central y simplemente enviar mensajes directamente a las entidades. También puede centralizar la comunicación mediante un despacho (la clase MessageQueue en nuestro caso) para dejar espacio para nuevas características en el futuro, como mensajes retrasados.

Espero que encuentre útil este método y lo agregue a su cinturón de utilidad para desarrolladores de juegos. El método puede parecer un exceso para los proyectos pequeños, pero sin duda le ahorrará algunos dolores de cabeza en el largo plazo para los juegos más grandes.

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.