Wie wir den Netzwerkcode des mobilen PvP-Shooters geschrieben haben: Player-Synchronisation auf dem Client

In einem der vorherigen Artikel haben wir Technologien besprochen, die in unserem neuen Projekt verwendet werden - einem schnellen Shooter für mobile Geräte. Jetzt möchte ich mitteilen, wie der Client-Teil des Netzwerkcodes des zukünftigen Spiels angeordnet ist, auf welche Schwierigkeiten wir gestoßen sind und wie sie gelöst werden können.




Im Allgemeinen haben sich die Ansätze zur Erstellung schneller Multiplayer-Spiele in den letzten 20 Jahren nicht wesentlich geändert. In der Netzwerkcodearchitektur können verschiedene Methoden unterschieden werden:

  1. Fehleinschätzung des Zustands der Welt auf dem Server und Anzeige der Ergebnisse auf dem Client ohne Vorhersage für den lokalen Spieler und mit der Möglichkeit, Spielereingaben (Eingaben) zu verlieren. Dieser Ansatz wird übrigens bei unserem anderen Projekt in der Entwicklung verwendet - Sie können hier darüber lesen.
  2. Lockstep
  3. Synchronisation des Weltzustands ohne deterministische Logik mit Vorhersage für einen lokalen Spieler.
  4. Eingangssynchronisation mit vollständig deterministischer Logik und Vorhersage für einen lokalen Spieler.

Die Besonderheit liegt in der Tatsache, dass bei Schützen die Reaktionsfähigkeit der Kontrolle am wichtigsten ist - der Spieler drückt einen Knopf (oder bewegt den Joystick) und möchte sofort das Ergebnis seiner Aktion sehen. Erstens, weil sich der Zustand der Welt in solchen Spielen sehr schnell ändert und es notwendig ist, sofort auf die Situation zu reagieren.

Infolgedessen waren Ansätze ohne einen Mechanismus zur Vorhersage der Aktionen eines lokalen Spielers (Vorhersage) für das Projekt nicht geeignet, und wir entschieden uns für eine Methode zur Synchronisierung des Weltzustands ohne deterministische Logik.

Vorteil des Ansatzes: Weniger Komplexität bei der Implementierung im Vergleich zur Synchronisationsmethode beim Austausch von Eingaben.
Minus: eine Zunahme des Datenverkehrs, wenn der gesamte Zustand der Welt an den Kunden gesendet wird. Wir mussten verschiedene Techniken zur Verkehrsoptimierung anwenden, damit das Spiel in einem Mobilfunknetz stabil funktioniert.

Im Zentrum der Gameplay-Architektur steht ECS, über das wir bereits gesprochen haben . Mit dieser Architektur können Sie Daten über die Spielwelt bequem speichern, serialisieren, kopieren und über das Netzwerk übertragen. Und auch, um den gleichen Code sowohl auf dem Client als auch auf dem Server auszuführen.

Die Simulation der Spielwelt erfolgt mit einer festen Frequenz von 30 Ticks pro Sekunde. Auf diese Weise können Sie die Verzögerung der Spielereingabe verringern und fast keine Interpolation verwenden, um den Zustand der Welt visuell anzuzeigen. Bei der Entwicklung eines solchen Systems sollte jedoch ein wesentlicher Nachteil berücksichtigt werden: Damit das Vorhersagesystem des lokalen Spielers ordnungsgemäß funktioniert, muss der Client die Welt mit derselben Häufigkeit wie der Server simulieren. Und wir haben viel Zeit darauf verwendet, die Simulation ausreichend für die Zielgeräte zu optimieren.

Vorhersagemechanismus für lokale Spieleraktionen (Vorhersage)


Der Client-Vorhersagemechanismus wird auf der Basis von ECS implementiert, da dieselben Systeme sowohl auf dem Client als auch auf dem Server ausgeführt werden. Es werden jedoch nicht alle Systeme auf dem Client ausgeführt, sondern nur diejenigen, die für den lokalen Spieler verantwortlich sind und keine relevanten Daten über andere Spieler benötigen.

Beispiel für Listen von Systemen, die auf dem Client und Server ausgeführt werden:



Derzeit laufen auf dem Client etwa 30 Systeme, die die Vorhersage des Players liefern, und auf dem Server etwa 80 Systeme. Wir sagen jedoch keine Dinge voraus, wie Schaden zuzufügen, Fähigkeiten einzusetzen oder Verbündete zu heilen. Bei dieser Mechanik gibt es zwei Probleme:

  1. Der Client weiß nichts über das Betreten anderer Spieler und die Vorhersage von Schäden oder Heilung weicht fast immer von den Daten auf dem Server ab.
  2. Das lokale Erstellen neuer Entitäten (Schüsse, Muscheln, einzigartige Fähigkeiten), die von einem Spieler generiert werden, birgt das Problem der Übereinstimmung mit auf dem Server erstellten Entitäten.

Für einen solchen Mechaniker verbirgt sich die Verzögerung auf andere Weise vor dem Spieler.

Beispiel: Wir ziehen den Effekt des Treffens sofort aus dem Schuss und aktualisieren das Leben des Feindes erst, nachdem wir vom Server eine Bestätigung des Treffers erhalten haben.

Das allgemeine Schema des Netzwerkcodes im Projekt




Der Client und der Server synchronisieren die Zeit anhand von Tick-Nummern. Aufgrund der Tatsache, dass die Datenübertragung über das Netzwerk einige Zeit in Anspruch nimmt, ist der Client dem Server immer um die Hälfte RTT + der Größe des Eingabepuffers auf dem Server voraus. Das obige Diagramm zeigt, dass der Client eine Eingabe für Tick 20 (a) sendet. Gleichzeitig wird Häkchen 15 (b) auf dem Server verarbeitet. Wenn die Eingabe des Clients den Server erreicht, wird das Häkchen 20 auf dem Server verarbeitet.

Der gesamte Prozess besteht aus den folgenden Schritten: Der Client sendet die Eingabe des Players an den Server (a) → Diese Eingabe wird auf dem Server verarbeitet, nachdem HRTT + Eingabepuffergröße (b) → der Server den resultierenden Weltstatus an den / die Client (s) gesendet hat → Der Client wendet den bestätigten Weltstatus mit an Serverzeit RTT + Eingabepuffergröße + Spielstatus-Interpolationspuffergröße (d).

Nachdem der Client vom Server (d) einen neuen bestätigten Status der Welt erhalten hat, muss er den Abstimmungsprozess abschließen. Tatsache ist, dass der Client eine Weltvorhersage nur auf der Grundlage der Eingaben des lokalen Spielers durchführt. Die Eingaben anderer Spieler sind ihm nicht bekannt. Bei der Berechnung des Weltzustands auf dem Server befindet sich der Player möglicherweise in einem anderen Zustand als vom Client vorhergesagt. Dies kann passieren, wenn ein Spieler betäubt oder getötet wird.

Das Genehmigungsverfahren besteht aus zwei Teilen:

  1. Vergleiche des vorhergesagten Zustands der Welt für vom Server empfangenes Häkchen N. An dem Vergleich sind nur die Daten beteiligt, die sich auf den lokalen Spieler beziehen. Die restlichen Daten der Welt werden immer aus dem Serverstatus entnommen und nehmen nicht an der Koordination teil.
  2. Während des Vergleichs können zwei Fälle auftreten:

- Wenn der vorhergesagte Zustand der Welt mit dem vom Server bestätigten übereinstimmt, simuliert der Client unter Verwendung der vorhergesagten Daten für den lokalen Spieler und der neuen Daten für den Rest der Welt die Welt weiterhin im normalen Modus.
- Wenn der vorhergesagte Status nicht übereinstimmt, verwendet der Client den gesamten Serverstatus der Welt und den Verlauf der Eingaben vom Client und gibt den neuen vorhergesagten Status der Welt des Spielers wieder.

Im Code sieht es ungefähr so ​​aus:
GameState Reconcile(int currentTick, ServerGameStateData serverStateData, GameState currentState, uint playerID) { var serverState = serverStateData.GameState; var serverTick = serverState.Time; var predictedState = _localStateHistory.Get(serverTick); //if predicted state matches server last state use server predicted state with predicted player if (_gameStateComparer.IsSame(predictedState, serverState, playerID)) { _tempState.Copy(serverState); _gameStateCopier.CopyPlayerEntities(currentState, _tempState, playerID); return _localStateHistory.Put(_tempState); // replace predicted state with correct server state } //if predicted state doesn't match server state, reapply local inputs to server state var last = _localStateHistory.Put(serverState); // replace wrong predicted state with correct server state for (var i = serverTick; i < currentTick; i++) { last = _prediction.Predict(last); // resimulate all wrong states } return last; } 


Der Vergleich zweier Weltzustände erfolgt nur für diejenigen Daten, die sich auf den lokalen Spieler beziehen und am Vorhersagesystem teilnehmen. Die Daten werden anhand der Spieler-ID abgetastet.

Vergleichsmethode:
 public bool IsSame(GameState s1, GameState s2, uint avatarId) { if (s1 == null && s2 != null || s1 != null && s2 == null) return false; if (s1 == null && s2 == null) return false; var entity1 = s1.WorldState[avatarId]; var entity2 = s2.WorldState[avatarId]; if (entity1 == null && entity2 == null) return false; if (entity1 == null || entity2 == null) return false; if (s1.Time != s2.Time) return false; if (s1.WorldState.Transform[avatarId] != s2.WorldState.Transform[avatarId]) return false; foreach (var s1Weapon in s1.WorldState.Weapon) { if (s1Weapon.Value.Owner.Id != avatarId) continue; var s2Weapon = s2.WorldState.Weapon[s1Weapon.Key]; if (s1Weapon.Value != s2Weapon) return false; var s1Ammo = s1.WorldState.WeaponAmmo[s1Weapon.Key]; var s2Ammo = s2.WorldState.WeaponAmmo[s1Weapon.Key]; if (s1Ammo != s2Ammo) return false; var s1Reload = s1.WorldState.WeaponReloading[s1Weapon.Key]; var s2Reload = s2.WorldState.WeaponReloading[s1Weapon.Key]; if (s1Reload != s2Reload) return false; } if (entity1.Aiming != entity2.Aiming) return false; if (entity1.ChangeWeapon != entity2.ChangeWeapon) return false; return true; } 


Vergleichsoperatoren für bestimmte Komponenten werden zusammen mit der gesamten EC-Struktur generiert, die speziell von einem Codegenerator geschrieben wurde. Zum Beispiel gebe ich den generierten Code des Transformationskomponenten-Vergleichsoperators an:

Code
 public static bool operator ==(Transform a, Transform b) { if ((object)a == null && (object)b == null) return true; if ((object)a == null && (object)b != null) return false; if ((object)a != null && (object)b == null) return false; if (Math.Abs(a.Angle - b.Angle) > 0.01f) return false; if (Math.Abs(a.Position.x - b.Position.x) > 0.01f || Math.Abs(a.Position.y - b.Position.y) > 0.01f) return false; return true; } 


Es ist zu beachten, dass unsere Float-Werte mit einem ziemlich hohen Fehler verglichen werden. Dies geschieht, um die Desynchronisierung zwischen Client und Server zu verringern. Für den Spieler ist ein solcher Fehler unsichtbar, dies spart jedoch erheblich die Rechenressourcen des Systems.

Die Komplexität des Koordinierungsmechanismus besteht darin, dass im Falle einer Fehlersynchronisation der Client- und Serverzustände (falsche Vorhersage) alle vorhergesagten Clientzustände, für die keine Bestätigung vom Server vorliegt, bis zum aktuellen Tick in einem Frame wiederholt simuliert werden müssen. Je nach Ping des Spielers können dies 5 bis 20 Simulations-Ticks sein. Wir mussten den Simulationscode deutlich optimieren, um in den Zeitrahmen zu passen: 30 fps.

Um den Genehmigungsprozess abzuschließen, müssen zwei Arten von Daten auf dem Client gespeichert werden:

  1. Eine Geschichte vorhergesagter Spielerzustände.
  2. Und die Geschichte der Eingabe.

Für diese Zwecke verwenden wir einen Ringpuffer. Die Puffergröße beträgt 32 Ticks. Dies ergibt bei einer Frequenz von 30 Hz etwa 1 Sekunde Echtzeit. Der Client kann sicher weiter am Vorhersagemechanismus arbeiten, ohne neue Daten vom Server zu erhalten, bis dieser Puffer gefüllt ist. Wenn der Unterschied zwischen der Zeit des Clients und des Servers mehr als eine Sekunde beträgt, muss der Client die Verbindung trennen, um erneut eine Verbindung herzustellen. Wir haben eine solche Puffergröße aufgrund der Kosten des Koordinierungsprozesses im Falle einer Diskrepanz zwischen den Staaten der Welt. Wenn der Unterschied zwischen dem Client und dem Server jedoch mehr als eine Sekunde beträgt, ist es billiger, eine vollständige Wiederverbindung mit dem Server durchzuführen.

Reduzierung der Verzögerungszeit


Das obige Diagramm zeigt, dass das Datenübertragungsschema im Spiel zwei Puffer enthält:

  • Eingabepuffer auf dem Server;
  • ein Puffer von Weltzuständen auf dem Client.

Der Zweck dieser Puffer ist der gleiche - Netzwerksprünge (Jitter) zu kompensieren. Tatsache ist, dass die Paketübertragung über das Netzwerk ungleichmäßig ist. Und da die Netzwerk-Engine mit einer festen Frequenz von 30 Hz arbeitet, müssen der Engine Daten mit derselben Frequenz zugeführt werden. Wir haben nicht die Möglichkeit, einige ms zu "warten", bis das nächste Paket den Empfänger erreicht. Wir verwenden Puffer für Eingabedaten und Weltzustände, um einen Zeitrahmen für die Jitterkompensation zu haben. Wir verwenden den Gamestate-Puffer auch zur Interpolation, wenn eines der Pakete verloren geht.

Zu Beginn des Spiels beginnt der Client erst dann mit der Synchronisierung mit dem Server, wenn er mehrere Weltzustände vom Server empfangen hat und der Gamestate-Puffer voll ist. Normalerweise beträgt die Größe dieses Puffers 3 Ticks (100 ms).

Wenn der Client mit dem Server synchronisiert wird, wird er gleichzeitig vor der Serverzeit um den Wert des Eingabepuffers auf dem Server "ausgeführt". Das heißt, Der Client selbst steuert, wie weit er dem Server voraus ist. Die Startgröße des Eingabepuffers beträgt ebenfalls 3 Ticks (100 ms).

Zunächst haben wir die Größe dieser Puffer als Konstanten implementiert. Das heißt, Unabhängig davon, ob der Jitter tatsächlich im Netzwerk vorhanden war oder nicht, gab es eine feste Verzögerung von 200 ms (Eingabepuffergröße + Spielstatuspuffergröße) für die Aktualisierung der Daten. Wenn wir den durchschnittlichen geschätzten Ping auf Mobilgeräten um 200 ms hinzufügen, betrug die tatsächliche Verzögerung zwischen der Verwendung der Eingabe auf dem Client und der Bestätigung der Anwendung vom Server 400 ms!

Das passte nicht zu uns.

Tatsache ist, dass einige Systeme nur auf dem Server ausgeführt werden - beispielsweise die Berechnung der HP des Players. Mit dieser Verzögerung schießt der Spieler und sieht erst nach 400 ms, wie er den Gegner tötet. Wenn dies in Bewegung geschah, gelang es dem Spieler normalerweise, hinter die Mauer oder in Deckung zu rennen und dort bereits zu sterben. Spieletests innerhalb des Teams haben gezeigt, dass eine solche Verzögerung das gesamte Gameplay vollständig beeinträchtigt.

Die Lösung für dieses Problem war die Implementierung dynamischer Größen von Eingabepuffern und Spielzuständen:
  • Bei einem Gamestate-Puffer kennt der Client immer den aktuellen Pufferinhalt. Zum Zeitpunkt der Berechnung des nächsten Ticks prüft der Client, wie viele Zustände sich bereits im Puffer befinden.
  • für den Eingabepuffer - Der Server hat zusätzlich zum Spielstatus begonnen, den Wert der aktuellen Füllung des Eingabepuffers für einen bestimmten Client an den Client zu senden. Der Kunde analysiert wiederum diese beiden Werte.

Der Algorithmus zur Größenänderung des Gamestate-Puffers lautet ungefähr wie folgt:

  1. Der Client berücksichtigt den Durchschnittswert der Puffergröße über einen Zeitraum und eine Varianz.
  2. Wenn die Varianz innerhalb normaler Grenzen liegt (d. H. Für einen bestimmten Zeitraum gab es keine großen Sprünge beim Füllen und Lesen aus dem Puffer), überprüft der Client den Wert der durchschnittlichen Puffergröße für diesen Zeitraum.
  3. Wenn die durchschnittliche Pufferfüllung größer als die obere Randbedingung war (dh der Puffer würde mehr als erforderlich gefüllt), „reduziert“ der Client die Puffergröße durch Ausführen eines zusätzlichen Simulationsticks.
  4. Wenn die durchschnittliche Pufferfüllung geringer war als die untere Randbedingung (dh der Puffer hatte keine Zeit zum Füllen, bevor der Client mit dem Lesen begann), erhöht der Client in diesem Fall die Puffergröße, indem er einen Tick der Simulation überspringt.
  5. In dem Fall, in dem die Varianz über dem Normalwert lag, können wir uns nicht auf diese Daten verlassen, weil Die Netzwerkstöße für einen bestimmten Zeitraum waren zu groß. Anschließend verwirft der Client alle aktuellen Daten und beginnt erneut mit der Erfassung von Statistiken.

Kompensation der Serververzögerung


Aufgrund der Tatsache, dass der Client mit einer Verzögerung (Verzögerung) Weltaktualisierungen vom Server empfängt, sieht der Spieler die Welt ein wenig anders als auf dem Server. Der Spieler sieht sich in der Gegenwart und im Rest der Welt - in der Vergangenheit. Auf dem Server existiert die ganze Welt auf einmal.


Aus diesem Grund schießt der Spieler lokal auf ein Ziel, das sich auf dem Server an einem anderen Ort befindet.

Um die Verzögerung auszugleichen, verwenden wir den Zeitrücklauf auf dem Server. Der Operationsalgorithmus ist ungefähr der folgende:

  1. Der Client sendet mit jeder Eingabe zusätzlich die Tick-Zeit an den Server, in der er den Rest der Welt sieht.
  2. Der Server überprüft diese Zeit: ist die Differenz zwischen der aktuellen Zeit und der sichtbaren Zeit der Client-Welt im Konfidenzintervall.
  3. Wenn die Zeit gültig ist, verlässt der Server den Spieler in der aktuellen Zeit und der Rest der Welt kehrt in den Zustand zurück, in dem der Spieler das Ergebnis des Schusses gesehen und berechnet hat.
  4. Wenn ein Spieler trifft, wird der Schaden in der aktuellen Serverzeit verursacht.

Die Rückspulzeit auf einem Server funktioniert wie folgt: Die Geschichte der Welt (in ECS) und die Geschichte der Physik (unterstützt von der Volatile Physics Engine) werden im Norden gespeichert. Zum Zeitpunkt der Berechnung des Schusses wurden die Daten des Spielers aus dem aktuellen Zustand der Welt und die verbleibenden Spieler aus der Geschichte entnommen.

Der Code für das Schussvalidierungssystem sieht ungefähr so ​​aus:
 public void Execute(GameState gs) { foreach (var shotPair in gs.WorldState.Shot) { var shot = shotPair.Value; var shooter = gs.WorldState[shotPair.Key]; var shooterTransform = shooter.Transform; var weaponStats = gs.WorldState.WeaponStats[shot.WeaponId]; // DeltaTime shouldn't exceed physics history size var shootDeltaTime = (int) (gs.Time - shot.ShotPlayerWorldTime); if (shootDeltaTime > PhysicsWorld.HistoryLength) { continue; } // Get the world at the time of shooting. var oldState = _immutableHistory.Get(shot.ShotPlayerWorldTime); var potentialTarget = oldState.WorldState[shot.Target.Id]; var hitTargetId = _singleShotValidator.ValidateTargetAvailabilityInLine(oldState, potentialTarget, shooter, shootDeltaTime, weaponStats.ShotDistance, shooter.Transform.Angle.GetDirection()); if (hitTargetId != 0) { gs.WorldState.CreateEntity().AddDamage(gs.WorldState[hitTargetId], shooter, weaponStats.ShotDamage); } } } 


Ein wesentlicher Nachteil des Ansatzes besteht darin, dass wir dem Kunden die Daten zum Zeitpunkt des Ticks anvertrauen, den er sieht. Potenziell kann ein Spieler einen Vorteil erzielen, indem er den Ping künstlich erhöht. Weil Je mehr Ping ein Spieler hat, desto weiter schießt er in der Vergangenheit.

Einige Probleme, auf die wir gestoßen sind


Während der Implementierung dieser Netzwerk-Engine sind wir auf viele Probleme gestoßen, von denen einige einen separaten Artikel wert sind, aber hier werde ich nur einige davon ansprechen.

Simulation der ganzen Welt in einem Vorhersagesystem und Kopieren


Anfangs hatten alle Systeme in unserem ECS nur eine Methode: void Execute (GameState gs). Bei dieser Methode wurden normalerweise Komponenten verarbeitet, die sich auf alle Spieler beziehen.

Ein Beispiel für ein Bewegungssystem in der ersten Implementierung:
 public sealed class MovementSystem : ISystem { public void Execute(GameState gs) { foreach (var movementPair in gs.WorldState.Movement) { var transform = gs.WorldState.Transform[movementPair.Key]; transform.Position += movementPair.Value.Velocity * GameState.TickDuration; } } } 


Im lokalen Spielervorhersagesystem mussten wir jedoch nur die Komponenten verarbeiten, die sich auf einen bestimmten Spieler beziehen. Zunächst haben wir dies mit copy implementiert.

Der Vorhersageprozess war wie folgt:

  1. Eine Kopie des Spielstatus wurde erstellt.
  2. Eine Kopie wurde an den ECS-Eingang geliefert.
  3. In ECS gab es eine Simulation der ganzen Welt.
  4. Alle Daten, die sich auf den lokalen Spieler beziehen, wurden aus dem neu empfangenen Spielstatus kopiert.

Die Vorhersagemethode sah folgendermaßen aus:
 void PredictNewState(GameState state) { var newState = _stateHistory.Get(state.Tick+1); var input = _inputHistory.Get(state.Tick); newState.Copy(state); _tempGameState.Copy(state); _ecsExecutor.Execute(_tempGameState, input); _playerEntitiesCopier.Copy(_tempGameState, newState); } 


Bei dieser Implementierung gab es zwei Probleme:

  1. Weil Wir verwenden Klassen, keine Strukturen - das Kopieren ist für uns eine ziemlich teure Operation (ca. 0,1-0,15 ms auf dem iPhone 5S).
  2. Die Simulation der ganzen Welt nimmt ebenfalls viel Zeit in Anspruch (ca. 1,5-2 ms auf dem iPhone 5S).

Wenn wir berücksichtigen, dass es während des Koordinierungsprozesses notwendig ist, 5 bis 15 Weltstaaten in einem Rahmen neu zu berechnen, dann war bei einer solchen Implementierung alles furchtbar langsam.

Die Lösung war ganz einfach: zu lernen, die Welt in Teilen zu simulieren, nämlich nur einen bestimmten Spieler zu simulieren. Wir haben alle Systeme neu geschrieben, damit Sie die ID des Spielers übertragen und nur ihn simulieren können.

Ein Beispiel für ein Bewegungssystem nach einer Änderung:
 public sealed class MovementSystem : ISystem { public void Execute(GameState gs) { foreach (var movementPair in gs.WorldState.Movement) { Move(gs.WorldState.Transform[movementPair.Key], movementPair.Value); } } public void ExecutePlayer(GameState gs, uint playerId) { var movement = gs.WorldState.Movement[playerId]; if(movement != null) { Move(gs.WorldState.Transform[playerId], movement); } } private void Move(Transform transform, Movement movement) { transform.Position += movement.Velocity * GameState.TickDuration; } } 


Nach den Änderungen konnten wir unnötige Kopien im Vorhersagesystem entfernen und die Belastung des Matching-Systems verringern.

Code:
 void PredictNewState(GameState state, uint playerId) { var newState = _stateHistory.Get(state.Tick+1); var input = _inputHistory.Get(state.Tick); newState.Copy(state); _ecsExecutor.Execute(newState, input, playerId); } 


Erstellen und Löschen von Entitäten in einem Vorhersagesystem


In unserem System erfolgt der Abgleich von Entitäten auf dem Server und dem Client durch eine Ganzzahlkennung (ID). Für alle Entitäten verwenden wir die End-to-End-Nummerierung von Bezeichnern. Jede neue Entität hat den Wert id = oldID + 1.

Dieser Ansatz ist sehr praktisch zu implementieren, hat jedoch einen wesentlichen Nachteil: Die Reihenfolge der Erstellung neuer Entitäten auf dem Client und dem Server kann unterschiedlich sein, und infolgedessen unterscheiden sich die Kennungen der Entitäten.

Dieses Problem trat auf, als wir ein System zur Vorhersage von Spielerschüssen implementierten. Jeder Schuss bei uns ist eine separate Einheit mit der Schusskomponente. Für jeden Client war die ID der Schussentitäten im Vorhersagesystem sequentiell. Wenn jedoch im selben Moment ein anderer Spieler schoss, unterschied sich auf dem Server die ID aller Schüsse vom Client.

Die Aufnahmen auf dem Server wurden in einer anderen Reihenfolge erstellt:



Bei Schüssen haben wir diese Einschränkung umgangen, basierend auf den Gameplay-Funktionen des Spiels. Schüsse sind schnell lebende Wesen, die im Bruchteil einer Sekunde nach ihrer Erstellung im System zerstört werden. Auf dem Client haben wir einen separaten Bereich von IDs hervorgehoben, die sich nicht mit Server-IDs überschneiden und Aufnahmen im Koordinationssystem nicht mehr berücksichtigen. Das heißt, Die Schüsse lokaler Spieler werden im Spiel immer nur gemäß dem Vorhersagesystem gezogen und berücksichtigen nicht die Daten vom Server.

Bei diesem Ansatz werden dem Spieler keine Artefakte auf dem Bildschirm angezeigt (Löschen, Neuerstellen, Zurücksetzen von Aufnahmen), und die Diskrepanzen mit dem Server sind gering und wirken sich nicht auf das gesamte Gameplay aus.

Diese Methode ermöglichte es, das Problem mit Aufnahmen zu lösen, jedoch nicht das gesamte Problem der Erstellung von Entitäten auf dem gesamten Client. Wir arbeiten noch an möglichen Methoden, um den Vergleich der auf dem Client und dem Server erstellten Objekte zu lösen.

Es sollte auch beachtet werden, dass dieses Problem nur die Erstellung neuer Entitäten (mit neuen IDs) betrifft. Das Hinzufügen und Entfernen von Komponenten zu bereits erstellten Entitäten erfolgt problemlos: Komponenten haben keine Bezeichner, und jede Entität kann nur eine Komponente eines bestimmten Typs haben. Daher erstellen wir normalerweise Entitäten auf dem Server und fügen in Vorhersagesystemen nur Komponenten hinzu / entfernen diese.

Abschließend möchte ich sagen, dass die Implementierung von Multiplayer nicht die einfachste und schnellste ist, aber es gibt viele Informationen dazu.

Was zu lesen


Source: https://habr.com/ru/post/de415959/


All Articles