Wie gehe ich mit Netcode um?

10

Ich bin daran interessiert, die verschiedenen Möglichkeiten zu bewerten, wie sich der Netcode in eine Spiel-Engine "einhaken" kann. Ich entwerfe gerade ein Multiplayer-Spiel und habe bisher festgestellt, dass ich (zumindest) einen separaten Thread haben muss, um die Netzwerk-Sockets zu verwalten, der sich vom Rest der Engine unterscheidet, die die Grafikschleife und das Scripting verwaltet.

Ich hatte eine Möglichkeit, ein vernetztes Spiel vollständig mit einem Thread zu erstellen, nämlich das Netzwerk nach dem Rendern jedes Frames mit nicht blockierenden Sockets zu überprüfen. Dies ist jedoch eindeutig nicht optimal, da die Zeit, die zum Rendern eines Frames benötigt wird, zur Netzwerkverzögerung hinzugefügt wird: Nachrichten, die über das Netzwerk eingehen, müssen warten, bis das aktuelle Frame-Rendering (und die Spielelogik) abgeschlossen sind. Aber zumindest auf diese Weise würde das Gameplay mehr oder weniger reibungslos bleiben.

Ein separater Thread für das Netzwerk ermöglicht es dem Spiel, vollständig auf das Netzwerk zu reagieren. Beispielsweise kann es ein ACK-Paket sofort nach Erhalt einer Statusaktualisierung vom Server zurücksenden. Aber ich bin ein wenig verwirrt darüber, wie ich am besten zwischen dem Spielcode und dem Netzwerkcode kommunizieren kann. Der Netzwerkthread schiebt das empfangene Paket in eine Warteschlange, und der Spielthread liest zum richtigen Zeitpunkt während seiner Schleife aus der Warteschlange, sodass wir diese Verzögerung von bis zu einem Frame nicht beseitigt haben.

Es scheint auch so, als ob ich möchte, dass der Thread, der das Senden von Paketen behandelt, von dem Thread getrennt ist, der nach Paketen sucht, die über die Pipe kommen, da er keine senden kann, während er sich in der Mitte befindet Überprüfen, ob eingehende Nachrichten vorhanden sind. Ich denke über die Funktionalität von selectoder ähnlichem nach.

Ich denke meine Frage ist, wie man das Spiel am besten für die beste Netzwerkreaktivität gestaltet. Es ist klar, dass der Client Benutzereingaben so schnell wie möglich an den Server senden sollte, damit ich den Net-Send-Code unmittelbar nach der Ereignisverarbeitungsschleife innerhalb der Spielschleife erhalten kann. Ist das sinnvoll?

Steven Lu
quelle

Antworten:

13

Reaktionsfähigkeit ignorieren. In einem LAN ist Ping unbedeutend. Im Internet ist eine Hin- und Rückfahrt von 60 bis 100 ms ein Segen. Bete zu den Lag-Göttern, dass du keine Spitzen von> 3K bekommst. Ihre Software müsste mit einer sehr geringen Anzahl von Updates / Sek. Ausgeführt werden, damit dies ein Problem darstellt. Wenn Sie 25 Updates / Sek. Aufnehmen, haben Sie zwischen dem Empfang eines Pakets und der Bearbeitung eine maximale Zeit von 40 ms. Und das ist der Single-Threaded-Fall ...

Entwerfen Sie Ihr System auf Flexibilität und Korrektheit. Hier ist meine Idee, wie man ein Netzwerk-Subsystem in den Spielcode einbindet: Messaging. Die Lösung für viele Probleme kann "Messaging" sein. Ich denke, Messaging hat Krebs bei Laborratten einmal geheilt. Durch Messaging erspare ich mindestens 200 US-Dollar bei meiner Kfz-Versicherung. Im Ernst, Messaging ist wahrscheinlich der beste Weg, um ein Subsystem an den Spielcode anzuhängen, während zwei unabhängige Subsysteme beibehalten werden.

Verwenden Sie Messaging für die Kommunikation zwischen dem Netzwerksubsystem und der Game Engine sowie für zwei beliebige Subsysteme. Inter-Subsystem-Messaging kann ein einfacher Datenblock sein, der vom Zeiger mithilfe von std :: list übergeben wird.

Stellen Sie einfach eine Warteschlange für ausgehende Nachrichten und einen Verweis auf die Spiel-Engine im Netzwerk-Subsystem bereit. Das Spiel kann Nachrichten, die es senden möchte, in die ausgehende Warteschlange werfen und automatisch senden lassen, oder wenn eine Funktion wie "flushMessages ()" aufgerufen wird. Wenn die Spiel-Engine eine große, gemeinsam genutzte Nachrichtenwarteschlange hatte, können alle Subsysteme, die zum Senden von Nachrichten benötigt werden (Logik, KI, Physik, Netzwerk usw.), alle Nachrichten in die Warteschlange ausgeben, in der die Hauptspielschleife dann alle Nachrichten lesen kann und dann entsprechend handeln.

Ich würde sagen, dass das Ausführen der Sockets auf einem anderen Thread in Ordnung ist, obwohl dies nicht erforderlich ist. Das einzige Problem bei diesem Design ist, dass es im Allgemeinen asynchron ist (Sie wissen nicht genau, wann die Pakete gesendet werden) und dass es schwierig sein kann, Fehler zu beheben und zeitliche Probleme zufällig erscheinen / verschwinden zu lassen. Wenn es richtig gemacht wird, sollte keines von beiden ein Problem sein.

Von einer höheren Ebene aus würde ich sagen, dass die Vernetzung von der Spiel-Engine selbst getrennt ist. Die Spiel-Engine kümmert sich nicht um Sockets oder Puffer, sondern um Ereignisse. Ereignisse sind Dinge wie "Spieler X hat einen Schuss abgegeben" "Eine Explosion bei Spiel T ist passiert". Diese können von der Spiel-Engine direkt interpretiert werden. Woher sie generiert werden (ein Skript, die Aktion eines Kunden, ein KI-Spieler usw.), spielt keine Rolle.

Wenn Sie Ihr Netzwerksubsystem als Mittel zum Senden / Empfangen von Ereignissen behandeln, erhalten Sie eine Reihe von Vorteilen gegenüber dem Aufruf von recv () an einem Socket.

Sie können die Bandbreite beispielsweise optimieren, indem Sie 50 kleine Nachrichten (1-32 Byte lang) aufnehmen und vom Netzwerksubsystem in ein großes Paket packen und senden lassen. Vielleicht könnte es sie vor dem Senden komprimieren, wenn das eine große Sache wäre. Am anderen Ende könnte der Code das große Paket wieder in 50 diskrete Ereignisse dekomprimieren / entpacken, damit die Spiel-Engine es lesen kann. Dies kann alles transparent geschehen.

Andere coole Dinge sind der Einzelspieler-Spielemodus, bei dem Ihr Netzwerkcode wiederverwendet wird, indem ein reiner Client + ein reiner Server auf demselben Computer ausgeführt werden, der über Messaging im gemeinsam genutzten Speicherplatz kommuniziert. Wenn Ihr Einzelspieler-Spiel ordnungsgemäß funktioniert, funktioniert auch ein Remote-Client (dh ein echter Multiplayer-Modus). Darüber hinaus müssen Sie im Voraus überlegen, welche Daten vom Client benötigt werden, da Ihr Einzelspieler-Spiel entweder richtig oder völlig falsch aussehen würde. Mix and Match, betreibe einen Server UND sei ein Client in einem Multiplayer-Spiel - alles funktioniert genauso einfach.

PatrickB
quelle
Sie haben erwähnt, dass Sie eine einfache std :: -Liste oder eine solche verwenden, um Nachrichten weiterzuleiten. Dies mag ein Thema für StackOverflow sein, aber stimmt es, dass alle Threads denselben Adressraum verwenden. Solange ich nicht verhindere, dass mehrere Threads gleichzeitig mit dem zu meiner Warteschlange gehörenden Speicher verschraubt werden, sollte es mir gut gehen. Ich kann einfach wie gewohnt Daten für die Warteschlange auf dem Heap zuweisen und nur einige Mutexe darin verwenden.
Steven Lu
Ja das ist richtig. Ein Mutex, der alle Aufrufe von std :: list schützt.
PatrickB
Danke für die Antwort! Ich habe bisher große Fortschritte bei meinen Threading-Routinen gemacht. Es ist so ein tolles Gefühl, meine eigene Spiel-Engine zu haben!
Steven Lu
4
Dieses Gefühl wird nachlassen. Die großen Messingteile, die Sie gewinnen, bleiben jedoch bei Ihnen.
ChrisE
@StevenLu Etwas [extrem] spät, aber ich möchte darauf hinweisen, dass es extrem schwierig sein kann, zu verhindern, dass Threads gleichzeitig mit dem Speicher verschraubt werden , je nachdem, wie Sie es versuchen und wie effizient Sie sein möchten. Wenn Sie dies heute tun, würde ich Sie auf eine der vielen hervorragenden Open-Source-Implementierungen für gleichzeitige Warteschlangen verweisen, sodass Sie ein kompliziertes Rad nicht neu erfinden müssen.
Fund Monica Klage
4

Ich muss (zumindest) einen separaten Thread haben, um die Netzwerk-Sockets zu verwalten

Nein, tust du nicht.

Ich hatte eine Möglichkeit, ein vernetztes Spiel vollständig mit einem Thread zu erstellen, nämlich das Netzwerk nach dem Rendern jedes Frames mit nicht blockierenden Sockets zu überprüfen. Dies ist jedoch eindeutig nicht optimal, da die Zeit, die zum Rendern eines Frames benötigt wird, zur Netzwerkverzögerung hinzugefügt wird:

Ist nicht unbedingt wichtig. Wann wird Ihre Logik aktualisiert? Es macht wenig Sinn, Daten aus dem Netzwerk zu holen, wenn Sie noch nichts damit anfangen können. Ebenso macht es wenig Sinn zu antworten, wenn Sie noch nichts zu sagen haben.

Beispielsweise kann es ein ACK-Paket sofort nach Erhalt einer Statusaktualisierung vom Server zurücksenden.

Wenn Ihr Spiel so schnell ist, dass das Warten auf das Rendern des nächsten Frames eine erhebliche Verzögerung darstellt, werden genügend Daten gesendet, sodass Sie keine separaten ACK-Pakete senden müssen. Fügen Sie einfach ACK-Werte in Ihre normalen Daten ein Nutzlasten, wenn Sie sie überhaupt brauchen.

Für die meisten vernetzten Spiele ist es durchaus möglich, eine Spielschleife wie diese zu haben:

while 1:
    read_network_messages()
    read_local_input()
    update_world()
    send_network_updates()
    render_world()

Sie können Aktualisierungen vom Rendern entkoppeln, was sehr zu empfehlen ist, aber alles andere kann so einfach bleiben, es sei denn, Sie haben einen bestimmten Bedarf. Was für ein Spiel machst du genau?

Kylotan
quelle
7
Das Entkoppeln der Aktualisierung vom Rendern wird nicht nur dringend empfohlen, sondern ist auch erforderlich, wenn Sie eine Non-Shit-Engine wünschen.
AttackingHobo
Die meisten Leute stellen jedoch keine Motoren her, und wahrscheinlich entkoppeln die meisten Spiele die beiden immer noch nicht. In den meisten Fällen funktioniert es akzeptabel, einen Wert für die verstrichene Zeit an die Aktualisierungsfunktion zu übergeben.
Kylotan
2

Dies ist jedoch eindeutig nicht optimal, da die Zeit, die zum Rendern eines Frames benötigt wird, zur Netzwerkverzögerung hinzugefügt wird: Nachrichten, die über das Netzwerk eingehen, müssen warten, bis das aktuelle Frame-Rendering (und die Spielelogik) abgeschlossen sind.

Das stimmt überhaupt nicht. Die Nachricht geht über das Netzwerk, während der Empfänger den aktuellen Frame rendert. Die Netzwerkverzögerung wird auf der Clientseite auf eine ganze Anzahl von Frames begrenzt. Ja, aber wenn der Kunde so wenig FPS hat, dass dies eine große Sache ist, dann haben sie größere Probleme.

DeadMG
quelle
0

Die Netzwerkkommunikation sollte gestapelt werden. Sie sollten sich um ein einzelnes Paket bemühen, das bei jedem Spiel-Tick gesendet wird (was häufig der Fall ist, wenn ein Frame gerendert wird, aber wirklich unabhängig sein sollte).

Ihre Spieleinheiten kommunizieren mit dem Netzwerk-Subsystem (NSS). Das NSS stapelt Nachrichten, ACKs usw. und sendet einige (hoffentlich eines) UDP-Pakete mit optimaler Größe (normalerweise ~ 1500 Byte). Das NSS emuliert Pakete, Kanäle, Prioritäten, erneutes Senden usw., während nur einzelne UDP-Pakete gesendet werden.

Lesen Sie das Gaffer im Tutorial der Spiele durch oder verwenden Sie einfach ENet, das viele von Glenn Fiedlers Ideen umsetzt.

Oder Sie können einfach TCP verwenden, wenn Ihr Spiel keine zuckenden Reaktionen benötigt. Dann verschwinden alle Stapel-, Sende- und ACK-Probleme. Sie möchten jedoch weiterhin, dass ein NSS Bandbreite und Kanäle verwaltet.

deft_code
quelle
0

"Reaktionsfähigkeit nicht völlig ignorieren". Es gibt wenig zu gewinnen, wenn bereits verzögerten Paketen eine weitere Latenz von 40 ms hinzugefügt wird. Wenn Sie ein paar Frames (mit 60 fps) hinzufügen, verzögern Sie die Verarbeitung einer Positionsaktualisierung um weitere Frames. Es ist besser, Pakete schnell anzunehmen und schnell zu verarbeiten, damit Sie die Genauigkeit der Simulation verbessern.

Ich hatte großen Erfolg bei der Optimierung der Bandbreite, indem ich über die Mindeststatusinformationen nachdachte, die erforderlich sind, um das darzustellen, was auf dem Bildschirm angezeigt wird. Schauen Sie sich dann jedes Datenbit an und wählen Sie ein Modell dafür aus. Positionsinformationen können als Deltawerte über die Zeit ausgedrückt werden. Sie können dafür entweder Ihre eigenen statistischen Modelle verwenden und ewig damit verbringen, sie zu debuggen, oder Sie können eine Bibliothek verwenden, um Ihnen zu helfen. Ich bevorzuge die Verwendung des Gleitkommamodells DataBlock_Predict_Float dieser Bibliothek. Dadurch ist es wirklich einfach, die für das Diagramm der Spielszene verwendete Bandbreite zu optimieren.

Justin
quelle