Wie wir unsere kleine Einheit von Grund auf neu gemacht haben



Unser Unternehmen verfügt über eine eigene Spiel-Engine, die für alle entwickelten Spiele verwendet wird. Es bietet alle wichtigen Grundfunktionen:

  • Rendern
  • mit SDK arbeiten;
  • mit dem Betriebssystem arbeiten;
  • mit Netzwerk und Ressourcen.

Es fehlte jedoch das, wofür Unity so geschätzt wird - ein praktisches System zum Organisieren von Szenen und Spielobjekten sowie Editoren für diese.

Hier möchte ich erzählen, wie wir all diese Annehmlichkeiten eingeführt haben und wozu wir gekommen sind.

Was ist jetzt?


Jetzt haben wir den Anschein eines Komponentensystems in Unity mit allen wichtigen Subsystemen und Editoren. Da wir jedoch von den Anforderungen unserer spezifischen Projekte ausgegangen sind, gibt es erhebliche Unterschiede.

Wir haben visuelle Objekte, die in Szenen gespeichert sind. Diese Objekte bestehen aus Knoten, die in einer Hierarchie organisiert sind, und jeder Knoten kann eine Reihe von Entitäten haben, z.

  • Transformation - Transformation des Knotens;
  • Komponente - ist mit dem Rendern beschäftigt und es kann nur eine oder gar keine geben. Komponenten sind Sprite, Mesh, Partikel und andere Objekte, die angezeigt werden können. Das nächste Äquivalent zu Unity ist Renderer.
  • Verhalten - verantwortlich für das Verhalten, und es kann mehrere geben. Dies ist ein direktes Analogon von MonoBehaviour in Unity, in das jede Logik geschrieben ist.
  • Sortieren ist eine Entität, die für die Reihenfolge verantwortlich ist, in der Knoten in einer Szene angezeigt werden. Da unser System mit der vorhandenen und vielfältigen Logik zur Anzeige von Objekten leicht in bereits laufende Spiele zu integrieren sein sollte, war es notwendig, neue Entitäten in alte zu integrieren. Durch Sortieren können Sie also die Kontrolle über die Anzeigereihenfolge auf den externen Code übertragen.

Wie bei Unity erstellen Programmierer ihre Komponenten, ihr Verhalten oder ihre Sortierung. Schreiben Sie dazu einfach eine Klasse, definieren Sie die erforderlichen Ereignisse (Update, OnStart usw.) neu und markieren Sie die erforderlichen Felder auf besondere Weise. In UnrealEngine erfolgt dies mit Makros, und wir haben uns entschieden, Tags in den Kommentaren zu verwenden.

/// @category(VSO.Basic) class SpriteComponent : public MaterialComponent { VISUAL_CLASS(MaterialComponent) public: /// @getter const std::string& GetId() const; /// @setter void SetId(const std::string& id); protected: void OnInit() override; void Draw() override; protected: /// @property Color _color = Color::WHITE; /// @property Sprite _sprite; }; 

Weiter in der Klasse wird unter Berücksichtigung der Tags der gesamte Code generiert, der zum Speichern und Laden von Daten, für die Arbeit von Redakteuren, zur Unterstützung des Klonens und anderer kleiner Funktionen erforderlich ist.

Die automatische Serialisierung und Generierung von Editoren wird nicht nur für Entitäten unterstützt, die in einem visuellen Objekt gespeichert sind, sondern auch für jede Klasse. Dazu reicht es aus, es von der speziellen Serializable-Klasse zu erben und die erforderlichen Eigenschaften mit Tags zu markieren. Wenn Instanzen der Klasse vollständige Assets sein sollen (ein Analogon zu ScriptableObject von Unity), sollte die Klasse von der Asset-Klasse geerbt werden.

Dadurch bietet die Bibliothek die Möglichkeit, schnell neue Funktionen zu entwickeln. Und jetzt kann ein Teil der Arbeit an der Entwicklung des Spiels, zum Beispiel das Erstellen von Effekten, das Layout der Benutzeroberfläche und das Entwerfen von Spielszenen, an Spezialisten übertragen werden, die besser damit umgehen können als Programmierer.

Hauptblöcke




Codegenerierung


Damit viele Systeme funktionieren, müssen Sie ziemlich viel Routinecode schreiben, was aufgrund der fehlenden Reflexion in C ++ erforderlich ist ( Reflexion - die Möglichkeit, auf Informationen über Typen im Programmcode zuzugreifen). Daher generieren wir den größten Teil dieses technischen Codes.

Ein Generator ist eine Reihe von Python-Skripten, die Header-Dateien analysieren und auf ihrer Basis den erforderlichen Code generieren. Für flexible Generierungseinstellungen werden in den Kommentaren spezielle Tags verwendet.

Wir können Code für die folgenden Subsysteme generieren:

  • Serialisierung - wird zum Speichern / Laden von Daten von der Festplatte oder beim Übertragen über ein Netzwerk verwendet. Wird später genauer betrachtet.
  • Bindungen für die Reflexionsbibliothek - Dient zum automatischen Anzeigen des Editors für die Daten. Wird im Kapitel über den Editor besprochen.
  • Code zum Klonen von Entitäten - wird zum Klonen von Entitäten im Editor und im Spiel verwendet.
  • Code für unsere leichte Laufzeitreflexion.

→ Ein Beispiel für den generierten Code für eine Klasse finden Sie hier.

Parsen von c ++


Fast alle Optionen zur Lösung des Problems beim Parsen von Header-Dateien führten zum Parsen von Code mit Clang. Nach den Experimenten wurde jedoch klar, dass die Geschwindigkeit einer solchen Lösung überhaupt nicht zu uns passte. Darüber hinaus wurde die Kraft, die Clang zur Verfügung stellte, für uns nicht benötigt.

Daher wurde eine andere Lösung gefunden: CppHeaderParser . Dies ist eine Python-Einzeldateibibliothek, die Header-Dateien lesen kann. Es ist sehr primitiv, folgt nicht #include, überspringt Makros, analysiert keine Zeichen und arbeitet sehr schnell.

Wir verwenden es bis heute, mussten jedoch eine ganze Reihe von Änderungen vornehmen, um Fehler zu beheben und unsere Funktionen zu erweitern. Insbesondere wurde die Unterstützung für Innovationen aus C ++ 17 hinzugefügt.

Wir wollten Missverständnisse im Zusammenhang mit der Unsicherheit des Status der Codegenerierung vermeiden. Daher wurde beschlossen, dass die Generierung vollautomatisch erfolgen soll. Wir verwenden CMake, bei dem die Generierung bei jeder Kompilierung beginnt (wir konnten die Generierung nicht so konfigurieren, dass sie nur startet, wenn sich die Abhängigkeiten ändern). Damit dies nicht viel Zeit in Anspruch nimmt und nicht stört, speichern wir einen Cache mit dem Ergebnis des Parsens aller Dateien und Verzeichnisinhalte. Infolgedessen dauert der Leerlaufstart der Codegenerierung nur wenige Sekunden.

Codegenerator


Mit der Generierung ist alles einfacher. Es gibt viele Bibliotheken, um etwas aus einer Vorlage zu generieren. Wir haben uns für Templite + entschieden , da es sehr klein ist, die erforderliche Funktionalität hat und ordnungsgemäß funktioniert.

Es gab zwei Ansätze zur Erzeugung. Die erste Version enthielt viele Bedingungen, Überprüfungen und anderen Code, sodass die Vorlagen selbst minimal waren und der größte Teil der Logik und des erzeugten Textes in Python-Code enthalten war. Es war praktisch, weil das Schreiben in Python-Code bequemer ist als in Vorlagen, und es war einfach, willkürlich knifflige Logik zu schrauben. Dies war jedoch auch schrecklich, da der Python-Code, gemischt mit einer großen Anzahl von Zeilen C ++ - Code, unpraktisch zu lesen oder zu schreiben war. Gebrauchte Python-Generatoren vereinfachten die Situation, beseitigten jedoch nicht das gesamte Problem.

Daher basiert die aktuelle Version der Generation auf Vorlagen, und Python-Code bereitet einfach die erforderlichen Daten vor und sieht jetzt viel besser aus.

Serialisierung


Bei der Serialisierung wurden verschiedene Bibliotheken berücksichtigt: Protobuf, FlexBuffers, Getreide usw.

Bibliotheken mit Codegenerierung (Protobuf, FlatBuffers und andere) passten nicht, da wir handgeschriebene Strukturen haben und es keine Möglichkeit gibt, die generierten Strukturen in Benutzercode zu integrieren. Und die Anzahl der Klassen nur für die Serialisierung zu verdoppeln, ist zu verschwenderisch.

Die Getreidebibliothek schien der beste Kandidat zu sein - nette Syntax, klare Implementierung, es ist praktisch, Serialisierungscode zu generieren. Das Binärformat passte jedoch nicht zu uns, ebenso wie das Format der meisten anderen Bibliotheken. Wichtige Formatanforderungen waren die Unabhängigkeit von der Hardware (Daten sollten unabhängig von Bytereihenfolge und Bittiefe gelesen werden) und das Binärformat sollte für das Schreiben aus Python geeignet sein.

Das Schreiben einer Binärdatei aus Python war wichtig, da wir ein plattformunabhängiges und projektunabhängiges universelles Skript haben wollten, das Daten von einer Textansicht in eine Binäransicht konvertiert. Aus diesem Grund haben wir ein Skript geschrieben, das sich als sehr praktisches Serialisierungswerkzeug herausstellte.

Die Hauptidee stammt aus Getreide, es basiert auf Basisarchiven zum Lesen und Schreiben von Daten. Daraus werden verschiedene Erben erstellt, die den Datensatz in verschiedenen Formaten implementieren: xml, json, binary. Der Serialisierungscode wird von Klassen generiert und verwendet diese Archive zum Schreiben von Daten.



Der Herausgeber


Wir verwenden die ImGui-Bibliothek für Editoren, in die wir alle Haupteditorfenster geschrieben haben: Szeneninhalte, Datei- und Asset-Viewer, Asset-Inspektor, Animationseditor usw.

Der Haupteditorcode wird von Hand geschrieben. Um jedoch die Eigenschaften bestimmter Klassen anzuzeigen und zu bearbeiten, verwenden wir die rttr-Bibliothek, die dafür generierte Gruppierung und den allgemeinen Inspektorcode, der mit rttr arbeiten kann.

Reflexionsbibliothek - rttr


Um die Reflexion in C ++ zu organisieren, wurde die rttr-Bibliothek ausgewählt. Es erfordert keine Eingriffe in die Klassen selbst, verfügt über eine praktische und verständliche API, unterstützt Sammlungen und Wrapper über Typen (z. B. intelligente Zeiger) und kann Ihre Wrapper registrieren. Außerdem können Sie alles Notwendige tun (Typen erstellen, Klassenmitglieder durchlaufen, Eigenschaften ändern, Aufrufmethoden usw.).

Sie können auch mit Zeigern arbeiten, wie mit regulären Feldern, und das Nullobjektmuster verwenden, was die Arbeit damit erheblich vereinfacht.

Das Minus der Bibliothek ist, dass sie sperrig und nicht sehr schnell ist, daher verwenden wir sie nur für Redakteure. Im Spielcode zum Arbeiten mit den Parametern von Objekten, beispielsweise für ein Animationssystem, verwenden wir die einfachste Reflexionsbibliothek unserer eigenen Produktion.

Die rttr-Bibliothek erfordert das Schreiben einer Bindung mit der Deklaration aller Methoden und Eigenschaften der Klasse. Diese Bindung wird aus Python-Code für alle Klassen generiert, die Bearbeitungsunterstützung benötigen. Aufgrund der Tatsache, dass rttr Metadaten für jede Entität hinzufügen kann, kann der Codegenerator verschiedene Einstellungen für Klassenmitglieder festlegen: QuickInfos, Parameter mit akzeptablen Wertegrenzen für numerische Felder, einen speziellen Inspektor für das Feld usw. Diese Metadaten werden im Inspektor zum Anzeigen der Bearbeitungsoberfläche verwendet .

→ Einen Beispielcode zum Deklarieren einer Klasse in rttr finden Sie hier

Inspektor


Der Code der Editoren selbst funktioniert sehr selten direkt mit rttr. Die am häufigsten verwendete Ebene ist, dass das Objekt einen ImGui-Inspektor dafür zeichnen kann. Dies ist handgeschriebener Code, der mit Daten von rttr arbeitet und ImGui-Steuerelemente dafür zeichnet.

Um die Anzeige der Datenbearbeitungsoberfläche anzupassen, werden die bei der Registrierung in rttr angegebenen Metadaten verwendet. Wir unterstützen alle primitiven Typen, Sammlungen, es ist möglich, Objekte zu erstellen, die nach Wert und Zeiger gespeichert sind. Wenn das Klassenmitglied ein Zeiger auf die Basisklasse ist, können Sie während der Erstellung einen bestimmten Erben auswählen.

Der Inspektorcode unterstützt auch das Abbrechen von Vorgängen. Beim Ändern von Werten wird ein Befehl zum Ändern der Daten erstellt, der dann zurückgesetzt werden kann.

Bisher haben wir kein System zur Bestimmung atomarer Veränderungen, mit dem sie angezeigt und gespeichert werden können. Dies bedeutet, dass wir das Speichern der geänderten Eigenschaften des Objekts in der Szene und das Anwenden dieser Änderungen nach dem Laden des Fertighauses nicht unterstützen. Außerdem werden beim Ändern der Eigenschaften eines Objekts keine automatisierten Animationsspuren erstellt.

Windows und Editoren


Derzeit wurden viele verschiedene Subsysteme und Editoren auf der Grundlage unserer Editoren, Codegenerierungs- und Asset-Erstellungssysteme erstellt:

  • Das Spielschnittstellensystem bietet ein flexibles und praktisches Layout und enthält alle erforderlichen Schnittstellenelemente. Für sie wurde ein System zur visuellen Skripterstellung des Fensterverhaltens erstellt.
  • Das System zum Umschalten des Animationsstatus ähnelt dem Statuseditor in Animationen in Unity, unterscheidet sich jedoch geringfügig im Funktionsprinzip und hat eine breitere Anwendung.
  • Mit dem Designer von Quests und Events können Sie Spielereignisse, Quests und Tutorials flexibel anpassen, fast ohne die Teilnahme von Programmierern.

Bei der Entwicklung all dieser Subsysteme und Editoren haben wir uns Unity , Unreal Engine genau angesehen und versucht, das Beste aus ihnen herauszuholen. Einige dieser Subsysteme werden auf der Seite von Spielprojekten erstellt.

Zusammenfassend


Abschließend möchte ich beschreiben, wie die Entwicklung durchgeführt wurde. Die erste Arbeitsversion wurde von ein paar Leuten in nur zwei Monaten erstellt und in einige Spielprojekte integriert. Es wurde noch kein Code generiert, und es gibt jetzt eine Fülle von Editoren. Gleichzeitig war es eine Arbeitsversion, mit der die Vorwärtsbewegung begann. Es kann nicht gesagt werden, dass dies zu dieser Zeit dem Hauptvektor der Motorentwicklung entsprach. Alles beruhte auf der Begeisterung mehrerer Menschen und einem klaren Verständnis der Notwendigkeit und Richtigkeit dessen, was wir getan haben.

Alle nachfolgenden Entwicklungen wurden sehr aktiv und evolutionär durchgeführt, Schritt für Schritt, jedoch immer unter Berücksichtigung der Interessen von Spielprojekten. Derzeit arbeiten mehr als zehn Menschen an der Entwicklung von „unserer kleinen Einheit“, und die Entwicklung einer neuen Version ist nicht mehr so ​​schnell und schnell wie zu Beginn.

Trotzdem haben wir in nur wenigen Jahren großartige Ergebnisse erzielt und werden nicht aufhören. Ich möchte, dass Sie zu dem übergehen, was Sie für richtig und wichtig für sich selbst und für das gesamte Unternehmen halten.

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


All Articles