Comment nous avons fait notre petite unité à partir de zéro



Notre société possède son propre moteur de jeu, qui est utilisé pour tous les jeux développés. Il fournit toutes les fonctionnalités de base importantes:

  • rendu
  • travailler avec SDK;
  • travailler avec le système d'exploitation;
  • avec le réseau et les ressources.

Cependant, il manquait à quoi Unity est si apprécié - un système pratique pour organiser les scènes et les objets de jeu, ainsi que des éditeurs pour eux.

Ici, je veux dire comment nous avons introduit toutes ces commodités et à quoi nous sommes arrivés.

Ce qui est maintenant


Nous avons maintenant un semblant de système de composants dans Unity avec tous les sous-systèmes et éditeurs importants. Cependant, comme nous sommes partis des besoins de nos projets spécifiques, il y a des différences assez importantes.

Nous avons des objets visuels qui sont stockés dans des scènes. Ces objets sont constitués de nœuds organisés dans une hiérarchie et chaque nœud peut avoir un certain nombre d'entités, telles que:

  • Transformer - transformation du nœud;
  • Composant - est engagé dans le rendu et il ne peut y en avoir qu'un ou pas du tout. Les composants sont un sprite, un maillage, une particule et d'autres entités pouvant s'afficher. L'équivalent le plus proche de Unity est Renderer;
  • Comportement - responsable du comportement, et il peut y en avoir plusieurs. Il s'agit d'un analogue direct de MonoBehaviour in Unity, toute logique y est écrite;
  • Le tri est une entité qui est responsable de l'ordre dans lequel les nœuds d'une scène sont affichés. Étant donné que notre système aurait dû être facile à intégrer dans des jeux déjà en cours d'exécution, avec la logique existante et diversifiée d'affichage des objets, il était nécessaire de pouvoir intégrer de nouvelles entités dans des anciennes. Le tri vous permet donc de transférer le contrôle de l'ordre d'affichage au code externe.

Comme avec Unity, les programmeurs créent leur composant, leur comportement ou leur tri. Pour ce faire, il suffit d'écrire une classe, de redéfinir les événements nécessaires (Update, OnStart, etc.) et de marquer les champs nécessaires de manière spéciale. Dans UnrealEngine, cela se fait avec des macros, et nous avons décidé d'utiliser des balises dans les commentaires.

/// @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; }; 

Plus loin dans la classe, en tenant compte des balises, tout le code sera généré, ce qui est nécessaire pour sauvegarder et charger des données, pour le travail des éditeurs, pour prendre en charge le clonage et d'autres petites fonctions.

La sérialisation et la génération automatiques des éditeurs sont prises en charge non seulement pour les entités stockées dans un objet visuel, mais également pour n'importe quelle classe. Pour ce faire, il suffit de l'hériter de la classe spéciale Serializable et de marquer les propriétés nécessaires avec des balises. Et si vous souhaitez que les instances de la classe soient des actifs complets (un analogue de ScriptableObject from Unity), la classe doit être héritée de la classe Asset.

En conséquence, la bibliothèque offre la possibilité de développer rapidement de nouvelles fonctionnalités. Et maintenant, une partie du travail sur le développement du jeu, par exemple, la création d'effets, l'interface utilisateur de mise en page, la conception de scènes de jeu, peut être transférée à des spécialistes qui peuvent mieux le gérer que les programmeurs.

Blocs principaux




Génération de code


Pour que de nombreux systèmes fonctionnent, vous devez écrire beaucoup de code de routine, ce qui est nécessaire en raison du manque de réflexion en C ++ ( réflexion - la possibilité d'accéder aux informations sur les types dans le code du programme). Par conséquent, nous générons la plupart de ce code technique.

Un générateur est un ensemble de scripts python qui analysent les fichiers d'en-tête et génèrent le code nécessaire sur leur base. Pour des paramètres de génération flexibles, des balises spéciales sont utilisées dans les commentaires.

Nous pouvons générer du code pour les sous-systèmes suivants:

  • Sérialisation - utilisée pour enregistrer / charger des données à partir du disque ou lors de la transmission sur un réseau. Seront examinés plus en détail ultérieurement.
  • Liaisons pour la bibliothèque de réflexions - utilisées pour afficher automatiquement l'éditeur aux données. Sera discuté dans le chapitre sur l'éditeur.
  • Code pour les entités de clonage - utilisé pour cloner des entités dans l'éditeur et dans le jeu.
  • Code pour notre réflexion d'exécution légère.

→ Un exemple du code généré pour une classe peut être trouvé ici.

Analyser c ++


Presque toutes les options pour résoudre le problème de l'analyse des fichiers d'en-tête ont conduit à l'analyse du code avec clang. Mais après les expériences, il est devenu clair que la vitesse d'une telle solution ne nous convenait pas du tout. De plus, le pouvoir fourni par clang n'était pas nécessaire pour nous.

Par conséquent, une autre solution a été trouvée: CppHeaderParser . Il s'agit d'une bibliothèque de fichiers uniques python qui peut lire les fichiers d'en-tête. Il est très primitif, ne suit pas #include, ignore les macros, n'analyse pas les caractères et fonctionne très rapidement.

Nous l'utilisons toujours à ce jour, cependant, nous avons dû apporter une bonne quantité de modifications pour corriger les bogues et étendre nos capacités, en particulier, le support des innovations de C ++ 17 a été ajouté.

Nous voulions éviter les malentendus liés à l'incertitude du statut de génération du code. Par conséquent, il a été décidé que la génération devrait se produire de manière entièrement automatique. Nous utilisons CMake, dans lequel la génération commence à chaque compilation (nous n'avons pas pu configurer la génération pour qu'elle démarre uniquement lorsque les dépendances changent). Pour que cela ne prenne pas beaucoup de temps et ne dérange pas, nous stockons un cache avec pour résultat d'analyser tous les fichiers et le contenu du répertoire. Par conséquent, le démarrage inactif de la génération de code ne prend que quelques secondes.

Générateur de code


Avec la génération, tout est plus simple. Il existe de nombreuses bibliothèques pour générer quoi que ce soit à partir d'un modèle. Nous avons choisi Templite + , car il est très petit, possède les fonctionnalités nécessaires et fonctionne correctement.

Il y avait deux approches de la génération. La première version contenait de nombreuses conditions, vérifications et autres codes, de sorte que les modèles eux-mêmes étaient minimes, et la plupart de la logique et du texte produits étaient en code python. C'était pratique, car en python, le code est plus pratique à écrire que dans les modèles, et il était facile de visser une logique arbitrairement délicate. Cependant, cela était également terrible, car le code python, mélangé à un grand nombre de lignes de code C ++, n'était pas pratique à lire ou à écrire. Les générateurs de python utilisés ont simplifié la situation, mais n'ont pas éliminé le problème dans son ensemble.

Par conséquent, la version actuelle de la génération est basée sur des modèles, et le code python prépare simplement les données nécessaires et maintenant il semble beaucoup mieux.

Sérialisation


Pour la sérialisation, différentes bibliothèques ont été envisagées: protobuf, FlexBuffers, céréales, etc.

Les bibliothèques avec génération de code (Protobuf, FlatBuffers et autres) ne convenaient pas, car nous avons des structures manuscrites et il n'y a aucun moyen d'intégrer les structures générées dans le code utilisateur. Et doubler le nombre de classes juste pour la sérialisation est trop de gaspillage.

La bibliothèque de céréales semblait être le meilleur candidat - syntaxe agréable, implémentation claire, il est pratique de générer du code de sérialisation. Cependant, son format binaire ne nous convenait pas, tout comme le format de la plupart des autres bibliothèques. Les exigences de format importantes étaient l'indépendance du matériel (les données doivent être lues indépendamment de l'ordre des octets et de la profondeur de bits) et le format binaire doit être pratique pour l'écriture à partir de python.

L'écriture d'un fichier binaire à partir de python était importante, car nous voulions avoir un script universel indépendant de la plate-forme et du projet qui convertirait les données d'une vue texte en une vue binaire. Par conséquent, nous avons écrit un script qui s'est avéré être un outil de sérialisation très pratique.

L'idée principale a été prise à partir de céréales, elle est basée sur des archives de base pour lire et écrire des données. Différents héritiers sont créés à partir d'eux qui implémentent l'enregistrement dans différents formats: xml, json, binaire. Et le code de sérialisation est généré par les classes et utilise ces archives pour écrire des données.



L'éditeur


Nous utilisons la bibliothèque ImGui pour les éditeurs, sur laquelle nous avons écrit toutes les fenêtres de l'éditeur principal: contenu de la scène, visionneuse de fichiers et d'actifs, inspecteur d'actifs, éditeur d'animation, etc.

Le code de l'éditeur principal est écrit à la main, mais pour afficher et modifier les propriétés de classes spécifiques, nous utilisons la bibliothèque rttr, le binning généré pour elle et le code d'inspecteur généralisé qui peut fonctionner avec rttr.

Bibliothèque de réflexion - rttr


Pour organiser la réflexion en C ++, la bibliothèque rttr a été choisie. Il ne nécessite aucune intervention dans les classes elles-mêmes, dispose d'une API pratique et compréhensible, prend en charge les collections et les wrappers sur les types (tels que les pointeurs intelligents) avec la possibilité d'enregistrer vos wrappers et vous permet de faire tout ce qui est nécessaire (créer des types, parcourir les membres de la classe, modifier les propriétés, méthodes d'appel, etc.).

Il vous permet également de travailler avec des pointeurs, comme avec des champs normaux, et utilise le modèle d'objet nul, ce qui simplifie considérablement son utilisation.

L'inconvénient de la bibliothèque est qu'elle est volumineuse et pas très rapide, nous ne l'utilisons donc que pour les éditeurs. Dans le code du jeu pour travailler avec les paramètres des objets, par exemple, pour un système d'animation, nous utilisons la bibliothèque de réflexion la plus simple de notre propre production.

La bibliothèque rttr nécessite d'écrire une liaison avec la déclaration de toutes les méthodes et propriétés de la classe. Cette liaison est générée à partir du code python pour toutes les classes qui nécessitent une prise en charge de l'édition. Et du fait que des métadonnées peuvent être ajoutées à rttr pour n'importe quelle entité, le générateur de code peut définir différents paramètres pour les membres de la classe: info-bulles, paramètres de limites de valeurs acceptables pour les champs numériques, un inspecteur spécial pour le champ, etc. Ces métadonnées sont utilisées dans l'inspecteur pour afficher l'interface d'édition. .

→ Un exemple de code pour déclarer une classe dans rttr peut être trouvé ici

Inspecteur


Le code des éditeurs eux-mêmes fonctionne très rarement directement avec rttr. Le calque le plus couramment utilisé est que l'objet est capable de dessiner un inspecteur ImGui pour lui. Il s'agit d'un code manuscrit qui fonctionne avec les données de rttr et qui en tire des contrôles ImGui.

Pour personnaliser l'affichage de l'interface d'édition des données, les métadonnées spécifiées lors de l'enregistrement dans rttr sont utilisées. Nous supportons tous les types primitifs, collections, il est possible de créer des objets stockés par valeur et par pointeur. Si le membre de la classe est un pointeur sur la classe de base, vous pouvez sélectionner un héritier spécifique lors de la création.

De plus, le code d'inspection prend en charge l'annulation des opérations - lors de la modification des valeurs, une commande est créée pour modifier les données, qui peuvent ensuite être annulées.

Bien que nous ne disposions pas d'un système pour déterminer les changements atomiques avec la possibilité de les visualiser et de les enregistrer. Cela signifie que nous ne prenons pas en charge l'enregistrement des propriétés modifiées de l'objet dans la scène et l'application de ces modifications après le chargement du préfabriqué. Et il n'y a pas non plus de création automatique de pistes animées lors du changement des propriétés d'un objet.

Windows et éditeurs


À l'heure actuelle, de nombreux sous-systèmes et éditeurs différents ont été créés sur la base de nos systèmes d'édition, de génération de code et de création d'actifs:

  • Le système d'interface de jeu offre une disposition flexible et pratique et comprend tous les éléments d'interface nécessaires. Un système de script visuel du comportement des fenêtres a été conçu pour elle.
  • Le système de changement d'état des animations est similaire à l'éditeur d'état des animations dans Unity, mais il diffère quelque peu par le principe de fonctionnement et a une application plus large.
  • Le concepteur de quêtes et d'événements vous permet de personnaliser de manière flexible les événements de jeu, les quêtes et les didacticiels, presque sans la participation de programmeurs.

Lors du développement de tous ces sous-systèmes et éditeurs, nous avons examiné de près Unity et Unreal Engine et avons essayé de tirer le meilleur parti d'eux. Et certains de ces sous-systèmes sont créés du côté des projets de jeux.

Pour résumer


En conclusion, je voudrais décrire comment le développement a été réalisé. La première version de travail a été créée et intégrée dans certains projets de jeu par quelques personnes en seulement deux mois. Il n'a pas encore eu de génération de code, et l'abondance d'éditeurs qui l'est maintenant. En même temps, c'était une version de travail, avec laquelle le mouvement en avant a commencé. Cela ne veut pas dire qu'à cette époque cela correspondait au principal vecteur de développement du moteur, tout reposait sur l'enthousiasme de plusieurs personnes et une compréhension claire de la nécessité et de la justesse de ce que nous faisions.

Tous les développements ultérieurs ont été réalisés de manière très active et évolutive, étape par étape, mais toujours en tenant compte des intérêts des projets de jeux. À l'heure actuelle, plus de dix personnes travaillent sur le développement de «notre petite unité» et le développement d'une nouvelle version n'est plus aussi rapide et rapide qu'au tout début.

Néanmoins, nous avons obtenu d'excellents résultats en seulement quelques années et ne nous arrêterons pas. Je vous souhaite d'avancer vers ce que vous pensez être juste et important pour vous et pour l'entreprise dans son ensemble.

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


All Articles