
Im Mai dieses Jahres nahm ich als Spieler an teil. Mir fiel auf, dass, wenn die Anzahl der Spieler eine bestimmte Zahl erreicht, einige von ihnen alle paar Minuten "abfallen". Glücklicherweise für euch (aber nicht für mich) war ich einer dieser Spieler, die sich jedes Mal, selbst bei einer stabilen Verbindung, abmeldeten. Ich sah das als persönliche Herausforderung an und begann nach den Ursachen des Problems zu suchen. Nach drei Wochen Debugging, Tests und Korrekturen wurde der Fehler schließlich behoben, aber diese Reise war alles andere als einfach.
Die Probleme in Mehrspieler-Spielen sind sehr schwer nachzuvollziehen. Sie treten üblicherweise unter sehr spezifischen Netzwerkbedingungen und in sehr speziellen Spielsituationen auf (in diesem Fall: mehr als 200 Spieler). Und selbst wenn es gelingt, das Problem zu reproduzieren, ist es nicht richtig debugbar, da das Einfügen von Kontrollpunkten das Spiel stoppt, Timer verwirrt und normalerweise dazu führt, dass die Verbindung aufgrund von Zeitüberschreitungen abbricht. Aber dank Hartnäckigkeit und eines wunderbaren Werkzeugs namens konnte ich herausfinden, was passiert.
Kurz gesagt: Aufgrund eines Fehlers und einer unvollständigen Implementierung der Verzögerungssimulation befand sich der Client manchmal in einer Situation, in der er in einem Takt ein Netzwerkpaket senden musste, das aus den Eingaben von Spielern für etwa 400 Spielelemente bestand (wir nennen dies "Megapaket"). Danach muss der Server nicht nur alle diese Eingaben korrekt empfangen, sondern sie auch an alle anderen Clients senden. Wenn man 200 Clients hat, wird das schnell zum Problem. Der Kanal zum Server wird schnell überlastet, was zu Paketverlust und einem Kaskadeneffekt von erneut angeforderten Paketen führt. Die Verzögerung der Eingabeverarbeitung führt dann dazu, dass noch mehr Clients Megapakete senden, wodurch die Lawine noch stärker wird. Den glücklichen Clients gelingt es, sich zu erholen, alle anderen "fallen ab".

Das Problem war ziemlich grundlegend, und ich habe zwei Wochen gebraucht, um es zu beheben. Es ist recht technisch, deshalb werde ich unten die saftigen technischen Einzelheiten erklären. Aber zunächst müssen Sie wissen, dass seit Version 0.17.54, die am 4. Juni veröffentlicht wurde, der Mehrspieler unter temporären Verbindungsproblemen stabiler ist und die Latenzverdeckung viel weniger fehlerhaft ist (weniger Ruckler und Teleportationen). Außerdem habe ich die Methode zur Verdeckung der Verzögerungen im Kampf geändert und hoffe, dass sie dadurch etwas flüssiger werden.
Mehrspieler-Megapaket – technische Einzelheiten
Um es vereinfacht zu erklären, funktioniert der Mehrspieler im Spiel wie folgt: Alle Clients simulieren den Spielzustand, indem sie nur die Eingaben der Spieler empfangen und senden (genannt "Input Actions" Input Actions). Die Hauptaufgabe des Servers ist die Übertragung Input Actions und die Kontrolle darüber, dass alle Clients die gleichen Aktionen in einem Takt ausführen. Mehr dazu können Sie in dem Beitrag lesen .
Da der Server Entscheidungen darüber treffen muss, welche Aktionen ausgeführt werden sollen, folgt die Aktion des Spielers ungefähr diesem Pfad: Aktion des Spielers → Spiel-Client → Netzwerk → Server → Netzwerk → Spiel-Client. Das bedeutet, dass jede Aktion des Spielers erst ausgeführt wird, nachdem sie den hin und zurück gehenden Weg durch das Netzwerk zurückgelegt hat. Daher würde das Spiel schrecklich ruckeln erscheinen, weshalb fast sofort nach dem Erscheinen des Mehrspielermodus ein Mechanismus zur Verdeckung der Latenzen eingeführt wurde. Die Latenzverdeckung simuliert die Eingaben des Spielers, ohne die Aktionen anderer Spieler und die Entscheidungen des Servers zu berücksichtigen.

In Factorio gibt es einen Spielzustand Game State – das vollständige Bild der Karte, des Spielers, der Entitäten und allem anderen. Es wird deterministisch in allen Clients basierend auf den vom Server empfangenen Aktionen simuliert. Der Spielzustand ist heilig, und wenn er jemals vom Server oder einem anderen Client abweicht, tritt eine Desynchronisierung ein.
Außerdem Game State Wir haben einen Latenzzustand Latency State. Er enthält eine kleine Untermenge des Hauptzustands. Latency State Er ist nicht heilig und stellt einfach dar, wie der Spielzustand in Zukunft basierend auf den Eingaben des Spielers aussehen wird. Input Actions.
Dafür speichern wir eine Kopie der erzeugten Input Actions in der Warteschlange der Verzögerungen.

Das heißt, am Ende des Prozesses sieht das Bild auf der Client-Seite etwa so aus:
- Wir wenden an Input Actions alle Spieler auf Game State so, wie diese Eingabeaktionen vom Server empfangen wurden.
- Wir entfernen aus der Warteschlange der Verzögerungen alle Input Actions, die laut den Angaben des Servers bereits auf Game State.
- Löschen Latency State angewendet wurden, und setzen es zurück, damit es genau so aussieht wie Game State.
- Wir wenden alle Aktionen aus der Warteschlange der Verzögerungen auf Latency State.
- Basierend auf den Daten Game State und Latency State rendern wir das Spiel für den Spieler.
All dies wiederholt sich in jedem Takt.
Zu kompliziert? Keine Sorge, das ist noch nicht alles. Um die Unzuverlässigkeit von Internetverbindungen auszugleichen, haben wir zwei Mechanismen entwickelt:
- Verpasste Takte: Wenn der Server entscheidet, dass Input Actions im Takt des Spiels ausgeführt werden, aber wenn er von einem Spieler keine Input Actions Informationen erhalten hat (zum Beispiel aufgrund einer erhöhten Verzögerung), wird er nicht warten, sondern diesem Client mitteilen: „Ich habe deine Input Actions, ich werde versuchen, sie im nächsten Takt hinzuzufügen.“ Das wurde so gemacht, damit aufgrund von Verbindungsproblemen (oder Problemen mit dem Computer) eines Spielers die Aktualisierung der Karte bei allen anderen nicht verzögert wird. Es ist anzumerken, dass Input Actions nicht ignoriert, sondern einfach verschoben werden.
- Die verzögerte gesamte Hin- und Rückweg: Der Server versucht zu schätzen, wie hoch die Verzögerung der Datenübertragung hin und zurück zwischen Client und Server für jeden Client ist. Alle 5 Sekunden bespricht er gegebenenfalls mit dem Client eine neue Verzögerung (abhängig davon, wie sich die Verbindung in der Vergangenheit verhalten hat) und passt die Hin- und Rückübertragungsverzögerung entsprechend an.
Diese Mechanismen sind an sich recht einfach, aber wenn sie zusammen verwendet werden (was häufig bei Verbindungsproblemen geschieht), wird die Logik des Codes schwer handhabbar und es gibt eine Menge Grenzfälle. Zudem müssen der Server und die Warteschlange der Verzögerungen, wenn diese Mechanismen ins Spiel kommen, richtig ein spezielles Input Action mit dem Namen StopMovementInTheNextTick. Dadurch wird sichergestellt, dass der Charakter bei Verbindungsproblemen nicht von selbst wegläuft (z. B. unter einen Zug).
Jetzt muss ich Ihnen erklären, wie die Auswahl von Entitäten funktioniert. Einer der übergebenen Typen Input Action — dies ist die Änderung des Auswahlstatus der Entität. Es informiert alle darüber, auf welche Entität der Spieler mit der Maus zeigt. Wie man verstehen kann, ist dies eine der häufigsten Eingabemaßnahmen, die von den Clients gesendet werden, daher haben wir es optimiert, um Bandbreite zu sparen, sodass es so wenig Platz wie möglich einnimmt. Dies wird so umgesetzt: Bei der Auswahl jeder Entität speichert das Spiel anstelle der absoluten, hochpräzisen Kartencoordinaten eine geringpräzise relative Verschiebung von der vorherigen Auswahl. Dies funktioniert gut, da das Markieren mit der Maus normalerweise sehr nah an der vorherigen Auswahl erfolgt. Daraus ergeben sich zwei wichtige Anforderungen: Input Actions dass sie niemals übergangen werden dürfen und dass sie in der richtigen Reihenfolge ausgeführt werden müssen. Diese Anforderungen werden für Game State. Aber da die Aufgabe Latency state darum besteht, "gut genug" für den Spieler auszusehen, werden sie im Latenzzustand nicht erfüllt. Latency State berücksichtigt , die mit Taktüberspringen und Änderungen der Hin- und Rücklaufverzögerungen verbunden sind.
Sie können bereits erraten, worauf es hinausläuft. Schließlich beginnen wir, die Ursachen des Megapakets zu erkennen. Der Grund für das Problem liegt darin, dass bei der Entscheidung, ob die Aktion der Auswahländerung übertragen werden soll, die Logik der Auswahl der Entitäten auf Latency Stateberuht, und dieser Zustand enthält nicht immer die richtigen Informationen. Daher wird das Megapaket ungefähr so generiert:
- Der Spieler hat Verbindungsprobleme.
- Es kommen Taktüberspring- und Verzögerungsregelmechanismen ins Spiel.
- Die Warteschlange des Latenzzustands berücksichtigt diese Mechanismen nicht. Das führt dazu, dass bestimmte Aktionen vorzeitig gelöscht oder in der falschen Reihenfolge ausgeführt werden, was zu einem fehlerhaften Latency State.
- Der Spieler hat kein Verbindungsproblem mehr und simuliert, um mit dem Server aufzuholen, bis zu 400 Takte.
- In jedem Takt wird eine neue Aktion zur Änderung der Auswahl der Entität generiert und zur Übertragung an den Server vorbereitet.
- Der Client sendet ein Megapaket von über 400 Änderungen der Entitätenauswahl an den Server (und auch andere Aktionen wie den Zustand des Schießens, Gehens usw. waren von diesem Problem betroffen).
- Der Server erhält 400 Eingabeverfahren. Da ihm nicht erlaubt ist, auch nur einen einzigen Eingabeschritt zu überspringen, befiehlt er allen Clients, diese Schritte auszuführen und sie über das Netzwerk zu senden.
Die Ironie besteht darin, dass der Mechanismus, der zur Einsparung der Bandbreite des Kanals gedacht war, letztendlich riesige Netzwerkpakete erzeugte.
Wir haben dieses Problem gelöst, indem wir alle Grenzfälle der Aktualisierung und Unterstützung der Verzögerungsschlange behoben haben. Obwohl es ziemlich viel Zeit in Anspruch nahm, hat es sich letztendlich gelohnt, alles richtig umzusetzen, anstatt auf schnelle Hacks zu vertrauen.
Quelle: habr.com
