Netzwerk Programmierung für 2D Shooter



  • Ich programmiere zurzeit einen Multiplayer 2D Shooter. Man kann sich schon gegenseitig abschießen und einige andere Sachen machen. Jetzt kommt nur leider die Übertragungsrate ins Spiel. Bis jetzt hat es mich nicht gestört wenn die Schüsse erst ca. 200 Pixel später beim Mitspieler angezeigt wurden, sie also schon ein Stück geflogen sind, aber so sollte die Endversion des Spiels natürlich nicht aussehen.
    Also meine Frage ist, ob sich hier jemand mit diesem Thema gut auskennt oder gute Tutorials zu dem Thema kennt wie man ein optimales Client-Server Verhältnis für ein solches Spiel programmiert. Ich habe schon mehrere Möglichkeiten ausprobiert wie zum Beispiel eine Anfrage mit den Daten des Spielers an den Server schicken und eine Antwort mit allen Informationen zu erhalten und wenn die angekommen ist wieder eine Anfrage schicken oder durchgängig Anfragen und Antworten schicken aber das hat, warum auch immer, mehr gelaggt als vorher. Wahrscheinlich wurde dann zu viel bearbeitet oder es wurde zu viel geschickt 🙄



  • Vorweg: ich hab' noch kein Netzwerk-Spiel programmiert, das was folgt ist das was ich mir über die Jahre so zusammengelesen bzw. ausgedacht habe. Wenn einer der Game-Gurus (rapso, ...) was einzuwenden hat empfehle ich also eher denen zu vertrauen als mir 🙂

    Verwendest du TCP/IP?
    Wenn ja, dann wäre es vielleicht angesagt auf UDP/IP umzusteigen.
    Wenn das nicht geht solltest du zumindest Nagle's Algorithm ausschalten (TCP_NODELAY).

    Am Client sammelst du dann alle Veränderungen die es an Objekten gegeben hat die der Client kontrolliert, und schickst sie jedes Frame an den Server (oder auch nur alle 2 oder 3 Frames -- aber je öfter desto besser, nur irgendwann macht natürlich die Netzwerkverbindung nicht mehr mit). Egal ob du eine Antwort vom Server erhalten hast oder nicht, auf Antworten wird nicht gewartet.

    Gleichzeitig kontrollierst du vor jedem Frame ob der Server eine Antwort geschickt hat.

    Der Server empfängt dann die Nachrichten von allen Clients, so wie sie halt eintreffen, und schickt in einem gewissen Takt Nachrichten an alle Clients raus, mit den Ändernungen an Daten von allen Objekten (bzw. von allen Objekten die nicht derjenige Client kontrolliert).
    Der Server wartet dabei auch nicht auf eine Nachricht von einem Client bevor er wieder Updates ausschickt. Erst wenn ein Client sich schon eine ganze Zeit lang nicht mehr gemeldet hat hört der Server auf Updates zu schicken -- oder kickt den Client gleich ganz.

    Wichtig ist dabei dass du das ganze so implementierst, dass der Client nie auf Daten vom Server wartet, oder darauf dass er selbst Daten senden kann, und dass der Server nie darauf wartet dass er Daten von einem bestimmten Client bekommt oder an einen bestimmten Client senden kann.
    D.h. entweder Threads oder non-blocking IO. Und bei TCP/IP mit non-blocking IO müssen Client und Server damit klarkommen unvollständige Nachrichten zu empfangen, damit sie mit dem was sonst noch zu tun ist weitermachen können (Client: Spiel ausführen, Server: andere Clients bedienen). D.h. Client und Server müssen unvollständige Nachrichten irgendwo puffern, und dann beim nächsten Durchgang den fehlenden Teil empfangen (bzw. beim übernächsten Durchgang - so lange wie es halt dauert).

    Damit das ganze wirklich gut funktioniert wirst du auch eine Game-Clock brauchen, die zwischen allen beteiligten synchronisiert wird. Der Server gibt dabei die Zeit vor, und die Clients müssen ihre eigene Clock danach synchronisieren. Was nicht ganz trivial ist, weil man dazu auch die Round-Trip-Time zwischen Client und Server kennen muss, und weil die Clocks von verschiedenen Computern ganz leicht gegeneinander driften werden (=nicht gleich schnell laufen). *

    Dann kann man nämlich jedes Paket, bzw. jedes Update über den Zustand eines Objekts mit einem Zeitstempel versehen. Was die einzige Technik ist die ich kenne, damit kein "Versatz" zwischen den Clients entsteht.

    Dabei Schickt dann der Client z.B. beim Update für einen Schuss die ID des Schusses, die aktuelle Position, aktuelle Geschwindigkeit und eben den Zeitstempel mit. Bzw. andere evtl. noch relevante Daten wie z.B. wie lange der Schuss noch zu leben hat oder sowas.

    Der Server verteilt diese Informationen dann an alle andere Clients. Nach einer kurzen Verzögerung haben dann alle Clients Informationen über den Schuss, und können die aktuelle Position extrapolieren. Das Extrapolieren ist nötig, weil die Position sich ja schon wieder verändert hat seit dem die Daten erfasst wurden, da seit dem ja Zeit vergangen ist.

    Damit das gut funktioniert ist natürlich noch eines nötig: die Berechnungen der Clients müssen möglichst exakt übereinstimmen, damit Rundungsfehler bzw. Quantisierungsfehler überall gleich sind. Bzw. die Fehler die bei numerischer Integration halt so entstehen.
    Die einfachste Möglichkeit das wiederum zu erreichen ist Grafik-Updates von den Gamestate-Updates zu entkoppeln.
    D.h. du rechnest nicht für jedes Frame ein Gamestate-Update, sondern fixierst die Gamestate-Update-Rate auf einen bestimmten Wert - z.B. 300 Hz.

    Wie viele Gamestate-Updates du dann zwischen zwei Frames machen musst bestimmst du anhand der synchronisierten Game-Clock.

    *:
    Mit dem oben beschriebenen System ist es natürlich etwas schwierig die Round-Trip-Time zu bestimmen, weil der Client ja nicht genau weiss wann der Server die letzte Nachricht abgeschickt hat. Das Versenden von Nachrichtem vom Server ist ja nicht mit dem Empfangen von Nachrichten vom Client synchronisiert.

    Dafür kann man am Server z.B. pro Client die Server-Zeit und Nachrichten-ID der zuletzt empfangenen Nachricht vom Client notieren. Beim nächsten Paket das an diesen Client rausgeht werden diese dann mitgeschickt, sowie die aktuelle Server-Zeit.

    Dann hat der Client genügend Informationen um die Trip-Time zu bestimmen. Er weiss wie viel Zeit nach seiner Clock vergangen ist seit er das Paket mit der vom Server genannten ID abgeschickt hat, und er weiss wie viel Zeit am Server vergangen ist bevor das Paket "beantwortet" wurde, und er weiss wann die Antwort nach seiner Clock angekommen ist. Was ausreichend sein sollte um sich die Round-Trip-Time auszurechnen.



  • Oha, danke!
    Auf so eine Antwort hab ich gehofft aber nicht erwartet dass sie kommen wird 😉

    Ja, zurzeit benutze ich noch TCP/IP aber gestern hab ich gelesen dass UDP in solchen Fällen um einiges schneller ist und hab angefangen das umzuprogrammieren bin aber noch nicht ganz fertig geworden. Ich sag bescheid obs da dran gelegen hat.

    Die Daten der einzelnen Waffen sind auf dem Server und dem Clienten gespeichert sodass ich nur ein paar Daten mitschicke wie zum Beispiel Die Startposition, den Winkel mit dem der Schuss abgefeuert wurde und die Zeit also undgefähr so wie du das beschrieben hast.

    Das einzige wodran ich also noch nicht gedacht hab was du noch beschrieben hast ist also das sycnhronisieren der Zeit auf den verschiedenen Clienten, wobei ich das noch nicht ganz verstanden habe. Wie soll ich berechnen um wieviel sich die Uhrzeit des Clienten von der des Servers unterscheidet? Ich kann natürlich die Zeit des Servers an den Clienten als erstes schicken und die Differenz ausrechnen aber ich weiß ja nicht wie lange das Schicken gedauert hat.

    Danke aufjedenfall nochmal!



  • Du gibst jedem Paket eine ID.
    Der Server merkt sich die ID sowie "seine" Zeit zu der er das letzte Paket von Client X erhalten hat.
    Wenn der Server dann das nächste Paket an Client X schickt, dann schickt er die ID des zuletzt von X erhaltenen Pakets mit, die vermerkte Zeit wann er es empfangen hat, und seine aktuelle Zeit.
    z.B.
    `LastReceivedPacketID = 123

    LastReceivedPacketTime = 40023

    ServerReplyTime = 40100`

    Der Client merkt sich bei jedem Paket das er abschickt die ID des Pakets sowie die Zeit zu der er es abgeschickt hat, sagen wir für die letzten 5-10 Pakete.
    `...

    ID = 120

    ClientSentTime = 12345000

    ID = 121

    ClientSentTime = 12345020

    ID = 122

    ClientSentTime = 12345055

    ID = 123

    ClientSentTime = 12345079

    ...`

    Wenn der Client jetzt das Paket vom Server empfängt, holt er sicher erstmal seine aktuelle Zeit. Sagen wir die ist 12345200 .
    Dann sucht er sich raus wann er das vom Server genannte Paket verschickt hat. Insgesamt hat der Client dann...

    Now = 12345200 (Aktuelle Client-Zeit)
    PacketID = 123 (ID des letzten Pakets das der Client an den Server geschickt hat)
    ClientSentTime = 12345079 (Client-Zeit zu der der Client es abgeschickt hat)

    LastReceivedPacketTime = 40023 (Server-Zeit zu der der Server es empfangen hat)
    ServerReplyTime = 40100 (Server-Zeit zu der der Server es beantwortet hat)

    Jetzt rechnet der Client einfach

    OverallDelay = Now - ClientSentTime = 12345200 - 12345079 = 121 (Gesamtdauer bis Paket beantwortet wurde)
    ServerWaitTime = ServerReplyTime - LastReceivedPacketTime = 40100 - 40023 = 77 (Verzögerung die nur durch Warten am Server entstanden ist)
    RoundTripTime = OverallDelay - ServerWaitTime = 121 - 77 = 44
    und
    OneWayTripTime = RoundTripTime / 2 = 44 / 2 = 22 (Delay in einer Richtung - sollt ein den meisten Fällen gut hinkommen)

    Die Verzögerung beträgt also 22ms.

    Bis hierher gibt es noch keine "Game-Clock", alle erwähnten Client Zeiten kommen von einer "nicht adjustierten" steady Clock die der Client verwendet, z.B. unter Windows einfach GetTickCount() oder timeGetTime().

    D.h. die aktuelle Server-Zeit, zu dem Zeitpunkt wo der Client das Paket empfangen hat ist ServerReplyTime + OneWayTripTime = 40100 + 22 = 40122
    Die aktuelle Client-Zeit zu der das Paket empfangen wurde war 12345200 , also ist der "Offset" 12345200 - 40122 = 12305078 .

    Angenommen die Pakete sind immer gleich lange unterwegs, und die Clocks von Server und Client laufen gleich schnell, dann sollte dieser Offset immer gleich bleiben. (Auf verschiedenen Clients ist er natürlich unterschiedlich, weil die Client Clocks ja zu unterschiedlichen Zeitpunkten gestartet wurden)

    Diesen Offset verwendet der Client dann um aus seiner Clock die Game-Clock zu berechnen - GameClock = ClientClock - Offset .

    Interessant wird es dann wenn man dem System beibringen möchte mit wechselnden Bedingungen und driftenden Clocks klarzukommen.
    Dazu wird der Client die Offset Werte beobachten müssen, und anhand irgendeines schlauen Algorithmus entscheiden wann es angebracht ist eine Korrektur vorzunehmen.



  • Hustbaer schrieb:

    ... Interessant wird es dann wenn man dem System beibringen möchte mit wechselnden Bedingungen und driftenden Clocks klarzukommen.
    Dazu wird der Client die Offset Werte beobachten müssen, und anhand irgendeines schlauen Algorithmus entscheiden wann es angebracht ist eine Korrektur vorzunehmen.

    Für diese Problemstellung kannst du dich z.B. am http://en.wikipedia.org/wiki/Precision_Time_Protocol orientieren.

    Greets
    Tobi



  • Danke, ich werd das die Tage dann so machen. Den TCP/UDP Server hab ich jetzt fertig gemacht.



  • Keine Ahnung, ob der Artikel nach Hustbärs Ausführungen jetzt überhaupt noch zusätzliches Wissen bringt, aber trotzdem: http://gafferongames.com/game-physics/networked-physics/


Anmelden zum Antworten