Bonjour, Habr! Récemment, nous avons sorti
notre jeu , que nous avons préparé depuis longtemps et au cours duquel un nombre considérable de sujets intéressants se sont accumulés qui méritent d'être partagés avec la communauté. Le sujet sera intéressant non seulement pour iOS et d'autres développeurs mobiles, mais aussi pour tous ceux qui sont intéressés par la façon dont toutes sortes de choses graphiques fonctionnent sous le capot, ainsi que tous les fans de stratégies 2D, que je suis moi-même depuis la troisième décennie.
Aujourd'hui, nous allons parler des nuances d'un sujet aussi important que les index z sur une surface isométrique (oui, tout n'est pas aussi simple qu'il y paraît à certains sages). Dans le monde 3D, nous avons, curieusement, trois coordonnées - x, y, z - qui déterminent complètement la position de l'objet dans l'espace. La tâche de déterminer la proximité des objets avec la caméra est également là, mais repose entièrement sur les épaules d'OpenGL. Le développeur ne fonctionne qu'avec des paramètres de haut niveau tels que la profondeur du tampon z, qui affectent les performances, mais sinon vous pouvez faire confiance à OpenGL comme une boîte noire - il a suffisamment d'informations.
Une situation complètement différente est observée dans notre monde «pseudo-3D» - chaque objet n'a que (x, y) - coordonnées et taille du sprite. La première tâche d'un programmeur lors de l'écriture d'un moteur est de déterminer quels objets doivent se chevaucher devant notre «caméra» virtuelle.
Synopsis
Les coordonnées SpriteKit (où (0; 0) est le centre du «monde» et Y monte) dans ce cas, cela ne nous intéresse pas du tout, car ils ne signifient rien dans notre «monde» isométrique avec vous, alors faisons une réservation - nous avons un champ en forme de diamant comme Age of Empires.

Une tuile avec les coordonnées (0; 0) est située dans le coin gauche du losange, l'abscisse X augmente "en bas" et "à droite", c'est-à-dire se rapproche de l'observateur, l'ordonnée Y augmente "vers le haut" et "vers la droite", c'est-à-dire diminue à mesure que vous vous approchez de l'observateur.
De plus, les rails doivent être «sous» le train, la fumée de la cheminée doit être «au-dessus» du train. Mais nous ne nous occuperons pas maintenant des «couches de l'être» - évidemment, rien ne nous empêche de faire autant de «tranches» isométriques que vous le souhaitez, en travaillant selon les mêmes règles. Nous supposons que dans une tuile, il y a toujours un objet - pour plus de clarté, plus n'est pas nécessaire.

Considérez les deux trains ci-dessus. De toute évidence, du point de vue de l'observateur, les wagons doivent être situés «en dessous» du train, c'est-à-dire leur indice z devrait être inférieur. En même temps, le train «supérieur» devrait «se chevaucher» avec le voisin, être «plus loin». Pouvons-nous, n'ayant que les coordonnées (x; y), construire une carte des z-indices pour chaque tuile?
Évidemment, oui, en utilisant la formule suivante (pseudocode a la swift):
zIndex = pos.x * field.size.width - pos.y
Ainsi, nous garantissons qu'à mesure que les ordonnées grandissent, les objets s'éloignent (-pos.y), ainsi qu'avec la croissance des abscisses, l'approche des objets (pos.x) et, surtout, tout objet qui a une abscisse de, disons, 44, est évidemment "plus proche" »Que tout objet ayant une abscisse 43. Pour ajouter ici« stratification »(rappelez-vous, les rails sous le train, fumée au-dessus de la cheminée), il suffit d'ajouter une« hauteur »constante de la couche:
zIndex = layerZIndex + pos.x * field.size.width - pos.y
Voilà, vous pouvez terminer l'article et vous féliciter pour les bases de la stéréométrie apprises en 10e année et démarrer la logique du jeu. Non? Si seulement! J'écrirais sur les choses évidentes! (Eh bien, comme c'est évident, quelques jours sont écrasés et cela)
Nous passons juste à la partie amusante, passant à autre chose.
Lutte pour la performance
Tout le monde, au moins une fois, a exécuté un projet de test pour SpriteKit (ou une noix de coco, ou tout autre moteur), a vu des nombres magiques - fps et nœuds.

Évidemment, fps est le nombre d'images par seconde, nœuds est le nombre de nœuds, principalement des sprites. Mais dans la pratique, la plupart de tous les fps sont définis non pas par le nombre de nœuds, mais par un autre paramètre, qui par défaut n'est pas affiché, mais qui peut également être affiché avec une seule ligne - le nombre de tirages redessinés.

Dans la même scène, comme vous le voyez maintenant, le nombre de nœuds est d'environ 6000, et le nombre de rendus est d'environ 120. C'est au zoom minimum (la caméra est aussi proche de la surface que possible), 1: 1.
Maintenant, déplaçons la caméra à la distance maximale (dans notre jeu, c'est 2,5: 1)

Nous avons changé l'échelle de seulement 2,5 fois (dans l'exemple, loin de tous les objets sont dessinés), et le nombre de tirages a augmenté de 5-6 fois avec le nombre de nœuds inchangé!
Bien sûr, le nombre de rendus affecte fps incommensurablement plus que le nombre abstrait de nœuds. SpriteKit ne dessine tout simplement pas les nœuds qui ne tombent pas dans la fenêtre (dans la caméra). La seule exception que j'ai trouvée jusqu'à présent concerne les émetteurs de particules, qui sont toujours dessinés, qu'ils soient visibles ou non.
Parlons maintenant de ce que signifie ce dessin "dessin". La carte vidéo possède tous les nœuds «couches», guidés par leurs z-index. Et cela passe en revue toute l'image encore et encore, du plus bas au plus haut. Le nombre de ces cycles de rendu est nul.
Vous comprenez maintenant que si vous dessinez chaque petit objet (et nous avons une grande carte, environ 6000 x 3000) avec son propre index z, cela ruinera les performances de n'importe quel téléphone.
Les problèmes sont mieux visibles sur les anciens 5 et 5, mais la présence de l'iPhone 10 ne garantit rien - avec la mauvaise approche, vous pouvez abandonner tout matériel puissant. Dans notre jeu, l'une des pierres angulaires était une extrême clarté. Au zoom le plus proche, les sprites en tête-à-tête correspondent aux pixels rétin. Je dois dire que dans la plupart des jeux mobiles, la résolution est de l'ordre de grandeur inférieure, donc les exigences ne sont pas si énormes, mais nous l'avons fait qualitativement, comme pour nous-mêmes ...
Il faut donc aller aux trucs.
- Tous les objets avec lesquels le joueur n'interagit pas et qui sont au même niveau dans la coordonnée X, peuvent généralement être fusionnés en un seul sprite. Pour une carte vidéo, il est beaucoup plus facile de dessiner un grand sprite que 10 petits. Par conséquent, les voies forestières entre les routes sont des sprites intégraux composés de plusieurs arbres. Et les arbres qui ne chevauchent pas les chemins et les autres arbres sont généralement cousus dans la carte. En alpha, au fait, il y avait pas mal de bugs lorsque le chêne centenaire poussait juste sous les rails d'un train ou sous un feu de circulation, alors testez soigneusement votre jeu afin de ne pas amuser les utilisateurs.
- Les objets qui ont un z-index sont dessinés dans l'ordre dans lequel ils apparaissent sur la carte vidéo. C'est-à-dire en ajoutant des objets «distants» avant ceux «proches», ils tomberont correctement, mais n'augmenteront pas le nombre de rendus de carte graphique.
Tout cela vous permet de réduire le nombre de tirages à certains moments, en fixant des fps même sur un ancien iPhone. J'ai dû leur limiter sévèrement certains effets, mais Apple n'a pas publié de mises à jour pour eux depuis un an maintenant - un péché se plaindra!
Élévation
Eh bien, tout, le moteur est prêt, pouvez-vous déjà démarrer quelque chose d'intéressant? Quelqu'un peut, mais c'est trop tôt pour nous. Après tout, le train devrait magnifiquement quitter le tunnel, et ici, tout n'est pas aussi simple qu'il y paraît.

Le train doit être situé «plus haut» que le mur «éloigné» du tunnel et «plus bas» que le toit du tunnel et les montagnes qui le suivent. C'est beau, après tout, quand la carte est à plusieurs niveaux, avec des changements d'altitude - encore une fois, nous ne faisons pas de bêtises sans âme, mais ce que nous aimons nous-mêmes!
Mais revenons aux détails - pour cela, la carte a été «coupée» comme suit.

La paroi intérieure du tunnel et tout le reste à gauche est inférieure et

le haut du tunnel avec les montagnes dans lesquelles il se jette. Ici, aucune génération procédurale d'indices z n'aidera, seulement un code dur biélorusse sévère.
L'habrayuzer attentif a remarqué dans la capture d'écran du jeu que près des tunnels, les arbres étaient soigneusement «tondus», exposant le sable de la plage immaculée. Cette faille apparemment vient de l'impossibilité fondamentale de réaliser de telles plantations d'arbres en 2D. Le train, sortant du tunnel, doit être délibérément «au-dessus» des arbres qu'il chevauche, en les recouvrant de lui-même. Mais ces mêmes arbres devraient chevaucher le toit du tunnel, sous lequel le train devrait appeler! Et le toit devrait être plus haut que le train, et donc dans un cercle, nous avons une contradiction logique ...
Pour une raison similaire, en raison de l'imperfection du moteur graphique, dans les anciens jeux comme Duke Nukem et Doom2, il n'y a pas de grandes différences dans les hauteurs et les planchers des bâtiments.
C'est pourquoi les arbres ne poussent pas près des tunnels.
J'espère que c'était intéressant, le
jouet en direct ici (gratuit à jouer) , le prochain article de la série sera consacré à une belle eau 2D réaliste, ne le manquez pas!
PS Au fait, une vidéo pour attirer l'attention peut être
visionnée sur youtube en qualité normale.
PPS Le jeu n'est pour l'instant disponible que dans la CEI, au Canada et en Irlande, si quelqu'un veut regarder depuis d'autres pays, envoyez un e-mail personnel avec appleId - je l'ajouterai à TestFlight