Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

20.02.2014, 13:13

Hi,

für meinen Dungeon Crawler will ich Model-View-Controller auf einer Client-Server-Architektur implementieren. Dazu habe ich mir folgende Klassenstruktur überlegt, bei der sich das Model auf dem Server und die View auf dem Client befinden. Anfragen des Clients nenne ich Requests. Auf sie gibt es keine synchrone Antwort. Der Server antwortet durch von seinem Model erzeugte Events, die der Client interpretiert und darstellt. Auf UML-Diagramme verzichte ich hier mal. Durch die ganzen Vererbungs-, Implementierungs- und Aggregrationspfeile wird das ganze ziemlich unübersichtlich.

Interfaces
  • IMapEventListener: hört auf Map-Events des Models und setzt diese in der View um (z.B. createEntity oder updateEntity).
  • IPlayerEventListener: hört auf Player-Events des Models und setzt diese in der View um (z.B. logoutPlayer).
  • IEventListener: erbt von IMapEventListener und IPlayerEVentListener und hört somit auf alle Events des Models und setzt diese in der View um.
  • IMapRequestListener: hört auf Map-Requests der View und setzt diese im Model um (z.B. moveEntity).
  • IPlayerRequestListener: hört auf Player-Requests der View und setzt diese im Model um (z.B. unauthPlayer).
  • IRequestListener: erbt von IMapRequestListener und IPlayerRequestListener und hört somit auf alle Requests der View und setzt diese im Model um.
Server
  • ModelManager
    • .. implementiert IRequestListener, um auf Anfragen der View reagieren zu können.
      Die Anfragen kommen direkt von der View und gehen an einen entsprechenden Unter-Manager.
    • .. implementiert IEventListener, um Ereignisse der View mitteilen zu können.
      Die Ereignisse gehen direkt an die View und kommen von einem entsprechenden Unter-Manager.
    • .. besitzt einen PlayerManager und einen MapManager.
  • PlayerManager
    • .. implementiert IPlayerRequestListener, um auf Player-Anfragen der View regieren zu können.
      Die Anfragen kommen vom ModelManager und werden hier bearbeitet.
      ggf. werden hier Events erzeugt und weitergeleitet.
    • .. implementiert IPlayerEventListener, um auf Player-Events anderer Unter-Manager reagieren zu können.
      Anschließend werden die Events weitergeleitet.
      Solche Ereignisse können z.B. von einer Admin-Schnittstelle ausgelöst werden (z.B. Spieler kicken)
    • .. besitzt einen Zeiger auf ein IPlayerEventListener (konkret: ModelManager), um Event an dieses weitergeben zu können.
    • .. besitzt einen Zeiger auf ein IMapEventListener (konkret: MapManager), um hier Map-Events (siehe nächster Unter-Manager) zu erzeugen.
  • MapManager
    • .. implementiert IMapRequestListener, um auf Map-Anfragen der View reagieren zu können.
      Die Anfragen kommen vom ModelManager und werden hier bearbeitet.
      ggf. werden hier Events erzeugt und weitergeleitet.
    • .. implementiert IMapEventListener, um auf Map-Evens anderer Unter-Manager reagieren zu können
      Anschließend werden diese Events weitergeleitet.
      Solche Ereignisse können z.B. ein vom PlayerManager ausgelöstes createEntity (für die Spielerfigur) sein.
    • .. besitzt einen Zeiger auf ein IMapEventListener (konkret: ModelManager), um Events an dieses weitergeben zu können.

Client
  • ViewManager
    • .. implementiert IEventListener, um auf Ereignisse des Models reagieren zu können.
      Die Ereignisse werden direkt vom Model erhalten und an die entsprechenden Unter-Manager weitergeleitet.
    • .. besitzt einen PlayerManager und einen MapManager.
  • PlayerManager .. implementiert IPlayerEventListener, um auf Ereignisse des Models reagieren zu können.
    Die Ereignisse werden vom ViewManager hier her weitergeleitet und hier ausgewertet.
  • MapManager .. implementiert IMapEventListener, um auf Ereignisse des Models reagieren zu können.
    Die Ereignisse werden vom ViewManager hier her weitergeleitet und hier ausgewertet.

Der Grund für die Verwendung von ModelManager und ViewManager ist die Kapselung der Netzwerkverbindung. D.h. dort findet die Serialisierung und Deserialisierung statt, so dass ich das für die Unter-Managern gekapselt habe. Damit sollten die Unter-Manager auch mit einem ModelManager arbeiten, der gar nicht über das Netzwerk kommuniziert.

Unterm Strich ist das Design recht gut entkoppelt, oder wie seht ihr das? Welche Probleme/Schwachstellen seht ihr?
Danke im Voraus :)
Ich hoffe ich habe jetzt nix vergessen :P

LG Glocke

2

20.02.2014, 13:40

Ein Bild sagt mehr als tausend Worte.

3

20.02.2014, 14:31

Ich habe eine Variante mal hier angehangen ;)
»Glocke« hat folgendes Bild angehängt:
  • client_server.png

TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

4

21.02.2014, 14:09

Prinzipiell ist das ja ein schichtbasierter Ansatz mit Listenern. Sieht soweit nicht verkehr aus, jedoch Frage ich mich, ob du bei diesem Konzept eine gute Client/Server kommunikation ermöglichen kannst. Es gibt Situationen, wo Events nicht hinreichend sind, um den Zustand auf dem Client korrekt darzustellen. Vor allem, wenn die Events aus verschiedenen Quellen kommen. Das ist aber denke ich schon mehr Detail, was die Client-Server kommunikation angeht.

Du hast ziemlich viele Manager, was eigentlich heute nicht mehr so gern gesehen wird. Man neigt dadurch dazu, viel in einer Klasse zentral zu verwalten, was nicht gut ist. Ich hätte wahrscheinlich eher einen Ansatz gewählt, dass du prinzipiell Objekte auf Client und Serverseite hast, die Eigenschaften haben, die der Server hält und dem Client publiziert. DerClient kann Kommandos an den Server schicken (welche du ja schon hast) und erhält irgendwann eine Antwort. Die speziellen Aufgaben können dann in speziellen Klassen verarbeitet werden, die von deinem generellen Objekt erben. Dann hast du aber auf Client/Server Seite nur eine Listener-implementierung, was deine Vererbung aber zwischen den Schichten stark vereinfacht.

5

21.02.2014, 15:28

Du hast ziemlich viele Manager, was eigentlich heute nicht mehr so gern gesehen wird. Man neigt dadurch dazu, viel in einer Klasse zentral zu verwalten, was nicht gut ist.

Welche Lösung würdest du vorschlagen? Damit ich nicht zu viel zentral in einer Klasse mache, hatte ich die Idee mit den Unter-Managern, die dann konkrete Teilaufgaben in sich geschlossen lösen (z.B. Erstellung, Veränderung und Zerstörung von Entities). Man könnte die Struktur mit den Managern sich hier erstmal wegdenken (siehe Anhang). Aber unterm Strich wäre das genau die Idee, die mir als direkteste Lösung einfällt.

Ich hätte wahrscheinlich eher einen Ansatz gewählt, dass du prinzipiell Objekte auf Client und Serverseite hast, die Eigenschaften haben, die der Server hält und dem Client publiziert. DerClient kann Kommandos an den Server schicken (welche du ja schon hast) und erhält irgendwann eine Antwort.

Eigentlich war genau das meine Idee :D Auf dem Server sind die Model-Objekte, auf dem Client die View-Objekte. Die gemeinsamen Eigenschaften sind die, die ich vom Server zum Client publizieren will (Position, Blickrichtung z.B.). Nur, dass ich keine synchrone Kommunikation mit Frage-Antwort habe. Synchronität will ich absichtlich nicht erzwingen. Das ist in meinen Augen (zumindest für meine Absichten) zu viel Aufwand.

LG Glocke
»Glocke« hat folgendes Bild angehängt:
  • client-server.png

TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

6

21.02.2014, 16:04

Was ich meine ist in etwas folgendes:


(Link)


Du hast ein GameObject (ich weiß viele mögen den Namen nicht, ist beliebig ersetzbar) für den Client und eines für den Server. Das GameObject hat ein ServerObjekt, welches ich hier mal ServerGameObject genannt habe. Jedes GameObject bietet verschiedene Eigenschaften an, die über GameObjectProperty verfügbar sind. Diese werden von den ServerObjekten bereitgestellt und aktualisiert. Für das aktualiseren ist das Listener-Konzept in Ordnung, welches ich direkt auf den Serverobjekten anwenden würde.
Der GameObjectListenerController dient dazu, die Werte von den ServerObjekten anzunehmen und die entsprechenden GameObject-Instanzen zu aktualisieren. Dazu könnte das GameObject etwa eine Methode UpdateFromServer() bekommen. Diese müssen dann die speziellen Clientklassen (Player, Map) selbst auswerten.
Wenn der Client etwas zum server schicken will, dann könnte man auf dem GameObject eine Methode SendCommand() anbieten, die ziemlich generisch sein sollte (Stringtext oder vielleicht ein interface). Dieses muss zum server übertragen werden und zum jeweiligen Objekt weitergeleitet werden.

Das hat den enormen Vorteil, dass die Kommunikation zwischen Client und Server über eine Schnittstelle erfolgt, und das eigentlich sogar erstmal unabhängig von der konkreten Nachricht.

Weiterhin solltest du, auch wenn es nach Mehraufwand aussieht, für die Views ein Interface aus den Spieleklassen extrahieren. Das rentiert sich wenn:

1) Du einen View für mehrere Spielobjekte verwenden willst.
2) Dinge schon ausprobieren willst, ohne dass die korrekte Implementierung vorhanden ist.
3) Eine echte Implementierung austauschen, um GUI-Effekte o. Ä. zu testen.
3) Du kannst getrennt View und Model implementieren.

7

21.02.2014, 16:40

Okay, danke! Das klingt schonmal sehr interessant :)

Jedes GameObject bietet verschiedene Eigenschaften an, die über GameObjectProperty verfügbar sind.

Kann ich das so verstehen, dass mein GameObject (je nach konkreter Ableitung) verschiedene oder gemeinsame Properties hat? Eine gemeinsame "Über-Property" wäre afaik nicht "schön". Dann müsste allerdings GameObject generisch (hinsichtlich des Property-Typs) sein. Meinst du das so?
Also, dass PlayerObject entsprechend PlayerObjectProperties und MapObject analog MapObjectProperties besitzt.

Dazu könnte das GameObject etwa eine Methode UpdateFromServer() bekommen. Diese müssen dann die speziellen Clientklassen (Player, Map) selbst auswerten.

D.h. ich würde der Methode die neuen Properties übergeben, so dass es sich entsprechend selber aktualisiert (je nach Implementierung der Methode durch die Ableitungen Player, Map etc.) ? Dann wäre allerdings das "Problem", dass ich einen Basistyp für die Properties bräuchte und dann z.B. in der UpdateFromServer-Implementierung der PlayerKlasse die BaseObjectProperties zu PlayerObjectProperties casten müssten - oder ich verwende meine BinaryStream-Klasse (siehe Frage unter dem nächsten Quote).

Also würden die Daten irgendwo über den Socket kommen, ich mir (z.B. anhand einer ID) das entsprechende GameObject raussuchen und updateFromServer(currentData) callen?

Wenn der Client etwas zum server schicken will, dann könnte man auf dem GameObject eine Methode SendCommand() anbieten, die ziemlich generisch sein sollte (Stringtext oder vielleicht ein interface). Dieses muss zum server übertragen werden und zum jeweiligen Objekt weitergeleitet werden.

Was meinst du genau mit "ziemlich generisch" und "Stringtext oder Interface" in diesem Zusammenhang?
Für die Netzwerk-Implementierung verwende ich eine Klasse "BinaryStream", die meine Daten binär (und Endianess-safe) enthält. Die Instanz kann ich direkt senden und empfangen. Was ich mir bei sendCommand vorstellen könnte wäre, dass meine konkrete Implementierung (wieder Player, Map, etc.) einen solchen BinaryStream erzeugt und an den Server gibt. Kommt das so mit dem hin was du meintest?

LG Glocke

/EDIT: Ich habe mein Diagramm mal so angepasst, wie ich es verstanden habe (bzw. verstehen würde^^)
»Glocke« hat folgendes Bild angehängt:
  • client-server_version2.png

Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von »Glocke« (21.02.2014, 17:12)


TrommlBomml

Community-Fossil

Beiträge: 2 117

Wohnort: Berlin

Beruf: Software-Entwickler

  • Private Nachricht senden

8

22.02.2014, 14:12

Erstmal wollte ich noch verdeutlichen, dass das hier nur Vorschläge von mir sind. Bitte überdenke sie auch kritisch :) Mein Ziel war es nicht, deine Architektur umzuwerfen^^.

Okay, danke! Das klingt schonmal sehr interessant

Zitat von »TrommlBomml«
Jedes GameObject bietet verschiedene Eigenschaften an, die über GameObjectProperty verfügbar sind.

Kann ich das so verstehen, dass mein GameObject (je nach konkreter Ableitung) verschiedene oder gemeinsame Properties hat? Eine gemeinsame "Über-Property" wäre afaik nicht "schön". Dann müsste allerdings GameObject generisch (hinsichtlich des Property-Typs) sein. Meinst du das so?
Also, dass PlayerObject entsprechend PlayerObjectProperties und MapObject analog MapObjectProperties besitzt.


Nein, jede Ausprägung hat seine eigenen Properties. Ein GameObject könnte halt zum Beispiel Player sein und erbt von GameObject. Player legt dann über geerbte Methoden von GameObject seine konkreten Properties, an, z.B. PositionX (float), PositionY (float) und Name (string).
Dadurch, dass die kommunikation mit dem server über GameObject läuft, werden bei einem Update von Server einfach alle Properties (PositionX, PositionY, Name) aktualisiert. Setzt natürlich voraus, das Client- und Serverobjekt dieselben Eigenschaften haben (genauer: Server stellt alle notwendigen Eigenschaften für den Client bereit), dieses Problem hast du aber immer bei Client/Server Architekturen.

Bei UpdateFromServer() erhältst du bspw. bei Player die Information, dass sich die PositionX- und PositionY-Properties geändert haben und dann kannst du im Client darauf reagieren. Wie man diese Notifikation genau implementiert, ist eine gute Frage. Im Zweifel merkt man sich die zuletzt vom Server erhaltenen Werte und vergleicht sie mit den aktuellen in den Properties.

Zitat von »Glocke«

Was meinst du genau mit "ziemlich generisch" und "Stringtext oder Interface" in diesem Zusammenhang?


Das es dem SendCommand egal ist, ob es ein Kommando vom Spieler oder der Map ist. Dadurch muss man es nur an einer stelle implementieren.

Um dir das ganze zu verdeutlichen werd ich das einfach mal versuchen in kurzen Snippets zu tippen (sobald ich zeit habe).

EDIT: Ich habe mal ein bisschen Code geschrieben. So in etwa könnte ich mir das vorstellen. Besonders spannend ist die CommandData-Implementierung. Ich wollte nur einen überblicksmäßigen Vorschlag machen.

Trommlbomml Code

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »TrommlBomml« (22.02.2014, 14:25)


9

22.02.2014, 17:07

Erstmal wollte ich noch verdeutlichen, dass das hier nur Vorschläge von mir sind. Bitte überdenke sie auch kritisch :) Mein Ziel war es nicht, deine Architektur umzuwerfen^^.

:thumbsup:

Ich habe gerade überlegt unsere beiden Konzepte zu kombinieren :D Und zwar habe ich in jedem Fall vor Teile der Logik in sich abgeschlossen als Klasse(n) zu implementieren. Das heißt konkret:
  • Eine Klasse verwaltet die Spieler, d.h. hält die Instanzen einer Player-Klasse, kann neue Spieler erzeugen (login behandeln) und Spieler entfernen (logout behandeln).
  • Analog beabsichtige ich Manager für die Verwaltung von Dungeons (DungeonManager), von Entities (EntityManager) und später auch Characteren etc. (CharManager).

Das ist erstmal das Konzept was ich eigentlich definitiv verfolgen will. Was ich mir (hinsichtlich deines GameObject-Vorschlags) gut vorstellen könnte wäre, jeden dieser Manager zu einem speziellen GameObject zu machen - anstatt jeden einzelnen Spieler und jeden einzelnen Mob auf der Map als spezielles GameObject darzustellen. Damit hätte ich immernoch eine Kommunikation mittels definierter Schnittstelle - nur, dass diese von konkretem Object zu Object abweicht - eben Implementierungs-bedingt durch den Parameter-Typ (ich denke btw immer sehr implementierungsbezogen bei sowas xD)

Konkretes Beispiel:
Jeder der Manager erhält eine update()-Methode mit entsprechendem Parameter (PlayerManager: PlayerUpdate etc.) und handhabt somit die Updates die seinen konkreten Bereich betreffen vollständig autonom und in sich abgeschlossen. Und die Struktur "PlayerUpdate" etc. könnte ich mir so vorstellen, dass jedes eine Aktions-ID (CREATE, UPDATE, DESTROY) und die entsprechenden Properties enthält. Anhand der Aktions-ID wird innerhalb der update()-Methode dann entsprechend ein Objekt erzeugt, aktualisiert oder zerstört. Genau da sehe ich sonst ein Problem: Gebe ich mein Update (z.B. Zerstöre Spieler #3) an das GameObject (den Spieler #3) müsste es sich selbst zerstören. Oder noch ein Beispiel: Soll ein Spieler #7 mit Daten XY erzeugt werden, gibt es ja noch gar kein GameObject dafür. Dann müsste ich ohne hin diese Unterscheidung durchführen.

Bei den Properties würde ich da keinen großen Wind machen: Eine Mischung aus Primitivtypen und STL-Containern. Für das Senden der Update-Daten muss ich die eh in meinen BinaryStream reinstopfen - von daher lasse ich Serialisierung etc. hier mal weg. Stellen wir uns vor es wäre vollkommen egal, ob die Daten via Netzwerk versendet werden, oder der Listener (der sie interpretiert) direkt in dieser Anwendung sitzt. Das erscheint mir hinsichtlich Komplexität eine vertretbare Position :)

Jeder Request vom Client (z.B. einen Feuerball zu casten) würde dann am Server in Teil-Schritte zerlegt werden:
  1. Voraussetzungen (z.B. genug Mana vorhanden?) prüfen
  2. neue Entity erzeugen --> Update an EntityManager --> Weitergabe an Client
Entsprechend würde diese Logik im übergeordneten Manager laden - was aber strukturell kein Problem sein sollte, weil diese Aktionen ja bzgl. der anderen Manager eh übgeordnet sind.

Joa, das wäre so meine Idee wenn ich deinen Vorschlag mit einflechte :) Gefällt mir insgesamt schon viel besser als mein erster Vorschlag. Was denkt ihr (oder du - je nach dem wer hier noch mitliest :D )

LG Glocke

Werbeanzeige