Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Game Development
  2. Game Engine Development

Wie man eine Nachrichtenwarteschlange in Ihrem Spiel implementieren und verwenden kann

by
Read Time:17 minsLanguages:

German (Deutsch) translation by Nikol Angelowa (you can also view the original English article)

Ein Spiel besteht normalerweise aus mehreren verschiedenen Einheiten, die miteinander interagieren. Diese Interaktionen sind in der Regel sehr dynamisch und eng mit dem Gameplay verbunden. Dieses Tutorial behandelt das Konzept und die Implementierung eines Nachrichtenwarteschlangensystems, das die Interaktionen von Entitäten vereinheitlichen kann, sodass Ihr Code bei zunehmender Komplexität verwaltbar und einfach zu warten ist.

Einführung

Eine Bombe kann mit einem Charakter interagieren, indem sie explodiert und Schaden anrichtet, ein Sanitätskit kann ein Wesen heilen, ein Schlüssel kann eine Tür öffnen und so weiter. Interaktionen in einem Spiel sind endlos, aber wie können wir den Spielcode überschaubar halten und trotzdem all diese Interaktionen bewältigen? Wie stellen wir sicher, dass sich der Code ändern und weiterhin funktionieren kann, wenn neue und unerwartete Interaktionen auftreten?

Interactions in a game tend to grow in complexity very quicklyInteractions in a game tend to grow in complexity very quicklyInteractions in a game tend to grow in complexity very quickly
Interaktionen in einem Spiel werden in der Regel sehr schnell komplexer.

Wenn Interaktionen hinzugefügt werden (insbesondere die unerwarteten), wird Ihr Code immer unübersichtlicher. Eine naive Implementierung führt schnell dazu, dass Sie Fragen stellen wie:

"Das ist Entity A, also sollte ich die Methode damage() darauf aufrufen, oder? Oder ist es damageByItem()? Vielleicht ist diese damageByWeapon() Methode die richtige?"

Stellen Sie sich vor, dass sich dieses überladene Chaos auf alle Ihre Spieleinheiten ausbreitet, weil sie alle auf unterschiedliche und eigentümliche Weise miteinander interagieren. Zum Glück gibt es eine bessere, einfachere und überschaubarere Methode.

Nachrichtenwarteschlange

Geben Sie die Nachrichtenwarteschlange ein. Die Grundidee dieses Konzepts besteht darin, alle Spielinteraktionen als Kommunikationssystem (das heute noch verwendet wird) umzusetzen: Nachrichtenaustausch. Menschen kommunizieren seit Jahrhunderten über Nachrichten (Briefe), weil es ein effektives und einfaches System ist.

In unseren realen Postdiensten kann sich der Inhalt jeder Nachricht unterscheiden, aber die Art und Weise, wie sie physisch gesendet und empfangen werden, bleibt gleich. Ein Absender steckt die Informationen in einen Umschlag und adressiert ihn an ein Ziel. Das Ziel kann antworten (oder nicht), indem es dem gleichen Mechanismus folgt, indem es einfach die "von/bis"-Felder auf dem Umschlag ändert.

Interactions made using a message queue systemInteractions made using a message queue systemInteractions made using a message queue system
Interaktionen, die unter Verwendung eines Nachrichtenwarteschlangensystems durchgeführt werden.

Wenn Sie diese Idee auf Ihr Spiel anwenden, können alle Interaktionen zwischen Entitäten als Nachrichten angesehen werden. Wenn eine Spielinstanz mit einer anderen (oder einer Gruppe von ihnen) interagieren möchte, muss sie lediglich eine Nachricht senden. Das Ziel verarbeitet oder reagiert auf die Nachricht basierend auf ihrem Inhalt und darauf, wer der Absender ist.

Bei diesem Ansatz wird die Kommunikation zwischen Spieleinheiten vereinheitlicht. Alle Entitäten können Nachrichten senden und empfangen. Egal wie komplex oder eigenartig die Interaktion oder Botschaft ist, der Kommunikationskanal bleibt immer derselbe.

In den nächsten Abschnitten werde ich beschreiben, wie Sie diesen Message Queue-Ansatz tatsächlich in Ihrem Spiel implementieren können.

Entwerfen eines Umschlags (Nachricht)

Beginnen wir damit, den Umschlag zu entwerfen, der das grundlegendste Element im Nachrichtenwarteschlangensystem ist.

Ein Umschlag kann wie in der folgenden Abbildung beschrieben werden:

Structure of a messageStructure of a messageStructure of a message
Aufbau einer Nachricht.

Die ersten beiden Felder (sender und destination) sind Verweise auf die Entität, die diese Nachricht erstellt hat, bzw. die Entität, die diese Nachricht empfängt. Anhand dieser Felder können sowohl der Absender als auch der Empfänger erkennen, wohin die Nachricht geht und woher sie stammt.

Die anderen beiden Felder (type und data) arbeiten zusammen, um sicherzustellen, dass die Nachricht richtig verarbeitet wird. Das Feld type beschreibt, worum es in dieser Nachricht geht; Wenn der Typ beispielsweise "damage" ist, wird der Empfänger diese Nachricht als Befehl behandeln, seine Gesundheitspunkte zu verringern; wenn der Typ "pursue" ist, nimmt der Empfänger es als Anweisung, etwas zu verfolgen - und so weiter.

Das data-Feld ist direkt mit dem type-Feld verbunden. Wenn der Nachrichtentyp in den vorherigen Beispielen "damage" lautet, enthält das data-Feld eine Zahl - sagen wir 10 -, die den Schaden beschreibt, den der Empfänger seinen Gesundheitspunkten zufügen sollte. Wenn der Nachrichtentyp "pursue" ist, enthalten die data ein Objekt, das das zu verfolgende Ziel beschreibt.

Das data-Feld kann beliebige Informationen enthalten, was den Umschlag zu einem vielseitigen Kommunikationsmittel macht. In diesem Feld kann alles platziert werden: Ganzzahlen, Floats, Strings und sogar andere Objekte. Als Faustregel gilt, dass der Empfänger basierend auf dem type-feld wissen muss, was im data-Feld steht.

All diese Theorie kann in eine sehr einfache Klasse namens Message übersetzt werden. Es enthält vier Eigenschaften, eine für jedes Feld:

Als Beispiel dafür, wenn eine Entität A eine "damage"-Nachricht an Entität B senden möchte, muss sie lediglich ein Objekt der Klasse Message instanziieren, die Eigenschaft to B setzen, die Eigenschaft auf from setzen selbst (Entität A), setze type auf "damage" und setze schließlich data auf eine Zahl (zum Beispiel 10):

Da wir nun eine Möglichkeit haben, Nachrichten zu erstellen, ist es an der Zeit, über die Klasse nachzudenken, die sie speichert und übermittelt.

Implementieren einer Warteschlange

Die für das Speichern und Zustellen der Nachrichten verantwortliche Klasse wird MessageQueue genannt. Es wird als Postamt funktionieren: Alle Nachrichten werden an diese Klasse übergeben, die sicherstellt, dass sie an ihr Ziel gesendet werden.

Im Moment hat die MessageQueue-Klasse eine sehr einfache Struktur:

Die Eigenschaft messages ist ein Array. Es speichert alle Nachrichten, die von der MessageQueue zugestellt werden. Die Methode add() erhält als Parameter ein Objekt der Klasse Message und fügt dieses Objekt der Liste der Nachrichten hinzu.

So würde unser vorheriges Beispiel für die Nachricht von Entität A, die Entität B über Schäden informiert, mit der MessageQueue-Klasse funktionieren:

Wir haben jetzt eine Möglichkeit, Nachrichten in einer Warteschlange zu erstellen und zu speichern. Es ist an der Zeit, dass sie ihr Ziel erreichen.

Zustellung von Nachrichten

Damit die MessageQueue-Klasse die geposteten Nachrichten tatsächlich versendet, müssen wir zunächst definieren, wie Entitäten Nachrichten behandeln und empfangen. Der einfachste Weg besteht darin, jeder Entität, die Nachrichten empfangen kann, eine Methode namens onMessage() hinzuzufügen:

Die MessageQueue-Klasse ruft die onMessage()-Methode jeder Entität auf, die eine Nachricht empfangen muss. Der an diese Methode übergebene Parameter ist die Nachricht, die vom Warteschlangensystem zugestellt wird (und vom Ziel empfangen wird).

Die MessageQueue-Klasse verteilt die Nachrichten in ihrer Warteschlange alle auf einmal in der Methode "dispatch()":

Diese Methode durchläuft alle Nachrichten in der Warteschlange und für jede Nachricht wird das Feld to verwendet, um eine Referenz auf den Empfänger abzurufen. Anschließend wird die Methode onMessage() des Empfängers mit der aktuellen Nachricht als Parameter aufgerufen und die zugestellte Nachricht anschließend aus der MessageQueue-Liste entfernt. Dieser Vorgang wird wiederholt, bis alle Nachrichten versendet sind.

Verwenden einer Nachrichtenwarteschlange

Es ist an der Zeit, alle Details dieser Implementierung gemeinsam zu sehen. Lassen Sie uns unser Nachrichtenwarteschlangensystem in einer sehr einfachen Demo verwenden, die aus wenigen beweglichen Entitäten besteht, die miteinander interagieren. Der Einfachheit halber arbeiten wir mit drei Entitäten: Healer, Runner und Hunter.

Der Runner hat eine Gesundheitsleiste und bewegt sich zufällig. Der Healer heilt jeden Runner, der in der Nähe vorbeikommt; Auf der anderen Seite fügt der Hunter jedem Runner in der Nähe Schaden zu. Alle Interaktionen werden über das Nachrichtenwarteschlangensystem abgewickelt.

Hinzufügen der Nachrichtenwarteschlange

Beginnen wir mit dem Erstellen des PlayState, der eine Liste von Entitäten (Heiler, Läufer und Jäger) und eine Instanz der MessageQueue-Klasse enthält:

In der Spielschleife, die durch die Methode update() repräsentiert wird, wird die Methode dispatch() der Nachrichtenwarteschlange aufgerufen, sodass alle Nachrichten am Ende jedes Spielrahmens zugestellt werden.

Hinzufügen der Läufer

Die Runner-Klasse hat die folgende Struktur:

Der wichtigste Teil ist die Methode onMessage(), die von der Nachrichtenwarteschlange jedes Mal aufgerufen wird, wenn eine neue Nachricht für diese Instanz vorliegt. Wie bereits erläutert, wird anhand des Feld type in der Nachricht entschieden, worum es bei dieser Kommunikation geht.

Basierend auf dem Nachrichtentyp wird die richtige Aktion ausgeführt: Wenn der Nachrichtentyp "damage" lautet, werden die Gesundheitspunkte verringert; wenn der Nachrichtentyp "heal" ist, werden die Lebenspunkte erhöht. Die Anzahl der Gesundheitspunkte, um die erhöht oder verringert werden soll, wird vom Absender im data-Feld der Nachricht definiert.

Im PlayState fügen wir der Liste der Entitäten einige Läufer hinzu:

Das Ergebnis sind vier Läufer, die sich zufällig bewegen:

Hinzufügen des Jägers

Die Hunter-Klasse hat folgende Struktur:

Die Jäger werden sich auch bewegen, aber sie werden allen Läufern, die sich in der Nähe befinden, Schaden zufügen. Dieses Verhalten ist in der Methode update() implementiert, bei der alle Entitäten des Spiels überprüft werden und die Läufer über Schäden informiert werden.

Die Schadensmeldung wird wie folgt erstellt:

Die Nachricht enthält die Informationen über das Ziel (in diesem Fall die entity, die in der aktuellen Iteration analysiert wird), den Absender (this repräsentiert den Jäger, der den Angriff durchführt), den Typ der Nachricht ("damage") und die Schadenshöhe (hier 2 dem data-Feld der Nachricht zugeordnet).

Die Nachricht wird dann über den Befehl this.getMessageQueue().add(msg) an das Ziel gesendet, der die neu erstellte Nachricht der Nachrichtenwarteschlange hinzufügt.

Schließlich fügen wir den Hunter der Liste der Entitäten im PlayState hinzu:

Das Ergebnis sind einige Läufer, die sich bewegen und Nachrichten vom Jäger erhalten, wenn sie sich einander nähern:

Ich habe die fliegenden Umschläge als visuelle Hilfe hinzugefügt, um zu zeigen, was vor sich geht.

Hinzufügen des Heilers

Die Healer-Klasse hat folgende Struktur:

Der Code und die Struktur sind der Hunter-Klasse sehr ähnlich, mit Ausnahme einiger Unterschiede. Ähnlich wie bei der Implementierung des Jägers durchläuft die update()-Methode des Heilers die Liste der Entitäten im Spiel und sendet eine Nachricht an jede Entität innerhalb ihrer Heilungsreichweite:

Die Nachricht hat auch ein Ziel (entity), einen Absender (das ist der Heiler, this die Aktion ausführt), einen Nachrichtentyp ("heal") und die Anzahl der Heilpunkte (2, zugewiesen im data-Feld der Nachricht) .

Wir fügen den Healer der Liste der Entitäten im PlayState hinzu, wie wir es beim Hunter getan haben, und das Ergebnis ist eine Szene mit Läufern, einem Jäger und einem Heiler:

Und das ist es! Wir haben drei verschiedene Entitäten, die miteinander interagieren, indem sie Nachrichten austauschen.

Diskussion über Flexibilität

Dieses Nachrichtenwarteschlangensystem ist eine vielseitige Möglichkeit, Interaktionen in einem Spiel zu verwalten. Die Interaktionen erfolgen über einen einheitlichen Kommunikationskanal mit einer einzigen Schnittstelle, die einfach zu bedienen und zu implementieren ist.

Wenn Ihr Spiel komplexer wird, sind möglicherweise neue Interaktionen erforderlich. Einige von ihnen können völlig unerwartet sein, daher müssen Sie Ihren Code anpassen, um mit ihnen umzugehen. Wenn Sie ein Nachrichtenwarteschlangensystem verwenden, müssen Sie irgendwo eine neue Nachricht hinzufügen und in einer anderen verarbeiten.

Stellen Sie sich zum Beispiel vor, Sie möchten, dass der Hunter mit dem Healer interagiert; Sie müssen den Hunter nur dazu bringen, eine Nachricht mit der neuen Interaktion zu senden – zum Beispiel "flee" – und sicherstellen, dass der Healer damit in der onMessage-Methode umgehen kann:

Warum nicht einfach Nachrichten direkt senden?

Obwohl der Austausch von Nachrichten zwischen Entitäten nützlich sein kann, denken Sie vielleicht, warum die MessageQueue überhaupt benötigt wird. Können Sie die onMessage()-Methode des Empfängers nicht einfach selbst aufrufen, anstatt sich auf die MessageQueue zu verlassen, wie im folgenden Code?

Ein solches Nachrichtensystem könnte man durchaus implementieren, aber der Einsatz einer MessageQueue hat einige Vorteile.

Durch die Zentralisierung des Versands von Nachrichten können Sie beispielsweise einige coole Funktionen wie verzögerte Nachrichten, die Möglichkeit, eine Gruppe von Entitäten zu benachrichtigen, und visuelle Debug-Informationen (wie die in diesem Tutorial verwendeten fliegenden Umschläge) implementieren.

In der MessageQueue-Klasse ist Raum für Kreativität, es liegt an Ihnen und den Anforderungen Ihres Spiels.

Abschluss

Die Handhabung von Interaktionen zwischen Spielentitäten mithilfe eines Nachrichtenwarteschlangensystems ist eine Möglichkeit, Ihren Code organisiert und für die Zukunft bereit zu halten. Neue Interaktionen können einfach und schnell hinzugefügt werden, selbst Ihre komplexesten Ideen, solange sie als Nachrichten gekapselt sind.

Wie im Tutorial beschrieben, können Sie die Verwendung einer zentralen Nachrichtenwarteschlange ignorieren und Nachrichten einfach direkt an die Entitäten senden. Sie können die Kommunikation auch über einen Dispatch (in unserem Fall die MessageQueue-Klasse) zentralisieren, um in Zukunft Platz für neue Funktionen wie verzögerte Nachrichten zu schaffen.

Ich hoffe, Sie finden diesen Ansatz nützlich und fügen ihn Ihrem Dienstprogramm für Spieleentwickler hinzu. Die Methode mag für kleine Projekte übertrieben erscheinen, wird Ihnen aber auf lange Sicht bei größeren Spielen sicherlich einige Kopfschmerzen ersparen.

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.