Cartes hexagonales dans Unity: parties 1 à 3

image

D'un traducteur: cet article est le premier d'une série détaillée (27 parties) de tutoriels sur la création de cartes à partir d'hexagones. C'est ce qui devrait arriver à la toute fin des tutoriels.

Parties 1-3: maillage, couleurs et hauteurs de cellule

Parties 4-7: bosses, rivières et routes

Parties 8-11: eau, reliefs et remparts

Parties 12-15: sauvegarde et chargement, textures, distances

Parties 16-19: trouver le chemin, équipes de joueurs, animations

Parties 20-23: Brouillard de guerre, recherche cartographique, génération procédurale

Parties 24-27: cycle de l'eau, érosion, biomes, carte cylindrique

Partie 1: maillage à partir d'hexagones


Table des matières


  • Convertissez les carrés en hexagones.
  • Triangulez une grille d'hexagones.
  • Nous travaillons avec des coordonnées cubiques.
  • Nous interagissons avec les cellules de la grille.
  • Créez un éditeur en jeu.

Ce tutoriel est le début d'une série sur les cartes hexagonales. Les filets hexagonaux sont utilisés dans de nombreux jeux, en particulier dans les stratégies, notamment Age of Wonders 3, Civilization 5 et Endless Legend. Nous commencerons par les bases, nous ajouterons progressivement de nouvelles fonctionnalités et, par conséquent, nous créerons un relief complexe basé sur des hexagones.

Ce didacticiel suppose que vous avez déjà étudié la série Mesh Basics , qui commence par la grille procédurale . Il a été créé sur Unity 5.3.1. La série utilise plusieurs versions d'Unity. La dernière partie est réalisée sur Unity 2017.3.0p3.


Une simple carte d'hexagones.

À propos des hexagones


Pourquoi les hexagones sont-ils nécessaires? Si nous avons besoin d'une grille, il est logique d'utiliser des carrés. Les carrés sont vraiment faciles à dessiner et à positionner, mais ils ont aussi un inconvénient. Regardez un seul carré de la grille, puis ses voisins.


La place et ses voisins.

Au total, la place a huit voisins. Quatre d'entre eux peuvent être atteints en traversant le bord du carré. Ce sont des voisins horizontaux et verticaux. Les quatre autres peuvent être atteints en traversant le coin de la place. Ce sont des voisins en diagonale.

Quelle est la distance entre les centres des cellules de grille carrées adjacentes? Si la longueur du bord est 1, alors pour les voisins horizontaux et verticaux, la réponse est 1. Mais pour les voisins diagonaux, la réponse est √2.

La différence entre les deux types de voisins entraîne des difficultés. Si nous utilisons un mouvement discret, alors comment percevoir le mouvement le long de la diagonale? Dois-je le permettre du tout? Comment rendre l'apparence plus organique? Différents jeux utilisent différentes approches avec leurs avantages et leurs inconvénients. Une approche n'est pas du tout d'utiliser une grille carrée, mais d'utiliser des hexagones à la place.


Hexagone et ses voisins.

Contrairement à un carré, un hexagone n'a pas huit, mais six voisins. Tous ces voisins sont adjacents aux bords, il n'y a pas de voisins d'angle. Autrement dit, il n'y a qu'un seul type de voisins, ce qui simplifie beaucoup. Bien sûr, une grille d'hexagones est plus difficile à construire qu'un carré, mais nous pouvons le gérer.

Avant de commencer, nous devons déterminer la taille des hexagones. Soit la longueur du bord égale à 10 unités. Étant donné que l'hexagone se compose d'un cercle de six triangles équilatéraux, la distance entre le centre et n'importe quel angle est également de 10. Cette valeur détermine le rayon extérieur de la cellule hexagonale.


Le rayon extérieur et intérieur de l'hexagone.

Il y a aussi un rayon intérieur, qui est la distance du centre à chacun des bords. Ce paramètre est important car la distance entre les centres des voisins est égale à cette valeur multipliée par deux. Le rayon intérieur est  f r a c s q r t 3 2  du rayon extérieur, c'est-à-dire dans notre cas 5 s q r t 3  . Mettons ces paramètres dans une classe statique pour plus de commodité.

using UnityEngine; public static class HexMetrics { public const float outerRadius = 10f; public const float innerRadius = outerRadius * 0.866025404f; } 

Comment dériver la valeur du rayon interne?
Prenez l'un des six triangles d'un hexagone. Le rayon intérieur est égal à la hauteur de ce triangle. Cette hauteur peut être obtenue en divisant le triangle en deux triangles réguliers, puis en utilisant le théorème de Pythagore.

Par conséquent, pour la longueur de la côte e le rayon intérieur est  sqrte2(e/2)2= sqrt3e2/4=e sqrt3/2 environ0,886e .

Si nous le faisons déjà, déterminons les positions des six coins par rapport au centre de la cellule. Il convient de noter qu'il existe deux façons d'orienter l'hexagone: vers le haut avec un côté pointu ou plat. Nous allons mettre le coin. Commençons par cet angle et ajoutons le reste dans le sens horaire. Placez-les sur le plan XZ afin que les hexagones soient au sol.


Orientations possibles.

  public static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius) }; 

paquet d'unité

Maillage


Pour construire une grille d'hexagones, nous avons besoin de cellules de grille. Pour cela, créez le composant HexCell . Pour l'instant, laissez-le vide car nous n'utilisons pas encore de cellules données.

 using UnityEngine; public class HexCell : MonoBehaviour { } 

Pour commencer avec le plus simple, créez un objet plan par défaut, ajoutez-y un composant de cellule et transformez-le en préfabriqué.


Utilisation d'un avion comme préfabriqué d'une cellule hexagonale.

Entrons maintenant dans le filet. Créons un composant simple avec des variables communes de largeur, hauteur et préfabriqué de cellule. Ajoutez ensuite un objet de jeu avec ce composant à la scène.

 using UnityEngine; public class HexGrid : MonoBehaviour { public int width = 6; public int height = 6; public HexCell cellPrefab; } 


Objet maillé hexagonal.

Commençons par créer une grille régulière de carrés, car nous savons déjà comment procéder. Sauvegardons les cellules dans un tableau pour pouvoir y accéder.

Étant donné que les avions par défaut ont une taille de 10 par 10 unités, nous déplacerons chaque cellule de ce montant.

  HexCell[] cells; void Awake () { cells = new HexCell[height * width]; for (int z = 0, i = 0; z < height; z++) { for (int x = 0; x < width; x++) { CreateCell(x, z, i++); } } } void CreateCell (int x, int z, int i) { Vector3 position; position.x = x * 10f; position.y = 0f; position.z = z * 10f; HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.SetParent(transform, false); cell.transform.localPosition = position; } 


Grille carrée d'avions.

Nous avons donc obtenu une belle grille solide de cellules carrées. Mais quelle cellule est où? Bien sûr, c'est facile à vérifier, mais il y a des difficultés avec les hexagones. Il serait pratique de voir simultanément les coordonnées de toutes les cellules.

Affichage des coordonnées


Ajoutez du canevas à la scène en sélectionnant GameObject / UI / Canvas et faites-en un enfant de notre objet maillé. Étant donné que ce canevas est uniquement à titre d'information, nous supprimerons son composant raycaster. Vous pouvez également supprimer l'objet système d'événement, qui a été automatiquement ajouté à la scène, car pour l'instant nous n'en avons pas besoin.

Définissez le mode de rendu sur Espace mondial et faites-le pivoter de 90 degrés le long de l'axe X de sorte que le canevas recouvre la grille. Réglez le pivot et la position sur zéro. Donnez-lui un léger décalage vertical pour que son contenu soit au sommet. La largeur et la hauteur ne sont pas importantes pour nous, car nous organisons nous-mêmes le contenu. Nous pouvons définir la valeur sur 0 pour supprimer le grand rectangle dans la fenêtre de la scène.

Comme touche finale, augmentez les pixels dynamiques par unité à 10. Nous garantissons donc que les objets texte utiliseront une résolution de texture suffisante.



Toile pour les coordonnées de grille des hexagones.

Pour afficher les coordonnées, créez un objet Texte ( GameObject / UI / Text ) et transformez-le en préfabriqué. Centrez ses ancres et son pivot, définissez la taille sur 5 par 15. Le texte doit également être aligné horizontalement et verticalement au centre. Définissez la taille de police sur 4. Enfin, nous ne voulons pas utiliser le texte par défaut et nous n'utiliserons pas de texte enrichi . En outre, peu importe que la cible Raycast soit activée, car pour notre canevas, elle n'est toujours pas nécessaire.



Étiquette de cellule préfabriquée.

Maintenant, nous devons indiquer à la grille le canevas et le préfabriqué. Ajoutez au début de son script en using UnityEngine.UI; pour accéder UnityEngine.UI.Text type UnityEngine.UI.Text . Une étiquette préfabriquée a besoin d'une variable partagée et le canevas peut être trouvé en appelant GetComponentInChildren .

  public Text cellLabelPrefab; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); … } 


Balises préfabriquées de connexion.

Après avoir connecté le préfabriqué de l'étiquette, nous pouvons créer ses instances et afficher les coordonnées de la cellule. Entre X et Z, insérez un caractère de nouvelle ligne de sorte qu'ils apparaissent sur des lignes distinctes.

  void CreateCell (int x, int z, int i) { … Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.SetParent(gridCanvas.transform, false); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); label.text = x.ToString() + "\n" + z.ToString(); } 


Affichage des coordonnées.

Positions hexagonales


Maintenant que nous pouvons reconnaître visuellement chaque cellule, commençons à les déplacer. Nous savons que la distance entre les cellules hexagonales adjacentes dans la direction X est égale au double du rayon intérieur. Nous allons l'utiliser. De plus, la distance jusqu'à la prochaine rangée de cellules doit être 1,5 fois supérieure au rayon extérieur.


Géométrie des hexagones voisins.

  position.x = x * (HexMetrics.innerRadius * 2f); position.y = 0f; position.z = z * (HexMetrics.outerRadius * 1.5f); 


Nous appliquons des distances entre hexagones sans décalages.

Bien sûr, les rangées ordinales des hexagones ne sont pas situées exactement l'une au-dessus de l'autre. Chaque ligne est décalée le long de l'axe X de la valeur du rayon intérieur. Cette valeur peut être obtenue en ajoutant la moitié de Z à X, puis en multipliant par deux le rayon intérieur.

  position.x = (x + z * 0.5f) * (HexMetrics.innerRadius * 2f); 


Un placement correct des hexagones crée une grille en forme de losange.

Bien que ce soit ainsi que nous avons placé les cellules aux bonnes positions des hexagones, notre grille remplit désormais le losange plutôt que le rectangle. Nous sommes beaucoup plus à l'aise de travailler avec des grilles rectangulaires, alors remettons les cellules en service. Cela peut être fait en reculant une partie du décalage. Dans chaque deuxième ligne, toutes les cellules doivent être reculées d'un pas supplémentaire. Pour ce faire, nous devons soustraire le résultat de la division entière de Z par 2 avant de multiplier.

  position.x = (x + z * 0.5f - z / 2) * (HexMetrics.innerRadius * 2f); 


L'emplacement des hexagones dans une zone rectangulaire.

paquet d'unité

Rendu hexagonal


Après avoir placé les cellules correctement, nous pouvons procéder à l'affichage des vrais hexagones. Nous devons d'abord nous débarrasser des avions, nous allons donc supprimer tous les composants à l'exception de HexCell du HexCell cellule.


Il n'y a plus d'avions.

Comme dans les didacticiels de base sur le maillage , nous utilisons un seul maillage pour rendre le maillage entier. Cependant, cette fois, nous ne prédéfinissons pas le nombre de sommets et de triangles requis. Au lieu de cela, nous utiliserons des listes.

Créez un nouveau composant HexMesh qui HexMesh soin de notre maillage. Il faudra un filtre et un rendu de maillage, il a un maillage et des listes pour les sommets et les triangles.

 using UnityEngine; using System.Collections.Generic; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class HexMesh : MonoBehaviour { Mesh hexMesh; List<Vector3> vertices; List<int> triangles; void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); hexMesh.name = "Hex Mesh"; vertices = new List<Vector3>(); triangles = new List<int>(); } } 

Créez un nouvel objet enfant pour ce maillage avec ce composant. Il recevra automatiquement un rendu de maillage, mais aucun matériel ne lui sera attribué. Par conséquent, ajoutez-y le matériau par défaut.



Objet maillé hexagonal.

HexGrid pourra désormais récupérer son maillage hexagonal de la même manière qu'il a trouvé la toile.

  HexMesh hexMesh; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); … } 

Après le maillage éveillé, il doit ordonner au maillage de trianguler ses cellules. Nous devons être sûrs que cela se produira après le composant Awake du maillage hexadécimal. Puisque Start est appelé plus tard, insérez-y le code approprié.

  void Start () { hexMesh.Triangulate(cells); } 

Cette méthode HexMesh.Triangulate peut être appelée à tout moment, même si les cellules ont déjà été triangulées auparavant. Par conséquent, nous devons commencer par nettoyer les anciennes données. Lorsque vous parcourez toutes les cellules, nous les triangulons individuellement. Après avoir terminé cette opération, nous attribuons les sommets et triangles générés au maillage et terminons en recomptant les normales du maillage.

  public void Triangulate (HexCell[] cells) { hexMesh.Clear(); vertices.Clear(); triangles.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } hexMesh.vertices = vertices.ToArray(); hexMesh.triangles = triangles.ToArray(); hexMesh.RecalculateNormals(); } void Triangulate (HexCell cell) { } 

Puisque les hexagones sont composés de triangles, créons une méthode pratique pour ajouter un triangle basé sur les positions de trois sommets. Il ajoutera simplement des sommets dans l'ordre. Il ajoute également les indices de ces sommets pour former un triangle. L'index du premier sommet est égal à la longueur de la liste des sommets avant d'y ajouter de nouveaux sommets. N'oubliez pas cela lors de l'ajout de sommets.

  void AddTriangle (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); } 

Maintenant, nous pouvons trianguler nos cellules. Commençons par le premier triangle. Son premier pic est au centre de l'hexagone. Les deux autres sommets sont les premier et deuxième angles par rapport au centre.

  void Triangulate (HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.corners[0], center + HexMetrics.corners[1] ); } 


Le premier triangle de chaque cellule.

Cela a fonctionné, alors passons en boucle autour des six triangles.

  Vector3 center = cell.transform.localPosition; for (int i = 0; i < 6; i++) { AddTriangle( center, center + HexMetrics.corners[i], center + HexMetrics.corners[i + 1] ); } 

Les pics peuvent-ils être partagés?
Oui tu peux. En fait, nous pouvons faire encore mieux et utiliser seulement quatre au lieu de six triangles pour le rendu. Mais en abandonnant cela, nous simplifierons notre travail, et ce sera correct, car dans les tutoriels suivants tout devient plus compliqué. L'optimisation des sommets et des triangles à ce stade nous gênera à l'avenir.

Malheureusement, ce processus entraînera une IndexOutOfRangeException . C'est parce que le dernier triangle essaie d'obtenir le septième coin, qui n'existe pas. Bien sûr, il devrait revenir en arrière et utiliser comme dernier sommet du premier coin. Ou nous pouvons dupliquer le premier coin dans HexMetrics.corners afin de ne pas dépasser les limites.

  public static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius), new Vector3(0f, 0f, outerRadius) }; 


Hexagones complètement.

paquet d'unité

Coordonnées hexagonales


Regardons à nouveau les coordonnées des cellules, maintenant dans le contexte d'une grille d'hexagones. La coordonnée Z semble bien, et la coordonnée X zigzague. Il s'agit d'un effet secondaire du décalage de ligne pour couvrir une zone rectangulaire.


Coordonnées de décalage avec des lignes nulles en surbrillance.

Lorsque vous travaillez avec des hexagones, ces coordonnées de décalage ne sont pas faciles à gérer. Ajoutons une structure HexCoordinates , qui peut être utilisée pour convertir en un autre système de coordonnées. Rendons-le sérialisable pour qu'Unity puisse le stocker et il subira une recompilation en mode Play. Nous rendons également ces coordonnées immuables en utilisant les propriétés publiques en lecture seule.

 using UnityEngine; [System.Serializable] public struct HexCoordinates { public int X { get; private set; } public int Z { get; private set; } public HexCoordinates (int x, int z) { X = x; Z = z; } } 

Ajoutez une méthode statique pour créer un ensemble de coordonnées à partir des coordonnées de décalage ordinaires. Pour l'instant, nous allons simplement copier ces coordonnées.

  public static HexCoordinates FromOffsetCoordinates (int x, int z) { return new HexCoordinates(x, z); } } 

Nous ajoutons également des méthodes de conversion de chaînes pratiques. La méthode ToString renvoie par défaut un nom de type struct, ce qui ne nous est pas très utile. Nous le redéfinissons afin qu'il renvoie les coordonnées sur une seule ligne. Nous ajouterons également une méthode d'affichage des coordonnées sur des lignes distinctes, car nous utilisons déjà un tel schéma.

  public override string ToString () { return "(" + X.ToString() + ", " + Z.ToString() + ")"; } public string ToStringOnSeparateLines () { return X.ToString() + "\n" + Z.ToString(); } 

Maintenant, nous pouvons passer beaucoup de coordonnées à notre composant HexCell .

 public class HexCell : MonoBehaviour { public HexCoordinates coordinates; } 

Modifiez HexGrid.CreateCell afin qu'il puisse utiliser les nouvelles coordonnées.

  HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.SetParent(transform, false); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.SetParent(gridCanvas.transform, false); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z); label.text = cell.coordinates.ToStringOnSeparateLines(); 

Refaisons maintenant ces coordonnées X afin qu'elles soient alignées le long d'un axe droit. Cela peut être fait en annulant le décalage horizontal. Le résultat obtenu est généralement appelé coordonnées axiales.

  public static HexCoordinates FromOffsetCoordinates (int x, int z) { return new HexCoordinates(x - z / 2, z); } 



Coordonnées axiales.

Ce système de coordonnées bidimensionnelles nous permet de décrire séquentiellement le mouvement de déplacement dans quatre directions. Cependant, deux directions restantes nécessitent encore une attention particulière. Cela nous fait réaliser qu'il existe une troisième dimension. Et en fait, si nous devions inverser horizontalement la dimension de X, nous obtiendrions la dimension manquante de Y.


La mesure Y apparaît.

Puisque ces mesures de X et Y sont des copies miroir l'une de l'autre, l'ajout de leurs coordonnées donne toujours le même résultat si Z reste constant. En fait, si vous additionnez les trois coordonnées, nous obtiendrons toujours zéro. Si vous augmentez une coordonnée, vous devez réduire l'autre. Et en fait, cela nous donne six directions de mouvement possibles. Ces coordonnées sont généralement appelées cubiques, car elles sont tridimensionnelles et la topologie ressemble à un cube.

Puisque la somme de toutes les coordonnées est nulle, nous pouvons toujours obtenir l'une des coordonnées des deux autres. Comme nous stockons déjà les coordonnées X et Z, nous n'avons pas besoin de stocker les coordonnées Y.
Nous pouvons ajouter une propriété qui l'évalue si nécessaire et l'utiliser dans des méthodes de chaîne.

  public int Y { get { return -X - Z; } } public override string ToString () { return "(" + X.ToString() + ", " + Y.ToString() + ", " + Z.ToString() + ")"; } public string ToStringOnSeparateLines () { return X.ToString() + "\n" + Y.ToString() + "\n" + Z.ToString(); } 


Coordonnées cubiques.

Coordonnées de l'inspecteur


En mode Lecture, sélectionnez l'une des cellules de la grille. Il s'avère que l'inspecteur n'affiche pas ses coordonnées, seul le préfixe HexCell.coordinates est HexCell.coordinates .


L'inspecteur n'affiche pas les coordonnées.

Bien qu'il n'y ait pas de gros problème avec cela, ce serait bien d'afficher les coordonnées. Unity n'affiche pas les coordonnées car elles ne sont pas marquées comme des champs sérialisables. Pour les afficher, vous devez spécifier explicitement des champs sérialisables pour X et Z.

  [SerializeField] private int x, z; public int X { get { return x; } } public int Z { get { return z; } } public HexCoordinates (int x, int z) { this.x = x; this.z = z; } 


Les coordonnées X et Z sont maintenant affichées, mais elles peuvent être modifiées. Nous n'en avons pas besoin, car les coordonnées doivent être fixes. Il n'est également pas très bon qu'ils soient affichés les uns sous les autres.

Nous pouvons faire mieux: définir notre propre tiroir de propriétés pour le type HexCoordinates . Créez un script HexCoordinatesDrawer et collez-le dans le dossier Editor , car ce script est uniquement destiné à l'éditeur.

La classe doit étendre UnityEditor.PropertyDrawer et a besoin de l'attribut UnityEditor.CustomPropertyDrawer pour l'associer à un type approprié.

 using UnityEngine; using UnityEditor; [CustomPropertyDrawer(typeof(HexCoordinates))] public class HexCoordinatesDrawer : PropertyDrawer { } 

Les tiroirs de propriétés affichent leur contenu à l'aide de la méthode OnGUI . Cette méthode a permis de dessiner des données de propriété sérialisables et l'étiquette du champ auquel elles appartiennent à l'intérieur du rectangle d'écran.

  public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { } 

Nous extrayons les valeurs de x et z de la propriété, puis les utilisons pour créer un nouvel ensemble de coordonnées. Ensuite, dessinez l'étiquette GUI à la position sélectionnée en utilisant notre méthode HexCoordinates.ToString .

  public override void OnGUI ( Rect position, SerializedProperty property, GUIContent label ) { HexCoordinates coordinates = new HexCoordinates( property.FindPropertyRelative("x").intValue, property.FindPropertyRelative("z").intValue ); GUI.Label(position, coordinates.ToString()); } 


Coordonnées sans étiquette de préfixe.

Cela affichera les coordonnées, mais il nous manque maintenant le nom du champ. Ces noms sont généralement rendus à l'aide de la méthode EditorGUI.PrefixLabel . En prime, il renvoie un rectangle aligné qui correspond à l'espace à droite de cette étiquette.

  position = EditorGUI.PrefixLabel(position, label); GUI.Label(position, coordinates.ToString()); 


Coordonné avec une étiquette.

paquet d'unité

Cellules tactiles


Une grille d'hexagones n'est pas très intéressante si on ne peut pas interagir avec elle. L'interaction la plus simple est de toucher la cellule, alors ajoutons un support pour elle. Pour l'instant, nous HexGrid simplement ce code directement dans HexGrid . Quand il commencera à fonctionner, nous le déplacerons vers un autre endroit.

Pour toucher une cellule, vous pouvez émettre des rayons dans la scène à partir de la position du curseur de la souris. Nous pouvons utiliser la même approche que dans le didacticiel sur la déformation du maillage .

  void Update () { if (Input.GetMouseButton(0)) { HandleInput(); } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { TouchCell(hit.point); } } void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); Debug.Log("touched at " + position); } 

Jusqu'à présent, le code ne fait rien. Nous devons ajouter un collisionneur à la grille afin que le faisceau puisse entrer en collision avec quelque chose. Par conséquent, nous donnerons le HexMesh collisionneur HexMesh .

  MeshCollider meshCollider; void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); meshCollider = gameObject.AddComponent<MeshCollider>(); … } 

Une fois la triangulation terminée, affectez un maillage au collisionneur.

  public void Triangulate (HexCell[] cells) { … meshCollider.sharedMesh = hexMesh; } 

Ne pouvons-nous pas simplement utiliser le collisionneur de boîtes?
Nous le pouvons, mais cela ne correspondra pas exactement au contour de notre grille. Oui, et notre grille ne restera pas plate longtemps, mais c'est un sujet pour les futurs tutoriels.

Maintenant, nous pouvons toucher la grille! Mais quelle cellule touchons-nous? Pour le savoir, nous devons convertir la position tactile en coordonnées des hexagones. Cela fonctionne pour HexCoordinates , nous HexCoordinates déclarer qu'il a une méthode FromPosition statique.

  public void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); Debug.Log("touched at " + coordinates.ToString()); } 

Comment cette méthode déterminera-t-elle quelle coordonnée appartient à la position? On peut commencer par diviser x par la largeur horizontale de l'hexagone. Et puisque la coordonnée Y est une image miroir de la coordonnée X, un x négatif nous donne y.

  public static HexCoordinates FromPosition (Vector3 position) { float x = position.x / (HexMetrics.innerRadius * 2f); float y = -x; } 

Bien sûr, cela nous donnerait les coordonnées correctes si Z était nul. Nous devons à nouveau déplacer lorsque nous nous déplaçons le long de Z. Toutes les deux lignes, nous devons déplacer vers la gauche d'une unité.

  float offset = position.z / (HexMetrics.outerRadius * 3f); x -= offset; y -= offset; 

En conséquence, nos valeurs x et y se révèlent être des entiers au centre de chaque cellule. Par conséquent, en les arrondissant à l'entier le plus proche, nous devons obtenir les coordonnées. Nous calculons également Z et obtenons ainsi les coordonnées finales.

  int iX = Mathf.RoundToInt(x); int iY = Mathf.RoundToInt(y); int iZ = Mathf.RoundToInt(-x -y); return new HexCoordinates(iX, iZ); 

Les résultats semblent prometteurs, mais ces coordonnées sont-elles correctes? Avec une étude attentive, vous pouvez constater que parfois nous obtenons les coordonnées, dont la somme n'est pas égale à zéro! Allumons la notification pour nous assurer que cela se produit réellement.

  if (iX + iY + iZ != 0) { Debug.LogWarning("rounding error!"); } return new HexCoordinates(iX, iZ); 

Nous recevons effectivement des notifications. Comment pouvons-nous corriger cette erreur? Il n'apparaît qu'à côté des bords entre les hexagones. Autrement dit, l'arrondi des coordonnées cause des problèmes. Quelle coordonnée est arrondie dans la mauvaise direction? Plus nous nous éloignons du centre de la cellule, plus nous obtenons d'arrondi. Par conséquent, il est logique de supposer que la coordonnée arrondie est surtout incorrecte.

Ensuite, la solution consiste à supprimer les coordonnées avec le plus grand delta d'arrondi et à les recréer à partir des valeurs des deux autres. Mais comme nous n'avons besoin que de X et Z, nous ne pouvons pas nous embêter à recréer Y.

  if (iX + iY + iZ != 0) { float dX = Mathf.Abs(x - iX); float dY = Mathf.Abs(y - iY); float dZ = Mathf.Abs(-x -y - iZ); if (dX > dY && dX > dZ) { iX = -iY - iZ; } else if (dZ > dY) { iZ = -iX - iY; } } 

Coloriage - Hexagones


Maintenant que nous pouvons toucher la bonne cellule, le moment est venu pour une véritable interaction. Changeons la couleur de chaque cellule dans laquelle nous entrons. Ajoutez pour les HexGridcouleurs personnalisées de la cellule par défaut et de la cellule affectée.

  public Color defaultColor = Color.white; public Color touchedColor = Color.magenta; 


Sélection de la couleur des cellules.

Ajoutez au HexCellchamp de couleur général.

 public class HexCell : MonoBehaviour { public HexCoordinates coordinates; public Color color; } 

Attribuez-le HexGrid.CreateCellà la couleur par défaut.

  void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.color = defaultColor; … } 

Nous devons également ajouter HexMeshdes informations sur la couleur.

  List<Color> colors; void Awake () { … vertices = new List<Vector3>(); colors = new List<Color>(); … } public void Triangulate (HexCell[] cells) { hexMesh.Clear(); vertices.Clear(); colors.Clear(); … hexMesh.vertices = vertices.ToArray(); hexMesh.colors = colors.ToArray(); … } 

Maintenant, lors de la triangulation, nous devons ajouter des données de couleur à chaque triangle. À cet effet, nous allons créer une méthode distincte.

  void Triangulate (HexCell cell) { Vector3 center = cell.transform.localPosition; for (int i = 0; i < 6; i++) { AddTriangle( center, center + HexMetrics.corners[i], center + HexMetrics.corners[i + 1] ); AddTriangleColor(cell.color); } } void AddTriangleColor (Color color) { colors.Add(color); colors.Add(color); colors.Add(color); } 

Retour à HexGrid.TouchCell. Tout d'abord, convertissez les coordonnées de la cellule en l'index correspondant du tableau. Pour une grille carrée, ce serait juste X plus Z multiplié par la largeur, mais dans notre cas, nous devrons également ajouter un décalage de moitié Z. Ensuite, nous prenons la cellule, changeons sa couleur et triangulons à nouveau le maillage.

Avons-nous vraiment besoin de retrianguler tout le maillage?
, . . , , . .

  public void TouchCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; HexCell cell = cells[index]; cell.color = touchedColor; hexMesh.Triangulate(cells); } 

Bien que nous puissions maintenant coloriser les cellules, les changements visuels ne sont pas encore visibles. Cela est dû au fait que le shader n'utilise pas les couleurs de sommet par défaut. Nous devons écrire le nôtre. Créez un nouveau shader par défaut ( Actifs / Créer / Shader / Shader de surface par défaut ). Seuls deux changements doivent y être apportés. Tout d'abord, ajoutez des données de couleur à sa structure d'entrée. Deuxièmement, multipliez l'albédo par cette couleur. Nous ne nous intéressons qu'aux canaux RVB, car le matériau est opaque.

 Shader "Custom/VertexColors" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; float4 color : COLOR; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb * IN.color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" } 

Créez un nouveau matériau à l'aide de ce shader, puis faites en sorte que le maillage du maillage utilise ce matériau. Grâce à cela, les couleurs des cellules apparaîtront.


Cellules colorées.

J'obtiens d'étranges artefacts d'ombre!
Unity . , , Z-. .

paquet d'unité

Éditeur de carte


Maintenant que nous savons comment changer les couleurs, créons un éditeur de jeu simple. Cette fonctionnalité ne s'applique pas aux capacités HexGrid, nous allons donc TouchCellen faire une méthode générale avec un paramètre de couleur supplémentaire. Supprimez également le champ touchedColor.

 public void ColorCell (Vector3 position, Color color) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; HexCell cell = cells[index]; cell.color = color; hexMesh.Triangulate(cells); } 

Créez un composant HexMapEditoret déplacez-y les méthodes Updateet HandleInput. Ajoutez-y un champ commun pour faire référence à la grille d'hexagones, un tableau de couleurs et un champ privé pour suivre la couleur active. Enfin, ajoutez une méthode générale pour sélectionner une couleur et faites-lui d'abord sélectionner la première couleur.

 using UnityEngine; public class HexMapEditor : MonoBehaviour { public Color[] colors; public HexGrid hexGrid; private Color activeColor; void Awake () { SelectColor(0); } void Update () { if (Input.GetMouseButton(0)) { HandleInput(); } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { hexGrid.ColorCell(hit.point, activeColor); } } public void SelectColor (int index) { activeColor = colors[index]; } } 

Ajoutez un autre canevas, cette fois en conservant les paramètres par défaut. Ajoutez-y un composant HexMapEditor, définissez plusieurs couleurs et connectez-le à une grille d'hexagones. Cette fois, nous avons besoin d'un objet système d'événement, et il a été automatiquement créé à nouveau.


Éditeur de carte hexagonale en quatre couleurs.

Ajoutez un panneau au canevas pour stocker les sélecteurs de couleurs ( GameObject / UI / Panel ). Ajoutez son groupe de bascule ( Composants / UI / Basculer le groupe ). Rendez le panneau petit et placez-le dans le coin de l'écran.


Panneau de couleur avec groupe de bascule.

Remplissez maintenant le panneau avec des commutateurs pour chaque couleur ( GameObject / UI / Toggle ). Tant que nous ne nous soucions pas de créer une interface utilisateur complexe, une simple configuration manuelle suffit.



Un interrupteur pour chaque couleur.

Allumez le premier interrupteur. Intégrez également tous les commutateurs au groupe de bascule de sorte qu'un seul d'entre eux puisse être sélectionné à la fois. Enfin, connectez-les à SelectColorla méthode de notre éditeur. Cela peut être fait en utilisant le bouton d'interface utilisateur "+" de l'événement On Value Changed . Sélectionnez l'objet éditeur de carte, puis sélectionnez la méthode souhaitée dans la liste déroulante.


Le premier interrupteur.

Cet événement transmet un argument booléen qui détermine si le commutateur est activé à chaque fois qu'il est modifié. Mais on s'en fout. Au lieu de cela, nous devrons passer manuellement un argument entier correspondant à l'indice de couleur que nous voulons utiliser. Par conséquent, laissez la valeur 0 pour le premier commutateur, définissez la valeur 1 sur le second, et ainsi de suite.

Quand la méthode d'événement de commutation est-elle appelée?
. , , .

, , . , SelectColor . , .


Coloration en plusieurs couleurs.

Bien que l'interface utilisateur fonctionne, il y a un détail ennuyeux. Pour le voir, déplacez le panneau afin qu'il recouvre la grille d'hexagones. Lors du choix d'une nouvelle couleur, nous coloriserons également les cellules sous l'interface utilisateur. Autrement dit, nous interagissons simultanément avec l'interface utilisateur et avec la grille. Il s'agit d'un comportement indésirable.

Cela peut être corrigé en demandant au système d'événements s'il a déterminé l'emplacement du curseur sur un objet. Comme elle ne connaît que les objets d'interface utilisateur, cela nous dira que nous interagissons avec l'interface utilisateur. Par conséquent, nous devrons traiter nous-mêmes l'entrée uniquement si cela ne se produit pas.

 using UnityEngine; using UnityEngine.EventSystems; … void Update () { if ( Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject() ) { HandleInput(); } } 

paquet d'unité

Partie 2: Mélanger les couleurs des cellules


Table des matières


  • Connectez les voisins.
  • Interpole les couleurs entre les triangles.
  • Créez des zones de fusion.
  • Simplifiez la géométrie.

Dans la partie précédente, nous avons posé les bases de la grille et ajouté la possibilité de modifier les cellules. Chaque cellule a sa propre couleur unie et les couleurs aux bords des cellules changent considérablement. Dans ce tutoriel, nous allons créer des zones de transition qui mélangent les couleurs des cellules voisines.


Transitions en douceur entre les cellules.

Cellules voisines


Avant d'effectuer un lissage entre les couleurs des cellules, nous devons découvrir lesquelles des cellules sont adjacentes les unes aux autres. Chaque cellule a six voisins qui peuvent être identifiés dans les directions des points cardinaux. Nous obtiendrons les directions suivantes: nord-est, est, sud-est, sud-ouest, ouest et nord-ouest. Créons une énumération pour eux et collez-la dans un fichier de script distinct.

 public enum HexDirection { NE, E, SE, SW, W, NW } 

Qu'est-ce que l'énumération?
enum , . . , . , .

enum . , integer . , - , integer.


Six voisins, six directions.

Pour stocker ces voisins, ajoutez-les au HexCelltableau. Bien que nous puissions le rendre général, nous le rendrons plutôt privé et donnerons accès à des méthodes utilisant des instructions. Nous le rendons également sérialisable afin que les liens ne soient pas perdus lors de la recompilation.

  [SerializeField] HexCell[] neighbors; 

Avons-nous besoin de stocker toutes les connexions avec les voisins?
, . — , .

Maintenant, le tableau des voisins est affiché dans l'inspecteur. Puisque chaque cellule a six voisins, pour notre préfabriqué de cellule hexadécimale, nous définissons la taille du tableau 6.


Il y a de la place pour six voisins dans notre préfabriqué.

Ajoutons maintenant une méthode générale pour obtenir un voisin de cellule dans une direction. Étant donné que la valeur de direction est toujours comprise entre 0 et 5, nous n'avons pas besoin de vérifier si l'index se trouve dans le tableau.

  public HexCell GetNeighbor (HexDirection direction) { return neighbors[(int)direction]; } 

Ajoutez une méthode pour spécifier un voisin.

  public void SetNeighbor (HexDirection direction, HexCell cell) { neighbors[(int)direction] = cell; } 

Les relations des voisins sont bidirectionnelles. Par conséquent, lors de la définition d'un voisin dans une direction, il serait logique de définir immédiatement un voisin dans la direction opposée.

  public void SetNeighbor (HexDirection direction, HexCell cell) { neighbors[(int)direction] = cell; cell.neighbors[(int)direction.Opposite()] = this; } 


Voisins dans des directions opposées.

Bien sûr, cela suggère que nous pouvons demander des instructions pour le voisin opposé. Nous pouvons implémenter cela en créant une méthode d'extension pour HexDirection. Pour obtenir la direction opposée, vous devez ajouter à l'original 3. Cependant, cela ne fonctionne que pour les trois premières directions, pour le reste, vous devez soustraire 3.

 public enum HexDirection { NE, E, SE, SW, W, NW } public static class HexDirectionExtensions { public static HexDirection Opposite (this HexDirection direction) { return (int)direction < 3 ? (direction + 3) : (direction - 3); } } 

Qu'est-ce qu'une méthode d'extension?
— , - . — , , , . this . , .

? , , , . ? — . , , .

Connexion voisine


Nous pouvons initialiser le lien des voisins HexGrid.CreateCell. En parcourant les cellules ligne par ligne, de gauche à droite, nous savons quelles cellules ont déjà été créées. Ce sont les cellules avec lesquelles nous pouvons nous connecter.

Le plus simple est le composé E - W. La première cellule de chaque rangée n'a pas de voisin oriental. Mais toutes les autres cellules l'ont. Et ces voisins sont créés avant la cellule avec laquelle nous travaillons actuellement. Par conséquent, nous pouvons les connecter.


La connexion de E à W lors de la création des cellules.

  void CreateCell (int x, int z, int i) { … cell.color = defaultColor; if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); } Text label = Instantiate<Text>(cellLabelPrefab); … } 


Les voisins orientaux et occidentaux sont connectés.

Nous devons créer deux connexions bidirectionnelles supplémentaires. Puisque ce sont les connexions entre les différentes lignes du réseau, nous ne pouvons communiquer qu'avec la ligne précédente. Cela signifie que nous devons complètement sauter la première ligne.

  if (x > 0) { cell.SetNeighbor(HexDirection.W, cells[i - 1]); } if (z > 0) { } 

Comme les lignes sont en zigzag, elles doivent être traitées différemment. Voyons d'abord les lignes paires. Étant donné que toutes les cellules de ces lignes ont un voisin sur SE, nous pouvons les y connecter.


Connexion de NW à SE pour les lignes paires.

  if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); } } 

Que fait z & 1?
&& — , & — . , . 1, 1. , 10101010 & 00001111 00001010 .

. 0 1. 1, 2, 3, 4 1, 10, 11, 100. , 0.

, , . 0, .

Nous pouvons nous connecter avec des voisins sur le SW, à l'exception de la première cellule de chaque ligne qui ne l'a pas.


Connexion du NE au SW pour les lignes paires.

  if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); if (x > 0) { cell.SetNeighbor(HexDirection.SW, cells[i - width - 1]); } } } 

Les lignes impaires suivent la même logique, mais dans une image miroir. Après avoir terminé ce processus, tous les voisins de notre réseau sont connectés.

  if (z > 0) { if ((z & 1) == 0) { cell.SetNeighbor(HexDirection.SE, cells[i - width]); if (x > 0) { cell.SetNeighbor(HexDirection.SW, cells[i - width - 1]); } } else { cell.SetNeighbor(HexDirection.SW, cells[i - width]); if (x < width - 1) { cell.SetNeighbor(HexDirection.SE, cells[i - width + 1]); } } } 


Tous les voisins sont connectés.

Bien sûr, toutes les cellules ne sont pas connectées à exactement six voisins. Les cellules à la limite de la grille ont au moins deux et pas plus de cinq voisins. Et cela doit être pris en compte.


Voisins pour chaque cellule.

paquet d'unité

Mélange de couleurs


Le mélange des couleurs compliquera la triangulation de chaque cellule. Par conséquent, séparons le code de triangulation dans une partie distincte. Puisque nous avons maintenant des directions, utilisons-les plutôt que des indices numériques pour indiquer les pièces.

  void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } } void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.corners[(int)direction], center + HexMetrics.corners[(int)direction + 1] ); AddTriangleColor(cell.color); } 

Maintenant, lorsque nous utilisons des directions, il serait pratique d'obtenir des angles avec des directions et de ne pas effectuer la conversion en index.

  AddTriangle( center, center + HexMetrics.GetFirstCorner(direction), center + HexMetrics.GetSecondCorner(direction) ); 

Pour ce faire, vous devez ajouter à HexMetricsdeux méthodes statiques. En prime, cela nous permet de rendre privé le tableau des angles.

  static Vector3[] corners = { new Vector3(0f, 0f, outerRadius), new Vector3(innerRadius, 0f, 0.5f * outerRadius), new Vector3(innerRadius, 0f, -0.5f * outerRadius), new Vector3(0f, 0f, -outerRadius), new Vector3(-innerRadius, 0f, -0.5f * outerRadius), new Vector3(-innerRadius, 0f, 0.5f * outerRadius), new Vector3(0f, 0f, outerRadius) }; public static Vector3 GetFirstCorner (HexDirection direction) { return corners[(int)direction]; } public static Vector3 GetSecondCorner (HexDirection direction) { return corners[(int)direction + 1]; } 

Plusieurs couleurs sur un triangle


Jusqu'à présent, la méthode HexMesh.AddTriangleColorn'a qu'un seul argument de couleur. Il ne peut créer qu'un triangle de couleur unie. Créons une alternative qui prend en charge des couleurs distinctes pour chaque sommet.

  void AddTriangleColor (Color c1, Color c2, Color c3) { colors.Add(c1); colors.Add(c2); colors.Add(c3); } 

Maintenant, nous pouvons commencer à mélanger les couleurs! Commençons par utiliser simplement la couleur voisine pour les deux autres sommets.

  void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; AddTriangle( center, center + HexMetrics.GetFirstCorner(direction), center + HexMetrics.GetSecondCorner(direction) ); HexCell neighbor = cell.GetNeighbor(direction); AddTriangleColor(cell.color, neighbor.color, neighbor.color); } 

Malheureusement, cela conduit à NullReferenceException, parce que les cellules à la frontière n'ont pas six voisins. Que faire en cas de pénurie de voisin? Soyons pragmatiques et utilisons la cellule elle-même comme remplacement.

  HexCell neighbor = cell.GetNeighbor(direction) ?? cell; 

Que fait l'opérateur ??
null-coalescing operator. , a ?? ba != null ? a : b .

, - Unity . null . .


Il y a un mélange de couleurs, mais c'est mal fait.

Où sont passées les étiquettes de coordonnées?
, UI.

Moyenne des couleurs


Le mélange des couleurs fonctionne, mais les résultats sont manifestement faux. La couleur sur les bords des hexagones doit être la moyenne de deux cellules adjacentes.

  HexCell neighbor = cell.GetNeighbor(direction) ?? cell; Color edgeColor = (cell.color + neighbor.color) * 0.5f; AddTriangleColor(cell.color, edgeColor, edgeColor); 


Mélange sur les côtes.
Bien que nous mélangions sur les bords, nous obtenons toujours des bordures de couleurs nettes. Cela se produit car chaque sommet de l'hexagone est partagé par trois hexagones.


Trois voisins, quatre couleurs.

Cela signifie que nous devons également tenir compte des voisins dans les directions précédente et suivante. Autrement dit, nous obtenons quatre couleurs en deux ensembles de trois.

Ajoutons HexDirectionExtensionsdeux méthodes d'addition pour une transition pratique vers les directions précédente et suivante.

  public static HexDirection Previous (this HexDirection direction) { return direction == HexDirection.NE ? HexDirection.NW : (direction - 1); } public static HexDirection Next (this HexDirection direction) { return direction == HexDirection.NW ? HexDirection.NE : (direction + 1); } 

Maintenant, nous pouvons obtenir les trois voisins et effectuer un mélange à trois voies.

  HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddTriangleColor( cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, (cell.color + neighbor.color + nextNeighbor.color) / 3f ); 


Mélangez dans les coins.

Nous obtenons donc les transitions de couleurs correctes, à l'exception de la bordure de maillage. Les cellules de bordure ne sont pas cohérentes avec les couleurs des voisins manquants, donc ici nous voyons toujours des bordures nettes. Cependant, en général, notre approche actuelle ne donne pas de bons résultats. Nous avons besoin d'une meilleure stratégie.

paquet d'unité

Zones de mélange


Le mélange sur toute la surface de l'hexagone conduit à un chaos flou. Nous ne pouvons pas voir clairement les cellules individuelles. Les résultats peuvent être grandement améliorés en mélangeant uniquement à côté des bords des hexagones. Dans ce cas, la région intérieure des hexagones conservera une couleur unie.


Ombrage continu des cents avec zones de mélange.

Quelle devrait être la taille de la région solide par rapport à la région de mélange? Des distributions différentes conduisent à des résultats différents. Nous définirons cette zone comme une fraction du rayon extérieur. Soit égal à 75%. Cela nous mènera à deux nouvelles mesures, totalisant 100%.

  public const float solidFactor = 0.75f; public const float blendFactor = 1f - solidFactor; 

En créant ce nouveau facteur de remplissage solide, nous pouvons écrire des méthodes pour obtenir les angles des hexagones internes solides.

  public static Vector3 GetFirstSolidCorner (HexDirection direction) { return corners[(int)direction] * solidFactor; } public static Vector3 GetSecondSolidCorner (HexDirection direction) { return corners[(int)direction + 1] * solidFactor; } 

Maintenant, modifiez- HexMesh.Triangulatele pour qu'il utilise ces angles d'ombrage solides au lieu des angles d'origine. Nous laissons les couleurs les mêmes pour l'instant.

  AddTriangle( center, center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); 


Hexagones solides sans bords.

Triangulation des zones de mélange


Nous devons remplir l'espace vide que nous avons créé en réduisant les triangles. Dans chaque direction, cet espace a la forme d'un trapèze. Pour le couvrir, vous pouvez utiliser le quadrangle (quad). Par conséquent, nous allons créer des méthodes pour ajouter un quadrilatère et ses couleurs.


Côte trapézoïdale.

  void AddQuad (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); vertices.Add(v4); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 3); } void AddQuadColor (Color c1, Color c2, Color c3, Color c4) { colors.Add(c1); colors.Add(c2); colors.Add(c3); colors.Add(c4); } 

On le refait HexMesh.Triangulatepour que le triangle reçoive une couleur, et le quadrilatère réalise un mélange entre une couleur unie et les couleurs de deux angles.

  void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); AddTriangle(center, v1, v2); AddTriangleColor(cell.color); Vector3 v3 = center + HexMetrics.GetFirstCorner(direction); Vector3 v4 = center + HexMetrics.GetSecondCorner(direction); AddQuad(v1, v2, v3, v4); HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddQuadColor( cell.color, cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, (cell.color + neighbor.color + nextNeighbor.color) / 3f ); } 


Mélange avec des côtes trapézoïdales.

Ponts entre les côtes


L'image s'améliore, mais le travail n'est pas encore terminé. Le mélange des couleurs entre deux voisins est contaminé par les cellules voisines. Pour éviter cela, nous devons couper les coins du trapèze et le transformer en rectangle. Après cela, il créera un pont entre la cellule et son voisin, laissant des lacunes sur les côtés.


Le pont entre les côtes.

Nous pouvons trouver de nouvelles positions v3et v4, en commençant par v1et v2, puis en longeant le pont jusqu'au bord de la cellule. Quel sera le déplacement du pont? Nous pouvons le trouver en prenant le point médian entre les deux angles correspondants, puis en lui appliquant le coefficient de mélange. Cela fera l'affaire HexMetrics.

  public static Vector3 GetBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * 0.5f * blendFactor; } 

De retour à HexMesh, il sera désormais logique d'ajouter une option AddQuadColorqui ne nécessite que deux couleurs.

  void AddQuadColor (Color c1, Color c2) { colors.Add(c1); colors.Add(c1); colors.Add(c2); colors.Add(c2); } 

Changez-le Triangulatepour qu'il crée des ponts correctement mélangés entre voisins.

  Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; AddQuad(v1, v2, v3, v4); HexCell prevNeighbor = cell.GetNeighbor(direction.Previous()) ?? cell; HexCell neighbor = cell.GetNeighbor(direction) ?? cell; HexCell nextNeighbor = cell.GetNeighbor(direction.Next()) ?? cell; AddQuadColor(cell.color, (cell.color + neighbor.color) * 0.5f); 


Ponts correctement peints avec des espaces d'angle.

Combler les lacunes


Nous avons maintenant formé un espace triangulaire à la jonction de trois cellules. Nous avons obtenu ces lacunes en découpant les côtés triangulaires du trapèze. Récupérons ces triangles.

Considérons d'abord un triangle se connectant à un voisin précédent. Son premier sommet a une couleur de cellule. La couleur du deuxième pic sera un mélange de trois couleurs. Et le dernier pic aura la même couleur que le point au milieu du pont.

  Color bridgeColor = (cell.color + neighbor.color) * 0.5f; AddQuadColor(cell.color, bridgeColor); AddTriangle(v1, center + HexMetrics.GetFirstCorner(direction), v3); AddTriangleColor( cell.color, (cell.color + prevNeighbor.color + neighbor.color) / 3f, bridgeColor ); 


Presque tout est prêt.

Un autre triangle fonctionne de la même manière, sauf que le pont ne touche pas le troisième, mais le deuxième pic.

  AddTriangle(v2, v4, center + HexMetrics.GetSecondCorner(direction)); AddTriangleColor( cell.color, bridgeColor, (cell.color + neighbor.color + nextNeighbor.color) / 3f ); 


Coloration complète.

Nous avons maintenant de belles zones de mélange que nous pouvons donner à n'importe quelle taille. Les bords peuvent être flous ou nets comme vous le souhaitez. Mais vous pouvez voir que la fusion près de la bordure du maillage n'est toujours pas implémentée correctement. Et encore une fois, nous le laisserons pour plus tard, en nous concentrant sur un autre sujet pour l'instant.

Mais les transitions entre les couleurs sont toujours laides
. . .

paquet d'unité

Fusion de côtes


Jetez un œil à la topologie de notre grille. Quelles formes sont visibles ici? Si vous ne faites pas attention à la frontière, nous pouvons distinguer trois types de formulaires distincts. Il existe des hexagones monochromes, des rectangles bicolores et des triangles tricolores. Ces trois couleurs apparaissent à la jonction des trois cellules.


Trois structures visuelles.

Ainsi, tous les deux hexagones sont reliés par un pont rectangulaire. Et tous les trois hexagones sont reliés par un triangle. Cependant, nous effectuons une triangulation plus complexe. Maintenant, nous utilisons deux quadrangles au lieu d'un pour connecter une paire d'hexagones. Et pour relier les trois hexagones, nous utilisons six triangles. C'est trop redondant. De plus, si nous devions nous connecter directement à une forme, nous n'aurions pas besoin de faire la moyenne des couleurs. Par conséquent, nous pourrions nous débrouiller avec moins de complexité, moins de travail et moins de triangles.


Plus dur que nécessaire.

Pourquoi en avons-nous même besoin?
, . , , . , , . , , .

Pontage direct


Maintenant, nos ponts entre les nervures se composent de deux quadrangles. Pour les étendre à l'hexagone suivant, nous devons doubler la longueur du pont. Cela signifie que nous n'avons plus besoin de faire la moyenne de deux angles HexMetrics.GetBridge. Au lieu de cela, nous les ajoutons simplement puis multiplions par le facteur de mélange.

  public static Vector3 GetBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * blendFactor; } 


Les ponts s'étendent sur toute la longueur et se chevauchent.

Les ponts créent désormais des connexions directes entre les hexagones. Mais nous générons toujours deux quadrangles par connexion, un dans chaque direction. Autrement dit, un seul d'entre eux devrait créer des ponts entre deux cellules.

Simplifions d'abord notre code de triangulation. Nous supprimerons tout ce qui concerne les triangles de bords et le mélange des couleurs. Déplacez ensuite le code qui ajoute le quadrilatère du pont à la nouvelle méthode. Nous passons les deux premiers sommets à cette méthode pour ne pas avoir à les recalculer.

  void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.transform.localPosition; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); AddTriangle(center, v1, v2); AddTriangleColor(cell.color); TriangulateConnection(direction, cell, v1, v2); } void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { HexCell neighbor = cell.GetNeighbor(direction) ?? cell; Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; AddQuad(v1, v2, v3, v4); AddQuadColor(cell.color, neighbor.color); } 

Maintenant, nous pouvons facilement limiter la triangulation des composés. Pour commencer, nous ajouterons le pont uniquement lorsque vous travaillerez avec la connexion NE.

  if (direction == HexDirection.NE) { TriangulateConnection(direction, cell, v1, v2); } 


Les ponts sont uniquement en direction de NE.

Il semble que nous pouvons couvrir tous les composés en les triangulant uniquement dans les trois premières directions: NE, E et SE.

  if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); } 


Tous les ponts internes et les ponts aux frontières.

Nous avons couvert toutes les connexions entre deux cellules voisines. Mais nous avons également obtenu des ponts menant de la cellule à nulle part. Débarrassons-nous d'eux, sortons TriangulateConnectionquand les voisins sont absents. Autrement dit, nous n'avons plus besoin de remplacer les voisins manquants par la cellule elle-même.

  void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } … } 


Seuls les ponts internes.

Articulations triangulaires


Nous devons maintenant refermer les espaces triangulaires. Faisons cela pour un triangle se connectant au prochain voisin. Et cela ne doit être fait à nouveau que lorsqu'un voisin existe.

  void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { AddTriangle(v2, v4, v2); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); } } 

Quelle sera la position du troisième pic? J'ai inséré en remplacement v2, mais c'est évidemment faux. Étant donné que chaque bord de ces triangles est connecté au pont, nous pouvons le trouver en marchant le long du pont jusqu'au prochain voisin.

  AddTriangle(v2, v4, v2 + HexMetrics.GetBridge(direction.Next())); 


Nous faisons à nouveau une triangulation complète.

Avons-nous fini? Pas encore, car maintenant nous créons des triangles qui se chevauchent. Étant donné que les trois cellules ont une connexion triangulaire commune, nous devons les ajouter pour seulement deux connexions. Par conséquent, NE et E. feront l'affaire.

  if (direction <= HexDirection.E && nextNeighbor != null) { AddTriangle(v2, v4, v2 + HexMetrics.GetBridge(direction.Next())); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); } 

paquet d'unité

Partie 3: hauteurs


Table des matières


  • Ajoutez la hauteur des cellules.
  • Triangulez les pentes.
  • Insérez les rebords.
  • Combinez corniches et falaises.

Dans cette partie du didacticiel, nous ajouterons la prise en charge de différents niveaux de hauteur et créerons des transitions spéciales entre eux.


Hauteurs et rebords.

Hauteur de cellule


Nous avons divisé notre carte en cellules distinctes couvrant une zone plate. Nous allons maintenant donner à chaque cellule son propre niveau de hauteur. Nous utiliserons des niveaux d'élévation discrets pour les stocker sous forme de champ entier dans HexCell.

  public int elevation; 

Quelle taille peut avoir chaque niveau de hauteur suivant? Nous pouvons utiliser n'importe quelle valeur, nous allons donc la définir comme une autre constante HexMetrics. Nous utiliserons un pas de cinq unités pour que les transitions soient clairement visibles. Dans un vrai jeu, j'utiliserais un pas plus petit.

  public const float elevationStep = 5f; 

Changer les cellules


Jusqu'à présent, nous ne pouvions changer que la couleur de la cellule, mais maintenant nous pouvons changer sa hauteur. Par conséquent, la méthode HexGrid.ColorCellne nous suffit pas. De plus, à l'avenir, nous pourrons ajouter d'autres options d'édition de cellules, nous avons donc besoin d'une nouvelle approche.

Renommer ColorCelldans GetCellet faire en sorte que ce sera plutôt revenir cellule de couleur de référence une cellule dans une position prédéterminée. Comme cette méthode ne change rien d'autre, nous devons immédiatement trianguler les cellules.

  public HexCell GetCell (Vector3 position) { position = transform.InverseTransformPoint(position); HexCoordinates coordinates = HexCoordinates.FromPosition(position); int index = coordinates.X + coordinates.Z * width + coordinates.Z / 2; return cells[index]; } 

L'éditeur s'occupe maintenant du changement de cellule. Une fois les travaux terminés, la grille doit être à nouveau triangulée. Pour ce faire, ajoutez une méthode générale HexGrid.Refresh.

  public void Refresh () { hexMesh.Triangulate(cells); } 

Changez HexMapEditorpour qu'il puisse travailler avec de nouvelles méthodes. Donnons-lui une nouvelle méthode EditCell, qui traitera toutes les modifications apportées à la cellule, après quoi elle mettra à jour la grille.

  void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCell(hexGrid.GetCell(hit.point)); } } void EditCell (HexCell cell) { cell.color = activeColor; hexGrid.Refresh(); } 

Nous pouvons modifier les hauteurs simplement en attribuant à la cellule souhaitée le niveau de hauteur souhaité.

  int activeElevation; void EditCell (HexCell cell) { cell.color = activeColor; cell.elevation = activeElevation; hexGrid.Refresh(); } 

Comme pour les couleurs, nous avons besoin d'une méthode pour définir le niveau de hauteur actif, que nous associerons à l'interface utilisateur. Pour sélectionner des valeurs dans l'intervalle de hauteur, nous utilisons le curseur. Puisque les curseurs fonctionnent avec float, notre méthode nécessite un paramètre de type float. Nous allons simplement le convertir en entier.

  public void SetElevation (float elevation) { activeElevation = (int)elevation; } 

Ajoutez un curseur sur la toile ( GameObject / Create / Slider ) et placez-le sous le panneau de couleurs. Nous le rendons vertical, de bas en haut, pour qu'il corresponde visuellement aux niveaux de hauteurs. Nous le limitons à des entiers et créons un intervalle approprié, par exemple de 0 à 6. Ensuite, nous attachons son événement On Value Changed à la méthode d' SetElevationobjet Hex Map Editor . La méthode doit être sélectionnée dans la liste dynamique pour être appelée avec la valeur du curseur.



Curseur de hauteur.

Visualisation de la hauteur


Lors du changement d'une cellule, nous définissons maintenant la couleur et la hauteur. Bien que dans l'inspecteur, nous puissions voir que la hauteur change réellement, le processus de triangulation l'ignore toujours.

Il nous suffit de changer la position locale verticale de la cellule lors du changement de hauteur. Pour plus de commodité, rendons la méthode HexCell.elevationprivée et ajoutons une propriété générale HexCell.Elevation.

  public int Elevation { get { return elevation; } set { elevation = value; } } int elevation; 

Nous pouvons maintenant modifier la position verticale de la cellule lors de la modification de la hauteur.

  set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; transform.localPosition = position; } 

Bien sûr, cela nécessite de petites modifications HexMapEditor.EditCell.

  void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation; hexGrid.Refresh(); } 


Cellules de hauteurs différentes.

Le collisionneur de mailles change-t-il pour s'adapter à la nouvelle hauteur?
Unity mesh collider null. , , null . . ( ) .

La hauteur des cellules est désormais visible, mais il y a deux problèmes. Tout d'abord. les étiquettes des cellules disparaissent sous les cellules surélevées. Deuxièmement, les connexions entre les cellules ignorent la hauteur. Corrigeons-le.

Modifier la position des étiquettes de cellule


Actuellement, les étiquettes d'interface utilisateur pour les cellules sont créées et placées une seule fois, après quoi nous les oublions. Pour mettre à jour leurs positions verticales, nous devons les suivre. Donnons à chacun un HexCelllien vers RectTransformses étiquettes d'interface utilisateur afin que vous puissiez le mettre à jour plus tard.

  public RectTransform uiRect; 

Attribuez-les à la fin HexGrid.CreateCell.

  void CreateCell (int x, int z, int i) { … cell.uiRect = label.rectTransform; } 

Nous pouvons maintenant développer la propriété HexCell.Elevationafin qu'elle modifie également la position de l'interface utilisateur de la cellule. Étant donné que le maillage de la toile des hexagones est tourné, les étiquettes doivent être déplacées dans le sens négatif le long de l'axe Z, et non dans le côté positif de l'axe Y.

  set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = elevation * -HexMetrics.elevationStep; uiRect.localPosition = uiPosition; } 


Tags avec hauteur.

Création de pistes


Maintenant, nous devons convertir les connexions de cellules plates en pentes. Cela se fait en HexMesh.TriangulateConnection. Dans le cas des connexions de bord, nous devons redéfinir la hauteur de l'autre extrémité du pont.

  Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep; 

Dans le cas des joints d'angle, nous devons faire de même avec le pont vers le prochain voisin.

  if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep; AddTriangle(v2, v4, v5); AddTriangleColor(cell.color, neighbor.color, nextNeighbor.color); } 


Connexion en tenant compte de la hauteur.

Nous avons maintenant un support pour les cellules à différentes hauteurs avec les articulations inclinées correctes entre elles. Mais ne nous arrêtons pas là. Nous rendrons ces pistes plus intéressantes.

paquet d'unité

Articulations nervurées avec rebords


Les pentes droites ne semblent pas très attrayantes. Nous pouvons les diviser en plusieurs étapes en ajoutant des étapes. Cette approche est utilisée dans le jeu Endless Legend.

Par exemple, nous pouvons insérer deux rebords sur chaque pente. En conséquence, une grande pente se transforme en trois petites, entre lesquelles se trouvent deux zones planes. Pour trianguler un tel schéma, nous devrons séparer chaque connexion en cinq étapes.


Deux corniches sur la pente.

Nous pouvons définir le nombre d'étapes pour la pente HexMetricset calculer le nombre d'étapes en fonction de cela.

  public const int terracesPerSlope = 2; public const int terraceSteps = terracesPerSlope * 2 + 1; 

Idéalement, nous pourrions simplement interpoler chaque étape le long de la pente. Mais ce n'est pas tout à fait trivial, car la coordonnée Y ne devrait changer qu'à des étapes impaires. Sinon, nous n'aurons pas de rebords plats. Ajoutons une méthode d'interpolation spéciale pour cela HexMetrics.

  public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { return a; } 

L'interpolation horizontale est simple si nous connaissons la taille de l'étape d'interpolation.

  public const float horizontalTerraceStepSize = 1f / terraceSteps; public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; ax += (bx - ax) * h; az += (bz - az) * h; return a; } 

Comment fonctionne l'interpolation entre deux valeurs?
a et b t . Quand t 0, a . 1, b . Quand t - 0 1, a et b . : (1t)a+tb .

, (1t)a+tb=ata+tb=a+t(ba) . a dans b (ba) . , .

Pour changer Y uniquement aux étapes impaires, nous pouvons utiliser ( s t e p + 1 ) / 2 . Si nous utilisons la division entière, cela transformera les séries 1, 2, 3, 4 en 1, 1, 2, 2.

  public const float verticalTerraceStepSize = 1f / (terracesPerSlope + 1); public static Vector3 TerraceLerp (Vector3 a, Vector3 b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; ax += (bx - ax) * h; az += (bz - az) * h; float v = ((step + 1) / 2) * HexMetrics.verticalTerraceStepSize; ay += (by - ay) * v; return a; } 

Ajoutons également une méthode pour interpoler les rebords des couleurs. Il suffit de les interpoler comme si les connexions étaient plates.

  public static Color TerraceLerp (Color a, Color b, int step) { float h = step * HexMetrics.horizontalTerraceStepSize; return Color.Lerp(a, b, h); } 

Triangulation


Comme la triangulation de la connexion de bord devient plus compliquée, nous supprimons le code correspondant HexMesh.TriangulateConnectionet le plaçons dans une méthode distincte. Dans les commentaires, je vais enregistrer le code source afin de pouvoir y faire référence à l'avenir.

  void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Elevation * HexMetrics.elevationStep; TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); // AddQuad(v1, v2, v3, v4); // AddQuadColor(cell.color, neighbor.color); … } void TriangulateEdgeTerraces ( Vector3 beginLeft, Vector3 beginRight, HexCell beginCell, Vector3 endLeft, Vector3 endRight, HexCell endCell ) { AddQuad(beginLeft, beginRight, endLeft, endRight); AddQuadColor(beginCell.color, endCell.color); } 

Commençons par la toute première étape du processus. Nous utiliserons nos méthodes d'interpolation spéciales pour créer le premier quad. Dans ce cas, une courte pente doit être créée, plus raide que celle d'origine.

  void TriangulateEdgeTerraces ( Vector3 beginLeft, Vector3 beginRight, HexCell beginCell, Vector3 endLeft, Vector3 endRight, HexCell endCell ) { Vector3 v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, 1); Vector3 v4 = HexMetrics.TerraceLerp(beginRight, endRight, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); } 


La première étape de la création d'un rebord.

Maintenant, nous allons immédiatement passer à la dernière étape, en sautant tout le reste. Cela complètera la connexion des bords, bien que jusqu'à présent avec une forme irrégulière.

  AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); AddQuad(v3, v4, endLeft, endRight); AddQuadColor(c2, endCell.color); 


La dernière étape de la création d'un rebord.

Des étapes intermédiaires peuvent être ajoutées via la boucle. À chaque étape, les deux derniers sommets précédents deviennent les nouveaux premiers. Il en va de même pour la couleur. Après avoir calculé les nouveaux vecteurs et couleurs, un autre quad est ajouté.

  AddQuad(beginLeft, beginRight, v3, v4); AddQuadColor(beginCell.color, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c2; v3 = HexMetrics.TerraceLerp(beginLeft, endLeft, i); v4 = HexMetrics.TerraceLerp(beginRight, endRight, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); AddQuad(v1, v2, v3, v4); AddQuadColor(c1, c2); } AddQuad(v3, v4, endLeft, endRight); AddQuadColor(c2, endCell.color); 


Toutes les étapes intermédiaires.

Désormais, tous les joints de bord ont deux rebords, ou tout autre nombre que vous spécifiez dans HexMetrics.terracesPerSlope. Bien sûr, jusqu'à ce que nous ayons créé des rebords pour les joints d'angle, nous laisserons cela pour plus tard.


Tous les joints des bords ont des rebords.

paquet d'unité

Types de connexion


Convertir tous les joints de bord en rebords n'est pas une si bonne idée. Ils ne semblent bons que lorsque la différence de hauteur n'est que d'un niveau. Mais avec une différence plus grande, des rebords étroits sont créés avec de grands écarts entre eux, et cela n'a pas l'air très beau. De plus, nous n'avons pas besoin de créer de rebords pour tous les joints.

Formalisons cela et définissons trois types d'arêtes: un plan, une pente et une falaise. Créons une énumération pour cela.

 public enum HexEdgeType { Flat, Slope, Cliff } 

Comment déterminer à quel type de connexion nous avons affaire? Pour ce faire, nous pouvons ajouter à une HexMetricsméthode qui utilise deux niveaux de hauteur.

  public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { } 

Si les hauteurs sont les mêmes, nous aurons une nervure plate.

  public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { if (elevation1 == elevation2) { return HexEdgeType.Flat; } } 

Si la différence de niveaux est égale à un pas, alors c'est une pente. Peu importe si elle monte ou descend. Dans tous les autres cas, nous obtenons une pause.

  public static HexEdgeType GetEdgeType (int elevation1, int elevation2) { if (elevation1 == elevation2) { return HexEdgeType.Flat; } int delta = elevation2 - elevation1; if (delta == 1 || delta == -1) { return HexEdgeType.Slope; } return HexEdgeType.Cliff; } 

Ajoutons également une méthode pratique HexCell.GetEdgeTypepour obtenir le type de bord de cellule dans une certaine direction.

  public HexEdgeType GetEdgeType (HexDirection direction) { return HexMetrics.GetEdgeType( elevation, neighbors[(int)direction].elevation ); } 

N'avons-nous pas besoin de vérifier si un voisin existe dans cette direction?
, , . , NullReferenceException . , , - . , . .

, , , . - , NullReferenceException .

Créer des corniches uniquement pour les pentes


Maintenant que nous pouvons déterminer le type de connexion, nous pouvons décider d'insérer des corniches. Changez HexMesh.TriangulateConnectionpour qu'il crée des corniches uniquement pour les pentes.

  if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } // AddQuad(v1, v2, v3, v4); // AddQuadColor(cell.color, neighbor.color); 

À ce stade, nous pouvons décommenter le code précédemment commenté afin qu'il puisse gérer les avions et les coupures.

  if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, v2, v3, v4); AddQuadColor(cell.color, neighbor.color); } 


Les marches sont créées uniquement sur les pentes.

paquet d'unité

Rebords avec rebords


Les joints d'angle sont plus complexes que les joints de bord car ils ne sont pas impliqués dans deux, mais dans trois cellules. Chaque coin est relié à trois bords, qui peuvent être des plans, des pentes ou des falaises. Par conséquent, il existe de nombreuses configurations possibles. Comme pour les articulations des côtes, il est préférable d'ajouter la HexMeshtriangulation à la nouvelle méthode.

Notre nouvelle méthode nécessitera les sommets d'un triangle angulaire et les cellules connectées. Pour plus de commodité, organisons les connexions pour savoir quelle cellule a la plus petite hauteur. Après cela, nous pouvons commencer à travailler en bas à gauche et à droite.


Joint d'angle.

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } 

Maintenant, je TriangulateConnectiondois déterminer laquelle des cellules est la plus basse. Nous vérifions d'abord si la cellule triangulée est en dessous de ses voisins ou est au même niveau avec le plus bas. Si oui, alors nous pouvons l'utiliser comme cellule la plus basse.

  void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Elevation * HexMetrics.elevationStep; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } } } } 

Si la vérification la plus profonde échoue, cela signifie que le prochain voisin est la cellule la plus basse. Pour une orientation correcte, nous devons faire pivoter le triangle dans le sens antihoraire.

  if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } } 

Si le premier test échoue, vous devez comparer deux cellules voisines. Si le voisin de la nervure est le plus bas, vous devez tourner dans le sens horaire, sinon - dans le sens antihoraire.

  if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v2, cell, v4, neighbor, v5, nextNeighbor); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner(v4, neighbor, v5, nextNeighbor, v2, cell); } else { TriangulateCorner(v5, nextNeighbor, v2, cell, v4, neighbor); } 


Tournez dans le sens antihoraire, pas de tour, rotation dans le sens horaire.

Triangulation des pentes


Pour savoir comment trianguler un angle, nous devons comprendre de quels types d'arêtes il s'agit. Pour simplifier cette tâche, ajoutons à HexCellune autre méthode pratique pour reconnaître la pente entre deux cellules quelconques.

  public HexEdgeType GetEdgeType (HexCell otherCell) { return HexMetrics.GetEdgeType( elevation, otherCell.elevation ); } 

Nous utilisons cette nouvelle méthode HexMesh.TriangulateCornerpour déterminer les types de bords gauche et droit.

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } 

Si les deux côtes sont des pentes, nous aurons des rebords à gauche et à droite. De plus, la cellule inférieure étant la plus basse, nous savons que ces pentes montent. De plus, les cellules gauche et droite ont la même hauteur, c'est-à-dire que la connexion du bord supérieur est plate. Nous pouvons désigner ce cas comme «pente-pente-plan», ou MTP.


Deux pentes et un avion, SSP.

Nous vérifierons si nous sommes dans cette situation, et si c'est le cas, nous appellerons une nouvelle méthode TriangulateCornerTerraces. Après cela, nous reviendrons de la méthode. Insérez cette vérification avant l'ancien code de triangulation afin qu'il remplace le triangle d'origine.

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } } AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { } 

Puisque nous ne faisons rien à l'intérieur TriangulateCornerTerraces, certaines jonctions de coin avec deux pentes deviendront vides. Le fait que la connexion devienne vide ou non dépend de la cellule la plus basse.


Il y a un vide.

Pour combler le vide, nous devons connecter le rebord gauche et droit à travers un espace. L'approche ici est la même que pour joindre des bords, mais à l'intérieur d'un triangle tricolore au lieu d'un quadrilatère bicolore. Commençons à nouveau avec la première étape, qui est maintenant un triangle.

  void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); Color c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, 1); AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); } 


La première étape du triangle.

Et nous passons à nouveau directement à la dernière étape. Il s'agit du quadrilatère formant un trapèze. La seule différence avec les connexions de bord ici est que nous ne traitons pas avec deux, mais avec quatre couleurs.

  AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); AddQuad(v3, v4, left, right); AddQuadColor(c3, c4, leftCell.color, rightCell.color); 


La dernière étape du quadrilatère.

Toutes les étapes entre elles sont également des quadrangles.

  AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, i); AddQuad(v1, v2, v3, v4); AddQuadColor(c1, c2, c3, c4); } AddQuad(v3, v4, left, right); AddQuadColor(c3, c4, leftCell.color, rightCell.color); 


Toutes les étapes.

Deux variations de pente


Le cas à deux pentes a deux variations avec des orientations différentes, selon laquelle des cellules est le fond. Nous pouvons les trouver en vérifiant les combinaisons gauche-droite pour pente-plan et plan-pente.


ATP et MSS.

Si le bord droit est plat, nous devrions commencer à créer des rebords sur la gauche et non sur le bas. Si le bord gauche est plat, vous devez commencer par la droite.

  if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); return; } } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } } 

Pour cette raison, les corniches contournent les cellules sans interruption jusqu'à ce qu'elles atteignent la falaise ou la fin de la carte.


Des rebords solides.

paquet d'unité

Fusion des pentes et des falaises


Que diriez-vous de relier la pente et la falaise? Si nous savons que le bord gauche est une pente et le bord droit est une falaise, alors quel sera le bord supérieur? Il ne peut pas être plat, mais il peut s'agir d'une pente ou d'une falaise.




SOS et COO.

Ajoutons une nouvelle méthode pour gérer tous les cas de pente-falaise.

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { } 

Elle doit être appelée comme dernière option TriangulateCornerlorsque le bord gauche est une pente.

  if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); return; } TriangulateCornerTerracesCliff( bottom, bottomCell, left, leftCell, right, rightCell ); return; } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } } 

Comment triangulons-nous cela? Cette tâche peut être divisée en deux parties: inférieure et supérieure.

Partie inférieure


La partie inférieure a des rebords à gauche et une falaise à droite. Nous devons en quelque sorte les combiner. La façon la plus simple de le faire est de serrer les rebords pour qu'ils se rejoignent dans le coin droit. Cela soulèvera les rebords.


Compression des rebords.

Mais en fait, nous ne voulons pas qu'ils se rencontrent dans le coin droit, car cela interférerait avec les rebords qui peuvent exister ci-dessus. De plus, nous pouvons faire face à une très haute falaise, en raison de laquelle nous obtenons des triangles très tombants et minces. Au lieu de cela, nous les compresserons jusqu'à un point limite qui se trouve le long d'une falaise.


Compression à la frontière.

Positionnons le point limite un niveau au-dessus de la cellule du bas. Vous pouvez le trouver par interpolation basée sur la différence de hauteur.

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); } 

Pour nous assurer que nous l'avons correctement, nous couvrons toute la partie inférieure avec un triangle.

  float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); AddTriangle(begin, left, boundary); AddTriangleColor(beginCell.color, leftCell.color, boundaryColor); 


Triangle inférieur.

Après avoir placé la bordure au bon endroit, nous pouvons procéder à la triangulation des rebords. Reprenons seulement à partir de la première étape.

  float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); 


La première étape de compression.

Cette fois, la dernière étape sera également un triangle.

  AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); 


La dernière étape de compression.

Et toutes les étapes intermédiaires sont également des triangles.

  AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangle(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); 


Corniches compressées.

Ne pouvons-nous pas garder le niveau du rebord?
, , , . . , . .

Achèvement du coin


Après avoir terminé le bas, vous pouvez aller au sommet. Si le bord supérieur est une pente, nous devrons à nouveau relier les rebords et la falaise. Déplaçons donc ce code dans une méthode distincte.

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); TriangulateBoundaryTriangle( begin, beginCell, left, leftCell, boundary, boundaryColor ); } void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangle(begin, v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangle(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangle(v2, left, boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); } 

Maintenant, compléter le sommet sera facile. Si nous avons une pente, ajoutez le triangle pivoté de la bordure. Sinon, un simple triangle suffit.

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, right, b); Color boundaryColor = Color.Lerp(beginCell.color, rightCell.color, b); TriangulateBoundaryTriangle( begin, beginCell, left, leftCell, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, leftCell, right, rightCell, boundary, boundaryColor ); } else { AddTriangle(left, right, boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); } } 



Triangulation complète des deux parties.

Cas en miroir


Nous avons examiné des cas de «pente-falaise». Il y a aussi deux étuis à miroir, dont chacun a une falaise sur la gauche.


OSS et CCA.

Nous utiliserons l'approche précédente, avec de légères différences dues à un changement d'orientation. Nous le copions TriangulateCornerTerracesCliffet le modifions en conséquence.

  void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); Vector3 boundary = Vector3.Lerp(begin, left, b); Color boundaryColor = Color.Lerp(beginCell.color, leftCell.color, b); TriangulateBoundaryTriangle( right, rightCell, begin, beginCell, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, leftCell, right, rightCell, boundary, boundaryColor ); } else { AddTriangle(left, right, boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); } } 

Ajoutez ces cas à TriangulateCorner.

  if (leftEdgeType == HexEdgeType.Slope) { … } if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); return; } TriangulateCornerCliffTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); return; } 



OSS triangulé et CCA.

Falaises doubles


Les seuls cas non plans restants sont les cellules inférieures avec des falaises des deux côtés. Dans ce cas, la nervure supérieure peut être quelconque - plate, inclinée ou falaise. Nous ne nous intéressons qu'au cas «falaise-falaise-pente», car il n'aura que des corniches.

En fait, il existe deux versions différentes de la "falaise-falaise-pente", selon le côté le plus élevé. Ce sont des images miroir les unes des autres. Désignons-les comme OOSP et OOSL.




OOSP et OOSL.

Nous pouvons couvrir les deux cas en TriangulateCornerappelant des méthodes TriangulateCornerCliffTerraceset TriangulateCornerTerracesCliffavec différentes rotations de cellules.

  if (leftEdgeType == HexEdgeType.Slope) { … } if (rightEdgeType == HexEdgeType.Slope) { … } if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { if (leftCell.Elevation < rightCell.Elevation) { TriangulateCornerCliffTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerTerracesCliff( left, leftCell, right, rightCell, bottom, bottomCell ); } return; } 

Cependant, cela crée une triangulation étrange. C'est parce que maintenant nous triangulons de haut en bas. Pour cette raison, notre frontière est interpolée comme négative, ce qui est incorrect. La solution ici est de toujours avoir des interpolateurs positifs.

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } … } void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } … } 



OOSP et OOSL triangulés.

Balayer


Nous avons examiné tous les cas qui nécessitent une manipulation spéciale pour assurer la triangulation correcte des rebords.


Triangulation complète avec rebords.

Nous pouvons nettoyer un peu en nous TriangulateCornerdébarrassant des opérateurs returnet en utilisant des blocs à la place else.

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { HexEdgeType leftEdgeType = bottomCell.GetEdgeType(leftCell); HexEdgeType rightEdgeType = bottomCell.GetEdgeType(rightCell); if (leftEdgeType == HexEdgeType.Slope) { if (rightEdgeType == HexEdgeType.Slope) { TriangulateCornerTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); } else if (rightEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( left, leftCell, right, rightCell, bottom, bottomCell ); } else { TriangulateCornerTerracesCliff( bottom, bottomCell, left, leftCell, right, rightCell ); } } else if (rightEdgeType == HexEdgeType.Slope) { if (leftEdgeType == HexEdgeType.Flat) { TriangulateCornerTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerCliffTerraces( bottom, bottomCell, left, leftCell, right, rightCell ); } } else if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { if (leftCell.Elevation < rightCell.Elevation) { TriangulateCornerCliffTerraces( right, rightCell, bottom, bottomCell, left, leftCell ); } else { TriangulateCornerTerracesCliff( left, leftCell, right, rightCell, bottom, bottomCell ); } } else { AddTriangle(bottom, left, right); AddTriangleColor(bottomCell.color, leftCell.color, rightCell.color); } } 

Le dernier bloc elsecouvre tous les cas restants qui n'ont pas encore été couverts. Ces cas sont RFP (avion-avion-avion), OOP, LLC et LLC. Tous sont couverts par un triangle.


.

unitypackage

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


All Articles