Mapas de hexágono en Unity: partes 1-3

imagen

De un traductor: este artículo es el primero de una serie detallada (27 partes) de tutoriales sobre la creación de mapas a partir de hexágonos. Esto es lo que debería suceder al final de los tutoriales.

Partes 1-3: malla, colores y alturas de celda

Partes 4-7: baches, ríos y caminos

Partes 8-11: agua, accidentes geográficos y murallas

Partes 12-15: guardar y cargar, texturas, distancias

Partes 16-19: encontrar el camino, escuadrones de jugadores, animaciones

Partes 20-23: niebla de guerra, investigación de mapas, generación de procedimientos

Partes 24-27: ciclo del agua, erosión, biomas, mapa cilíndrico

Parte 1: mallado de hexágonos


Tabla de contenidos


  • Convierte cuadrados en hexágonos.
  • Triangular una cuadrícula de hexágonos.
  • Trabajamos con coordenadas cúbicas.
  • Interactuamos con celdas de cuadrícula.
  • Crea un editor en el juego.

Este tutorial es el comienzo de una serie sobre tarjetas hexagonales. Las redes hexagonales se usan en muchos juegos, especialmente en estrategias, como Age of Wonders 3, Civilization 5 y Endless Legend. Comenzaremos con lo básico, agregaremos gradualmente nuevas características y, como resultado, crearemos un relieve complejo basado en hexágonos.

Este tutorial asume que ya has estudiado la serie Mesh Basics , que comienza con la cuadrícula de procedimientos . Fue creado en Unity 5.3.1. La serie usa varias versiones de Unity. La última parte se realiza en Unity 2017.3.0p3.


Un mapa simple de hexágonos.

Sobre hexágonos


¿Por qué se necesitan hexágonos? Si necesitamos una cuadrícula, entonces es lógico usar cuadrados. Los cuadrados son realmente fáciles de dibujar y colocar, pero también tienen un inconveniente. Mire un solo cuadrado de la cuadrícula y luego a sus vecinos.


La plaza y sus vecinos.

En total, la plaza tiene ocho vecinos. Cuatro de ellos se pueden lograr cruzando el borde del cuadrado. Estos son vecinos horizontales y verticales. Los otros cuatro se pueden lograr cruzando la esquina de la plaza. Estos son vecinos diagonales.

¿Cuál es la distancia entre los centros de las celdas cuadradas adyacentes? Si la longitud del borde es 1, entonces para vecinos horizontales y verticales la respuesta es 1. Pero para vecinos diagonales la respuesta es √2.

La diferencia entre los dos tipos de vecinos conduce a dificultades. Si usamos movimiento discreto, ¿cómo percibir el movimiento a lo largo de la diagonal? ¿Debería permitirlo? ¿Cómo hacer que la apariencia sea más orgánica? Los diferentes juegos utilizan diferentes enfoques con sus ventajas y desventajas. Un enfoque no es utilizar una cuadrícula cuadrada, sino utilizar hexágonos en su lugar.


Hexagon y sus vecinos.

A diferencia de un cuadrado, un hexágono no tiene ocho, sino seis vecinos. Todos estos vecinos son adyacentes a los bordes, no hay vecinos de esquina. Es decir, solo hay un tipo de vecinos, lo que simplifica mucho. Por supuesto, una cuadrícula de hexágonos es más difícil de construir que un cuadrado, pero podemos manejarlo.

Antes de comenzar, necesitamos determinar el tamaño de los hexágonos. Deje que la longitud del borde sea igual a 10 unidades. Como el hexágono consiste en un círculo de seis triángulos equiláteros, la distancia desde el centro a cualquier ángulo también es 10. Este valor determina el radio exterior de la celda hexagonal.


El radio exterior e interior del hexágono.

También hay un radio interno, que es la distancia desde el centro a cada uno de los bordes. Este parámetro es importante porque la distancia entre los centros de los vecinos es igual a este valor multiplicado por dos. El radio interno es  f r a c s q r t 3 2  desde el radio exterior, es decir, en nuestro caso 5 s q r t 3  . Pongamos estos parámetros en una clase estática por conveniencia.

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

¿Cómo derivar el valor del radio interno?
Toma uno de los seis triángulos de un hexágono. El radio interno es igual a la altura de este triángulo. Esta altura se puede obtener dividiendo el triángulo en dos triángulos regulares y luego usando el teorema de Pitágoras.

Por lo tanto, para la longitud de la costilla e radio interior es  sqrte2(e/2)2= sqrt3e2/4=e sqrt3/2 aprox.0.886e .

Si ya estamos haciendo esto, entonces determinemos las posiciones de las seis esquinas en relación con el centro de la celda. Cabe señalar que hay dos formas de orientar el hexágono: hacia arriba con un lado afilado o plano. Pondremos la esquina. Comencemos desde este ángulo y agreguemos el resto en el sentido de las agujas del reloj. Colóquelos en el plano XZ para que los hexágonos estén en el suelo.


Posibles orientaciones.

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

paquete de la unidad

Malla


Para construir una cuadrícula de hexágonos, necesitamos celdas de cuadrícula. Para este propósito, cree el componente HexCell . Por ahora, déjelo en blanco porque todavía no estamos usando ninguna celda dada.

 using UnityEngine; public class HexCell : MonoBehaviour { } 

Para comenzar con el más simple, cree un objeto plano predeterminado, agregue un componente de celda y conviértalo en un prefabricado.


Usando un plano como prefabricado de una celda hexagonal.

Ahora entremos a la red. Creemos un componente simple con variables comunes de ancho de celda, altura y prefabricado. Luego agrega un objeto de juego con este componente a la escena.

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


Objeto de malla hexagonal.

Comencemos creando una cuadrícula regular de cuadrados, porque ya sabemos cómo hacerlo. Guardemos las celdas en una matriz para poder acceder a ellas.

Dado que los planos tienen por defecto un tamaño de 10 por 10 unidades, desplazaremos cada celda por esta cantidad.

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


Cuadrícula cuadrada de planos.

Entonces obtuvimos una hermosa cuadrícula sólida de celdas cuadradas. ¿Pero qué celda es dónde? Por supuesto, esto es fácil de verificar, pero existen dificultades con los hexágonos. Sería conveniente si pudiéramos ver simultáneamente las coordenadas de todas las celdas.

Pantalla coordinada


Agregue lienzo a la escena seleccionando GameObject / UI / Canvas y conviértalo en un elemento secundario de nuestro objeto de malla. Dado que este lienzo es solo para información, eliminaremos su componente raycaster. También puede eliminar el objeto del sistema de eventos, que se agregó automáticamente a la escena, porque por ahora no lo necesitamos.

Establezca el Modo de renderizado en World Space y gírelo 90 grados a lo largo del eje X para que el lienzo se superponga a la cuadrícula. Ajuste el pivote y la posición a cero. Dele un ligero desplazamiento vertical para que su contenido esté en la parte superior. El ancho y la altura no son importantes para nosotros, porque organizamos los contenidos por nuestra cuenta. Podemos establecer el valor en 0 para eliminar el rectángulo grande en la ventana de la escena.

Como toque final, aumente los píxeles dinámicos por unidad a 10. Por lo tanto, garantizamos que los objetos de texto usarán una resolución de textura suficiente.



Lienzo para coordenadas de cuadrícula de hexágonos.

Para mostrar las coordenadas, cree un objeto de texto ( GameObject / UI / Text ) y conviértalo en un prefabricado. Centre sus anclajes y pivote, establezca el tamaño en 5 por 15. El texto también debe estar alineado horizontal y verticalmente en el centro. Establezca el tamaño de fuente en 4. Finalmente, no queremos usar el texto predeterminado y no usaremos Texto enriquecido . Además, no nos importa si Raycast Target está activado, porque para nuestro lienzo todavía no es necesario.



Etiqueta de celda prefabricada.

Ahora tenemos que decirle a la cuadrícula sobre lienzo y prefabricados. Agregue al comienzo de su script using UnityEngine.UI; para acceder cómodamente al tipo UnityEngine.UI.Text . Una etiqueta prefabricada necesita una variable compartida, y el lienzo se puede encontrar llamando a GetComponentInChildren .

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


Conexión de etiquetas prefabricadas.

Después de conectar la etiqueta prefabricada, podemos crear instancias y mostrar las coordenadas de la celda. Entre X y Z, inserte un carácter de nueva línea para que aparezcan en líneas separadas.

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


Visualización coordinada.

Posiciones Hexagonales


Ahora que podemos reconocer visualmente cada celda, comencemos a moverlas. Sabemos que la distancia entre las celdas hexagonales adyacentes en la dirección X es igual al doble del radio interno. Lo usaremos Además, la distancia a la siguiente fila de celdas debe ser 1,5 veces mayor que el radio exterior.


Geometría de hexágonos vecinos.

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


Aplicamos distancias entre hexágonos sin compensaciones.

Por supuesto, las filas ordinales de los hexágonos no se encuentran exactamente una encima de la otra. Cada fila está desplazada a lo largo del eje X por el valor del radio interno. Este valor se puede obtener agregando la mitad de Z a X, y luego multiplicar por dos veces el radio interno.

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


La colocación adecuada de los hexágonos crea una cuadrícula en forma de diamante.

Aunque así es como colocamos las celdas en las posiciones correctas de los hexágonos, nuestra cuadrícula ahora llena el rombo en lugar del rectángulo. Estamos mucho más cómodos trabajando con cuadrículas rectangulares, así que hagamos que las celdas vuelvan a funcionar. Esto se puede hacer moviendo hacia atrás parte del desplazamiento. En cada segunda fila, todas las celdas deben desplazarse un paso adicional. Para hacer esto, necesitamos restar el resultado de la división entera de Z por 2 antes de multiplicar.

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


La ubicación de los hexágonos en un área rectangular.

paquete de la unidad

Render Hexagonal


Habiendo colocado las celdas correctamente, podemos proceder a mostrar los hexágonos reales. Primero necesitamos deshacernos de los planos, por lo que eliminaremos todos los componentes, excepto HexCell de la celda HexCell .


No hay más aviones.

Al igual que en los tutoriales de Mesh Basics , utilizamos una malla para representar la malla completa. Sin embargo, esta vez no preestableceremos el número de vértices y triángulos requeridos. En cambio, usaremos listas.

Cree un nuevo componente HexMesh que cuide nuestra malla. Necesitará un filtro de malla y un procesador, tiene una malla y listas para vértices y triángulos.

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

Cree un nuevo objeto hijo para esta malla con este componente. Recibirá automáticamente un renderizador de malla, pero no se le asignará ningún material. Por lo tanto, agregue el material predeterminado.



Objeto de malla hexagonal.

HexGrid ahora podrá recuperar su malla hexagonal de la misma manera que encontró el lienzo.

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

Después de la malla despierta, debe ordenar que la malla triangule sus celdas. Necesitamos estar seguros de que esto sucederá después del componente Despierto de la malla hexagonal. Como se llama a Start más tarde, inserte el código apropiado allí.

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

Se puede llamar a este método HexMesh.Triangulate en cualquier momento, incluso si las celdas ya se han triangulado anteriormente. Por lo tanto, debemos comenzar limpiando los datos antiguos. Al recorrer todas las celdas, las triangulamos individualmente. Después de completar esta operación, asignamos los vértices y triángulos generados a la malla, y terminamos recalculando las normales de la malla.

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

Como los hexágonos están compuestos de triángulos, creemos un método conveniente para agregar un triángulo basado en las posiciones de tres vértices. Solo agregará vértices en orden. También agrega los índices de estos vértices para formar un triángulo. El índice del primer vértice es igual a la longitud de la lista de vértices antes de agregarle nuevos vértices. No te olvides de esto cuando agregues vértices.

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

Ahora podemos triangular nuestras células. Comencemos con el primer triángulo. Su primer pico está en el centro del hexágono. Los otros dos vértices son los ángulos primero y segundo relativos al centro.

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


El primer triángulo de cada celda.

Esto funcionó, así que recorramos los seis triángulos.

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

¿Se pueden compartir los picos?
Si puedes. De hecho, podemos hacerlo aún mejor y usar solo cuatro en lugar de seis triángulos para renderizar. Pero al abandonar esto, simplificaremos nuestro trabajo, y será correcto, porque en los siguientes tutoriales todo se vuelve más complicado. Optimizar vértices y triángulos en esta etapa nos obstaculizará en el futuro.

Desafortunadamente, este proceso dará como resultado una IndexOutOfRangeException . Esto se debe a que el último triángulo está tratando de obtener la séptima esquina, que no existe. Por supuesto, debe regresar y usarlo como el último vértice de la primera esquina. O podemos duplicar la primera esquina en HexMetrics.corners para no ir más allá de los límites.

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


Hexágonos por completo.

paquete de la unidad

Coordenadas hexagonales


Veamos nuevamente las coordenadas de las celdas, ahora en el contexto de una cuadrícula de hexágonos. La coordenada Z se ve bien, y la coordenada X zigzaguea. Este es un efecto secundario del desplazamiento de línea para cubrir un área rectangular.


Coordenadas desplazadas con líneas cero resaltadas.

Cuando se trabaja con hexágonos, tales coordenadas de desplazamiento no son fáciles de manejar. HexCoordinates una estructura HexCoordinates , que se puede usar para convertir a otro sistema de coordenadas. Hagámoslo serializable para que Unity pueda almacenarlo y experimente la recompilación en el modo Play. También hacemos que estas coordenadas sean inmutables utilizando las propiedades públicas de solo lectura.

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

Agregue un método estático para crear un conjunto de coordenadas a partir de coordenadas de desplazamiento ordinarias. Por ahora, simplemente copiaremos estas coordenadas.

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

También agregamos métodos convenientes de conversión de cadenas. El método ToString por defecto devuelve un nombre de tipo struct, que no es muy útil para nosotros. Lo redefinimos para que devuelva las coordenadas en una línea. También agregaremos un método para mostrar coordenadas en líneas separadas, porque ya usamos dicho esquema.

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

Ahora podemos pasar muchas coordenadas a nuestro componente HexCell .

 public class HexCell : MonoBehaviour { public HexCoordinates coordinates; } 

Cambie HexGrid.CreateCell para que pueda usar las nuevas coordenadas.

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

Ahora vamos a rehacer estas coordenadas X para que estén alineadas a lo largo de un eje recto. Esto se puede hacer cancelando el desplazamiento horizontal. El resultado resultante generalmente se llama coordenadas axiales.

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



Coordenadas axiales

Este sistema de coordenadas bidimensional nos permite describir secuencialmente el movimiento de desplazamiento en cuatro direcciones. Sin embargo, las dos direcciones restantes aún requieren atención especial. Esto nos hace darnos cuenta de que hay una tercera dimensión. Y, de hecho, si volteáramos horizontalmente la dimensión de X, obtendríamos la dimensión faltante de Y.


Aparece la medida Y.

Dado que estas medidas de X e Y son copias especulares entre sí, la adición de sus coordenadas siempre da el mismo resultado si Z permanece constante. De hecho, si suma las tres coordenadas, siempre obtendremos cero. Si aumenta una coordenada, debe reducir la otra. Y, de hecho, esto nos da seis posibles direcciones de movimiento. Tales coordenadas generalmente se llaman cúbicas, porque son tridimensionales y la topología se asemeja a un cubo.

Como la suma de todas las coordenadas es cero, siempre podemos obtener cualquiera de las coordenadas de las otras dos. Como ya almacenamos las coordenadas X y Z, no necesitamos almacenar la coordenada Y.
Podemos agregar una propiedad que la evalúa si es necesario y usarla en métodos de cadena.

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


Coordenadas cúbicas

Inspector Coordina


En el modo de reproducción, seleccione una de las celdas de la cuadrícula. Resulta que el inspector no muestra sus coordenadas, solo se HexCell.coordinates etiqueta de prefijo HexCell.coordinates .


El inspector no muestra las coordenadas.

Aunque no hay un gran problema con esto, sería genial mostrar las coordenadas. Unity no muestra coordenadas porque no están marcadas como campos serializables. Para mostrarlos, debe especificar explícitamente campos serializables para X y Z.

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


Las coordenadas X y Z ahora se muestran, pero se pueden cambiar. No necesitamos esto, porque las coordenadas deben ser fijas. Tampoco es muy bueno que se muestren uno debajo del otro.

Podemos hacerlo mejor: definir nuestro propio cajón de propiedades para el tipo HexCoordinates . Cree un script HexCoordinatesDrawer y péguelo en la carpeta Editor , porque este script es solo para el editor.

La clase debe extender UnityEditor.PropertyDrawer y necesita el atributo UnityEditor.CustomPropertyDrawer para asociarlo con un tipo adecuado.

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

Los cajones de propiedades muestran su contenido utilizando el método OnGUI . Este método permitió dibujar datos de propiedad serializables y la etiqueta del campo al que pertenecen dentro del rectángulo de la pantalla.

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

Extraemos los valores de x y z de la propiedad, y luego los usamos para crear un nuevo conjunto de coordenadas. Luego dibuje la etiqueta GUI en la posición seleccionada utilizando nuestro método HexCoordinates.ToString .

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


Coordenadas sin una etiqueta de prefijo.

Esto mostrará las coordenadas, pero ahora nos falta el nombre del campo. Estos nombres generalmente se representan utilizando el método EditorGUI.PrefixLabel . Como beneficio adicional, devuelve un rectángulo alineado que coincide con el espacio a la derecha de esta etiqueta.

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


Coordina con una etiqueta.

paquete de la unidad

Células táctiles


Una cuadrícula de hexágonos no es muy interesante si no podemos interactuar con ella. La interacción más simple es tocar la celda, así que agreguemos soporte para ella. Por ahora, simplemente HexGrid este código directamente en HexGrid . Cuando comience a funcionar, lo trasladaremos a otro lugar.

Para tocar una celda, puede emitir rayos en la escena desde la posición del cursor del mouse. Podemos usar el mismo enfoque que en el tutorial de Deformación de malla .

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

Hasta ahora, el código no está haciendo nada. Necesitamos agregar un colisionador a la cuadrícula para que el rayo pueda colisionar con algo. Por lo tanto, le daremos la malla del colisionador HexMesh .

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

Una vez completada la triangulación, asigne una malla al colisionador.

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

¿No podemos usar el colisionador de cajas?
Podemos, pero no coincidirá exactamente con el contorno de nuestra cuadrícula. Sí, y nuestra cuadrícula no permanecerá plana por mucho tiempo, pero este es un tema para futuros tutoriales.

¡Ahora podemos tocar la cuadrícula! ¿Pero qué celda tocamos? Para averiguarlo, necesitamos convertir la posición táctil a las coordenadas de los hexágonos. Esto es un trabajo para HexCoordinates , por lo que declararemos que tiene un método estático FromPosition .

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

¿Cómo determinará este método qué coordenada pertenece a la posición? Podemos comenzar dividiendo x por el ancho horizontal del hexágono. Y dado que la coordenada Y es una imagen especular de la coordenada X, una x negativa nos da y.

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

Por supuesto, esto nos daría las coordenadas correctas si Z fuera cero. Debemos movernos nuevamente cuando nos movemos a lo largo de Z. Cada dos líneas, debemos mover hacia la izquierda una unidad.

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

Como resultado, nuestros valores x e y resultan ser enteros en el centro de cada celda. Por lo tanto, redondeándolos al entero más cercano, debemos obtener las coordenadas. También calculamos Z y así obtenemos las coordenadas finales.

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

Los resultados parecen prometedores, pero ¿son correctas estas coordenadas? Con un estudio cuidadoso, puede encontrar que a veces obtenemos las coordenadas, ¡cuya suma no es igual a cero! Activemos la notificación para asegurarnos de que esto realmente esté sucediendo.

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

En realidad recibimos notificaciones. ¿Cómo solucionamos este error? Surge solo al lado de los bordes entre los hexágonos. Es decir, el redondeo de coordenadas causa problemas. ¿Qué coordenada se redondea en la dirección incorrecta? Cuanto más nos alejamos del centro de la celda, más redondeo obtenemos. Por lo tanto, es lógico suponer que la coordenada redondeada sobre todo es incorrecta.

Luego, la solución es soltar la coordenada con el delta de redondeo más grande y recrearla a partir de los valores de los otros dos. Pero como solo necesitamos X y Z, no podemos molestarnos en recrear Y.

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

Página para colorear de hexágonos


Ahora que podemos tocar la celda correcta, ha llegado el momento de una interacción real. Cambiemos el color de cada celda en la que nos metemos. Agregue los HexGridcolores personalizados de la celda predeterminada y la celda afectada.

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


Selección de color de celda.

Agregar al HexCellcampo de color general.

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

Asignarlo HexGrid.CreateCellal color predeterminado.

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

También necesitamos agregar a HexMeshla información de color.

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

Ahora, al triangular, debemos agregar datos de color a cada triángulo. Para este propósito crearemos un método separado.

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

De vuelta a HexGrid.TouchCell. Primero, convierta las coordenadas de la celda al índice correspondiente de la matriz. Para una cuadrícula cuadrada, esto sería solo X más Z veces el ancho, pero en nuestro caso, también tendremos que agregar un desplazamiento de la mitad Z. Luego tomamos la celda, cambiamos su color y triangulamos la malla nuevamente.

¿Realmente necesitamos volver a triangular toda la malla?
, . . , , . .

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

Aunque ahora podemos colorear las celdas, los cambios visuales aún no son visibles. Esto se debe a que el sombreador no usa colores de vértice de manera predeterminada. Tenemos que escribir el nuestro. Cree un nuevo sombreador predeterminado ( Activos / Crear / Sombreador / Sombreador de superficie predeterminado ). Solo se necesitan hacer dos cambios. Primero, agregue datos de color a su estructura de entrada. En segundo lugar, multiplique el albedo por este color. Solo nos interesan los canales RGB, porque el material es opaco.

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

Cree un nuevo material con este sombreador y luego haga que la malla de malla use este material. Gracias a esto, aparecerán los colores de las celdas.


Células de colores.

¡Obtengo extraños artefactos de sombra!
Unity . , , Z-. .

paquete de la unidad

Editor de mapas


Ahora que sabemos cómo cambiar los colores, creemos un editor simple en el juego. Esta funcionalidad no se aplica a las capacidades HexGrid, por lo que la convertiremos TouchCellen un método general con un parámetro de color adicional. También elimine el campo touchedColor.

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

Cree un componente HexMapEditory mueva los métodos Updatey a él HandleInput. Agregue un campo común para referirse a la cuadrícula de hexágonos, una matriz de colores y un campo privado para rastrear el color activo. Finalmente, agregue un método general para seleccionar un color y haga que inicialmente seleccione el primer color.

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

Agregue otro lienzo, esta vez manteniendo la configuración predeterminada. Agregue un componente HexMapEditor, defina varios colores y conéctelo a una cuadrícula de hexágonos. Esta vez necesitamos un objeto de sistema de eventos, y se creó automáticamente de nuevo.


Editor de mapas hexagonales de cuatro colores.

Agregue un panel al lienzo para almacenar selectores de color ( GameObject / UI / Panel ). Agregue su grupo de alternar ( Componentes / UI / Grupo de alternar ). Haga el panel pequeño y colóquelo en la esquina de la pantalla.


Panel de color con grupo de alternar.

Ahora llene el panel con interruptores para cada color ( GameObject / UI / Toggle ). Mientras no nos molestemos en crear una IU compleja, basta con una configuración manual simple.



Un interruptor para cada color.

Encienda el primer interruptor. Además, haga que todos los interruptores formen parte del grupo de alternancia para que solo se pueda seleccionar uno de ellos a la vez. Finalmente, conéctelos al método de SelectColornuestro editor. Esto se puede hacer usando el botón "+" UI del evento On Value Changed . Seleccione el objeto del editor de mapas, luego seleccione el método deseado de la lista desplegable.


El primer cambio.

Este evento pasa un argumento booleano que determina si el interruptor se enciende cada vez que se cambia. Pero no nos importa. En su lugar, tendremos que pasar manualmente un argumento entero correspondiente al índice de color que queremos usar. Por lo tanto, deje el valor 0 para el primer interruptor, establezca el valor 1 en el segundo, y así sucesivamente.

¿Cuándo se llama al método de evento de cambio?
. , , .

, , . , SelectColor . , .


Colorear en varios colores.

Aunque la interfaz de usuario funciona, hay un detalle molesto. Para verlo, mueva el panel de modo que cubra la cuadrícula de hexágonos. Al elegir un nuevo color, también colorearemos las celdas debajo de la interfaz de usuario. Es decir, estamos interactuando simultáneamente con la interfaz de usuario y con la cuadrícula. Este es un comportamiento indeseable.

Esto se puede solucionar preguntando al sistema de eventos si determinó la ubicación del cursor sobre algún objeto. Como solo conoce los objetos de la interfaz de usuario, esto nos dirá que estamos interactuando con la interfaz de usuario. Por lo tanto, necesitaremos procesar la entrada nosotros mismos solo si esto no sucede.

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

paquete de la unidad

Parte 2: mezclar colores de celda


Tabla de contenidos


  • Conecta a los vecinos.
  • Interpolar los colores entre los triángulos.
  • Crear áreas de fusión.
  • Simplifica la geometría.

En la parte anterior, sentamos las bases de la cuadrícula y agregamos la capacidad de editar celdas. Cada celda tiene su propio color sólido y los colores en los bordes de las celdas cambian dramáticamente. En este tutorial, crearemos zonas de transición que mezclan los colores de las celdas vecinas.


Transiciones suaves entre las células.

Células vecinas


Antes de realizar el suavizado entre los colores de las celdas, necesitamos descubrir cuáles de las celdas están adyacentes entre sí. Cada celda tiene seis vecinos que pueden identificarse en las direcciones de los puntos cardinales. Obtendremos las siguientes direcciones: noreste, este, sureste, suroeste, oeste y noroeste. Vamos a crear una enumeración para ellos y pegarla en un archivo de script separado.

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

¿Qué es la enumeración?
enum , . . , . , .

enum . , integer . , - , integer.


Seis vecinos, seis direcciones.

Para almacenar estos vecinos, agregue a la HexCellmatriz. Aunque podemos hacerlo general, en cambio lo haremos privado y brindaremos acceso a los métodos usando instrucciones. También lo hacemos serializable para que los enlaces no se pierdan al volver a compilar.

  [SerializeField] HexCell[] neighbors; 

¿Necesitamos almacenar todas las conexiones con los vecinos?
, . — , .

Ahora la matriz de vecinos se muestra en el inspector. Como cada celda tiene seis vecinos, para nuestro prefabricado Hex Cell establecemos el tamaño de la matriz 6.


Hay espacio para seis vecinos en nuestra casa prefabricada.

Ahora agreguemos un método general para obtener una celda vecina en una dirección. Dado que el valor de la dirección siempre está en el rango de 0 a 5, no necesitamos verificar si el índice está dentro de la matriz.

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

Agregue un método para especificar un vecino.

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

Las relaciones de vecinos son bidireccionales. Por lo tanto, cuando se establece un vecino en una dirección, sería lógico establecer inmediatamente un vecino en la dirección opuesta.

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


Vecinos en direcciones opuestas.

Por supuesto, esto sugiere que podemos solicitar indicaciones para el vecino opuesto. Podemos implementar esto creando un método de extensión para HexDirection. Para obtener la dirección opuesta, debe agregar al original 3. Sin embargo, esto solo funciona para las tres primeras direcciones, para el resto debe restar 3.

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

¿Qué es un método de extensión?
— , - . — , , , . this . , .

? , , , . ? — . , , .

Conexión vecina


Podemos inicializar el enlace de vecinos en HexGrid.CreateCell. Al atravesar celdas línea por línea, de izquierda a derecha, sabemos qué celdas ya se han creado. Estas son las células con las que podemos conectarnos.

El más simple es el compuesto E - W. La primera celda de cada fila no tiene un vecino oriental. Pero todas las otras células lo tienen. Y estos vecinos se crean antes de la celda con la que estamos trabajando actualmente. Por lo tanto, podemos conectarlos.


La conexión de E a W durante la creación de células.

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


Los vecinos orientales y occidentales están conectados.

Necesitamos crear dos conexiones bidireccionales más. Dado que estas son las conexiones entre diferentes líneas de la cuadrícula, solo podemos comunicarnos con la línea anterior. Esto significa que debemos omitir por completo la primera línea.

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

Como las líneas son en zigzag, deben procesarse de manera diferente. Primero tratemos con líneas pares. Dado que todas las celdas en tales filas tienen un vecino en SE, podemos conectarlas a él.


Conexión de NO a SE para líneas uniformes.

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

¿Qué hace z & 1?
&& — , & — . , . 1, 1. , 10101010 & 00001111 00001010 .

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

, , . 0, .

Podemos conectarnos con vecinos en el SW, excepto por la primera celda de cada fila que no lo tiene.


Conexión de NE a SW para líneas uniformes.

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

Las líneas impares siguen la misma lógica, pero en una imagen especular. Después de completar este proceso, todos los vecinos de nuestra red están conectados.

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


Todos los vecinos están conectados.

Por supuesto, no todas las celdas están conectadas a exactamente seis vecinos. Las celdas en el límite de la cuadrícula tienen al menos dos y no más de cinco vecinos. Y esto debe tenerse en cuenta.


Vecinos para cada celda.

paquete de la unidad

Mezcla de colores


La mezcla de colores complicará la triangulación de cada celda. Por lo tanto, separemos el código de triangulación en una parte separada. Como ahora tenemos direcciones, usémoslas en lugar de índices numéricos para indicar partes.

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

Ahora, cuando usamos direcciones, sería conveniente obtener ángulos con direcciones, y no realizar la conversión a índices.

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

Para hacer esto, debe agregar HexMetricsdos métodos estáticos. Como beneficio adicional, esto nos permite hacer que la matriz de ángulos sea privada.

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

Varios colores en un triangulo


Hasta ahora, el método HexMesh.AddTriangleColorsolo tiene un argumento de color. Solo puede crear un triángulo con color sólido. Creemos una alternativa que admita colores separados para cada vértice.

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

¡Ahora podemos comenzar a mezclar colores! Comencemos simplemente usando el color vecino para los otros dos vértices.

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

Desafortunadamente, esto lleva a NullReferenceException, porque las celdas en la frontera no tienen seis vecinos. ¿Qué debemos hacer cuando hay escasez de un vecino? Seamos pragmáticos y usemos la célula como reemplazo.

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

¿Qué hace el operador?
null-coalescing operator. , a ?? ba != null ? a : b .

, - Unity . null . .


Hay una mezcla de colores, pero se hace incorrectamente.

¿A dónde fueron las etiquetas de coordenadas?
, UI.

Promedio de color


La mezcla de colores funciona, pero los resultados son obviamente incorrectos. El color en los bordes de los hexágonos debe ser el promedio de dos celdas adyacentes.

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


Mezclando en las costillas.
Aunque estamos mezclando en los bordes, todavía obtenemos bordes de color nítidos. Esto sucede porque cada vértice del hexágono es compartido por tres hexágonos.


Tres vecinos, cuatro colores.

Esto significa que también debemos considerar a los vecinos en las instrucciones anteriores y siguientes. Es decir, obtenemos cuatro colores en dos conjuntos de tres.

Agreguemos HexDirectionExtensionsdos métodos de adición para una transición conveniente a las instrucciones anteriores y siguientes.

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

Ahora podemos obtener los tres vecinos y realizar mezclas de tres vías.

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


Mezclar en las esquinas.

Entonces obtenemos las transiciones de color correctas, con la excepción del borde de malla. Las celdas del borde no son consistentes con los colores de los vecinos que faltan, por lo que aquí todavía vemos bordes afilados. Sin embargo, en general, nuestro enfoque actual no da buenos resultados. Necesitamos una mejor estrategia.

paquete de la unidad

Áreas de mezcla


Mezclar sobre toda la superficie del hexágono conduce a un caos borroso. No podemos ver claramente las células individuales. Los resultados pueden mejorarse enormemente al mezclar solo al lado de los bordes de los hexágonos. En este caso, la región interna de los hexágonos retendrá un color sólido.


Sombreado continuo de centavos con áreas de mezcla.

¿Cuál debería ser el tamaño de la región sólida en comparación con la región de mezcla? Diferentes distribuciones conducen a resultados diferentes. Definiremos esta área como una fracción del radio exterior. Que sea igual al 75%. Esto nos llevará a dos nuevas métricas, que suman 100%.

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

Al crear este nuevo factor de relleno sólido, podemos escribir métodos para obtener los ángulos de hexágonos internos sólidos.

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

Ahora, cámbielo HexMesh.Triangulatepara que use estos ángulos de sombreado sólidos en lugar de los ángulos originales. Dejamos los colores iguales por ahora.

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


Hexágonos sólidos sin bordes.

Triangulación de áreas de mezcla.


Necesitamos completar el espacio vacío que creamos reduciendo los triángulos. En cada dirección, este espacio tiene la forma de un trapecio. Para cubrirlo, puede usar el cuadrángulo (quad). Por lo tanto, crearemos métodos para agregar un cuadrángulo y sus colores.


Costilla trapezoidal.

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

Lo rehacemos HexMesh.Triangulatepara que el triángulo reciba un color, y el cuadrángulo realiza una mezcla entre un color sólido y los colores de dos ángulos.

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


Mezcla con costillas trapezoidales.

Puentes entre costillas


La imagen está mejorando, pero el trabajo aún no está terminado. La mezcla de colores entre dos vecinos está contaminada por células vecinas. Para evitar esto, necesitamos cortar las esquinas del trapecio y convertirlo en un rectángulo. Después de eso, creará un puente entre la celda y su vecino, dejando espacios a los lados.


El puente entre las costillas.

Podemos encontrar nuevas posiciones v3y v4, comenzando con v1y v2, y luego avanzando a lo largo del puente hasta el borde de la celda. ¿Cuál será el desplazamiento del puente? Podemos encontrarlo tomando el punto medio entre los dos ángulos correspondientes y luego aplicando el coeficiente de mezcla. Esto se dedica HexMetrics.

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

Volviendo a HexMesh, ahora será lógico agregar una opción AddQuadColorque requiera solo dos colores.

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

Cámbielo Triangulatepara que cree puentes correctamente mezclados entre vecinos.

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


Puentes pintados correctamente con espacios en las esquinas.

Llenando los huecos


Ahora hemos formado un espacio triangular en la unión de tres celdas. Obtuvimos estas brechas cortando los lados triangulares del trapecio. Recuperemos estos triángulos.

Primero, considere un triángulo que se conecta a un vecino anterior. Su primer vértice tiene un color de celda. El color del segundo pico será una mezcla de tres colores. Y el último pico tendrá el mismo color que el punto en el medio del puente.

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


Casi todo está listo.

Otro triángulo funciona de la misma manera, excepto que el puente no toca el tercero, sino el segundo pico.

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


Coloración completa

Ahora tenemos hermosas áreas de mezcla que podemos dar a cualquier tamaño. Los bordes se pueden hacer borrosos o nítidos a su gusto. Pero puede ver que la combinación cerca del borde de malla todavía no se implementa correctamente. Y nuevamente lo dejaremos para más tarde, enfocándonos en otro tema por ahora.

Pero las transiciones entre colores siguen siendo feas.
. . .

paquete de la unidad

Costilla Fusion


Eche un vistazo a la topología de nuestra cuadrícula. ¿Qué formas son notables aquí? Si no presta atención a la frontera, podemos distinguir tres tipos diferentes de formas. Hay hexágonos de un color, rectángulos de dos colores y triángulos de tres colores. Todos estos tres colores aparecen en la unión de las tres celdas.


Tres estructuras visuales.

Entonces, cada dos hexágonos están conectados por un puente rectangular. Y cada tres hexágonos están conectados por un triángulo. Sin embargo, realizamos una triangulación más compleja. Ahora usamos dos cuadrángulos en lugar de uno para conectar un par de hexágonos. Y para conectar los tres hexágonos, usamos seis triángulos. Esto es demasiado redundante. Además, si tuviéramos que conectarnos directamente a una forma, entonces no necesitaríamos promediar ningún color. Por lo tanto, podríamos sobrevivir con menos complejidad, menos trabajo y menos triángulos.


Más duro de lo necesario.

¿Por qué necesitamos esto?
, . , , . , , . , , .

Puente directo


Ahora nuestros puentes entre las costillas consisten en dos cuadrángulos. Para extenderlos al siguiente hexágono, necesitamos duplicar la longitud del puente. Esto significa que ya no necesitamos promediar dos ángulos HexMetrics.GetBridge. En cambio, simplemente los agregamos y luego multiplicamos por el factor de mezcla.

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


Los puentes atravesaban toda la longitud y se superponían entre sí.

Los puentes ahora crean conexiones directas entre hexágonos. Pero aún generamos dos cuadrángulos por conexión, uno en cada dirección. Es decir, solo uno de ellos debería crear puentes entre dos celdas.

Simplifiquemos primero nuestro código de triangulación. Eliminaremos todo lo relacionado con los triángulos de los bordes y la mezcla de colores. Luego mueva el código que agrega el cuadrángulo del puente al nuevo método. Pasamos los dos primeros vértices a este método para que no tengamos que volver a calcularlos.

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

Ahora podemos limitar fácilmente la triangulación de compuestos. Para comenzar, agregaremos el puente solo cuando trabajemos con la conexión NE.

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


Los puentes son solo en dirección a NE.

Parece que podemos cubrir todos los compuestos triangulándolos solo en las tres primeras direcciones: NE, E y SE.

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


Todos los puentes internos y puentes en las fronteras.

Cubrimos todas las conexiones entre dos celdas vecinas. Pero también tenemos algunos puentes que conducen desde la celda a ninguna parte. Vamos a deshacernos de ellos, salir TriangulateConnectioncuando los vecinos estén fuera. Es decir, ya no necesitamos reemplazar a los vecinos desaparecidos con la celda misma.

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


Solo puentes internos.

Articulaciones triangulares


Ahora necesitamos cerrar los huecos triangulares nuevamente. Hagamos esto para un triángulo que se conecta al próximo vecino. Y esto nuevamente debe hacerse solo cuando existe un vecino.

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

¿Cuál será la posición del tercer pico? Inserté como reemplazo v2, pero esto obviamente es incorrecto. Como cada borde de estos triángulos está conectado al puente, podemos encontrarlo caminando a lo largo del puente hasta el próximo vecino.

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


Estamos haciendo triangulación completa de nuevo.

Hemos terminado? Todavía no, porque ahora estamos creando triángulos superpuestos. Dado que las tres celdas tienen una conexión triangular común, necesitamos agregarlas solo para dos conexiones. Por lo tanto, NE y E. lo harán.

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

paquete de la unidad

Parte 3: alturas


Tabla de contenidos


  • Agregar celdas de altura.
  • Triangular las pendientes.
  • Inserte las repisas.
  • Combina repisas y acantilados.

En esta parte del tutorial, agregaremos soporte para diferentes niveles de altura y crearemos transiciones especiales entre ellos.


Alturas y repisas.

Altura de la celda


Dividimos nuestro mapa en celdas separadas que cubren un área plana. Ahora le daremos a cada celda su propio nivel de altura. Utilizaremos niveles de elevación discretos para almacenarlos como un campo entero en HexCell.

  public int elevation; 

¿Qué tan grande puede ser cada nivel posterior de altura? Podemos usar cualquier valor, así que configurémoslo como otra constante HexMetrics. Usaremos un paso de cinco unidades para que las transiciones sean claramente visibles. En un juego real, usaría un paso más pequeño.

  public const float elevationStep = 5f; 

Cambiar celdas


Hasta ahora, solo podíamos cambiar el color de la celda, pero ahora podemos cambiar su altura. Por lo tanto, el método HexGrid.ColorCellno es suficiente para nosotros. Además, en el futuro, podemos agregar otras opciones de edición de celdas, por lo que necesitamos un nuevo enfoque.

Cambiar el nombre ColorCellde GetCelly hacer de modo que en su lugar devolverá celular color de referencia de una celda en una posición predeterminada. Dado que este método no cambia nada más, necesitamos triangular inmediatamente las celdas.

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

Ahora el editor se ocupará del cambio de celda. Después de completar el trabajo, la cuadrícula necesita ser triangulada nuevamente. Para hacer esto, agregue un método general HexGrid.Refresh.

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

Cambie HexMapEditorpara que pueda trabajar con nuevos métodos. Vamos a darle un nuevo método EditCell, que se ocupará de todos los cambios en la celda, después de lo cual actualizará la cuadrícula.

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

Podemos cambiar las alturas simplemente asignando a la celda deseada el nivel de altura deseado.

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

Al igual que con los colores, necesitamos un método para establecer el nivel de altura activo, que asociaremos con la interfaz de usuario. Para seleccionar valores del intervalo de altura, utilizamos el control deslizante. Dado que los controles deslizantes funcionan con flotante, nuestro método requiere un parámetro de tipo flotante. Simplemente lo convertiremos a entero.

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

Agregue un control deslizante ( GameObject / Create / Slider ) al lienzo y colóquelo debajo de la barra de colores. Lo hacemos vertical, de abajo hacia arriba, para que visualmente corresponda a los niveles de altura. Lo limitamos a enteros y creamos un intervalo adecuado, por ejemplo, de 0 a 6. Luego adjuntamos su evento On Value Changed al método del SetElevationobjeto Hex Map Editor . El método debe seleccionarse de la lista dinámica para que se llame con el valor del control deslizante.



Control deslizante de altura.

Visualización de la altura


Al cambiar una celda, ahora establecemos tanto el color como la altura. Aunque en el inspector podemos ver que la altura realmente cambia, el proceso de triangulación aún la ignora.

Es suficiente para nosotros cambiar la posición local vertical de la celda al cambiar la altura. Por conveniencia, hagamos que el método sea HexCell.elevationprivado y agreguemos una propiedad general HexCell.Elevation.

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

Ahora podemos cambiar la posición vertical de la celda al editar la altura.

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

Por supuesto, esto requiere pequeños cambios en HexMapEditor.EditCell.

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


Células con diferentes alturas.

¿El colisionador de malla cambia para adaptarse a la nueva altura?
Unity mesh collider null. , , null . . ( ) .

Las alturas de las celdas ahora son visibles, pero hay dos problemas. Primero de todo Las etiquetas de las celdas desaparecen debajo de las celdas elevadas. En segundo lugar, las conexiones entre celdas ignoran la altura. Vamos a arreglarlo

Cambiar la posición de las etiquetas de celda


Actualmente, las etiquetas de IU para las celdas se crean y colocan solo una vez, después de lo cual nos olvidamos de ellas. Para actualizar sus posiciones verticales, necesitamos rastrearlos. Démosle a todos un HexCellenlace a RectTransformsus etiquetas de IU para que pueda actualizarlo más tarde.

  public RectTransform uiRect; 

Asignarlos al final HexGrid.CreateCell.

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

Ahora podemos expandir la propiedad HexCell.Elevationpara que también cambie la posición de la interfaz de usuario de la celda. Dado que el lienzo de la cuadrícula hexagonal se gira, las etiquetas deben moverse en la dirección negativa a lo largo del eje Z y no en el lado positivo del eje Y.

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


Etiquetas con altura.

Creación de pistas.


Ahora necesitamos convertir las conexiones de celda plana en pendientes. Esto se hace en HexMesh.TriangulateConnection. En el caso de las conexiones de borde, necesitamos redefinir la altura del otro extremo del puente.

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

En el caso de las juntas de esquina, debemos hacer lo mismo con el puente al próximo vecino.

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


Conexión teniendo en cuenta la altura.

Ahora tenemos soporte para células a diferentes alturas con las juntas inclinadas correctas entre ellas. Pero no nos quedemos ahí. Haremos que estas pendientes sean más interesantes.

paquete de la unidad

Costillas con repisas


Las pendientes rectas no se ven muy atractivas. Podemos dividirlos en varios pasos agregando pasos. Este enfoque se utiliza en el juego Endless Legend.

Por ejemplo, podemos insertar dos repisas en cada pendiente. Como resultado, una pendiente grande se convierte en tres pequeñas, entre las cuales hay dos áreas planas. Para triangular dicho esquema, tendremos que separar cada conexión en cinco etapas.


Dos repisas en la ladera.

Podemos establecer el número de pasos para la pendiente HexMetricsy calcular el número de etapas en función de esto.

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

Idealmente, podríamos simplemente interpolar cada paso a lo largo de la pendiente. Pero esto no es del todo trivial, porque la coordenada Y solo debería cambiar en etapas impares. De lo contrario, no obtendremos repisas planas. Agreguemos un método de interpolación especial para esto HexMetrics.

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

La interpolación horizontal es simple si conocemos el tamaño del paso de interpolación.

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

¿Cómo funciona la interpolación entre dos valores?
a y b t . t 0, a . 1, b . t - 0 1, a y b . : (1t)a+tb .

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

Para cambiar Y solo en etapas impares, podemos usar ( s t e p + 1 ) / 2 . Si usamos la división de enteros, convertirá las series 1, 2, 3, 4 en 1, 1, 2, 2.

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

Agreguemos también un método para interpolar cornisas para colores. Simplemente interpolarlos como si las conexiones fueran planas.

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

Triangulación


A medida que la triangulación de la conexión de borde se vuelve más complicada, eliminamos el código correspondiente HexMesh.TriangulateConnectiony lo colocamos en un método separado. En los comentarios, guardaré el código fuente para consultarlo en el futuro.

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

Comencemos desde el primer paso del proceso. Utilizaremos nuestros métodos especiales de interpolación para crear el primer quad. En este caso, se debe crear una pendiente corta, más pronunciada que la original.

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


El primer paso para crear una repisa.

Ahora procederemos inmediatamente a la última etapa, omitiendo todo lo que se encuentre en el medio. Esto completará la conexión de los bordes, aunque hasta ahora con una forma irregular.

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


El último paso para crear una repisa.

Se pueden agregar pasos intermedios a través del bucle. En cada etapa, los dos últimos vértices anteriores se convierten en los nuevos primero. Lo mismo vale para el color. Después de calcular los nuevos vectores y colores, se agrega otro quad.

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


Todos los pasos intermedios.

Ahora todas las juntas de borde tienen dos repisas, o cualquier otro número que especifique HexMetrics.terracesPerSlope. Por supuesto, hasta que hayamos creado repisas para las esquinas, dejaremos esto para más adelante.


Todas las juntas de los bordes tienen repisas.

paquete de la unidad

Tipos de conexión


Convertir todas las juntas de borde en repisas no es una buena idea. Solo se ven bien cuando la diferencia de altura es solo de un nivel. Pero con una diferencia más grande, se crean repisas estrechas con grandes espacios entre ellas, y esto no se ve muy hermoso. Además, no necesitamos crear repisas para todas las juntas.

Formalicemos esto y definamos tres tipos de aristas: un plano, una pendiente y un acantilado. Vamos a crear una enumeración para esto.

 public enum HexEdgeType { Flat, Slope, Cliff } 

¿Cómo determinar con qué tipo de conexión estamos tratando? Para hacer esto, podemos agregar a un HexMetricsmétodo que usa dos niveles de altura.

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

Si las alturas son las mismas, tendremos una costilla plana.

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

Si la diferencia en los niveles es igual a un paso, entonces esta es una pendiente. No importa si sube o baja. En todos los demás casos, tenemos un descanso.

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

Agreguemos también un método conveniente HexCell.GetEdgeTypepara obtener el tipo de borde de celda en una determinada dirección.

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

¿No necesitamos comprobar si existe un vecino en esta dirección?
, , . , NullReferenceException . , , - . , . .

, , , . - , NullReferenceException .

Crear repisas solo para pendientes


Ahora que podemos determinar el tipo de conexión, podemos decidir si insertar repisas. Cambie HexMesh.TriangulateConnectionpara que cree repisas solo para las pendientes.

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

En este punto, podemos descomentar el código comentado previamente para que pueda manejar planos y recortes.

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


Los pasos se crean solo en las pendientes.

paquete de la unidad

Repisas con repisas


Las juntas de esquina son más complejas que las juntas de borde porque están involucradas no en dos, sino en tres celdas. Cada esquina está conectada a tres bordes, que pueden ser planos, pendientes o acantilados. Por lo tanto, hay muchas configuraciones posibles. Al igual que con las costillas, es mejor agregar HexMeshtriangulación al nuevo método.

Nuestro nuevo método requerirá los vértices de un triángulo angular y celdas conectadas. Para mayor comodidad, organicemos las conexiones para saber qué celda tiene la altura más pequeña. Después de eso, podemos comenzar a trabajar desde la parte inferior izquierda y derecha.


Articulación de esquina.

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

Ahora TriangulateConnectiondebo determinar cuál de las celdas es la más baja. Primero verificamos si la celda triangulada está por debajo de sus vecinos o está en el mismo nivel con el más bajo. Si es así, entonces podemos usarlo como la celda más baja.

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

Si falla la verificación más profunda, esto significa que el próximo vecino es la celda más baja. Para una orientación adecuada, debemos rotar el triángulo en sentido antihorario.

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

Si la primera prueba falla, entonces necesita comparar dos celdas vecinas. Si el vecino de la costilla es el más bajo, entonces debe girar en sentido horario, de lo contrario, en sentido antihorario.

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


Gire en sentido antihorario, sin giro, rotación en sentido horario.

Triangulación de pendiente


Para saber cómo triangular un ángulo, necesitamos entender con qué tipos de aristas estamos tratando. Para simplificar esta tarea, agreguemos a HexCellotro método conveniente para reconocer la pendiente entre dos celdas.

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

Utilizamos este nuevo método HexMesh.TriangulateCornerpara determinar los tipos de bordes izquierdo y derecho.

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

Si ambas costillas son inclinadas, tendremos salientes tanto a la izquierda como a la derecha. Además, dado que la celda inferior es la más baja, sabemos que estas pendientes se elevan. Además, las celdas izquierda y derecha tienen la misma altura, es decir, la conexión del borde superior es plana. Podemos designar este caso como "pendiente-pendiente-plano", o MTP.


Dos pendientes y un avión, SSP.

Comprobaremos si estamos en esta situación, y si es así, llamaremos a un nuevo método TriangulateCornerTerraces. Después de eso, volveremos del método. Inserte esta verificación antes del antiguo código de triangulación para que reemplace el triángulo original.

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

Como no estamos haciendo nada en el interior TriangulateCornerTerraces, algunos cruces de esquina con dos pendientes se convertirán en vacíos. Si la conexión se vacía o no depende de cuál de las celdas es más baja.


Hay un vacío

Para llenar el vacío, necesitamos conectar la repisa izquierda y derecha a través de un espacio. El enfoque aquí es el mismo que para unir bordes, pero dentro de un triángulo de tres colores en lugar de un cuadrilátero de dos colores. Comencemos nuevamente con la primera etapa, que ahora es un triángulo.

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


La primera etapa del triángulo.

Y nuevamente vamos directamente a la última etapa. Este es el cuadrángulo que forma un trapecio. La única diferencia con las conexiones de borde aquí es que no estamos tratando con dos, sino con cuatro colores.

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


La última etapa del cuadrángulo.

Todas las etapas entre ellos también son cuadrángulos.

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


Todas las etapas

Dos variaciones de pendiente


El caso con dos pendientes tiene dos variaciones con diferentes orientaciones, dependiendo de cuál de las celdas es la parte inferior. Podemos encontrarlos comprobando combinaciones izquierda-derecha para pendiente-plano y plano-pendiente.


ATP y MSS.

Si el borde derecho es plano, entonces deberíamos comenzar a crear repisas a la izquierda, y no a la parte inferior. Si el borde izquierdo es plano, debe comenzar por el derecho.

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

Debido a esto, las repisas irán alrededor de las celdas sin interrupción hasta que lleguen al acantilado o al final del mapa.


Repisas sólidas.

paquete de la unidad

Fusión de laderas y acantilados.


¿Qué hay de conectar la pendiente y el acantilado? Si sabemos que el borde izquierdo es una pendiente y el derecho es un acantilado, ¿cuál será el borde superior? No puede ser plano, pero puede ser una pendiente o un acantilado.




SOS y COO.

Agreguemos un nuevo método para manejar todos los casos de pendiente-acantilado.

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

Debería llamarse como la última opción TriangulateCornercuando el borde izquierdo es una pendiente.

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

¿Cómo triangulamos esto? Esta tarea se puede dividir en dos partes: inferior y superior.

Parte inferior


La parte inferior tiene repisas a la izquierda y un acantilado a la derecha. Necesitamos combinarlos de alguna manera. La forma más fácil de hacer esto es apretando las repisas para que se encuentren en la esquina derecha. Esto elevará las repisas.


Compresión de repisas.

Pero, de hecho, no queremos que se reúnan en la esquina derecha, porque esto interferirá con las repisas que puedan existir arriba. Además, podemos lidiar con un acantilado muy alto, debido al cual obtenemos triángulos que caen muy finos y delgados. En cambio, los comprimiremos hasta un punto límite que se encuentra a lo largo de un acantilado.


Compresión en la frontera.

Coloquemos el punto límite un nivel por encima de la celda inferior. Puede encontrarlo por interpolación en función de la diferencia de altura.

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

Para asegurarnos de que lo tenemos correctamente, cubrimos toda la parte inferior con un triángulo.

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


Triángulo inferior

Después de colocar el borde en el lugar correcto, podemos proceder a la triangulación de las repisas. Comencemos de nuevo solo desde la primera etapa.

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


La primera etapa de compresión.

Esta vez, la última etapa también será un triángulo.

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


La última etapa de compresión.

Y todos los pasos intermedios también son triángulos.

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


Repisas comprimidas.

¿No podemos mantener el nivel de la repisa?
, , , . . , . .

Terminación de la esquina


Habiendo terminado la parte inferior, puedes ir a la parte superior. Si el borde superior es una pendiente, entonces nuevamente tendremos que conectar las repisas y el acantilado. Así que vamos a mover este código a un método separado.

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

Ahora completar la parte superior será fácil. Si tenemos una pendiente, entonces agregue el triángulo girado del borde. De lo contrario, un triángulo simple es suficiente.

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



Triangulación completa de ambas partes.

Estuches reflejados


Examinamos los casos de "pendiente-acantilado". También hay dos cajas de espejo, cada una de las cuales tiene un acantilado a la izquierda.


OSS y CCA.

Utilizaremos el enfoque anterior, con ligeras diferencias debido a un cambio de orientación. Lo copiamos TriangulateCornerTerracesCliffy lo cambiamos en consecuencia.

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

Agregue estos casos a TriangulateCorner.

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



OSS triangulado y CCA.

Acantilados dobles


Los únicos casos no planos restantes son las celdas inferiores con acantilados en ambos lados. En este caso, la costilla superior puede ser cualquiera: plana, inclinada o acantilada. Solo nos interesa el caso “acantilado-acantilado-pendiente”, porque solo tendrá repisas.

De hecho, hay dos versiones diferentes de "acantilado-acantilado-pendiente", dependiendo de qué lado es más alto. Son imágenes especulares el uno del otro. Vamos a designarlos como OOSP y OOSL.




OOSP y OOSL.

Podemos cubrir ambos casos TriangulateCornerllamando a métodos TriangulateCornerCliffTerracesy TriangulateCornerTerracesCliffcon diferentes rotaciones de células.

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

Sin embargo, esto crea una triangulación extraña. Esto se debe a que ahora estamos triangulando de arriba a abajo. Debido a esto, nuestra frontera se interpola como negativa, lo cual es incorrecto. La solución aquí es tener siempre interpoladores positivos.

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



OOSP triangulado y OOSL.

Barrer


Examinamos todos los casos que requieren un manejo especial para garantizar la triangulación correcta de las repisas.


Triangulación completa con repisas.

Podemos limpiar un poco TriangulateCornerdeshaciéndonos de los operadores returny usando bloques en su lugar else.

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

El último bloque elsecubre todos los casos restantes que aún no se han cubierto. Estos casos son RFP (plane-plane-plane), OOP, LLC y LLC. Todos ellos están cubiertos por un triángulo.


.

unitypackage

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


All Articles