Cartes hexagonales dans Unity: brouillard de guerre, recherche cartographique, génération procédurale

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

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

Parties 8-11: eau, reliefs et remparts

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

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

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

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

Partie 20: le brouillard de la guerre


  • Enregistrez les données de cellule dans la texture.
  • Modifiez les types de relief sans triangulation.
  • Nous suivons la visibilité.
  • Obscurcissez tout ce qui est invisible.

Dans cette partie, nous ajouterons l'effet de brouillard de guerre à la carte.

Maintenant, la série sera créée sur Unity 2017.1.0.


Maintenant, nous voyons que nous pouvons et ne pouvons pas voir.

Données de cellule dans le shader


De nombreux jeux de stratégie utilisent le concept du brouillard de guerre. Cela signifie que la vision du joueur est limitée. Il ne peut voir que ce qui est proche de ses unités ou de sa zone contrôlée. Bien que nous puissions voir le soulagement, nous ne savons pas ce qui se passe là-bas. Habituellement, le terrain invisible est rendu plus sombre. Pour réaliser cela, nous devons suivre la visibilité de la cellule et la rendre en conséquence.

Le moyen le plus simple de modifier l'apparence des cellules masquées consiste à ajouter une métrique de visibilité aux données de maillage. Cependant, en même temps, nous devrons commencer une nouvelle triangulation en relief avec un changement de visibilité. C'est une mauvaise décision car la visibilité change constamment pendant le match.

La technique de rendu sur la topographie d'une surface translucide est souvent utilisée, qui masque partiellement les cellules invisibles au joueur. Cette méthode convient aux terrains relativement plats en combinaison avec un angle de vision limité. Mais comme notre terrain peut contenir des hauteurs et des objets très variables qui peuvent être vus sous différents angles, nous avons besoin pour cela d'un maillage très détaillé qui correspond à la forme du terrain. Cette méthode sera plus coûteuse que l'approche la plus simple mentionnée ci-dessus.

Une autre approche consiste à transférer les données des cellules vers le shader lors du rendu séparément du maillage en relief. Cela nous permettra d'effectuer une triangulation une seule fois. Les données des cellules peuvent être transférées en utilisant la texture. Changer la texture est un processus beaucoup plus simple que de trianguler le terrain. De plus, l'exécution de plusieurs échantillons de texture supplémentaires est plus rapide que le rendu d'une seule couche translucide.

Qu'en est-il de l'utilisation de tableaux de shaders?
Vous pouvez également transférer des données de cellule vers le shader à l'aide d'un tableau de vecteurs. Cependant, les tableaux de shaders ont une limite de taille, mesurée en milliers d'octets, et les textures peuvent contenir des millions de pixels. Pour prendre en charge de grandes cartes, nous utiliserons des textures.

Gestion des données cellulaires


Nous avons besoin d'un moyen de contrôler la texture contenant les données des cellules. Créons un nouveau composant HexCellShaderData qui fera cela.

 using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; } 

Lors de la création ou du chargement d'une nouvelle carte, nous devons créer une nouvelle texture avec la bonne taille. Par conséquent, nous y ajoutons une méthode d'initialisation qui crée une texture. Nous utilisons une texture RGBA sans textures de mip et espace colorimétrique linéaire. Nous n'avons pas besoin de mélanger les données des cellules, nous utilisons donc le filtrage ponctuel. De plus, les données ne doivent pas être réduites. Chaque pixel de la texture contiendra les données d'une cellule.

  public void Initialize (int x, int z) { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; } 

La taille de la texture doit-elle correspondre à la taille de la carte?
Non, il suffit d'avoir suffisamment de pixels pour contenir toutes les cellules. Avec une correspondance exacte avec la taille de la carte, une texture avec des tailles qui ne sont pas des puissances de deux (non-puissance de deux, NPOT) sera très probablement créée, et ce format de texture n'est pas le plus efficace. Bien que nous puissions configurer le code pour qu'il fonctionne avec des textures de la taille d'une puissance de deux, il s'agit d'une optimisation mineure, qui complique l'accès aux données des cellules.

En fait, nous n'avons pas à créer une nouvelle texture chaque fois que nous créons une nouvelle carte. Il suffit de redimensionner la texture si elle existe déjà. Nous n'avons même pas besoin de vérifier si nous avons déjà la bonne taille, car Texture2D.Resize est assez intelligent pour le faire pour nous.

  public void Initialize (int x, int z) { if (cellTexture) { cellTexture.Resize(x, z); } else { cellTexture = new Texture2D( cellCountX, cellCountZ, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; } } 

Au lieu d'appliquer des données de cellule un pixel à la fois, nous utilisons un tampon de couleur et appliquons les données de toutes les cellules à la fois. Pour ce faire, nous utiliserons le tableau Color32 . Si nécessaire, nous créerons une nouvelle instance de tableau à la fin de Initialize . Si nous avons déjà un tableau de la bonne taille. puis nous effaçons son contenu.

  Texture2D cellTexture; Color32[] cellTextureData; public void Initialize () { … if (cellTextureData == null || cellTextureData.Length != x * z) { cellTextureData = new Color32[x * z]; } else { for (int i = 0; i < cellTextureData.Length; i++) { cellTextureData[i] = new Color32(0, 0, 0, 0); } } } 

Qu'est-ce que color32?
Les textures RGBA non compressées standard contiennent des pixels de quatre octets. Chacun des quatre canaux de couleur reçoit un octet, c'est-à-dire qu'il a 256 valeurs possibles. Lorsque vous utilisez la structure Unity Color , ses composants à virgule flottante dans l'intervalle 0–1 sont convertis en octets dans l'intervalle 0–255. Lors de l'échantillonnage, le GPU effectue la transformation inverse.

La structure Color32 fonctionne directement avec les octets, donc ils prennent moins d'espace et ne nécessitent pas de conversion, ce qui augmente l'efficacité de leur utilisation. Étant donné que nous stockons des données de cellule au lieu de couleurs, il sera plus logique de travailler directement avec des données de texture brutes et non avec Color .

HexGrid doit gérer la création et l'initialisation de ces cellules dans le shader. Par conséquent, nous y ajouterons un champ cellShaderData et créerons un composant dans Awake .

  HexCellShaderData cellShaderData; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); CreateMap(cellCountX, cellCountZ); } 

Lors de la création d'une nouvelle carte, cellShaderData doit également être cellShaderData .

  public bool CreateMap (int x, int z) { … cellCountX = x; cellCountZ = z; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; cellShaderData.Initialize(cellCountX, cellCountZ); CreateChunks(); CreateCells(); return true; } 

Modification des données de cellule


Jusqu'à présent, lors de la modification des propriétés d'une cellule, il était nécessaire de mettre à jour un ou plusieurs fragments, mais maintenant il peut être nécessaire de mettre à jour les données des cellules. Cela signifie que les cellules doivent avoir un lien vers les données de cellule dans le shader. Pour ce faire, ajoutez une propriété à HexCell .

  public HexCellShaderData ShaderData { get; set; } 

Dans HexGrid.CreateCell nous assignerons un composant de données shader à cette propriété.

  void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.ShaderData = cellShaderData; … } 

Nous pouvons maintenant demander aux cellules de mettre à jour leurs données de shader. Bien que nous ne surveillions pas la visibilité, nous pouvons utiliser des données de shader pour autre chose. Le type de relief de la cellule détermine la texture utilisée pour la rendre. Cela n'affecte pas la géométrie de la cellule, nous pouvons donc stocker l'indice de type d'élévation dans les données de la cellule, et non dans les données du maillage. Cela nous permettra de nous débarrasser du besoin de triangulation lors du changement du type de relief de la cellule.

Ajoutez une méthode HexCellShaderData à RefreshTerrain pour simplifier cette tâche pour une cellule spécifique. Laissons cette méthode vide pour l'instant.

  public void RefreshTerrain (HexCell cell) { } 

Modifiez HexCell.TerrainTypeIndex afin qu'il HexCell.TerrainTypeIndex cette méthode et n'ordonne pas de mettre à jour les fragments.

  public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value; // Refresh(); ShaderData.RefreshTerrain(this); } } } 

Nous l'appellerons également dans HexCell.Load après avoir reçu le type de topographie de cellule.

  public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); RefreshPosition(); … } 

Index des cellules


Pour changer ces cellules, nous devons connaître l'indice de la cellule. La façon la plus simple de procéder consiste à ajouter la propriété Index à HexCell . Il indiquera l'index de la cellule dans la liste des cellules de la carte, ce qui correspond à son index dans les cellules données du shader.

  public int Index { get; set; } 

Cet index est déjà dans HexGrid.CreateCell , il suffit donc de l'affecter à la cellule créée.

  void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; … } 

Désormais, HexCellShaderData.RefreshTerrain peut utiliser cet index pour spécifier des données de cellule. Enregistrons l'index du type d'élévation dans la composante alpha de son pixel en convertissant simplement le type en octet. Cela supportera jusqu'à 256 types de terrains, ce qui nous suffira.

  public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; } 

Pour appliquer des données à une texture et les transmettre au GPU, nous devons appeler Texture2D.SetPixels32 , puis Texture2D.Apply . Comme pour les fragments, nous reporterons ces opérations sur LateUpdate afin qu'elles ne puissent être effectuées plus d'une fois par trame, quel que soit le nombre de cellules modifiées.

  public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; enabled = true; } void LateUpdate () { cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = false; } 

Pour vous assurer que les données seront mises à jour après la création d'une nouvelle carte, activez le composant après l'initialisation.

  public void Initialize (int x, int z) { … enabled = true; } 

Triangulation des indices cellulaires


Puisque nous stockons maintenant l'indice de type d'élévation dans ces cellules, nous n'avons plus besoin de les inclure dans le processus de triangulation. Mais pour utiliser les données de cellule, le shader doit savoir quels index utiliser. Par conséquent, vous devez stocker les indices de cellule dans les données de maillage, en remplaçant les indices de type d'élévation. De plus, nous avons toujours besoin du canal de couleur du maillage pour mélanger les cellules lors de l'utilisation de ces cellules.

Nous useColors champs communs obsolètes useColors et useTerrainTypes . Remplacez-les par un champ useCellData .

 // public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; // public bool useTerrainTypes; public bool useCollider, useCellData, useUVCoordinates, useUV2Coordinates; 

Nous refactorisons le changement de nom de la liste cellIndices en cellIndices . Refactorisons également les colors en cellWeights - ce nom fera mieux.

 // [NonSerialized] List<Vector3> vertices, terrainTypes; // [NonSerialized] List<Color> colors; [NonSerialized] List<Vector3> vertices, cellIndices; [NonSerialized] List<Color> cellWeights; [NonSerialized] List<Vector2> uvs, uv2s; [NonSerialized] List<int> triangles; 

Modifiez Clear sorte que lorsque vous utilisez ces cellules, il obtienne deux listes ensemble, et non séparément.

  public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useCellData) { cellWeights = ListPool<Color>.Get(); cellIndices = ListPool<Vector3>.Get(); } // if (useColors) { // colors = ListPool<Color>.Get(); // } if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } if (useUV2Coordinates) { uv2s = ListPool<Vector2>.Get(); } // if (useTerrainTypes) { // terrainTypes = ListPool<Vector3>.Get(); // } triangles = ListPool<int>.Get(); } 

Effectuez le même regroupement dans Apply .

  public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useCellData) { hexMesh.SetColors(cellWeights); ListPool<Color>.Add(cellWeights); hexMesh.SetUVs(2, cellIndices); ListPool<Vector3>.Add(cellIndices); } // if (useColors) { // hexMesh.SetColors(colors); // ListPool<Color>.Add(colors); // } if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } if (useUV2Coordinates) { hexMesh.SetUVs(1, uv2s); ListPool<Vector2>.Add(uv2s); } // if (useTerrainTypes) { // hexMesh.SetUVs(2, terrainTypes); // ListPool<Vector3>.Add(terrainTypes); // } hexMesh.SetTriangles(triangles, 0); ListPool<int>.Add(triangles); hexMesh.RecalculateNormals(); if (useCollider) { meshCollider.sharedMesh = hexMesh; } } 

AddTriangleColor toutes les AddTriangleTerrainTypes AddTriangleColor et AddTriangleTerrainTypes . Remplacez-les par les méthodes AddTriangleCellData appropriées, qui ajoutent des index et des pondérations à la fois.

  public void AddTriangleCellData ( Vector3 indices, Color weights1, Color weights2, Color weights3 ) { cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellWeights.Add(weights1); cellWeights.Add(weights2); cellWeights.Add(weights3); } public void AddTriangleCellData (Vector3 indices, Color weights) { AddTriangleCellData(indices, weights, weights, weights); } 

Faites de même dans la méthode AddQuad appropriée.

  public void AddQuadCellData ( Vector3 indices, Color weights1, Color weights2, Color weights3, Color weights4 ) { cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellWeights.Add(weights1); cellWeights.Add(weights2); cellWeights.Add(weights3); cellWeights.Add(weights4); } public void AddQuadCellData ( Vector3 indices, Color weights1, Color weights2 ) { AddQuadCellData(indices, weights1, weights1, weights2, weights2); } public void AddQuadCellData (Vector3 indices, Color weights) { AddQuadCellData(indices, weights, weights, weights, weights); } 

Refactorisation HexGridChunk


À ce stade, nous obtenons de nombreuses erreurs de compilation dans HexGridChunk qui doivent être HexGridChunk . Mais d'abord, par souci de cohérence, nous refactorisons-renommons les couleurs statiques en poids.

  static Color weights1 = new Color(1f, 0f, 0f); static Color weights2 = new Color(0f, 1f, 0f); static Color weights3 = new Color(0f, 0f, 1f); 

Commençons par fixer TriangulateEdgeFan . Il avait besoin d'un type, mais maintenant il a besoin d'un index de cellule. AddTriangleColor code AddTriangleColor et AddTriangleTerrainTypes par le code AddTriangleCellData correspondant.

  void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float index) { terrain.AddTriangle(center, edge.v1, edge.v2); terrain.AddTriangle(center, edge.v2, edge.v3); terrain.AddTriangle(center, edge.v3, edge.v4); terrain.AddTriangle(center, edge.v4, edge.v5); Vector3 indices; indices.x = indices.y = indices.z = index; terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // terrain.AddTriangleColor(weights1); // Vector3 types; // types.x = types.y = types.z = type; // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); } 

Cette méthode est appelée à plusieurs endroits. Examinons-les et assurons-nous que l'index de la cellule y est transféré, et non le type de terrain.

  TriangulateEdgeFan(center, e, cell.Index); 

Vient ensuite TriangulateEdgeStrip . Tout est un peu plus compliqué ici, mais nous utilisons la même approche. Renommez également les noms des paramètres c1 et c2 en w1 et w2 .

  void TriangulateEdgeStrip ( EdgeVertices e1, Color w1, float index1, EdgeVertices e2, Color w2, float index2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); Vector3 indices; indices.x = indices.z = index1; indices.y = index2; terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // terrain.AddQuadColor(c1, c2); // Vector3 types; // types.x = types.z = type1; // types.y = type2; // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } } 

Modifiez les appels à cette méthode afin que l'index de cellule leur soit transmis. Nous gardons également les noms de variables cohérents.

  TriangulateEdgeStrip( m, weights1, cell.Index, e, weights1, cell.Index ); … TriangulateEdgeStrip( e1, weights1, cell.Index, e2, weights2, neighbor.Index, hasRoad ); … void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color w2 = HexMetrics.TerraceLerp(weights1, weights2, 1); float i1 = beginCell.Index; float i2 = endCell.Index; TriangulateEdgeStrip(begin, weights1, i1, e2, w2, i2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color w1 = w2; e2 = EdgeVertices.TerraceLerp(begin, end, i); w2 = HexMetrics.TerraceLerp(weights1, weights2, i); TriangulateEdgeStrip(e1, w1, i1, e2, w2, i2, hasRoad); } TriangulateEdgeStrip(e2, w2, i1, end, weights2, i2, hasRoad); } 

Passons maintenant aux méthodes d'angle. Ces modifications sont simples, mais elles doivent être effectuées dans une grande quantité de code. Tout d'abord chez TriangulateCorner .

  void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); Vector3 indices; indices.x = bottomCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; terrain.AddTriangleCellData(indices, weights1, weights2, weights3); // terrain.AddTriangleColor(weights1, weights2, weights3); // Vector3 types; // types.x = bottomCell.TerrainTypeIndex; // types.y = leftCell.TerrainTypeIndex; // types.z = rightCell.TerrainTypeIndex; // terrain.AddTriangleTerrainTypes(types); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); } 

Venir à TriangulateCornerTerraces .

  void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color w3 = HexMetrics.TerraceLerp(weights1, weights2, 1); Color w4 = HexMetrics.TerraceLerp(weights1, weights3, 1); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleCellData(indices, weights1, w3, w4); // terrain.AddTriangleColor(weights1, w3, w4); // terrain.AddTriangleTerrainTypes(indices); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color w1 = w3; Color w2 = w4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); w3 = HexMetrics.TerraceLerp(weights1, weights2, i); w4 = HexMetrics.TerraceLerp(weights1, weights3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadCellData(indices, w1, w2, w3, w4); // terrain.AddQuadColor(w1, w2, w3, w4); // terrain.AddQuadTerrainTypes(indices); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadCellData(indices, w3, w4, weights2, weights3); // terrain.AddQuadColor(w3, w4, weights2, weights3); // terrain.AddQuadTerrainTypes(indices); } 

Puis dans TriangulateCornerTerracesCliff .

  void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryWeights = Color.Lerp(weights1, weights3, b); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; TriangulateBoundaryTriangle( begin, weights1, left, weights2, boundary, boundaryWeights, indices ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, weights2, right, weights3, boundary, boundaryWeights, indices ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleCellData( indices, weights2, weights3, boundaryWeights ); // terrain.AddTriangleColor(weights2, weights3, boundaryColor); // terrain.AddTriangleTerrainTypes(indices); } } 

Et un peu différemment dans TriangulateCornerCliffTerraces .

  void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryWeights = Color.Lerp(weights1, weights2, b); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; TriangulateBoundaryTriangle( right, weights3, begin, weights1, boundary, boundaryWeights, indices ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, weights2, right, weights3, boundary, boundaryWeights, indices ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleCellData( indices, weights2, weights3, boundaryWeights ); // terrain.AddTriangleColor(weights2, weights3, boundaryWeights); // terrain.AddTriangleTerrainTypes(indices); } } 

Les deux méthodes précédentes utilisent le TriangulateBoundaryTriangle , qui nécessite également une mise à jour.

  void TriangulateBoundaryTriangle ( Vector3 begin, Color beginWeights, Vector3 left, Color leftWeights, Vector3 boundary, Color boundaryWeights, Vector3 indices ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color w2 = HexMetrics.TerraceLerp(beginWeights, leftWeights, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleCellData(indices, beginWeights, w2, boundaryWeights); // terrain.AddTriangleColor(beginColor, c2, boundaryColor); // terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color w1 = w2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); w2 = HexMetrics.TerraceLerp(beginWeights, leftWeights, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleCellData(indices, w1, w2, boundaryWeights); // terrain.AddTriangleColor(c1, c2, boundaryColor); // terrain.AddTriangleTerrainTypes(types); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleCellData(indices, w2, leftWeights, boundaryWeights); // terrain.AddTriangleColor(c2, leftColor, boundaryColor); // terrain.AddTriangleTerrainTypes(types); } 

La dernière méthode qui doit être modifiée est TriangulateWithRiver .

  void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangle(centerL, m.v1, m.v2); terrain.AddQuad(centerL, center, m.v2, m.v3); terrain.AddQuad(center, centerR, m.v3, m.v4); terrain.AddTriangle(centerR, m.v4, m.v5); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; terrain.AddTriangleCellData(indices, weights1); terrain.AddQuadCellData(indices, weights1); terrain.AddQuadCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); // terrain.AddTriangleColor(weights1); // terrain.AddQuadColor(weights1); // terrain.AddQuadColor(weights1); // terrain.AddTriangleColor(weights1); // Vector3 types; // types.x = types.y = types.z = cell.TerrainTypeIndex; // terrain.AddTriangleTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddQuadTerrainTypes(types); // terrain.AddTriangleTerrainTypes(types); … } 

Pour que tout fonctionne, nous devons indiquer que nous utiliserons les données de cellule pour l'élément enfant du relief du fragment préfabriqué.


Le relief utilise des données cellulaires.

À ce stade, le maillage contient des index de cellules au lieu d'indices de type d'élévation. Comme le shader d'élévation les interprète toujours comme des indices d'élévation, nous verrons que la première cellule est rendue avec la première texture et ainsi de suite jusqu'à ce que la dernière texture en relief soit atteinte.


Utilisation d'indices de cellule comme indices de texture d'élévation.

Je ne peux pas faire fonctionner le code refactorisé. Qu'est-ce que je fais mal?
À un moment donné, nous avons modifié une grande quantité de code de triangulation, il y a donc une forte probabilité d'erreurs ou d'oubli. Si vous ne trouvez pas l'erreur, essayez de télécharger le package à partir de cette section et extrayez les fichiers appropriés. Vous pouvez les importer dans un projet distinct et les comparer avec votre propre code.

Transférer des données de cellule vers un shader


Pour utiliser ces cellules, le shader de terrain doit y avoir accès. Cela peut être implémenté via la propriété shader. Cela nécessitera HexCellShaderData définir la propriété matérielle du relief. Ou nous pouvons rendre la texture de ces cellules globalement visible à tous les shaders. C'est pratique car nous en avons besoin dans plusieurs shaders, nous allons donc utiliser cette approche.

Après avoir créé la texture de cellule, appelez la méthode statique Shader.SetGlobalTexture pour la rendre globalement visible en tant que _HexCellData .

  public void Initialize (int x, int z) { … else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } … } 

Lorsque vous utilisez la propriété shader, Unity met la taille de texture à la disposition du shader via la variable textureName_TexelSize . Il s'agit d'un vectoriseur à quatre composants contenant des valeurs qui sont inverses à la largeur et à la hauteur, ainsi qu'à la largeur et à la hauteur elles-mêmes. Mais lors de la définition de la texture globale, cela n'est pas effectué. Par conséquent, nous le ferons nous-mêmes en utilisant le Shader.SetGlobalVector après avoir créé ou redimensionné la texture.

  else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } Shader.SetGlobalVector( "_HexCellData_TexelSize", new Vector4(1f / x, 1f / z, x, z) ); 

Accès aux données du shader


Créez un nouveau fichier d'inclusion de shader dans le dossier des matériaux appelé HexCellData . À l'intérieur, nous définissons des variables pour obtenir des informations sur la texture et la taille de ces cellules. Nous créons également une fonction pour obtenir les données de cellule pour les données de maillage de sommet données.

 sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 GetCellData (appdata_full v) { } 


Nouveau fichier d'inclusion.

Les indices de cellule sont stockés dans v.texcoord2 , comme c'était le cas avec les types de terrain. Commençons par le premier index - v.texcoord2.x . Malheureusement, nous ne pouvons pas utiliser directement l'index pour échantillonner la texture de ces cellules. Nous devrons le convertir en coordonnées UV.

La première étape de la création de la coordonnée U consiste à diviser l'indice de cellule par la largeur de la texture. Nous pouvons le faire en le multipliant par _HexCellData_TexelSize.x .

 float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; } 

Le résultat sera un nombre sous la forme ZU, où Z est l'index de ligne et U est la coordonnée de la cellule U. Nous pouvons extraire la chaîne en arrondissant le nombre vers le bas puis en le soustrayant du nombre pour obtenir la coordonnée U.

 float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; } 

La coordonnée V divise la ligne par la hauteur de la texture.

 float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = row * _HexCellData_TexelSize.y; } 

Puisque nous échantillonnons la texture, nous devons utiliser les coordonnées au centre des pixels, pas à leurs bords. De cette façon, nous garantissons que les bons pixels sont échantillonnés. Par conséquent, après avoir divisé par la taille de la texture, ajoutez ½.

 float4 GetCellData (appdata_full v) { float2 uv; uv.x = (v.texcoord2.x + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; } 

Cela nous donne les coordonnées UV correctes pour l'indice de la première cellule stockée dans les données de sommet. Mais en plus, nous pouvons avoir jusqu'à trois indices différents. Par conséquent, nous allons le faire GetCellDatafonctionner pour n'importe quel index. Ajoutez-y un paramètre entier index, que nous utiliserons pour accéder à la composante vectorielle avec l'index de cellule.

 float4 GetCellData (appdata_full v, int index) { float2 uv; uv.x = (v.texcoord2[index] + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; } 

Maintenant que nous avons toutes les coordonnées nécessaires pour ces cellules, nous pouvons échantillonner _HexCellData. Puisque nous échantillonnons la texture dans le programme vertex, nous devons dire explicitement au shader quelle texture de mip utiliser. Cela peut être fait en utilisant une fonction tex2Dlodqui nécessite les coordonnées de quatre textures. Puisque ces cellules n'ont pas de textures mip, nous attribuons des valeurs nulles aux coordonnées supplémentaires.

 float4 GetCellData (appdata_full v, int index) { float2 uv; uv.x = (v.texcoord2[index] + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; float4 data = tex2Dlod(_HexCellData, float4(uv, 0, 0)); } 

Le quatrième composant de données contient un index de type d'élévation, que nous stockons directement sous forme d'octets. Cependant, le GPU l'a automatiquement converti en une valeur à virgule flottante comprise entre 0 et 1. Pour le reconvertir à la valeur correcte, multipliez-le par 255. Après cela, vous pouvez renvoyer les données.

  float4 data = tex2Dlod(_HexCellData, float4(uv, 0, 0)); data.w *= 255; return data; 

Pour utiliser cette fonctionnalité, activez HexCellData dans le shader Terrain . Depuis que j'ai placé ce shader dans Matériaux / Terrain , j'ai besoin d'utiliser le chemin relatif ../HexCellData.cginc .

  #include "../HexCellData.cginc" UNITY_DECLARE_TEX2DARRAY(_MainTex); 

Dans le programme vertex, nous obtenons des données de cellule pour les trois indices cellulaires stockés dans les données de vertex. Attribuez ensuite data.terrainleurs indices d'élévation.

  void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); // data.terrain = v.texcoord2.xyz; float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.terrain.x = cell0.w; data.terrain.y = cell1.w; data.terrain.z = cell2.w; } 

À ce stade, la carte a de nouveau commencé à afficher le terrain correct. La grande différence est que la modification des types de terrain ne conduit plus à de nouvelles triangulations. Si, lors de la modification, toute autre donnée de cellule est modifiée, la triangulation sera effectuée comme d'habitude.

paquet d'unité

Visibilité


Après avoir créé la base de ces cellules, nous pouvons continuer à soutenir la visibilité. Pour ce faire, nous utilisons le shader, les cellules elles-mêmes et les objets qui déterminent la visibilité. Notez que le processus de triangulation n'en sait absolument rien.

Shader


Commençons par expliquer au shader Terrain la visibilité. Il recevra les données de visibilité du programme vertex et les transmettra au programme de fragments à l'aide de la structure Input. Puisque nous passons trois indices d'élévation distincts, nous passerons également trois valeurs de visibilité.

  struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float3 visibility; }; 

Pour stocker la visibilité, nous utilisons le premier composant de ces cellules.

  void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.terrain.x = cell0.w; data.terrain.y = cell1.w; data.terrain.z = cell2.w; data.visibility.x = cell0.x; data.visibility.y = cell1.x; data.visibility.z = cell2.x; } 

Une visibilité de 0 signifie que la cellule est actuellement invisible. S'il était visible, il aurait la valeur de visibilité 1. On peut donc assombrir le terrain en multipliant le résultat GetTerrainColorpar le vecteur de visibilité correspondant. Ainsi, nous modulons individuellement la couleur du relief de chaque cellule mixte.

  float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3(IN.worldPos.xz * 0.02, IN.terrain[index]); float4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, uvw); return c * (IN.color[index] * IN.visibility[index]); } 


Les cellules sont devenues noires.

Ne pouvons-nous pas plutôt combiner la visibilité dans un programme de vertex?
, . . . , . , .

L'obscurité totale est un buste pour les cellules temporairement invisibles. Pour que nous puissions toujours voir le relief, nous devons augmenter l'indicateur utilisé pour les cellules cachées. Passons de 0–1 à ¼ - 1, ce qui peut être fait en utilisant la fonction lerpà la fin du programme vertex.

  void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x; data.visibility.y = cell1.x; data.visibility.z = cell2.x; data.visibility = lerp(0.25, 1, data.visibility); } 


Cellules ombrées.

Suivi de la visibilité des cellules


Pour que la visibilité fonctionne, les cellules doivent suivre leur visibilité. Mais comment une cellule détermine-t-elle si elle est visible? Nous pouvons le faire en suivant le nombre d'entités qui le voient. Lorsque quelqu'un commence à voir une cellule, il doit signaler cette cellule. Et lorsque quelqu'un arrête de voir la cellule, il doit également l'en informer. La cellule garde simplement une trace du nombre d'observateurs, quelles que soient ces entités. Si une cellule a une valeur de visibilité d'au moins 1, elle est visible, sinon elle est invisible. Pour implémenter ce comportement, nous ajoutons HexCelldeux méthodes et une propriété à la variable.

  public bool IsVisible { get { return visibility > 0; } } … int visibility; … public void IncreaseVisibility () { visibility += 1; } public void DecreaseVisibility () { visibility -= 1; } 

Ensuite, ajoutez à la HexCellShaderDataméthode RefreshVisibility, qui fait la même chose que RefreshTerrain, juste pour des raisons de visibilité. Enregistrez les données dans le composant R des cellules de données. Comme nous travaillons avec des octets convertis en valeurs 0–1, nous utilisons pour indiquer la visibilité (byte)255.

  public void RefreshVisibility (HexCell cell) { cellTextureData[cell.Index].r = cell.IsVisible ? (byte)255 : (byte)0; enabled = true; } 

Nous appellerons cette méthode avec une visibilité croissante et décroissante, en changeant la valeur entre 0 et 1.

  public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { ShaderData.RefreshVisibility(this); } } public void DecreaseVisibility () { visibility -= 1; if (visibility == 0) { ShaderData.RefreshVisibility(this); } } 

Créer la visibilité de l'escouade


Faisons en sorte que les unités puissent voir la cellule qu'elles occupent. Ceci est accompli en utilisant un appel IncreaseVisibilityau nouvel emplacement de l'unité pendant la tâche HexUnit.Location. Nous demandons également l'ancien emplacement (s'il existe) DecreaseVisibility.

  public HexCell Location { get { return location; } set { if (location) { location.DecreaseVisibility(); location.Unit = null; } location = value; value.Unit = this; value.IncreaseVisibility(); transform.localPosition = value.Position; } } 


Les unités peuvent voir où elles se trouvent.

Enfin nous avons utilisé la visibilité! Lorsqu'elles sont ajoutées à une carte, les unités rendent leur cellule visible. De plus, leur portée est téléportée lors du déplacement vers leur nouvel emplacement. Mais leur portée reste active lors de la suppression d'unités de la carte. Pour résoudre ce problème, nous réduirons la visibilité de leur emplacement lors de la destruction d'unités.

  public void Die () { if (location) { location.DecreaseVisibility(); } location.Unit = null; Destroy(gameObject); } 

Plage de visibilité


Jusqu'à présent, nous ne voyons que la cellule dans laquelle se trouve le détachement, ce qui limite les possibilités. Au moins, nous devons voir les cellules voisines. Dans le cas général, les unités peuvent voir toutes les cellules à une certaine distance, qui dépend de l'unité.

Ajoutons à la HexGridméthode pour trouver toutes les cellules visibles d'une cellule en tenant compte de la plage. Nous pouvons créer cette méthode en dupliquant et en changeant Search. Modifiez ses paramètres et faites-lui retourner une liste de cellules pour lesquelles vous pouvez utiliser le pool de listes.

À chaque itération, la cellule actuelle est ajoutée à la liste. Il n'y a plus de cellule finale, la recherche ne se terminera donc jamais lorsqu'elle atteindra ce point. Nous nous débarrassons également de la logique des déplacements et du coût du déplacement. Faire les propriétésPathFromon ne les a plus posées car nous n'en avons pas besoin et nous ne voulons pas interférer avec le chemin le long de la grille.

À chaque étape, la distance augmente simplement de 1. Si elle dépasse la plage, cette cellule est ignorée. Et nous n'avons pas besoin d'une heuristique de recherche, nous l'initialisons donc avec une valeur de 0. Autrement dit, nous sommes revenus à l'algorithme de Dijkstra.

  List<HexCell> GetVisibleCells (HexCell fromCell, int range) { List<HexCell> visibleCells = ListPool<HexCell>.Get(); searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; visibleCells.Add(current); // if (current == toCell) { // return true; // } // int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } // … // int moveCost; // … int distance = current.Distance + 1; if (distance > range) { continue; } // int turn = (distance - 1) / speed; // if (turn > currentTurn) { // distance = turn * speed + moveCost; // } if (neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; // neighbor.PathFrom = current; neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; // neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } } } return visibleCells; } 

Ne pouvons-nous pas utiliser un algorithme plus simple pour trouver toutes les cellules à portée?
, , .

Ajoutez également des HexGridméthodes IncreaseVisibilityet DecreaseVisibility. Ils obtiennent la cellule et la plage, prennent une liste des cellules correspondantes et augmentent / diminuent leur visibilité. Une fois terminé, ils devraient retourner la liste dans son pool.

  public void IncreaseVisibility (HexCell fromCell, int range) { List<HexCell> cells = GetVisibleCells(fromCell, range); for (int i = 0; i < cells.Count; i++) { cells[i].IncreaseVisibility(); } ListPool<HexCell>.Add(cells); } public void DecreaseVisibility (HexCell fromCell, int range) { List<HexCell> cells = GetVisibleCells(fromCell, range); for (int i = 0; i < cells.Count; i++) { cells[i].DecreaseVisibility(); } ListPool<HexCell>.Add(cells); } 

Pour utiliser ces méthodes HexUnitnécessite un accès à la grille, ajoutez-y donc une propriété Grid.

  public HexGrid Grid { get; set; } 

Lorsque vous ajoutez une escouade à une grille, elle attribue une grille à cette propriété HexGrid.AddUnit.

  public void AddUnit (HexUnit unit, HexCell location, float orientation) { units.Add(unit); unit.Grid = this; unit.transform.SetParent(transform, false); unit.Location = location; unit.Orientation = orientation; } 

Pour commencer, une plage de visibilité de trois cellules sera suffisante. Pour ce faire, nous ajoutons à la HexUnitconstante, qui à l'avenir peut toujours se transformer en variable. Ensuite, nous ferons en sorte que l'équipe invoque des méthodes pour la grille IncreaseVisibilityet DecreaseVisibility, en transmettant également sa plage de visibilité, et non pas simplement aller à cet endroit.

  const int visionRange = 3; … public HexCell Location { get { return location; } set { if (location) { // location.DecreaseVisibility(); Grid.DecreaseVisibility(location, visionRange); location.Unit = null; } location = value; value.Unit = this; // value.IncreaseVisibility(); Grid.IncreaseVisibility(value, visionRange); transform.localPosition = value.Position; } } … public void Die () { if (location) { // location.DecreaseVisibility(); Grid.DecreaseVisibility(location, visionRange); } location.Unit = null; Destroy(gameObject); } 


Unités avec une plage de visibilité pouvant se chevaucher.

Visibilité lors du déplacement


À l'heure actuelle, la zone de visibilité de l'escouade après la commande de déplacement est immédiatement téléportée au point final. Il aurait été préférable que l'unité et son champ de visibilité se rapprochent. La première étape consiste à ne plus définir la propriété Locationc HexUnit.Travel. Au lieu de cela, nous changerons directement le champ location, en évitant le code de propriété. Par conséquent, nous effacerons manuellement l'ancien emplacement et configurerons un nouvel emplacement. La visibilité restera inchangée.

  public void Travel (List<HexCell> path) { // Location = path[path.Count - 1]; location.Unit = null; location = path[path.Count - 1]; location.Unit = this; pathToTravel = path; StopAllCoroutines(); StartCoroutine(TravelPath()); } 

À l'intérieur des coroutines, TravelPathnous ne réduirons la visibilité de la première cellule qu'après l'achèvement LookAt. Après cela, avant de passer à une nouvelle cellule, nous augmenterons la visibilité de cette cellule. Cela fait, nous en réduisons encore la visibilité. Enfin, augmentez la visibilité depuis la dernière cellule.

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; // transform.localPosition = c; yield return LookAt(pathToTravel[1].Position); Grid.DecreaseVisibility(pathToTravel[0], visionRange); float t = Time.deltaTime * travelSpeed; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; Grid.IncreaseVisibility(pathToTravel[i], visionRange); for (; t < 1f; t += Time.deltaTime * travelSpeed) { … } Grid.DecreaseVisibility(pathToTravel[i], visionRange); t -= 1f; } a = c; b = location.Position; // We can simply use the destination here. c = b; Grid.IncreaseVisibility(location, visionRange); for (; t < 1f; t += Time.deltaTime * travelSpeed) { … } … } 


Visibilité en mouvement.

Tout cela fonctionne, sauf lorsqu'un nouvel ordre est émis au moment du déplacement du détachement. Cela conduit à la téléportation, qui devrait également s'appliquer à la visibilité. Pour réaliser cela, nous devons suivre l'emplacement actuel de l'équipe pendant le déplacement.

  HexCell location, currentTravelLocation; 

Nous mettrons à jour cet emplacement chaque fois que nous touchons une nouvelle cellule pendant le déplacement, jusqu'à ce que l'équipe atteigne la cellule finale. Ensuite, il doit être réinitialisé.

  IEnumerator TravelPath () { … for (int i = 1; i < pathToTravel.Count; i++) { currentTravelLocation = pathToTravel[i]; a = c; b = pathToTravel[i - 1].Position; c = (b + currentTravelLocation.Position) * 0.5f; Grid.IncreaseVisibility(pathToTravel[i], visionRange); for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); yield return null; } Grid.DecreaseVisibility(pathToTravel[i], visionRange); t -= 1f; } currentTravelLocation = null; … } 

Maintenant, après avoir terminé le virage, TravelPathnous pouvons vérifier si l'ancien emplacement intermédiaire du chemin est connu. Si oui, vous devez réduire la visibilité dans cette cellule, et non au début du chemin.

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); Grid.DecreaseVisibility( currentTravelLocation ? currentTravelLocation : pathToTravel[0], visionRange ); … } 

Nous devons également corriger la visibilité après recompilation qui s'est produite lors du mouvement de l'équipe. Si l'emplacement intermédiaire est toujours connu, réduisez la visibilité et augmentez la visibilité au point final, puis réinitialisez l'emplacement intermédiaire.

  void OnEnable () { if (location) { transform.localPosition = location.Position; if (currentTravelLocation) { Grid.IncreaseVisibility(location, visionRange); Grid.DecreaseVisibility(currentTravelLocation, visionRange); currentTravelLocation = null; } } } 

paquet d'unité

Visibilité des routes et de l'eau


Bien que les changements de couleur du relief soient basés sur la visibilité, cela n'affecte pas les routes et l'eau. Ils ont l'air trop lumineux pour les cellules invisibles. Pour appliquer la visibilité aux routes et à l'eau, nous devons ajouter des indices de cellules et mélanger les poids à leurs données de maillage. Par conséquent, nous vérifierons les enfants des données d' utilisation des cellules pour les rivières , les routes , l' eau , la rive de l'eau et les estuaires du fragment préfabriqué.

Les routes


Nous partirons des routes. La méthode est HexGridChunk.TriangulateRoadEdgeutilisée pour créer une petite partie de la route au centre de la cellule, elle a donc besoin d'un index de cellule. Ajoutez-y un paramètre et générez des données de cellule pour le triangle.

  void TriangulateRoadEdge ( Vector3 center, Vector3 mL, Vector3 mR, float index ) { roads.AddTriangle(center, mL, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); Vector3 indices; indices.x = indices.y = indices.z = index; roads.AddTriangleCellData(indices, weights1); } 

Un autre moyen simple de créer des routes est TriangulateRoadSegment. Il est utilisé à l'intérieur et entre les cellules, il devrait donc fonctionner avec deux index différents. Pour cela, il est pratique d'utiliser le paramètre vecteur d'index. Étant donné que les segments de route peuvent faire partie de rebords, les poids doivent également passer par des paramètres.

  void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6, Color w1, Color w2, Vector3 indices ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); roads.AddQuadUV(0f, 1f, 0f, 0f); roads.AddQuadUV(1f, 0f, 0f, 0f); roads.AddQuadCellData(indices, w1, w2); roads.AddQuadCellData(indices, w1, w2); } 

Passons maintenant à TriangulateRoad, ce qui crée des routes à l'intérieur des cellules. Il a également besoin d'un paramètre d'index. Il transmet ces données aux méthodes routières qu'il appelle et les ajoute aux triangles qu'il crée.

  void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e, bool hasRoadThroughCellEdge, float index ) { if (hasRoadThroughCellEdge) { Vector3 indices; indices.x = indices.y = indices.z = index; Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment( mL, mC, mR, e.v2, e.v3, e.v4, weights1, weights1, indices ); 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) ); roads.AddTriangleCellData(indices, weights1); roads.AddTriangleCellData(indices, weights1); } else { TriangulateRoadEdge(center, mL, mR, index); } } 

Il reste à ajouter les arguments de méthode nécessaires TriangulateRoad, TriangulateRoadEdgeet TriangulateRoadSegmentcorriger toutes les erreurs du compilateur.

  void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Index); 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), cell.Index ); } } … void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge, cell.Index); if (previousHasRiver) { TriangulateRoadEdge(roadCenter, center, mL, cell.Index); } if (nextHasRiver) { TriangulateRoadEdge(roadCenter, mR, center, cell.Index); } } … void TriangulateEdgeStrip () { … if (hasRoad) { TriangulateRoadSegment( e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4, w1, w2, indices ); } } 

Maintenant, les données de maillage sont correctes et nous allons passer au shader Road . Il a besoin d'un programme vertex et il doit contenir HexCellData .

  #pragma surface surf Standard fullforwardshadows decal:blend vertex:vert #pragma target 3.0 #include "HexCellData.cginc" 

Comme nous ne mélangeons pas plusieurs matériaux, il nous suffira de passer un indicateur de visibilité dans le programme des fragments.

  struct Input { float2 uv_MainTex; float3 worldPos; float visibility; }; 

Il suffit qu'un nouveau programme vertex reçoive des données de deux cellules. Nous mélangeons immédiatement leur visibilité, l'ajustons et ajoutons à la sortie.

  void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility = lerp(0.25, 1, data.visibility); } 

Dans le programme de fragments, nous avons juste besoin d'ajouter de la visibilité à la couleur.

  void surf (Input IN, inout SurfaceOutputStandard o) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color * ((noise.y * 0.75 + 0.25) * IN.visibility); … } 


Routes avec visibilité.

Eau libre


Il peut sembler que la visibilité a déjà affecté l'eau, mais ce n'est que la surface d'un terrain immergé dans l'eau. Commençons par appliquer la visibilité en eau libre. Pour cela, nous devons changer HexGridChunk.TriangulateOpenWater.

  void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … water.AddTriangle(center, c1, c2); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; water.AddTriangleCellData(indices, weights1); if (direction <= HexDirection.SE && neighbor != null) { … water.AddQuad(c1, c2, e1, e2); indices.y = neighbor.Index; water.AddQuadCellData(indices, weights1, weights2); if (direction <= HexDirection.E) { … water.AddTriangle( c2, e2, c2 + HexMetrics.GetWaterBridge(direction.Next()) ); indices.z = nextNeighbor.Index; water.AddTriangleCellData( indices, weights1, weights2, weights3 ); } } } 

Nous devons également ajouter des données cellulaires aux fans des triangles près des côtes.

  void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … water.AddTriangle(center, e1.v1, e1.v2); water.AddTriangle(center, e1.v2, e1.v3); water.AddTriangle(center, e1.v3, e1.v4); water.AddTriangle(center, e1.v4, e1.v5); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); … } 

Le shader Water doit être changé de la même manière que le shader Road , mais il doit combiner la visibilité non pas de deux, mais de trois cellules.

  #pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float3 worldPos; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { float waves = Waves(IN.worldPos.xz, _MainTex); fixed4 c = saturate(_Color + waves); o.Albedo = c.rgb * IN.visibility; … } 


Eau libre avec visibilité.

Côte et estuaire


Pour soutenir la côte, nous devons changer à nouveau HexGridChunk.TriangulateWaterShore. Nous avons déjà créé un vecteur d'index, mais nous n'avons utilisé qu'un seul indice de cellule pour l'eau libre. La côte a également besoin d'un index voisin, alors changez le code.

  Vector3 indices; // indices.x = indices.y = indices.z = cell.Index; indices.x = indices.z = cell.Index; indices.y = neighbor.Index; 

Ajoutez les données des cellules aux quads et au triangle de la côte. Nous transmettons également les index lors de l'appel TriangulateEstuary.

  if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary( e1, e2, cell.IncomingRiver == direction, indices ); } else { … waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); } HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { … waterShore.AddTriangleUV( … ); indices.z = nextNeighbor.Index; waterShore.AddTriangleCellData( indices, weights1, weights2, weights3 ); } 

Ajoutez le paramètre nécessaire TriangulateEstuaryet prenez soin de ces cellules pour la côte et la bouche. N'oubliez pas que la bouche est en trapèze avec deux triangles de la côte sur les côtés. Nous nous assurons que les poids sont transférés dans le bon ordre.

  void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver, Vector3 indices ) { waterShore.AddTriangle(e2.v1, e1.v2, e1.v1); waterShore.AddTriangle(e2.v5, e1.v5, e1.v4); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); waterShore.AddTriangleCellData(indices, weights2, weights1, weights1); waterShore.AddTriangleCellData(indices, weights2, weights1, weights1); estuaries.AddQuad(e2.v1, e1.v2, e2.v2, e1.v3); estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5); estuaries.AddQuadUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0f, 0f) ); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(1f, 1f) ); estuaries.AddQuadUV( new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0f, 1f) ); estuaries.AddQuadCellData( indices, weights2, weights1, weights2, weights1 ); estuaries.AddTriangleCellData(indices, weights1, weights2, weights2); estuaries.AddQuadCellData(indices, weights1, weights2); … } 

Dans le shader WaterShore , vous devez effectuer les mêmes modifications que dans le shader Water , en mélangeant la visibilité des trois cellules.

  #pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float3 worldPos; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { … fixed4 c = saturate(_Color + max(foam, waves)); o.Albedo = c.rgb * IN.visibility; … } 

Le shader Estuary mélange la visibilité de deux cellules, tout comme le shader Road . Il a déjà un programme de sommets, car nous avons besoin de lui pour transmettre les coordonnées UV des rivières.

  #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float2 riverUV; float3 worldPos; float visibility; }; half _Glossiness; half _Metallic; fixed4 _Color; void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.riverUV = v.texcoord1.xy; float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); o.visibility = cell0.x * v.color.x + cell1.x * v.color.y; o.visibility = lerp(0.25, 1, o.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { … fixed4 c = saturate(_Color + water); o.Albedo = c.rgb * IN.visibility; … } 


Côte et estuaire avec visibilité.

Rivières


Les dernières régions aquatiques avec lesquelles travailler sont les rivières. Ajoutez un HexGridChunk.TriangulateRiverQuadvecteur d'index au paramètre et ajoutez-le au maillage afin qu'il puisse conserver la visibilité de deux cellules.

  void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, float v, bool reversed, Vector3 indices ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, v, reversed, indices); } void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float v, bool reversed, Vector3 indices ) { … rivers.AddQuadCellData(indices, weights1, weights2); } 

TriangulateWithRiverBeginOrEndcrée des points d'extrémité de rivière avec un quad et un triangle au centre de la cellule. Ajoutez les données de cellule nécessaires pour cela.

  void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.HasIncomingRiver; Vector3 indices; indices.x = indices.y = indices.z = cell.Index; TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed, indices ); center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); … rivers.AddTriangleCellData(indices, weights1); } } 

Nous avons déjà ces indices de cellule TriangulateWithRiver, nous les transmettons donc simplement à l'appel TriangulateRiverQuad.

  void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.IncomingRiver == direction; TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, 0.4f, reversed, indices ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed, indices ); } } 

Nous ajoutons également un support d'index aux chutes d'eau qui se déversent en eau profonde.

  void TriangulateWaterfallInWater ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float waterY, Vector3 indices ) { … rivers.AddQuadCellData(indices, weights1, weights2); } 

Et enfin, changez-le TriangulateConnectionpour qu'il passe les index nécessaires aux méthodes des rivières et cascades.

  void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (hasRiver) { e2.v3.y = neighbor.StreamBedY; Vector3 indices; indices.x = indices.z = cell.Index; indices.y = neighbor.Index; if (!cell.IsUnderwater) { if (!neighbor.IsUnderwater) { TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction, indices ); } else if (cell.Elevation > neighbor.WaterLevel) { TriangulateWaterfallInWater( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, neighbor.WaterSurfaceY, indices ); } } else if ( !neighbor.IsUnderwater && neighbor.Elevation > cell.WaterLevel ) { TriangulateWaterfallInWater( e2.v4, e2.v2, e1.v4, e1.v2, neighbor.RiverSurfaceY, cell.RiverSurfaceY, cell.WaterSurfaceY, indices ); } } … } 

Le shader River doit apporter les mêmes modifications que le shader Road .

  #pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { float river = River(IN.uv_MainTex, _MainTex); fixed4 c = saturate(_Color + river); o.Albedo = c.rgb * IN.visibility; … } 


Rivières avec visibilité.

paquet d'unité

Objets et visibilité


La visibilité fonctionne désormais pour l'ensemble du terrain généré par la procédure, mais jusqu'à présent, elle n'affecte pas les caractéristiques du terrain. Les bâtiments, les fermes et les arbres sont créés à partir de préfabriqués et non à partir de la géométrie procédurale, nous ne pouvons donc pas ajouter d'indices de cellules et mélanger les poids avec leurs sommets. Étant donné que chacun de ces objets appartient à une seule cellule, nous devons déterminer dans quelle cellule ils se trouvent. Si nous pouvons le faire, nous aurons alors accès aux données des cellules correspondantes et appliquerons la visibilité.

Nous pouvons déjà transformer les positions XZ du monde en indices cellulaires. Cette transformation a été utilisée pour modifier le terrain et gérer les escouades. Cependant, le code correspondant n'est pas trivial. Il utilise des opérations entières et nécessite une logique pour fonctionner avec les bords. Ceci n'est pas pratique pour un shader, nous pouvons donc cuire la majeure partie de la logique dans une texture et l'utiliser.

Nous utilisons déjà une texture avec un motif hexagonal pour projeter la grille sur la topographie. Cette texture définit une zone de cellule de 2 × 2. Par conséquent, nous pouvons facilement calculer dans quelle zone nous nous trouvons. Après cela, vous pouvez appliquer une texture contenant des décalages X et Z pour les cellules de cette zone et utiliser ces données pour calculer la cellule dans laquelle nous nous trouvons.

Voici une texture similaire. Le décalage X est stocké dans son canal rouge et le décalage Z est stocké dans le canal vert. Puisqu'il couvre la zone de 2 × 2 cellules, nous avons besoin de décalages de 0 et 2. Ces données ne peuvent pas être stockées dans le canal de couleur, donc les décalages sont réduits de moitié. Nous n'avons pas besoin de bords clairs des cellules, donc une petite texture suffit.


La texture des coordonnées de la grille.

Ajoutez de la texture au projet. Réglez son mode d' habillage sur Répéter , tout comme les autres textures de maillage. Nous n'avons pas besoin de mélange, donc pour le mode Blend, nous choisirons Point . Désactivez également la compression afin que les données ne soient pas déformées. Désactivez le mode sRGB pour que lors du rendu en mode linéaire, aucune conversion d'espace colorimétrique ne soit effectuée. Et enfin, nous n'avons pas besoin de textures mip.


Options d'importation de texture.

Ombrage d'objets avec visibilité


Créez un nouvel ombrage de fonction pour ajouter un support de visibilité aux objets. Il s'agit d'un simple shader de surface avec un programme de vertex. Ajoutez-y HexCellData et passez l'indicateur de visibilité au programme de fragment, et comme d'habitude, considérez-le en couleur. La différence ici est que nous ne pouvons pas l'utiliser GetCellDatacar les données de maillage requises n'existent pas. Au lieu de cela, nous avons une position dans le monde. Mais pour l'instant, laissez la visibilité égale à 1.

 Shader "Custom/Feature" { 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 [NoTilingOffset] _GridCoordinates ("Grid Coordinates", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.0 #include "../HexCellData.cginc" sampler2D _MainTex, _GridCoordinates; half _Glossiness; half _Metallic; fixed4 _Color; struct Input { float2 uv_MainTex; float visibility; }; void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float3 pos = mul(unity_ObjectToWorld, v.vertex); data.visibility = 1; } void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb * IN.visibility; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" } 

Modifiez tous les matériaux des objets pour qu'ils utilisent le nouveau shader et affectez-leur la texture des coordonnées de la grille.


Urbain avec texture en maille.

Accéder aux données des cellules


Pour échantillonner la texture des coordonnées de la grille dans le programme des sommets, nous avons à nouveau besoin d' tex2Dlodun vecteur de coordonnées de texture à quatre composants. Les deux premières coordonnées sont la position du monde XZ. Les deux autres sont égaux à zéro comme précédemment.

  void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float3 pos = mul(unity_ObjectToWorld, v.vertex); float4 gridUV = float4(pos.xz, 0, 0); data.visibility = 1; } 

Comme dans le shader Terrain , nous étirons les coordonnées UV afin que la texture ait le rapport d'aspect correct correspondant à la grille d'hexagones.

  float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); 

Nous pouvons découvrir dans quelle partie des cellules 2 × 2 nous nous trouvons en prenant la valeur des coordonnées UV arrondies vers le bas. Cela constitue la base des coordonnées des cellules.

  float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); float2 cellDataCoordinates = floor(gridUV.xy); 

Pour trouver les coordonnées de la cellule dans laquelle nous nous trouvons, nous ajoutons les déplacements stockés dans la texture.

  float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg; 

Puisqu'une partie de la grille est de 2 × 2 et que les décalages sont divisés par deux, nous devons doubler le résultat pour obtenir les coordonnées finales.

  float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg; cellDataCoordinates *= 2; 

Maintenant, nous avons les coordonnées XZ de la grille de cellules que nous devons convertir en coordonnées UV de ces cellules. Cela peut être fait en se déplaçant simplement vers le centre des pixels, puis en les divisant en tailles de texture. Ajoutons donc une fonction pour cela au fichier d'inclusion HexCellData qui gérera également l'échantillonnage.

 float4 GetCellData (float2 cellDataCoordinates) { float2 uv = cellDataCoordinates + 0.5; uv.x *= _HexCellData_TexelSize.x; uv.y *= _HexCellData_TexelSize.y; return tex2Dlod(_HexCellData, float4(uv, 0, 0)); } 

Maintenant , nous pouvons utiliser dans le programme de vertex shader la fonction .

  cellDataCoordinates *= 2; data.visibility = GetCellData(cellDataCoordinates).x; data.visibility = lerp(0.25, 1, data.visibility); 


Objets avec visibilité.

Enfin, la visibilité affecte l'ensemble de la carte, à l'exception des unités qui sont toujours visibles. Puisque nous déterminons la visibilité des objets pour chaque sommet, puis pour l'objet traversant la limite des cellules, la visibilité des cellules qu'il ferme sera mélangée. Mais les objets sont si petits qu'ils restent constamment à l'intérieur de leur cellule, même en tenant compte de la distorsion des positions. Cependant, certains peuvent faire partie des sommets d'une autre cellule. Par conséquent, notre approche est bon marché, mais imparfaite. Ceci est plus visible dans le cas des murs, dont la visibilité varie entre les visibilités des cellules voisines.


Murs à visibilité variable.

Étant donné que les segments de mur sont générés de manière procédurale, nous pouvons ajouter des données de cellule à leur maillage et utiliser l'approche que nous avons utilisée pour le relief. Malheureusement, les tours sont préfabriquées, nous aurons donc encore des incohérences. De manière générale, l'approche existante semble assez bonne pour la géométrie simple que nous utilisons. À l'avenir, nous envisagerons des modèles et des murs plus détaillés, par conséquent, nous améliorerons la méthode de mélange de leur visibilité.

paquet d'unité

Partie 21: recherche cartographique


  • Nous affichons tout lors de l'édition.
  • Nous suivons les cellules enquêtées.
  • Nous cachons ce qui est encore inconnu.
  • Nous forçons les unités à éviter les zones inexplorées.

Dans la partie précédente, nous avons ajouté le brouillard de guerre, que nous allons maintenant affiner pour mettre en œuvre la recherche cartographique.


Nous sommes prêts à explorer le monde.

Afficher la carte entière en mode édition


Le sens de l'étude est que jusqu'à ce que les cellules ne soient pas vues, elles sont considérées comme inconnues et donc invisibles. Ils ne doivent pas être masqués, mais pas affichés du tout. Par conséquent, avant d'ajouter un support de recherche, nous activerons la visibilité en mode édition.

Commutation de visibilité


Nous pouvons contrôler si les shaders utilisent la visibilité en utilisant le mot-clé, comme cela a été fait avec la superposition sur la grille. Utilisons le mot clé HEX_MAP_EDIT_MODE pour indiquer l'état du mode d'édition. Puisque plusieurs shaders doivent connaître ce mot-clé, nous le définirons globalement en utilisant des méthodes statiques Shader.EnableKeyWordet Shader.DisableKeyword. Nous appellerons la méthode appropriée HexGameUI.SetEditModelors du changement de mode d'édition.

  public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); if (toggle) { Shader.EnableKeyword("HEX_MAP_EDIT_MODE"); } else { Shader.DisableKeyword("HEX_MAP_EDIT_MODE"); } } 

Shaders du mode édition


Lorsque HEX_MAP_EDIT_MODE est défini, les shaders ignorent la visibilité. Cela se résume au fait que la visibilité des cellules sera toujours considérée comme égale à 1. Ajoutons une fonction pour filtrer les données des cellules en fonction du mot-clé au début du fichier inclus HexCellData .

 sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.x = 1; #endif return data; } 

Nous passons par cette fonction le résultat des deux fonctions GetCellDataavant de la retourner.

 float4 GetCellData (appdata_full v, int index) { … return FilterCellData(data); } float4 GetCellData (float2 cellDataCoordinates) { … return FilterCellData(tex2Dlod(_HexCellData, float4(uv, 0, 0))); } 

Pour que tout fonctionne, tous les shaders pertinents doivent recevoir la directive multi_compile pour créer des options au cas où le mot clé HEX_MAP_EDIT_MODE est défini. Ajoutez la ligne appropriée aux shaders Estuaire , Entité , Rivière , Route , Terrain , Eau et Rive d'eau , entre la directive cible et la première directive d'inclusion.

  #pragma multi_compile _ HEX_MAP_EDIT_MODE 

Maintenant, lorsque vous passez en mode d'édition de carte, le brouillard de guerre disparaîtra.

paquet d'unité

Recherche cellulaire


Par défaut, les cellules doivent être considérées comme inexplorées. Ils sont explorés lorsqu'une équipe les voit. Après cela, ils continuent de faire l'objet d'une enquête si un détachement peut les voir.

Suivi de l'état de l'étude


Pour ajouter un support pour le suivi du statut des études, nous ajoutons à la HexCellpropriété générale IsExplored.

  public bool IsExplored { get; set; } 

L'état de l'étude est déterminé par la cellule elle-même. Par conséquent, cette propriété doit être définie uniquement HexCell. Pour ajouter cette restriction, nous allons mettre le setter privé.

  public bool IsExplored { get; private set; } 

La première fois que la visibilité de la cellule devient supérieure à zéro, la cellule commence à être considérée comme étudiée et IsExploredune valeur doit donc être attribuée true. En fait, il nous suffira de simplement marquer la cellule comme examinée lorsque la visibilité augmente à 1. Cela doit être fait avant l'appel RefreshVisibility.

  public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { IsExplored = true; ShaderData.RefreshVisibility(this); } } 

Transfert de l'état de la recherche aux shaders


Comme dans le cas de la visibilité des cellules, nous transférons leur état de recherche aux shaders via les données du shader. Au final, ce n'est qu'un autre type de visibilité. HexCellShaderData.RefreshVisibilitystocke l'état de visibilité dans le canal de données R. Gardons l'état de l'étude dans les données du canal G.

  public void RefreshVisibility (HexCell cell) { int index = cell.Index; cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; enabled = true; } 

Relief noir inexploré


Nous pouvons maintenant utiliser des shaders pour visualiser l'état de la recherche cellulaire. Pour nous assurer que tout fonctionne comme il se doit, nous rendons simplement le terrain inexploré noir. Mais d'abord, pour que le mode d'édition fonctionne, modifiez-le FilterCellDataafin qu'il filtre les données de recherche.

 float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.xy = 1; #endif return data; } 

Le shader Terrain transmet les données de visibilité des trois cellules possibles au programme de fragments. Dans le cas de l'état de recherche, nous les combinons dans le programme vertex et transférons la seule valeur au programme fragment. Ajoutez le visibilityquatrième composant à l' entrée afin d'avoir une place pour cela.

  struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float4 visibility; }; 

Maintenant, dans le programme vertex, lorsque nous changeons l'indice de visibilité, nous devons explicitement y accéder data.visibility.xyz.

  void vert (inout appdata_full v, out Input data) { … data.visibility.xyz = lerp(0.25, 1, data.visibility.xyz); } 

Après cela, nous combinons les états de l'étude et écrivons le résultat data.visibility.w. Cela revient à combiner la visibilité dans d'autres shaders, mais en utilisant le composant Y de ces cellules.

  data.visibility.xyz = lerp(0.25, 1, data.visibility.xyz); data.visibility.w = cell0.y * v.color.x + cell1.y * v.color.y + cell2.y * v.color.z; 

Le statut de la recherche est désormais disponible dans le programme Fragment via IN.visibility.w. Considérez-le dans le calcul de l'albédo.

  void surf (Input IN, inout SurfaceOutputStandard o) { … float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } 


La topographie inexplorée est maintenant noire.

Le relief des cellules inexplorées a désormais une couleur noire. Mais cela n'a pas encore affecté les objets, les routes et l'eau. Cependant, cela suffit pour s'assurer que l'étude fonctionne.

Enregistrement et chargement de l'état de la recherche


Maintenant que nous avons ajouté un support de recherche, nous devons nous assurer que l'état de la recherche est pris en compte lors de l'enregistrement et du chargement des cartes. Par conséquent, nous devons augmenter la version des fichiers de carte à 3. Pour rendre ces modifications plus pratiques, ajoutons une SaveLoadMenuconstante pour cela .

  const int mapFileVersion = 3; 

Nous utiliserons cette constante lors de l'écriture de la version du fichier Saveet lors de la vérification de la prise en charge des fichiers Load.

  void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(mapFileVersion); hexGrid.Save(writer); } } void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= mapFileVersion) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } } 

Comme étape finale, HexCell.Savenous enregistrons le statut de l'étude.

  public void Save (BinaryWriter writer) { … writer.Write(IsExplored); } 

Et nous le lirons à la fin Load. Après cela, nous appellerons RefreshVisibilityau cas où l'état de l'étude serait différent du précédent.

  public void Load (BinaryReader reader) { … IsExplored = reader.ReadBoolean(); ShaderData.RefreshVisibility(this); } 

Pour conserver la compatibilité descendante avec les anciens fichiers de sauvegarde, nous devons ignorer la lecture de l'état de sauvegarde si la version du fichier est inférieure à 3. Dans ce cas, par défaut, les cellules auront l'état «inexploré». Pour ce faire, nous devons ajouter des Loaddonnées d'en-tête en tant que paramètre .

  public void Load (BinaryReader reader, int header) { … IsExplored = header >= 3 ? reader.ReadBoolean() : false; ShaderData.RefreshVisibility(this); } 

Il HexGrid.Loadva maintenant devoir passer les HexCell.Loaddonnées d' en- tête.

  public void Load (BinaryReader reader, int header) { … for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … } 

Désormais, lors de l'enregistrement et du chargement des cartes, l'état d'exploration des cellules sera pris en compte.

paquet d'unité

Masquer les cellules inconnues


Au stade actuel, les cellules inexplorées sont visuellement indiquées par un relief noir. Mais en réalité, nous voulons que ces cellules soient invisibles car elles sont inconnues. Nous pouvons rendre la géométrie opaque transparente afin qu'elle ne soit pas visible. Cependant, le framework d'ombrage de surface Unity a été développé sans cette possibilité à l'esprit. Au lieu d'utiliser une véritable transparence, nous changerons les shaders pour qu'ils correspondent à l'arrière-plan, ce qui les rendra également invisibles.

Rendre le relief vraiment noir


Bien que le relief étudié soit noir, on peut encore le reconnaître car il a toujours un éclairage spéculaire. Pour se débarrasser de l'éclairage, nous devons le rendre parfaitement noir mat. Afin de ne pas affecter les autres propriétés de surface, il est plus facile de changer la couleur spéculaire en noir. Cela est possible si vous utilisez un shader de surface qui fonctionne avec du spéculaire, mais maintenant nous utilisons le métal standard. Commençons donc par basculer le shader Terrain sur spéculaire.

Remplacez la propriété de couleur _Metallic sur la propriété _Specular . Par défaut, sa valeur de couleur doit être égale à (0,2, 0,2, 0,2). Nous garantissons donc qu'elle correspondra à l'apparence de la version métallique.

  Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _GridTex ("Grid Texture", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 // _Metallic ("Metallic", Range(0,1)) = 0.0 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) } 

Modifiez également les variables de shader correspondantes. La couleur des shaders de surface spéculaires est définie comme fixed3, utilisons-la donc.

  half _Glossiness; // half _Metallic; fixed3 _Specular; fixed4 _Color; 

Modifiez le surf surfacique pragma de Standard à StandardSpecular . Cela forcera Unity à générer des shaders en utilisant spéculaire.

  #pragma surface surf StandardSpecular fullforwardshadows vertex:vert 

Maintenant, la fonction a surfbesoin que le deuxième paramètre soit de type SurfaceOutputStandardSpecular. De plus, vous devez maintenant attribuer la valeur non o.Metallic, mais o.Specular.

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; // o.Metallic = _Metallic; o.Specular = _Specular; o.Smoothness = _Glossiness; o.Alpha = ca; } 

Maintenant, nous pouvons masquer les reflets en considérant la exploredcouleur spéculaire.

  o.Specular = _Specular * explored; 


Terrain inexploré sans éclairage réfléchi.

Comme vous pouvez le voir sur la photo, le relief inexploré est maintenant d'un noir terne. Cependant, lorsqu'elles sont vues sous un angle tangent, les surfaces se transforment en miroir, à cause de quoi le relief commence à refléter l'environnement, c'est-à-dire la skybox.

Pourquoi les surfaces deviennent-elles des miroirs?
. . Rendering .


Les zones inexplorées reflètent toujours l'environnement.

Pour se débarrasser de ces reflets, on considérera le relief inexploré complètement ombré. Ceci est accompli en attribuant une valeur au exploredparamètre d'occlusion, que nous utilisons comme masque de réflexion.

  float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca; 


Inexploré sans reflets.

Fond assorti


Maintenant que le terrain inexploré ignore tout l'éclairage, vous devez le faire correspondre à l'arrière-plan. Comme notre caméra regarde toujours d'en haut, l'arrière-plan est toujours gris. Pour indiquer au shader Terrain quelle couleur utiliser, ajoutez la propriété _BackgroundColor , qui est par défaut le noir.

  Properties { … _BackgroundColor ("Background Color", Color) = (0,0,0) } … half _Glossiness; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor; 

Pour utiliser cette couleur, nous l'ajouterons comme lumière émissive. Ceci est o.Emissionaccompli en attribuant une valeur de couleur d'arrière-plan multipliée par un moins exploré.

  o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored); 

Puisque nous utilisons la skybox par défaut, la couleur d'arrière-plan visible n'est en fait pas la même. En général, un gris légèrement rougeâtre serait la meilleure couleur. Lors de la configuration du matériau en relief, vous pouvez utiliser le code 68615BFF pour Hex Color .


Matériau en relief avec fond gris.

En général, cela fonctionne, bien que si vous savez où chercher, vous remarquerez des silhouettes très faibles. Pour que le joueur ne puisse pas les voir, vous pouvez attribuer une couleur d'arrière-plan uniforme de 68615BFF à la caméra au lieu de skybox.


Appareil photo avec une couleur de fond uniforme.

Pourquoi ne pas retirer la skybox?
, , environmental lighting . , .

Maintenant, nous ne pouvons pas trouver la différence entre le fond et les cellules inexplorées. Une topographie inexplorée élevée peut toujours masquer une topographie explorée basse à de faibles angles de caméra. De plus, des parties inexplorées projettent toujours des ombres sur l'exploré. Mais ces indices minimaux peuvent être négligés.


Les cellules inexplorées ne sont plus visibles.

Que faire si vous n'utilisez pas une couleur d'arrière-plan uniforme?
, , . . , . , , , UV- .

Masquer les objets en relief


Maintenant, nous n'avons plus que le maillage du relief caché. Le reste de l'état de l'étude n'a pas encore été affecté.


Jusqu'à présent, seul le relief est caché.

Modifions le shader Feature , qui est un shader opaque comme Terrain . Transformez-le en un shader spéculaire et ajoutez-lui la couleur d'arrière-plan. Commençons par les propriétés.

  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 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) _BackgroundColor ("Background Color", Color) = (0,0,0) [NoScaleOffset] _GridCoordinates ("Grid Coordinates", 2D) = "white" {} } 

Surface et variables du pragma supplémentaires, comme précédemment.

  #pragma surface surf StandardSpecular fullforwardshadows vertex:vert … half _Glossiness; // half _Metallic; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor; 

visibilityun autre composant est également requis. Puisque Feature combine la visibilité pour chaque sommet, il ne lui fallait qu'une seule valeur flottante. Il nous en faut maintenant deux.

  struct Input { float2 uv_MainTex; float2 visibility; }; 

Modifiez-le vertafin qu'il utilise explicitement pour les données de visibilité data.visibility.x, puis affectez la data.visibility.yvaleur des données d'étude.

  void vert (inout appdata_full v, out Input data) { … float4 cellData = GetCellData(cellDataCoordinates); data.visibility.x = cellData.x; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cellData.y; } 

Modifiez-le surfpour qu'il utilise les nouvelles données, comme Terrain .

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; float explored = IN.visibility.y; o.Albedo = c.rgb * (IN.visibility.x * explored); // o.Metallic = _Metallic; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored); o.Alpha = ca; } 


Objets de secours cachés.

Masquer l'eau


Ensuite, les shaders Water et Water Shore . Commençons par les convertir en shaders spéculaires. Cependant, ils n'ont pas besoin d'une couleur d'arrière-plan car ce sont des shaders transparents.

Après la conversion, ajoutez visibilityun autre composant et modifiez-le en conséquence vert. Les deux shaders combinent les données de trois cellules.

  struct Input { … float2 visibility; }; … void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cell0.y * v.color.x + cell1.y * v.color.y + cell2.y * v.color.z; } 

Water and Water Shore effectuent surfdes opérations différentes, mais définissent leurs propriétés de surface de la même manière. Puisqu'ils sont transparents, nous prendrons en compte exploredans le canal alpha, et nous ne fixerons pas d'émission.

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.y; o.Albedo = c.rgb * IN.visibility.x; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca * explored; } 


Eau cachée.

Nous cachons les estuaires, les rivières et les routes


Nous avons toujours les shaders pour l' estuaire , la rivière et la route . Les trois sont transparents et combinent les données de deux cellules. Mettez-les tous en spéculaire, puis ajoutez-les aux visibilitydonnées de recherche.

  struct Input { … float2 visibility; }; … void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cell0.y * v.color.x + cell1.y * v.color.y; } 

Modifiez la fonction des surfshaders Estuaire et Rivière pour qu'elle utilise les nouvelles données. Les deux doivent apporter les mêmes modifications.

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.y; fixed4 c = saturate(_Color + water); o.Albedo = c.rgb * IN.visibility.x; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca * explored; } 

Le Shader Road est un peu différent car il utilise une métrique de mélange supplémentaire.

  void surf (Input IN, inout SurfaceOutputStandardSpecular o) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color * ((noise.y * 0.75 + 0.25) * IN.visibility.x); float blend = IN.uv_MainTex.x; blend *= noise.x + 0.5; blend = smoothstep(0.4, 0.7, blend); float explored = IN.visibility.y; o.Albedo = c.rgb; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = blend * explored; } 


Tout est caché.

paquet d'unité

Éviter les cellules inexplorées


Bien que tout ce qui est inconnu soit visuellement caché, l'état de l'étude n'est pas pris en compte lors de la recherche d'un chemin. En conséquence, les unités peuvent être commandées pour se déplacer à travers et à travers des cellules inexplorées, déterminant magiquement la voie à suivre. Nous devons forcer les unités pour éviter les cellules inexplorées.


Parcourez les cellules inexplorées.

Les escouades déterminent le coût du déplacement


Avant de s'attaquer aux cellules inexplorées, refaisons le code pour transférer le coût du déplacement de HexGridà HexUnit. Cela simplifiera la prise en charge des unités avec différentes règles de mouvement.

Ajoutez à la HexUnitméthode générale GetMoveCostpour déterminer le coût du déménagement. Il a besoin de savoir quelles cellules se déplacent entre elles, ainsi que la direction. Nous copions le code correspondant pour les coûts de passage de HexGrid.Searchcette méthode et modifions les noms des variables.

  public int GetMoveCost ( HexCell fromCell, HexCell toCell, HexDirection direction) { HexEdgeType edgeType = fromCell.GetEdgeType(toCell); if (edgeType == HexEdgeType.Cliff) { continue; } int moveCost; if (fromCell.HasRoadThroughEdge(direction)) { moveCost = 1; } else if (fromCell.Walled != toCell.Walled) { continue; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += toCell.UrbanLevel + toCell.FarmLevel + toCell.PlantLevel; } } 

La méthode doit renvoyer le coût du déménagement. J'ai utilisé l'ancien code pour ignorer les mouvements invalides continue, mais cette approche ne fonctionnera pas ici. Si le mouvement n'est pas possible, nous rembourserons les coûts négatifs du déménagement.

  public int GetMoveCost ( HexCell fromCell, HexCell toCell, HexDirection direction) { HexEdgeType edgeType = fromCell.GetEdgeType(toCell); if (edgeType == HexEdgeType.Cliff) { return -1; } int moveCost; if (fromCell.HasRoadThroughEdge(direction)) { moveCost = 1; } else if (fromCell.Walled != toCell.Walled) { return -1; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += toCell.UrbanLevel + toCell.FarmLevel + toCell.PlantLevel; } return moveCost; } 

Maintenant, nous devons savoir quand trouver le chemin, non seulement la vitesse, mais aussi l'unité sélectionnée. Changez en conséquence HexGameUI.DoPathFinding.

  void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, selectedUnit); } else { grid.ClearPath(); } } } 

Étant donné que nous avons encore besoin d'accéder à la vitesse de l'équipe, nous allons ajouter à la HexUnitpropriété Speed. Alors qu'il renverra une valeur constante de 24.

  public int Speed { get { return 24; } } 

En HexGridmutation, FindPathet Searchpour qu'ils puissent travailler avec notre nouvelle approche.

  public void FindPath (HexCell fromCell, HexCell toCell, HexUnit unit) { ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, unit); ShowPath(unit.Speed); } bool Search (HexCell fromCell, HexCell toCell, HexUnit unit) { int speed = unit.Speed; … } 

Maintenant, nous allons supprimer de l' Searchancien code qui déterminait s'il est possible de passer à la cellule suivante et quels sont les coûts de déplacement. Au lieu de cela, nous appellerons HexUnit.IsValidDestinationet HexUnit.GetMoveCost. Nous sauterons la cellule si le coût du déménagement est négatif.

  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } // if (neighbor.IsUnderwater || neighbor.Unit) { // continue; // } // HexEdgeType edgeType = current.GetEdgeType(neighbor); // if (edgeType == HexEdgeType.Cliff) { // continue; // } // int moveCost; // if (current.HasRoadThroughEdge(d)) { // moveCost = 1; // } // else if (current.Walled != neighbor.Walled) { // continue; // } // else { // moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; // moveCost += neighbor.UrbanLevel + neighbor.FarmLevel + // neighbor.PlantLevel; // } if (!unit.IsValidDestination(neighbor)) { continue; } int moveCost = unit.GetMoveCost(current, neighbor, d); if (moveCost < 0) { continue; } int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … } 

Contourner les zones inexplorées


Pour éviter les cellules inexplorées, il nous suffit de HexUnit.IsValidDestinationvérifier si la cellule est examinée.

  public bool IsValidDestination (HexCell cell) { return cell.IsExplored && !cell.IsUnderwater && !cell.Unit; } 


Plus d'unités ne pourront pas accéder à des cellules inexplorées.

Étant donné que les cellules inexplorées ne sont plus des points de terminaison valides, les escouades les éviteront lors du déplacement vers le point de terminaison. Autrement dit, les zones inexplorées agissent comme des barrières qui allongent le chemin ou même le rendent impossible. Nous devrons rapprocher les unités d'un terrain inconnu afin d'abord d'explorer la région.

Que faire si un chemin plus court apparaît pendant le déménagement?
. , . .

, , . , .

paquet d'unité

Partie 22: Visibilité améliorée


  • Modifiez en douceur la visibilité.
  • Utilisez la hauteur de la cellule pour déterminer la portée.
  • Masquez le bord de la carte.

En ajoutant la prise en charge de l'exploration cartographique, nous améliorerons les calculs et les transitions de l'oscilloscope.


Pour voir plus loin, grimpez plus haut.

Transitions de visibilité


La cellule est visible ou invisible, car elle est ou non dans le périmètre du détachement. Même s'il semble qu'il faut un certain temps à une unité pour se déplacer entre les cellules, sa portée passe instantanément de cellule en cellule. En conséquence, la visibilité des cellules environnantes change considérablement. Le mouvement de l'escouade semble fluide, mais les changements de visibilité sont soudains.

Idéalement, la visibilité devrait également changer en douceur. Une fois dans le champ de visibilité, les cellules doivent être éclairées progressivement et, en la laissant, s'assombrir progressivement. Ou peut-être préférez-vous des transitions instantanées? Ajoutons à la HexCellShaderDatapropriété qui change les transitions instantanées. Par défaut, les transitions seront fluides.

  public bool ImmediateMode { get; set; } 

Suivi des cellules de transition


Même lors de l'affichage de transitions fluides, les données de visibilité réelle restent binaires, c'est-à-dire que l'effet n'est que visuel. Cela signifie que les transitions de visibilité doivent être traitées HexCellShaderData. Nous lui donnerons une liste de cellules dans lesquelles la transition est effectuée. Assurez-vous qu'à chaque initialisation, il est vide.

 using System.Collections.Generic; using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; Color32[] cellTextureData; List<HexCell> transitioningCells = new List<HexCell>(); public bool ImmediateMode { get; set; } public void Initialize (int x, int z) { … transitioningCells.Clear(); enabled = true; } … } 

Pour le moment, nous définissons RefreshVisibilitydirectement les données des cellules . Ceci est toujours correct pour le mode de transition instantanée, mais lorsqu'il est désactivé, nous devons ajouter une cellule à la liste des cellules de transition.

  public void RefreshVisibility (HexCell cell) { int index = cell.Index; if (ImmediateMode) { cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; } else { transitioningCells.Add(cell); } enabled = true; } 

La visibilité ne semble plus fonctionner, car pour l'instant, nous ne faisons rien avec les cellules de la liste.

Boucle à travers les cellules en boucle


Au lieu de régler instantanément les valeurs correspondantes sur 255 ou 0, nous augmenterons / diminuerons ces valeurs progressivement. La fluidité de la transition dépend du taux de changement. Cela ne devrait pas être très rapide et pas très lent. Un bon compromis entre de belles transitions et la commodité du jeu est de changer en une seconde. Fixons une constante pour que cela soit plus facile à changer.

  const float transitionSpeed = 255f; 

Maintenant, LateUpdatenous pouvons définir le delta appliqué aux valeurs. Pour ce faire, multipliez le delta temporel par la vitesse. Il doit s'agir d'un entier car nous ne savons pas quelle taille il peut avoir. Une forte baisse de la fréquence d'images peut rendre le delta supérieur à 255.

De plus, nous devons mettre à jour lorsqu'il y a des cellules de transition. Par conséquent, le code doit être inclus tant qu'il y a quelque chose dans la liste.

  void LateUpdate () { int delta = (int)(Time.deltaTime * transitionSpeed); cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = transitioningCells.Count > 0; } 

Également théoriquement possible des fréquences d'images très élevées. En combinaison avec une faible vitesse de transition, cela peut nous donner un delta de 0. Pour que le changement ait lieu, nous forçons le delta minimum à 1.

  int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; } 

Après avoir reçu le delta, nous pouvons boucler autour de toutes les cellules de transition et mettre à jour leurs données. Supposons que nous ayons une méthode pour cela UpdateCellData, dont les paramètres sont la cellule et le delta correspondants.

  int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; } for (int i = 0; i < transitioningCells.Count; i++) { UpdateCellData(transitioningCells[i], delta); } 

À un moment donné, la transition cellulaire devrait se terminer. Supposons que la méthode renvoie des informations indiquant si la transition est toujours en cours. Lorsque cela cesse, nous pouvons supprimer la cellule de la liste. Après cela, nous devons décrémenter l'itérateur afin de ne pas sauter les cellules.

  for (int i = 0; i < transitioningCells.Count; i++) { if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells.RemoveAt(i--); } } 

L'ordre dans lequel les cellules de transition sont traitées n'est pas important. Par conséquent, nous n'avons pas à supprimer la cellule à l'index en cours, ce qui forcerait RemoveAttoutes les cellules à se déplacer après lui. Au lieu de cela, nous déplaçons la dernière cellule vers l'index en cours, puis supprimons la dernière.

  if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells[i--] = transitioningCells[transitioningCells.Count - 1]; transitioningCells.RemoveAt(transitioningCells.Count - 1); } 

Nous devons maintenant créer une méthode UpdateCellData. Pour faire son travail, il aura besoin d'un index et de données de cellule, alors commençons par les obtenir. Il doit également déterminer s'il convient de poursuivre la mise à jour de la cellule. Par défaut, nous supposerons que ce n'est pas nécessaire. Une fois les travaux terminés, il est nécessaire d'appliquer les données modifiées et de renvoyer l'état "la mise à jour se poursuit".

  bool UpdateCellData (HexCell cell, int delta) { int index = cell.Index; Color32 data = cellTextureData[index]; bool stillUpdating = false; cellTextureData[index] = data; return stillUpdating; } 

Mise à jour des données de cellule


A ce stade, nous avons une cellule en transition ou déjà terminée. Tout d'abord, vérifions l'état de la sonde cellulaire. Si la cellule est examinée, mais que sa valeur G n'est pas encore égale à 255, alors elle est en cours de transition, nous allons donc surveiller cela.

  bool stillUpdating = false; if (cell.IsExplored && data.g < 255) { stillUpdating = true; } cellTextureData[index] = data; 

Pour effectuer la transition, nous ajouterons un delta à la valeur G de la cellule. Les opérations arithmétiques ne fonctionnent pas avec les octets, elles sont d'abord converties en entier. Par conséquent, la somme aura le format entier, qui doit être converti en octet.

  if (cell.IsExplored && data.g < 255) { stillUpdating = true; int t = data.g + delta; data.g = (byte)t; } 

Mais avant la conversion, vous devez vous assurer que la valeur ne dépasse pas 255.

  int t = data.g + delta; data.g = t >= 255 ? (byte)255 : (byte)t; 

Ensuite, nous devons faire de même pour la visibilité, qui utilise la valeur de R.

  if (cell.IsExplored && data.g < 255) { … } if (cell.IsVisible && data.r < 255) { stillUpdating = true; int t = data.r + delta; data.r = t >= 255 ? (byte)255 : (byte)t; } 

Puisque la cellule peut redevenir invisible, nous devons vérifier s'il est nécessaire de diminuer la valeur de R. Cela se produit lorsque la cellule est invisible, mais R est supérieur à zéro.

  if (cell.IsVisible) { if (data.r < 255) { stillUpdating = true; int t = data.r + delta; data.r = t >= 255 ? (byte)255 : (byte)t; } } else if (data.r > 0) { stillUpdating = true; int t = data.r - delta; data.r = t < 0 ? (byte)0 : (byte)t; } 

Maintenant, il est UpdateCellDataprêt et les transitions de visibilité sont effectuées correctement.


Transitions de visibilité.

Protection contre les éléments de transition en double


Les transitions fonctionnent, mais des éléments en double peuvent apparaître dans la liste. Cela se produit si l'état de visibilité de la cellule change alors qu'elle est encore en transition. Par exemple, lorsque la cellule n'est visible pendant le mouvement de l'équipe que pendant une courte période.

En raison de l'apparition d'éléments dupliqués, la transition de cellule est mise à jour plusieurs fois par image, ce qui entraîne des transitions plus rapides et un travail supplémentaire. Nous pouvons éviter cela en vérifiant avant d'ajouter une cellule si elle est déjà dans la liste. Cependant, une liste de recherche à chaque appelRefreshVisibilitycoûteux, en particulier lorsque plusieurs transitions cellulaires sont effectuées. À la place, utilisons un autre canal qui n'a pas encore été utilisé pour indiquer si la cellule est en cours de transition, par exemple, la valeur B. Lorsque nous ajoutons une cellule à la liste, nous lui attribuons la valeur 255 et n'ajoutons que les cellules dont la valeur n'est pas égale à 255.

  public void RefreshVisibility (HexCell cell) { int index = cell.Index; if (ImmediateMode) { cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; } else if (cellTextureData[index].b != 255) { cellTextureData[index].b = 255; transitioningCells.Add(cell); } enabled = true; } 

Pour que cela fonctionne, nous devons réinitialiser la valeur de B après la fin de la transition cellulaire.

  bool UpdateCellData (HexCell cell, int delta) { … if (!stillUpdating) { data.b = 0; } cellTextureData[index] = data; return stillUpdating; } 


Transitions sans doublons.

Chargement instantané de la visibilité


Les changements de visibilité sont désormais toujours progressifs, même lors du chargement d'une carte. Ceci est illogique, car la carte décrit l'état dans lequel les cellules sont déjà visibles, donc la transition est inappropriée ici. De plus, effectuer des transitions pour les nombreuses cellules visibles d'une grande carte peut ralentir le jeu après le chargement. Par conséquent, avant de charger des cellules et des escouades, passons HexGrid.Loadau mode de transition instantanée.

  public void Load (BinaryReader reader, int header) { … cellShaderData.ImmediateMode = true; for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … } 

Nous redéfinissons donc le réglage initial du mode de transition instantanée, quel qu'il soit. Peut-être qu'il est déjà désactivé ou a fait une option de configuration, nous nous souviendrons donc du mode initial et y basculerons après la fin des travaux.

  public void Load (BinaryReader reader, int header) { … bool originalImmediateMode = cellShaderData.ImmediateMode; cellShaderData.ImmediateMode = true; … cellShaderData.ImmediateMode = originalImmediateMode; } 

paquet d'unité

Portée dépendante de la hauteur


Jusqu'à présent, nous avons utilisé une portée constante de trois pour toutes les unités, mais en réalité, c'est plus compliqué. Dans le cas général, nous ne pouvons pas voir l'objet pour deux raisons: soit un obstacle nous empêche de le voir, soit l'objet est trop petit ou trop éloigné. Dans notre jeu, nous n'implémentons que la limitation de portée.

Nous ne pouvons pas voir ce qui se trouve de l'autre côté de la Terre, car la planète nous obscurcit. Nous ne pouvons que voir à l'horizon. Puisque la planète peut être considérée approximativement comme une sphère, plus le point de vue est élevé, plus nous pouvons voir de surface, c'est-à-dire que l'horizon dépend de la hauteur.


L'horizon dépend de la hauteur du point de vue.

La visibilité limitée de nos unités imite l'effet d'horizon créé par la courbure de la Terre. La portée de leur examen dépend de la taille de la planète et de l'échelle de la carte. C'est du moins l'explication logique. Mais la principale raison de la réduction de la portée est le gameplay, c'est une limitation appelée le brouillard de guerre. Cependant, en comprenant la physique sous-jacente au champ de vision, nous pouvons conclure qu'un point de vue élevé devrait avoir une valeur stratégique, car il éloigne l'horizon et vous permet de regarder les obstacles inférieurs. Mais jusqu'à présent, nous ne l'avons pas mis en œuvre.

Hauteur pour examen


Pour tenir compte de la hauteur lors de la détermination de la portée, nous devons connaître la hauteur. Ce sera la hauteur ou le niveau d'eau habituel, selon qu'il s'agit de la cellule terrestre ou de l'eau. Ajoutons cela à la HexCellpropriété.

  public int ViewElevation { get { return elevation >= waterLevel ? elevation : waterLevel; } } 

Mais si la hauteur affecte la portée, alors avec un changement de la hauteur de vision de la cellule, la situation de visibilité peut également changer. Étant donné que la cellule a bloqué ou bloque désormais la portée de plusieurs unités, il n'est pas si facile de déterminer ce qui doit être changé. La cellule elle-même ne pourra pas résoudre ce problème, alors laissez-la signaler un changement de situation HexCellShaderData. Supposons que vous HexCellShaderDataayez une méthode pour cela ViewElevationChanged. Nous l'appellerons lors de la cession HexCell.Elevation, si nécessaire.

  public int Elevation { get { return elevation; } set { if (elevation == value) { return; } int originalViewElevation = ViewElevation; elevation = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } … } } 

Il en va de même WaterLevel.

  public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } int originalViewElevation = ViewElevation; waterLevel = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } ValidateRivers(); Refresh(); } } 

Réinitialiser la visibilité


Nous devons maintenant créer une méthode HexCellShaderData.ViewElevationChanged. Déterminer comment une situation de visibilité générale change est une tâche complexe, en particulier lorsque vous changez plusieurs cellules en même temps. Par conséquent, nous ne proposerons aucune astuce, mais prévoyons simplement de réinitialiser la visibilité de toutes les cellules. Ajoutez un champ booléen pour savoir si cela doit être fait. Dans la méthode, nous allons simplement la définir sur true et inclure le composant. Quel que soit le nombre de cellules qui ont changé simultanément, cela entraînera une seule réinitialisation.

  bool needsVisibilityReset; … public void ViewElevationChanged () { needsVisibilityReset = true; enabled = true; } 

Pour réinitialiser les valeurs de visibilité de toutes les cellules, vous devez y avoir accès, ce que vous n'avez HexCellShaderDatapas. Déléguons donc cette responsabilité HexGrid. Pour ce faire, vous devez ajouter à la HexCellShaderDatapropriété, ce qui vous permet de vous référer à la grille. Ensuite, nous pouvons l'utiliser LateUpdatepour demander une réinitialisation.

  public HexGrid Grid { get; set; } … void LateUpdate () { if (needsVisibilityReset) { needsVisibilityReset = false; Grid.ResetVisibility(); } … } 

Passons à HexGrid: définir le lien vers la grille HexGrid.Awakeaprès avoir créé les données du shader.

  void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); cellShaderData.Grid = this; CreateMap(cellCountX, cellCountZ); } 

HexGriddevrait également obtenir une méthode ResetVisibilitypour éliminer toutes les cellules. Faites-le contourner toutes les cellules de la boucle et déléguez-lui la réinitialisation.

  public void ResetVisibility () { for (int i = 0; i < cells.Length; i++) { cells[i].ResetVisibility(); } } 

Maintenant, nous devons ajouter à la HexCellméthode ResetVisibilty. Il mettra simplement à zéro la visibilité et déclenchera la mise à jour de la visibilité. Cela doit être fait lorsque la visibilité des cellules est supérieure à zéro.

  public void ResetVisibility () { if (visibility > 0) { visibility = 0; ShaderData.RefreshVisibility(this); } } 

Après avoir réinitialisé toutes les données de visibilité, HexGrid.ResetVisibilityil doit à nouveau appliquer la visibilité à toutes les escouades, pour lesquelles il doit connaître la portée de chaque escouade. Supposons qu'il puisse être obtenu à l'aide de la propriété VisionRange.

  public void ResetVisibility () { for (int i = 0; i < cells.Length; i++) { cells[i].ResetVisibility(); } for (int i = 0; i < units.Count; i++) { HexUnit unit = units[i]; IncreaseVisibility(unit.Location, unit.VisionRange); } } 

Pour que cela fonctionne, nous allons refactoriser le changement HexUnit.visionRangede nom HexUnit.VisionRangeet le transformer en propriété. Bien qu'il recevra une valeur constante de 3, mais à l'avenir, il changera.

  public int VisionRange { get { return 3; } } 

Pour cette raison, les données de visibilité seront réinitialisées et resteront correctes après avoir modifié la hauteur de visualisation des cellules. Mais il est probable que nous allons changer les règles pour déterminer la portée et exécuter la recompilation en mode Play. Pour que l'étendue change indépendamment, exécutons une réinitialisation HexGrid.OnEnablelorsque la recompilation est détectée.

  void OnEnable () { if (!HexMetrics.noiseSource) { … ResetVisibility(); } } 

Vous pouvez maintenant modifier le code de portée et voir les résultats, tout en restant en mode Lecture.

Élargir l'horizon


Le calcul de la portée est déterminé HexGrid.GetVisibleCells. Pour que la hauteur affecte la portée, nous pouvons simplement utiliser la hauteur de vision en fromCellredéfinissant temporairement la zone transmise. Nous pouvons donc facilement vérifier si cela fonctionne.

  List<HexCell> GetVisibleCells (HexCell fromCell, int range) { … range = fromCell.ViewElevation; fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); … } 


Utilisez la hauteur comme portée.

Obstacles à la visibilité


L'application d'une hauteur de visualisation en tant que portée ne fonctionne correctement que lorsque toutes les autres cellules sont à une hauteur nulle. Mais si toutes les cellules ont la même hauteur que le point de vue, alors le champ de vision doit être nul. De plus, les cellules avec des hauteurs élevées devraient bloquer la visibilité des cellules basses derrière elles. Jusqu'à présent, rien de tout cela n'a été mis en œuvre.


La portée n'interfère pas.

La façon la plus correcte de déterminer la portée serait de vérifier par l'émission de rayons, mais cela deviendrait rapidement coûteux et produirait encore des résultats étranges. Nous avons besoin d'une solution rapide qui crée des résultats suffisamment bons qui ne doivent pas nécessairement être parfaits. De plus, il est important que les règles de détermination de la portée soient simples, intuitives et prévisibles pour les joueurs.

Notre solution sera la suivante - lors de la détermination de la visibilité d'une cellule, nous ajouterons la hauteur de vision de la cellule voisine à la distance parcourue. En fait, cela réduit la portée lorsque nous regardons ces cellules, et si elles sont ignorées, cela ne nous permettra pas d'atteindre les cellules derrière elles.

  int distance = current.Distance + 1; if (distance + neighbor.ViewElevation > range) { continue; } 


Les cellules hautes bloquent la vue.

Ne devrions-nous pas voir de grandes cellules au loin?
, , , . , .

Ne regardez pas dans les coins


Il semble maintenant que les cellules hautes bloquent la vue à un niveau bas, mais parfois la portée pénètre à travers elles, bien qu'il semble que cela ne devrait pas être le cas. Cela se produit car l'algorithme de recherche trouve toujours un chemin vers ces cellules, contournant les cellules bloquantes. En conséquence, il semble que notre zone de visibilité puisse contourner les obstacles. Pour éviter cela, nous devons nous assurer que seuls les chemins les plus courts sont pris en compte lors de la détermination de la visibilité des cellules. Cela peut être fait en supprimant les chemins qui deviennent plus longs que nécessaire.

  HexCoordinates fromCoordinates = fromCell.coordinates; while (searchFrontier.Count > 0) { … for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + 1; if (distance + neighbor.ViewElevation > range || distance > fromCoordinates.DistanceTo(neighbor.coordinates) ) { continue; } … } } 


Nous utilisons uniquement les chemins les plus courts.

Nous avons donc corrigé la plupart des cas manifestement erronés. Pour les cellules voisines, cela fonctionne bien, car il n'y a que les chemins les plus courts vers elles. Les cellules plus éloignées ont plus d'options pour les trajets; par conséquent, sur de longues distances, une enveloppe de visibilité peut toujours se produire. Ce ne sera pas un problème si les zones de visibilité restent petites et que les différences de hauteur adjacentes ne sont pas trop importantes.

Et enfin, au lieu de remplacer le champ de vision transmis, nous y ajoutons la hauteur de la vue. Le propre champ de vision de l’escouade indique sa hauteur, son altitude de vol ou ses capacités de reconnaissance.

  range += fromCell.ViewElevation; 


Vue avec plein champ de vision à un point de vue bas.

C'est-à-dire que les dernières règles de visibilité s'appliquent à la vision lors du déplacement sur le chemin le plus court vers le champ de vision, en tenant compte de la différence de hauteur de cellule par rapport au point de vue. Lorsqu'une cellule est hors de portée, elle bloque tous les chemins la traversant. En conséquence, les points d'observation élevés, d'où rien n'empêche la vue, deviennent stratégiquement précieux.

Qu'en est-il de gêner la visibilité des objets?
, , . , , . .

paquet d'unité

Cellules qui ne peuvent pas être explorées


Le dernier problème de visibilité concerne les bords de la carte. Le relief se termine brusquement et sans transitions, car les cellules en bordure n'ont pas de voisins.


Bord marqué de la carte.

Idéalement, l'affichage visuel des zones et des bords inexplorés de la carte devrait être le même. Nous pouvons y parvenir en ajoutant des cas spéciaux lors de la triangulation des bords, lorsqu'ils n'ont pas de voisins, mais cela nécessitera une logique supplémentaire et nous devrons travailler avec des cellules manquantes. Par conséquent, une telle solution n'est pas triviale. Une autre approche consiste à forcer les cellules limites de la carte à être inexplorées, même si elles sont dans la portée de l'escouade. Cette approche est beaucoup plus simple, alors utilisons-la. Il vous permet également de marquer comme inexploré et d'autres cellules, ce qui facilite la création de bords inégaux de la carte. De plus, les cellules cachées sur les bords vous permettent de créer des routes et des rivières qui entrent et sortent de la carte de la rivière et de la route, car leurs points d'extrémité seront hors de portée.De plus, avec l'aide de cette solution, vous pouvez ajouter des unités entrant et sortant de la carte.

Nous marquons les cellules comme enquêtées


Pour indiquer qu'une cellule peut être examinée, ajoutez à la HexCellpropriété Explorable.

  public bool Explorable { get; set; } 

Désormais, une cellule peut être visible si elle fait l'objet d'une enquête. IsVisibleNous allons donc modifier la propriété pour en tenir compte.

  public bool IsVisible { get { return visibility > 0 && Explorable; } } 

La même chose s'applique à IsExplored. Cependant, pour cela, nous avons étudié la propriété standard. Nous devons le convertir en une propriété explicite afin de pouvoir changer la logique de son getter.

  public bool IsExplored { get { return explored && Explorable; } private set { explored = value; } } … bool explored; 

Masquer le bord de la carte


Vous pouvez masquer le bord d'une carte rectangulaire dans la méthode HexGrid.CreateCell. Les cellules qui ne sont pas sur le bord sont étudiées, tout le reste est inexploré.

  void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; cell.Explorable = x > 0 && z > 0 && x < cellCountX - 1 && z < cellCountZ - 1; … } 

Maintenant, les cartes sont assombries sur les bords, se cachant derrière elles d'immenses espaces inexplorés. Par conséquent, la taille de la zone étudiée des cartes diminue de deux dans chaque dimension.


Bord inexploré de la carte.

Est-il possible de rendre l'état de la recherche modifiable?
, , . .

Des cellules inexplorées nuisent à la visibilité


Enfin, si la cellule ne peut pas être examinée, elle devrait nuire à la visibilité. Modifiez HexGrid.GetVisibleCellspour en tenir compte.

  if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase || !neighbor.Explorable ) { continue; } 

paquet d'unité

Partie 23: générer des terres


  • Remplissez de nouvelles cartes avec des paysages générés.
  • Nous élevons des terres au-dessus de l'eau, nous en inondons.
  • Nous contrôlons la quantité de terrain créé, sa hauteur et ses irrégularités.
  • Nous ajoutons la prise en charge de diverses options de configuration pour créer des cartes variables.
  • Nous faisons en sorte que la même carte puisse être générée à nouveau.

Cette partie du didacticiel sera le début d'une série sur la génération de cartes procédurales.

Cette partie a été créée dans Unity 2017.1.0.


L'une des nombreuses cartes générées.

Génération de cartes


Bien que nous puissions créer n'importe quelle carte, cela prend beaucoup de temps. Il serait pratique que l'application puisse aider le concepteur en générant pour lui des cartes qu'il pourra ensuite modifier à son goût. Vous pouvez prendre une autre étape et vous débarrasser complètement de la création manuelle de la conception, transférant complètement la responsabilité de générer la carte finie à l'application. Pour cette raison, le jeu peut être joué à chaque fois avec une nouvelle carte et chaque session de jeu sera différente. Pour que tout cela soit possible, nous devons créer un algorithme de génération de carte.

Le type d'algorithme de génération dont vous avez besoin dépend du type de carte dont vous avez besoin. Il n'y a pas de bonne approche, il faut toujours chercher un compromis entre crédibilité et jouabilité.

Pour qu'une carte soit crédible, elle doit sembler tout à fait possible et réelle au joueur. Cela ne signifie pas que la carte doit ressembler à une partie de notre planète. Ce peut être une planète différente ou une réalité complètement différente. Mais s'il doit indiquer le relief de la Terre, il doit lui ressembler au moins partiellement.

La jouabilité est liée à la façon dont les cartes correspondent au gameplay. Parfois, cela entre en conflit avec la crédibilité. Par exemple, bien que les chaînes de montagnes puissent être belles, en même temps, elles limitent considérablement le mouvement et la vue des unités. Si cela n'est pas souhaitable, vous devez vous passer de montagnes, ce qui réduira la crédibilité et limiter l'expressivité du jeu. Ou nous pouvons sauver les montagnes, mais réduire leur impact sur le gameplay, ce qui peut également réduire la crédibilité.

De plus, la faisabilité doit être considérée. Par exemple, vous pouvez créer une planète semblable à la terre très réaliste en simulant des plaques tectoniques, l'érosion, les pluies, les éruptions volcaniques, les effets des météorites et de la lune, etc. Mais le développement d'un tel système demandera beaucoup de temps. De plus, la génération d'une telle planète peut prendre beaucoup de temps et les joueurs ne voudront pas attendre quelques minutes avant de commencer une nouvelle partie. Autrement dit, la simulation est un outil puissant, mais elle a un prix.

Les jeux utilisent souvent des compromis entre crédibilité, jouabilité et faisabilité. Parfois, ces compromis sont invisibles et semblent tout à fait normaux, et parfois ils semblent aléatoires, incohérents ou chaotiques, selon les décisions prises au cours du processus de développement. Cela s'applique non seulement à la génération de cartes, mais lors du développement d'un générateur de cartes procédural, vous devez y prêter une attention particulière. Vous pouvez passer beaucoup de temps à créer un algorithme qui génère de belles cartes qui s'avèrent inutiles pour le jeu que vous créez.

Dans cette série de didacticiels, nous allons créer un relief semblable à la terre. Il devrait être intéressant, avec une grande variabilité et l'absence de grandes zones homogènes. L'échelle du relief sera grande, les cartes couvriront un ou plusieurs continents, régions des océans ou même une planète entière. Nous devons contrôler la géographie, y compris les masses terrestres, le climat, le nombre de régions et les bosses du terrain. Dans cette partie, nous allons jeter les bases de la création de sushi.

Démarrer en mode édition


Nous allons nous concentrer sur la carte, pas sur le gameplay, il sera donc plus pratique de lancer l'application en mode édition. Grâce à cela, nous pouvons immédiatement voir les cartes. Par conséquent, nous allons changer en HexMapEditor.Awakedéfinissant le mode d'édition sur true et en activant le mot-clé shader de ce mode.

  void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); Shader.EnableKeyword("HEX_MAP_EDIT_MODE"); SetEditMode(true); } 

Générateur de cartes


Étant donné que beaucoup de code est nécessaire pour générer des cartes procédurales, nous ne les ajouterons pas directement à HexGrid. Au lieu de cela, nous allons créer un nouveau composant HexMapGeneratoret HexGridnous ne le saurons pas. Cela simplifiera la transition vers un autre algorithme si nous en avons besoin.

Le générateur a besoin d'un lien vers la grille, nous allons donc lui ajouter un champ général. De plus, nous ajoutons une méthode générale GenerateMapqui traitera du travail de l'algorithme. Nous allons lui donner les dimensions de la carte en tant que paramètres, puis la forcer à être utilisée pour créer une nouvelle carte vide.

 using System.Collections.Generic; using UnityEngine; public class HexMapGenerator : MonoBehaviour { public HexGrid grid; public void GenerateMap (int x, int z) { grid.CreateMap(x, z); } } 

Ajoutez un objet avec un composant à la scène HexMapGeneratoret connectez-le à la grille.


Objet générateur de carte.

Changer le menu d'une nouvelle carte


Nous allons le changer NewMapMenuafin qu'il puisse générer des cartes, pas seulement en créer des vides. Nous contrôlerons sa fonctionnalité via un champ booléen generateMaps, qui par défaut a une valeur true. Créons une méthode générale pour définir ce champ, comme nous l'avons fait pour changer d'options HexMapEditor. Ajoutez le commutateur approprié au menu et connectez-le à la méthode.

  bool generateMaps = true; public void ToggleMapGeneration (bool toggle) { generateMaps = toggle; } 


Menu d'une nouvelle carte avec un interrupteur.

Donnez au menu un lien vers le générateur de carte. Ensuite, nous le forcerons à appeler la méthode du GenerateMapgénérateur si nécessaire , et pas seulement à exécuter la CreateMapgrille.

  public HexMapGenerator mapGenerator; … void CreateMap (int x, int z) { if (generateMaps) { mapGenerator.GenerateMap(x, z); } else { hexGrid.CreateMap(x, z); } HexMapCamera.ValidatePosition(); Close(); } 


Connexion au générateur.

Accès cellulaire


Pour que le générateur fonctionne, il doit avoir accès aux cellules. Nous HexGridavons déjà des méthodes communes GetCellqui nécessitent ou vecteur de position, ou les coordonnées hexagonaux. Le générateur n'a pas besoin de fonctionner avec l'un ou l'autre, nous ajoutons donc deux méthodes pratiques HexGrid.GetCellqui fonctionneront avec les coordonnées du décalage ou de l'index de la cellule.

  public HexCell GetCell (int xOffset, int zOffset) { return cells[xOffset + zOffset * cellCountX]; } public HexCell GetCell (int cellIndex) { return cells[cellIndex]; } 

Maintenant, il HexMapGeneratorpeut recevoir directement des cellules. Par exemple, après avoir créé une nouvelle carte, il peut utiliser les coordonnées de l'herbe pour définir l'herbe comme relief de la colonne centrale des cellules.

  public void GenerateMap (int x, int z) { grid.CreateMap(x, z); for (int i = 0; i < z; i++) { grid.GetCell(x / 2, i).TerrainTypeIndex = 1; } } 


Colonne d'herbe sur une petite carte.

paquet d'unité

Faire des sushis


Lors de la génération d'une carte, nous commençons complètement sans terrain. On peut imaginer que le monde entier est inondé d'un immense océan. Une terre est créée lorsqu'une partie du plancher océanique est tellement élevée qu'elle s'élève au-dessus de l'eau. Nous devons décider combien de terres devraient être créées de cette façon, où elles apparaîtront et quelle forme elles auront.

Augmentez le soulagement


Commençons petit - élever un morceau de terre au-dessus de l'eau. Nous créons pour cela une méthode RaiseTerrainavec un paramètre pour contrôler la taille du tracé. Appelez cette méthode dans GenerateMap, en remplaçant le code de test précédent. Commençons par un petit terrain composé de sept cellules.

  public void GenerateMap (int x, int z) { grid.CreateMap(x, z); // for (int i = 0; i < z; i++) { // grid.GetCell(x / 2, i).TerrainTypeIndex = 1; // } RaiseTerrain(7); } void RaiseTerrain (int chunkSize) {} 

Jusqu'à présent, nous utilisons le relief de type «herbe» pour désigner la terre élevée, et le relief «sable» d'origine se réfère à l'océan. Faites-nous RaiseTerrainprendre une cellule au hasard et changez le type de son relief jusqu'à ce que nous obtenions la bonne quantité de terrain.

Pour obtenir une cellule aléatoire, nous ajoutons une méthode GetRandomCellqui détermine un indice de cellule aléatoire et obtient la cellule correspondante à partir de la grille.

  void RaiseTerrain (int chunkSize) { for (int i = 0; i < chunkSize; i++) { GetRandomCell().TerrainTypeIndex = 1; } } HexCell GetRandomCell () { return grid.GetCell(Random.Range(0, grid.cellCountX * grid.cellCountZ)); } 


Sept cellules de sushi aléatoires.

Comme à la fin, nous pourrions avoir besoin de beaucoup de cellules aléatoires ou parcourir plusieurs fois toutes les cellules, gardons une trace du nombre de cellules dans la cellule elle-même HexMapGenerator.

  int cellCount; public void GenerateMap (int x, int z) { cellCount = x * z; … } … HexCell GetRandomCell () { return grid.GetCell(Random.Range(0, cellCount)); } 

Création d'un site


Jusqu'à présent, nous transformons sept cellules aléatoires en terres, et elles peuvent être n'importe où. Très probablement, ils ne forment pas une seule zone terrestre. De plus, nous pouvons sélectionner plusieurs fois les mêmes cellules, donc nous obtenons moins de terrain. Pour résoudre les deux problèmes, sans restrictions, nous sélectionnerons uniquement la première cellule. Après cela, nous devons sélectionner uniquement les cellules qui sont à côté de celles sélectionnées précédemment. Ces restrictions sont similaires aux limitations de la recherche de chemin, nous utilisons donc la même approche ici.

Nous ajoutons HexMapGeneratornotre propre propriété et le compteur de la phase de la frontière de recherche, comme c'était le cas HexGrid.

  HexCellPriorityQueue searchFrontier; int searchFrontierPhase; 

Vérifiez que la file d'attente prioritaire existe avant d'en avoir besoin.

  public void GenerateMap (int x, int z) { cellCount = x * z; grid.CreateMap(x, z); if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } RaiseTerrain(7); } 

Après avoir créé une nouvelle carte, la limite de recherche pour toutes les cellules est nulle. Mais si nous allons rechercher des cellules dans le processus de génération de carte, nous augmenterons leur frontière de recherche dans ce processus. Si nous effectuons de nombreuses opérations de recherche, elles peuvent être en avance sur la phase de la limite de recherche enregistrée HexGrid. Cela peut interférer avec la recherche de chemins d'unité. Pour éviter cela, à la fin du processus de génération de carte, nous remettrons à zéro la phase de recherche de toutes les cellules.

  RaiseTerrain(7); for (int i = 0; i < cellCount; i++) { grid.GetCell(i).SearchPhase = 0; } 

Maintenant, je RaiseTerraindois chercher les cellules appropriées et ne pas les sélectionner au hasard. Ce processus est très similaire à la méthode de recherche dans HexGrid. Cependant, nous ne visiterons pas les cellules plus d'une fois, il nous suffira donc d'augmenter la phase de la bordure de recherche de 1 au lieu de 2. Ensuite, nous initialisons la bordure avec la première cellule, qui est sélectionnée au hasard. Comme d'habitude, en plus de définir sa phase de recherche, nous attribuons sa distance et son heuristique à zéro.

  void RaiseTerrain (int chunkSize) { // for (int i = 0; i < chunkSize; i++) { // GetRandomCell().TerrainTypeIndex = 1; // } searchFrontierPhase += 1; HexCell firstCell = GetRandomCell(); firstCell.SearchPhase = searchFrontierPhase; firstCell.Distance = 0; firstCell.SearchHeuristic = 0; searchFrontier.Enqueue(firstCell); } 

Après cela, la boucle de recherche nous sera pour la plupart familière. De plus, pour continuer la recherche jusqu'à ce que la bordure soit vide, nous devons nous arrêter lorsque le fragment atteint la taille souhaitée, nous allons donc le suivre. À chaque itération, nous allons extraire la cellule suivante de la file d'attente, définir le type de son relief, augmenter la taille, puis contourner les voisins de cette cellule. Tous les voisins sont simplement ajoutés à la frontière s'ils n'y ont pas encore été ajoutés. Nous n'avons pas besoin de faire de changements ou de comparaisons. Après avoir terminé, vous devez effacer la frontière.

  searchFrontier.Enqueue(firstCell); int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.TerrainTypeIndex = 1; size += 1; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor && neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = 0; neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } } } searchFrontier.Clear(); 


Une ligne de cellules.

Nous avons obtenu une seule parcelle de la bonne taille. Il ne sera plus petit que s'il n'y a pas un nombre suffisant de cellules. En raison de la façon dont la frontière est remplie, le tracé se compose toujours d'une ligne allant vers le nord-ouest. Il ne change de direction que lorsqu'il atteint le bord de la carte.

Nous connectons les cellules


Les zones terrestres ressemblent rarement à des lignes, et si elles le font, elles ne sont pas toujours orientées de la même manière. Pour changer la forme du site, nous devons changer les priorités des cellules. La première cellule aléatoire peut être utilisée comme centre du tracé. La distance à toutes les autres cellules sera alors relative à ce point. Nous accorderons donc une priorité plus élevée aux cellules plus proches du centre, de sorte que le site ne se développera pas en ligne, mais autour du centre.

  searchFrontier.Enqueue(firstCell); HexCoordinates center = firstCell.coordinates; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.TerrainTypeIndex = 1; size += 1; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor && neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = neighbor.coordinates.DistanceTo(center); neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } } } 


L'accumulation de cellules.

Et en fait, maintenant nos sept cellules sont joliment emballées dans une zone hexagonale compacte si la cellule centrale n'apparaît pas sur le bord de la carte. Essayons maintenant d'utiliser une taille de tracé de 30.

  RaiseTerrain(30); 


Masse de sushi en 30 cellules.

Nous avons de nouveau la même forme, bien qu'il n'y ait pas assez de cellules pour obtenir le bon hexagone. Étant donné que le rayon du tracé est plus grand, il est plus susceptible d'être proche du bord de la carte, ce qui l'obligera à prendre une forme différente.

Randomisation des sushis


Nous ne voulons pas que toutes les zones soient identiques, nous allons donc modifier légèrement les priorités des cellules. Chaque fois que nous ajoutons une cellule voisine à la bordure, si le nombre suivant est Random.valueinférieur à une certaine valeur de seuil, l'heuristique de cette cellule devient non pas 0, mais 1. Utilisons la valeur 0,5 comme seuil, c'est-à-dire qu'elle affectera très probablement la moitié des cellules.

  neighbor.Distance = neighbor.coordinates.DistanceTo(center); neighbor.SearchHeuristic = Random.value < 0.5f ? 1: 0; searchFrontier.Enqueue(neighbor); 


Zone déformée.

En augmentant l'heuristique de recherche de la cellule, nous l'avons rendue visite plus tard que prévu. En même temps, d'autres cellules situées un peu plus loin du centre seront visitées plus tôt, à moins qu'elles n'augmentent également l'heuristique. Cela signifie que si nous augmentons l'heuristique de toutes les cellules d'une valeur, cela n'affectera pas la carte. Autrement dit, le seuil 1 n'aura pas d'effet, comme le seuil 0. Et le seuil 0,8 sera équivalent à 0,2. Autrement dit, la probabilité de 0,5 rend le processus de recherche le plus "tremblant".

La quantité d'oscillation appropriée dépend du type de terrain souhaité, nous allons donc la personnaliser. Ajouter un champ flottant générique jitterProbabilityavec l'attribut au générateurRangelimité dans la plage de 0 à 0,5. Donnons-lui une valeur par défaut égale à la moyenne de cet intervalle, soit 0,25. Cela nous permettra de configurer le générateur dans la fenêtre de l'inspecteur Unity.

  [Range(0f, 0.5f)] public float jitterProbability = 0.25f; 


Probabilité de fluctuations.

Pouvez-vous le rendre personnalisable dans l'interface utilisateur du jeu?
, . UI, . , UI. , . , .

Maintenant, pour décider quand l'heuristique doit être égale à 1, nous utilisons la probabilité au lieu d'une valeur constante.

  neighbor.SearchHeuristic = Random.value < jitterProbability ? 1: 0; 

Nous utilisons des valeurs heuristiques de 0 et 1. Bien que des valeurs plus grandes puissent être utilisées, cela aggravera considérablement la déformation des sections, les transformant très probablement en un tas de bandes.

Élever des terres


Nous ne nous limiterons pas à la génération d'un seul terrain. Par exemple, nous plaçons un appel RaiseTerraindans une boucle pour obtenir cinq sections.

  for (int i = 0; i < 5; i++) { RaiseTerrain(30); } 


Cinq parcelles de terrain.

Bien que nous générions maintenant cinq parcelles de 30 cellules chacune, mais pas nécessairement exactement 150 cellules de terrain. Étant donné que chaque site est créé séparément, ils ne se connaissent pas, ils peuvent donc se croiser. C'est normal car cela peut créer des paysages plus intéressants qu'un simple ensemble de sections isolées.

Pour augmenter la variabilité des terres, nous pouvons également changer la taille de chaque parcelle. Ajoutez deux champs entiers pour contrôler les tailles minimale et maximale des tracés. Attribuez-leur un intervalle suffisamment grand, par exemple 20-200. Je ferai le minimum standard égal à 30, et le maximum standard - 100.

  [Range(20, 200)] public int chunkSizeMin = 30; [Range(20, 200)] public int chunkSizeMax = 100; 


Intervalle de dimensionnement.

Nous utilisons ces champs pour déterminer aléatoirement la taille de la zone lors de l'appel RaiseTerrain.

  RaiseTerrain(Random.Range(chunkSizeMin, chunkSizeMax + 1)); 


Cinq sections de taille aléatoire sur la carte du milieu.

Créez suffisamment de sushis


Certes, on ne peut pas particulièrement contrôler la quantité de terrain générée. Bien que nous puissions ajouter l'option de configuration pour le nombre de parcelles, les parcelles elles-mêmes sont de taille aléatoire et peuvent se chevaucher légèrement ou fortement. Par conséquent, le nombre de sites ne garantit pas la réception sur la carte de la quantité de terrain requise. Ajoutons une option pour contrôler directement le pourcentage de terrain exprimé en nombre entier. Étant donné que 100% de la terre ou de l'eau n'est pas très intéressant, nous le limitons à l'intervalle 5–95, avec une valeur de 50 par défaut.

  [Range(5, 95)] public int landPercentage = 50; 


Pourcentage de sushi.

Pour garantir la création de la bonne quantité de terrain, il nous suffit de continuer à élever des zones du terrain jusqu'à ce que nous obtenions une quantité suffisante. Pour ce faire, nous devons contrôler le processus, ce qui compliquera la génération de terres. Par conséquent, remplaçons le cycle existant de création de sites en appelant une nouvelle méthode CreateLand. La première chose que fait cette méthode est de calculer le nombre de cellules qui devraient devenir des terres. Ce montant sera notre somme totale de cellules de sushi.

  public void GenerateMap (int x, int z) { … // for (int i = 0; i < 5; i++) { // RaiseTerrain(Random.Range(chunkSizeMin, chunkSizeMax + 1)); // } CreateLand(); for (int i = 0; i < cellCount; i++) { grid.GetCell(i).SearchPhase = 0; } } void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); } 

CreateLandprovoquera RaiseTerrainjusqu'à ce que nous ayons dépensé la totalité des cellules. Afin de ne pas dépasser le montant, nous modifions RaiseTerrainpour qu'il reçoive le montant comme paramètre supplémentaire. Après avoir terminé ses travaux, il doit restituer le montant restant.

 // void RaiseTerrain (int chunkSize) { int RaiseTerrain (int chunkSize, int budget) { … return budget; } 

Le montant devrait diminuer chaque fois que la cellule est retirée de la frontière et convertie en terre. Si après cela, le montant total est dépensé, alors nous devons arrêter la recherche et terminer le site. De plus, cela ne devrait être fait que lorsque la cellule actuelle n'est pas encore atterrie.

  while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); if (current.TerrainTypeIndex == 0) { current.TerrainTypeIndex = 1; if (--budget == 0) { break; } } size += 1; … } 

Maintenant, il CreateLandpeut élever des terres jusqu'à ce qu'il dépense la totalité des cellules.

  void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); while (landBudget > 0) { landBudget = RaiseTerrain( Random.Range(chunkSizeMin, chunkSizeMax + 1), landBudget ); } } 


Exactement la moitié de la carte est devenue un terrain.

paquet d'unité

Tenez compte de la hauteur


La terre n'est pas seulement une plaque plate, limitée par le littoral. Elle a une hauteur changeante, contenant des collines, des montagnes, des vallées, des lacs, etc. De grandes différences de hauteur existent en raison de l'interaction des plaques tectoniques se déplaçant lentement. Bien que nous ne le simulions pas, nos terres devraient en quelque sorte ressembler à de telles plaques. Les sites ne bougent pas, mais peuvent se croiser. Et nous pouvons en profiter.

Poussez la terre


Chaque parcelle représente une portion de terre expulsée du fond de l'océan. Par conséquent, augmentons constamment la hauteur de la cellule actuelle RaiseTerrainet voyons ce qui se passe.

  HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.TerrainTypeIndex == 0) { … } 


Terrain avec des hauteurs.

Nous avons atteint les hauteurs, mais c'est difficile à voir. Vous pouvez les rendre plus lisibles si vous utilisez votre propre type de terrain pour chaque niveau de hauteur, comme la superposition géographique. Nous ne le ferons que pour que les hauteurs soient plus visibles, vous pouvez donc simplement utiliser le niveau de hauteur comme indice d'élévation.

Que se passe-t-il si la hauteur dépasse le nombre de types de terrain?
. , .

Au lieu de mettre à jour le type de terrain de la cellule à chaque changement de hauteur, créons une méthode distincte SetTerrainTypepour définir tous les types de terrain une seule fois.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); cell.TerrainTypeIndex = cell.Elevation; } } 

Nous appellerons cette méthode après avoir créé des sushis.

  public void GenerateMap (int x, int z) { … CreateLand(); SetTerrainType(); … } 

Maintenant, il RaiseTerrainne peut plus gérer le type de relief et se concentrer sur les hauteurs. Pour ce faire, vous devez modifier sa logique. Si la nouvelle hauteur de la cellule actuelle est 1, alors elle vient de devenir plus sèche, donc la somme des cellules a diminué, ce qui peut conduire à l'achèvement de la croissance du site.

  HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == 1 && --budget == 0) { break; } // if (current.TerrainTypeIndex == 0) { // current.TerrainTypeIndex = 1; // if (--budget == 0) { // break; // } // } 


Stratification des couches.

Ajouter de l'eau


Indiquons explicitement quelles cellules sont de l'eau ou de la terre, en réglant le niveau d'eau pour toutes les cellules sur 1. Faites-le GenerateMapavant de créer la terre.

  public void GenerateMap (int x, int z) { cellCount = x * z; grid.CreateMap(x, z); if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } for (int i = 0; i < cellCount; i++) { grid.GetCell(i).WaterLevel = 1; } CreateLand(); … } 

Maintenant, pour la désignation des couches terrestres, nous pouvons utiliser tous les types de terrain. Toutes les cellules sous-marines resteront sablonneuses, tout comme les cellules terrestres les plus basses. Cela peut être fait en soustrayant le niveau d'eau de la hauteur et en utilisant la valeur comme indice du type de relief.

  void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); if (!cell.IsUnderwater) { cell.TerrainTypeIndex = cell.Elevation - cell.WaterLevel; } } } 


Terre et eau.

Augmentez le niveau d'eau


Nous ne sommes pas limités à un seul niveau d'eau. Rendons-le personnalisable à l'aide d'un champ commun avec un intervalle de 1 à 5 et une valeur par défaut de 3. Utilisez ce niveau lors de l'initialisation des cellules.

  [Range(1, 5)] public int waterLevel = 3; … public void GenerateMap (int x, int z) { … for (int i = 0; i < cellCount; i++) { grid.GetCell(i).WaterLevel = waterLevel; } … } 



Niveau d'eau 3.

Lorsque le niveau d'eau est à 3, nous obtenons moins de terres que prévu. C'est parce qu'il RaiseTerraincroit toujours que le niveau d'eau est 1. Corrigeons-le.

  HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == waterLevel && --budget == 0) { break; } 

L'utilisation de niveaux d'eau plus élevés conduit à cela. que les cellules ne deviennent pas des terres immédiatement. Lorsque le niveau d'eau est à 2, la première section restera toujours sous l'eau. Le fond de l'océan a augmenté, mais reste toujours sous l'eau. Un terrain n'est formé qu'à l'intersection d'au moins deux sections. Plus le niveau d'eau est élevé, plus les sites doivent traverser pour créer des terres. Par conséquent, avec la montée des eaux, la terre devient plus chaotique. En outre, lorsque davantage de parcelles sont nécessaires, il est plus probable qu'elles se recoupent sur des terres déjà existantes, en raison des montagnes qui seront plus communes et des terres plates moins probables, comme dans le cas de l'utilisation de parcelles plus petites.





Les niveaux d'eau sont de 2 à 5, les sushis sont toujours à 50%.

paquet d'unité

Mouvement vertical


Jusqu'à présent, nous avons élevé les parcelles d'un niveau à la fois, mais nous n'avons pas à nous limiter à cela.

Sites élevés


Bien que chaque section augmente la hauteur de ses cellules d'un niveau, des coupures peuvent se produire. Cela se produit lorsque les bords de deux sections se touchent. Cela peut créer des falaises isolées, mais les longues lignes de falaise seront rares. Nous pouvons augmenter la fréquence de leur apparition en augmentant la hauteur de l'intrigue de plus d'un pas. Mais cela ne doit être fait que pour une certaine proportion de sites. Si toutes les zones montent haut, il sera très difficile de se déplacer le long du terrain. Rendons donc ce paramètre personnalisable à l'aide d'un champ de probabilité avec une valeur par défaut de 0,25.

  [Range(0f, 1f)] public float highRiseProbability = 0.25f; 


La probabilité d'une forte augmentation des cellules.

Bien que nous puissions utiliser n'importe quelle augmentation de hauteur pour les zones élevées, cela devient rapidement incontrôlable. La différence de hauteur 2 crée déjà des falaises, donc cela suffit. Comme vous pouvez sauter une hauteur égale au niveau de l'eau, nous devons changer la façon dont nous déterminons si une cellule est devenue une terre. Si elle était en dessous du niveau de l'eau, et maintenant elle est au même niveau ou plus, alors nous avons créé une nouvelle cellule terrestre.

  int rise = Random.value < highRiseProbability ? 2 : 1; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; current.Elevation = originalElevation + rise; if ( originalElevation < waterLevel && current.Elevation >= waterLevel && --budget == 0 ) { break; } size += 1; … } 





Les probabilités d'une forte augmentation de la hauteur sont de 0,25, 0,50, 0,75 et 1.

Abaisser le terrain


La terre ne monte pas toujours, parfois elle tombe. Lorsque la terre tombe suffisamment bas, l'eau la remplit et elle est perdue. Jusqu'à présent, nous ne faisons pas cela. Comme nous ne faisons que pousser les zones vers le haut, le terrain ressemble généralement à un ensemble de zones plutôt rondes mélangées. Si nous abaissons parfois la zone, nous obtenons des formes plus variées.


Grande carte sans sushi coulé.

Nous pouvons contrôler la fréquence de l'affaissement des terres en utilisant un autre champ de probabilité. Étant donné que l'abaissement peut détruire des terres, la probabilité d'abaissement doit toujours être inférieure à la probabilité d'augmenter. Sinon, cela peut prendre beaucoup de temps pour obtenir le bon pourcentage de terres. Par conséquent, utilisons une probabilité d'abaissement maximale de 0,4 avec une valeur par défaut de 0,2.

  [Range(0f, 0.4f)] public float sinkProbability = 0.2f; 


Probabilité d'abaissement.

L'abaissement du site est similaire à l'augmentation, avec quelques différences. Par conséquent, nous dupliquons la méthode RaiseTerrainet changeons son nom en SinkTerrain. Au lieu de déterminer l'ampleur de l'augmentation, nous avons besoin d'une valeur d'abaissement qui peut utiliser la même logique. Dans le même temps, des comparaisons pour vérifier si nous avons traversé la surface de l'eau doivent être retournées. De plus, lors de l'abaissement du relief, nous ne sommes pas limités à la somme des cellules. Au lieu de cela, chaque cellule de sushi perdue renvoie le montant dépensé, nous l'augmentons donc et continuons à travailler.

  int SinkTerrain (int chunkSize, int budget) { … int sink = Random.value < highRiseProbability ? 2 : 1; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; current.Elevation = originalElevation - sink; if ( originalElevation >= waterLevel && current.Elevation < waterLevel // && --budget == 0 ) { // break; budget += 1; } size += 1; … } searchFrontier.Clear(); return budget; } 

Maintenant, à chaque itération à l'intérieur, CreateLandnous devons soit baisser, soit élever le terrain, selon la probabilité d'abaissement.

  void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); while (landBudget > 0) { int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); if (Random.value < sinkProbability) { landBudget = SinkTerrain(chunkSize, landBudget); } else { landBudget = RaiseTerrain(chunkSize, landBudget); } } } 





La probabilité de chute est de 0,1, 0,2, 0,3 et 0,4.

Hauteur limite


Au stade actuel, nous pouvons potentiellement chevaucher de nombreuses sections, parfois avec plusieurs augmentations de hauteur, dont certaines peuvent descendre puis remonter. Dans le même temps, nous pouvons créer des hauteurs très élevées, et parfois très basses, en particulier lorsqu'un pourcentage élevé de terrain est nécessaire.


Énormes hauteurs à 90% du terrain.

Pour limiter la hauteur, ajoutons un minimum et un maximum personnalisés. Un minimum raisonnable se situera entre −4 et 0, et un maximum acceptable peut être compris entre 6 et 10. Laissez les valeurs par défaut être -2 et 8. Lorsque vous modifiez manuellement la carte, elles seront en dehors de la limite acceptable, vous pouvez donc changer le curseur de l'interface utilisateur de l'éditeur ou le laisser tel quel.

  [Range(-4, 0)] public int elevationMinimum = -2; [Range(6, 10)] public int elevationMaximum = 8; 


Hauteurs minimum et maximum.

Maintenant, RaiseTerrainnous devons nous assurer que la hauteur ne dépasse pas le maximum autorisé. Cela peut être fait en vérifiant si les cellules actuelles sont trop élevées. Si c'est le cas, nous les ignorons sans modifier la hauteur et ajouter leurs voisins. Cela conduira au fait que les zones terrestres éviteront les zones qui ont atteint une hauteur maximale et se développeront autour d'elles.

  HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; int newElevation = originalElevation + rise; if (newElevation > elevationMaximum) { continue; } current.Elevation = newElevation; if ( originalElevation < waterLevel && newElevation >= waterLevel && --budget == 0 ) { break; } size += 1; 

Faites de même à l'intérieur SinkTerrain, mais pour une hauteur minimale.

  HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; int newElevation = current.Elevation - sink; if (newElevation < elevationMinimum) { continue; } current.Elevation = newElevation; if ( originalElevation >= waterLevel && newElevation < waterLevel ) { budget += 1; } size += 1; 


Hauteur limitée avec 90% de terrain.

Préservation de l'altitude négative


À ce stade, le code de sauvegarde et de chargement ne peut pas gérer les hauteurs négatives car nous stockons la hauteur en octets. Un nombre négatif est converti lorsqu'il est enregistré en un grand positif. Par conséquent, lors de l'enregistrement et du chargement de la carte générée, des cartes très hautes peuvent apparaître à la place des cellules sous-marines d'origine.

Nous pouvons ajouter la prise en charge des hauteurs négatives en la stockant sous forme d'entier et non d'octet. Cependant, nous n'avons toujours pas besoin de prendre en charge plusieurs niveaux de hauteur. De plus, nous pouvons compenser la valeur stockée en ajoutant 127. Cela nous permettra de stocker correctement les hauteurs dans la plage −127–128 dans un octet. Changez en HexCell.Saveconséquence.

  public void Save (BinaryWriter writer) { writer.Write((byte)terrainTypeIndex); writer.Write((byte)(elevation + 127)); … } 

Puisque nous avons changé la façon dont nous enregistrons les données cartographiques, nous les augmentons SaveLoadMenu.mapFileVersionà 4.

  const int mapFileVersion = 4; 

Et enfin, changez-le HexCell.Loadpour qu'il soustrait 127 des hauteurs chargées à partir des fichiers de la version 4.

  public void Load (BinaryReader reader, int header) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); if (header >= 4) { elevation -= 127; } … } 

paquet d'unité

Recréer la même carte


Nous pouvons maintenant créer une grande variété de cartes. Lors de la génération de chaque nouveau résultat sera aléatoire. Nous pouvons contrôler à l'aide des options de configuration uniquement les caractéristiques de la carte, mais pas la forme la plus précise. Mais parfois, nous devons recréer exactement la même carte. Par exemple, pour partager une belle carte avec un ami ou recommencer après l'avoir modifiée manuellement. Il est également utile dans le processus de développement du jeu, alors ajoutons cette fonctionnalité.

Utilisation de semences


Pour rendre le processus de génération de carte imprévisible, nous utilisons Random.Rangeet Random.value. Pour obtenir à nouveau la même séquence pseudo-aléatoire de nombres, vous devez utiliser la même valeur de départ. Nous avons déjà adopté une approche similaire auparavant, en HexMetrics.InitializeHashGrid. Il enregistre d'abord l'état actuel du générateur de nombres initialisé avec une valeur de départ spécifique, puis restaure son état d'origine. Nous pouvons utiliser la même approche pour HexMapGenerator.GenerateMap. Nous pouvons à nouveau nous souvenir de l'ancien état et le restaurer après l'achèvement, afin de ne pas interférer avec quoi que ce soit d'autre qui utilise Random.

  public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; … Random.state = originalRandomState; } 

Ensuite, nous devons mettre à disposition la graine utilisée pour générer la dernière carte. Cela se fait à l'aide d'un champ entier commun.

  public int seed; 


Afficher la graine.

Maintenant, nous avons besoin de la valeur de départ pour initialiser Random. Pour créer des cartes aléatoires, vous devez utiliser une graine aléatoire. L'approche la plus simple consiste à utiliser une valeur de départ arbitraire pour générer Random.Range. Pour que cela n'affecte pas l'état aléatoire initial, nous devons le faire après l'avoir enregistré.

  public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; seed = Random.Range(0, int.MaxValue); Random.InitState(seed); … } 

Depuis la fin, nous restaurons un état aléatoire, si nous générons immédiatement une autre carte, nous obtenons ainsi la même valeur de départ. De plus, nous ne savons pas comment l'état aléatoire initial a été initialisé. Par conséquent, bien qu'il puisse servir de point de départ arbitraire, nous avons besoin de quelque chose de plus pour le randomiser à chaque appel.

Il existe différentes façons d'initialiser des générateurs de nombres aléatoires. Dans ce cas, vous pouvez simplement combiner plusieurs valeurs arbitraires qui varient sur une large plage, c'est-à-dire que la probabilité de recréer la même carte sera faible. Par exemple, nous utilisons les 32 bits inférieurs de l'heure système, exprimés en cycles, plus le temps d'exécution actuel de l'application. Combinez ces valeurs à l'aide de l'opération OU exclusive au niveau du bit afin que le résultat ne soit pas très grand.

  seed = Random.Range(0, int.MaxValue); seed ^= (int)System.DateTime.Now.Ticks; seed ^= (int)Time.unscaledTime; Random.InitState(seed); 

Le nombre résultant peut être négatif, ce qui pour une graine de valeur publique n'est pas très joli. Nous pouvons le rendre strictement positif en utilisant un masquage au niveau du bit avec une valeur entière maximale qui réinitialisera le bit de signe.

  seed ^= (int)Time.unscaledTime; seed &= int.MaxValue; Random.InitState(seed); 

Graine réutilisable


Nous générons toujours des cartes aléatoires, mais maintenant nous pouvons voir quelle valeur de graine a été utilisée pour chacune d'elles. Pour recréer à nouveau la même carte, nous devons ordonner au générateur d'utiliser à nouveau la même valeur de départ, plutôt que d'en créer une nouvelle. Nous allons le faire en ajoutant un commutateur à l'aide d'un champ booléen.

  public bool useFixedSeed; 


Possibilité d'utiliser une graine constante.

Si une graine constante est sélectionnée, nous sautons simplement la génération de la nouvelle graine GenerateMap. Si nous ne modifions pas manuellement le champ de départ, le résultat sera à nouveau la même carte.

  Random.State originalRandomState = Random.state; if (!useFixedSeed) { seed = Random.Range(0, int.MaxValue); seed ^= (int)System.DateTime.Now.Ticks; seed ^= (int)Time.time; seed &= int.MaxValue; } Random.InitState(seed); 

Maintenant, nous pouvons copier la valeur de départ de la carte que nous aimons et la sauvegarder quelque part, afin de la générer à nouveau à l'avenir. N'oubliez pas que nous n'obtiendrons la même carte que si nous utilisons exactement les mêmes paramètres de générateur, c'est-à-dire la même taille de carte, ainsi que toutes les autres options de configuration. Même un petit changement dans ces probabilités peut créer une carte complètement différente. Par conséquent, en plus de la graine, nous devons nous souvenir de tous les paramètres.



Grandes cartes avec des valeurs de départ 0 et 929396788, paramètres standard.

paquet d'unité

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


All Articles