Game Messaging System Design

8

Ich mache ein einfaches Spiel und habe beschlossen, ein Messaging-System zu implementieren.

Das System sieht im Grunde so aus:

Entität generiert Nachricht -> Nachricht wird in die globale Nachrichtenwarteschlange gestellt -> messageManager benachrichtigt jedes Objekt über die neue Nachricht über onMessageReceived (Nachrichtennachricht) -> wenn das Objekt dies wünscht, wirkt es auf die Nachricht.

Die Art und Weise, wie ich Nachrichtenobjekte erstelle, ist wie folgt:

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

PlayerDiedMessage extends Message{
  int livesLeft;
}

Jetzt kann meine SoundManagerEntity in ihrer onMessageReceived () -Methode so etwas tun

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

Die Profis dieses Ansatzes:

  1. Sehr einfach und leicht zu implementieren
  2. Die Nachricht kann so viele Informationen enthalten, wie Sie möchten, da Sie einfach eine neue Nachrichtenunterklasse erstellen können, die alle erforderlichen Informationen enthält.

Die Nachteile:

  1. Ich kann nicht herausfinden, wie ich Nachrichtenobjekte in einen Objektpool recyceln kann , es sei denn, ich habe für jede Unterklasse von Nachrichten einen anderen Pool. Ich habe also im Laufe der Zeit sehr viel Objekterstellung / Speicherzuweisung.
  2. Ich kann keine Nachricht an einen bestimmten Empfänger senden, aber das habe ich in meinem Spiel noch nicht gebraucht, daher macht es mir nichts aus.

Was fehlt mir hier? Es muss eine bessere Implementierung oder eine Idee geben, die mir fehlt.

you786
quelle
1) Warum müssen Sie diese Objektpools verwenden, was auch immer sie sind? (Nebenbei bemerkt, was sind sie und welchen Zweck erfüllen sie?) Es besteht keine Notwendigkeit, über das hinaus zu entwerfen, was funktioniert. Und was Sie derzeit haben, scheint damit keine Probleme zu haben. 2) Wenn Sie eine Nachricht an einen bestimmten Empfänger senden müssen, finden Sie sie und rufen onMessageReceived auf. Es sind keine ausgefallenen Systeme erforderlich.
Schlange5
Nun, ich meinte, dass ich die Objekte derzeit nicht in einen Allzweck-Objektpool recyceln kann. Wenn ich also jemals die erforderlichen Speicherzuordnungen reduzieren möchte, kann ich das nicht. Im Moment ist dies kein wirkliches Problem, da mein Spiel so einfach ist, dass es so wie es ist einwandfrei läuft.
you786
Ich gehe davon aus, dass dies Java ist. Sieht so aus, als würden Sie versuchen, ein Ereignissystem zu entwickeln.
Justin Skiles
@ JustinSkiles Richtig, das ist Java. Ich denke du hast recht, es wird wirklich als Ereignissystem verwendet. Welche anderen Möglichkeiten können Sie ein Nachrichtensystem verwenden?
you786

Antworten:

11

Eine einfache Antwort ist, dass Sie keine Nachrichten recyceln möchten. Man recycelt Objekte, die entweder zu Tausenden in jedem Frame erstellt (und entfernt) werden, wie Partikel, oder sehr schwer sind (Dutzende von Eigenschaften, langer Initialisierungsprozess).

Wenn Sie ein Schneepartikel mit einer Bitmap, Geschwindigkeiten und physikalischen Attributen haben, die gerade unter Ihrer Ansicht liegen, möchten Sie es möglicherweise recyceln, anstatt ein neues Partikel zu erstellen und die Bitmap zu initialisieren und die Attribute erneut zu randomisieren. In Ihrem Beispiel enthält die Nachricht nichts Nützliches, um sie beizubehalten, und Sie verschwenden mehr Ressourcen für das Entfernen der instanzspezifischen Eigenschaften als für das Erstellen eines neuen Nachrichtenobjekts.

Markus von Broady
quelle
3
Gute Antwort. Ich verwende einen Pool für meine Nachrichten und habe nie untersucht, ob sie nützlich sind oder nicht. (Vergangene) Zeit zum Profilieren!
Raptormeat
Hmm. Was ist, wenn ich eine Nachricht habe, die möglicherweise in jedem Frame gesendet wird? Ich denke, die Antwort wäre, einen bestimmten Pool für diese Art von Objekt zu haben, was Sinn macht.
you786
@ you786 Ich habe gerade einen schnellen Dirty-Test in Actionscript 3 durchgeführt. Das Erstellen eines Arrays und das Auffüllen mit 1000 Nachrichten dauert in einer langsamen Debug-Umgebung 1 ms. Ich hoffe das klärt die Dinge auf.
Markus von Broady
@ you786 Er hat Recht, identifiziere dies zuerst als Engpass. Wenn du ein Indie-Spieleentwickler sein willst, musst du lernen, Dinge schnell und effektiv zu machen. Es lohnt sich nur, Zeit in die Feinabstimmung Ihres Codes zu investieren, wenn dieser Code Ihr Spiel derzeit verlangsamt und diese Änderungen die Leistung erheblich steigern können. Das Erstellen von Sprtie-Instanzen in AS3 wäre beispielsweise sehr zeitaufwändig. Es ist viel besser, sie zu recyceln. Das Erstellen von Punktobjekten ist nicht so. Das Recycling wäre eine Verschwendung von Codierungszeit und Lesbarkeit.
AturSams
Nun, ich hätte das klarer machen sollen, aber ich mache mir keine Sorgen um die Zuweisung von Speicher. Ich mache mir Sorgen, dass Javas Garbage Collector mitten im Spiel läuft und zu Verlangsamungen führt. Ihr Standpunkt zur vorzeitigen Optimierung ist jedoch weiterhin gültig.
you786
7

In meinem Java-basierten Spiel habe ich eine sehr ähnliche Lösung gefunden, außer dass mein Code für die Nachrichtenverarbeitung so aussieht:

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

Ich benutze Anmerkungen, um eine Methode als Ereignishandler zu markieren. Dann verwende ich beim Spielstart Reflection, um eine Zuordnung (oder, wie ich es nenne, "Piping") von Nachrichten zu berechnen - welche Methode für welches Objekt aufgerufen werden soll, wenn ein Ereignis einer bestimmten Klasse gesendet wird. Das funktioniert ... großartig.

Die Rohrleitungsberechnung kann etwas kompliziert werden, wenn Sie dem Mix Schnittstellen und Unterklassen hinzufügen möchten, ist aber dennoch recht einfach. Während des Spielladens kommt es zu einer leichten Verlangsamung, wenn alle Klassen gescannt werden müssen, dies geschieht jedoch nur einmal (und kann wahrscheinlich in einen separaten Thread verschoben werden). Stattdessen ist das tatsächliche Senden von Nachrichten billiger - ich muss nicht jede "messageReceived" -Methode in der Codebasis aufrufen, um zu überprüfen, ob das Ereignis von einer guten Klasse ist.

Liosan
quelle
3
Sie haben also im Grunde einen Weg gefunden, wie Ihr Spiel langsamer laufen kann? : D
Markus von Broady
3
@ MarkusvonBroady Wenn es einmal geladen ist, lohnt es sich. Vergleichen Sie das mit der Monstrosität in meiner Antwort.
michael.bartnett
1
@MarkusvonBroady Der Aufruf durch Reflexion ist nur geringfügig langsamer als der normale Aufruf, wenn alle Teile vorhanden sind (zumindest zeigt meine Profilerstellung dort kein Problem). Entdeckung ist sicher teuer.
Liosan
@ michael.bartnett Ich codiere für einen Benutzer, nicht für mich. Ich kann damit leben, dass mein Code für eine schnellere Ausführung größer ist. Aber das war nur meine subjektive Bemerkung.
Markus von Broady
+1, der Annotationspfad ist etwas, an das ich nie gedacht habe. Um dies zu verdeutlichen, hätten Sie im Grunde eine Reihe überladener Methoden, die als messageReceived mit verschiedenen Unterklassen von Message bezeichnet werden. Dann verwenden Sie zur Laufzeit Reflection, um eine Art Map zu erstellen, die MessageSubclass => OverloadedMethod berechnet.
you786
5

EDIT Liosans Antwort ist sexier. Schau in Es hinein.


Wie Markus sagte, sind Nachrichten kein häufiger Kandidat für Objektpools. Kümmere dich nicht darum aus den Gründen, die er erwähnt hat. Wenn Ihr Spiel Unmengen von Nachrichten bestimmter Typen sendet, lohnt es sich vielleicht. Aber an diesem Punkt würde ich vorschlagen, dass Sie zu direkten Methodenaufrufen wechseln.

Apropos,

Ich kann keine Nachricht an einen bestimmten Empfänger senden, aber das habe ich in meinem Spiel noch nicht gebraucht, daher macht es mir nichts aus.

Wenn Sie einen bestimmten Empfänger kennen, an den Sie ihn senden möchten, wäre es dann nicht ziemlich einfach, einen Verweis auf dieses Objekt zu erhalten und einen direkten Methodenaufruf darauf durchzuführen? Sie können auch bestimmte Objekte hinter Manager-Klassen ausblenden, um den systemübergreifenden Zugriff zu erleichtern.

Ihre Implementierung hat einen Nachteil, und es besteht das Potenzial, dass viele Objekte Nachrichten erhalten, die ihnen nicht wirklich wichtig sind. Im Idealfall erhalten Ihre Klassen nur Nachrichten, die sie wirklich interessieren.

Mit a können Sie HashMap<Class, LinkedList<Messagable>>Nachrichtentypen einer Liste von Objekten zuordnen, die diesen Nachrichtentyp empfangen möchten.

Das Abonnieren eines Nachrichtentyps würde ungefähr so ​​aussehen:

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

Sie könnten dies so implementieren subscribe(verzeihen Sie mir einige Fehler, ich habe seit ein paar Jahren kein Java mehr geschrieben):

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

Und dann könnten Sie eine Nachricht wie folgt senden:

globalMessageQueue.broadcastMessage(new PlayerDiedMessage(42));

Und broadcastMessagekönnte so implementiert werden:

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

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

Sie können Nachrichten auch so abonnieren, dass Sie Ihre Nachrichtenverarbeitung in verschiedene anonyme Funktionen aufteilen können:

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 ist ein wenig ausführlich, hilft aber bei der Organisation. Sie können auch eine zweite Funktion implementieren subscribeAll, die eine separate Liste von Messagables enthält, die über alles hören möchten.

michael.bartnett
quelle
1

1 . Tu es nicht.

Nachrichten sind kostengünstig. Ich stimme Markus von Broady zu. GC wird sie schnell sammeln. Versuchen Sie, nicht viele zusätzliche Informationen für Nachrichten anzuhängen. Sie sind nur Nachrichten. Das Finden einer Instanz von ist eine Information für sich. Verwenden Sie es (wie in Ihrem Beispiel).

2 . Sie können versuchen, MessageDispatcher zu verwenden .

Im Gegensatz zur ersten Lösung könnten Sie einen zentralen Nachrichten-Dispatcher haben, der Nachrichten verarbeitet und an beabsichtigte Objekte weiterleitet.

edin-m
quelle