Diseño del sistema de mensajería del juego

8

Estoy haciendo un juego simple y he decidido intentar implementar un sistema de mensajería.

El sistema básicamente se ve así:

La entidad genera el mensaje -> el mensaje se publica en la cola de mensajes global -> messageManager notifica a cada objeto del nuevo mensaje a través de onMessageReceived (Message msg) -> si el objeto lo desea, actúa sobre el mensaje.

La forma en que estoy creando objetos de mensaje es así:

//base message class, never actually instantiated
abstract class Message{
  Entity sender;
}

PlayerDiedMessage extends Message{
  int livesLeft;
}

Ahora mi SoundManagerEntity puede hacer algo como esto en su método onMessageReceived ()

public void messageReceived(Message msg){
  if(msg instanceof PlayerDiedMessage){
     PlayerDiedMessage diedMessage = (PlayerDiedMessage) msg;
     if(diedMessage.livesLeft == 0)
       playSound(SOUND_DEATH);
  }
}

Los pros de este enfoque:

  1. Muy simple y fácil de implementar.
  2. El mensaje puede contener tanta información como desee, ya que puede crear una nueva subclase de mensaje que tenga la información necesaria.

Los contras:

  1. No puedo entender cómo puedo reciclar los objetos de mensaje a un grupo de objetos , a menos que tenga un grupo diferente para cada subclase de mensaje. Así que tengo mucha, mucha creación de objetos / asignación de memoria con el tiempo.
  2. No puedo enviar un mensaje a un destinatario específico, pero aún no lo he necesitado en mi juego, así que no me importa demasiado.

¿Que me estoy perdiendo aqui? Debe haber una mejor implementación o alguna idea que me falta.

you786
fuente
1) ¿Por qué necesita utilizar esos grupos de objetos, cualesquiera que sean? (En una nota al margen, ¿qué son y para qué sirven?) No hay necesidad de diseñar más allá de lo que funciona. Y lo que tienes actualmente parece no tener problemas con eso. 2) Si necesita enviar un mensaje a un destinatario específico, lo encuentra y llama a onMessageReceived, no se requieren sistemas sofisticados.
serpiente5
Bueno, quise decir que actualmente no puedo reciclar los objetos en un grupo de objetos de propósito general. Entonces, si alguna vez quiero reducir las asignaciones de memoria necesarias, no puedo. En este momento, esto no es realmente un problema porque mi juego es tan simple que funciona perfectamente bien.
you786
¿Asumo que esto es Java? Parece que estás tratando de desarrollar un sistema de eventos.
Justin Skiles
@JustinSkiles Correcto, esto es Java. Supongo que tienes razón, realmente se usa para actuar como un sistema de eventos. ¿De qué otras formas puedes usar un sistema de mensajería?
you786

Respuestas:

11

La respuesta simple es que no desea reciclar mensajes. Uno recicla objetos que son creados (y eliminados) por miles cada cuadro, como partículas, o que son muy pesados ​​(decenas de propiedades, largo proceso de inicialización).

Si tiene una partícula de nieve con algún mapa de bits, velocidades, atributos físicos asociados que se ubicaron debajo de su vista, es posible que desee reciclarla en lugar de crear una nueva partícula e inicializar el mapa de bits y aleatorizar nuevamente los atributos. En su ejemplo, no hay nada útil en el Mensaje para mantenerlo, y desperdiciará más recursos para eliminar sus propiedades específicas de la instancia, de lo que gastaría en crear un nuevo objeto Mensaje.

Markus von Broady
fuente
3
Buena respuesta. Estoy usando un grupo para mis mensajes, y nunca investigué si eran útiles o no. (Pasado) tiempo para el perfil!
Raptormeat
Hmm ¿Qué pasa si tengo un mensaje que se emite quizás cada cuadro? Supongo que la respuesta sería tener un grupo específico para ese tipo de objeto, lo cual tiene sentido.
you786
@ you786 Acabo de hacer una prueba rápida y sucia en Actionscript 3. Crear una matriz y llenarla con 1000 mensajes lleva 1 ms en un entorno de depuración lento. Espero que eso aclare las cosas.
Markus von Broady
@ you786 Él tiene razón, primero identifica esto como un cuello de botella, si quieres ser un desarrollador de juegos independiente, debes aprender a hacer las cosas de manera rápida y efectiva. Invertir tiempo en ajustar su código solo vale la pena si ese código actualmente está ralentizando su juego y hacer estos cambios puede aumentar significativamente el rendimiento. Por ejemplo, crear instancias de Sprtie en AS3 llevaría mucho tiempo, es mucho mejor reciclarlas. Crear objetos Point no lo es, reciclarlos sería una pérdida de tiempo de codificación y legibilidad.
AturSams
Bueno, debería haber dejado esto más claro, pero no me preocupa la asignación de memoria. Me preocupa que el recolector de basura de Java se ejecute en el medio del juego y cause ralentizaciones. Pero su punto sobre la optimización prematura sigue siendo válido.
you786
7

En mi juego basado en Java, se me ocurrió una solución muy similar, excepto que mi código de manejo de mensajes se ve así:

@EventHandler
public void messageReceived(PlayerDiedMessage msg){
  if(diedMessage.livesLeft == 0)
     playSound(SOUND_DEATH);
}

Utilizo anotaciones para marcar un método como controlador de eventos. Luego, al inicio del juego, utilizo la reflexión para calcular un mapeo (o, como lo llamo, "tubería") de mensajes, qué método invocar sobre qué objeto cuando se envía un evento de una clase específica. Esto funciona ... genial.

El cálculo de la tubería puede ser un poco complicado si desea agregar interfaces y subclases a la mezcla, pero de todos modos es bastante sencillo. Hay una ligera desaceleración durante la carga del juego cuando todas las clases necesitan ser escaneadas, pero se hace solo una vez (y probablemente se puede mover a un hilo separado). En cambio, lo que se gana es que el envío real de mensajes es más barato: no tengo que invocar todos los métodos "messageReceived" en la base de código solo para que compruebe si el evento es de buena clase.

Liosan
fuente
3
Entonces, ¿básicamente encontraste una manera de que tu juego se ejecute más lento? : D
Markus von Broady
3
@ MarkusvonBroady Si alguna vez está en carga, vale la pena. Compare eso con la monstruosidad en mi respuesta.
michael.bartnett
1
@MarkusvonBroady La invocación a través de la reflexión es solo un poco más lenta que la invocación normal, si tiene todas las piezas en su lugar (al menos mi perfil no muestra ningún problema allí). El descubrimiento es costoso, seguro.
Liosan
@ michael.bartnett Codifico para un usuario, no para mí. Puedo vivir con mi código siendo más grande para una ejecución más rápida. Pero ese fue solo mi comentario subjetivo.
Markus von Broady
+1, el camino de las anotaciones es algo en lo que nunca pensé. Entonces, solo para aclarar, básicamente tendrías un montón de métodos sobrecargados que se llaman messageReceived con diferentes subclases de Message. Luego, en tiempo de ejecución, utiliza la reflexión para crear algún tipo de mapa que calcule MessageSubclass => OverloadedMethod?
you786
5

EDITAR La respuesta de Liosan es más sexy. Mirar dentro.


Como dijo Markus, los mensajes no son un candidato común para los grupos de objetos. No te molestes por las razones que mencionó. Si tu juego envía toneladas de mensajes de tipos específicos, entonces quizás valga la pena. Pero luego, en ese punto, te sugiero que cambies a llamadas de método directo.

Hablando de que,

No puedo enviar un mensaje a un destinatario específico, pero aún no lo he necesitado en mi juego, así que no me importa demasiado.

Si conoce a un destinatario específico al que desea enviarlo, ¿no sería bastante fácil obtener una referencia a ese objeto y hacer una llamada directa al método? También puede ocultar objetos específicos detrás de las clases de administrador para facilitar el acceso a través de los sistemas.

Hay una desventaja en su implementación, y es que existe la posibilidad de que muchos objetos reciban mensajes que realmente no les interesan. Idealmente, sus clases solo recibirían mensajes que realmente les interesen.

Puede usar a HashMap<Class, LinkedList<Messagable>>para asociar tipos de mensajes con una lista de objetos que desean recibir ese tipo de mensaje.

Suscribirse a un tipo de mensaje se vería así:

globalMessageQueue.subscribe(PlayerDiedMessage.getClass(), this);

Podrías implementar subscribeasí (perdóname algunos errores, no he escrito Java en un par de años):

private HashMap<Class, LinkedList<? extends Messagable>> subscriberMap;

void subscribe(Class messageType, Messagable receiver) {
    LinkedList<Messagable> subscriberList = subscriberMap.get(messageType);
    if (subscriberList == null) {
        subscriberList = new LinkedList<? extends Messagable>();
        subscriberMap.put(messageType, subscriberList);
    }

    subscriberList.add(receiver);
}

Y luego podrías transmitir un mensaje como este:

globalMessageQueue.broadcastMessage(new PlayerDiedMessage(42));

Y broadcastMessagepodría implementarse así:

void broadcastMessage(Message message) {
    Class messageType = message.getClass();
    LinkedList<? extends Messagable> subscribers = subscriberMap.get(messageType);

    for (Messagable receiver : subscribers) {
        receiver.onMessage(message);
    }
}

También puede suscribirse al mensaje de tal manera que pueda dividir el manejo de su mensaje en diferentes funciones anónimas:

void setupMessageSubscribers() {
    globalMessageQueue.subscribe(PlayerDiedMessage.getClass(), new Messagable() {
        public void onMessage(Message message) {
            PlayerDiedMessage msg = (PlayerDiedMessage)message;
            if (msg.livesLeft <= 0) {
                doSomething();
            }
        }
    });

    globalMessageQueue.subscriber(EnemySpawnedMessage.getClass(), new Messagable() {
        public void onMessage(Message message) {
            EnemySpawnedMessage msg = (EnemySpawnedMessage)message;
            if (msg.isBoss) {
                freakOut();
            }
        }
    });
}

Es un poco detallado, pero ayuda con la organización. También podría implementar una segunda función, subscribeAllque mantendrá una lista separada de mensajes de Messagablecorreo electrónico que quieran escuchar sobre todo.

michael.bartnett
fuente
1

1 . No lo hagas

Los mensajes son de bajo costo. Estoy de acuerdo con Markus von Broady. GC los recogerá rápido. Intenta no adjuntar mucha información adicional para los mensajes. Son solo mensajes. Encontrar instancia de es una información en sí misma. Úselo (como lo hizo en su ejemplo).

2 . Podría intentar usar MessageDispatcher .

A diferencia de la primera solución, podría tener un despachador de mensajes centralizado que procesa los mensajes y los pasa a los objetos deseados.

edin-m
fuente