2016-09-01 4 views
5

Ich versuche, eine Architektur für ein MMO-Spiel zu machen, und ich kann nicht herausfinden, wie ich so viele Variablen in GameObjects speichern kann, ohne viele Anrufe zu haben schicke sie zur gleichen Zeit auf ein Kabel, ich aktualisiere sie.C++ Member Variable Change Listeners (100+ Klassen)

Was ich habe, ist jetzt:

Game::ChangePosition(Vector3 newPos) { 
    gameobject.ChangePosition(newPos); 
    SendOnWireNEWPOSITION(gameobject.id, newPos); 
} 

er den Code Müll macht, schwer zu pflegen, zu verstehen, zu erweitern. So denken Sie an einen Meister Beispiel:

GameObject Champion example

Ich muss eine Menge Funktionen für jede Variable machen würde. Und das ist nur die Verallgemeinerung für diesen Champion, ich könnte 1-2 andere Mitgliedsvariable für jeden Champion-Typ/"Klasse" haben.

Es wäre perfekt, wenn ich OnPropertyChange von .NET oder etwas Ähnliches haben könnte. Die Architektur ich versuche funktionieren würde zu erraten schön ist, wenn ich etwas ähnliches hatte:

Für HP: wenn ich es aktualisieren, automatisch SendFloatOnWire("HP", hp);

Für Position nennen: wenn ich es aktualisieren, rufen automatisch SendVector3OnWire("Position", Position)

Für Name: wenn ich es aktualisieren, rufen automatisch SendSOnWire("Name", Name);

Was genau sind SendFloatOnWire, SendVector3OnWire, SendSOnWire? Funktionen, die diese Typen in einem Zeichenpuffer serialisieren.

oder Methode 2 (Bevorzugtes), aber vielleicht

aktualisieren Hp, Position normal und dann jedes Netzwerk Thema tick-Scan alle Instanzen Gameobject auf dem Server für die geänderten Variablen und senden diejenigen teuer sein.

Wie würde das auf einem großen Spieleserver implementiert werden und was sind meine Optionen? Irgendein nützliches Buch für solche Fälle?

Würden sich Makros als nützlich erweisen? Ich denke, ich wurde zu etwas Quellcode von etwas ähnlichem explodiert und ich denke, dass es Makros verwendete.

Vielen Dank im Voraus.

EDIT: Ich denke, ich habe eine Lösung gefunden, aber ich weiß nicht, wie robust es tatsächlich ist. Ich werde es ausprobieren und sehen, wo ich danach stehe. https://developer.valvesoftware.com/wiki/Networking_Entities

+0

Sind Sie sicher, dass der Server die ein nur ein dummer Renderer das Spiel, und der Client ausgeführt werden nicht wollen? Auf diese Weise müssen Sie nicht alle diese Attribute über die Leitung senden (und es hilft, Betrug zu vermeiden, da der Client nicht manipuliert werden kann, um unehrliche Werte zu melden). – Cornstalks

+0

Genau so funktioniert es, aber ich muss dem Kunden immer noch mitteilen, was er rendern muss. – ioanb7

+0

ChangePosition() ist nur ein Beispiel, wo der Client entscheidet, wohin er gehen möchte. Es ist nur ein Ziel, der Server kann entscheiden, ob es gültig ist oder nicht, usw. Ich gab es rein als Beispiel. – ioanb7

Antwort

0

Gesamtschlussfolgerung Ich kam zu: Einen weiteren Anruf nachdem ich die Position aktualisiert habe, ist nicht so schlimm. Es ist eine Zeile Code länger, aber es ist besser für verschiedene Motive:

  1. Es ist explizit. Du weißt genau, was passiert.
  2. Sie verlangsamen den Code nicht, indem Sie alle Arten von Hacks machen, damit es funktioniert.
  3. Sie verwenden keinen zusätzlichen Speicher.

Methoden Ich habe versucht:

  1. Mit Karten für jeden Typ, wie @Christophe vorschlagen. Der größte Nachteil war, dass es nicht fehleranfällig war. Sie hätten HP und HP in derselben Map deklarieren können, und es hätte eine weitere Ebene von Problemen und Frustrationen ergeben können, wie das Deklarieren von Maps für jeden Typ und dann das Voranstellen jeder Variable mit dem Mapnamen.
  2. Verwendung von etwas SIMILAR zu Ventil engine: Es erstellt eine separate Klasse für jede Netzwerkvariable, die Sie wollten. Anschließend verwendete es eine Vorlage, um die von Ihnen deklarierten Basistypen (int, float, bool) und erweiterte Operatoren für diese Vorlage zu schließen. Es verwendet viel zu viel Speicher und zusätzliche Aufrufe für grundlegende Funktionalität.
  3. Verwenden Sie eine data mapper, die Zeiger für jede Variable im Konstruktor hinzugefügt und dann mit einem Offset gesendet. Ich habe das Projekt vorzeitig verlassen, als mir klar wurde, dass der Code verwirrend und schwer zu pflegen war.
  4. Verwenden Sie eine Struktur, die jedes Mal gesendet wird, wenn sich etwas ändert, manuell. Dies ist einfach mit protobuf. Das Erweitern von Strukturen ist auch einfach.
  5. Jedes Häkchen, generieren Sie eine neue Struktur mit den Daten für die Klassen und senden Sie es. Dies hält sehr wichtige Dinge immer auf dem neuesten Stand, aber isst viel Bandbreite.
  6. Verwenden Sie Reflexion mit Hilfe von Boost. Es war keine großartige Lösung.

Immerhin ging ich mit einer Mischung aus 4 und 5. Und jetzt implementiere ich es in meinem Spiel. Ein großer Vorteil von protobuf ist die Möglichkeit, Strukturen aus einer .proto-Datei zu generieren und gleichzeitig die Serialisierung für die Struktur zu ermöglichen. Es ist unglaublich schnell.

Für diese speziellen benannten Variablen, die in Unterklassen erscheinen, habe ich eine andere Struktur erstellt. Alternativ könnte ich mit Hilfe von Protobuf eine Reihe von Eigenschaften haben, die so einfach sind: ENUM_KEY_BYTE VALUE. Wobei ENUM_KEY_BYTE nur ein Byte ist, das eine enum auf Eigenschaften wie IS_FLYING, IS_UP, IS_POISONED und VALUE verweist, ist ein string.

Das Wichtigste, was ich daraus gelernt habe, ist so viel Serialisierung wie möglich zu haben. Es ist besser, mehr CPU an beiden Enden zu verwenden, als mehr Input & Output zu haben.

Wenn jemand Fragen hat, kommentieren und ich werde mein Bestes tun, Ihnen zu helfen.

ioanb7

1

Auf Methode 1:

Ein solcher Ansatz relativ sein könnte „easy“ eine Karten zu implementieren verwenden, die über Getter/Setter zugegriffen wird. Die allgemeine Idee wäre so etwas wie:

class GameCharacter { 
    map<string, int> myints; 
    // same for doubles, floats, strings 
public: 
    GameCharacter() { 
     myints["HP"]=100; 
     myints["FP"]=50; 
    } 
    int getInt(string fld) { return myints[fld]; }; 
    void setInt(string fld, int val) { myints[fld]=val; sendIntOnWire(fld,val); } 
}; 

Online demo

Wenn Sie die Eigenschaften in der Klasse zu halten bevorzugen, können Sie für eine Karte auf Zeiger oder Mitglied Zeiger statt Werte gehen würde. Bei der Konstruktion würden Sie dann die Karte mit den entsprechenden Zeigern initialisieren.Wenn Sie sich entscheiden, die Membervariable zu ändern, sollten Sie jedoch immer über den Setter gehen.

Sie könnten sogar weiter gehen und Ihre Champion abstrahieren, indem Sie es zu einer Sammlung von Eigenschaften und Verhaltensweisen machen, auf die über die Karte zugegriffen werden kann. Diese Komponentenarchitektur wird von Mike McShaffry in Game Coding Complete (ein Muss für jeden Spieleentwickler Buch lesen) ausgesetzt. Es gibt eine community site für das Buch mit etwas Quellcode zum Download. Sie können sich die Datei actor.h und actor.cpp ansehen. Trotzdem empfehle ich wirklich, die vollständigen Erklärungen in dem Buch zu lesen.

Der Vorteil der Komponentenisierung besteht darin, dass Sie Ihre Netzwerkweiterleitungslogik in die Basisklasse aller Eigenschaften einbetten können. Dies könnte Ihren Code um eine Größenordnung vereinfachen.

Auf Methode 2:

Ich denke, die Grundidee perfekt geeignet ist, mit der Ausnahme, dass eine vollständige Analyse (oder noch schlimmer, Übertragung) aller Objekte zuviel des Guten wäre.

Eine nette Alternative wäre eine Markierung, die gesetzt wird, wenn eine Änderung gemacht wird und zurückgesetzt wird, wenn die Änderung übertragen wird. Wenn Sie markierte Objekte (und möglicherweise nur markierte Eigenschaften davon) übertragen, minimieren Sie die Arbeitslast Ihres Synchronisationsthreads und reduzieren den Netzwerkaufwand, indem Sie die Übertragung mehrerer Änderungen, die sich auf dasselbe Objekt auswirken, bündeln.

+2

Um Methode 2 zu erweitern, könnten Sie eine globale Gruppe von Objekten speichern, die veraltet sind. Wenn sich eine Eigenschaft ändert, fügen Sie einen Zeiger auf die Änderung zu dieser Gruppe hinzu und im Netzwerk-Tick, durchlaufen Sie nur die Objekte in dieser Gruppe, übertragen Sie ihre Mitglieder, die sich geändert haben, und löschen Sie am Ende die Menge. Dadurch sparen Sie die Notwendigkeit, bei jedem Netzwerk-Tick über alle Objekte zu iterieren, um zu prüfen, ob sie aktualisiert werden müssen. – Rotem

+0

Hallo, danke für deine Vorschläge. Glauben Sie, dass Sie Ihre Antwort mit einem echten Code oder Pseudocode ergänzen könnten? Durch die Markierung der geänderten Eigenschaften meinst du, ich hätte eine Menge void * -Zeiger auf jedes (object-> propertyName)? – ioanb7

+0

@ ioanb7 Ich habe meine Antwort bearbeitet, um sie weiter zu verdeutlichen und zu illustrieren. Aber ein wichtiger Punkt: Niemals 'void *'! Sie könnten eine Familie von Funktionen nach Typ wie Ihre 'SendXXXOnWire()' für die verschiedenen Typen, die Sie unterstützen, ablehnen (ich zeigte dies in einer Bearbeitung). Oder besser Template-Getter/Setter verwenden, um Code-Redundanz zu vermeiden. Alternativ könnten Sie sich für 'boost :: variant' oder eine Union entscheiden. – Christophe

Verwandte Themen