Wie kann ein Gamestate mit Entitätskomponenten in einem rundenbasierten Spiel weiterentwickelt werden?


9

Bisher haben die von mir verwendeten Entity-Komponentensysteme hauptsächlich wie Javas Artemis funktioniert:

  • Alle Daten in Komponenten
  • Zustandslose unabhängige Systeme (zumindest in dem Maße, in dem sie bei der Initialisierung keine Eingabe erfordern) iterieren über jede Entität, die nur die Komponenten enthält, an denen ein bestimmtes System interessiert ist
  • Alle Systeme verarbeiten ihre Entitäten mit einem Tick, dann beginnt das Ganze von vorne.

Jetzt versuche ich, dies zum ersten Mal auf ein rundenbasiertes Spiel anzuwenden, mit Tonnen von Ereignissen und Reaktionen, die in einer festgelegten Reihenfolge relativ zueinander auftreten müssen, bevor das Spiel fortgesetzt werden kann. Ein Beispiel:

Spieler A erhält Schaden durch ein Schwert. Als Reaktion darauf tritt A's Rüstung ein und verringert den erlittenen Schaden. Die Bewegungsgeschwindigkeit von A wird ebenfalls verringert, weil es schwächer wird.

  • Der erlittene Schaden löst die gesamte Interaktion aus
  • Die Rüstung muss berechnet und auf den ankommenden Schaden angewendet werden, bevor der Schaden auf den Spieler angewendet wird
  • Die Reduzierung der Bewegungsgeschwindigkeit kann erst dann auf eine Einheit angewendet werden, wenn der Schaden tatsächlich verursacht wurde, da dies von der endgültigen Schadensmenge abhängt.

Ereignisse können auch andere Ereignisse auslösen. Das Reduzieren des Schwertschadens mithilfe von Rüstungen kann dazu führen, dass das Schwert zerbricht (dies muss erfolgen, bevor die Schadensreduzierung abgeschlossen ist), was wiederum zusätzliche Ereignisse als Reaktion darauf verursachen kann, im Wesentlichen eine rekursive Bewertung von Ereignissen.

Alles in allem scheint dies zu einigen Problemen zu führen:

  1. Viele verschwendete Verarbeitungszyklen: Die meisten Systeme (abgesehen von Dingen, die immer ausgeführt werden, wie z. B. das Rendern) haben einfach nichts Wertvolles zu tun, wenn sie nicht an der Reihe sind, zu arbeiten, und verbringen die meiste Zeit damit, auf den Eintritt des Spiels zu warten ein gültiger Arbeitszustand. Dies verschmutzt jedes dieser Systeme mit Schecks, die immer größer werden, je mehr Zustände dem Spiel hinzugefügt werden.
  2. Um herauszufinden, ob ein System Entitäten verarbeiten kann, die im Spiel vorhanden sind, müssen sie andere nicht verwandte Entitäts- / Systemzustände überwachen (das System, das für das Verursachen von Schaden verantwortlich ist, muss wissen, ob eine Rüstung angewendet wurde oder nicht). Dies verwirrt entweder die Systeme mit mehreren Verantwortlichkeiten oder macht zusätzliche Systeme ohne anderen Zweck erforderlich, als die Entitätssammlung nach jedem Verarbeitungszyklus zu scannen und mit einer Reihe von Listenern zu kommunizieren, indem ihnen mitgeteilt wird, wann es in Ordnung ist, etwas zu tun.

Bei den beiden oben genannten Punkten wird davon ausgegangen, dass die Systeme mit derselben Gruppe von Entitäten arbeiten, die ihren Status mithilfe von Flags in ihren Komponenten ändern.

Eine andere Möglichkeit, dies zu lösen, wäre das Hinzufügen / Entfernen von Komponenten (oder das Erstellen völlig neuer Entitäten) als Ergebnis einer einzelnen Systemarbeit, um den Spielstatus zu verbessern. Dies bedeutet, dass ein System, wenn es tatsächlich eine übereinstimmende Entität hat, weiß, dass es diese verarbeiten darf.

Dies macht Systeme jedoch für das Auslösen nachfolgender Systeme verantwortlich, was es schwierig macht, über das Programmverhalten nachzudenken, da Fehler nicht als Ergebnis einer einzelnen Systeminteraktion auftreten. Das Hinzufügen neuer Systeme wird auch schwieriger, da sie nicht implementiert werden können, ohne genau zu wissen, wie sie sich auf andere Systeme auswirken (und frühere Systeme möglicherweise geändert werden müssen, um die Zustände auszulösen, an denen das neue System interessiert ist), was den Zweck separater Systeme irgendwie zunichte macht mit einer einzigen Aufgabe.

Muss ich damit leben? Jedes einzelne ECS-Beispiel, das ich gesehen habe, war in Echtzeit, und es ist wirklich leicht zu sehen, wie diese One-Iteration-per-Game-Schleife in solchen Fällen funktioniert. Und ich brauche es immer noch zum Rendern, es scheint nur wirklich ungeeignet für Systeme zu sein, die jedes Mal, wenn etwas passiert, die meisten Aspekte von sich selbst anhalten.

Gibt es ein Entwurfsmuster zum Vorwärtsbewegen des Spielstatus, das dafür geeignet ist, oder sollte ich einfach die gesamte Logik aus der Schleife verschieben und sie stattdessen nur bei Bedarf auslösen?


Sie möchten nicht wirklich nach einem Ereignis fragen. Ein Ereignis tritt nur auf, wenn es auftritt. Ermöglicht Artemis nicht, dass Systeme miteinander kommunizieren?
Sidar

Dies geschieht jedoch nur durch Kopplung mit Methoden.
Aeris130

Antworten:


3

Mein Rat hier stammt aus früheren Erfahrungen mit einem RPG-Projekt, bei dem wir ein Komponentensystem verwendet haben. Ich werde sagen, dass ich es hasste, in diesem Spielcode zu arbeiten, weil es Spaghetti-Code war. Ich biete hier also keine große Antwort, nur eine Perspektive:

Die Logik, die Sie beschreiben, um einem Spieler mit Schwertschaden umzugehen, scheint eine zu sein System für all das verantwortlich sein sollte.

Irgendwo gibt es eine HandleWeaponHit () -Funktion. Es würde auf die Rüstungskomponente der Spielerentität zugreifen, um die entsprechende Rüstung zu erhalten. Es würde auf die WeaponComponent der angreifenden Waffeneinheit zugreifen, um die Waffe möglicherweise zu zerstören. Nach der Berechnung des endgültigen Schadens wird die Bewegungskomponente berührt, damit der Spieler die Geschwindigkeitsreduzierung erreicht.

Was verschwendete Verarbeitungszyklen betrifft ... HandleWeaponHit () sollte nur bei Bedarf ausgelöst werden (nach Erkennung des Schwerttreffers).

Vielleicht ist der Punkt, den ich versuche, zu machen: Sicherlich möchten Sie eine Stelle im Code, an der Sie einen Haltepunkt setzen, ihn treffen und dann die gesamte Logik durchgehen können, die ausgeführt werden soll, wenn ein Schwertschlag auftritt. Mit anderen Worten, die Logik sollte nicht über die tick () - Funktionen mehrerer Systeme verteilt sein.


Wenn Sie dies auf diese Weise tun, wird die Funktion hit () baloon, wenn mehr Verhalten hinzugefügt wird. Nehmen wir an, es gibt einen Feind, der jedes Mal lachend herunterfällt, wenn ein Schwert ein Ziel (ein beliebiges Ziel) innerhalb seiner Sichtlinie trifft. Sollte HandleWeaponHit wirklich dafür verantwortlich sein, das auszulösen?
Aeris130

1
Sie haben eine eng verflochtene Kampfsequenz, also ist der Treffer für das Auslösen von Effekten verantwortlich. Nicht alles muss in kleine Systeme aufgeteilt werden, lassen Sie dieses eine System damit umgehen, denn es ist wirklich Ihr "Kampfsystem" und es handhabt ... Kampf ...
Patrick Hughes

3

Es ist eine einjährige Frage, aber jetzt habe ich die gleichen Probleme mit meinem hausgemachten Spiel, während ich ECS studiere, also etwas Nekromanie. Hoffentlich wird es in einer Diskussion oder zumindest einigen Kommentaren enden.

Ich bin nicht sicher, ob es gegen ECS-Konzepte verstößt, aber was ist, wenn:

  • Fügen Sie einen EventBus hinzu, damit Systeme Ereignisobjekte ausgeben / abonnieren können (reine Daten, aber keine Komponente, denke ich).
  • Erstellen Sie Komponenten für jeden Zwischenzustand

Beispiel:

  • UserInputSystem löst ein Angriffsereignis mit [DamageDealerEntity, DamageReceiverEntity, Skill / Weapon used info] aus.
  • CombatSystem hat es abonniert und berechnet die Ausweichchance für DamageReceiver. Wenn das Ausweichen fehlschlägt, wird ein Schadensereignis mit denselben Parametern ausgelöst
  • DamageSystem hat ein solches Ereignis abonniert und damit ausgelöst
  • DamageSystem verwendet Stärke, BaseWeapon-Schaden, seinen Typ usw. und schreibt ihn mit [DamageDealerEntity, FinalOutgoingDamage, DamageType] in eine neue IncomingDamageComponent und hängt ihn an die Entität / Entitäten des Schadensempfängers an
  • DamageSystem löst ein OutgoingDamageCalculated aus
  • ArmorSystem wird dadurch ausgelöst, nimmt eine Empfängerentität auf oder sucht nach diesem IncomingDamage-Aspekt in Entities, um IncomingDamageComponent (die letzte könnte wahrscheinlich besser für mehrere Angriffe mit Ausbreitung sein) aufzunehmen, und berechnet die Rüstung und den Schaden, die auf sie angewendet werden. Löst optional Ereignisse für das Zerbrechen von Schwertern aus
  • ArmorSystems entfernt IncomingDamageComponent in jeder Entität und ersetzt es durch DamageReceivedComponent mit endgültig berechneten Zahlen, die sich auf die HP- und Geschwindigkeitsreduzierung von Wunden auswirken
  • ArmorSystems sendet ein IncomingDamageCalculated-Ereignis
  • Das Geschwindigkeitssystem ist abonniert und berechnet die Geschwindigkeit neu
  • HealthSystem ist abonniert und verringert die tatsächlichen HP
  • usw
  • Irgendwie aufräumen

Vorteile:

  • Das System löst sich gegenseitig aus und liefert Zwischendaten für komplexe Kettenereignisse
  • Entkopplung durch EventBus

Nachteile:

  • Ich habe das Gefühl, dass ich zwei Arten der Weitergabe von Dingen mische: in Ereignisparametern und in den temporären Komponenten. Es könnte ein schwacher Ort sein. Um die Dinge homogen zu halten, könnte ich theoretisch nur Enum-Ereignisse ohne Daten auslösen, damit Systems die implizierten Parameter in den Komponenten der Entität nach Aspekten findet ... Ich bin mir jedoch nicht sicher, ob es in Ordnung ist
  • Ich bin mir nicht sicher, wie ich wissen soll, ob alle potenziell interessierten Systeme IncomingDamageCalculated verarbeitet haben, damit es bereinigt werden kann und die nächste Runde stattfinden kann. Vielleicht eine Art Überprüfung in CombatSystem ...

2

Nachdem ich die Lösung veröffentlicht hatte, entschied ich mich schließlich, ähnlich wie bei Jakowlew.

Grundsätzlich habe ich ein Ereignissystem verwendet, da ich es sehr intuitiv fand, seiner Logik über Runden hinweg zu folgen. Das System war letztendlich für die Einheiten im Spiel verantwortlich, die sich an die rundenbasierte Logik hielten (Spieler, Monster und alles, mit dem sie interagieren können). Echtzeitaufgaben wie das Rendern und das Abrufen von Eingaben wurden an anderer Stelle platziert.

Systeme implementieren eine onEvent-Methode, die ein Ereignis und eine Entität als Eingabe verwendet und signalisiert, dass die Entität das Ereignis empfangen hat. Jedes System abonniert auch Ereignisse und Entitäten mit einem bestimmten Satz von Komponenten. Der einzige Interaktionspunkt, der den Systemen zur Verfügung steht, ist der Entity Manager-Singleton, mit dem Ereignisse an Entitäten gesendet und Komponenten von einer bestimmten Entität abgerufen werden.

Wenn der Entitätsmanager ein Ereignis empfängt, das mit der Entität gekoppelt ist, an die er gesendet wird, platziert er das Ereignis am Ende einer Warteschlange. Während sich Ereignisse in der Warteschlange befinden, wird das wichtigste Ereignis abgerufen und an jedes System gesendet, das das Ereignis abonniert und an der Komponente der Entität interessiert ist, die das Ereignis empfängt. Diese Systeme können wiederum die Komponenten der Entität verarbeiten und zusätzliche Ereignisse an den Manager senden.

Beispiel: Der Spieler erleidet Schaden, daher erhält die Spielerentität ein Schadensereignis. Das DamageSystem abonniert Schadensereignisse, die an eine Entität mit der Integritätskomponente gesendet werden, und verfügt über eine onEvent-Methode (Entität, Ereignis), die die Integrität in der Entitätskomponente um den im Ereignis angegebenen Betrag verringert.

Dies macht es einfach, ein Rüstungssystem einzufügen, das Schadensereignisse abonniert, die an Entitäten mit einer Rüstungskomponente gesendet werden. Die onEvent-Methode reduziert den Schaden im Ereignis um die Rüstungsmenge in der Komponente. Dies bedeutet, dass die Angabe der Reihenfolge, in der Systeme Ereignisse erhalten, sich auf die Spiellogik auswirkt, da das Rüstungssystem das Schadensereignis vor dem Schadenssystem verarbeiten muss, um zu funktionieren.

Manchmal muss ein System jedoch die empfangende Entität verlassen. Um mit meiner Antwort an Eric Undersander fortzufahren, wäre es trivial, ein System hinzuzufügen, das auf die Spielkarte zugreift und nach Entitäten mit der FallsDownLaughingComponent innerhalb von x Feldern der Entität sucht, die Schaden erleidet, und ihnen dann ein FallDownLaughingEvent zu senden. Dieses System müsste so geplant werden, dass es das Ereignis nach dem Schadenssystem empfängt. Wenn das Schadensereignis zu diesem Zeitpunkt nicht abgebrochen wurde, wurde der Schaden verursacht.

Ein Problem war, wie sichergestellt werden kann, dass Antwortereignisse in der Reihenfolge verarbeitet werden, in der sie gesendet werden, da einige Antworten möglicherweise zusätzliche Antworten hervorrufen. Beispiel:

Der Spieler bewegt sich und veranlasst ein Bewegungsereignis, das an die Entität des Spielers gesendet und vom Bewegungssystem aufgenommen wird.

In der Warteschlange: Bewegung

Wenn die Bewegung erlaubt ist, passt das System die Position des Spielers an. Wenn nicht (der Spieler hat versucht, sich in ein Hindernis zu bewegen), wird das Ereignis als abgebrochen markiert, sodass der Entitätsmanager es verwirft, anstatt es an nachfolgende Systeme zu senden. Am Ende der Liste der an dem Ereignis interessierten Systeme befindet sich das TurnFinishedSystem, das bestätigt, dass der Spieler seinen Zug damit verbracht hat, den Charakter zu bewegen, und dass sein Zug nun beendet ist. Dies führt dazu, dass ein TurnOver-Ereignis an die Player-Entität gesendet und in die Warteschlange gestellt wird.

In der Warteschlange: TurnOver

Sagen Sie nun, dass der Spieler auf eine Falle getreten ist und Schaden verursacht hat. Das TrapSystem erhält die Bewegungsmeldung vor dem TurnFinishedSystem, sodass das Schadensereignis zuerst gesendet wird. Jetzt sieht die Warteschlange stattdessen so aus:

In der Warteschlange: Damage, TurnOver

Bis jetzt ist alles in Ordnung, das Schadensereignis wird verarbeitet und dann endet der Zug. Was ist jedoch, wenn zusätzliche Ereignisse als Reaktion auf den Schaden gesendet werden? Jetzt würde die Ereigniswarteschlange folgendermaßen aussehen:

In der Warteschlange: Damage, TurnOver, ResponseToDamage

Mit anderen Worten, die Runde würde enden, bevor Antworten auf Schäden verarbeitet würden.

Um dies zu lösen, habe ich zwei Methoden zum Senden von Ereignissen verwendet: send (Ereignis, Entität) und reply (Ereignis, eventToRespondTo, Entität).

Jedes Ereignis zeichnet frühere Ereignisse in einer Antwortkette auf, und jedes Mal, wenn die Methode reply () verwendet wird, landet das Ereignis, auf das geantwortet wird (und jedes Ereignis in seiner Antwortkette), an der Spitze der Kette in dem Ereignis, das früher verwendet wurde Antworten mit. Das Anfangsbewegungsereignis hat keine solchen Ereignisse. Die nachfolgende Schadensreaktion enthält das Bewegungsereignis in ihrer Liste.

Darüber hinaus wird ein Array mit variabler Länge verwendet, um mehrere Ereigniswarteschlangen zu enthalten. Immer wenn ein Ereignis vom Manager empfangen wird, wird das Ereignis einer Warteschlange an einem Index im Array hinzugefügt, der der Anzahl der Ereignisse in der Antwortkette entspricht. Somit wird das anfängliche Bewegungsereignis bei [0] zur Warteschlange hinzugefügt, und der Schaden sowie TurnOver-Ereignisse werden bei [1] zu einer separaten Warteschlange hinzugefügt, da beide als Antworten auf die Bewegung gesendet wurden.

Wenn die Antworten auf das Schadensereignis gesendet werden, enthalten diese Ereignisse sowohl das Schadensereignis selbst als auch die Bewegung und stellen sie in eine Warteschlange am Index [2]. Solange index [n] Ereignisse in seiner Warteschlange hat, werden diese Ereignisse verarbeitet, bevor mit [n-1] fortgefahren wird. Dies ergibt eine Verarbeitungsreihenfolge von:

Bewegung -> Schaden [1] -> ResponseToDamage [2] -> [2] ist leer -> TurnOver [1] -> [1] ist leer -> [0] ist leer

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.