Design do Sistema de Mensagens do Jogo

8

Estou fazendo um jogo simples e decidi tentar implementar um sistema de mensagens.

O sistema basicamente se parece com isso:

A entidade gera a mensagem -> a mensagem é postada na fila de mensagens global -> messageManager notifica todos os objetos da nova mensagem através de onMessageReceived (Message msg) -> se o objeto desejar, ele atua na mensagem.

A maneira como estou criando objetos de mensagem é assim:

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

PlayerDiedMessage extends Message{
  int livesLeft;
}

Agora meu SoundManagerEntity pode fazer algo assim no método onMessageReceived ()

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

Os profissionais dessa abordagem:

  1. Muito simples e fácil de implementar
  2. A mensagem pode conter as informações que você desejar, porque você pode apenas criar uma nova subclasse de Mensagens com as informações necessárias.

Os contras:

  1. Não consigo descobrir como reciclar objetos Message para um pool de objetos , a menos que eu tenha um pool diferente para cada subclasse de Message. Portanto, tenho imensas criações de objetos / alocação de memória ao longo do tempo.
  2. Não consigo enviar uma mensagem para um destinatário específico, mas ainda não precisei disso no meu jogo, por isso não me importo muito.

O que estou perdendo aqui? Deve haver uma implementação melhor ou alguma idéia que estou perdendo.

you786
fonte
1) Por que você precisa utilizar esses pools de objetos, sejam eles quais forem? (Em uma nota lateral, quais são eles e a que finalidade eles servem?) Não há necessidade de projetar além do que funciona. E o que você tem atualmente parece não ter problemas com isso. 2) Se você precisar enviar uma mensagem para um destinatário específico, localize-a e ligue para onMessageReceived, sem necessidade de sistemas sofisticados.
snake5
Bem, eu quis dizer que atualmente não posso reciclar os objetos em um pool de objetos de uso geral. Portanto, se eu quiser reduzir as alocações de memória necessárias, não posso. No momento, isso não é realmente um problema, porque meu jogo é tão simples que funciona perfeitamente bem.
you786
Estou assumindo que isso é Java? Parece que você está tentando desenvolver um sistema de eventos.
23612 Justin Biles
@ JustinSkiles Correto, este é o Java. Eu acho que você está certo, ele realmente é usado para agir como um sistema de eventos. De que outras maneiras você pode usar um sistema de mensagens?
you786

Respostas:

11

Resposta simples é que você não deseja reciclar mensagens. Recicla-se objetos que são criados (e removidos) por milhares a cada quadro, como partículas, ou são muito pesados ​​(dezenas de propriedades, longo processo de inicialização).

Se você tiver uma partícula de neve com alguns bitmap, velocidades e atributos físicos associados que ficaram abaixo da sua visualização, convém reciclá-la em vez de criar uma nova partícula e inicializar novamente o bitmap e os atributos aleatórios. No seu exemplo, não há nada útil na Mensagem para mantê-lo, e você desperdiçará mais recursos removendo suas propriedades específicas da instância do que apenas criando um novo objeto Message.

Markus von Broady
fonte
3
Boa resposta. Estou usando um pool para minhas mensagens e nunca investiguei se elas eram úteis ou não. Tempo (passado) para o perfil!
Raptormeat
Hmm. E se eu tiver uma mensagem emitida talvez em cada quadro? Acho que a resposta seria ter um pool específico para esse tipo de objeto, o que faz sentido.
you786
@ you786 Acabei de fazer um teste rápido e sujo no Actionscript 3. Criar uma matriz e preenchê-la com 1000 mensagens leva 1ms em um ambiente lento de depuração. Espero que isso esclareça as coisas.
Markus von Broady
@ you786 Ele está correto, primeiro identifique isso como um gargalo. Se você quer ser um desenvolvedor de jogos independente, precisa aprender a tornar as coisas rápidas e efetivas. Investir tempo no ajuste fino do seu código só vale a pena se esse código estiver diminuindo o ritmo do jogo e fazer essas alterações aumentar o desempenho significativamente. Por exemplo, criar instâncias Sprtie no AS3 consumiria muito tempo, é muito melhor reciclá-las. Criar objetos Point não é, reciclá-los seria uma perda de tempo e legibilidade da codificação.
AturSams 13/10/12
Bem, eu deveria ter deixado isso mais claro, mas não estou preocupado com a alocação de memória. Estou preocupado com o coletor de lixo do Java rodando no meio do jogo e causando lentidão. Mas seu argumento sobre otimização prematura ainda é válido.
you786
7

No meu jogo baseado em Java, eu vim com uma solução muito semelhante, exceto que meu código de manipulação de mensagens se parece com o seguinte:

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

Uso anotações para marcar um método como manipulador de eventos. Então, no início do jogo, uso a reflexão para calcular um mapeamento (ou, como chamo, "canalização") de mensagens - qual método chamar em qual objeto quando um evento de uma classe específica é enviado. Isso funciona ... incrível.

O cálculo da tubulação pode ficar um pouco complicado se você quiser adicionar interfaces e subclasses à mistura, mas é bastante simples, no entanto. Há uma ligeira desaceleração durante o carregamento do jogo, quando todas as classes precisam ser verificadas, mas isso é feito apenas uma vez (e provavelmente pode ser movido para um thread separado). O que se ganhou é que o envio real de mensagens é mais barato - não preciso invocar todos os métodos "messageReceived" na base de código apenas para verificar se o evento é de boa classe.

Liosan
fonte
3
Então, você basicamente encontrou uma maneira de o seu jogo correr mais devagar? : D
Markus von Broady
3
@MarkusvonBroady Se for uma vez carregado, vale totalmente a pena. Compare isso com a monstruosidade na minha resposta.
22612 michael.bartnett
11
@MarkusvonBroady A invocação através da reflexão é apenas um pouco mais lenta que a normal, se você tiver todas as peças no lugar (pelo menos meu perfil não mostra nenhum problema). A descoberta é cara, com certeza.
Liosan # 11/12
@ michael.bartnett Eu codigo para um usuário, não para mim. Eu posso viver com meu código maior para uma execução mais rápida. Mas essa foi apenas a minha observação subjetiva.
Markus von Broady
+1, o caminho das anotações é algo em que nunca pensei. Então, só para esclarecer, você basicamente teria vários métodos sobrecarregados chamados messageReceived com diferentes subclasses de Message. Em tempo de execução, você usa a reflexão para criar algum tipo de mapa que calcula MessageSubclass => OverloadedMethod?
you786
5

EDIT A resposta da Liosan é mais sexy. Olhe para isso.


Como Markus disse, as mensagens não são um candidato comum para pools de objetos. Não se preocupe com isso pelas razões que ele mencionou. Se o seu jogo enviar toneladas de mensagens de tipos específicos, talvez valha a pena. Mas, nesse momento, sugiro que você mude para chamadas de método diretas.

Falando nisso,

Não consigo enviar uma mensagem para um destinatário específico, mas ainda não precisei disso no meu jogo, por isso não me importo muito.

Se você conhece um destinatário específico para o qual deseja enviá-lo, não seria muito fácil obter uma referência a esse objeto e fazer uma chamada direta de método? Você também pode ocultar objetos específicos por trás das classes de gerenciador para facilitar o acesso entre os sistemas.

Há um problema na sua implementação, e é que existe o potencial de muitos objetos receberem mensagens com as quais eles realmente não se importam. Idealmente, suas aulas receberiam apenas mensagens com as quais realmente se importam.

Você pode usar um HashMap<Class, LinkedList<Messagable>>para associar tipos de mensagens a uma lista de objetos que desejam receber esse tipo de mensagem.

A inscrição em um tipo de mensagem seria algo como isto:

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

Você pode implementar subscribedessa maneira (perdoe-me alguns erros, não escrevo java há alguns anos):

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);
}

E então você pode transmitir uma mensagem como esta:

globalMessageQueue.broadcastMessage(new PlayerDiedMessage(42));

E broadcastMessagepoderia ser implementado assim:

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

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

Você também pode assinar a mensagem de maneira que possa dividir seu tratamento de mensagens em diferentes funções 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();
            }
        }
    });
}

É um pouco detalhado, mas ajuda na organização. Você também pode implementar uma segunda função, subscribeAllque manterá uma lista separada de Messagables que desejam ouvir sobre tudo.

michael.bartnett
fonte
1

1 . Não.

As mensagens são de baixo custo. Eu concordo com Markus von Broady. O GC os coletará rapidamente. Tente não anexar muitas informações adicionais às mensagens. São apenas mensagens. Encontrar instância de é uma informação por si só. Use-o (como você fez no seu exemplo).

2 . Você pode tentar usar o MessageDispatcher .

Ao contrário da primeira solução, você poderia ter o despachante centralizado de mensagens que processa as mensagens e as passa para os objetos pretendidos.

edin-m
fonte