Battle Prime ist das erste Projekt unseres Studios. Trotz der Tatsache, dass viele Mitglieder des Teams gute Erfahrung in der Entwicklung von Spielen haben, hatten wir natürlich verschiedene Schwierigkeiten, als wir daran arbeiteten. Sie entstanden sowohl bei der Arbeit an der Engine als auch bei der Entwicklung des Spiels.
In der Gamedev-Branche gibt es eine große Anzahl von Entwicklern, die bereitwillig ihre Geschichten, Best Practices und Architekturentscheidungen in der einen oder anderen Form teilen. Diese Erfahrung, die im öffentlichen Raum in Form von Artikeln, Präsentationen und Berichten präsentiert wird, ist eine hervorragende Quelle für Ideen und Inspirationen. Zum Beispiel waren die Berichte des Overwatch-Entwicklungsteams für uns bei der Arbeit am Motor sehr nützlich. Wie das Spiel selbst sind sie sehr talentiert und ich rate jedem, der daran interessiert ist, sie zu sehen. Verfügbar im GDC-Tresor und auf
YouTube .
Dies ist einer der Gründe, warum wir auch zur gemeinsamen Sache beitragen wollen - und dieser Artikel ist einer der ersten, der sich mit den technischen Details der Entwicklung und des Spielens der Blitz-Engine befasst - Battle Prime.
Der Artikel wird in zwei Teile unterteilt:
- ECS: Implementierung des Entity-Component-System-Musters in der Blitz Engine. Dieser Abschnitt ist wichtig für das Verständnis der Codebeispiele im Artikel und an sich ein separates interessantes Thema.
- Netcode und Gameplay: Alles über den High-Level-Netzwerkteil und seine Verwendung im Spiel - Client-Server-Architektur, Client-Vorhersagen, Replikation. Eines der wichtigsten Dinge in einem Schützen ist das Schießen, daher wird mehr Zeit dafür aufgewendet.
Unter dem Schnitt viele Megabyte Gifs!In jedem Abschnitt werde ich zusätzlich zu der Geschichte über die Funktionalität und ihre Verwendung versuchen, die Mängel zu beschreiben, die sie in sich trägt - sei es ihre Grenzen, Unannehmlichkeiten bei der Arbeit oder nur Gedanken über ihre Verbesserungen in der Zukunft.
Ich werde auch versuchen, Codebeispiele und einige Statistiken zu geben. Erstens ist es nur interessant, und zweitens gibt es einen kleinen Kontext zum Umfang der Verwendung dieser oder jener Funktionalität und dieses Projekts.
ECS
Innerhalb der Engine verwenden wir den Begriff „Welt“, um eine Szene zu beschreiben, die eine Hierarchie von Objekten enthält.
Welten arbeiten nach der Vorlage Entity-Component-System (
Beschreibung auf Wikipedia ):
- Entität - ein Objekt innerhalb der Szene. Es ist ein Repository für eine Reihe von Komponenten. Objekte können verschachtelt werden und eine Hierarchie innerhalb der Welt bilden.
- Komponente - sind die Daten, die für den Betrieb einer Mechanik erforderlich sind und die das Verhalten des Objekts bestimmen. Beispielsweise enthält "TransformComponent" die Transformation des Objekts und "DynamicBodyComponent" enthält Daten für die physikalische Simulation. Einige Komponenten verfügen möglicherweise nicht über zusätzliche Daten. Ihre einfache Anwesenheit im Objekt beschreibt den Status dieses Objekts. In Battle Prime werden beispielsweise "AliveComponent" und "DeadComponent" verwendet, die lebende bzw. tote Charaktere markieren.
- System - ein regelmäßig genannter Satz von Funktionen, die die Lösung seiner Aufgabe unterstützen. Bei jedem Aufruf verarbeitet das System Objekte, die eine bestimmte Bedingung erfüllen (normalerweise mit einem bestimmten Satz von Komponenten), und ändert sie gegebenenfalls. Die gesamte Spielelogik und der größte Teil der Engine werden auf Systemebene implementiert. In der Engine befindet sich beispielsweise ein "LodSystem", das die LOD-Indizes (Detaillierungsgrad) für ein Objekt basierend auf seiner Transformation in der Welt und anderen Daten berechnet. Dieser in der `LodComponent` enthaltene Index wird dann von anderen Systemen für ihre Aufgaben verwendet.
Dieser Ansatz macht es einfach, verschiedene Mechaniken innerhalb eines Objekts zu kombinieren. Sobald die Entität genügend Daten für die Arbeit einiger Mechaniker erhält, beginnen die für diese Mechanik verantwortlichen Systeme, dieses Objekt zu verarbeiten.
In der Praxis reduziert sich das Hinzufügen einer neuen Funktion auf eine neue Komponente (oder einen Satz von Komponenten) und ein neues System (oder einen Satz von Systemen), die diese Funktion implementieren. In den allermeisten Fällen ist es praktisch, an diesem Muster zu arbeiten.
Reflexion
Bevor ich mit der Beschreibung von Komponenten und Systemen fortfahre, werde ich ein wenig auf den Reflexionsmechanismus eingehen, da er häufig in Codebeispielen verwendet wird.
Mit Reflection können Sie Informationen zu Typen empfangen und verwenden, während die Anwendung ausgeführt wird. Insbesondere stehen folgende Funktionen zur Verfügung:
- Holen Sie sich eine Liste von Typen nach einem bestimmten Kriterium (zum Beispiel die Erben einer Klasse oder mit einem speziellen Tag),
- Holen Sie sich eine Liste der Klassenfelder,
- Holen Sie sich eine Liste der Methoden innerhalb der Klasse,
- Holen Sie sich eine Liste der Aufzählungswerte,
- Rufen Sie eine Methode auf oder ändern Sie den Wert eines Feldes.
- Ruft die Metadaten eines Felds oder einer Methode ab, die für eine bestimmte Funktion verwendet werden können.
Viele Module im Motor verwenden die Reflexion für ihre eigenen Zwecke. Einige Beispiele:
- Integrationen von Skriptsprachen verwenden Reflection, um mit in C ++ - Code deklarierten Typen zu arbeiten.
- Der Editor verwendet Reflection, um eine Liste der Komponenten zu erhalten, die dem Objekt hinzugefügt werden können, sowie um deren Felder anzuzeigen und zu bearbeiten.
- Das Netzwerkmodul verwendet die Feldmetadaten in den Komponenten für eine Reihe von Funktionen: Sie geben die Parameter für das Replizieren von Feldern vom Server auf Clients, die Datenquantisierung während der Replikation usw. an.
- Verschiedene Konfigurationen werden unter Verwendung von Reflexion in Objekte der entsprechenden Typen deserialisiert.
Wir verwenden unsere eigene Implementierung, deren Benutzeroberfläche sich nicht wesentlich von anderen vorhandenen Lösungen unterscheidet (z. B.
github.com/rttrorg/rttr ). Am Beispiel von CapturePointComponent (das den Erfassungspunkt für den Spielmodus beschreibt) sieht das Hinzufügen von Reflexionen zum Typ folgendermaßen aus:
Ich möchte besonders auf die Metadaten von Typen, Feldern und Methoden achten, die mit dem Ausdruck deklariert werden
M<T>()
Dabei ist "T" der Typ der Metadaten (innerhalb des Befehls verwenden wir nur den Begriff "Meta", in Zukunft werde ich ihn verwenden). Sie werden von verschiedenen Modulen für ihre eigenen Zwecke verwendet. Beispielsweise verwendet der Editor "DisplayName", um Typnamen und Felder im Editor anzuzeigen, und das Netzwerkmodul empfängt eine Liste aller Komponenten und sucht unter anderem nach Feldern, die als "Replizierbar" gekennzeichnet sind. Diese werden vom Server an die Clients gesendet.
Beschreibung der Komponenten und deren Hinzufügung zum Objekt
Jede Komponente ist ein Erbe der Basisklasse "Komponente" und kann mit Hilfe der Reflexion die von ihr verwendeten Felder beschreiben (falls erforderlich).
So wird die "AvatarHitComponent" im Spiel deklariert und beschrieben:
class AvatarHitComponent final : public Component { BZ_VIRTUAL_REFLECTION(Component); public: PlayerId source_id = NetConstants::INVALID_PLAYER_ID; PlayerId target_id = NetConstants::INVALID_PLAYER_ID; HitboxType hitbox_type = HitboxType::UNKNOWN; }; BZ_VIRTUAL_REFLECTION_IMPL(AvatarHitComponent) { ReflectionRegistrar::begin_class<AvatarHitComponent>() .ctor_by_pointer() .copy_ctor_by_pointer() .field("source_id", &AvatarHitComponent::source_id)[M<Replicable>()] .field("target_id", &AvatarHitComponent::target_id)[M<Replicable>()] .field("hitbox_type", &AvatarHitComponent::hitbox_type)[M<Replicable>()]; }
Diese Komponente markiert ein Objekt, das erstellt wird, wenn ein Spieler einen anderen Spieler schlägt. Es enthält Informationen zu diesem Ereignis, z. B. die Kennungen des angreifenden Spielers und seines Ziels sowie die Art der Trefferbox, auf der der Treffer aufgetreten ist.
Einfach ausgedrückt wird dieses Objekt auf ähnliche Weise im Serversystem erstellt:
Entity hit_entity = world->create_entity(); auto* const avatar_hit_component = hit_entity.add<AvatarHitComponent>(); avatar_hit_component->source_id = source_player_id; avatar_hit_component->target_id = target_player_id; avatar_hit_component->hitbox_type = hitbox_type;
Das Objekt mit der "AvatarHitComponent" wird dann von verschiedenen Systemen verwendet: um die Geräusche von Schlagern zu spielen, Statistiken zu sammeln, Spielererfolge zu verfolgen und so weiter.
Beschreibung der Systeme und ihrer Arbeit
Ein System ist ein Objekt mit einem von "System" geerbten Typ, der Methoden enthält, die eine bestimmte Aufgabe implementieren. In der Regel reicht eine Methode aus. Es sind mehrere Methoden erforderlich, wenn sie zu unterschiedlichen Zeitpunkten innerhalb desselben Frames durchgeführt werden müssen.
Ähnlich wie die Komponenten, die ihre Felder beschreiben, beschreibt jedes System die Methoden, die von der Welt ausgeführt werden sollten.
Beispielsweise wird das für die Explosionen verantwortliche ExplosiveSystem wie folgt deklariert und beschrieben:
Die folgenden Daten sind in der Systembeschreibung angegeben:
- Das Tag, zu dem das System gehört. Jede Welt enthält eine Reihe von Tags, auf denen sich die Systeme befinden, die in dieser Welt funktionieren sollten. In diesem Fall bedeutet das "Schlacht" -Tag die Welt, in der der Kampf zwischen den Spielern stattfindet. Andere Beispiele für Tags sind "Server" und "Client" (das System wird nur auf dem Server bzw. Client ausgeführt) und "Rendern" (das System wird nur im GUI-Modus ausgeführt).
- Die Gruppe, in der dieses System ausgeführt wird, und die Liste der Komponenten, die dieses System verwendet - zum Schreiben, Lesen und Erstellen;
- Update-Typ - Gibt an, ob dieses System bei normalen Updates, festen Updates oder anderen funktionieren soll.
- Explizite Berechtigungsabhängigkeiten zwischen Systemen.
Weitere Informationen zu Systemgruppen, Abhängigkeiten und Aktualisierungstypen werden nachfolgend beschrieben.
Deklarierte Methoden werden von der Welt zum richtigen Zeitpunkt aufgerufen, um die Funktionalität dieses Systems aufrechtzuerhalten. Der Inhalt der Methode hängt vom System ab. In der Regel werden jedoch alle Objekte durchlaufen, die die Kriterien dieses Systems erfüllen, und anschließend aktualisiert. Das "ExplosiveSystem" im Spiel wird beispielsweise wie folgt aktualisiert:
void ExplosiveSystem::update(float dt) { const auto* time_single_component = world->get<TimeSingleComponent>();
Die Gruppen im obigen Beispiel (`new_explosives_group` und` explosives_group`) sind Hilfscontainer, die die Systemimplementierung vereinfachen. new_explosives_group ist ein Container mit neuen Objekten, die für dieses System erforderlich sind und noch nie verarbeitet wurden, und explosives_group ist ein Container mit allen Objekten, die in jedem Frame verarbeitet werden müssen. Die Welt ist direkt für das Befüllen dieser Behälter verantwortlich. Ihr Empfang beim System erfolgt in seinem Konstruktor:
ExplosiveSystem::ExplosiveSystem(World* world) : System(world) {
Welt Update
Die Welt, ein Objekt vom Typ "Welt", ruft in jedem Frame die notwendigen Methoden in einer Reihe von Systemen auf. Welche Systeme aufgerufen werden, hängt von ihrem Typ ab.
Als Teil des Systems wird jeder Frame unbedingt aktualisiert (der Begriff „normales Update“ wird in der Engine verwendet). Dieser Typ umfasst alle Systeme, die sich auf das Rendern des Frames und der Sounds auswirken: Skelettanimationen, Partikel, Benutzeroberfläche usw. Der andere Teil wird mit einer festen, vorgegebenen Häufigkeit ausgeführt (wir verwenden den Begriff „festes Update“ und für die Anzahl der festen Updates pro Sekunde - FFPS) - sie verarbeiten den größten Teil der Spiellogik und alles, was zwischen Client und Server synchronisiert werden muss - Zum Beispiel Teil der Eingabe des Spielers, Bewegung des Charakters, Schießen, Teil der physischen Simulation.

Die Häufigkeit der Ausführung eines festen Updates sollte ausgewogen sein - ein zu kleiner Wert führt zu einem nicht reagierenden Gameplay (z. B. wird die Eingabe des Spielers weniger häufig und daher mit einer längeren Verzögerung verarbeitet) und zu hoch - und zu hohen Leistungsanforderungen des Geräts, auf dem die Anwendung ausgeführt wird. Dies bedeutet auch, dass die Kosten für die Serverkapazität umso höher sind, je höher die Frequenz ist (weniger Schlachten können gleichzeitig auf demselben Computer ausgeführt werden).
Im folgenden GIF arbeitet die Welt mit einer Häufigkeit von 5 festen Aktualisierungen pro Sekunde. Sie können die Verzögerung zwischen dem Drücken der W-Taste und dem Beginn der Bewegung sowie die Verzögerung zwischen dem Loslassen der Taste und dem Stoppen der Bewegung des Charakters feststellen:
Im nächsten GIF arbeitet die Welt mit einer Häufigkeit von 30 festen Aktualisierungen pro Sekunde, was eine wesentlich reaktionsschnellere Kontrolle ermöglicht:
Im Moment läuft die Welt im festen Update von Battle Prime 31 Mal pro Sekunde. Ein solcher „hässlicher“ Wert wurde speziell ausgewählt - er kann Fehler verursachen, die in anderen Situationen nicht auftreten würden, wenn die Anzahl der Aktualisierungen pro Sekunde beispielsweise eine runde Zahl oder ein Vielfaches der Bildschirmaktualisierungsrate ist.
Systemausführungsreihenfolge
Eines der Dinge, die die Arbeit mit ECS erschweren, ist die Ausführung von Systemen. Zum Zeitpunkt des Schreibens gibt es im Battle Prime-Client während des Kampfes zwischen den Spielern ein 251-System, und ihre Anzahl wächst nur.
Ein System, das fälschlicherweise zur falschen Zeit ausgeführt wird, kann zu subtilen Fehlern oder zu einer Verzögerung des Betriebs einiger Mechaniker für einen Rahmen führen (wenn beispielsweise das Schadenssystem am Anfang des Rahmens und das Projektilflugsystem am Ende funktioniert, wird Schaden angerichtet mit einer Verzögerung von einem Frame).
Die Ausführungsreihenfolge von Systemen kann auf verschiedene Arten festgelegt werden, zum Beispiel:
- Explizite Bestellung
- Angabe der numerischen „Priorität“ des Systems und anschließende Sortierung nach Priorität;
- Erstellen Sie automatisch ein Diagramm der Abhängigkeiten zwischen Systemen und installieren Sie sie an den richtigen Stellen in der Ausführungsreihenfolge.
Im Moment verwenden wir die dritte Option. Jedes System gibt an, welche Komponenten es zum Lesen, welche zum Schreiben und welche Komponenten es erstellt. Dann werden die Systeme automatisch in der erforderlichen Reihenfolge untereinander angeordnet:
- Die Systemlesekomponente A kommt nach dem Systemschreiben in Komponente A;
- Das System, das in Komponente B schreibt oder diese liest, folgt dem System, das Komponente B erstellt.
- Wenn beide Systeme in Komponente C schreiben, kann die Reihenfolge beliebig sein (kann jedoch bei Bedarf manuell angegeben werden).
Theoretisch minimiert eine solche Lösung die Kontrolle über die Ausführungsreihenfolge. Sie müssen lediglich Komponentenmasken für das System festlegen. In der Praxis führt dies mit dem Wachstum des Projekts zu immer mehr Zyklen zwischen den Systemen. Wenn System-1 in Komponente A schreibt und Komponente B liest und System-2 Komponente A liest und in Komponente B schreibt, ist dies ein Zyklus, der manuell aufgelöst werden muss. Oft gibt es mehr als zwei Systeme in einem Zyklus. Ihre Lösung erfordert Zeit und explizite Angaben zur Beziehung zwischen ihnen.
Daher verfügt die Blitz Engine über „Gruppen“ von Systemen. Innerhalb von Gruppen werden Systeme automatisch in der gewünschten Reihenfolge ausgerichtet (und Zyklen werden immer noch manuell aufgelöst), und die Reihenfolge der Gruppen wird explizit festgelegt. Diese Entscheidung ist eine Kreuzung zwischen einer vollständig manuellen und einer vollautomatisierten Bestellung, und die Größe der Gruppen wirkt sich ernsthaft auf deren Wirksamkeit aus. Sobald die Gruppe zu groß wird, stoßen Programmierer häufig auf die Probleme von Schleifen in ihnen.
Derzeit gibt es 10 Gruppen in Battle Prime. Dies ist immer noch nicht genug, und wir planen, ihre Anzahl zu erhöhen, indem wir eine strikte logische Abfolge zwischen ihnen erstellen und die automatische Erstellung eines Diagramms in jedem von ihnen verwenden.
Die Angabe, welche Komponenten von Systemen zum Schreiben oder Lesen verwendet werden, ermöglicht es in Zukunft auch, Systeme automatisch in „Blöcke“ zu gruppieren, die parallel zueinander ausgeführt werden.
Im Folgenden finden Sie ein Hilfsprogramm, das eine Liste der Systeme und die Abhängigkeiten zwischen ihnen in jeder der Gruppen anzeigt (vollständige Diagramme in den Gruppen sehen einschüchternd aus). Die orange Farbe zeigt explizit definierte Abhängigkeiten zwischen Systemen:
Kommunikation zwischen Systemen und deren Konfiguration
Die Aufgaben, die die Systeme in sich selbst ausführen, können bis zu dem einen oder anderen Grad von den Ergebnissen anderer Systeme abhängen. Beispielsweise hängt ein System, das Kollisionen zweier Objekte verarbeitet, von einer Simulation der Physik ab, die diese Kollisionen registriert. Und das Schadenssystem hängt von den Ergebnissen des ballistischen Systems ab, das für die Bewegung der Granaten verantwortlich ist.
Die einfachste und naheliegendste Art der Kommunikation zwischen Systemen ist die Verwendung von Komponenten. Ein System fügt die Ergebnisse seiner Arbeit in eine Komponente ein, und das zweite System liest diese Ergebnisse aus der Komponente und löst das Problem auf ihrer Grundlage.
Ein komponentenbasierter Ansatz kann in einigen Fällen unpraktisch sein:
- Was ist, wenn das Ergebnis des Systems nicht direkt an ein Objekt gebunden ist? Zum Beispiel ein System, das Kampfstatistiken sammelt (die Anzahl der Schüsse, Treffer, Todesfälle usw.) - sammelt sie global, basierend auf der gesamten Schlacht;
- Was ist, wenn das System auf irgendeine Weise konfiguriert werden muss? Beispielsweise muss ein physikalisches Simulationssystem wissen, welche Objekttypen Kollisionen zwischen sich aufzeichnen sollen und welche nicht.
Um diese Probleme zu lösen, verwenden wir den Ansatz, den wir vom Overwatch-Entwicklungsteam übernommen haben - Einzelkomponenten.
Einzelne Komponente ist eine Komponente, die in der Welt in einer einzigen Kopie existiert und direkt von der Welt bezogen wird. Systeme können damit die Ergebnisse ihrer Arbeit addieren, die dann von anderen Systemen verwendet werden, oder ihre Arbeit konfigurieren.
Derzeit umfasst das Projekt (Engine-Module + Spiel) etwa 120 Einzelkomponenten, die für verschiedene Zwecke verwendet werden - von der Speicherung globaler Daten der Welt bis zur Konfiguration einzelner Systeme.
"Sauberer" Ansatz
In seiner reinsten Form erfordert ein solcher Ansatz für Systeme und Komponenten die Verfügbarkeit von Daten nur innerhalb der Komponenten und das Vorhandensein von Logik nur innerhalb der Systeme. Meiner Meinung nach ist es in der Praxis selten sinnvoll, diese Einschränkung strikt einzuhalten (obwohl die Debatten zu diesem Thema immer noch regelmäßig geführt werden).
Die folgenden Argumente für einen weniger „strengen“ Ansatz können hervorgehoben werden:
- Ein Teil des Codes sollte gemeinsam genutzt werden - und synchron von verschiedenen Systemen oder beim Festlegen einiger Eigenschaften von Komponenten ausgeführt werden. Eine ähnliche Logik wird separat beschrieben. Als Teil der Engine verwenden wir den Begriff Utils. Zum Beispiel enthält "DamageUtils" im Spiel die Logik, die mit der Anwendung von Schaden verbunden ist - die von verschiedenen Systemen aus angewendet werden kann;
- Es macht keinen Sinn, die privaten Daten des Systems an einem anderen Ort als diesem System selbst aufzubewahren - niemand wird sie außer diesem benötigen, und das Verschieben an einen anderen Ort ist nicht besonders nützlich. Es gibt eine Ausnahme von dieser Regel, die mit der Funktionalität von Client-Vorhersagen verbunden ist. Sie wird im folgenden Abschnitt beschrieben.
- Für Komponenten ist es nützlich, eine kleine Menge an Logik zu haben - zum größten Teil handelt es sich dabei um intelligente Getter und Setter, die die Arbeit mit der Komponente vereinfachen.
Netcode
Battle Prime verwendet eine Architektur mit autoritären Server- und Client-Vorhersagen. Dies ermöglicht es dem Spieler, auch bei hohen Pings und Paketverlusten und dem gesamten Projekt sofortiges Feedback von seinen Aktionen zu erhalten - um das Betrügen durch die Spieler zu minimieren, weil Der Server diktiert alle Simulationsergebnisse innerhalb des Kampfes.
Der gesamte Code im Spielprojekt ist in drei Teile unterteilt:
- Client - Systeme und Komponenten, die nur auf dem Client funktionieren. Dazu gehören Dinge wie Benutzeroberfläche, automatische Aufnahme und Interpolation;
- Server - Systeme und Komponenten, die nur auf dem Server funktionieren. Zum Beispiel alles, was mit Schaden und Spawn-Charakteren zu tun hat;
- Allgemein - Dies ist alles, was sowohl auf dem Server als auch auf dem Client funktioniert. Insbesondere alle Systeme, die die Bewegung des Charakters, den Zustand der Waffe (Anzahl der Runden, Abklingzeiten) und alles andere berechnen, was auf dem Client vorhergesagt werden muss. Die meisten Systeme, die für visuelle Effekte verantwortlich sind, sind ebenfalls üblich - der Server kann optional im GUI-Modus gestartet werden (größtenteils nur zum Debuggen).
Benutzereingabe (Eingabe)
Bevor Sie mit den Details der Replikation und Vorhersagen auf dem Client fortfahren, sollten Sie sich mit der Eingabe innerhalb der Engine befassen. Die Details hierzu sind in den folgenden Abschnitten wichtig.
Alle Eingaben des Players sind in zwei Typen unterteilt: Low-Level und High-Level:
- Low-Level-Eingabe - Dies sind Ereignisse von Eingabegeräten, z. B. Tastenanschläge, Berühren des Bildschirms usw. Eine solche Eingabe wird von Spielsystemen selten verarbeitet.
- High-Level-Eingabe - sind die Aktionen des Benutzers, die er im Kontext des Spiels ausgeführt hat: Schuss, Waffenwechsel, Charakterbewegung usw. Für solche Aktionen auf hoher Ebene verwenden wir den Begriff "Aktion". Außerdem können der Aktion zusätzliche Daten zugeordnet werden, z. B. die Bewegungsrichtung oder der Index der ausgewählten Waffe. Die überwiegende Mehrheit der Systeme arbeitet mit Aktionen.
Eine Eingabe auf hoher Ebene wird entweder auf der Basis von Bindemitteln aus einer Eingabe auf niedriger Ebene oder programmgesteuert erzeugt. Zum Beispiel kann eine Schussaktion an einen Mausklick gebunden sein oder von dem für das automatische Schießen verantwortlichen System generiert werden. Sobald der Spieler auf den Feind gerichtet hat, generiert dieses System einen Aktionsschuss, wenn der Benutzer die entsprechende Einstellung aktiviert hat. Aktionen können auch vom UI-System gesendet werden: zum Beispiel durch Drücken der entsprechenden Taste oder beim Bewegen des Bildschirm-Joysticks. Ein System, das ausgelöst wird, spielt keine Rolle, wie diese Aktion erstellt wurde.
Logisch verwandte Aktionen werden zusammengefasst (Objekte vom Typ "ActionSet"). Gruppen können getrennt werden, wenn sie im aktuellen Kontext nicht benötigt werden. In Battle Prime gibt es beispielsweise mehrere Gruppen, darunter:
- Aktionen zur Steuerung der Bewegung des Charakters,
- Aktionen zum Abfeuern von automatischen Waffen,
- Aktionen zum Abfeuern von halbautomatischen Waffen.
Von den letzten beiden Gruppen ist je nach Art der ausgewählten Waffe jeweils nur eine aktiv - sie unterscheiden sich darin, wie die FIRE-Aktion generiert wird: während die Taste gedrückt wird (für automatische Waffen) oder nur einmal, wenn die Taste gedrückt wird (für halbautomatische Waffen) )
In ähnlicher Weise werden Aktionsgruppen innerhalb des Spiels in einem der Systeme erstellt und konfiguriert:
static const Map<FastName, ActionSet> action_sets = { {
Battle Prime beschreibt ungefähr 40 Aktionen. Einige von ihnen werden nur zum Debuggen oder Aufzeichnen von Clips verwendet.Replikation
Bei der Replikation werden Daten von einem Server an Clients übertragen. Alle Daten werden durch Objekte in der Welt übertragen:- Ihre Erstellung und Löschung,
- Erstellen und Löschen von Komponenten für Objekte,
- Ändern Sie die Komponenteneigenschaften.
Die Replikation wird mit der entsprechenden Komponente konfiguriert. In ähnlicher Weise richtet das Spiel beispielsweise die Replikation der Waffen des Spielers ein: auto* replication_component = weapon_entity.add<ReplicationComponent>(); replication_component->enable_replication<WeaponDescriptorComponent>(Privacy::PUBLIC); replication_component->enable_replication<WeaponBaseStatsComponent>(Privacy::PUBLIC); replication_component->enable_replication<WeaponComponent>(Privacy::PRIVATE); replication_component->enable_replication<BallisticsStatsComponent>(Privacy::PRIVATE);
Für jede Komponente wird der Datenschutz angegeben, der während der Replikation verwendet wird. Private Komponenten werden vom Server nur an den Spieler gesendet, dem diese Waffe gehört. Öffentliche Komponenten werden an alle gesendet. In diesem Beispiel sind "WeaponDescriptorComponent" und "WeaponBaseStatsComponent" öffentlich - sie enthalten die Daten, die für die korrekte Anzeige anderer Spieler erforderlich sind. Beispielsweise werden der Index des Slots, in dem die Waffe liegt, und ihr Typ für Animationen benötigt. Die restlichen Komponenten werden privat an den Spieler gesendet, dem diese Waffe gehört - die Parameter der Ballistik der Granaten, Informationen über die Gesamtzahl der Runden, die verfügbaren Schießmodi usw. Es gibt speziellere Datenschutzmodi: Sie können beispielsweise eine Komponente nur an Verbündete oder nur an Feinde senden.Jede Komponente in ihrer Beschreibung muss angeben, welche Felder in dieser Komponente repliziert werden sollen. Beispielsweise sind alle Felder in der "WeaponComponent" als "Replizierbar" markiert: BZ_VIRTUAL_REFLECTION_IMPL(WeaponComponent) { ReflectionRegistrar::begin_class<WeaponComponent>() .ctor_by_pointer() .copy_ctor_by_pointer() .field("owner", &WeaponComponent::owner)[M<Replicable>()] .field("fire_mode", &WeaponComponent::fire_mode)[M<Replicable>()] .field("loaded_ammo", &WeaponComponent::loaded_ammo)[M<Replicable>()] .field("ammo", &WeaponComponent::ammo)[M<Replicable>()] .field("shooting_cooldown_end_ms", &WeaponComponent::shooting_cooldown_end_ms)[M<Replicable>()]; }
Dieser Mechanismus ist sehr bequem zu bedienen. Innerhalb des Serversystems, das für das "Auswerfen" von Token von getöteten Gegnern verantwortlich ist (in einem speziellen Spielmodus), reicht es beispielsweise aus, "ReplicationComponent" auf einem solchen Token hinzuzufügen und zu konfigurieren. Es sieht so aus: for (const Component* component : added_dead_avatars->components) { Entity kill_token_entity = world->create_entity();
In diesem Beispiel wird die physische Simulation des Tokens beim Auftreten auf dem Server ausgeführt, und die endgültige Transformation des Tokens wird gesendet und auf den Client angewendet. Auf dem Client arbeitet auch ein Interpolationssystem, das die Bewegung dieses Tokens unter Berücksichtigung der Häufigkeit von Aktualisierungen, der Qualität der Verbindung zum Server usw. glättet. Andere Systeme, die diesem Spielmodus zugeordnet sind, fügen Objekten mit "KillTokenComponent" einen visuellen Teil hinzu und überwachen deren Auswahl.Die einzige Unannehmlichkeit des aktuellen Ansatzes, auf die Sie achten möchten und die Sie in Zukunft beseitigen möchten, ist die Unfähigkeit, den Datenschutz für jedes Komponentenfeld festzulegen. Dies ist nicht sehr kritisch, da ein ähnliches Problem leicht gelöst werden kann, indem die Komponente in mehrere aufgeteilt wird: Beispielsweise enthält das Spiel "ShooterPublicComponent" und "ShooterPrivateComponent" mit der entsprechenden Privatsphäre. Trotz der Tatsache, dass sie an einen Mechaniker gebunden sind (Schießen), sind zwei Komponenten erforderlich, um Verkehr zu sparen. Einige Felder werden bei Kunden, die diese Komponenten nicht besitzen, einfach nicht benötigt. Dies fügt dem Programmierer jedoch Arbeit hinzu.Im Allgemeinen können auf einen Client replizierte Objekte Status für verschiedene Frames haben. Daher wurde die Möglichkeit hinzugefügt, Objekte durch Bilden von Replikationsgruppen zu gruppieren. Alle Komponenten auf Objekten innerhalb derselben Gruppe haben immer einen Status für denselben Frame auf dem Client. Dies ist erforderlich, damit die Vorhersagen korrekt funktionieren (mehr dazu weiter unten). Zum Beispiel gehören eine Waffe und ein Charakter, dem sie gehört, zur selben Gruppe. Wenn sich die Objekte in verschiedenen Gruppen befinden, kann sich ihr Status in der Welt auf verschiedene Frames beziehen.Das Replikationssystem versucht, das Verkehrsaufkommen zu minimieren, indem es insbesondere die übertragenen Daten komprimiert (jedes Feld innerhalb der Komponente kann optional entsprechend für die Komprimierung markiert werden) und nur die Wertdifferenz zwischen den beiden Frames überträgt.Kundenvorhersagen
Kundenvorhersagen (der Begriff clientseitige Vorhersage wird im Englischen verwendet) ermöglichen es dem Spieler, sofortiges Feedback zu den meisten seiner Aktionen im Spiel zu erhalten. Da sich das letzte Wort immer hinter dem Server befindet, muss der Client es im Falle eines Fehlers in der Simulation (der Begriff Fehlvorhersage wird im Englischen verwendet, ich werde sie in Zukunft einfach als "Fehlvorhersagen" bezeichnen) beheben. Weitere Details zu Vorhersagefehlern und wie sie korrigiert werden, werden unten beschrieben.Kundenvorhersagen funktionieren nach folgenden Regeln:- Der Client simuliert sich vorwärts durch N Frames;
- Alle vom Client generierten Eingaben werden an den Server gesendet (in Form von Aktionen, die vom Spieler ausgeführt werden).
- N hängt von der Qualität der Verbindung zum Server ab. Je kleiner dieser Wert ist, desto „aktueller“ ist das Bild der Welt für den Kunden (dh die Zeitlücke zwischen dem lokalen Spieler und anderen Spielern ist kleiner).
Infolgedessen führen sowohl der Server als auch der Client die Simulation basierend auf Client-Eingaben durch. Der Server sendet dann die Ergebnisse dieser Simulation an den Client. Wenn der Client feststellt, dass seine Ergebnisse nicht mit denen des Servers übereinstimmen, versucht er, den Fehler zu korrigieren. Er rollt sich auf den letzten bekannten Serverstatus zurück und simuliert erneut N Frames voraus. Dann geht alles nach einem ähnlichen Schema weiter - der Client simuliert sich auch in Zukunft in Bezug auf den Server und der Server sendet ihm die Ergebnisse seiner Simulation. Daraus folgt, dass der gesamte Code, der die Clientvorhersagen beeinflusst, zwischen Client und Server gemeinsam genutzt werden muss.Um Datenverkehr zu sparen, wird die gesamte Eingabe basierend auf einem vordefinierten Schema vorkomprimiert. Dann wird es an den Server gesendet und sofort wieder an den Client dekomprimiert. Das Packen und anschließende Entpacken auf dem Client ist erforderlich, um den Unterschied in den Werten zu beseitigen, die mit der Eingabe zwischen Client und Server verbunden sind. Beim Erstellen eines Schemas wird der Wertebereich für diese Aktion und die Anzahl der Bits angegeben, in die es gepackt werden soll. In ähnlicher Weise sieht die Ankündigung des Verpackungsschemas in Battle Prime wie in einem gemeinsamen System zwischen Client und Server aus: auto* input_packing_sc = world->get_for_write<InputPackingSingleComponent>(); input_packing_sc->packing_schema = { { ActionNames::MOVE, AnalogStatePrecision{ 8, { -1.f, 1.f }, false } }, { ActionNames::LOOK, AnalogStatePrecision{ 16, { -PI, PI }, false } }, { ActionNames::JUMP, nullopt },
Eine kritische Bedingung für die Leistung von Client-Vorhersagen ist die Notwendigkeit, dass die Eingabe Zeit hat, um zum Zeitpunkt der Rahmensimulation, auf die sich diese Eingabe bezieht, zum Server zu gelangen. Für den Fall, dass die Eingabe den gewünschten Frame auf dem Server nicht erreicht hat (dies kann beispielsweise während eines scharfen Ping-Sprungs passieren), versucht der Server, die Eingabe dieses Clients aus dem vorherigen Frame zu verwenden. Dies ist ein Sicherungsmechanismus, mit dessen Hilfe in einigen Situationen falsche Vorhersagen auf dem Client beseitigt werden können. Wenn ein Client beispielsweise einfach in eine Richtung ausgeführt wird und sich seine Eingabe für eine relativ lange Zeit nicht ändert, ist die Verwendung der Eingabe für den letzten Frame erfolgreich - der Server "errät" sie und es gibt keine Diskrepanz zwischen Client und Server. Ein ähnliches Schema wird in Overwatch verwendet (wurde in einem Vortrag über GDC erwähnt:www.youtube.com/watch?v=W3aieHjyNvw ).Derzeit sagt der Battle Prime-Client den Status der folgenden Objekte voraus:- Spieler-Avatar (Position in der Welt und alles, was sie beeinflussen kann, Stand der Fähigkeiten usw.);
- Alle Waffen des Spielers (Anzahl der Runden im Laden, Abklingzeiten zwischen den Schüssen usw.).
Bei der Verwendung von Client-Vorhersagen müssen die "PredictionComponent" auf dem Client zu den gewünschten Objekten hinzugefügt und konfiguriert werden. Beispielsweise wird die Vorhersage des Avatars eines Spielers in einem der Systeme auf ähnliche Weise aktiviert:
Dieser Code bedeutet, dass die Felder in den oben genannten Komponenten ständig mit den ähnlichen Feldern der Serverkomponenten verglichen werden. Wenn eine Diskrepanz in den Werten innerhalb eines einzelnen Frames festgestellt wird, wird auf dem Client eine Anpassung vorgenommen.Das Diskrepanzkriterium hängt von der Art der Daten ab. In den meisten Fällen ist dies nur ein Aufruf von "operator ==". Die Ausnahme bilden Daten, die auf float basieren. Für sie ist der maximal zulässige Fehler derzeit behoben und beträgt 0,005. In Zukunft besteht der Wunsch, die Möglichkeit hinzuzufügen, die Genauigkeit für jedes Komponentenfeld separat einzustellen.Der Replikations- und Clientvorhersage-Workflow basiert auf der Tatsache, dass alle für die Simulation erforderlichen Daten in den Komponenten enthalten sind. Oben im Abschnitt über ECS habe ich geschrieben, dass Systeme einen Teil der Daten enthalten dürfen - dies kann in einigen Fällen praktisch sein. Dies gilt nicht für Daten, die sich auf die Simulation auswirken. Sie müssen sich immer innerhalb der Komponenten befinden, da die Client- und Server-Snapshot-Systeme nur mit den Komponenten arbeiten.Neben der Vorhersage von Feldwerten innerhalb von Komponenten ist es möglich, die Erstellung und Entfernung von Komponenten vorherzusagen. Wenn beispielsweise aufgrund der Verwendung der Fähigkeit dem Charakter eine "SpeedModifierComponent" überlagert wird (die die Bewegungsgeschwindigkeit ändert, z. B. den Spieler beschleunigt), muss sie dem Charakter sowohl auf dem Server als auch auf dem Client im selben Frame hinzugefügt werden, andernfalls wird sie hinzugefügt führt zu einer falschen Vorhersage der Position des Charakters auf dem Client.Das Vorhersagen des Erstellens und Löschens von Objekten wird derzeit nicht unterstützt. Dies kann in einigen Situationen praktisch sein, erschwert jedoch auch Netzwerkmodule. Vielleicht werden wir in Zukunft darauf zurückkommen.Unten sehen Sie ein GIF, in dem die Zeichensteuerung mit RTT etwa 1,5 Sekunden lang erfolgt. Wie Sie sehen können, wird der Charakter trotz der hohen Verzögerung sofort gesteuert: Bewegen, Schießen, Nachladen, Granatenwerfen - alles geschieht, ohne auf Informationen vom Server zu warten. Sie können auch feststellen, dass die Erfassung eines Punkts (einer durch Dreiecke begrenzten Zone) mit einer Verzögerung beginnt. Diese Mechanik funktioniert nur auf dem Server und wird vom Client nicht vorhergesagt.Fehleinschätzungen und Resimulationen
Fehlvorhersage - Diskrepanz zwischen den Ergebnissen von Server- und Client-Simulationen. Bei der Resimulation wird diese Diskrepanz vom Kunden korrigiert.Der erste Grund für das Auftreten von Fehlvorhersagen sind die scharfen Ping-Sprünge, für die der Kunde keine Zeit hatte, sich anzupassen. In einer solchen Situation hat die Eingabe vom Player möglicherweise keine Zeit, um zum Server zu gelangen, und der Server verwendet den oben beschriebenen Sicherungsmechanismus, wobei die letzte Eingabe für einige Zeit dupliziert wird, und nach einer Weile wird die Verwendung eingestellt.Der zweite Grund ist die Interaktion des Charakters mit Objekten, die vollständig vom Server gesteuert werden und vom Client nicht lokal vorhergesagt werden. Zum Beispiel führt eine Kollision mit einem anderen Spieler zu einer Fehlvorhersage - da diese tatsächlich in zwei verschiedenen Zeiträumen leben (der lokale Charakter ist in Zukunft relativ zu einem anderen Spieler - dessen Position vom Server stammt und interpoliert wird).Der dritte und unangenehmste Grund sind Fehler im Code. Beispielsweise kann ein System fälschlicherweise nicht replizierte Daten verwenden, um die Simulation zu steuern, oder die Systeme arbeiten in der falschen Reihenfolge oder sogar in unterschiedlichen Reihenfolgen auf dem Server und dem Client.Das Auffinden dieser Fehler dauert manchmal ziemlich lange. Um die Suche zu vereinfachen, haben wir verschiedene Hilfstools erstellt. Während die Anwendung ausgeführt wird, können Sie Folgendes sehen:- Replizierte Komponenten
- Die Anzahl der falschen Vorhersagen
- Auf welchen Frames sind sie passiert,
- Welche Daten befanden sich auf dem Server und auf dem Client in den divergierenden Komponenten?
- Welche Eingabe wurde auf dem Server und auf dem Client für diesen Frame angewendet?
Leider dauert die Suche nach den Ursachen für Resimulationen auch bei ihnen noch recht lange. Zweifellos müssen Tools und Validierungen entwickelt werden, um die Wahrscheinlichkeit von Fehlern zu verringern und deren Suche zu vereinfachen.Um den Betrieb von Resimulationen zu unterstützen, muss das System von einer bestimmten Klasse "ResimulatableSystem" erben. In einer Situation, in der eine Fehlvorhersage auftritt, "rollt" die Welt alle Objekte auf den letzten bekannten Serverstatus zurück und führt dann die erforderliche Anzahl von Simulationen durch, um diesen Fehler zu beheben. Nur resimulierbare Systeme werden daran teilnehmen.Im Allgemeinen sollten Kunden-Resimulationen für Spieler nicht erkennbar sein. Wenn sie auftreten, werden alle Komponentenfelder reibungslos in neue Werte interpoliert, um mögliche „Zuckungen“ visuell auszugleichen. Es ist jedoch wichtig, die Anzahl so gering wie möglich zu halten.Schießen
Der Schaden für Spieler wird vollständig vom Server diktiert - Kunden können sich nicht auf eine so wichtige Mechanik verlassen, um die Wahrscheinlichkeit von Betrug zu verringern. Aber wie bei Bewegungen sollte das Schießen auf den Client so reaktionsschnell wie möglich und ohne Verzögerungen sein - der Spieler muss sofortiges Feedback in Form von Effekten und Geräuschen erhalten - Mündungsblitz, die Spur des Projektilfluges sowie die Auswirkungen des Projektils auf die Umgebung und andere Spieler.Daher wird der gesamte Zustand des Charakters, der mit dem Schießen verbunden ist, vom Kunden vorhergesagt - wie viele Runden sich im Laden befinden, die Streuung während des Schießens, die Verzögerung zwischen den Schüssen, die Zeit des letzten Schusses und so weiter. Auf dem Client befinden sich dieselben Systeme, die für die Bewegung von Shells verantwortlich sind wie auf dem Server. Auf diese Weise können Sie Aufnahmen auf dem Client simulieren, ohne auf die Ergebnisse ihrer Simulation auf dem Server warten zu müssen.Die Ballistik der Schalen selbst wird nicht vorhergesagt - da sie mit sehr hoher Geschwindigkeit fliegen und ihre Bewegung in der Regel in wenigen Frames beenden, hat die Schale bereits Zeit, einen Punkt auf der Welt zu erreichen und den Effekt zu verlieren, bevor wir die Simulationsergebnisse erhalten Dies ist ein Projektil vom Server (oder das Fehlen von Ergebnissen, wenn der Client aufgrund eines Fehlers das Projektil versehentlich abgefeuert hat).Das Arbeitsschema langsam fliegender Projektile ist etwas anders. Wenn ein Spieler eine Granate wirft, sich jedoch aufgrund der falschen Vorhersage herausstellt, dass die Granate nicht geworfen wurde, wird sie auf dem Client zerstört. Wenn ein Client die Zerstörung einer Granate falsch vorhergesagt hat (sie ist bereits auf dem Server explodiert, aber noch nicht auf dem Client), wird auch die Client-Granate zerstört. Alle Informationen zu den auf dem Client angezeigten Explosionen stammen vom Server, um Situationen zu vermeiden, in denen aufgrund eines Clientfehlers die Serverexplosion an einem Ort und auf dem Client an einem anderen Ort aufgetreten ist.Idealerweise möchte ich langsam fliegende Granaten in der Zukunft vollständig vorhersagen - nicht nur die Zeit des Lebens, sondern auch ihre Position.Verzögerungskompensation
Die Verzögerungskompensation ist eine Technik, mit der Sie den Effekt der Verzögerung zwischen dem Server und dem Client auf die Genauigkeit der Aufnahme ausgleichen können. In diesem Abschnitt gehe ich davon aus, dass das Schießen immer von "Hitscan" -Waffen stammt - d. H. Ein von einer Waffe abgefeuertes Projektil bewegt sich mit unendlicher Geschwindigkeit. Aber alles, was hier beschrieben wird, ist auch bei anderen Waffentypen von Bedeutung.Die folgenden Punkte machen es erforderlich, die Verzögerung beim Schießen auszugleichen:- Der vom Spieler kontrollierte Charakter befindet sich in Zukunft relativ zum Server (Vorhersage seines Status für eine bestimmte Anzahl von Frames);
- Folglich ist der Rest der Spieler in der Vergangenheit in Beziehung zu ihm;
- Beim Auslösen wird die entsprechende Aktion vom Client an den Server gesendet und auf denselben Frame angewendet, auf den sie auf den Client angewendet wurde (falls möglich).
Wenn wir annehmen, dass der Spieler auf einen Feind zielt, der auf den Kopf zuläuft, und den Schussknopf drückt, erhalten Sie das folgende Bild:- Auf dem Client: Der Schütze auf Bild N1 schießt auf den Kopf eines Feindes auf Bild N0 (N0 <N1).
- Auf dem Server: Der Schütze auf Bild N1 schießt auf den Kopf des Feindes, der sich ebenfalls auf Bild N1 befindet (auf dem Server sind alle gleichzeitig).
Das Ergebnis ist mit hoher Wahrscheinlichkeit ein Fehlschuss während eines Schusses. Da der Kunde auf der Grundlage seines Weltbildes zielt, das nicht mit dem Bild der Serverwelt übereinstimmt, um in den Feind zu gelangen, muss er auch mit Trefferwaffen auf ihn zielen, und die Entfernung, vor der er schießen muss, hängt von der Qualität der Verbindung mit ab Server. Dies ist, gelinde gesagt, keine gute Erfahrung für einen Schützen.Um dieses Problem zu beseitigen, wird eine Verzögerungskompensation verwendet. Das Schema ihrer Arbeit ist wie folgt:- Der Server verfügt über eine begrenzte Anzahl von Snapshots der Welt.
- Wenn sie abgefeuert werden, „rollen“ die Feinde (oder ein Teil der Feinde) so zurück, dass die Welt auf dem Server mit der Welt übereinstimmt, die der Client an sich gesehen hat - der Client befindet sich in der „Gegenwart“ (dem Moment des Schusses) und die Feinde befinden sich in der Vergangenheit.
- Die Mechanik der Treffererkennung funktioniert, Treffer werden aufgezeichnet.
- Die Welt kehrt in ihren ursprünglichen Zustand zurück.
Da das Bild der Welt auf dem Client auch von der Funktionsweise des Interpolationssystems abhängt, sendet der Client ihm zusätzliche Daten, um die Welt auf den genauesten Clientstatus auf dem Server zurückzusetzen, die Differenz - den Unterschied zwischen dem aktuellen Frame des Clients und dem Frame, für den er alle anderen Spieler sieht (im Moment sind dies zwei Bytes pro Bild) sowie der Zeitpunkt der Erzeugung der Aufnahmeeingabe relativ zum Bildanfang.Die Verzögerungskompensation erfolgt auf der Ebene eines separaten Moduls im Motor und ist nicht an ein bestimmtes Projekt gebunden. Aus Sicht des Entwicklers der Spielmechanik wird diese wie folgt verwendet:- "LagCompensationComponent" wird dem Player hinzugefügt und die Liste der Trefferfelder, die im Verlauf gespeichert werden sollen, wird gefüllt.
- Beim Schießen (oder bei anderen Mechaniken, die eine Kompensation erfordern - zum Beispiel bei Nahkampfangriffen) wird "LagCompensation :: invoke" aufgerufen, wobei der Funktor übergeben wird, der aus Sicht eines bestimmten Spielers in der "kompensierten" Welt ausgeführt wird. Es muss über alle erforderlichen Treffererkennungen verfügen.
Code mit einem Beispiel für die Verwendung der Verzögerungskompensation von Batle Prime beim Bewegen ballistischer Projektile:
Ich möchte auch darauf hinweisen, dass die Verzögerungskompensation ein Schema ist, das die Erfahrung des Schützen über die Erfahrung des Ziels stellt, auf das er schießt. Aus Sicht des Ziels kann der Feind zu einem Zeitpunkt in ihn eindringen, an dem er sich bereits hinter einem Hindernis befindet (eine häufige Beschwerde in Spielforen). Zu diesem Zweck verfügt die Verzögerungskompensation über eine begrenzte Anzahl von Frames, für die Ziele „abgepumpt“ werden können. Im Moment kann in Battle Prime ein Schütze mit einer RTT von etwa 400 Millisekunden Feinde bequem treffen. Wenn die RTT höher ist, müssen Sie vorausschießen.Ein Beispiel für ein Schießen ohne Entschädigung - Sie müssen vorausschießen, um den Feind stetig zu treffen:Und mit Entschädigung können Sie bequem direkt auf den Feind zielen:Unsere Build-Agenten führen außerdem regelmäßig Autotests durch, bei denen die Arbeit verschiedener Mechaniker überprüft wird. Unter diesen gibt es auch einen Autotest für die Zündgenauigkeit mit aktivierter Verzögerungskompensation. Im GIF unten wird dieser Test gezeigt - der Charakter schießt einfach auf den Kopf eines vorbeirennenden Feindes und zählt die Anzahl der Treffer auf ihn. Zum Debuggen werden zusätzlich die Hitboxen des Feindes angezeigt, die sich zum Zeitpunkt des Schusses auf dem Server befanden (in Weiß), und Hitboxen, die zur Treffererkennung in der kompensierten Welt (in Blau) verwendet wurden:
Ein weiterer Faktor, der die Genauigkeit der Aufnahme beeinflusst, ist die Position der Trefferfelder auf dem Charakter. Hitboxen hängen von Skelettanimationen ab und ihre Phasen sind derzeit in keiner Weise synchronisiert. Daher ist eine Situation möglich, in der sich Hitboxen zwischen Client und Server unterscheiden. Die Konsequenzen hängen von den Animationen selbst ab. Je größer der Bewegungsbereich innerhalb der Animation ist, desto größer ist der potenzielle Unterschied in der Position der Hitboxen zwischen Server und Client. In der Praxis ist ein solcher Unterschied für den Spieler nicht erkennbar und betrifft den Unterkörper stärker, was im Vergleich zum Oberkörper (Kopf, Rumpf, Arme) weniger kritisch ist. Dennoch möchte ich in Zukunft das Problem der Synchronisierung von Animationen zwischen Server und Client genauer behandeln.Fazit
In diesem Artikel habe ich versucht, die Grundlage zu beschreiben, auf der Battle Prime basiert - dies ist die Implementierung des ECS-Musters in der Blitz Engine sowie des Netzwerkmoduls, das für die Replikation, Client-Vorhersagen und verwandte Mechaniken verantwortlich ist. Trotz einiger Mängel (an deren Behebung wir weiter arbeiten) ist die Verwendung dieser Funktionalität jetzt einfach und bequem.Um das Gesamtbild von Battle Prime zu zeigen, musste ich eine Vielzahl von Themen ansprechen. Viele von ihnen werden möglicherweise in Zukunft separaten Artikeln gewidmet sein, in denen sie ausführlicher beschrieben werden!Das Spiel wird bereits in der Türkei und auf den Philippinen getestet.Unsere vorherigen Artikel finden Sie unter folgenden Links:- habr.com/de/post/461623
- habr.com/de/post/465343