Parties 1-3: maillage, couleurs et hauteurs de celluleParties 4-7: bosses, rivières et routesParties 8-11: eau, reliefs et rempartsParties 12-15: sauvegarde et chargement, textures, distancesParties 16-19: trouver le chemin, équipes de joueurs, animationsParties 20-23: Brouillard de guerre, recherche cartographique, génération procéduraleParties 24-27: cycle de l'eau, érosion, biomes, carte cylindriquePartie 4: Rugosités
Table des matières
- Échantillonnez la texture du bruit.
- Déplacez les sommets.
- Nous préservons la planéité des cellules.
- Subdivisez les bords des cellules.
Alors que notre grille était un modèle strict de nids d'abeilles. Dans cette partie, nous ajouterons des bosses pour rendre la carte plus naturelle.
Plus d'hexagones pairs.Le bruit
Pour ajouter des bosses, nous avons besoin de randomisation, mais pas de vrai hasard. Nous voulons que tout soit cohérent lors du changement de carte. Sinon, lorsque vous apportez des modifications, les objets sautent. Autrement dit, nous avons besoin d'une certaine forme de bruit pseudo-aléatoire reproductible.
Un bon candidat est le bruit de Perlin. Il est reproductible partout. En combinant plusieurs fréquences, il crée également un bruit qui peut varier considérablement sur de longues distances, mais reste presque le même sur de courtes distances. Grâce à cela, des distorsions relativement lisses peuvent être créées. Les points adjacents restent généralement proches et ne sont pas dispersés dans des directions opposées.
Nous pouvons générer du bruit Perlin par programmation. Dans le didacticiel sur le
bruit , j'explique comment procéder. Mais nous pouvons également échantillonner à partir d'une texture de bruit pré-générée. L'avantage d'utiliser la texture est qu'elle est plus simple et beaucoup plus rapide que le calcul du bruit multifréquence de Perlin. Son inconvénient est que la texture prend plus de mémoire et ne couvre qu'une petite zone de bruit. Par conséquent, il doit être connecté de manière transparente et suffisamment grand pour que la répétition ne soit pas frappante.
Texture de bruit
Nous utiliserons la texture, donc le tutoriel sur le
bruit est facultatif. Nous avons donc besoin d'une texture. Le voici:
Connectez de manière transparente la texture du bruit perlin.La texture montrée ci-dessus contient le bruit multifréquence parfaitement couplé de Perlin. Il s'agit d'une image en niveaux de gris. Sa valeur moyenne est de 0,5 et les valeurs extrêmes tendent à 0 et 1.
Mais attendez, il n'y a qu'une seule valeur pour chaque point. Si nous avons besoin d'une distorsion 3D, nous avons besoin d'au moins trois échantillons pseudo-aléatoires! Par conséquent, nous avons besoin de deux textures supplémentaires avec un bruit différent.
Nous pouvons les créer ou stocker différentes valeurs de bruit dans chacun des canaux de couleur. Cela nous permettra de stocker jusqu'à quatre modèles de bruit dans une seule texture. Voici cette texture.
Quatre en un.Comment créer une telle texture?J'ai utilisé
NumberFlow . Il s'agit de l'éditeur de texture procédurale que j'ai créé pour Unity.
Téléchargez cette texture et importez-la dans votre projet Unity. Puisque nous allons échantillonner la texture via du code, elle doit être lisible.
Réglez Type de texture sur
Avancé et activez la
lecture / écriture activée . Cela enregistrera les données de texture en mémoire et sera accessible à partir du code C #. Réglez
Format sur
Automatic Truecolor , sinon rien ne fonctionnera. Nous ne voulons pas que la compression de texture détruise notre modèle de bruit.
Vous pouvez désactiver
Generate Mip Maps , car nous n'en avons pas besoin.
Activez également le
contournement de l'échantillonnage sRGB . Nous n'en aurons pas besoin, mais ce sera le cas. Ce paramètre indique que la texture ne contient pas de données de couleur dans l'espace gamma.
Texture de bruit importée.
Quand l'échantillonnage sRGB est-il important?Si nous voulions utiliser une texture dans un shader, cela ferait une différence. Lorsque vous utilisez le mode de rendu linéaire, l'échantillonnage de la texture convertit automatiquement les données de couleur de la gamme en un espace colorimétrique linéaire. Dans le cas de notre texture de bruit, cela entraînera des résultats incorrects, nous n'en avons donc pas besoin.
Pourquoi mes paramètres d'importation de texture sont-ils différents?Ils ont été modifiés après la rédaction de ce didacticiel. Vous devez utiliser les paramètres de texture 2D par défaut, sRGB (Texture couleur) doit être désactivé et la compression doit être définie sur Aucune .
Échantillonnage du bruit
Ajoutons une fonctionnalité d'échantillonnage du bruit à
HexMetrics
afin de pouvoir l'utiliser n'importe où. Cela signifie que
HexMetrics
doit contenir une référence à la texture du bruit.
public static Texture2D noiseSource;
Comme il ne s'agit pas d'un composant, nous ne pouvons pas lui affecter de texture via l'éditeur. Par conséquent, en tant qu'intermédiaire, nous utilisons
HexGrid
. Puisque
HexGrid
agira en premier, ce sera bien si nous passons la texture au début de sa méthode
Awake
.
public Texture2D noiseSource; void Awake () { HexMetrics.noiseSource = noiseSource; … }
Cependant, cette approche ne survivra pas à la recompilation en mode Play. Les variables statiques ne sont pas sérialisées par le moteur Unity. Pour résoudre ce problème, réaffectez également la texture dans la méthode d'événement
OnEnable
. Cette méthode sera appelée après recompilation.
void OnEnable () { HexMetrics.noiseSource = noiseSource; }
Attribuez une texture de bruit.Maintenant que
HexMetrics
a accès à la texture, ajoutons-y une méthode d'échantillonnage de bruit pratique. Cette méthode prend position dans le monde et crée un vecteur 4D contenant quatre échantillons de bruit.
public static Vector4 SampleNoise (Vector3 position) { }
Les échantillons ont été créés en échantillonnant la texture en utilisant un filtrage bilinéaire, dans lequel les coordonnées du monde X et Z ont été utilisées comme coordonnées UV. Puisque notre source de bruit est bidimensionnelle, nous ignorons la troisième coordonnée du monde. Si la source de bruit était tridimensionnelle, nous utiliserions également la coordonnée Y.
En conséquence, nous obtenons une couleur qui peut être convertie en vecteur 4D. Une telle réduction peut être indirecte, c'est-à-dire que nous pouvons renvoyer la couleur directement, sans inclure explicitement
(Vector4)
.
public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear(position.x, position.z); }
Comment fonctionne le filtrage bilinéaire?Pour une explication des coordonnées UV et du filtrage de texture, voir le didacticiel
Rendu 2, Fondamentaux du shader .
paquet d'unitéMouvement du sommet
Nous allons déformer notre grille lisse de nids d'abeilles, en déplaçant individuellement chacun des sommets. Pour ce faire, ajoutons la méthode
Perturb
à
Perturb
. Il prend un point immobile et renvoie celui déplacé. Pour ce faire, il utilise un point non décalé lors de l'échantillonnage du bruit.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); }
Ajoutons simplement les échantillons de bruit X, Y et Z directement aux coordonnées des points correspondants et utilisons-les en conséquence.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x; position.y += sample.y; position.z += sample.z; return position; }
Comment changer rapidement
HexMesh
pour déplacer tous les sommets?
AddTriangle
modifiant chaque sommet lors de l'ajout de sommets à la liste dans les
AddQuad
AddTriangle
et
AddQuad
. Faisons-le.
void AddTriangle (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); … } void AddQuad (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); vertices.Add(Perturb(v4)); … }
Les quadrangles resteront-ils plats après avoir déplacé leurs sommets?Probablement pas. Ils sont constitués de deux triangles qui ne se trouveront plus dans le même plan. Cependant, comme ces triangles ont deux sommets communs, les normales de ces sommets seront lissées. Cela signifie que nous n'aurons pas de transitions nettes entre deux triangles. Si la distorsion n'est pas trop importante, alors nous percevrons toujours les quadrangles comme plats.
Les sommets sont déplacés ou non.Bien que les modifications ne soient pas très visibles, seules les étiquettes des cellules ont disparu. Cela est arrivé parce que nous avons ajouté des échantillons de bruit aux points, et ils sont toujours positifs. Par conséquent, en conséquence, tous les triangles se sont élevés au-dessus de leurs marques, les fermant. Il faut centrer les changements pour qu'ils se produisent dans les deux sens. Modifiez l'intervalle de l'échantillon de bruit de 0–1 à −1–1.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x * 2f - 1f; position.y += sample.y * 2f - 1f; position.z += sample.z * 2f - 1f; return position; }
Déplacement centré.L'ampleur (force) du déplacement
Il est maintenant évident que nous avons déformé la grille, mais l'effet est à peine perceptible. Le changement dans chaque dimension ne dépasse pas 1 unité. Autrement dit, le déplacement maximal théorique est de √3 ≈ 1,73 unités, ce qui se produira extrêmement rarement, voire pas du tout. Le rayon extérieur des cellules étant de 10 unités, les déplacements sont relativement faibles.
La solution consiste à ajouter un paramètre de
HexMetrics
à
HexMetrics
afin de pouvoir mettre à l'échelle les mouvements. Essayons d'utiliser la force 5. Dans ce cas, le déplacement maximum théorique sera de √75 ≈ 8,66 unités, ce qui est beaucoup plus notable.
public const float cellPerturbStrength = 5f;
Nous appliquons la force en la multipliant par des échantillons dans
HexMesh.Perturb
.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength; position.y += (sample.y * 2f - 1f) * HexMetrics.cellPerturbStrength; position.z += (sample.z * 2f - 1f) * HexMetrics.cellPerturbStrength; return position; }
Augmentation de la force.Échelle de bruit
Bien que la grille semble bonne avant le changement, tout peut mal tourner après l'apparition des rebords. Leurs pics peuvent être déformés dans des directions imprévisibles différentes, créant le chaos. Lorsque vous utilisez du bruit Perlin, cela ne devrait pas se produire.
Le problème vient du fait que nous utilisons directement les coordonnées du monde pour échantillonner le bruit. Pour cette raison, la texture est masquée à travers chaque unité et les cellules sont beaucoup plus grandes que cette valeur. En fait, la texture est échantillonnée à des points arbitraires, détruisant son intégrité existante.
Des lignes de 10 x 10 cellules se chevauchent.Nous devrons mettre à l'échelle l'échantillonnage de bruit afin que la texture couvre une zone beaucoup plus grande. Ajoutons cette échelle à
HexMetrics
et
HexMetrics
-lui une valeur de 0,003, puis
HexMetrics
à l'échelle les coordonnées des échantillons selon ce facteur.
public const float noiseScale = 0.003f; public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear( position.x * noiseScale, position.z * noiseScale ); }
Il s'avère soudain que notre texture couvre 333 & frac13; unités carrées, et son intégrité locale devient apparente.
Bruit à l'échelle.De plus, une nouvelle échelle augmente la distance entre les joints de bruit. En fait, comme les cellules ont un diamètre interne de 10√3 unités, elles ne seront jamais exactement carrelées dans la dimension X. Cependant, en raison de l'intégrité locale du bruit, à plus grande échelle, nous serons toujours en mesure de reconnaître des motifs répétitifs, environ toutes les 20 cellules, même si les détails ne correspondent pas. Mais ils ne seront évidents que sur la carte sans autres caractéristiques.
paquet d'unitéAligner les centres cellulaires
Déplacer tous les sommets donne à la carte un aspect plus naturel, mais il y a plusieurs problèmes. Comme les cellules sont maintenant irrégulières, leurs étiquettes se croisent avec le maillage. Et dans les joints des corniches avec des falaises, des fissures apparaissent. Nous laisserons les fissures pour plus tard, mais nous allons maintenant nous concentrer sur les surfaces des cellules.
La carte est devenue moins stricte, mais plus de problèmes sont apparus.Le moyen le plus simple de résoudre le problème d'intersection est de rendre les centres des cellules plats. Ne changeons tout simplement pas la coordonnée Y dans
HexMesh.Perturb
.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength;
Cellules alignées.Avec ce changement, toutes les positions verticales resteront inchangées, à la fois au centre des cellules et aux marches des rebords. Il convient de noter que cela réduit le déplacement maximal à √50 ≈ 7,07 uniquement dans le plan XZ.
C'est un bon changement, car il simplifie l'identification des cellules individuelles et ne permet pas aux rebords de devenir trop chaotiques. Mais ce serait quand même bien d'ajouter un petit mouvement vertical.
Déplacer la hauteur des cellules
Au lieu d'appliquer un mouvement vertical à chaque sommet, nous pouvons l'appliquer à une cellule. Dans ce cas, chaque cellule restera plate, mais la variabilité restera toujours entre les cellules. Il serait également logique d'utiliser une échelle différente pour déplacer la hauteur, alors ajoutez-la à
HexMetrics
. Une force de 1,5 unité crée une légère variation, approximativement égale à la hauteur d'une marche du rebord.
public const float elevationPerturbStrength = 1.5f;
Modifiez la propriété
HexCell.Elevation
afin qu'elle
HexCell.Elevation
ce déplacement à la position verticale de la cellule.
public int Elevation { get { return elevation; } set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; } }
Pour que le déplacement soit appliqué immédiatement, nous devons définir explicitement la hauteur de chaque cellule dans
HexGrid.CreateCell
. Sinon, la grille sera initialement plate. Faisons-le à la fin, après avoir créé l'interface utilisateur.
void CreateCell (int x, int z, int i) { … cell.Elevation = 0; }
Hauteurs déplacées avec fissures.En utilisant les mêmes hauteurs
De nombreuses fissures sont apparues dans le maillage, car lorsque nous triangulons le maillage, nous n'utilisons pas les mêmes hauteurs de cellule. Ajoutons une propriété à
HexCell
pour obtenir sa position afin de pouvoir l'utiliser n'importe où.
public Vector3 Position { get { return transform.localPosition; } }
Nous pouvons maintenant utiliser cette propriété dans
HexMesh.Triangulate
pour déterminer le centre de la cellule.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; … }
Et nous pouvons l'utiliser dans
TriangulateConnection
lors de la définition des positions verticales des cellules voisines.
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.Position.y; … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; … } }
Utilisation cohérente de la hauteur des cellules.paquet d'unitéUnité de bord de cellule
Bien que les cellules aient une belle variation, elles ressemblent toujours à des hexagones évidents. Ce n'est pas en soi un problème, mais nous pouvons améliorer leur apparence.
Cellules hexagonales clairement visibles.Si nous avions plus de sommets, alors il y aurait une plus grande variabilité locale. Divisons donc chaque bord de la cellule en deux parties en ajoutant le haut du bord au milieu entre chaque paire de coins. Cela signifie que
HexMesh.Triangulate
doit ajouter non pas un, mais deux triangles.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); Vector3 e1 = Vector3.Lerp(v1, v2, 0.5f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, v2); AddTriangleColor(cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); } }
Douze côtés au lieu de six.Le doublement des sommets et des triangles ajoute plus de variabilité aux bords de la cellule. Rendons-les encore plus inégaux en triplant le nombre de sommets.
Vector3 e1 = Vector3.Lerp(v1, v2, 1f / 3f); Vector3 e2 = Vector3.Lerp(v1, v2, 2f / 3f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, e2); AddTriangleColor(cell.color); AddTriangle(center, e2, v2); AddTriangleColor(cell.color);
18 côtés.Division conjointe des côtes
Bien sûr, nous devons également subdiviser les joints de bord. Par conséquent, nous passerons les nouveaux bords de sommet à
TriangulateConnection
.
if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, e1, e2, v2); }
Ajoutez les paramètres appropriés à
TriangulateConnection
afin qu'il puisse fonctionner avec des sommets supplémentaires.
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 e1, Vector3 e2, Vector3 v2 ) { … }
Nous devons également calculer les arêtes supplémentaires des arêtes pour les cellules voisines. Nous pouvons les calculer après avoir connecté le pont de l'autre côté.
Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Position.y; Vector3 e3 = Vector3.Lerp(v3, v4, 1f / 3f); Vector3 e4 = Vector3.Lerp(v3, v4, 2f / 3f);
Ensuite, nous devons changer la triangulation de la nervure. Jusqu'à ce que nous ignorions les pentes avec les rebords, ajoutez simplement trois au lieu d'un quad.
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, e1, v3, e3); AddQuadColor(cell.color, neighbor.color); AddQuad(e1, e2, e3, e4); AddQuadColor(cell.color, neighbor.color); AddQuad(e2, v2, e4, v4); AddQuadColor(cell.color, neighbor.color); }
Connexions subdivisées.L'union des bords des bords
Étant donné que pour décrire les arêtes, nous avons maintenant besoin de quatre sommets, il serait logique de les combiner en un ensemble. C'est plus pratique que de travailler avec quatre sommets indépendants. Créez une structure
EdgeVertices
simple pour cela. Il doit contenir quatre sommets allant dans le sens horaire le long du bord de la cellule.
using UnityEngine; public struct EdgeVertices { public Vector3 v1, v2, v3, v4; }
Ne devraient-ils pas être sérialisables?Nous utiliserons cette structure uniquement pour la triangulation. À ce stade, nous n'avons pas besoin de stocker les sommets des bords, ils ne sont donc pas nécessaires pour être sérialisables.
Ajoutez-y une méthode constructeur pratique, qui traitera du calcul des points intermédiaires de l'arête.
public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 1f / 3f); v3 = Vector3.Lerp(corner1, corner2, 2f / 3f); v4 = corner2; }
Nous pouvons maintenant ajouter une méthode de triangulation distincte à
HexMesh
pour créer un éventail de triangles entre le centre de la cellule et l'un de ses bords.
void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); }
Et une méthode pour trianguler une bande de quadrangles entre deux arêtes.
void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); }
Cela nous permettra de simplifier la méthode des
Triangulate
.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); TriangulateEdgeFan(center, e, cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
Passons à
TriangulateConnection
. Nous pouvons maintenant utiliser
TriangulateEdgeStrip
, mais d'autres remplacements doivent être effectués. Là où nous
e1.v1
v1
, nous devons utiliser
e1.v1
. De même,
v2
devient
e1.v4
,
v3
devient
e2.v1
et
v4
devient
e2.v4
.
void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v4 + bridge ); if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1.v1, e1.v4, cell, e2.v1, e2.v4, neighbor); } else { TriangulateEdgeStrip(e1, cell.color, e2, neighbor.color); } HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = e1.v4 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e1.v4, cell, e2.v4, neighbor, v5, nextNeighbor ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e2.v4, neighbor, v5, nextNeighbor, e1.v4, cell ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } }
Division du rebord
Nous devons diviser les corniches. Par conséquent, nous transmettons les bords à
TriangulateEdgeTerraces
.
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor); }
Nous devons maintenant modifier
TriangulateEdgeTerraces
afin qu'il interpole entre les arêtes et non entre les paires de sommets. Supposons
EdgeVertices
dispose d'une méthode statique pratique pour ce faire. Cela nous permettra de simplifier
TriangulateEdgeTerraces
plutôt que de le compliquer.
void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); TriangulateEdgeStrip(begin, beginCell.color, e2, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); TriangulateEdgeStrip(e1, c1, e2, c2); } TriangulateEdgeStrip(e2, c2, end, endCell.color); }
La méthode
EdgeVertices.TerraceLerp
interpole simplement les rebords entre les quatre paires de sommets de deux arêtes.
public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); return result; }
Corniches subdivisées.paquet d'unitéReconnectez les falaises et les corniches
Jusqu'à présent, nous avons ignoré les fissures à la jonction des falaises et des corniches. Il est temps de résoudre ce problème. Examinons d'abord les cas falaise-pente-pente (OSS) et pente-falaise-pente (SOS).
Trous de maille.Le problème vient du fait que les sommets des frontières se sont déplacés. Cela signifie que maintenant ils ne se trouvent pas exactement sur le côté de la falaise, ce qui conduit à une fissure. Parfois, ces trous sont invisibles et parfois frappants.
La solution est de ne pas déplacer le haut de la frontière. Cela signifie que nous devons contrôler si le point sera déplacé. Le moyen le plus simple serait de créer une alternative
AddTriangle
qui ne déplace pas du tout les sommets.
void AddTriangleUnperturbed (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); }
Modifiez le
TriangulateBoundaryTriangle
afin qu'il utilise cette méthode. Cela signifie qu'il devra déplacer explicitement tous les sommets, à l'exception des sommets.
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); AddTriangleUnperturbed(Perturb(begin), Perturb(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); AddTriangleUnperturbed(Perturb(v1), Perturb(v2), boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(Perturb(v2), Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Il convient de noter ce qui suit: puisque nous n'utilisons pas la
v2
pour obtenir un autre point, nous pouvons le déplacer immédiatement. Il s'agit d'une optimisation simple qui réduit la quantité de code, nous allons donc l'introduire.
void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangleUnperturbed(Perturb(begin), v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangleUnperturbed(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(v2, Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Frontières immobiles.Ça a l'air mieux, mais nous n'avons pas encore fini. À l'intérieur de la méthode
TriangulateCornerTerracesCliff
, le point limite est interpolé entre les points gauche et droit. Cependant, ces points n'ont pas encore été déplacés. Pour que le point limite corresponde à la falaise résultante, nous devons interpoler entre les points déplacés.
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(right), b);
Il en va de même pour la méthode
TriangulateCornerCliffTerraces
.
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(left), b);
Les trous ont disparu.Double falaise et pente
Dans tous les cas problématiques restants, deux falaises et une pente sont présentes.
Gros trou à cause d'un seul triangle.Ce problème est résolu en déplaçant manuellement un seul triangle dans le bloc
else
à la fin de
TriangulateCornerTerracesCliff
.
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
Il en va de même pour
TriangulateCornerCliffTerraces
.
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
Débarrassez-vous des dernières fissures.paquet d'unitéAchèvement
Nous avons maintenant un maillage déformé complètement correct. Son apparence dépend du bruit spécifique, de son échelle et des forces de distorsion. Dans notre cas, la distorsion peut sembler trop forte. Bien que cette irrégularité soit belle, nous ne voulons pas que les cellules s'écartent trop de la grille uniforme. Au final, nous l'utilisons toujours pour définir la cellule à redimensionner. Et si la taille des cellules varie trop, il nous sera plus difficile d'y placer le contenu.Mailles non déformées et déformées.Il semble que la force 5 de déformation des cellules soit trop importante.La distorsion des cellules est de 0 à 5.Réduisons-la à 4 pour augmenter la commodité de la grille, sans la rendre trop correcte. Cela garantit que le décalage XZ maximum sera de √32 ≈ 5,66 unités. public const float cellPerturbStrength = 4f;
Force de distorsion cellulaire 4.Une autre valeur qui peut être modifiée est le coefficient d'intégrité. Si nous l'augmentons, les centres plats des cellules deviendront plus grands, c'est-à-dire qu'il y aura plus d'espace pour le contenu futur. Bien sûr, ce faisant, ils deviendront plus hexagonaux.Coefficient d'intégrité de 0,75 à 0,95.Une légère augmentation du coefficient d'intégrité à 0,8 simplifiera légèrement notre vie à l'avenir. public const float solidFactor = 0.8f;
Coefficient d'intégrité 0,8.Enfin, vous remarquerez peut-être que les différences entre les niveaux d'élévation sont trop marquées. C'est pratique lorsque vous devez vous assurer que le maillage est généré correctement, mais nous avons déjà terminé avec cela. Réduisons-le à 1 unité par étape, c'est-à-dire à 3. public const float elevationStep = 3f;
La hauteur est réduite à 3.Nous pouvons également modifier la force de la distorsion de hauteur. Mais maintenant, il a une valeur de 1,5, ce qui équivaut à un demi-pas de hauteur, ce qui nous convient.De petites marches de hauteurs permettent une utilisation plus logique des sept niveaux de hauteur. Cela augmente la variabilité de la carte.Nous utilisons sept niveaux de hauteurs.paquet d'unitéPartie 5: cartes plus grandes
- Nous divisons la grille en fragments.
- Nous contrôlons la caméra.
- Colorisez les couleurs et les hauteurs séparément.
- Utilisez la brosse agrandie des cellules.
Jusqu'à présent, nous avons travaillé avec une toute petite carte. Il est temps de l'augmenter.Il est temps de zoomer.Fragments de maille
Nous ne pouvons pas rendre la grille trop grande, car nous nous heurtons aux limites de ce qui peut tenir dans un maillage. Comment résoudre ce problème? Utilisez plusieurs maillages. Pour ce faire, nous devons diviser notre grille en plusieurs fragments. Nous utilisons des fragments rectangulaires de taille constante.Diviser la grille en segments de 3 par 3.Utilisons 5 blocs par 5, soit 25 cellules par fragment. Définissez-les HexMetrics
. public const int chunkSizeX = 5, chunkSizeZ = 5;
Quelle taille de fragment peut être considérée comme appropriée?. , . . , (frustum culling), . .
Maintenant, nous ne pouvons pas utiliser de taille pour le maillage, il doit être un multiple de la taille du fragment. Par conséquent, modifions-le HexGrid
afin qu'il définisse sa taille non pas dans des cellules distinctes, mais en fragments. Définissez la taille par défaut sur 4 par 3 fragments, c'est-à-dire seulement 12 fragments ou 300 cellules. Nous obtenons donc une carte de test pratique. public int chunkCountX = 4, chunkCountZ = 3;
Nous utilisons toujours width
et height
, mais maintenant ils devraient devenir privés. Et renommez-les en cellCountX
et cellCountZ
. Utilisez l'éditeur pour renommer toutes les occurrences de ces variables à la fois. Maintenant, il sera clair quand nous aurons affaire au nombre de fragments ou de cellules.
Spécifiez la taille en fragments.Modifiez Awake
pour que, si nécessaire, le nombre de cellules soit calculé à partir du nombre de fragments. Nous mettons en évidence la création de cellules dans une méthode distincte, afin de ne pas obstruer Awake
. void Awake () { HexMetrics.noiseSource = noiseSource; gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateCells(); } void CreateCells () { cells = new HexCell[cellCountZ * cellCountX]; for (int z = 0, i = 0; z < cellCountZ; z++) { for (int x = 0; x < cellCountX; x++) { CreateCell(x, z, i++); } } }
Fragment préfabriqué
Pour décrire les fragments de maillage, nous avons besoin d'un nouveau type de composant. using UnityEngine; using UnityEngine.UI; public class HexGridChunk : MonoBehaviour { }
Ensuite, nous allons créer un fragment préfabriqué. Nous le ferons en dupliquant l'objet Hex Grid et en le renommant Hex Grid Chunk . Supprimez son composant HexGrid
et ajoutez un composant à la place HexGridChunk
. Transformez-le ensuite en un préfabriqué et retirez l'objet de la scène.Un fragment préfabriqué avec sa propre toile et maillage.Puisqu'il créera des instances de ces fragments HexGrid
, nous lui donnerons un lien vers le préfabriqué du fragment. public HexGridChunk chunkPrefab;
Maintenant avec des fragments.La création d'instances de fragments ressemble beaucoup à la création d'instances de cellules. Nous allons les suivre à l'aide d'un tableau et utiliser une double boucle pour le remplir. HexGridChunk[] chunks; void Awake () { … CreateChunks(); CreateCells(); } void CreateChunks () { chunks = new HexGridChunk[chunkCountX * chunkCountZ]; for (int z = 0, i = 0; z < chunkCountZ; z++) { for (int x = 0; x < chunkCountX; x++) { HexGridChunk chunk = chunks[i++] = Instantiate(chunkPrefab); chunk.transform.SetParent(transform); } } }
L'initialisation d'un fragment est similaire à la façon dont nous avons initialisé une grille d'hexagones. Elle met tout en place Awake
et effectue la triangulation Start
. Il nécessite une référence à son canevas et maillage, ainsi qu'un tableau pour les cellules. Cependant, le fragment ne créera pas ces cellules. La grille continuera de le faire. public class HexGridChunk : MonoBehaviour { HexCell[] cells; HexMesh hexMesh; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; } void Start () { hexMesh.Triangulate(cells); } }
Affectation de cellules à des fragments
HexGrid
crée toujours toutes les cellules. C'est normal, mais maintenant nous devons ajouter chaque cellule à un fragment approprié, et non les définir en utilisant notre propre maillage et toile. void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab);
Nous pouvons trouver le fragment correct en utilisant la division entière x
et z
par taille de fragment. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; }
En utilisant des résultats intermédiaires, nous pouvons également déterminer l'indice local de la cellule dans ce fragment. Après cela, vous pouvez ajouter une cellule au fragment. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; int localX = x - chunkX * HexMetrics.chunkSizeX; int localZ = z - chunkZ * HexMetrics.chunkSizeZ; chunk.AddCell(localX + localZ * HexMetrics.chunkSizeX, cell); }
Ensuite, il HexGridChunk.AddCell
place la cellule dans son propre tableau, puis il définit les éléments parents de la cellule et de son interface utilisateur. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }
Balayer
À ce stade, il HexGrid
peut se débarrasser de la toile et du maillage hexagonal de ses enfants, ainsi que du code.
Depuis que nous nous sommes débarrassés Refresh
, nous ne devons HexMapEditor
plus l'utiliser. void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation;
La grille nettoyée d'hexagones.Après avoir démarré le mode Lecture, la carte est toujours la même. Mais la hiérarchie des objets sera différente. Hex Grid crée maintenant des objets enfants fragmentés qui contiennent des cellules, ainsi que leur maillage et leur canevas.Fragments enfant en mode Lecture.Nous avons peut-être des problèmes avec les étiquettes des cellules. Initialement, nous avons défini la largeur de l'étiquette sur 5. Cela suffisait pour afficher les deux caractères qui nous suffisaient sur une petite carte. Mais maintenant, nous pouvons avoir des coordonnées comme −10, dans lesquelles il y a trois caractères. Ils ne rentreront pas et seront coupés. Pour résoudre ce problème, augmentez la largeur de l'étiquette de cellule à 10, voire plus.Étiquettes de cellules étendues.Nous pouvons maintenant créer des cartes beaucoup plus grandes! Comme nous générons l'intégralité de la grille au démarrage, la création de grandes cartes peut prendre beaucoup de temps. Mais après l'achèvement, nous aurons un immense espace d'expérimentation.Correction de l'édition des cellules
L'édition ne semble pas fonctionner au stade actuel, car nous ne mettons plus à jour la grille. Nous devons mettre à jour des fragments individuels, alors ajoutez une méthode Refresh
à HexGridChunk
. public void Refresh () { hexMesh.Triangulate(cells); }
Quand devrions-nous appeler cette méthode? Nous avons mis à jour la grille entière à chaque fois car nous n'avions qu'un seul maillage. Mais maintenant, nous avons de nombreux fragments. Au lieu de les mettre à jour à chaque fois, il sera beaucoup plus efficace de mettre à jour les fragments modifiés. Sinon, changer de grandes cartes deviendra une opération très lente.Mais comment savoir quel fragment mettre à jour? Le moyen le plus simple est de faire savoir à chaque cellule à quel fragment elle appartient. La cellule pourra alors mettre à jour son fragment lors du changement de cette cellule. Donnons donc un HexCell
lien vers son fragment. public HexGridChunk chunk;
HexGridChunk
peut s'ajouter à la cellule lors de l'ajout. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.chunk = this; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }
En les connectant, nous ajoutons à la HexCell
méthode Refresh
. Chaque fois qu'une cellule est mise à jour, elle met simplement à jour son fragment. void Refresh () { chunk.Refresh(); }
Nous n'avons pas besoin de le rendre HexCell.Refresh
commun, car la cellule elle-même sait mieux quand elle a été changée. Par exemple, une fois sa hauteur modifiée. public int Elevation { get { return elevation; } set { … Refresh(); } }
En fait, nous devons le mettre à jour uniquement lorsque sa hauteur a changé pour une valeur différente. Elle n'a même pas besoin de recalculer quoi que ce soit si nous lui attribuons la même hauteur qu'auparavant. Par conséquent, nous pouvons quitter le début du setter. public int Elevation { get { return elevation; } set { if (elevation == value) { return; } … } }
Cependant, nous ignorerons également les calculs pour la première fois lorsque la hauteur est définie sur 0, car il s'agit de la valeur de hauteur de maillage par défaut. Pour éviter cela, nous allons créer la valeur initiale telle que nous ne l'utilisons jamais. int elevation = int.MinValue;
Qu'est-ce que int.MinValue?, integer. C# integer —
32- , 2 32 integer, , . .
— −2 31 = −2 147 483 648. !
2 31 − 1 = 2 147 483 647. 2 31 - .
Pour reconnaître le changement de couleur de la cellule, nous devons également la transformer en propriété. Renommez-le en Color
majuscules, puis transformez-le en une propriété avec une variable privée color
. La valeur de couleur par défaut sera le noir transparent, ce qui nous convient. public Color Color { get { return color; } set { if (color == value) { return; } color = value; Refresh(); } } Color color;
Maintenant, lorsque nous démarrons le mode Lecture, nous obtenons des exceptions de référence nulle. Cela se produit car nous définissons la couleur et la hauteur à leurs valeurs par défaut avant d'affecter une cellule à son fragment. Il est normal que nous ne mettions pas à jour les fragments à ce stade, car nous les triangulons une fois l’initialisation terminée. En d'autres termes, nous mettons à jour un fragment uniquement s'il est affecté. void Refresh () { if (chunk) { chunk.Refresh(); } }
Nous pouvons enfin changer à nouveau les cellules! Cependant, un problème se pose. Lorsque vous dessinez le long des bordures de fragments, des coutures apparaissent.Erreurs aux limites des fragments.C'est logique, car lorsqu'une seule cellule change, toutes les connexions avec ses voisins changent également. Et ces voisins peuvent être dans d'autres fragments. La solution la plus simple consiste à mettre à jour toutes les cellules voisines si elles sont différentes. void Refresh () { if (chunk) { chunk.Refresh(); for (int i = 0; i < neighbors.Length; i++) { HexCell neighbor = neighbors[i]; if (neighbor != null && neighbor.chunk != chunk) { neighbor.chunk.Refresh(); } } } }
Bien que cela fonctionne, il se peut que nous mettions à jour un fragment plusieurs fois. Et lorsque nous commençons à colorer plusieurs cellules à la fois, tout va empirer.Mais nous ne sommes pas tenus de trianguler immédiatement après la mise à jour du fragment. Au lieu de cela, nous écrivons simplement qu'une mise à jour est nécessaire et triangulons une fois le changement terminé.Puisqu'il ne HexGridChunk
fait rien d'autre, nous pouvons utiliser son état activé pour signaler la nécessité de mises à jour. Lors de sa mise à jour, nous incluons le composant. L'activer plusieurs fois ne changera rien. Le composant est mis à jour ultérieurement. Nous allons trianguler à ce stade et désactiver à nouveau le composant.Nous utilisons à la LateUpdate
placeUpdate
pour garantir que la triangulation se produit une fois la modification terminée pour la trame actuelle. public void Refresh () {
Quelle est la différence entre Update et LateUpdate?Update
- . LateUpdate
. , .
Comme notre composant est activé par défaut, nous n'avons plus besoin de trianguler explicitement Start
. Par conséquent, cette méthode peut être supprimée.
Fragments de 20 par 20 contenant 10 000 cellules.Listes généralisées
Bien que nous ayons considérablement modifié la façon dont la grille est triangulée, HexMesh
elle reste la même. Tout ce dont il a besoin pour travailler est un tableau de cellules. Il ne se soucie pas s'il y a un maillage d'hexagones, ou plusieurs d'entre eux. Mais nous n'avons pas encore envisagé d'utiliser plusieurs maillages. Peut-être que quelque chose peut être amélioré ici?Les HexMesh
listes utilisées sont essentiellement des tampons temporaires. Ils sont utilisés uniquement pour la triangulation. Et les fragments sont triangulés un à la fois. Par conséquent, en fait, nous avons besoin d'un seul ensemble de listes, et non d'un ensemble pour chaque objet maillé hexagonal. Cela peut être réalisé en rendant les listes statiques. static List<Vector3> vertices = new List<Vector3>(); static List<Color> colors = new List<Color>(); static List<int> triangles = new List<int>(); void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); meshCollider = gameObject.AddComponent<MeshCollider>(); hexMesh.name = "Hex Mesh";
Les listes statiques sont-elles vraiment si importantes?. , , .
, . 20 20 100.
paquet d'unitéContrôle de la caméra
Le grand appareil photo est merveilleux, mais il est inutile si nous ne pouvons pas le voir. Pour inspecter la carte entière, nous devons déplacer la caméra. Le zoom est également utile. Par conséquent, créons une caméra pour effectuer ces actions.Créez un objet factice et appelez-le Caméra Hex Map . Déposez son composant de transformation de sorte qu'il se déplace vers l'origine sans modifier sa rotation et son échelle. Ajoutez-y un enfant appelé Swivel et ajoutez-y un enfant Stick . Faites de la caméra principale un enfant du Stick et réinitialisez son composant de transformation.La hiérarchie de la caméra.Le but de la charnière de la caméra (pivotante) est de contrôler l'angle sous lequel la caméra regarde la carte. Donnons-lui un tour (45, 0, 0). La poignée (Stick) contrôle la distance à laquelle les caméras sont situées. Fixons-lui une position (0, 0, -45).Maintenant, nous avons besoin d'un composant pour contrôler ce système. Attribuez ce composant à la racine de la hiérarchie des caméras. Donnez-lui un lien vers la charnière et la poignée, en les faisant entrer Awake
. using UnityEngine; public class HexMapCamera : MonoBehaviour { Transform swivel, stick; void Awake () { swivel = transform.GetChild(0); stick = swivel.GetChild(0); } }
Caméra de carte hexagonale.Zoom
La première fonction que nous allons créer est le zoom (zoom). Nous pouvons contrôler le niveau de zoom actuel en utilisant la variable flottante. Une valeur de 0 signifie que nous sommes complètement éloignés et une valeur de 1 signifie que nous sommes complètement proches. Commençons par un zoom maximum. float zoom = 1f;
Le zoom est généralement effectué avec la molette de la souris ou un contrôle analogique. Nous pouvons l'implémenter en utilisant l'axe d'entrée par défaut de la molette de la souris . Nous ajoutons une méthode Update
qui vérifie la présence d'un delta d'entrée, et s'il y en a un, il appelle la méthode pour changer le zoom. void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } } void AdjustZoom (float delta) { }
Pour modifier le niveau de zoom, nous y ajoutons simplement un delta, puis limitons la valeur (pince) pour rester dans la plage 0–1. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); }
Lors d'un zoom avant ou arrière, la distance par rapport à la caméra doit changer en conséquence. Cela peut être fait en changeant la position de la poignée en Z. Ajoutez deux variables flottantes communes pour ajuster la position de la poignée au zoom minimum et maximum. Puisque nous développons une carte relativement petite, définissez les valeurs sur -250 et -45. public float stickMinZoom, stickMaxZoom;
Après avoir changé le zoom, nous effectuons une interpolation linéaire entre ces deux valeurs en fonction de la nouvelle valeur de zoom. Mettez ensuite à jour la position de la poignée. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); float distance = Mathf.Lerp(stickMinZoom, stickMaxZoom, zoom); stick.localPosition = new Vector3(0f, 0f, distance); }
Valeurs de stick minimum et maximum.Maintenant, le zoom fonctionne, mais jusqu'à présent, il n'est pas très utile. Habituellement, lorsque le zoom est plus éloigné, la caméra passe en vue de dessus. Nous pouvons le réaliser en tournant la charnière. Par conséquent, nous ajoutons les variables min et max pour la charnière. Fixons-leur les valeurs 90 et 45. public float swivelMinZoom, swivelMaxZoom;
Comme pour la position de la poignée, nous interpolons pour trouver un angle de zoom approprié. Ensuite, nous définissons la rotation de la charnière. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); float distance = Mathf.Lerp(stickMinZoom, stickMaxZoom, zoom); stick.localPosition = new Vector3(0f, 0f, distance); float angle = Mathf.Lerp(swivelMinZoom, swivelMaxZoom, zoom); swivel.localRotation = Quaternion.Euler(angle, 0f, 0f); }
La valeur minimale et maximale de Swivel.Le taux de changement du zoom peut être ajusté en changeant la sensibilité des paramètres d'entrée de la molette de la souris. Ils peuvent être trouvés dans Edit / Project Settings / Input . Par exemple, en les changeant de 0,1 à 0,025, nous obtenons un changement de zoom plus lent et plus fluide.Options de saisie de la molette de la souris.Déménagement
Passons maintenant au déplacement de la caméra. Le mouvement dans la direction de X et Z doit être implémenté dans Update
, comme dans le cas du zoom. Nous pouvons utiliser des axes d'entrée horizontaux et verticaux pour cela . Cela nous permettra de déplacer la caméra avec les flèches et les touches WASD. void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } float xDelta = Input.GetAxis("Horizontal"); float zDelta = Input.GetAxis("Vertical"); if (xDelta != 0f || zDelta != 0f) { AdjustPosition(xDelta, zDelta); } } void AdjustPosition (float xDelta, float zDelta) { }
L'approche la plus simple consiste à obtenir la position actuelle du système de caméra, à y ajouter des deltas X et Z et à affecter le résultat à la position du système. void AdjustPosition (float xDelta, float zDelta) { Vector3 position = transform.localPosition; position += new Vector3(xDelta, 0f, zDelta); transform.localPosition = position; }
Pour cette raison, la caméra se déplacera tout en maintenant les flèches ou WASD, mais pas à une vitesse constante. Cela dépendra de la fréquence d'images. Pour déterminer la distance à parcourir, nous utilisons le delta temporel ainsi que la vitesse requise. Par conséquent, nous ajoutons une variable commune moveSpeed
et la définissons sur 100, puis la multiplions par le delta temporel pour obtenir le delta de position. public float moveSpeed; void AdjustPosition (float xDelta, float zDelta) { float distance = moveSpeed * Time.deltaTime; Vector3 position = transform.localPosition; position += new Vector3(xDelta, 0f, zDelta) * distance; transform.localPosition = position; }
Vitesse de déplacement.Nous pouvons maintenant nous déplacer à une vitesse constante le long des axes X ou Z. Mais lorsque vous vous déplacez le long des deux axes en même temps (en diagonale), le mouvement sera plus rapide. Pour résoudre ce problème, nous devons normaliser le vecteur delta. Cela vous permettra de l'utiliser comme destination. void AdjustPosition (float xDelta, float zDelta) { Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized; float distance = moveSpeed * Time.deltaTime; Vector3 position = transform.localPosition; position += direction * distance; transform.localPosition = position; }
Le mouvement diagonal est maintenant correctement mis en œuvre, mais il s'avère soudain que la caméra continue de bouger assez longtemps même après avoir relâché toutes les touches. Cela se produit car les axes d'entrée ne sautent pas instantanément aux valeurs limites immédiatement après avoir appuyé sur les touches. Ils ont besoin de temps pour cela. Il en va de même pour la libération des clés. Il faut du temps pour revenir à des valeurs d'axe nulles. Cependant, puisque nous avons normalisé les valeurs d'entrée, la vitesse maximale est constamment maintenue.Nous pouvons ajuster les paramètres d'entrée pour éliminer les retards, mais ils donnent une sensation de douceur qui mérite d'être sauvegardée. Nous pouvons appliquer la valeur la plus extrême des axes comme coefficient d'amortissement du mouvement. Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized; float damping = Mathf.Max(Mathf.Abs(xDelta), Mathf.Abs(zDelta)); float distance = moveSpeed * damping * Time.deltaTime;
Mouvement avec atténuation.Maintenant, le mouvement fonctionne bien, au moins avec une augmentation du zoom. Mais à distance cela s'avère trop lent. Avec un zoom réduit, nous devons accélérer. Cela peut être fait en remplaçant une variable moveSpeed
par deux pour le zoom minimum et maximum, puis en interpolant. Attribuez-leur des valeurs de 400 et 100.
La vitesse de déplacement varie en fonction du niveau de zoom.Nous pouvons maintenant nous déplacer rapidement sur la carte! En fait, nous pouvons aller bien au-delà de la carte, mais ce n'est pas souhaitable. La caméra doit rester à l'intérieur de la carte. Pour ce faire, nous devons connaître les limites de la carte, donc un lien vers la grille est nécessaire. Ajoutez-le et connectez-le. public HexGrid grid;
Besoin de demander la taille de la grille.Après avoir déplacé vers une nouvelle position, nous la limiterons en utilisant la nouvelle méthode. void AdjustPosition (float xDelta, float zDelta) { … transform.localPosition = ClampPosition(position); } Vector3 ClampPosition (Vector3 position) { return position; }
La position X a une valeur minimale de 0 et la valeur maximale est déterminée par la taille de la carte. Vector3 ClampPosition (Vector3 position) { float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); return position; }
Il en va de même pour la position Z. Vector3 ClampPosition (Vector3 position) { float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = grid.chunkCountZ * HexMetrics.chunkSizeZ * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); return position; }
En fait, c'est un peu inexact. Le point de départ est au centre de la cellule, pas à gauche. Par conséquent, nous voulons que la caméra s'arrête au centre des cellules les plus à droite. Pour ce faire, soustrayez la moitié de la cellule du maximum de X. float xMax = (grid.chunkCountX * HexMetrics.chunkSizeX - 0.5f) * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax);
Pour la même raison, nous devons réduire le maximum Z. Puisque les mesures sont légèrement différentes, nous devons soustraire la cellule entière. float zMax = (grid.chunkCountZ * HexMetrics.chunkSizeZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax);
Avec le mouvement, nous avons terminé, il ne reste qu'un petit détail. Parfois, l'interface utilisateur réagit aux touches fléchées, ce qui conduit au fait que lorsque vous déplacez la caméra, le curseur se déplace. Cela se produit lorsque l'interface utilisateur se considère comme active, après avoir cliqué dessus et que le curseur reste au-dessus.Vous pouvez empêcher l'interface utilisateur d'écouter les entrées au clavier. Cela peut être fait en demandant à l' objet EventSystem de ne pas exécuter Send Navigation Events .Plus d'événements de navigation.Tourner
Vous voulez voir ce qu'il y a derrière la falaise? Il serait pratique de pouvoir faire pivoter la caméra! Ajoutons cette fonctionnalité.Le niveau de zoom n'est pas important pour la rotation, seule la vitesse suffit. Ajoutez une variable commune rotationSpeed
et réglez-la sur 180 degrés. Vérifiez le delta de rotation en Update
échantillonnant l'axe de rotation et en changeant la rotation si nécessaire. public float rotationSpeed; void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } float rotationDelta = Input.GetAxis("Rotation"); if (rotationDelta != 0f) { AdjustRotation(rotationDelta); } float xDelta = Input.GetAxis("Horizontal"); float zDelta = Input.GetAxis("Vertical"); if (xDelta != 0f || zDelta != 0f) { AdjustPosition(xDelta, zDelta); } } void AdjustRotation (float delta) { }
Vitesse de rotation.En fait, l'axe de rotation n'est pas par défaut. Nous devrons le créer nous-mêmes. Accédez aux paramètres d'entrée et dupliquez l'entrée supérieure verticale . Changez le nom du doublon en Rotation et changez les clés en QE et une virgule (,) avec un point (.).Tournez l'axe d'entrée.J'ai téléchargé le paquet d'unité, pourquoi n'ai-je pas cette entrée?. Unity. , . , , .
L'angle de rotation que nous suivrons et changerons AdjustRotation
. Après quoi, nous allons faire pivoter l'ensemble du système de caméra. float rotationAngle; void AdjustRotation (float delta) { rotationAngle += delta * rotationSpeed * Time.deltaTime; transform.localRotation = Quaternion.Euler(0f, rotationAngle, 0f); }
Étant donné que le cercle complet est de 360 degrés, nous roulons l'angle de rotation de sorte qu'il se situe dans la plage de 0 à 360. void AdjustRotation (float delta) { rotationAngle += delta * rotationSpeed * Time.deltaTime; if (rotationAngle < 0f) { rotationAngle += 360f; } else if (rotationAngle >= 360f) { rotationAngle -= 360f; } transform.localRotation = Quaternion.Euler(0f, rotationAngle, 0f); }
Mettez en action.Maintenant, la rotation fonctionne. Si vous le vérifiez, vous pouvez voir que le mouvement est absolu. Par conséquent, après avoir tourné à 180 degrés, le mouvement sera l'opposé de ce qui était attendu. Il serait beaucoup plus pratique pour l'utilisateur que le mouvement soit effectué par rapport à l'angle de vue de la caméra. Nous pouvons le faire en multipliant la rotation actuelle par la direction du mouvement. void AdjustPosition (float xDelta, float zDelta) { Vector3 direction = transform.localRotation * new Vector3(xDelta, 0f, zDelta).normalized; … }
Déplacement relatif.paquet d'unitéÉdition avancée
Maintenant que nous avons une carte plus grande, vous pouvez améliorer les outils d'édition de carte. Changer une cellule à la fois est trop long, donc ce serait bien de créer un pinceau plus grand. Il sera également pratique si vous pouviez choisir de peindre ou de modifier la hauteur, en laissant tout le reste inchangé.Couleur et hauteur en option
Nous pouvons rendre les couleurs facultatives en ajoutant une option de sélection vide au groupe de bascule. Dupliquez l'un des commutateurs de couleur et remplacez son étiquette par --- ou quelque chose de similaire pour indiquer qu'il ne s'agit pas d'une couleur. Remplacez ensuite l'argument de son événement On Value Changed par −1.Index de couleur non valide.Bien sûr, cet index n'est pas valide pour un tableau de couleurs. Nous pouvons l'utiliser pour déterminer si la couleur doit être appliquée aux cellules. bool applyColor; public void SelectColor (int index) { applyColor = index >= 0; if (applyColor) { activeColor = colors[index]; } } void EditCell (HexCell cell) { if (applyColor) { cell.Color = activeColor; } cell.Elevation = activeElevation; }
La hauteur est contrôlée par un curseur, nous ne pouvons donc pas y ajouter d'interrupteur. Au lieu de cela, nous pouvons utiliser un commutateur séparé pour activer ou désactiver la modification de la hauteur. Par défaut, il sera activé. bool applyElevation = true; void EditCell (HexCell cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } }
Ajoutez un nouveau commutateur de hauteur à l'interface utilisateur. Je mettrai également tout sur un nouveau panneau et rendrai le curseur de hauteur horizontal pour que l'interface utilisateur soit plus belle.Couleur et hauteur en option.Pour activer la hauteur, nous avons besoin d'une nouvelle méthode, que nous connecterons à l'interface utilisateur. public void SetApplyElevation (bool toggle) { applyElevation = toggle; }
En le connectant au commutateur de hauteur, assurez-vous que la méthode bool dynamique est utilisée en haut de la liste des méthodes. Les versions correctes n'affichent pas de coche dans l'inspecteur.Nous transmettons l'état de l'interrupteur de hauteur.Maintenant, nous pouvons choisir uniquement la coloration avec des fleurs ou seulement la hauteur. Ou les deux, comme d'habitude. Nous pouvons même choisir de ne changer ni l'un ni l'autre, mais pour l'instant cela ne nous est pas particulièrement utile.Basculez entre la couleur et la hauteur.Pourquoi la hauteur s'éteint-elle lors du choix d'une couleur?, toggle group. , , toggle group.
Taille du pinceau
Pour prendre en charge la taille du pinceau redimensionnable, ajoutez une variable entière brushSize
et une méthode pour la définir via l'interface utilisateur. Nous allons utiliser le curseur, donc encore une fois nous devrons convertir la valeur de float en int. int brushSize; public void SetBrushSize (float size) { brushSize = (int)size; }
Curseur de taille de brosse.Vous pouvez créer un nouveau curseur en dupliquant le curseur de hauteur. Modifiez sa valeur maximale à 4 et associez-la à la méthode correspondante. Je lui ai également ajouté un tag.Paramètres du curseur de taille de pinceau.Maintenant que nous pouvons éditer plusieurs cellules en même temps, nous devons utiliser la méthode EditCells
. Cette méthode fera appel EditCell
à toutes les cellules impliquées. La cellule initialement sélectionnée sera considérée comme le centre du pinceau. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCells(hexGrid.GetCell(hit.point)); } } void EditCells (HexCell center) { } void EditCell (HexCell cell) { … }
La taille du pinceau détermine le rayon de l'édition. Avec un rayon de 0, ce ne sera qu'une seule cellule centrale. Avec un rayon de 1, ce sera le centre et ses voisins. Dans un rayon de 2, les voisins du centre et leurs voisins immédiats sont allumés. Et ainsi de suite.
Jusqu'au rayon 3.Pour modifier des cellules, vous devez les contourner en boucle. Nous avons d'abord besoin des coordonnées X et Z du centre. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; }
Nous trouvons la coordonnée Z minimale en soustrayant le rayon. Nous définissons donc la ligne zéro. À partir de cette ligne, nous bouclons jusqu'à ce que nous couvrions la ligne au centre. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { } }
La première cellule de la ligne inférieure a la même coordonnée X que la cellule centrale. Cette coordonnée diminue avec l'augmentation du nombre de lignes.La dernière cellule a toujours une coordonnée X égale à la coordonnée centrale plus le rayon.Maintenant, nous pouvons boucler autour de chaque ligne et obtenir des cellules par leurs coordonnées. for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { for (int x = centerX - r; x <= centerX + brushSize; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } }
Nous n'avons pas encore de méthode HexGrid.GetCell
avec un paramètre de coordonnées, alors créez-le. Convertissez les coordonnées des déplacements et obtenez la cellule. public HexCell GetCell (HexCoordinates coordinates) { int z = coordinates.Z; int x = coordinates.X + z / 2; return cells[x + z * cellCountX]; }
La partie inférieure de la brosse, taille 2.Nous couvrons le reste de la brosse en effectuant un cycle de haut en bas jusqu'au centre. Dans ce cas, la logique est mise en miroir et la ligne centrale doit être exclue. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { for (int x = centerX - r; x <= centerX + brushSize; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } } for (int r = 0, z = centerZ + brushSize; z > centerZ; z--, r++) { for (int x = centerX - brushSize; x <= centerX + r; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } } }
Le pinceau entier, taille 2.Cela fonctionne, à moins que notre pinceau ne dépasse les limites de la grille. Lorsque cela se produit, nous obtenons une exception d'index hors plage. Pour éviter cela, vérifiez les limites HexGrid.GetCell
et revenez null
lorsqu'une cellule inexistante est demandée. public HexCell GetCell (HexCoordinates coordinates) { int z = coordinates.Z; if (z < 0 || z >= cellCountZ) { return null; } int x = coordinates.X + z / 2; if (x < 0 || x >= cellCountX) { return null; } return cells[x + z * cellCountX]; }
Pour éviter l'exception de référence nulle, il HexMapEditor
doit vérifier avant de modifier si la cellule existe vraiment. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } } }
Utilisation de plusieurs tailles de pinceau.Basculer la visibilité des libellés de cellule
Plus souvent qu'autrement, nous n'avons pas besoin de voir les étiquettes des cellules. Rendons-les donc facultatifs. Puisque chaque fragment contrôle son propre canevas, ajoutez une méthode ShowUI
à HexGridChunk
. Lorsque l'interface utilisateur doit être visible, nous activons le canevas. Sinon, désactivez-le. public void ShowUI (bool visible) { gridCanvas.gameObject.SetActive(visible); }
Cachons l'interface utilisateur par défaut. void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; ShowUI(false); }
Étant donné que la visibilité de l'interface utilisateur est commutée pour l'ensemble de la carte, nous ajoutons la méthode ShowUI
à HexGrid
. Il transmet simplement la demande à ses fragments. public void ShowUI (bool visible) { for (int i = 0; i < chunks.Length; i++) { chunks[i].ShowUI(visible); } }
HexMapEditor
obtient la même méthode, en passant la demande à la grille. public void ShowUI (bool visible) { hexGrid.ShowUI(visible); }
Enfin, nous pouvons ajouter un commutateur à l'interface utilisateur et le connecter.Commutateur de visibilité des balises.paquet d'unitéPartie 6: rivières
- Ajout de rivières aux cellules.
- Faites glisser et déposez le support pour dessiner des rivières.
- Création de lits de rivière.
- Utilisation de plusieurs maillages par fragment.
- Créez un pool de listes partagées.
- Triangulation et animation de l'eau qui coule.
Dans la partie précédente, nous avons parlé de la prise en charge de grandes cartes. Nous pouvons maintenant passer à des éléments de relief plus grands. Cette fois, nous parlerons des rivières.Les rivières coulent des montagnes.Cellules fluviales
Il existe trois façons d'ajouter des rivières à une grille d'hexagones. La première consiste à les laisser passer de cellule en cellule. C'est ainsi qu'il est implémenté dans Endless Legend. La deuxième façon consiste à leur permettre de circuler entre les cellules, d'un bord à l'autre. Il est donc implémenté dans Civilization 5. La troisième façon n'est pas de créer des structures fluviales spéciales, mais d'utiliser des cellules d'eau pour les suggérer. Les rivières sont donc implémentées dans Age of Wonders 3.Dans notre cas, les bords des cellules sont déjà occupés par des pentes et des falaises. Cela laisse peu de place aux rivières. Par conséquent, nous les ferons passer de cellule en cellule. Cela signifie que dans chaque cellule, il n'y aura pas de rivière, ou qu'une rivière coulera le long de celle-ci, ou qu'il y aura un début ou une fin de rivière en elle. Dans les cellules le long desquelles la rivière coule, elle peut couler directement, faire un tour d'un pas ou deux pas.Cinq configurations de rivière possibles.Nous ne soutiendrons pas la ramification ou la fusion de rivières. Cela compliquera encore les choses, en particulier l'écoulement de l'eau. De plus, nous ne serons pas intrigués par de grands volumes d'eau. Nous les considérerons dans un autre tutoriel.Suivi de la rivière
La cellule le long de laquelle la rivière coule peut être considérée simultanément comme ayant une rivière entrante et sortante. S'il contient le début d'une rivière, il n'a alors qu'une rivière sortante. Et s'il contient la fin de la rivière, alors il n'a qu'une rivière entrante. Nous pouvons stocker ces informations en HexCell
utilisant deux valeurs booléennes. bool hasIncomingRiver, hasOutgoingRiver;
Mais cela ne suffit pas. Nous devons également connaître la direction de ces rivières. Dans le cas d'une rivière sortante, elle indique où elle se déplace. Dans le cas d'une rivière entrante, elle indique d'où elle vient. bool hasIncomingRiver, hasOutgoingRiver; HexDirection incomingRiver, outgoingRiver;
Nous aurons besoin de ces informations lors de la triangulation des cellules, alors ajoutez des propriétés pour y avoir accès. Nous ne prendrons pas en charge leur attribution directe. Pour ce faire, nous ajouterons en outre une méthode distincte. public bool HasIncomingRiver { get { return hasIncomingRiver; } } public bool HasOutgoingRiver { get { return hasOutgoingRiver; } } public HexDirection IncomingRiver { get { return incomingRiver; } } public HexDirection OutgoingRiver { get { return outgoingRiver; } }
Une question importante est de savoir s'il y a une rivière dans la cellule, quels que soient les détails. Par conséquent, ajoutons également une propriété pour cela. public bool HasRiver { get { return hasIncomingRiver || hasOutgoingRiver; } }
Une autre question logique: c'est le début ou la fin de la rivière dans la cellule. Si l'état de la rivière entrante et sortante est différent, alors c'est juste le cas. Par conséquent, nous en ferons une autre propriété. public bool HasRiverBeginOrEnd { get { return hasIncomingRiver != hasOutgoingRiver; } }
Et enfin, il sera utile de savoir si la rivière traverse une certaine crête, qu'elle soit entrante ou sortante. public bool HasRiverThroughEdge (HexDirection direction) { return hasIncomingRiver && incomingRiver == direction || hasOutgoingRiver && outgoingRiver == direction; }
Enlèvement de la rivière
Avant de commencer à ajouter une rivière à une cellule, implémentons d'abord la prise en charge de la suppression de la rivière. Pour commencer, nous écrirons une méthode pour ne retirer que la partie sortante de la rivière.S'il n'y a pas de rivière sortante dans la cellule, alors rien ne doit être fait. Sinon, désactivez-le et effectuez la mise à jour. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; Refresh(); }
Mais ce n'est pas tout. La rivière sortante doit se déplacer quelque part. Par conséquent, il doit y avoir un voisin avec la rivière entrante. Nous devons aussi nous débarrasser d'elle. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; Refresh(); HexCell neighbor = GetNeighbor(outgoingRiver); neighbor.hasIncomingRiver = false; neighbor.Refresh(); }
Une rivière ne peut-elle pas sortir d'une carte?, . , .
Supprimer une rivière d'une cellule ne change que l'apparence de cette cellule. Contrairement à la modification de la hauteur ou de la couleur, cela n'affecte pas les voisins. Par conséquent, nous devons mettre à jour uniquement la cellule elle-même, mais pas ses voisins. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; RefreshSelfOnly(); HexCell neighbor = GetNeighbor(outgoingRiver); neighbor.hasIncomingRiver = false; neighbor.RefreshSelfOnly(); }
Cette méthode RefreshSelfOnly
met simplement à jour le fragment auquel appartient la cellule. Comme nous ne changeons pas la rivière lors de l'initialisation du réseau, nous n'avons pas à nous inquiéter si un fragment a déjà été attribué. void RefreshSelfOnly () { chunk.Refresh(); }
La suppression des rivières entrantes fonctionne de la même manière. public void RemoveIncomingRiver () { if (!hasIncomingRiver) { return; } hasIncomingRiver = false; RefreshSelfOnly(); HexCell neighbor = GetNeighbor(incomingRiver); neighbor.hasOutgoingRiver = false; neighbor.RefreshSelfOnly(); }
Et la suppression de la rivière entière signifie simplement la suppression des parties entrantes et sortantes de la rivière. public void RemoveRiver () { RemoveOutgoingRiver(); RemoveIncomingRiver(); }
Ajout de rivières
Pour soutenir la création de rivières, nous avons besoin d'une méthode pour spécifier la rivière sortante de la cellule. Il doit redéfinir toutes les rivières sortantes précédentes et définir la rivière entrante correspondante.Pour commencer, nous n'avons rien à faire si la rivière existe déjà. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } }
Ensuite, nous devons nous assurer qu'il y a un voisin dans la bonne direction. De plus, les rivières ne peuvent pas remonter. Par conséquent, nous devons terminer l'opération si le voisin est plus élevé. HexCell neighbor = GetNeighbor(direction); if (!neighbor || elevation < neighbor.elevation) { return; }
Ensuite, nous devons nettoyer la rivière sortante précédente. Et nous devons également supprimer la rivière entrante, si elle est superposée à une nouvelle rivière sortante. RemoveOutgoingRiver(); if (hasIncomingRiver && incomingRiver == direction) { RemoveIncomingRiver(); }
Nous pouvons maintenant passer à la mise en place de la rivière sortante. hasOutgoingRiver = true; outgoingRiver = direction; RefreshSelfOnly();
Et n'oubliez pas de définir la rivière entrante pour une autre cellule après avoir supprimé sa rivière entrante actuelle, si elle existe. neighbor.RemoveIncomingRiver(); neighbor.hasIncomingRiver = true; neighbor.incomingRiver = direction.Opposite(); neighbor.RefreshSelfOnly();
Se débarrasser des rivières qui coulent
Maintenant que nous avons permis d'ajouter uniquement les bonnes rivières, d'autres actions peuvent encore créer les mauvaises. Lorsque nous modifions la hauteur de la cellule, nous devons à nouveau nous assurer avec force que les rivières ne peuvent que couler. Toutes les rivières irrégulières doivent être supprimées. public int Elevation { get { return elevation; } set { … if ( hasOutgoingRiver && elevation < GetNeighbor(outgoingRiver).elevation ) { RemoveOutgoingRiver(); } if ( hasIncomingRiver && elevation > GetNeighbor(incomingRiver).elevation ) { RemoveIncomingRiver(); } Refresh(); } }
paquet d'unitéChanger les rivières
Pour prendre en charge l'édition de la rivière, nous devons ajouter un commutateur de rivière à l'interface utilisateur. En fait. nous avons besoin de la prise en charge de trois modes d'édition. Nous devons soit ignorer les rivières, soit les ajouter, soit les supprimer. Nous pouvons utiliser une simple énumération d'aide de commutateurs pour suivre l'état. Comme nous ne l'utiliserons qu'à l'intérieur de l'éditeur, nous pouvons le définir à l'intérieur de la classe HexMapEditor
, avec le champ mode rivière. enum OptionalToggle { Ignore, Yes, No } OptionalToggle riverMode;
Et nous avons besoin d'une méthode pour changer le régime fluvial via l'interface utilisateur. public void SetRiverMode (int mode) { riverMode = (OptionalToggle)mode; }
Pour contrôler le régime de la rivière, ajoutez trois commutateurs à l'interface utilisateur et connectez-les au nouveau groupe de bascule, comme nous l'avons fait avec les couleurs. J'ai configuré les commutateurs de sorte que leurs étiquettes soient sous les cases à cocher. Pour cette raison, ils resteront suffisamment minces pour s'adapter aux trois options sur une seule ligne.Fleuves UIPourquoi ne pas utiliser une liste déroulante?, . dropdown list Unity Play. , .
Reconnaissance par glisser-déposer
Pour créer une rivière, nous avons besoin à la fois d'une cellule et d'une direction. Pour le moment, HexMapEditor
ne nous fournit pas ces informations. Par conséquent, nous devons ajouter la prise en charge du glisser-déposer d'une cellule à l'autre.Nous devons savoir si cette traînée sera correcte et également déterminer sa direction. Et pour reconnaître le glisser-déposer, nous devons nous souvenir de la cellule précédente. bool isDrag; HexDirection dragDirection; HexCell previousCell;
Initialement, lorsque le glissement n'est pas effectué, la cellule précédente ne l'est pas. Autrement dit, lorsqu'il n'y a pas d'entrée ou que nous n'interagissons pas avec la carte, vous devez lui attribuer une valeur null
. void Update () { if ( Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject() ) { HandleInput(); } else { previousCell = null; } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCells(hexGrid.GetCell(hit.point)); } else { previousCell = null; } }
La cellule actuelle est celle que nous avons trouvée en croisant le faisceau avec le maillage. Après avoir modifié les cellules, il est mis à jour et devient la cellule précédente pour une nouvelle mise à jour. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); EditCells(currentCell); previousCell = currentCell; } else { previousCell = null; } }
Après avoir déterminé la cellule actuelle, nous pouvons la comparer avec la cellule précédente, le cas échéant. Si nous obtenons deux cellules différentes, nous pouvons avoir le glisser-déposer correct et nous devons le vérifier. Sinon, ce n'est certainement pas un glisser-déposer. if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } EditCells(currentCell); previousCell = currentCell; isDrag = true; }
Comment vérifions-nous le glisser-déposer? Vérifier si la cellule actuelle est voisine de la précédente. Nous vérifions cela en contournant ses voisins dans un cycle. Si nous trouvons une correspondance, nous reconnaissons également immédiatement la direction de la traînée. void ValidateDrag (HexCell currentCell) { for ( dragDirection = HexDirection.NE; dragDirection <= HexDirection.NW; dragDirection++ ) { if (previousCell.GetNeighbor(dragDirection) == currentCell) { isDrag = true; return; } } isDrag = false; }
Allons-nous créer des traînées saccadées?, . «» , .
, . .
Changer les cellules
Maintenant que nous pouvons reconnaître le glisser-déposer, nous pouvons définir les rivières sortantes. Nous pouvons également supprimer des rivières; pour cela, le support par glisser-déposer n'est pas nécessaire. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } else if (isDrag && riverMode == OptionalToggle.Yes) { previousCell.SetOutgoingRiver(dragDirection); } } }
Ce code dessinera la rivière de la cellule précédente au courant. Mais il ignore la taille de la brosse. C'est assez logique, mais dessinons les rivières pour toutes les cellules fermées par le pinceau. Cela peut être fait en effectuant des opérations sur la cellule modifiée. Dans notre cas, nous devons nous assurer qu'une autre cellule existe vraiment. else if (isDrag && riverMode == OptionalToggle.Yes) { HexCell otherCell = cell.GetNeighbor(dragDirection.Opposite()); if (otherCell) { otherCell.SetOutgoingRiver(dragDirection); } }
Nous pouvons maintenant éditer les rivières, mais ne les voyons pas encore. Nous pouvons vérifier que cela fonctionne en examinant les cellules modifiées dans l'inspecteur de débogage.Une cellule avec une rivière dans l'inspecteur de débogage.Qu'est-ce qu'un inspecteur de débogage?. . , .
paquet d'unitéLits de rivière entre les cellules
Lors de la triangulation d'une rivière, nous devons considérer deux parties: l'emplacement du lit de la rivière et l'eau qui la traverse. Tout d'abord, nous allons créer un canal et quitter l'eau pour plus tard.La partie la plus simple de la rivière est l'endroit où elle coule en jonction entre les cellules. Pendant que nous triangulons cette zone avec une bande de trois quadruples. Nous pouvons y ajouter un lit de rivière en abaissant le quad central et en ajoutant deux parois de canaux.Ajout d'une rivière à une bande de côtes.Pour cela, dans le cas de la rivière, deux quads supplémentaires seront nécessaires et un canal avec deux parois verticales sera créé. Une autre approche consiste à utiliser quatre quadruples. Ensuite, nous abaissons le pic central pour créer un lit avec des murs en pente.Toujours quatre quad.L'utilisation constante du même nombre de quadrangles est pratique, alors choisissons cette option.Ajout de sommets de bord
La transition de trois à quatre par arête nécessite la création d'un sommet supplémentaire de l'arête. Nous réécrivons EdgeVertices
en renommant d'abord v4
en v5
, puis en renommant v3
en v4
. Les actions dans cet ordre garantissent que tout le code continue de référencer les bons sommets. Utilisez l'option renommer ou refactoriser de votre éditeur pour que les modifications s'appliquent partout. Sinon, vous devrez inspecter manuellement l'intégralité du code et apporter des modifications. public Vector3 v1, v2, v4, v5;
Après avoir renommé tout, ajoutez-en un nouveau v3
. public Vector3 v1, v2, v3, v4, v5;
Ajoutez un nouveau sommet au constructeur. Il est situé au milieu entre les sommets des coins. De plus, les autres sommets devraient maintenant être en ½ et ¾, et non en & frac13; et & frac23;. public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 0.25f); v3 = Vector3.Lerp(corner1, corner2, 0.5f); v4 = Vector3.Lerp(corner1, corner2, 0.75f); v5 = corner2; }
Ajouter v3
et ajouter TerraceLerp
. public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); result.v5 = HexMetrics.TerraceLerp(a.v5, b.v5, step); return result; }
Maintenant, je HexMesh
dois inclure un sommet supplémentaire dans les triangles en éventail de la nervure. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); AddTriangle(center, edge.v4, edge.v5); AddTriangleColor(color); }
Et aussi dans ses rayures de quadrangles. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); AddQuadColor(c1, c2); }
Comparaison de quatre et cinq sommets par arête.La hauteur du lit de la rivière
Nous avons créé le canal en abaissant le bas en haut de la nervure. Il détermine la position verticale du lit de la rivière. Bien que la position verticale exacte de chaque cellule soit déformée, nous devons maintenir la même hauteur du lit de la rivière dans les cellules de même hauteur. Grâce à cette eau, elle n'a pas à s'écouler en amont. De plus, le lit doit être suffisamment bas pour rester en dessous, même dans le cas des cellules verticales les plus déviées, tout en laissant suffisamment de place pour l'eau.Définissons ce décalage HexMetrics
et exprimons-le en hauteur. Des décalages d'un niveau suffiront. public const float streamBedElevationOffset = -1f;
Nous pouvons utiliser cette métrique pour ajouter des propriétés HexCell
pour obtenir la position verticale du lit de la rivière cellulaire. public float StreamBedY { get { return (elevation + HexMetrics.streamBedElevationOffset) * HexMetrics.elevationStep; } }
Création d'une chaîne
Lorsqu'une HexMesh
des six parties triangulaires d'une cellule est triangulée, nous pouvons déterminer si une rivière coule le long de son bord. Si c'est le cas, nous pouvons alors abaisser le pic médian de la côte à la hauteur du lit de la rivière. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; } TriangulateEdgeFan(center, e, cell.Color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
Modifiez le sommet médian de la côte.Nous pouvons voir comment apparaissent les premiers signes de la rivière, mais des trous apparaissent dans le relief. Pour les fermer, nous devons changer un autre bord, puis trianguler la connexion. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v5 + bridge ); if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; } … }
Canaux terminés des articulations des côtes.paquet d'unitéLits de rivière passant à travers une cellule
Maintenant, nous avons les bons lits de rivière entre les cellules. Mais lorsque la rivière traverse la cellule, les canaux se terminent toujours en son centre. Pour résoudre ce problème devra travailler. Commençons par le cas où une rivière traverse directement une cellule, d'un bord à l'autre.S'il n'y a pas de rivière, chaque partie de la cellule peut être un simple éventail de triangles. Mais lorsque la rivière coule directement, il faut insérer un canal. En fait, nous devons étirer le sommet central en une ligne, transformant ainsi les deux triangles du milieu en quadrangles. Puis l'éventail de triangles se transforme en trapèze.Nous insérons le canal dans le triangle.Ces canaux seront beaucoup plus longs que ceux passant par la connexion des cellules. Cela devient apparent lorsque les positions des sommets sont déformées. Par conséquent, divisons le trapèze en deux segments en insérant un autre ensemble d'arêtes de sommet au milieu entre le centre et l'arête.Triangulation des canaux.Étant donné que la triangulation avec une rivière sera très différente de la triangulation sans rivière, créons-en une méthode distincte. Si nous avons une rivière, nous utilisons cette méthode, sinon nous laisserons un éventail de triangles. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; TriangulateWithRiver(direction, cell, center, e); } } else { TriangulateEdgeFan(center, e, cell.Color); } if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } } void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { }
Trous dans lesquels il devrait y avoir des rivières.Pour mieux voir ce qui se passe, désactivez temporairement la distorsion cellulaire. public const float cellPerturbStrength = 0f;
Pics non déformés.Triangulation directement à travers la cellule
Pour créer un canal directement à travers une partie de la cellule, nous devons étirer le centre en ligne. Cette ligne doit avoir la même largeur que le canal. Nous pouvons trouver le sommet gauche en déplaçant ¼ de la distance du centre au premier coin de la partie précédente. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { Vector3 centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; }
De même pour le sommet droit. Dans ce cas, nous avons besoin du deuxième coin de la partie suivante. Vector3 centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; Vector3 centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f;
La ligne médiane peut être trouvée en créant des arêtes de sommet entre le centre et l'arête. EdgeVertices m = new EdgeVertices( Vector3.Lerp(centerL, e.v1, 0.5f), Vector3.Lerp(centerR, e.v5, 0.5f) );
Ensuite, changez le sommet moyen de la nervure centrale, ainsi que le centre, car ils deviendront les points inférieurs du canal. m.v3.y = center.y = e.v3.y;
Maintenant, nous pouvons utiliser TriangulateEdgeStrip
pour remplir l'espace entre la ligne médiane et la ligne de bord. TriangulateEdgeStrip(m, cell.Color, e, cell.Color);
Canaux compressés.Malheureusement, les canaux semblent compressés. Cela se produit car les sommets médians de la nervure sont trop proches les uns des autres. Pourquoi est-ce arrivé?Si nous supposons que la longueur du bord extérieur est de 1, la longueur de la ligne médiane sera de ½. Étant donné que le bord central est au milieu entre eux, sa longueur doit être égale à ¾.La largeur du canal est de ½ et doit rester constante. Comme la longueur du bord central est is, il ne reste que ¼, selon & frac18; des deux côtés du canal.Longueurs relatives.Comme la longueur du bord central est ¾, alors & frac18; devient par rapport à la longueur de la nervure centrale égale à & frac16 ;. Cela signifie que ses deuxième et quatrième sommets doivent être interpolés avec des sixièmes, pas des quarts.Nous pouvons fournir un support pour une telle interpolation alternative en ajoutant à EdgeVertices
un autre constructeur. Au lieu d'interpolations fixes pour v2
et v4
utilisons un paramètre. public EdgeVertices (Vector3 corner1, Vector3 corner2, float outerStep) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, outerStep); v3 = Vector3.Lerp(corner1, corner2, 0.5f); v4 = Vector3.Lerp(corner1, corner2, 1f - outerStep); v5 = corner2; }
Nous pouvons maintenant l'utiliser avec & frac16; c HexMesh.TriangulateWithRiver
. EdgeVertices m = new EdgeVertices( Vector3.Lerp(centerL, e.v1, 0.5f), Vector3.Lerp(centerR, e.v5, 0.5f), 1f / 6f );
Chaînes directes.Après avoir rectifié le canal, nous pouvons passer à la deuxième partie du trapèze. Dans ce cas, nous ne pouvons pas utiliser la bande de côtes, nous devons donc le faire manuellement. Créons d'abord des triangles sur les côtés. AddTriangle(centerL, m.v1, m.v2); AddTriangleColor(cell.Color); AddTriangle(centerR, m.v4, m.v5); AddTriangleColor(cell.Color);
Triangles latéraux.Il semble bon, remplissons donc l'espace restant avec deux quadrangles, créant la dernière partie du canal. AddTriangle(centerL, m.v1, m.v2); AddTriangleColor(cell.Color); AddQuad(centerL, center, m.v2, m.v3); AddQuadColor(cell.Color); AddQuad(center, centerR, m.v3, m.v4); AddQuadColor(cell.Color); AddTriangle(centerR, m.v4, m.v5); AddTriangleColor(cell.Color);
En fait, nous n'avons pas d'alternative AddQuadColor
ne nécessitant qu'un seul paramètre. Alors que nous n'en avions pas besoin. Créons-le donc. void AddQuadColor (Color color) { colors.Add(color); colors.Add(color); colors.Add(color); colors.Add(color); }
Chaînes droites terminées.Début et fin de la triangulation
La triangulation d'une partie qui n'a que le début ou la fin d'une rivière est assez différente, et nécessite donc sa propre méthode. Par conséquent, nous allons vérifier cela Triangulate
et appeler la méthode appropriée. if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; if (cell.HasRiverBeginOrEnd) { TriangulateWithRiverBeginOrEnd(direction, cell, center, e); } else { TriangulateWithRiver(direction, cell, center, e); } } }
Dans ce cas, nous voulons terminer le canal au centre, mais nous utilisons toujours deux étapes pour cela. Par conséquent, nous allons à nouveau créer le bord médian entre le centre ou le bord. Puisque nous voulons terminer le canal, nous sommes très heureux qu'il soit compressé. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) ); }
Afin que le canal ne devienne pas trop peu profond, nous assignerons la hauteur du lit de la rivière au pic moyen. Mais le centre n'a pas besoin d'être changé. m.v3.y = e.v3.y;
On peut trianguler avec une bande de côtes et un éventail. TriangulateEdgeStrip(m, cell.Color, e, cell.Color); TriangulateEdgeFan(center, m, cell.Color);
Points de départ et d'arrivée.Virages en une étape
Ensuite, considérez les virages serrés qui zigzaguent entre les cellules adjacentes. Nous les gérerons aussi TriangulateWithRiver
. Par conséquent, nous devons déterminer avec quel type de rivière nous travaillons.Rivière Zigzag.Si la cellule a une rivière qui coule dans la direction opposée, ainsi que dans la direction avec laquelle nous travaillons, alors ce devrait être une rivière droite. Dans ce cas, nous pouvons enregistrer la ligne médiane que nous avons déjà calculée. Sinon, il revient à un point, repliant la ligne médiane. Vector3 centerL, centerR; if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else { centerL = centerR = center; }
Zigzags recourbés.Nous pouvons reconnaître les virages serrés en vérifiant si la cellule a une rivière traversant la partie suivante ou précédente de la cellule. S'il y en a, alors nous devons aligner la ligne centrale avec le bord entre celui-ci et la partie voisine. Nous pouvons le faire en plaçant le côté correspondant de la ligne au milieu entre le centre et l'angle commun. L'autre côté de la ligne devient alors le centre. if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 0.5f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 0.5f); centerR = center; } else { centerL = centerR = center; }
Après avoir décidé où se trouvent les points gauche et droit, nous pouvons déterminer le centre résultant en les faisant la moyenne. if (cell.HasRiverThroughEdge(direction.Opposite())) { … } center = Vector3.Lerp(centerL, centerR, 0.5f);
Côte centrale décalée.Bien que le canal ait la même largeur des deux côtés, il semble assez compressé. Cela est dû au fait de tourner la ligne médiane de 60 °. Vous pouvez lisser cet effet en augmentant légèrement la largeur de la ligne centrale. Au lieu d'interpoler avec ½, nous utilisons & frac23 ;. else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 2f / 3f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 2f / 3f); centerR = center; }
Zigzag sans compression.Virages en deux étapes
Les autres cas se situent entre des zigzags et des rivières droites. Ce sont des virages en deux étapes qui créent des rivières légèrement courbes.La rivière sinueuse.Pour distinguer entre deux orientations possibles, nous devons utiliser direction.Next().Next()
. Mais nous allons en ajoutant le rendre plus pratique des HexDirection
méthodes d'extension Next2
et Previous2
. public static HexDirection Previous2 (this HexDirection direction) { direction -= 2; return direction >= HexDirection.NE ? direction : (direction + 6); } public static HexDirection Next2 (this HexDirection direction) { direction += 2; return direction <= HexDirection.NW ? direction : (direction - 6); }
Retour à HexMesh.TriangulateWithRiver
. Maintenant, nous pouvons reconnaître la direction de notre rivière sinueuse avec direction.Next2()
. if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 2f / 3f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 2f / 3f); centerR = center; } else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = centerR = center; } else { centerL = centerR = center; }
Dans ces deux derniers cas, nous devons déplacer la ligne médiane vers la partie de la cellule située à l'intérieur de la courbe. Si nous avions un vecteur au milieu d'un bord solide, nous pourrions l'utiliser pour positionner le point final. Imaginons que nous ayons une méthode pour cela. else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = center; centerR = center + HexMetrics.GetSolidEdgeMiddle(direction.Next()) * 0.5f; } else { centerL = center + HexMetrics.GetSolidEdgeMiddle(direction.Previous()) * 0.5f; centerR = center; }
Bien sûr, nous devons maintenant ajouter une telle méthode à HexMetrics
. Il lui suffit de faire la moyenne de deux vecteurs d'angles adjacents et d'appliquer le coefficient d'intégrité. public static Vector3 GetSolidEdgeMiddle (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * (0.5f * solidFactor); }
Courbes légèrement compressées.Nos axes médians sont maintenant correctement tournés de 30 °. Mais ils ne sont pas assez longs, c'est pourquoi les canaux sont un peu compressés. Cela se produit parce que le milieu de la côte est plus proche du centre que l'angle de la côte. Sa distance est égale au rayon intérieur, pas à l'extérieur. Autrement dit, nous travaillons à la mauvaise échelle.Nous convertissons déjà le rayon externe en rayon interne à HexMetrics
. Nous devons effectuer l'opération inverse. Rendons donc les deux facteurs de conversion disponibles via HexMetrics
. public const float outerToInner = 0.866025404f; public const float innerToOuter = 1f / outerToInner; public const float outerRadius = 10f; public const float innerRadius = outerRadius * outerToInner;
Nous pouvons maintenant passer à la bonne échelle HexMesh.TriangulateWithRiver
. Les chaînes resteront encore un peu serrées à cause de leur tour, mais cela est beaucoup moins prononcé que dans le cas des zigzags. Par conséquent, nous n'avons pas besoin de compenser cela. else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = center; centerR = center + HexMetrics.GetSolidEdgeMiddle(direction.Next()) * (0.5f * HexMetrics.innerToOuter); } else { centerL = center + HexMetrics.GetSolidEdgeMiddle(direction.Previous()) * (0.5f * HexMetrics.innerToOuter); centerR = center; }
Courbes lisses.paquet d'unitéTriangulation au voisinage des rivières
Nos rivières sont prêtes. Mais nous n'avons pas encore triangulé d'autres parties des cellules contenant les rivières. Nous allons maintenant fermer ces trous.Trous près des canaux.Si la cellule a une rivière, mais qu'elle ne coule pas dans la direction actuelle, alors Triangulate
nous appellerons une nouvelle méthode dans. if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; if (cell.HasRiverBeginOrEnd) { TriangulateWithRiverBeginOrEnd(direction, cell, center, e); } else { TriangulateWithRiver(direction, cell, center, e); } } else { TriangulateAdjacentToRiver(direction, cell, center, e); } } else { TriangulateEdgeFan(center, e, cell.Color); }
Dans cette méthode, nous remplissons le triangle cellulaire avec une bande et un ventilateur. Un simple ventilateur ne nous suffira pas, car les pics doivent correspondre au bord médian des parties contenant la rivière. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) ); TriangulateEdgeStrip(m, cell.Color, e, cell.Color); TriangulateEdgeFan(center, m, cell.Color); }
Superposition dans les courbes et les rivières droites.Faire correspondre la chaîne
Bien sûr, nous devons faire correspondre le centre que nous utilisons à la partie centrale utilisée par les parties de la rivière. Avec les zigzags, tout est en ordre, et les courbes et les rivières droites nécessitent de l'attention. Par conséquent, nous devons déterminer à la fois le type de rivière et son orientation relative.Commençons par vérifier si nous sommes à l'intérieur de la courbe. Dans ce cas, les directions précédente et suivante contiennent la rivière. Si c'est le cas, nous devons déplacer le centre vers le bord. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } } EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) );
Correction d'un cas où la rivière coulait des deux côtés.Si nous avons une rivière dans une direction différente, mais pas dans la précédente, alors nous vérifions si elle est droite. Si c'est le cas, déplacez le centre vers le premier coin. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } else if ( cell.HasRiverThroughEdge(direction.Previous2()) ) { center += HexMetrics.GetFirstSolidCorner(direction) * 0.25f; } }
Correction d'une demi-superposition avec une rivière droite.Nous avons donc résolu le problème avec la moitié des parties adjacentes aux rivières droites. Le dernier cas - nous avons une rivière dans la direction précédente, et elle est droite. Dans ce cas, vous devez déplacer le centre vers le coin suivant. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } else if ( cell.HasRiverThroughEdge(direction.Previous2()) ) { center += HexMetrics.GetFirstSolidCorner(direction) * 0.25f; } } else if ( cell.HasRiverThroughEdge(direction.Previous()) && cell.HasRiverThroughEdge(direction.Next2()) ) { center += HexMetrics.GetSecondSolidCorner(direction) * 0.25f; }
Plus de superpositions.paquet d'unitéGénéralisation HexMesh
Nous avons terminé la triangulation des canaux. Maintenant, nous pouvons les remplir d'eau. Étant donné que l'eau est différente de la terre, nous devrons utiliser un maillage différent avec différentes données de sommet et différents matériaux. Ce serait très pratique si nous pouvions utiliser à la HexMesh
fois des sushis et de l'eau. Généralisons donc HexMesh
en la transformant en une classe qui traite de ces maillages, quelle que soit leur utilisation. Nous passerons la tâche de trianguler ses cellules HexGridChunk
.Déplacer la méthode Perturb
Étant donné que la méthode est Perturb
assez généralisée et sera utilisée à différents endroits, déplaçons-la HexMetrics
. Tout d'abord, renommez-le en HexMetrics.Perturb
. Il s'agit d'un nom de méthode incorrect, mais il refacture tout le code pour son utilisation appropriée. Si votre éditeur de code possède des fonctionnalités spéciales pour déplacer des méthodes, utilisez-le.En déplaçant la méthode vers l'intérieur HexMetrics
, rendez-la générale et statique, puis corrigez son nom. public static Vector3 Perturb (Vector3 position) { Vector4 sample = SampleNoise(position); position.x += (sample.x * 2f - 1f) * cellPerturbStrength; position.z += (sample.z * 2f - 1f) * cellPerturbStrength; return position; }
Déplacement des méthodes de triangulation
Dans HexGridChunk
le changement de variables hexMesh
dans la variable partagée terrain
. public HexMesh terrain;
Ensuite, nous refactorisons toutes les méthodes Add…
de HexMesh
c terrain.Add…
. Déplacez ensuite toutes les méthodes Triangulate…
vers HexGridChunk
. Après cela, vous pouvez corriger les noms des méthodes Add…
dans HexMesh
et les rendre généraux. En conséquence, toutes les méthodes de triangulation complexes seront trouvées HexGridChunk
et les méthodes simples pour ajouter des données au maillage resteront HexMesh
.Nous n'avons pas encore fini. Maintenant, il HexGridChunk.LateUpdate
devrait appeler sa propre méthode Triangulate
. En outre, il ne doit plus passer de cellules en argument. Par conséquent, il Triangulate
peut perdre son paramètre. Et il doit déléguer le nettoyage et l'application des données de maillage HexMesh
. void LateUpdate () { Triangulate();
Ajoutez les méthodes nécessaires Clear
et Apply
en HexMesh
. public void Clear () { hexMesh.Clear(); vertices.Clear(); colors.Clear(); triangles.Clear(); } public void Apply () { hexMesh.SetVertices(vertices); hexMesh.SetColors(colors); hexMesh.SetTriangles(triangles, 0); hexMesh.RecalculateNormals(); meshCollider.sharedMesh = hexMesh; }
Qu'en est-il de SetVertices, SetColors et SetTriangles?Mesh
. . , .
SetTriangles
integer, . , .
Enfin, attachez manuellement l'enfant du maillage au préfabriqué de fragment. Nous ne pouvons plus le faire automatiquement, car nous ajouterons bientôt un deuxième enfant au maillage. Renommez-le en Terrain pour indiquer son objectif.Attribuez un relief.Renommer un enfant préfabriqué ne fonctionne pas?. , . , Apply , . .
Création de pools de listes
Bien que nous ayons déplacé un peu de code, notre carte devrait toujours fonctionner de la même manière qu'auparavant. L'ajout d'un autre maillage au fragment ne changera rien à cela. Mais si nous faisons cela avec le présent HexMesh
, des erreurs peuvent survenir.Le problème est que nous avons supposé que nous ne travaillerions qu'avec un maillage à la fois. Cela nous a permis d'utiliser des listes statiques pour stocker des données de maillage temporaires. Mais après avoir ajouté de l'eau, nous travaillerons simultanément avec deux mailles, nous ne pourrons donc plus utiliser de listes statiques.Cependant, nous ne reviendrons pas sur les ensembles de listes pour chaque instance HexMesh
. Au lieu de cela, nous utilisons un pool de listes statiques. Par défaut, ce pool n'existe pas, commençons donc par créer nous-mêmes une classe de pool de liste commune. public static class ListPool<T> { }
Comment fonctionne ListPool <T>?, List<int>
. <T>
ListPool
, , . , T
( template).
Pour stocker une collection de listes dans un pool, nous pouvons utiliser la pile. Je n'utilise généralement pas de listes car Unity ne les sérialise pas, mais dans ce cas, cela n'a pas d'importance. using System.Collections.Generic; public static class ListPool<T> { static Stack<List<T>> stack = new Stack<List<T>>(); }
Que signifie stack <list <t>>?. , . .
Ajoutez une méthode statique commune pour obtenir la liste du pool. Si la pile n'est pas vide, nous allons extraire la liste du haut et retourner celle-ci. Sinon, nous créerons une nouvelle liste en place. public static List<T> Get () { if (stack.Count > 0) { return stack.Pop(); } return new List<T>(); }
Pour réutiliser des listes, vous devez les ajouter au pool une fois que vous avez fini de travailler avec elles. ListPool
effacera la liste et la poussera sur la pile. public static void Add (List<T> list) { list.Clear(); stack.Push(list); }
Maintenant, nous pouvons utiliser les piscines dans HexMesh
. Remplacez les listes statiques par des liens privés non statiques. Marquons-les NonSerialized
pour que Unity ne les conserve pas lors de la recompilation. Ou écrivez System.NonSerialized
ou ajoutez using System;
au début du script. [NonSerialized] List<Vector3> vertices; [NonSerialized] List<Color> colors; [NonSerialized] List<int> triangles;
Puisque le maillage est nettoyé juste avant d'y ajouter de nouvelles données, c'est ici que vous devez obtenir les listes des pools. public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); colors = ListPool<Color>.Get(); triangles = ListPool<int>.Get(); }
Après avoir appliqué ces maillages, nous n'en avons plus besoin, nous pouvons donc les ajouter ici aux pools. public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); hexMesh.SetColors(colors); ListPool<Color>.Add(colors); hexMesh.SetTriangles(triangles, 0); ListPool<int>.Add(triangles); hexMesh.RecalculateNormals(); meshCollider.sharedMesh = hexMesh; }
Nous avons donc mis en œuvre plusieurs utilisations des listes, quel que soit le nombre de mailles que nous remplissons en même temps.Collisionneur en option
Bien que notre terrain ait besoin d'un collisionneur, il n'est pas vraiment nécessaire pour les rivières. Les rayons traversent simplement l'eau et se croisent avec le canal en dessous. Faisons en sorte que nous puissions configurer la présence d'un collisionneur pour HexMesh
. Nous le réalisons en ajoutant un champ commun bool useCollider
. Pour le terrain, nous l'activons. public bool useCollider;
Utilisation d'un collisionneur de mailles.Nous devons créer et affecter le collisionneur uniquement lorsqu'il est activé. void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); if (useCollider) { meshCollider = gameObject.AddComponent<MeshCollider>(); } hexMesh.name = "Hex Mesh"; } public void Apply () { … if (useCollider) { meshCollider.sharedMesh = hexMesh; } … }
Couleurs en option
Les couleurs des sommets peuvent également être facultatives. Nous en avons besoin pour démontrer différents types de relief, mais l'eau ne change pas de couleur. Nous pouvons les rendre facultatifs tout comme nous avons rendu le collisionneur facultatif. public bool useCollider, useColors; public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useColors) { colors = ListPool<Color>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useColors) { hexMesh.SetColors(colors); ListPool<Color>.Add(colors); } … }
Bien sûr, le terrain doit utiliser les couleurs des sommets, alors activez-les.Utilisation de couleurs.UV en option
Pendant ce temps, nous pouvons également ajouter la prise en charge des coordonnées UV facultatives. Bien que le relief ne les utilise pas, nous en aurons besoin pour l'eau. public bool useCollider, useColors, useUVCoordinates; [NonSerialized] List<Vector2> uvs; public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useColors) { colors = ListPool<Color>.Get(); } if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useColors) { hexMesh.SetColors(colors); ListPool<Color>.Add(colors); } if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } … }
Nous n'utilisons pas de coordonnées UV.Pour utiliser cette fonction, créez des méthodes pour ajouter des coordonnées UV aux triangles et quadrangles. public void AddTriangleUV (Vector2 uv1, Vector2 uv2, Vector3 uv3) { uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); } public void AddQuadUV (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) { uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); uvs.Add(uv4); }
Ajoutons une méthode supplémentaire AddQuadUV
pour ajouter facilement une zone UV rectangulaire. C'est le cas standard lorsque le quad et sa texture sont les mêmes, nous l'utiliserons pour l'eau de la rivière. public void AddQuadUV (float uMin, float uMax, float vMin, float vMax) { uvs.Add(new Vector2(uMin, vMin)); uvs.Add(new Vector2(uMax, vMin)); uvs.Add(new Vector2(uMin, vMax)); uvs.Add(new Vector2(uMax, vMax)); }
paquet d'unitéRivières actuelles
Enfin, il est temps de créer de l'eau! Nous le ferons avec un quad, qui indiquera la surface de l'eau. Et puisque nous travaillons avec les rivières, l'eau doit couler. Pour ce faire, nous utilisons des coordonnées UV indiquant l'orientation de la rivière. Pour visualiser cela, nous avons besoin d'un nouveau shader. Par conséquent, créez un nouveau shader standard et appelez-le River . Modifiez-le pour que les coordonnées UV soient enregistrées dans les canaux d'albédo vert et rouge. Shader "Custom/River" { … 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; o.Albedo.rg = IN.uv_MainTex; } ENDCG } FallBack "Diffuse" }
Ajoutez au HexGridChunk
champ général HexMesh rivers
. Nous le nettoyons et l'appliquons de la même manière qu'en cas de relief. public HexMesh terrain, rivers; public void Triangulate () { terrain.Clear(); rivers.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); }
Aurons-nous des appels de tirage supplémentaires, même si nous n'avons pas de rivières?Unity , . , - .
Modifiez le préfabriqué (via l'instance), dupliquez son objet terrain, renommez-le Rivers et connectez-le.Fragment préfabriqué avec rivières.Créez le matériau River à l' aide de notre nouveau shader et faites en sorte que l'objet Rivers l' utilise . Nous avons également configuré le composant maillage hexagonal de l'objet afin qu'il utilise les coordonnées UV, mais pas les couleurs de sommet ni le collisionneur.Sous-objet Rivers.Trianguler l'eau
Avant de pouvoir trianguler l'eau, nous devons déterminer le niveau de sa surface. Faisons un changement de hauteur HexMetrics
, comme nous l'avons fait avec le lit de la rivière. Étant donné que la distorsion verticale de la cellule est égale à la moitié du décalage de hauteur, utilisons-la pour déplacer la surface de la rivière. Nous garantissons donc que l'eau ne sera jamais au-dessus de la topographie de la cellule. public const float riverSurfaceElevationOffset = -0.5f;
Pourquoi ne pas le faire un peu plus bas?, . , .
Ajoutez une HexCell
propriété pour obtenir la position verticale de la surface de sa rivière. public float RiverSurfaceY { get { return (elevation + HexMetrics.riverSurfaceElevationOffset) * HexMetrics.elevationStep; } }
Maintenant, nous pouvons travailler HexGridChunk
! Puisque nous allons créer de nombreux quadrilatères de rivières, ajoutons une méthode distincte pour cela. Donnons-lui quatre sommets et une hauteur comme paramètres. Cela nous permettra de définir commodément la position verticale des quatre sommets simultanément avant d'ajouter un quad. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y ) { v1.y = v2.y = v3.y = v4.y = y; rivers.AddQuad(v1, v2, v3, v4); }
Nous ajouterons ici les coordonnées UV du quadrilatère. Il suffit de faire le tour de gauche à droite et de bas en haut. rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0f, 1f);
TriangulateWithRiver
- C'est la première méthode à laquelle nous ajouterons les quadrangles des rivières. Le premier quad se situe entre le centre et le milieu. Le second est entre le milieu et la côte. Nous utilisons simplement les sommets que nous avons déjà. Étant donné que ces pics seront sous-estimés, l'eau sera par conséquent partiellement sous les parois inclinées du canal. Par conséquent, nous n'avons pas à nous soucier de la position exacte du bord de l'eau. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateRiverQuad(centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY); TriangulateRiverQuad(m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY); }
Les premiers signes d'eau.Pourquoi la largeur de l'eau change-t-elle?, , — . . .
Se déplacer avec le flux
Actuellement, les coordonnées UV ne sont pas cohérentes avec la direction de la rivière. Nous devons maintenir la cohérence ici. Supposons que la coordonnée U soit 0 sur le côté gauche de la rivière et 1 sur la droite, en regardant en aval. Et la coordonnée V devrait varier de 0 à 1 en direction de la rivière.En utilisant cette spécification, les UV seront corrects lors de la triangulation de la rivière sortante, mais ils se révéleront incorrects et devront être retournés lors de la triangulation de la rivière entrante. Pour simplifier le travail, ajoutez au TriangulateRiverQuad
paramètre bool reversed
. Utilisez-le pour retourner les UV si nécessaire. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, bool reversed ) { v1.y = v2.y = v3.y = v4.y = y; rivers.AddQuad(v1, v2, v3, v4); if (reversed) { rivers.AddQuadUV(1f, 0f, 1f, 0f); } else { rivers.AddQuadUV(0f, 1f, 0f, 1f); } }
Comme TriangulateWithRiver
nous savons que nous devons tourner la direction, lorsqu'il s'agit de rivière entrant. bool reversed = cell.IncomingRiver == direction; TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, reversed ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, reversed );
La direction convenue des rivières.Le début et la fin de la rivière
À l'intérieur, TriangulateWithRiverBeginOrEnd
nous devons seulement vérifier si nous avons une rivière entrante pour déterminer la direction du débit. Ensuite, nous pouvons insérer une autre rivière quad entre le milieu et la côte. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … bool reversed = cell.HasIncomingRiver; TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, reversed ); }
La partie entre le centre et le milieu est un triangle, nous ne pouvons donc pas l'utiliser TriangulateRiverQuad
. La seule différence significative ici est que le pic central est au milieu de la rivière. Par conséquent, sa coordonnée U est toujours égale à ½. center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); if (reversed) { rivers.AddTriangleUV( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { rivers.AddTriangleUV( new Vector2(0.5f, 0f), new Vector2(0f, 1f), new Vector2(1f, 1f) ); }
L'eau au début et à la fin.Y a-t-il des portions d'eau manquantes aux extrémités?, quad , . . .
, . , . .
Flux entre les cellules
Lors de l'ajout d'eau entre les cellules, nous devons faire attention à la différence de hauteur. Pour que l'eau puisse descendre les pentes et les falaises, elle TriangulateRiverQuad
doit supporter deux paramètres de hauteur. Ajoutons donc un deuxième. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, bool reversed ) { v1.y = v2.y = y1; v3.y = v4.y = y2; rivers.AddQuad(v1, v2, v3, v4); if (reversed) { rivers.AddQuadUV(1f, 0f, 1f, 0f); } else { rivers.AddQuadUV(0f, 1f, 0f, 1f); } }
Aussi, pour plus de commodité, ajoutons une option qui recevra une hauteur. Il appellera simplement une autre méthode. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, bool reversed ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, reversed); }
Maintenant, nous pouvons ajouter quad river et in TriangulateConnection
. Étant entre les cellules, nous ne pouvons pas immédiatement savoir de quel type de rivière il s'agit. Pour déterminer si un virage est nécessaire, nous devons vérifier si nous avons une rivière entrante et si elle se déplace dans notre direction. if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, cell.HasIncomingRiver && cell.IncomingRiver == direction ); }
La rivière terminée.Étirement des coordonnées V
Jusqu'à présent, dans chaque segment de la rivière, nous avons des coordonnées V allant de 0 à 1. Autrement dit, il n'y en a que quatre sur la cellule. Cinq si l'on ajoute également des connexions entre les cellules. Tout ce que nous utilisons pour texturer la rivière, il doit être répété autant de fois.Nous pouvons réduire le nombre de répétitions en étirant les coordonnées V pour qu'elles passent de 0 à 1 dans toute la cellule plus une connexion. Cela peut être fait en augmentant la coordonnée V dans chaque segment de 0,2. Si nous mettons 0,4 au centre, au milieu, cela deviendra 0,6 et sur le bord, il atteindra 0,8. Ensuite, dans la connexion de cellule, la valeur sera 1.Si la rivière coule dans la direction opposée, nous pouvons toujours mettre 0,4 au centre, mais au milieu, elle devient 0,2, et sur le bord - 0. Si nous continuons jusqu'à ce que la cellule se rejoigne, nous obtenons -0,2 en conséquence. Ceci est normal car il est similaire à 0,8 pour une texture avec mode de filtrage répété, tout comme 0 équivaut à 1.Changement de coordonnées V.Pour créer un support pour cela, nous devons ajouter TriangulateRiverQuad
un paramètre supplémentaire. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, float v, bool reversed ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, v, reversed); } void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float v, bool reversed ) { … }
Lorsque la direction n'est pas inversée, nous utilisons simplement les coordonnées transmises au bas du quadrilatère et ajoutons 0,2 en haut. else { rivers.AddQuadUV(0f, 1f, v, v + 0.2f); }
Nous pouvons travailler avec une direction inversée en soustrayant les coordonnées de 0,8 et 0,6. if (reversed) { rivers.AddQuadUV(1f, 0f, 0.8f - v, 0.6f - v); }
Maintenant, nous devons transmettre les coordonnées correctes, comme s'il s'agissait d'un fleuve sortant. Commençons par TriangulateWithRiver
. TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, 0.4f, reversed ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed );
Modifiez ensuite TriangulateConnection
comme suit. TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction );
Et enfin TriangulateWithRiverBeginOrEnd
. TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed ); center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); if (reversed) { rivers.AddTriangleUV( new Vector2(0.5f, 0.4f), new Vector2(1f, 0.2f), new Vector2(0f, 0.2f) ); } else { rivers.AddTriangleUV( new Vector2(0.5f, 0.4f), new Vector2(0f, 0.6f), new Vector2(1f, 0.6f) ); }
Coordonnées V étirées.Pour afficher correctement le pliage des coordonnées V, assurez-vous qu'elles restent positives dans le shader de la rivière. if (IN.uv_MainTex.y < 0) { IN.uv_MainTex.y += 1; } o.Albedo.rg = IN.uv_MainTex;
Coordonnées réduites V.package d'unitéAnimation fluviale
Après avoir fini avec les coordonnées UV, nous pouvons passer à l'animation des rivières. Le shader de rivière le fera pour que nous n'ayons pas à mettre à jour constamment le maillage.Nous ne créerons pas de shader de rivière complexe dans ce tutoriel, mais nous le ferons plus tard. Pour l'instant, nous allons créer un effet simple qui permet de comprendre le fonctionnement de l'animation.L'animation est créée en décalant les coordonnées V en fonction de l'heure de la partie. Unity vous permet d'obtenir sa valeur à l'aide d'une variable _Time
. Sa composante Y contient le temps inchangé, que nous utilisons. D'autres composants contiennent des échelles de temps différentes.Nous allons nous débarrasser du pliage le long de V, car nous n'en avons plus besoin. Au lieu de cela, nous soustrayons l'heure actuelle de la coordonnée V. Cela déplace la coordonnée vers le bas, ce qui crée l'illusion du courant qui coule en aval de la rivière. // if (IN.uv_MainTex.y < 0) { // IN.uv_MainTex.y += 1; // } IN.uv_MainTex.y -= _Time.y; o.Albedo.rg = IN.uv_MainTex;
En une seconde, la coordonnée V à tous les points deviendra inférieure à zéro, nous ne verrons donc plus la différence. Encore une fois, cela est normal lorsque vous utilisez le filtrage en mode de répétition de texture. Mais pour voir ce qui se passe, nous pouvons prendre la partie fractionnaire de la coordonnée V. IN.uv_MainTex.y -= _Time.y; IN.uv_MainTex.y = frac(IN.uv_MainTex.y); o.Albedo.rg = IN.uv_MainTex;
Coordonnées V animées.Utilisation du bruit
Maintenant, notre rivière est animée, mais dans le sens et la vitesse, il y a de fortes transitions. Notre motif UV les rend assez évidents, mais il sera plus difficile à reconnaître si vous utilisez un motif plus aqueux. Ainsi, au lieu d'afficher des UV bruts, échantillonnons la texture. Nous pouvons utiliser notre texture de bruit existante. Nous l'échantillons et multiplions la couleur du matériau par le premier canal de bruit. void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.uv_MainTex; uv.y -= _Time.y; float4 noise = tex2D(_MainTex, uv); fixed4 c = _Color * noise.r; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Attribuez la texture du bruit au matériau de la rivière et assurez-vous qu'elle est blanche.Utilisation de la texture du bruit.Les coordonnées V étant très étirées, la texture du bruit s'étend également le long de la rivière. Malheureusement, le parcours n'est pas très beau. Essayons de l'étirer d'une autre manière - en réduisant considérablement l'échelle des coordonnées de U. Un seizième suffira. Cela signifie que nous n'échantillonnerons qu'une bande étroite de texture de bruit. float2 uv = IN.uv_MainTex; uv.x *= 0.0625; uv.y -= _Time.y;
Étirement de la coordonnée U.Ralentissons également à un quart par seconde pour que l'achèvement du cycle de texture prenne quatre secondes. uv.y -= _Time.y * 0.25;
Le bruit actuel.Mélange de bruit
Tout semble déjà beaucoup mieux, mais le motif reste toujours le même. L'eau ne se comporte pas comme ça.Puisque nous n'utilisons qu'une petite bande de bruit, nous pouvons faire varier le motif en décalant cette bande le long de la texture. Cela se fait en ajoutant du temps à la coordonnée U. Nous devons le faire lentement, sinon la rivière semblera couler latéralement. Essayons le coefficient de 0,005. Cela signifie qu'il faut 200 secondes pour terminer le motif. uv.x = uv.x * 0.0625 + _Time.y * 0.005;
Bruit en mouvement.Malheureusement, cela n'a pas l'air très beau. L'eau semble toujours statique et le changement est clairement perceptible, bien qu'il soit très lent. Nous pouvons masquer le décalage en combinant deux échantillons de bruit et en les décalant dans des directions opposées. Et si nous utilisons des valeurs légèrement différentes pour déplacer le deuxième échantillon, nous créerons une animation légère du changement.De sorte que, par conséquent, nous ne chevauchons jamais le même motif de bruit, nous utilisons un canal différent pour le deuxième échantillon. float2 uv = IN.uv_MainTex; uv.x = uv.x * 0.0625 + _Time.y * 0.005; uv.y -= _Time.y * 0.25; float4 noise = tex2D(_MainTex, uv); float2 uv2 = IN.uv_MainTex; uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052; uv2.y -= _Time.y * 0.23; float4 noise2 = tex2D(_MainTex, uv2); fixed4 c = _Color * (noise.r * noise2.a);
Une combinaison de deux modèles de bruit changeant.Eau translucide
Notre modèle semble assez dynamique. L'étape suivante consiste à le rendre translucide.Tout d'abord, assurez-vous que l'eau ne projette pas d'ombres. Vous pouvez les désactiver via le composant de rendu de l'objet Rivers dans le préfabriqué.La projection d'ombres est désactivée.Basculez maintenant le shader en mode transparent. Pour indiquer cela, utilisez des balises shader. Ajoutez ensuite le #pragma surface
mot - clé à la ligne alpha
. Pendant que nous sommes ici, vous pouvez supprimer le mot-clé fullforwardshadows
, car nous ne projetons toujours pas d'ombres. Tags { "RenderType"="Transparent" "Queue"="Transparent" } LOD 200 CGPROGRAM #pragma surface surf Standard alpha // fullforwardshadows #pragma target 3.0
Maintenant, nous allons changer la façon dont nous définissons la couleur de la rivière. Au lieu de multiplier le bruit par la couleur, nous y ajouterons du bruit. Ensuite, nous utilisons la fonction saturate
pour limiter le résultat afin qu'il ne dépasse pas 1. fixed4 c = saturate(_Color + noise.r * noise2.a);
Cela nous permettra d'utiliser la couleur du matériau comme couleur de base. Le bruit augmentera sa luminosité et son opacité. Essayons d'utiliser une couleur bleue avec une opacité assez faible. En conséquence, nous obtenons de l'eau translucide bleue avec des éclaboussures blanches.Eau translucide colorée.paquet d'unitéAchèvement
Maintenant que tout semble fonctionner, il est temps de déformer à nouveau les pics. En plus de déformer les bords des cellules, cela rendra nos rivières inégales. public const float cellPerturbStrength = 4f;
Pics déformés et déformés.Examinons le terrain pour les problèmes qui sont survenus en raison de la distorsion. On dirait qu'ils sont! Voyons les grandes cascades.Eau tronquée par des falaises.L'eau qui tombe d'une haute cascade disparaît derrière une falaise. Lorsque cela se produit, c'est très visible, nous devons donc faire quelque chose.Beaucoup moins évident est que les chutes d'eau peuvent être en pente, plutôt que de descendre directement vers le bas. Bien qu'en réalité l'eau ne coule pas comme ça, elle n'est pas particulièrement visible. Notre cerveau va l'interpréter de telle manière qu'il nous semble normal. Alors ignorez-le.La façon la plus simple d'éviter la perte d'eau est d'approfondir les lits des rivières. Nous allons donc créer plus d'espace entre la surface de l'eau et le lit de la rivière. Cela rendra également les parois du canal plus verticales, alors n'allez pas trop loin. DemandonsHexMetrics.streamBedElevationOffset
valeur -1,75. Cela résoudra la majeure partie des problèmes et le lit ne deviendra pas trop profond. Une partie de l'eau sera toujours coupée, mais pas la totalité des cascades. public const float streamBedElevationOffset = -1.75f;
Canaux approfondis.paquet d'unitéPartie 7: routes
- Ajoutez un support routier.
- Triangulez la route.
- Nous combinons routes et rivières.
- Amélioration de l'apparence des routes.
Les premiers signes de civilisation.Cellules avec routes
Comme les rivières, les routes vont de cellule en cellule, au milieu des bords de la cellule. La grande différence est qu'il n'y a pas d'eau qui coule sur les routes, donc elles sont bidirectionnelles. De plus, des intersections sont nécessaires pour un réseau routier fonctionnel, nous devons donc prendre en charge plus de deux routes par cellule.Si vous autorisez les routes à aller dans les six directions, la cellule peut contenir de zéro à six routes. C'est un total de quatorze configurations de routes possibles. Cela représente bien plus de cinq configurations de rivière possibles. Pour gérer cela, nous devons utiliser une approche plus générale qui peut gérer toutes les configurations.14 configurations de routes possibles.Suivi routier
Le moyen le plus simple de suivre les routes dans une cellule est d'utiliser un tableau de valeurs booléennes. Ajoutez le champ privé du tableau à HexCell
et rendez-le sérialisable pour que vous puissiez le voir dans l'inspecteur. Définissez la taille du tableau via le préfabriqué de cellule afin qu'il prenne en charge six routes. [SerializeField] bool[] roads;
Cellule préfabriquée à six routes.Ajoutez une méthode pour vérifier si la cellule a un chemin dans une certaine direction. public bool HasRoadThroughEdge (HexDirection direction) { return roads[(int)direction]; }
Il sera également pratique de savoir s'il y a au moins une route dans la cellule, nous allons donc ajouter une propriété pour cela. Il suffit de faire le tour du tableau dans la boucle et de revenir true
dès que nous trouvons le chemin. S'il n'y a pas de routes, revenez false
. public bool HasRoads { get { for (int i = 0; i < roads.Length; i++) { if (roads[i]) { return true; } } return false; } }
Enlèvement de route
Comme pour les rivières, nous ajouterons une méthode pour supprimer toutes les routes de la cellule. Cela peut être fait avec une boucle qui déconnecte chaque route qui était précédemment activée. public void RemoveRoads () { for (int i = 0; i < neighbors.Length; i++) { if (roads[i]) { roads[i] = false; } } }
Bien sûr, nous devons également désactiver les cellules coûteuses correspondantes chez les voisins. if (roads[i]) { roads[i] = false; neighbors[i].roads[(int)((HexDirection)i).Opposite()] = false; }
Après cela, nous devons mettre à jour chacune des cellules. Étant donné que les routes sont locales aux cellules, nous devons mettre à jour uniquement les cellules elles-mêmes sans leurs voisins. if (roads[i]) { roads[i] = false; neighbors[i].roads[(int)((HexDirection)i).Opposite()] = false; neighbors[i].RefreshSelfOnly(); RefreshSelfOnly(); }
Ajout de routes
L'ajout de routes est similaire à la suppression de routes. La seule différence est que nous attribuons une valeur à Boolean true
, non false
. Nous pouvons créer une méthode privée qui peut effectuer les deux opérations. Ensuite, il sera possible de l'utiliser à la fois pour ajouter et supprimer la route. public void AddRoad (HexDirection direction) { if (!roads[(int)direction]) { SetRoad((int)direction, true); } } public void RemoveRoads () { for (int i = 0; i < neighbors.Length; i++) { if (roads[i]) { SetRoad(i, false); } } } void SetRoad (int index, bool state) { roads[index] = state; neighbors[index].roads[(int)((HexDirection)index).Opposite()] = state; neighbors[index].RefreshSelfOnly(); RefreshSelfOnly(); }
Nous ne pouvons pas avoir à la fois une rivière et une route allant dans la même direction en même temps. Par conséquent, avant d'ajouter la route, nous vérifierons s'il y a une place pour celle-ci. public void AddRoad (HexDirection direction) { if (!roads[(int)direction] && !HasRiverThroughEdge(direction)) { SetRoad((int)direction, true); } }
De plus, les routes ne peuvent pas être combinées avec des falaises car elles sont trop tranchantes. Ou peut-être qu'il vaut la peine d'ouvrir la voie à travers une falaise basse, mais pas à travers une haute? Pour le déterminer, nous devons créer une méthode qui nous indique la différence de hauteur dans une certaine direction. public int GetElevationDifference (HexDirection direction) { int difference = elevation - GetNeighbor(direction).elevation; return difference >= 0 ? difference : -difference; }
Maintenant, nous pouvons ajouter des routes à une différence de hauteur suffisamment petite. Je ne me limiterai qu'aux pentes, c'est-à-dire au maximum 1 unité. public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } }
Supprimer les mauvaises routes
Nous avons ajouté des routes uniquement lorsque cela est autorisé. Maintenant, nous devons nous assurer qu'ils sont supprimés s'ils deviennent incorrects ultérieurement, par exemple, lors de l'ajout d'une rivière. Nous pouvons interdire le placement de rivières au-dessus des routes, mais les rivières ne sont pas interrompues par des routes. Laissez-les nettoyer la route.Il nous suffira de demander la route false
, qu'elle soit ou non. Il y aura toujours mis à jour les deux cellules, donc on n'a plus besoin d'appeler explicitement RefreshSelfOnly
dans SetOutgoingRiver
. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } HexCell neighbor = GetNeighbor(direction); if (!neighbor || elevation < neighbor.elevation) { return; } RemoveOutgoingRiver(); if (hasIncomingRiver && incomingRiver == direction) { RemoveIncomingRiver(); } hasOutgoingRiver = true; outgoingRiver = direction;
Une autre opération qui peut fausser la route est un changement de hauteur. Dans ce cas, nous devrons vérifier les routes dans toutes les directions. Si la différence de hauteur est trop importante, la route existante doit être supprimée. public int Elevation { get { return elevation; } set { … for (int i = 0; i < roads.Length; i++) { if (roads[i] && GetElevationDifference((HexDirection)i) > 1) { SetRoad(i, false); } } Refresh(); } }
paquet d'unitéEdition de route
La modification des routes fonctionne comme la modification des rivières. Par conséquent HexMapEditor
, un commutateur supplémentaire est requis, ainsi qu'une méthode pour définir son état. OptionalToggle riverMode, roadMode; public void SetRiverMode (int mode) { riverMode = (OptionalToggle)mode; } public void SetRoadMode (int mode) { roadMode = (OptionalToggle)mode; }
La méthode EditCell
devrait maintenant prendre en charge la suppression avec l'ajout de routes. Cela signifie que lors d'un glisser-déposer, il peut effectuer l'une des deux actions possibles. Nous restructurons un peu le code afin que lors de l'exécution du glisser-déposer correct, les états des deux commutateurs soient vérifiés. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (isDrag) { HexCell otherCell = cell.GetNeighbor(dragDirection.Opposite()); if (otherCell) { if (riverMode == OptionalToggle.Yes) { otherCell.SetOutgoingRiver(dragDirection); } if (roadMode == OptionalToggle.Yes) { otherCell.AddRoad(dragDirection); } } } } }
Nous pouvons rapidement ajouter une barre de route à l'interface utilisateur en copiant la barre de la rivière et en changeant la méthode appelée par les commutateurs.En conséquence, nous obtenons une interface utilisateur assez élevée. Pour résoudre ce problème, j'ai changé la disposition des panneaux de couleur pour l'adapter aux panneaux de route et de rivière plus compacts.UI avec des routes.Depuis maintenant, j'utilise deux lignes de trois options pour les couleurs, il y a de la place pour une autre couleur. J'ai donc ajouté un élément pour l'orange.Cinq couleurs: jaune, vert, bleu, orange et blanc.Nous pouvons maintenant modifier les routes, mais jusqu'à présent, elles ne sont pas visibles. Vous pouvez utiliser l'inspecteur pour vous assurer que tout fonctionne.Cellule avec routes dans l'inspecteur.paquet d'unitéTriangulation routière
Pour afficher les routes, vous devez les trianguler. Cela revient à créer un maillage pour les rivières, seul le lit de la rivière n'apparaîtra pas dans le relief.Tout d'abord, créez un nouveau shader standard qui utilisera à nouveau les coordonnées UV pour peindre la surface de la route. Shader "Custom/Road" { 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; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = fixed4(IN.uv_MainTex, 1, 1); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
Créez un matériau routier à l'aide de ce shader.Route matérielle.Définissez le préfabriqué du fragment afin qu'il reçoive un autre maillage enfant d'hexagones pour les routes. Ce maillage ne doit pas projeter d'ombres et doit utiliser uniquement des coordonnées UV. Le moyen le plus rapide de le faire consiste à utiliser une instance préfabriquée - dupliquez l'objet Rivers et remplacez son matériau.Routes d'objets enfants.Après cela, ajoutez au HexGridChunk
champ général HexMesh roads
et incluez-le dans Triangulate
. Connectez-le dans l'inspecteur avec l'objet Roads . public HexMesh terrain, rivers, roads; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); }
L'objet Roads est connecté.Routes entre les cellules
Voyons d'abord les segments de route entre les cellules. Comme les rivières, les routes sont fermées par deux quadruples moyens. Nous couvrons complètement ces quadrangles de connexion avec les quadrangles routiers afin que les positions des mêmes six pics puissent être utilisées. Ajoutez pour cela à la HexGridChunk
méthode TriangulateRoadSegment
. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); }
Comme nous n'avons plus à nous soucier du débit d'eau, la coordonnée V n'est pas nécessaire, nous lui affectons donc la valeur 0 partout. Nous pouvons utiliser la coordonnée U pour indiquer si nous sommes au milieu de la route ou sur le côté. Soit égal à 1 au milieu et égal à 0 des deux côtés. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); roads.AddQuadUV(0f, 1f, 0f, 0f); roads.AddQuadUV(1f, 0f, 0f, 0f); }
Un segment de la route entre les cellules.Il serait logique d'appeler cette méthode TriangulateEdgeStrip
, mais seulement s'il y a vraiment une route. Ajoutez un paramètre booléen à la méthode pour transmettre ces informations. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad ) { … }
Bien sûr, nous allons maintenant recevoir des erreurs de compilation, car jusqu'à présent, ces informations n'ont pas encore été transmises. Comme dernier argument dans tous les cas, l'appel TriangulateEdgeStrip
peut être ajouté false
. Cependant, nous pouvons également déclarer que la valeur par défaut de ce paramètre est égale false
. Pour cette raison, le paramètre deviendra facultatif et les erreurs de compilation disparaîtront. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { … }
Comment fonctionnent les paramètres facultatifs?, . ,
int MyMethod (int x = 1, int y = 2) { return x + y; }
int MyMethod (int x, int y) { return x + y; } int MyMethod (int x) { return MyMethod(x, 2); } int MyMethod () { return MyMethod(1, 2}; }
. . . .
Pour trianguler la route, il suffit d'appeler TriangulateRoadSegment
avec les six pics du milieu, si nécessaire. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } }
C'est ainsi que nous gérons les connexions à cellules plates. Pour soutenir les routes sur les rebords, nous devons également indiquer TriangulateEdgeTerraces
où la route doit être ajoutée. Il peut simplement transmettre ces informations TriangulateEdgeStrip
. void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, 1); TriangulateEdgeStrip(begin, beginCell.Color, e2, c2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, i); TriangulateEdgeStrip(e1, c1, e2, c2, hasRoad); } TriangulateEdgeStrip(e2, c2, end, endCell.Color, hasRoad); }
TriangulateEdgeTerraces
appelé à l'intérieur TriangulateConnection
. C'est ici que nous pouvons déterminer s'il existe réellement une route allant dans la direction actuelle, à la fois lors de la triangulation d'une arête et lors de la triangulation des rebords. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces( e1, cell, e2, neighbor, cell.HasRoadThroughEdge(direction) ); } else { TriangulateEdgeStrip( e1, cell.Color, e2, neighbor.Color, cell.HasRoadThroughEdge(direction) ); }
Segments de route entre les cellules.Cellule sur le rendu
Lorsque vous dessinez des routes, vous verrez que des segments de route apparaissent entre les cellules. Le milieu de ces segments sera violet avec une transition vers le bleu sur les bords.Cependant, lorsque vous déplacez la caméra, les segments peuvent scintiller et parfois disparaître complètement. En effet, les triangles des routes chevauchent exactement les triangles de terrain. Les triangles pour le rendu sont sélectionnés au hasard. Ce problème peut être résolu en deux étapes.Premièrement, nous voulons dessiner les routes après le tracé du relief. Ceci peut être réalisé en les rendant après avoir rendu la géométrie habituelle, c'est-à-dire en les plaçant dans une file d'attente de rendu ultérieure. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" }
Deuxièmement, nous devons nous assurer que les routes sont tracées sur des triangles de terrain dans la même position. Cela peut être fait en ajoutant le décalage du test de profondeur. Cela permettra au GPU de supposer que les triangles sont plus proches de la caméra qu'ils ne le sont réellement. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" } LOD 200 Offset -1, -1
Routes à travers les cellules
Lors de la triangulation des rivières, nous n'avons pas eu à traiter plus de deux directions de rivière par cellule. Nous pourrions identifier cinq options possibles et les trianguler différemment pour créer les rivières qui paraissent correctes. Cependant, dans le cas des routes, il y a quatorze options possibles. Nous n'utiliserons pas d'approches distinctes pour chacune de ces options. Au lieu de cela, nous traiterons chacune des six directions de cellule de la même manière, quelle que soit la configuration de route spécifique.Lorsqu'une route passe le long d'une partie de la cellule, nous la tracerons directement au centre de la cellule, sans quitter la zone de triangles. Nous allons dessiner un segment de la route du bord à la moitié en direction du centre. Ensuite, nous utilisons deux triangles pour fermer le reste au centre.Triangulation d'une partie de la route.Pour trianguler ce schéma, nous devons connaître le centre de la cellule, les sommets du milieu gauche et droit et les sommets du bord. Ajoutez une méthode TriangulateRoad
avec les paramètres appropriés. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { }
Pour construire un segment de route, nous avons besoin d'un pic supplémentaire. Il est situé entre les pics médians gauche et droit. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); }
Maintenant, nous pouvons également ajouter les deux triangles restants. TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR);
Nous devons également ajouter les coordonnées UV des triangles. Deux de leurs sommets sont au milieu de la route, et le reste est sur le bord. roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) );
Pour l'instant, confinons-nous aux cellules dans lesquelles il n'y a pas de rivières. Dans ces cas, cela Triangulate
crée simplement un éventail de triangles. Déplacez ce code vers une méthode distincte. Ensuite, nous ajoutons un appel TriangulateRoad
lorsque la route est réellement. Les sommets du milieu gauche et droit peuvent être trouvés par interpolation entre le centre et deux sommets d'angle. void Triangulate (HexDirection direction, HexCell cell) { … if (cell.HasRiver) { … } else { TriangulateWithoutRiver(direction, cell, center, e); } … } void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoadThroughEdge(direction)) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e ); } }
Routes traversant les cellules.Côtes de route
Maintenant, nous pouvons voir les routes, mais plus près du centre des cellules, elles se rétrécissent. Comme nous ne vérifions pas laquelle des quatorze options que nous traitons, nous ne pouvons pas déplacer le centre de la route pour créer des formes plus belles. Au lieu de cela, nous pouvons ajouter des bords de route supplémentaires dans d'autres parties de la cellule.Lorsque les routes traversent la cellule, mais pas dans la direction actuelle, nous ajouterons un triangle aux bords de la route. Ce triangle est défini par les sommets centraux central, gauche et droit. Dans ce cas, seul le pic central se situe au milieu de la route. Les deux autres se trouvent sur sa côte. void TriangulateRoadEdge (Vector3 center, Vector3 mL, Vector3 mR) { roads.AddTriangle(center, mL, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); }
Une partie du bord de la route.Lorsque nous devons trianguler une route pleine ou juste un bord, nous devons la laisser pour TriangulateRoad
. Pour ce faire, cette méthode doit savoir si la route passe par la direction du bord de cellule actuel. Par conséquent, nous ajoutons un paramètre pour cela. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e, bool hasRoadThroughCellEdge ) { if (hasRoadThroughCellEdge) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { TriangulateRoadEdge(center, mL, mR); } }
Maintenant, TriangulateWithoutRiver
il devra appeler TriangulateRoad
lorsque des routes traverseront la cellule. Et il devra transmettre des informations pour savoir si la route passe par le bord actuel. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoads) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e, cell.HasRoadThroughEdge(direction) ); } }
Routes avec nervures terminées.Lissage des routes
Les routes sont maintenant terminées. Malheureusement, cette approche crée des renflements au centre des cellules. Placer les pics gauche et droit au milieu entre le centre et les coins nous convient quand il y a une route à côté d'eux. Mais si ce n'est pas le cas, il y a un renflement. Pour éviter cela, dans de tels cas, nous pouvons placer les sommets plus près du centre. Plus précisément, puis interpoler avec ¼, pas avec ½.Créons une méthode distincte pour déterminer les interpolateurs à utiliser. Puisqu'il y en a deux, nous pouvons mettre le résultat Vector2
. Son composant X sera l'interpolateur du point gauche, et le composant Y sera l'interpolateur du point droit. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; return interpolators; }
S'il y a une route dans la direction actuelle, nous pouvons placer les points au milieu. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; if (cell.HasRoadThroughEdge(direction)) { interpolators.x = interpolators.y = 0.5f; } return interpolators; }
Sinon, les options peuvent être différentes. Pour le point de gauche, nous pouvons utiliser ½ s'il y a une route qui va dans la direction précédente. Si ce n'est pas le cas, nous devons utiliser ¼. La même chose s'applique au bon point, mais en tenant compte de la direction suivante. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; if (cell.HasRoadThroughEdge(direction)) { interpolators.x = interpolators.y = 0.5f; } else { interpolators.x = cell.HasRoadThroughEdge(direction.Previous()) ? 0.5f : 0.25f; interpolators.y = cell.HasRoadThroughEdge(direction.Next()) ? 0.5f : 0.25f; } return interpolators; }
Vous pouvez maintenant utiliser cette nouvelle méthode pour déterminer quels interpolateurs sont utilisés. Grâce à cela, les routes seront lissées. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoads) { Vector2 interpolators = GetRoadInterpolators(direction, cell); TriangulateRoad( center, Vector3.Lerp(center, e.v1, interpolators.x), Vector3.Lerp(center, e.v5, interpolators.y), e, cell.HasRoadThroughEdge(direction) ); } }
Routes lisses.paquet d'unitéLa combinaison des rivières et des routes
Au stade actuel, nous avons des routes fonctionnelles, mais seulement s'il n'y a pas de rivières. S'il y a une rivière dans la cellule, les routes ne seront pas triangulées.Il n'y a pas de routes près des rivières.Créons une méthode TriangulateRoadAdjacentToRiver
pour gérer cette situation. Nous le réglons sur les paramètres habituels. Nous l'appellerons au début de la méthode TriangulateAdjacentToRiver
. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { if (cell.HasRoads) { TriangulateRoadAdjacentToRiver(direction, cell, center, e); } … } void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { }
Pour commencer, nous ferons de même que pour les routes sans rivières. Nous vérifierons si la route passe par le bord actuel, obtiendrons des interpolateurs, créerons des pics moyens et appellerons TriangulateRoad
. Mais comme les rivières apparaîtront sur le chemin, nous devons en éloigner les routes. Par conséquent, le centre de la route sera dans une position différente. Nous utilisons une variable pour stocker cette nouvelle position roadCenter
. Initialement, il sera égal au centre de la cellule. void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); }
Nous allons donc créer des routes partielles dans des cellules avec des rivières. Les directions par lesquelles les rivières passent couperont les lacunes des routes.Routes avec espaces.Début ou fin de la rivière
Examinons d'abord les cellules contenant le début ou la fin d'une rivière. Pour que les routes ne se chevauchent pas avec de l'eau, éloignons le centre de la route de la rivière. Pour obtenir la direction de la rivière entrante ou sortante, ajoutez la HexCell
propriété. public HexDirection RiverBeginOrEndDirection { get { return hasIncomingRiver ? incomingRiver : outgoingRiver; } }
Maintenant, nous pouvons utiliser cette propriété HexGridChunk.TriangulateRoadAdjacentToRiver
pour déplacer le centre de la route dans la direction opposée. Il suffira de déplacer un tiers vers la côte médiane dans cette direction. bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge);
Routes modifiées.Ensuite, nous devons combler les lacunes. Nous le ferons en ajoutant des triangles supplémentaires aux bords de la route lorsque nous serons près de la rivière. S'il y a une rivière dans la direction précédente, alors nous ajoutons un triangle entre le centre de la route, le centre de la cellule et le point central gauche. Et si la rivière est dans la direction suivante, alors nous ajoutons un triangle entre le centre de la route, le point milieu droit et le centre de la cellule.Nous le ferons quelle que soit la configuration de la rivière, alors mettez ce code à la fin de la méthode. Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); if (cell.HasRiverThroughEdge(direction.Previous())) { TriangulateRoadEdge(roadCenter, center, mL); } if (cell.HasRiverThroughEdge(direction.Next())) { TriangulateRoadEdge(roadCenter, mR, center); }
Vous ne pouvez pas utiliser l'instruction else?. , .
Routes prêtes.Rivières droites
Les cellules avec des rivières droites sont particulièrement difficiles car elles divisent essentiellement le centre de la cellule en deux. Nous ajoutons déjà des triangles supplémentaires pour combler les lacunes entre les rivières, mais nous devons également déconnecter les routes sur les côtés opposés de la rivière.Routes chevauchant une rivière droite.Si la cellule n'a pas le début ou la fin de la rivière, nous pouvons vérifier si les rivières entrantes et sortantes vont dans des directions opposées. Si oui, alors nous avons une rivière directe. if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { }
Pour déterminer où la rivière est par rapport à la direction actuelle, nous devons vérifier les directions voisines. La rivière est à gauche ou à droite. Puisque nous le faisons à la fin de la méthode, nous mettons en cache ces requêtes dans des variables booléennes. Cela simplifiera également la lecture de notre code. bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); bool previousHasRiver = cell.HasRiverThroughEdge(direction.Previous()); bool nextHasRiver = cell.HasRiverThroughEdge(direction.Next()); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { if (previousHasRiver) { } else { } } Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); if (previousHasRiver) { TriangulateRoadEdge(roadCenter, center, mL); } if (nextHasRiver) { TriangulateRoadEdge(roadCenter, mR, center); }
Nous devons déplacer le centre de la route vers un vecteur angulaire pointant dans la direction opposée à la rivière. Si la rivière passe par la direction précédente, c'est le deuxième angle solide. Sinon, c'est le premier angle solide. else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { Vector3 corner; if (previousHasRiver) { corner = HexMetrics.GetSecondSolidCorner(direction); } else { corner = HexMetrics.GetFirstSolidCorner(direction); } }
Pour déplacer la route de manière à ce qu'elle soit adjacente à la rivière, nous devons déplacer le centre de la route de la moitié de la distance jusqu'à ce coin. Ensuite, nous devons également déplacer le centre de la cellule d'un quart de la distance dans cette direction. else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { Vector3 corner; if (previousHasRiver) { corner = HexMetrics.GetSecondSolidCorner(direction); } else { corner = HexMetrics.GetFirstSolidCorner(direction); } roadCenter += corner * 0.5f; center += corner * 0.25f; }
Routes divisées.Nous avons partagé un réseau de routes à l'intérieur de cette cellule. C'est normal lorsque les routes sont des deux côtés de la rivière. Mais si d'un côté il n'y a pas de route, alors nous aurons un petit morceau de route isolée. C'est illogique, alors débarrassons-nous de ces parties.Assurez-vous qu'il y a une route qui va dans la direction actuelle. Si ce n'est pas le cas, vérifiez dans l'autre sens du même côté de la rivière la présence de la route. S'il n'y a pas de route de passage là ou là, alors on quitte la méthode avant de trianguler. if (previousHasRiver) { if ( !hasRoadThroughEdge && !cell.HasRoadThroughEdge(direction.Next()) ) { return; } corner = HexMetrics.GetSecondSolidCorner(direction); } else { if ( !hasRoadThroughEdge && !cell.HasRoadThroughEdge(direction.Previous()) ) { return; } corner = HexMetrics.GetFirstSolidCorner(direction); }
Routes tronquées.Rivières en zigzag
Le prochain type de rivière est le zigzag. Ces rivières ne partagent pas le réseau routier, il nous suffit donc de déplacer le centre de la route.Zigzags traversant les routes.La façon la plus simple de vérifier les zigzags est de comparer les directions des rivières entrantes et sortantes. S'ils sont adjacents, alors nous avons un zigzag. Cela conduit à deux options possibles, selon le sens de l'écoulement. if (cell.HasRiverBeginOrEnd) { … } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { … } else if (cell.IncomingRiver == cell.OutgoingRiver.Previous()) { } else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { }
Nous pouvons déplacer le centre de la route en utilisant l'un des coins de la direction de la rivière entrante. L'angle que vous sélectionnez dépend de la direction du flux. Déplacez le centre de la route de cet angle avec un facteur de 0,2. else if (cell.IncomingRiver == cell.OutgoingRiver.Previous()) { roadCenter -= HexMetrics.GetSecondCorner(cell.IncomingRiver) * 0.2f; } else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { roadCenter -= HexMetrics.GetFirstCorner(cell.IncomingRiver) * 0.2f; }
La route s'éloigna des zigzags.À l'intérieur des rivières tortueuses
La dernière configuration de rivière est une courbe lisse. Comme pour la rivière directe, celle-ci peut également séparer les routes. Mais dans ce cas, les parties seront différentes. Nous devons d'abord travailler avec l'intérieur de la courbe.Une rivière incurvée avec des routes pavées.Lorsque nous avons une rivière des deux côtés de la direction actuelle, nous sommes à l'intérieur de la courbe. else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { … } else if (previousHasRiver && nextHasRiver) { }
Nous devons déplacer le centre de la route vers le bord actuel de la cellule, raccourcissant un peu la route. Un coefficient de 0,7 fera l'affaire. Le centre cellulaire devrait également se déplacer avec un coefficient de 0,5. else if (previousHasRiver && nextHasRiver) { Vector3 offset = HexMetrics.GetSolidEdgeMiddle(direction) * HexMetrics.innerToOuter; roadCenter += offset * 0.7f; center += offset * 0.5f; }
Routes raccourcies.Comme dans le cas des rivières droites, nous devrons couper les parties isolées des routes. Dans ce cas, il suffit de vérifier uniquement la direction actuelle. else if (previousHasRiver && nextHasRiver) { if (!hasRoadThroughEdge) { return; } Vector3 offset = HexMetrics.GetSolidEdgeMiddle(direction) * HexMetrics.innerToOuter; roadCenter += offset * 0.7f; center += offset * 0.5f; }
Coupez les routes.En dehors des rivières tortueuses
Après avoir vérifié tous les cas précédents, la seule option restante était la partie extérieure de la rivière incurvée. Dehors, il y a trois parties de la cellule. Nous devons trouver la direction du milieu. Après l'avoir reçue, nous pouvons déplacer le centre de la route vers cette nervure d'un facteur de 0,25. else if (previousHasRiver && nextHasRiver) { … } else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } roadCenter += HexMetrics.GetSolidEdgeMiddle(middle) * 0.25f; }
Changé l'extérieur de la route.Comme dernière étape, nous devons tronquer les routes de ce côté de la rivière. Le moyen le plus simple consiste à vérifier les trois directions de la route par rapport au milieu. S'il n'y a pas de routes, nous arrêtons de travailler. else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } if ( !cell.HasRoadThroughEdge(middle) && !cell.HasRoadThroughEdge(middle.Previous()) && !cell.HasRoadThroughEdge(middle.Next()) ) { return; } roadCenter += HexMetrics.GetSolidEdgeMiddle(middle) * 0.25f; }
Routes avant et après détourage.Après avoir traité toutes les options fluviales, nos rivières et nos routes peuvent coexister. Les rivières ignorent les routes et les routes s'adaptent aux rivières.La combinaison des rivières et des routes.paquet d'unitéL'apparence des routes
Jusqu'à ce moment, nous utilisions leurs coordonnées UV comme couleurs de route. Étant donné que seule la coordonnée U a changé, nous avons en fait affiché la transition entre le milieu et le bord de la route.Affichage des coordonnées UV.Maintenant que les routes sont correctement triangulées correctement, nous pouvons changer le shader de la route pour qu'il ressemble davantage à des routes. Comme dans le cas des rivières, ce sera une visualisation simple, sans fioritures.Nous commencerons par utiliser la couleur unie pour les routes. Utilisez simplement la couleur du matériau. Je l'ai rendu rouge. void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Routes rouges.Et ça a déjà l'air beaucoup mieux! Mais continuons et mélangeons la route avec le terrain, en utilisant la coordonnée U comme facteur de mélange. void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; float blend = IN.uv_MainTex.x; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = blend; }
Il semble que cela n'ait rien changé. C'est arrivé parce que notre shader est opaque. Maintenant, il a besoin d'un mélange alpha. En particulier, nous avons besoin d'un shader pour une surface de décalcomanie d'accouplement. Nous pouvons obtenir le shader requis en ajoutant une #pragma surface
ligne à la directive decal:blend
. #pragma surface surf Standard fullforwardshadows decal:blend
Le mélange des routes.Nous avons donc créé un mélange linéaire lisse du milieu au bord qui n'est pas très joli. Pour qu'elle ressemble à une route, nous avons besoin d'une zone solide, suivie d'une transition rapide vers une zone opaque. Vous pouvez utiliser la fonction pour cela smoothstep
. Il convertit une progression linéaire de 0 à 1 en une courbe en forme de S.Progression linéaire et lisse.La fonction smoothstep
a un paramètre minimum et maximum pour ajuster la courbe dans un intervalle arbitraire. Les valeurs d'entrée en dehors de la plage sont limitées pour maintenir la courbe plate. Utilisons 0,4 au début de la courbe et 0,7 à la fin. Cela signifie que la coordonnée U de 0 à 0,4 sera complètement transparente. Et les coordonnées U de 0,7 à 1 seront complètement opaques. La transition se produit entre 0,4 et 0,7. float blend = IN.uv_MainTex.x; blend = smoothstep(0.4, 0.7, blend);
Transition rapide entre les zones opaques et transparentes.Route avec bruit
Étant donné que le maillage de la route sera déformé, les routes ont des largeurs variables. Par conséquent, la largeur de la transition sur les bords sera également variable. Parfois, c'est flou, parfois dur. Une telle variabilité nous convient, si nous percevons les routes comme sablonneuses ou terreuses.Prenons la prochaine étape et ajoutons du bruit aux bords de la route. Cela les rendra plus inégaux et moins polygonaux. Nous pouvons le faire en échantillonnant la texture du bruit. Pour l'échantillonnage, vous pouvez utiliser les coordonnées du monde XZ, comme nous l'avons fait lors de la distorsion des sommets des cellules.Pour accéder à la position du monde dans le shader de surface, ajoutez à la structure d'entrée float3 worldPos
. struct Input { float2 uv_MainTex; float3 worldPos; };
Maintenant, nous pouvons utiliser cette position surf
pour échantillonner la texture principale. Effectuez également un zoom arrière, sinon la texture se répétera trop souvent. float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color; float blend = IN.uv_MainTex.x;
Nous déformons la transition en multipliant la coordonnée U par noise.x
. Mais comme les valeurs de bruit sont en moyenne de 0,5, la plupart des routes disparaîtront. Pour éviter cela, ajoutez 0,5 au bruit avant multiplication. float blend = IN.uv_MainTex.x; blend *= noise.x + 0.5; blend = smoothstep(0.4, 0.7, blend);
Bords déformés de la route.Pour y mettre fin, nous dénaturerons également la couleur des routes. Cela donnera aux routes une sensation de saleté correspondant aux bords flous.Multipliez la couleur par un autre canal de bruit, disons par noise.y
. Nous obtenons donc en moyenne la moitié de la valeur de la couleur. Comme c'est trop, nous réduirons légèrement l'échelle de bruit et ajouterons une constante pour que la somme puisse atteindre 1. fixed4 c = _Color * (noise.y * 0.75 + 0.25);
Routes cahoteuses.paquet d'unité