Progettazione del sistema di messaggistica di gioco


8

Sto realizzando un gioco semplice e ho deciso di provare a implementare un sistema di messaggistica.

Il sistema appare sostanzialmente così:

L'entità genera un messaggio -> il messaggio viene pubblicato nella coda globale dei messaggi -> messageManager notifica ogni oggetto del nuovo messaggio tramite onMessageReceived (Messaggio msg) -> se l'oggetto lo desidera, agisce sul messaggio.

Il modo in cui sto creando oggetti messaggio è così:

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

PlayerDiedMessage extends Message{
  int livesLeft;
}

Ora il mio SoundManagerEntity può fare qualcosa del genere nel suo metodo onMessageReceived ()

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

I pro di questo approccio:

  1. Molto semplice e facile da implementare
  2. Il messaggio può contenere tutte le informazioni che desideri, poiché puoi semplicemente creare una nuova sottoclasse di messaggi con tutte le informazioni necessarie.

I contro:

  1. Non riesco a capire come posso riciclare gli oggetti Message in un pool di oggetti , a meno che non abbia un pool diverso per ogni sottoclasse di Message. Quindi ho un sacco di creazione di oggetti / allocazione di memoria nel tempo.
  2. Non riesco a inviare un messaggio a un destinatario specifico, ma non ne ho ancora avuto bisogno nel mio gioco, quindi non mi dispiace troppo.

Cosa mi sto perdendo qui? Ci deve essere una migliore implementazione o qualche idea che mi manchi.


1) Perché è necessario utilizzare quei pool di oggetti, qualunque essi siano? (In una nota a margine, quali sono e a quale scopo servono?) Non è necessario progettare oltre ciò che funziona. E quello che hai attualmente sembra non avere problemi con quello. 2) Se devi inviare un messaggio a un destinatario specifico, lo trovi e lo chiami onMessageReceived, non sono richiesti sistemi elaborati.
snake5,

Bene, intendevo dire che attualmente non posso riciclare gli oggetti in un pool di oggetti per scopi generici. Quindi, se mai volessi ridurre le allocazioni di memoria necessarie, non posso. In questo momento questo non è davvero un problema perché il mio gioco è così semplice che funziona perfettamente così com'è.
you786,

Suppongo che questo sia Java? Sembra che tu stia cercando di sviluppare un sistema di eventi.
Justin Skiles,

@JustinSkiles Corretto, questo è Java. Immagino che tu abbia ragione, è davvero usato per fungere da sistema di eventi. In quali altri modi è possibile utilizzare un sistema di messaggistica?
tu786,

Risposte:


11

La risposta semplice è che non vuoi riciclare i messaggi. Uno ricicla gli oggetti che vengono creati (e rimossi) da migliaia di fotogrammi, come particelle, o che sono molto pesanti (decine di proprietà, lungo processo di inizializzazione).

Se hai una particella di neve con bitmap, velocità e attributi fisici associati che sono appena scesi sotto la tua vista, potresti voler riciclarla invece di creare una nuova particella e inizializzare la bitmap e randomizzare nuovamente gli attributi. Nel tuo esempio non c'è nulla di utile nel messaggio per mantenerlo e sprecherai più risorse nella rimozione delle proprietà specifiche dell'istanza di quanto non faresti semplicemente per creare un nuovo oggetto Messaggio.


3
Buona risposta. Sto usando un pool per i miei messaggi e non ho mai realmente indagato se fossero utili o meno. (Passato) tempo di profilare!
Raptormeat

Hmm. Cosa succede se ho un messaggio che viene emesso forse ogni frame? Immagino che la risposta sarebbe quella di avere un pool specifico per quel tipo di oggetto, il che ha senso.
tu786,

@ you786 Ho appena fatto un rapido test sporco in Actionscript 3. La creazione di un array e il popolamento con 1000 messaggi richiede 1ms in un ambiente di debug lento. Spero che chiarisca le cose.
Markus von Broady,

@ you786 Ha ragione, prima identifica questo come un collo di bottiglia, se vuoi essere uno sviluppatore di giochi indie, devi imparare a rendere le cose veloci ed efficaci. Investire tempo nella messa a punto del codice vale la pena solo se quel codice sta rallentando il gioco e apportando queste modifiche può aumentare significativamente le prestazioni. Ad esempio, la creazione di istanze Sprtie in AS3 richiederebbe molto tempo, è molto meglio riciclarle. Creare oggetti Point non lo è, riciclarli sarebbe una perdita di tempo di programmazione e leggibilità.
AturSams,

Bene, avrei dovuto renderlo più chiaro, ma non sono preoccupato per l'allocazione della memoria. Sono preoccupato per il garbage collector di Java in esecuzione nel mezzo del gioco e che causa rallentamenti. Ma il tuo punto sull'ottimizzazione prematura è ancora valido.
tu786,

7

Nel mio gioco basato su Java, ho trovato una soluzione molto simile, tranne per il fatto che il mio codice di gestione dei messaggi è simile al seguente:

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

Uso le annotazioni per contrassegnare un metodo come gestore di eventi. Quindi, all'avvio del gioco, uso la riflessione per calcolare una mappatura (o, come la chiamo io, "piping") di messaggi - quale metodo invocare su quale oggetto quando viene inviato un evento di una classe specifica. Funziona ... fantastico.

Il calcolo delle tubazioni può diventare un po 'complicato se si desidera aggiungere interfacce e sottoclassi al mix, ma è comunque abbastanza semplice. C'è un leggero rallentamento durante il caricamento del gioco quando tutte le classi devono essere scansionate, ma è fatto solo una volta (e probabilmente può essere spostato in un thread separato). Ciò che si guadagna invece è che l'invio effettivo di messaggi è più economico - non devo invocare tutti i metodi "messageReceived" nella base di codice solo per controllarlo se l'evento è di una buona classe.


3
Quindi fondamentalmente hai trovato un modo per far funzionare il tuo gioco più lentamente? : D
Markus von Broady,

3
@MarkusvonBroady Se viene caricato una volta, ne vale la pena. Confrontalo con la mostruosità nella mia risposta.
michael.bartnett,

1
@MarkusvonBroady L'invocazione attraverso la riflessione è solo leggermente più lenta dell'invocazione normale, se hai tutti i pezzi a posto (almeno la mia profilazione non mostra alcun problema lì). La scoperta è costosa, di sicuro.
Liosan,

@ michael.bartnett I codice per un utente, non per me stesso. Posso vivere con il mio codice più grande per un'esecuzione più rapida. Ma questa era solo la mia osservazione soggettiva.
Markus von Broady,

+1, il percorso delle annotazioni è qualcosa a cui non avevo mai pensato. Quindi, solo per chiarire, avresti sostanzialmente un mucchio di metodi sovraccarichi che si chiamano messageRicevuto con diverse sottoclassi di Message. Quindi in fase di esecuzione si utilizza la riflessione per creare una sorta di mappa che calcola MessageSubclass => OverloadedMethod?
tu786,

5

EDIT La risposta di Liosan è più sexy. Guardaci dentro.


Come ha detto Markus, i messaggi non sono un candidato comune per i pool di oggetti. Non preoccuparti per i motivi che ha citato. Se il tuo gioco invia tonnellate di messaggi di tipi specifici, allora forse varrebbe la pena. Ma poi a quel punto ti suggerirei di passare alle chiamate di metodo dirette.

A proposito,

Non riesco a inviare un messaggio a un destinatario specifico, ma non ne ho ancora avuto bisogno nel mio gioco, quindi non mi dispiace troppo.

Se conosci un destinatario specifico a cui vuoi inviarlo, non sarebbe abbastanza facile ottenere un riferimento a quell'oggetto e fare un metodo diretto su di esso? È inoltre possibile nascondere oggetti specifici dietro le classi manager per un accesso più semplice tra i sistemi.

C'è una truffa nella tua implementazione, ed è che esiste il potenziale per molti oggetti di ricevere messaggi che non gli interessano davvero. Idealmente, le tue classi riceveranno solo messaggi a cui tengono davvero.

È possibile utilizzare a HashMap<Class, LinkedList<Messagable>>per associare i tipi di messaggio a un elenco di oggetti che desiderano ricevere quel tipo di messaggio.

Iscriversi a un tipo di messaggio sarebbe simile al seguente:

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

Potresti implementare subscribecosì (perdonami alcuni errori, non scrivo java da un paio d'anni):

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 poi potresti trasmettere un messaggio come questo:

globalMessageQueue.broadcastMessage(new PlayerDiedMessage(42));

E broadcastMessagepotrebbe essere implementato in questo modo:

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

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

Puoi anche iscriverti al messaggio in modo tale da suddividere la gestione dei messaggi in diverse funzioni anonime:

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

È un po 'prolisso, ma aiuta con l'organizzazione. Potresti anche implementare una seconda funzione, subscribeAllche manterrà un elenco separato di messaggi Messagableche vogliono sapere tutto.


1

1 . Non farlo.

I messaggi sono a basso costo. Sono d'accordo con Markus von Broady. GC li raccoglierà rapidamente. Cerca di non allegare molte informazioni aggiuntive per i messaggi. Sono solo messaggi. Trovare un'istanza di è un'informazione da sola. Usalo (come hai fatto nel tuo esempio).

2 . Puoi provare a utilizzare MessageDispatcher .

Contrariamente alla prima soluzione, è possibile disporre di un dispatcher di messaggi centralizzato che elabora i messaggi e li passa agli oggetti previsti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.