Wie vernetzt man dieses Entitätssystem?

33

Ich habe ein Entity-System für einen FPS entworfen. Es funktioniert im Grunde so:

Wir haben ein "Welt" -Objekt namens GameWorld. Dies beinhaltet ein Array von GameObject sowie ein Array von ComponentManager.

GameObject enthält eine Reihe von Komponenten. Es bietet auch einen Ereignismechanismus, der wirklich einfach ist. Komponenten selbst können ein Ereignis an die Entität senden, das an alle Komponenten gesendet wird.

Komponente ist im Grunde etwas, das einem GameObject bestimmte Eigenschaften verleiht, und da GameObject eigentlich nur ein Container von ihnen ist, geschieht in den Komponenten alles, was mit einem Spielobjekt zu tun hat. Beispiele hierfür sind ViewComponent, PhysicsComponent und LogicComponent. Wenn eine Kommunikation zwischen ihnen erforderlich ist, kann dies durch die Verwendung von Ereignissen erfolgen.

ComponentManager ist genau wie Component eine Schnittstelle, und für jede Component-Klasse sollte es im Allgemeinen eine ComponentManager-Klasse geben. Diese Komponentenmanager sind dafür verantwortlich, die Komponenten zu erstellen und sie mit Eigenschaften zu initialisieren, die aus so etwas wie einer XML-Datei gelesen werden.

ComponentManager kümmert sich auch um Massenaktualisierungen von Komponenten, wie die PhysicsComponent, bei der ich eine externe Bibliothek verwenden werde (die alles auf der Welt auf einmal erledigt).

Aus Gründen der Konfigurierbarkeit verwende ich eine Factory für die Entitäten, die entweder eine XML-Datei oder ein Skript lesen, die in der Datei angegebenen Komponenten erstellen (die für Massenaktualisierungen auch einen Verweis darauf im rechten Komponentenmanager hinzufügen) und Füge sie dann in ein GameObject-Objekt ein.

Jetzt kommt mein Problem: Ich werde versuchen, dies für Multiplayer-Spiele zu verwenden. Ich habe keine Ahnung, wie ich das angehen soll.

Erstens: Welche Entitäten sollten die Kunden von Anfang an haben? Ich sollte damit beginnen, zu erklären, wie eine Einzelspieler-Engine bestimmen würde, welche Entitäten erstellt werden sollen.

Im Ebeneneditor können Sie "Pinsel" und "Objekte" erstellen. Pinsel sind für Dinge wie Wände, Böden und Decken im Grunde einfache Formen. Entities sind das GameObject, von dem ich dir erzählt habe. Beim Erstellen von Entitäten im Ebeneneditor können Sie Eigenschaften für jede seiner Komponenten angeben. Diese Eigenschaften werden direkt an einen Konstruktor im Skript der Entität übergeben.

Wenn Sie die Ebene für das Laden der Engine speichern, wird sie in eine Liste von Entitäten und deren zugehörigen Eigenschaften zerlegt. Die Pinsel werden in eine "Worldspawn" -Entität umgewandelt.

Wenn Sie diese Ebene laden, werden nur alle Entitäten instanziiert. Hört sich einfach an, oder?

Für die Vernetzung der Einheiten stoße ich nun auf zahlreiche Probleme. Erstens, welche Entitäten sollten von Anfang an auf dem Client vorhanden sein? Angenommen, sowohl der Server als auch der Client verfügen über die Level-Datei, könnte der Client genauso gut alle Entitäten in dem Level instanziieren, selbst wenn sie nur für die Zwecke der Spielregeln auf dem Server vorhanden sind.

Eine andere Möglichkeit besteht darin, dass der Client eine Entität instanziiert, sobald der Server Informationen darüber sendet. Dies bedeutet, dass der Client nur Entitäten hat, die er benötigt.

Ein weiteres Problem ist das Senden der Informationen. Ich denke, der Server könnte Delta-Komprimierung verwenden, was bedeutet, dass er nur dann neue Informationen sendet, wenn sich etwas ändert, anstatt bei jedem Frame einen Snapshot an den Client zu senden. Dies bedeutet jedoch, dass der Server den Überblick darüber behalten muss, was jeder Client im Moment weiß.

Und schließlich, wie soll das Networking in die Engine eingebunden werden? Ich denke an eine Komponente, NetworkComponent, die in jede Entität injiziert wird, die vernetzt werden soll. Aber woher sollte die Netzwerkkomponente wissen, welche Variablen vernetzt werden sollen und wie auf diese zugegriffen werden soll, und schließlich, wie die entsprechende Netzwerkkomponente auf dem Client die vernetzten Variablen ändern soll?

Ich habe große Probleme damit. Ich würde mich sehr freuen, wenn Sie mir auf dem Weg helfen würden. Ich bin auch offen für Tipps zur Verbesserung des Komponentensystemdesigns, also haben Sie keine Angst davor, dies vorzuschlagen.

Fuhrmann
quelle

Antworten:

13

Dies ist ein verdammtes Tier mit vielen Details +1. Auf jeden Fall genug, um Menschen zu helfen, die darauf stoßen.

Ich wollte nur meistens meine 2 Cent dazuzählen, weil ich keine Physikdaten gesendet habe !! Ich kann das nicht genug betonen. Selbst wenn Sie es so weit optimiert haben, dass Sie praktisch 40 Kugeln senden können, die mit Mikrokollisionen herumspringen, und es in einem Schüttelraum, der die Bildrate nicht einmal verringert, auf Hochtouren laufen könnte. Ich beziehe mich auf die Durchführung Ihrer "Delta-Komprimierung / Codierung", auch als Daten-Differenzierung bekannt, die Sie besprochen haben. Es ist ziemlich ähnlich zu dem, was ich ansprechen wollte.

Dead Reckoning VS Data Differencing: Sie sind unterschiedlich genug und belegen nicht die gleichen Methoden, sodass Sie beide implementieren können, um die Optimierung noch weiter zu steigern! Hinweis: Ich habe beide nicht zusammen verwendet, sondern mit beiden gearbeitet.

Delta-Codierung oder Datenunterschiede: Der Server überträgt Daten über das, was Clients wissen, und sendet nur die Unterschiede zwischen alten Daten und dem, was geändert werden soll, an den Client. zB pseudo-> in einem Beispiel könnten Sie die Daten "315 435 222 3546 33" senden, wenn die Daten bereits "310 435 210 4000 40" sind. Einige sind nur geringfügig geändert, und eine wird überhaupt nicht geändert! Stattdessen würden Sie (im Delta) "5 0 12 -454 -7" senden, was erheblich kürzer ist.

Bessere Beispiele können sich viel weiter ändern, zum Beispiel, wenn ich gerade eine verknüpfte Liste mit 45 verknüpften Objekten habe. Ich möchte 30 von ihnen töten, also mache ich das und sende dann an alle, was die neuen Paketdaten sind, was den Server verlangsamen würde, wenn er nicht bereits für solche Dinge gebaut worden wäre, und es geschah, weil es versucht wurde sich zum Beispiel zu korrigieren. Bei der Delta-Codierung würden Sie einfach (Pseudo) "list.kill 30 at 5" eingeben und 30 Objekte aus der Liste entfernen, nachdem das fünfte Objekt die Daten authentifiziert hat, jedoch auf jedem Client und nicht auf dem Server.

Vorteile: (Kann mir momentan nur eines von beiden vorstellen)

  1. Geschwindigkeit: Offensichtlich habe ich in meinem letzten Beispiel beschrieben. Es wäre ein viel größerer Unterschied als im vorherigen Beispiel. Im Allgemeinen kann ich aus Erfahrung nicht ehrlich sagen, welche davon häufiger vorkommen würden, da ich viel mehr mit toten Zahlen arbeite

Nachteile:

  1. Wenn Sie Ihr System aktualisieren und weitere Daten hinzufügen möchten, die über das Delta bearbeitet werden sollen, müssen Sie neue Funktionen erstellen, um diese Daten zu ändern! (zB wie früher "list.kill 30 at 5" Oh shit, ich brauche eine Undo-Methode, die dem Client hinzugefügt wurde! "list.kill undo")

Dead Reckoning: Einfach ausgedrückt, hier ist eine Analogie. Ich schreibe eine Karte für jemanden darüber, wie er an einen Ort kommt, und gebe nur die Punkte an, an denen er im Allgemeinen hinkommt, weil es gut genug ist (beim Bauen anhalten, links abbiegen). Jemand anderes auf der Karte hat die Straßennamen und auch, um wie viel Grad man links abbiegen muss, ist das überhaupt nötig? (Nein...)

Bei Dead Reckoning hat jeder Client einen Algorithmus, der pro Client konstant ist. Daten werden so gut wie nur geändert, indem angegeben wird, welche Daten geändert werden müssen und wie dies zu tun ist. Der Kunde ändert die Daten selbstständig. Ein Beispiel ist, dass wenn ich einen Charakter habe, der nicht mein Spieler ist, aber von einer anderen Person bewegt wird, die mit mir spielt, ich die Daten nicht bei jedem Frame aktualisieren muss, da viele Daten konsistent sind!

Angenommen, mein Charakter bewegt sich in eine Richtung. Viele Server senden Daten an die Clients, die angeben (fast pro Frame), wo sich der Player befindet und dass er sich bewegt (aus Animationsgründen). Das sind so viele unnötige Daten! Warum zum Teufel muss ich jeden einzelnen Frame aktualisieren, wo sich das Gerät befindet und in welche Richtung es zeigt UND dass es sich bewegt? Einfach ausgedrückt: ich nicht. Sie aktualisieren die Clients nur, wenn sich die Richtung ändert, wenn sich das Verb ändert (isMoving = true?) Und was das Objekt ist! Dann wird jeder Client das Objekt entsprechend verschieben.

Persönlich ist dies eine Taktik des gesunden Menschenverstands. Es ist etwas, von dem ich dachte, dass ich es klug finde, es vor langer Zeit zu erfinden.

Antworten

Um ganz ehrlich zu sein, lies James 'Post und lies, was ich über die Daten gesagt habe. Ja, Sie sollten auf jeden Fall Delta-Codierung verwenden, aber überlegen Sie auch, ob Sie Dead Reckoning verwenden.

Persönlich würde ich die Daten auf dem Client instanziieren, wenn er vom Server Informationen darüber erhält (etwas, das Sie vorgeschlagen haben).

Nur Objekte, die sich ändern können, sollten überhaupt als bearbeitbar eingestuft werden, oder? Ich mag Ihre Vorstellung, dass ein Objekt Netzwerkdaten über Ihr Komponenten- und Entitätssystem haben sollte! Es ist klug und sollte gut funktionieren. Sie sollten jedoch überhaupt keinen Netzwerkmethoden Pinsel (oder Daten, die absolut konsistent sind) geben. Sie brauchen es nicht, da es sich nicht ändern kann (Client zu Client also).

Wenn es so etwas wie eine Tür ist, würde ich ihr Netzwerkdaten geben, aber nur einen booleschen Wert dafür, ob sie offen sind oder nicht, dann ist es offensichtlich, was für ein Objekt es ist. Der Client sollte wissen, wie er es ändern kann, z. B. ist es offen, schließt es, jeder Client erhält die Aufforderung, dass alle ihn schließen sollen, sodass Sie die booleschen Daten ändern und dann die Tür zum Schließen animieren.

Was die Art und Weise betrifft, wie die zu vernetzenden Variablen ermittelt werden sollen, habe ich möglicherweise eine Komponente, die wirklich ein SUB-Objekt ist, und gebe ihr Komponenten, die Sie vernetzen möchten. Eine andere Idee ist, nicht nur zu haben, AddComponent("whatever")sondern auch, AddNetComponent("and what have you")weil es sich persönlich schlauer anhört.

Joshua Hedges
quelle
Das ist eine lächerlich lange Antwort! Das tut mir schrecklich leid. Da wollte ich nur ein bisschen Wissen liefern und dann meine 2 Cent über einiges. Ich verstehe also, dass vieles unnötig sein kann.
Joshua Hedges
3

Wollte einen Kommentar schreiben, entschied aber, dass dies genug Information für eine Antwort sein könnte.

Zuerst +1 für eine so schön geschriebene Frage mit Unmengen von Details, anhand derer die Antwort beurteilt werden kann.

Für das Laden der Daten hätte ich den Client die Welt aus der Weltdatei laden lassen. Wenn Ihre Entitäten über IDs verfügen, die aus der Datendatei stammen, würde ich sie auch standardmäßig laden, damit Ihr Netzwerksystem sie nur referenzieren kann, um zu wissen, um welche Objekte es sich handelt. Jeder, der die gleichen Anfangsdaten lädt, sollte bedeuten, dass alle für diese Objekte die gleichen IDs haben.

Zweitens sollten Sie keine NetworkComponent-Komponente erstellen, da dies nichts anderes bewirkt, als Daten in andere vorhandene Komponenten zu replizieren (Physik, Animation und dergleichen sind einige übliche Dinge, die übertragen werden müssen). Um Ihre eigene Benennung zu verwenden, möchten Sie möglicherweise einen NetworkComponentManager erstellen. Dies unterscheidet sich geringfügig von der Beziehung zwischen der anderen Komponente und dem ComponentManager. Dies kann jedoch ausgelöst werden, wenn Sie ein vernetztes Spiel starten und alle Arten von Komponenten, die einen Netzwerkaspekt haben, dem Manager ihre Daten zur Verfügung stellen, damit dieser sie packen kann und losschicken. Hier könnte Ihre Save / Load-Funktionalität zum Einsatz kommen, wenn Sie über einen Serialisierungs- / Deserialisierungsmechanismus verfügen, für den Sie, wie erwähnt, auch Daten packen können.

In Anbetracht Ihrer Frage und des Informationsniveaus glaube ich nicht, dass ich viel mehr ins Detail gehen muss, aber wenn etwas nicht klar ist, senden Sie bitte einen Kommentar und ich werde die Antwort aktualisieren, um dies zu beheben.

Hoffe das hilft.

James
quelle
Sie sagen also, dass Komponenten, die vernetzt werden sollen, eine Schnittstelle wie diese implementieren sollen: void SetNetworkedVariable (Zeichenfolgenname, NetworkedVariable-Wert); NetworkedVariable GetNetworkedVariable (Zeichenfolgenname); Wobei NetworkedVariable zum Interpolieren und für andere Netzwerkaufgaben verwendet wird. Ich weiß jedoch nicht, wie ich feststellen soll, welche Komponenten dies implementieren. Ich könnte die Typidentifikation zur Laufzeit verwenden, aber das erscheint mir hässlich.
Carter