Plugin isométrique pour Unity3D


C'est une histoire sur la façon d'écrire un plugin pour Unity Asset Store , de tenter de résoudre les problèmes isométriques bien connus dans les jeux, et de gagner un peu d'argent de café à partir de cela, et aussi de comprendre à quel point l'éditeur Unity est extensible. Images, code, graphiques et pensées à l'intérieur.


Prologue


Donc, c'était une nuit quand j'ai découvert que je n'avais pratiquement rien à faire. L'année à venir n'était pas vraiment prometteuse dans ma vie professionnelle (contrairement à celle personnelle, cependant, mais c'est une toute autre histoire). Quoi qu'il en soit, j'ai eu cette idée d'écrire quelque chose d'amusant pour le bon vieux temps, qui serait assez personnel, quelque chose par moi-même, mais qui a quand même un petit avantage commercial (j'aime juste ce sentiment chaleureux quand votre projet est intéressant pour quelqu'un d'autre, sauf pour votre employeur). Et tout cela est allé de pair avec le fait que j'attendais depuis longtemps de vérifier les possibilités d'extension de l'éditeur Unity et de voir s'il y a du bon dans sa plate-forme pour vendre les propres extensions du moteur.


J'ai consacré une journée à l'étude du Asset Store: maquettes, scripts, intégrations avec différents services. Et d'abord, il semblait que tout avait déjà été écrit et intégré, ayant même un certain nombre d'options de différents niveaux de qualité et de détail, tout autant que les prix et le support. Donc, tout de suite, je l'ai réduit à:


  • code uniquement (après tout, je suis programmeur)
  • 2D uniquement (car j'adore la 2D et ils viennent de proposer un support prêt à l'emploi correct pour Unity)

Et puis je me suis souvenu du nombre de cactus que j'ai mangés et du nombre de souris mortes lorsque nous faisions un jeu isométrique auparavant. Vous ne croirez pas combien de temps nous avons tué sur la recherche de solutions viables et combien de copies nous avons cassées en essayant de trier cette isométrie et de la dessiner. Alors, luttant pour garder mes mains immobiles, j'ai cherché par différents mots clés et pas tellement de mots clés et je n'ai rien trouvé sauf un énorme tas d'art isométrique, jusqu'à ce que je décide finalement de faire un plugin isométrique à partir de zéro.


Fixer les objectifs


Le premier dont j'avais besoin était de décrire brièvement les problèmes que ce plugin était censé résoudre et quelle utilisation le développeur de jeux isométriques en ferait. Ainsi, les problèmes d'isométrie sont les suivants:


  • trier les objets par éloignement afin de les dessiner correctement
  • extension pour la création, le positionnement et le déplacement d'objets isométriques dans l'éditeur

Ainsi, avec les principaux objectifs formulés pour la première version, je me suis fixé un délai de 2-3 jours pour la première version provisoire. Cela ne pouvait donc pas être différé, voyez-vous, car l'enthousiasme est une chose fragile et si vous n'avez pas quelque chose de prêt dans les premiers jours, il y a de grandes chances que vous le ruiniez. Et les vacances du Nouvel An ne sont pas aussi longues que cela puisse paraître, même en Russie, et je voulais sortir la première version dans un délai d'environ dix jours.


Tri


Pour faire court, l'isométrie est une tentative faite par les sprites 2D pour ressembler à des modèles 3D. Cela se traduit bien sûr par des dizaines de problèmes. Le principal est que les sprites doivent être triés dans l'ordre dans lequel ils devaient être dessinés pour éviter les problèmes de chevauchement mutuel.



Sur la capture d'écran, vous pouvez voir comment c'est le sprite vert qui est dessiné en premier (2,1), puis le bleu va (1,1)


La capture d'écran montre le tri incorrect lorsque le sprite bleu est dessiné en premier

Dans ce cas simple, le tri ne sera pas un tel problème, et il y aura des options, par exemple:


  • tri par position de Y sur l'écran, qui est * (isoX + isoY) 0,5 + isoZ **
  • dessin à partir de la cellule de grille isométrique la plus éloignée de gauche à droite, de haut en bas [(3,3), (2,3), (3,2), (1,3), (2,2), (3, 1), ...]
  • et tout un tas d'autres façons intéressantes et pas vraiment intéressantes

Ils sont tous assez bons, rapides et fonctionnent, mais uniquement dans le cas de tels objets ou colonnes unicellulaires étendus dans la direction isoZ :) Après tout, j'étais intéressé par une solution plus courante qui fonctionnerait pour les objets étendus dans la direction d'une coordonnée, ou même les "clôtures" qui n'ont absolument aucune largeur, mais qui s'étendent dans le même sens que la hauteur nécessaire.



La capture d'écran montre la bonne façon de trier les objets étendus 3x1 et 1x3 avec des "clôtures" mesurant 3x0 et 0x3

Et c'est là que nos problèmes commencent et nous mettent en place où nous devons décider de la voie à suivre:


  • diviser les objets "multicellulaires" en objets "unicellulaires", c'est-à-dire les couper verticalement puis trier les bandes émergées


  • penser à la nouvelle méthode de tri, plus compliquée et intéressante



J'ai choisi la deuxième option, n'ayant aucune envie particulière d'entrer dans le traitement délicat de chaque objet, dans la découpe (même automatique), et l'approche particulière de la logique. Pour mémoire, ils ont utilisé la première voie dans quelques jeux célèbres comme Fallout 1 et Fallout 2 . Vous pouvez réellement voir ces bandes si vous entrez dans les données des jeux.


Ainsi, la deuxième option n'implique aucun critère de tri. Cela signifie qu'il n'y a pas de valeur pré-calculée permettant de trier les objets. Si vous ne me croyez pas (et je suppose que beaucoup de gens qui n'ont jamais travaillé avec l'isométrie ne le font pas), prenez un morceau de papier et dessinez de petits objets mesurant comme 2x8 et, par exemple, 2x2 . Si vous parvenez à trouver une valeur pour calculer sa profondeur et son tri, ajoutez simplement un objet 8x2 et essayez de les trier dans des positions différentes les unes par rapport aux autres.


Donc, il n'y a pas une telle valeur, mais nous pouvons toujours utiliser des dépendances entre elles (grosso modo, lesquelles se chevauchent qui) pour le tri topologique . Nous pouvons calculer les dépendances des objets en utilisant des projections de coordonnées isométriques sur l'axe isométrique.



La capture d'écran montre le cube bleu dépendant du rouge


La capture d'écran montre le cube vert dépendant du bleu

Un pseudocode pour la détermination des dépendances pour deux axes (le même fonctionne avec l'axe Z):


bool IsIsoObjectsDepends(IsoObject obj_a, IsoObject obj_b) { var obj_a_max_size = obj_a.position + obj_a.size; return obj_b.position.x < obj_a_max_size.x && obj_b.position.y < obj_a_max_size.y; } 

Avec une telle approche, nous construisons des dépendances entre tous les objets, en les passant récursivement et en marquant la coordonnée Z d'affichage. La méthode est assez universelle et, surtout, elle fonctionne. Vous pouvez lire la description détaillée de cet algorithme, par exemple ici ou ici . Ils utilisent également ce type d'approche dans la bibliothèque isométrique flash populaire ( as3isolib ).


Et tout était tout simplement génial, sauf que la complexité temporelle de cette approche est O (N ^ 2) car nous devons comparer chaque objet à tous les autres afin de créer les dépendances. J'ai laissé l'optimisation pour les versions ultérieures, après avoir ajouté uniquement un nouveau tri paresseux afin que rien ne soit trié jusqu'à ce que quelque chose bouge. Nous allons donc parler d'optimisation un peu plus tard.


Extension de l'éditeur


Désormais, j'avais les objectifs suivants:


  • le tri des objets devait fonctionner dans l'éditeur (pas seulement dans un jeu)
  • il devait y avoir un autre type de Gizmos-Arrow (flèches pour déplacer des objets)
  • facultativement, il y aurait un alignement avec des tuiles lorsque l'objet est déplacé
  • les tailles de tuiles seraient appliquées et définies automatiquement dans l'inspecteur du monde isométrique
  • Les objets AABB sont dessinés selon leurs tailles isométriques
  • sortie de coordonnées isométriques dans l'inspecteur d'objets, en changeant ce que nous changerions la position de l'objet dans le monde du jeu

Et tous ces objectifs ont été atteints. Unity permet vraiment d'étendre considérablement son éditeur. Vous pouvez ajouter de nouveaux onglets, fenêtres, boutons, nouveaux champs dans l'inspecteur d'objets. Si vous le souhaitez, vous pouvez même créer un inspecteur personnalisé pour un composant du type exact dont vous avez besoin. Vous pouvez également générer des informations supplémentaires dans la fenêtre de l'éditeur (dans mon cas, sur les objets AABB) et remplacer également les gizmos de déplacement standard des objets. Le problème du tri à l'intérieur de l'éditeur a été résolu via cette balise magique ExecuteInEditMode , qui permet d'exécuter des composants de l'objet en mode éditeur, c'est-à-dire de la même manière que dans un jeu.


Tout cela a été fait, bien sûr, non sans difficultés et astuces de toutes sortes, mais il n'y avait pas de problème unique sur lequel j'avais passé plus de quelques heures (Google, les forums et les communautés m'ont certainement aidé à résoudre tous les problèmes (qui n’ont pas été mentionnés dans la documentation).



La capture d'écran montre mes gadgets pour les objets en mouvement dans le monde isométrique

Relâchez


J'ai donc préparé la première version, j'ai fait la capture d'écran. J'ai même dessiné une icône et écrit une description. Il est temps. J'ai donc fixé un prix nominal de 5 $, téléchargé le plugin dans le magasin et attendu qu'il soit approuvé par Unity. Je n'ai pas beaucoup réfléchi au prix, car je ne voulais pas encore vraiment gagner beaucoup d'argent. Mon but était de savoir s'il y avait une demande générale et si c'était le cas, je voudrais l'estimer. Je voulais aussi aider les développeurs de jeux isométriques qui se sont en quelque sorte retrouvés absolument privés d'opportunités et d'ajouts.


En 5 jours assez douloureux (j'ai passé à peu près le même temps à écrire la première version, mais je savais ce que je faisais, sans plus me demander et sans trop réfléchir, ce qui m'a donné une vitesse plus élevée par rapport aux gens qui venaient juste de commencer à travailler avec l'isométrie) J'ai reçu une réponse d'Unity disant que le plugin était approuvé et que je pouvais déjà le voir dans le magasin, tout comme ses ventes nulles (jusqu'à présent). Il s'est enregistré sur le forum local, a intégré Google Analytics dans la page du plugin dans le magasin et s'est préparé à attendre que l'herbe pousse.


Il n'a pas fallu beaucoup de temps avant les premières ventes, tout comme les retours sur le forum et la boutique. Pour les jours restants du 12 janvier, des exemplaires de mon plugin ont été vendus, que j'ai considérés comme un signe d'intérêt public et que j'ai décidé de continuer.


Optimisation


Donc, j'étais mécontent de deux choses:


  • Complexité temporelle du tri - O (N ^ 2)
  • Problèmes avec la récupération de place et les performances générales

Algorithme


Ayant 100 objets et O (N ^ 2) j'avais 10 000 itérations à faire juste pour trouver des dépendances, et aussi je devrais les passer tous et marquer l'affichage Z pour le tri. Il aurait dû y avoir une solution pour cela. J'ai donc essayé un grand nombre d'options, je n'ai pas pu dormir en pensant à ce problème. Quoi qu'il en soit, je ne vais pas vous parler de toutes les méthodes que j'ai essayées, mais je vais décrire celle que j'ai trouvée la meilleure jusqu'à présent.


Tout d'abord, bien sûr, nous ne trions que les objets visibles. Ce que cela signifie, c'est que nous devons constamment savoir ce qui est dans notre tir. S'il y a un nouvel objet, nous devons l'ajouter dans le processus de tri, et si l'un des anciens est parti - ignorez-le. Désormais, Unity ne permet pas de déterminer la Boîte englobante de l'objet avec ses enfants dans l'arborescence de la scène. Passer sur les enfants (à chaque fois, d'ailleurs, car ils peuvent être ajoutés et supprimés) ne fonctionnerait pas - trop lent. Nous ne pouvons pas non plus utiliser OnBecameVisible et d'autres événements car ceux-ci ne fonctionnent que pour les objets parents. Mais nous pouvons obtenir tous les composants Renderer de l'objet nécessaire et de ses enfants. Bien sûr, cela ne semble pas être notre meilleure option, mais je n'ai pas pu trouver un autre moyen, même universel et acceptable par les performances.


 List<Renderer> _tmpRenderers = new List<Renderer>(); bool IsIsoObjectVisible(IsoObject iso_object) { iso_object.GetComponentsInChildren<Renderer>(_tmpRenderers); for ( var i = 0; i < _tmpRenderers.Count; ++i ) { if ( _tmpRenderers[i].isVisible ) { return true; } } return false; } 

Il y a une petite astuce d'utiliser la fonction GetComponentsInChildren qui permet d'obtenir des composants sans allocations dans le tampon nécessaire, contrairement à un autre qui retourne un nouveau tableau de composants


Deuxièmement, je devais encore faire quelque chose pour O (N ^ 2) . J'ai essayé un certain nombre de techniques de fractionnement d'espace avant de m'arrêter sur une simple grille bidimensionnelle dans l'espace d'affichage où je projette mes objets isométriques. Chacun de ces secteurs contient une liste d'objets isométriques qui le traversent. Donc, l'idée est simple: si les projections des objets ne sont pas croisées, il ne sert à rien de créer des dépendances entre les objets. Ensuite, nous passons sur tous les objets visibles et créons des dépendances uniquement dans les secteurs où cela est nécessaire, réduisant ainsi la complexité temporelle de l'algorithme et augmentant les performances. Nous calculons la taille de chaque secteur en moyenne entre les tailles de tous les objets. J'ai trouvé le résultat plus que satisfaisant.


Performance générale


Bien sûr, je pourrais écrire un article séparé à ce sujet ... D'accord, essayons de faire ce court. Tout d'abord, nous encaissons les composants (nous utilisons GetComponent pour les trouver, ce qui n'est pas rapide). Je recommande à tout le monde d'être vigilant lorsque vous travaillez avec tout ce qui a à voir avec Update . Vous devez toujours garder à l'esprit que cela se produit pour chaque image, vous devez donc être très prudent. N'oubliez pas non plus toutes les fonctionnalités intéressantes comme l' opérateur == personnalisé . Il y a beaucoup de choses à garder à l'esprit, mais à la fin, vous apprenez à connaître chacune d'entre elles dans le profileur intégré. Il est beaucoup plus facile de les mémoriser et de les utiliser :)


Vous comprenez également vraiment la douleur du ramasse-miettes. Besoin de performances supérieures? Ensuite, oubliez tout ce qui peut allouer de la mémoire, qui en C # (en particulier dans l'ancien compilateur Mono ) peut être fait par n'importe quoi, allant de foreach (!) Aux lambdas émergents, sans parler de LINQ qui vous est désormais interdit même dans les cas les plus simples. En fin de compte, au lieu de C # avec son sucre syntaxique, vous obtenez un semblant de C avec des capacités ridicules.


Ici, je vais donner quelques liens sur le sujet que vous pourriez trouver utile:
Part1 , Part2 , Part3 .


Résultats


Je n'ai jamais connu personne utilisant cette technique d'optimisation auparavant, donc j'étais particulièrement heureux de voir les résultats. Et si dans les premières versions il fallait littéralement 50 objets en mouvement pour que le jeu le transforme en diaporama, maintenant cela fonctionne plutôt bien même s'il y a 800 objets dans un cadre: tout tourne à vitesse maximale et re-trie juste pour 3-6 ms ce qui est très bon pour ce nombre d'objets en isométrie. De plus, après l'initialisation, il n'a presque pas alloué de mémoire pour une trame :)


D'autres opportunités


Après avoir lu les commentaires et les suggestions, j'ai ajouté quelques fonctionnalités dans les versions précédentes.


Mélange 2D / 3D


Mélanger 2D et 3D dans des jeux isométriques est une opportunité intéressante permettant de minimiser le dessin de différentes options de mouvement et de rotation (par exemple, des modèles 3D de personnages animés). Ce n'est pas vraiment difficile à faire, mais nécessite une intégration dans le système de tri. Il vous suffit d'obtenir une boîte englobante du modèle avec tous ses enfants, puis de déplacer le modèle le long de l'affichage Z de la largeur de la boîte.


 Bounds IsoObject3DBounds(IsoObject iso_object) { var bounds = new Bounds(); iso_object.GetComponentsInChildren<Renderer>(_tmpRenderers); if ( _tmpRenderers.Count > 0 ) { bounds = _tmpRenderers[0].bounds; for ( var i = 1; i < _tmpRenderers.Count; ++i ) { bounds.Encapsulate(_tmpRenderers[i].bounds); } } return bounds; } 

c'est un exemple de la façon dont vous pouvez obtenir la boîte englobante du modèle avec tous ses enfants



et voilà à quoi ça ressemble quand c'est fait

Paramètres isométriques personnalisés


C'est relativement simple. On m'a demandé de permettre de régler l'angle isométrique, le rapport hauteur / largeur, la hauteur des carreaux. Après avoir souffert de douleurs liées aux mathématiques, vous obtenez quelque chose comme ceci:




La physique


Et ici, cela devient plus intéressant. Étant donné que l'isométrie simule le monde 3D, la physique est également censée être tridimensionnelle, avec la hauteur et tout. J'ai trouvé ce tour fascinant. Je reproduis tous les composants de la physique, tels que Rigidbody , Collider et ainsi de suite, pour le monde isométrique. Selon ces descriptions et configurations, je fais la copie du monde physique tridimensionnel invisible en utilisant le moteur lui-même et PhysX intégré. Après cela, je prends les données de simulation calculées et j'obtiens ces bacl en dupliquant des composants pour le monde isométrique. Ensuite, je fais de même pour simuler des événements de déclenchement et de déclenchement.



La démo physique du jeu d'outils GIF

Épilogue et conclusions


Après avoir mis en œuvre toutes les suggestions du forum, j'ai décidé d'augmenter le prix jusqu'à 40 dollars, donc cela ne ressemblerait pas à un autre plugin bon marché avec cinq lignes de code :) Je serai très heureux de répondre aux questions et écoutez vos conseils. Comme c'est la première fois que j'écris quelque chose sur Habr, j'accueille toutes sortes de critiques, merci! Et maintenant, quelque chose que j'économisais pour la fin, les statistiques des ventes du mois:


Mois5 $40 $
Janvier120
Fevrier220
Mars170
Avril90
Mai90
Juin90
Juillet74
Août04
Septembre05

Lien vers la page Unity Asset Store: jeu d'outils isométrique 2.5D

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


All Articles