Mapas de hexágono en Unity: Path Finder, escuadrones de jugadores, animaciones

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 16: encontrar el camino


  • Resaltar celdas
  • Seleccione un objetivo de búsqueda
  • Encuentra el camino más corto
  • Crear una cola prioritaria

Después de calcular las distancias entre las celdas, procedimos a encontrar los caminos entre ellas.

A partir de esta parte, se crearán tutoriales de mapas hexagonales en Unity 5.6.0. Cabe señalar que en 5.6 hay un error que destruye los conjuntos de texturas en ensamblajes para varias plataformas. Puede evitarlo si incluye Es legible en el inspector de matriz de texturas.


Planeando un viaje

Celdas resaltadas


Para buscar la ruta entre dos celdas, primero debemos seleccionar estas celdas. Es más que solo elegir una celda y monitorear la búsqueda en el mapa. Por ejemplo, primero seleccionaremos la celda inicial y luego la final. En este caso, sería conveniente que se resaltaran. Por lo tanto, agreguemos dicha funcionalidad. Hasta que creamos una forma sofisticada o eficiente de resaltar, solo creamos algo que nos ayude con el desarrollo.

Textura del esquema


Una forma sencilla de seleccionar celdas es agregarles una ruta. La forma más fácil de hacerlo es con una textura que contiene un contorno hexagonal. Aquí puedes descargar dicha textura. Es transparente, excepto por el contorno blanco del hexágono. Habiéndolo hecho blanco, en el futuro podremos colorearlo como lo necesitemos.


Esquema de celda sobre fondo negro

Importe la textura y establezca su Tipo de textura en Sprite . Su Modo Sprite se establecerá en Individual con la configuración predeterminada. Dado que esta es una textura excepcionalmente blanca, no necesitamos convertir a sRGB . El canal alfa indica transparencia, por lo que habilitar Alfa es Transparencia . También configuré la textura del Modo de filtro en Trilineal , porque de lo contrario las transiciones de mip para las rutas pueden ser demasiado notables.


Opciones de importación de texturas

Un sprite por celda


La forma más rápida es agregar un posible contorno a las celdas, agregando cada propio sprite. Cree un nuevo objeto de juego, agregue el componente Imagen ( Componente / UI / Imagen ) y asígnele nuestro sprite de esquema. Luego inserte la instancia prefabricada de etiqueta de celda hexadecimal en la escena, haga que el objeto sprite sea secundario, aplique los cambios al prefab y luego elimine el prefab.



Elemento de selección hijo prefabricado

Ahora cada celda tiene un sprite, pero será demasiado grande. Para hacer que los contornos coincidan con los centros de las celdas, cambie el Ancho y la Altura del componente de transformación del sprite a 17.


Selección de sprites parcialmente ocultos por un relieve

Dibujando encima de todo


Dado que el contorno se superpone en el área de los bordes de las celdas, a menudo aparece bajo la geometría del relieve. Debido a esto, parte del circuito desaparece. Esto se puede evitar elevando ligeramente los sprites verticalmente, pero no en el caso de roturas. En cambio, podemos hacer lo siguiente: dibujar siempre sprites encima de todo lo demás. Para hacer esto, crea tu propio sombreador de sprites. Será suficiente para nosotros copiar el sombreador de sprites estándar de Unity y hacerle algunos cambios.

Shader "Custom/Highlight" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1) [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1) [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {} [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off ZWrite Off Blend One OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex SpriteVert #pragma fragment SpriteFrag #pragma target 2.0 #pragma multi_compile_instancing #pragma multi_compile _ PIXELSNAP_ON #pragma multi_compile _ ETC1_EXTERNAL_ALPHA #include "UnitySprites.cginc" ENDCG } } } 

El primer cambio es que ignoramos el búfer de profundidad, haciendo que la prueba Z siempre tenga éxito.

  ZWrite Off ZTest Always 

El segundo cambio es que estamos renderizando después del resto de la geometría transparente. Suficiente para agregar 10 a la cola de transparencia.

  "Queue"="Transparent+10" 

Crea un nuevo material que usará este sombreador. Podemos ignorar todas sus propiedades, adhiriéndonos a los valores predeterminados. Luego haga que el sprite prefabricado use este material.



Usamos nuestro propio material de sprite

Ahora los contornos de la selección son siempre visibles. Incluso si la celda está oculta bajo un relieve más alto, su contorno se dibujará sobre todo lo demás. Puede que no se vea hermoso, pero las celdas seleccionadas siempre estarán visibles, lo cual es útil para nosotros.


Ignorar el búfer de profundidad

Control de selección


No queremos que todas las celdas se resalten al mismo tiempo. De hecho, inicialmente todos deberían estar sin seleccionar. Podemos implementar esto deshabilitando el componente Imagen del objeto prefabricado Highlight .


Componente de imagen deshabilitado

Para habilitar la selección de celdas, agregue el método EnableHighlight a EnableHighlight . Debe tomar el único hijo de su uiRect e incluir su componente de imagen. También crearemos el método DisableHighlight .

  public void DisableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = false; } public void EnableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = true; } 

Finalmente, podemos especificar el color para que cuando se encienda, le dé un tono a la luz de fondo.

  public void EnableHighlight (Color color) { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.color = color; highlight.enabled = true; } 

paquete de la unidad

Encontrando el camino


Ahora que podemos seleccionar las celdas, debemos avanzar y seleccionar dos celdas, y luego encontrar la ruta entre ellas. Primero necesitamos seleccionar las celdas, luego restringir la búsqueda a una ruta entre ellas y finalmente mostrar esta ruta.

Inicio de búsqueda


Necesitamos seleccionar dos celdas diferentes, los puntos inicial y final de la búsqueda. Suponga que para seleccionar la celda de búsqueda inicial, mantenga presionada la tecla Mayús izquierda mientras hace clic con el mouse. En este caso, la celda se resalta en azul. Necesitamos guardar el enlace a esta celda para buscar más. Además, al elegir una nueva celda inicial, la selección de la anterior debe estar deshabilitada. Por lo tanto, agregamos el campo searchFromCell a searchFromCell .

  HexCell previousCell, searchFromCell; 

Dentro de HandleInput podemos usar Input.GetKey(KeyCode.LeftShift) para probar la tecla Shift presionada.

  if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); } else { hexGrid.FindDistancesTo(currentCell); } 


Donde mirar

Punto final de búsqueda


En lugar de buscar todas las distancias a una celda, ahora estamos buscando una ruta entre dos celdas específicas. Por lo tanto, cambie el nombre de HexGrid.FindDistancesTo a HexGrid.FindPath y dele el segundo parámetro HexCell . Además, cambie el método de Search .

  public void FindPath (HexCell fromCell, HexCell toCell) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell)); } IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; frontier.Add(fromCell); … } 

Ahora HexMapEditor.HandleInput debería llamar al método modificado, utilizando searchFromCell y currentCell como argumentos. Además, solo podemos buscar cuando sabemos desde qué celda buscar. Y no tenemos que molestarnos en buscar si los puntos inicial y final coinciden.

  if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { … } else if (searchFromCell && searchFromCell != currentCell) { hexGrid.FindPath(searchFromCell, currentCell); } 

En cuanto a la búsqueda, primero debemos deshacernos de todas las selecciones anteriores. Por lo tanto, haga que HexGrid.Search desactive la selección al restablecer distancias. Como esto también apaga la iluminación de la celda inicial, enciéndala nuevamente. En esta etapa, también podemos resaltar el punto final. Hagámosla roja.

  IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].DisableHighlight(); } fromCell.EnableHighlight(Color.blue); toCell.EnableHighlight(Color.red); … } 


Puntos finales de una ruta potencial

Limitar búsqueda


En este punto, nuestro algoritmo de búsqueda aún calcula las distancias a todas las celdas a las que se puede llegar desde la celda inicial. Pero ya no lo necesitamos. Podemos detenernos tan pronto como encontremos la distancia final a la celda final. Es decir, cuando la celda actual es finita, podemos salir del ciclo del algoritmo.

  while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); if (current == toCell) { break; } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } } 


Pare en el punto final

¿Qué sucede si no se puede alcanzar el punto final?
Luego, el algoritmo continuará funcionando hasta que encuentre todas las celdas accesibles. Sin la posibilidad de una salida prematura, funcionará como el antiguo método FindDistancesTo .

Visualización de la ruta


Podemos encontrar la distancia entre el principio y el final del camino, pero aún no sabemos cuál será el camino real. Para encontrarlo, necesita rastrear cómo se llega a cada celda. ¿Pero cómo hacerlo?

Al agregar una celda al borde, hacemos esto porque es un vecino de la celda actual. La única excepción es la celda inicial. Todas las otras celdas se han alcanzado a través de la celda actual. Si hacemos un seguimiento de qué celda se llegó a cada celda, como resultado obtenemos una red de celdas. Más precisamente, una red en forma de árbol, cuya raíz es el punto de partida. Podemos usarlo para construir el camino después de llegar al punto final.


Red de árboles que describe caminos hacia el centro

Podemos guardar esta información agregando un enlace a otra celda en HexCell . No necesitamos serializar estos datos, por lo que utilizamos la propiedad estándar para esto.

  public HexCell PathFrom { get; set; } 

En HexGrid.Search establezca el valor PathFrom del vecino en la celda actual al agregarlo al borde. Además, debemos cambiar este enlace cuando encontremos un camino más corto hacia el vecino.

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; neighbor.PathFrom = current; } 

Una vez alcanzado el punto final, podemos visualizar la ruta siguiendo estos enlaces de regreso a la celda inicial y seleccionándolos.

  if (current == toCell) { current = current.PathFrom; while (current != fromCell) { current.EnableHighlight(Color.white); current = current.PathFrom; } break; } 


Ruta encontrada

Vale la pena considerar que a menudo hay varios caminos más cortos. El encontrado depende del orden de procesamiento de las celdas. Algunos caminos pueden verse bien, otros pueden ser malos, pero nunca hay un camino más corto. Volveremos a esto más tarde.

Cambiar inicio de búsqueda


Después de seleccionar el punto de inicio, cambiar el punto final desencadenará una nueva búsqueda. Lo mismo debería suceder al elegir una nueva celda inicial. Para que esto sea posible, HexMapEditor también debe recordar el punto final.

  HexCell previousCell, searchFromCell, searchToCell; 

Usando este campo, también podemos iniciar una nueva búsqueda al elegir un nuevo comienzo.

  else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell); } 

Además, debemos evitar los mismos puntos de inicio y finalización.

  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { … } 

paquete de la unidad

Búsqueda más inteligente


Aunque nuestro algoritmo encuentra el camino más corto, pasa mucho tiempo explorando puntos que obviamente no formarán parte de este camino. Al menos es obvio para nosotros. El algoritmo no puede mirar hacia abajo en el mapa; no puede ver que una búsqueda en algunas direcciones no tendrá sentido. Prefiere moverse por las carreteras, a pesar de que se dirigen en la dirección opuesta desde el punto final. ¿Es posible hacer la búsqueda más inteligente?

Por el momento, al elegir la celda que se procesará a continuación, consideramos solo la distancia desde la celda hasta el comienzo. Si queremos ser más inteligentes, también debemos considerar la distancia hasta el punto final. Desafortunadamente, aún no lo conocemos. Pero podemos crear una estimación de la distancia restante. Agregar esta estimación a la distancia a la celda nos da una idea de la longitud total de la ruta que pasa por esta celda. Entonces podemos usarlo para priorizar las búsquedas de celdas.

Búsqueda heurística


Cuando usamos estimaciones o conjeturas en lugar de datos exactamente conocidos, esto se llama usar heurística de búsqueda. Esta heurística representa la mejor suposición de la distancia restante. Debemos determinar este valor para cada celda que estamos buscando, por lo que agregaremos una propiedad entera HexCell . No necesitamos serializarlo, por lo que bastará con otra propiedad estándar.

  public int SearchHeuristic { get; set; } 

¿Cómo hacemos una suposición sobre la distancia restante? En el caso más ideal, tendremos un camino que conduce directamente al punto final. Si es así, la distancia es igual a la distancia sin cambios entre las coordenadas de esta celda y la celda final. Aprovechemos esto en nuestra heurística.

Dado que la heurística no depende de una ruta recorrida anteriormente, es constante en el proceso de búsqueda. Por lo tanto, necesitamos calcularlo solo una vez cuando HexGrid.Search agrega una celda al borde.

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); frontier.Add(neighbor); } 

Prioridad de búsqueda


A partir de ahora, determinaremos la prioridad de la búsqueda en función de la distancia a la celda más sus heurísticas. HexCell una propiedad para este valor en HexCell .

  public int SearchPriority { get { return distance + SearchHeuristic; } } 

Para que esto funcione, HexGrid.Search para que use esta propiedad para ordenar el borde.

  frontier.Sort( (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) ); 



Buscar sin heurística y con heurística

Heurística válida


Gracias a las nuevas prioridades de búsqueda, como resultado, visitaremos menos celdas. Sin embargo, en un mapa uniforme, el algoritmo aún procesa celdas que están en la dirección incorrecta. Esto se debe a que, por defecto, los costos para cada paso de movimiento son 5, y la heurística por paso agrega solo 1. Es decir, la influencia de la heurística no es muy fuerte.

Si los costos de mover todas las tarjetas son iguales, entonces podemos usar los mismos costos para determinar la heurística. En nuestro caso, esta será la heurística actual multiplicada por 5. Esto reducirá significativamente el número de celdas procesadas.


Usando heurística × 5

Sin embargo, si hay carreteras en el mapa, podemos sobreestimar la distancia restante. Como resultado, el algoritmo puede cometer errores y crear una ruta que en realidad no es la más corta.



Heurística sobrevalorada y válida

Para garantizar que se encuentre el camino más corto, debemos asegurarnos de que nunca sobreestimamos la distancia restante. Este enfoque se llama heurística válida. Dado que el costo mínimo de mudanza es 1, no tenemos más remedio que usar los mismos costos para determinar la heurística.

Estrictamente hablando, es bastante normal usar costos aún más bajos, pero esto solo debilitará la heurística. El mínimo heurístico posible es cero, lo que nos da solo el algoritmo de Dijkstra. Con una heurística distinta de cero, el algoritmo se llama A * (pronunciado "A star").

¿Por qué se llama A *?
Niels Nilsson propuso por primera vez la idea de agregar heurística al algoritmo de Dijkstra. Llamó a su versión A1. Bertram Rafael más tarde se le ocurrió la mejor versión que llamó A2. Entonces Peter Hart demostró que con una buena heurística A2 es óptimo, es decir, no puede haber una versión mejor. Esto lo obligó a llamar al algoritmo A * para mostrar que no podía mejorarse, es decir, A3 o A4 no aparecerían. Entonces sí, el algoritmo A * es lo mejor que podemos obtener, pero es tan bueno como heurístico.

paquete de la unidad

Cola de prioridad


Aunque A * es un buen algoritmo, nuestra implementación no es tan efectiva, porque para almacenar el borde, utilizamos una lista que debe ordenarse en cada iteración. Como se mencionó en la parte anterior, necesitamos una cola prioritaria, pero su implementación estándar no existe. Por lo tanto, creémoslo usted mismo.

Nuestro turno debe apoyar la operación de configuración y exclusión de la cola en función de la prioridad. También debería permitir cambiar la prioridad de una celda que ya está en la cola. Idealmente, lo implementamos, minimizando la búsqueda de clasificación y memoria asignada. Además, debería seguir siendo simple.

Crea tu propia cola


Cree una nueva clase HexCellPriorityQueue con los métodos comunes requeridos. Usamos una lista simple para rastrear el contenido de una cola. Además, agregaremos el método Clear para borrar la cola y poder usarla repetidamente.

 using System.Collections.Generic; public class HexCellPriorityQueue { List<HexCell> list = new List<HexCell>(); public void Enqueue (HexCell cell) { } public HexCell Dequeue () { return null; } public void Change (HexCell cell) { } public void Clear () { list.Clear(); } } 

Almacenamos las prioridades de las células en las propias células. Es decir, antes de agregar una celda a la cola, se debe establecer su prioridad. Pero en caso de un cambio de prioridad, probablemente será útil saber cuál era la prioridad anterior. Así que agreguemos esto a Change como parámetro.

  public void Change (HexCell cell, int oldPriority) { } 

También es útil saber cuántas celdas hay en la cola, así que agreguemos la propiedad Count para esto. Simplemente use el campo para el que realizaremos el incremento y la disminución correspondientes.

  int count = 0; public int Count { get { return count; } } public void Enqueue (HexCell cell) { count += 1; } public HexCell Dequeue () { count -= 1; return null; } … public void Clear () { list.Clear(); count = 0; } 

Agregar a la cola


Cuando se agrega una celda a la cola, primero usemos su prioridad como índice, tratando la lista como una matriz simple.

  public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; list[priority] = cell; } 

Sin embargo, esto solo funciona si la lista es lo suficientemente larga, de lo contrario, iremos más allá de las fronteras. Puede evitar esto agregando elementos vacíos a la lista hasta que alcance la longitud requerida. Estos elementos vacíos no hacen referencia a la celda, por lo que puede crearlos agregando null a la lista.

  int priority = cell.SearchPriority; while (priority >= list.Count) { list.Add(null); } list[priority] = cell; 


Lista con agujeros

Pero así es como almacenamos solo una celda por prioridad, y lo más probable es que haya varias. Para rastrear todas las celdas con la misma prioridad, necesitamos usar otra lista. Aunque podemos usar una lista real para cada prioridad, también podemos agregar una propiedad a HexCell para unirlas. Esto nos permite crear una cadena de celdas llamada lista vinculada.

  public HexCell NextWithSamePriority { get; set; } 

Para crear una cadena, deje que HexCellPriorityQueue.Enqueue obligue a la celda recién agregada a referirse al valor actual con la misma prioridad, antes de eliminarlo.

  cell.NextWithSamePriority = list[priority]; list[priority] = cell; 


Lista de listas vinculadas

Eliminar de la cola


Para obtener una celda de una cola prioritaria, necesitamos acceder a la lista vinculada en el índice no vacío más bajo. Por lo tanto, recorreremos la lista en un ciclo hasta que la encontremos. Si no lo encontramos, entonces la cola está vacía y devolvemos null .

De la cadena encontrada, podemos devolver cualquier celda, porque todas tienen la misma prioridad. La forma más fácil es devolver la celda desde el comienzo de la cadena.

  public HexCell Dequeue () { count -= 1; for (int i = 0; i < list.Count; i++) { HexCell cell = list[i]; if (cell != null) { return cell; } } return null; } 

Para mantener el enlace a la cadena restante, use la siguiente celda con la misma prioridad que el nuevo inicio. Si solo había una celda en este nivel de prioridad, el elemento se vuelve null y se omitirá en el futuro.

  if (cell != null) { list[i] = cell.NextWithSamePriority; return cell; } 

Seguimiento mínimo


Este enfoque funciona, pero itera a través de la lista cada vez que se recibe una celda. No podemos evitar encontrar el índice no vacío más pequeño, pero no estamos obligados a comenzar desde cero cada vez. En cambio, podemos rastrear la prioridad mínima y comenzar la búsqueda con ella. Inicialmente, el mínimo es esencialmente igual al infinito.

  int minimum = int.MaxValue; … public void Clear () { list.Clear(); count = 0; minimum = int.MaxValue; } 

Al agregar una celda a la cola, cambiamos el mínimo según sea necesario.

  public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; if (priority < minimum) { minimum = priority; } … } 

Y cuando nos retiramos de la cola, usamos al menos la lista para las iteraciones, y no comenzamos desde cero.

  public HexCell Dequeue () { count -= 1; for (; minimum < list.Count; minimum++) { HexCell cell = list[minimum]; if (cell != null) { list[minimum] = cell.NextWithSamePriority; return cell; } } return null; } 

Esto reduce significativamente la cantidad de tiempo que lleva pasar por alto en el bucle de lista de prioridades.

Cambiar prioridades


Al cambiar la prioridad de una celda, debe eliminarse de la lista vinculada de la que forma parte. Para hacer esto, debemos seguir la cadena hasta que la encontremos.

Comencemos declarando que el encabezado de la lista de prioridades anterior será la celda actual, y también rastrearemos la siguiente celda. Podemos tomar inmediatamente la siguiente celda, porque sabemos que hay al menos una celda por este índice.

  public void Change (HexCell cell, int oldPriority) { HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; } 

Si la celda actual es una celda cambiada, entonces esta es la celda principal y podemos cortarla como si la hubiéramos sacado de la cola.

  HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; if (current == cell) { list[oldPriority] = next; } 

Si este no es el caso, entonces debemos seguir la cadena hasta que estemos en la celda frente a la celda cambiada. Contiene un enlace a la celda que se ha modificado.

  if (current == cell) { list[oldPriority] = next; } else { while (next != cell) { current = next; next = current.NextWithSamePriority; } } 

En este punto, podemos eliminar la celda modificada de la lista vinculada, omitiéndola.

  while (next != cell) { current = next; next = current.NextWithSamePriority; } current.NextWithSamePriority = cell.NextWithSamePriority; 

Después de eliminar una celda, debe agregarla nuevamente para que aparezca en la lista de su nueva prioridad.

  public void Change (HexCell cell, int oldPriority) { … Enqueue(cell); } 

El método Enqueue incrementa el contador, pero en realidad no estamos agregando una nueva celda. Por lo tanto, para compensar esto, tendremos que disminuir el contador.

  Enqueue(cell); count -= 1; 

Uso de la cola


Ahora podemos aprovechar nuestra cola prioritaria en HexGrid . Esto se puede hacer con una sola instancia, reutilizable para todas las búsquedas.

  HexCellPriorityQueue searchFrontier; … IEnumerator Search (HexCell fromCell, HexCell toCell) { if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } … } 

Antes de comenzar el ciclo, el método Searchprimero debe agregarse a la cola fromCell, y cada iteración comienza con la salida de la celda desde la cola. Esto reemplazará el antiguo código de borde.

  WaitForSeconds delay = new WaitForSeconds(1 / 60f); // List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; // frontier.Add(fromCell); searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { yield return delay; HexCell current = searchFrontier.Dequeue(); // frontier.RemoveAt(0); … } 

Cambie el código para que agregue y cambie el vecino. Antes del cambio recordaremos la antigua prioridad.

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); // frontier.Add(neighbor); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } 

Además, ya no necesitamos ordenar el borde.

 // frontier.Sort( // (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) // ); 


Buscar usando una cola de prioridad

Como se mencionó anteriormente, la ruta más corta encontrada depende del orden de procesamiento de las celdas. Nuestro turno crea un orden diferente del orden de la lista ordenada, para que podamos obtener otras formas. Como agregamos y eliminamos del encabezado de la lista vinculada para cada prioridad, se parecen más a las pilas que a las colas. Las celdas agregadas en último lugar se procesan primero. Un efecto secundario de este enfoque es que el algoritmo es propenso a zigzag. Por lo tanto, la probabilidad de caminos en zigzag también aumenta. Afortunadamente, tales caminos generalmente se ven mejor, por lo que este efecto secundario está a nuestro favor.



Lista y cola ordenadas con prioridad de

unitpackage

Parte 17: movimiento limitado


  • Encontramos formas de movimiento paso a paso.
  • Mostrar inmediatamente el camino.
  • Creamos una búsqueda más efectiva.
  • Visualizamos solo el camino.

En esta parte dividiremos el movimiento en movimientos y aceleraremos la búsqueda tanto como sea posible.


Viaja desde varios movimientos

Movimiento paso a paso


Los juegos de estrategia que usan redes hexagonales casi siempre se basan en turnos. Las unidades que se mueven en el mapa tienen una velocidad limitada, lo que limita la distancia recorrida en un turno.

Velocidad


Para proporcionar soporte para movimientos limitados, agregamos dentro HexGrid.FindPathy dentro del HexGrid.Searchparámetro entero speed. Determina el rango de movimiento para un movimiento.

  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell, speed)); } IEnumerator Search (HexCell fromCell, HexCell toCell, int speed) { … } 

Los diferentes tipos de unidades en el juego usan diferentes velocidades. La caballería es rápida, la infantería es lenta, y así sucesivamente. Todavía no tenemos unidades, así que por ahora usaremos una velocidad constante. Tomemos un valor de 24. Este es un valor bastante grande, no divisible por 5 (el costo predeterminado de la mudanza). Añadir un argumento a favor FindPathde HexMapEditor.HandleInputuna velocidad constante.

  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); } 

Movimientos


Además de rastrear el costo total de moverse a lo largo del camino, ahora también necesitamos saber cuántos movimientos se necesitarán para avanzar. Pero no necesitamos almacenar esta información en cada celda. Se puede obtener dividiendo la distancia recorrida por la velocidad. Como estos son enteros, utilizaremos la división de enteros. Es decir, las distancias totales que no exceden 24 corresponden al curso 0. Esto significa que todo el camino puede completarse en el curso actual. Si el punto final está a una distancia de 30, entonces este debe ser el turno 1. Para llegar al punto final, la unidad tendrá que gastar todo su movimiento en el turno actual y en parte del siguiente turno.

Determinemos el curso de la celda actual y todos sus vecinos dentroHexGrid.Search. El curso de la celda actual se puede calcular solo una vez, justo antes de dar la vuelta en el ciclo vecino. El movimiento del vecino se puede determinar tan pronto como encontremos la distancia a él.

  int currentTurn = current.Distance / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else if (current.Walled != neighbor.Walled) { continue; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int turn = distance / speed; … } 

Movimiento perdido


Si el movimiento del vecino es mayor que el movimiento actual, entonces cruzamos el límite del movimiento. Si el movimiento necesario para llegar a un vecino era 1, entonces todo está bien. Pero si pasar a la siguiente celda es más costoso, entonces todo se vuelve más complicado.

Supongamos que nos movemos a lo largo de un mapa homogéneo, es decir, para entrar en cada celda necesita 5 unidades de movimiento. Nuestra velocidad es 24. Después de cuatro pasos, gastamos 20 unidades de nuestro stock de movimiento, y quedan 4. En el siguiente paso, se necesitan nuevamente 5 unidades, es decir, una más que las disponibles. ¿Qué necesitamos hacer en esta etapa?

Hay dos enfoques para esta situación. El primero es permitir que la unidad ingrese a la quinta celda en el turno actual, incluso si no tenemos suficiente movimiento. El segundo es prohibir el movimiento durante el movimiento actual, es decir, los puntos de movimiento restantes no se pueden usar y se perderán.

La elección de la opción depende del juego. En general, el primer enfoque es más apropiado para juegos en los que las unidades pueden moverse solo unos pocos pasos por turno, por ejemplo, para juegos de la serie Civilization. Esto asegura que las unidades siempre puedan mover al menos una celda por turno. Si las unidades pueden mover muchas celdas por turno, como en Age of Wonders o en Battle for Wesnoth, entonces la segunda opción es mejor.

Como usamos la velocidad 24, elijamos el segundo enfoque. Para que comience a funcionar, necesitamos aislar los costos de ingresar a la siguiente celda antes de agregarlo a la distancia actual.

 // int distance = current.Distance; int moveCost; if (current.HasRoadThroughEdge(d)) { moveCost = 1; } else if (current.Walled != neighbor.Walled) { continue; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int distance = current.Distance + moveCost; int turn = distance / speed; 

Si como resultado cruzamos el borde del movimiento, entonces primero usamos todos los puntos de movimiento del movimiento actual. Podemos hacer esto simplemente multiplicando el movimiento por la velocidad. Después de eso, agregamos el costo de la mudanza.

  int distance = current.Distance + moveCost; int turn = distance / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } 

Como resultado de esto, completaremos el primer movimiento en la cuarta celda con 4 puntos de movimiento no utilizados. Estos puntos perdidos se suman a los costos de la quinta celda, por lo que su distancia se convierte en 29, no en 25. Como resultado, las distancias son mayores que antes. Por ejemplo, la décima celda tenía una distancia de 50. Pero ahora para entrar en ella, necesitamos cruzar los bordes de dos movimientos, perdiendo 8 puntos de movimiento, es decir, la distancia a ella ahora se convierte en 58.


Más largo de lo esperado

Dado que los puntos de movimiento no utilizados se agregan a las distancias a las celdas, se tienen en cuenta al determinar la ruta más corta. La forma más efectiva es desperdiciar la menor cantidad de puntos posible. Por lo tanto, a diferentes velocidades, podemos obtener diferentes caminos.

Mostrando movimientos en lugar de distancias


Cuando jugamos el juego, no estamos muy interesados ​​en los valores de distancia utilizados para encontrar el camino más corto. Estamos interesados ​​en la cantidad de movimientos necesarios para llegar al punto final. Por lo tanto, en lugar de distancias, vamos a mostrar los movimientos.

Primero, deshazte de UpdateDistanceLabelsu llamada HexCell.

  public int Distance { get { return distance; } set { distance = value; // UpdateDistanceLabel(); } } … // void UpdateDistanceLabel () { // UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); // label.text = distance == int.MaxValue ? "" : distance.ToString(); // } 

En cambio, agregaremos al HexCellmétodo general SetLabelque recibe una cadena arbitraria.

  public void SetLabel (string text) { UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); label.text = text; } 

Utilizamos este nuevo método en la HexGrid.Searchlimpieza de celdas. Para ocultar celdas, simplemente asígnelas null.

  for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); } 

Luego asignamos a la marca del vecino el valor de su movimiento. Después de eso, podremos ver cuántos movimientos adicionales se necesitarán para llegar hasta el final.

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } 


El número de movimientos necesarios para moverse a lo largo de la ruta del paquete de la

unidad.

Caminos instantáneos


Además, cuando jugamos el juego, no nos importa cómo el algoritmo de búsqueda de ruta encuentra el camino. Queremos ver la ruta solicitada de inmediato. Por el momento, podemos estar seguros de que el algoritmo funciona, así que eliminemos la visualización de búsqueda.

Sin corutina


Para un paso lento a través del algoritmo, usamos corutina. Ya no necesitamos hacer esto, así que nos libraremos de las llamadas StartCoroutiney StopAllCoroutinesc HexGrid. En cambio, simplemente lo invocamos Searchcomo un método regular.

  public void Load (BinaryReader reader, int header) { // StopAllCoroutines(); … } public void FindPath (HexCell fromCell, HexCell toCell, int speed) { // StopAllCoroutines(); // StartCoroutine(Search(fromCell, toCell, speed)); Search(fromCell, toCell, speed); } 

Como ya no lo usamos Searchcomo rutina, no necesita rendimiento, por lo que eliminaremos este operador. Esto significa que también eliminaremos la declaración WaitForSecondsy cambiaremos el tipo de retorno del método void.

  void Search (HexCell fromCell, HexCell toCell, int speed) { … // WaitForSeconds delay = new WaitForSeconds(1 / 60f); fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { // yield return delay; HexCell current = searchFrontier.Dequeue(); … } } 


Resultados instantáneos

Definición del tiempo de búsqueda


Ahora podemos obtener las rutas al instante, pero ¿qué tan rápido se calculan? Las rutas cortas aparecen casi de inmediato, pero las rutas largas en mapas grandes pueden parecer un poco lentas.

Midamos cuánto tiempo lleva encontrar y mostrar el camino. Podemos usar un generador de perfiles para determinar el tiempo de búsqueda, pero esto es demasiado y crea costos adicionales. Usemos en su lugar Stopwatch, que está en el espacio de nombres System.Diagnostics. Como lo usamos solo temporalmente, no agregaré la construcción usingal comienzo del script.

Justo antes de la búsqueda, cree un nuevo cronómetro e inícielo. Una vez completada la búsqueda, detenga el cronómetro y muestre el tiempo transcurrido en la consola.

  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); } 

Elija el peor de los casos para nuestro algoritmo: una búsqueda desde la esquina inferior izquierda hasta la esquina superior derecha de un mapa grande. Lo peor es un mapa uniforme, porque el algoritmo tendrá que procesar las 4.800 celdas del mapa.


Buscar en el peor de los casos El tiempo

dedicado a buscarlo puede ser diferente, porque el editor de Unity no es el único proceso que se ejecuta en su máquina. Por lo tanto, pruébelo varias veces para comprender la duración promedio. En mi caso, la búsqueda toma alrededor de 45 milisegundos. Esto no es mucho y corresponde a 22.22 rutas por segundo; denota esto como 22 pps (rutas por segundo). Esto significa que la velocidad de fotogramas del juego también disminuirá en un máximo de 22 fps en ese fotograma cuando se calcule esta ruta. Y esto sin tener en cuenta todos los demás trabajos, por ejemplo, renderizar el marco en sí. Es decir, obtenemos una disminución bastante grande en la velocidad de fotogramas, se reducirá a 20 fps.

Al realizar una prueba de rendimiento de este tipo, debe tener en cuenta que el rendimiento del editor de Unity no será tan alto como el rendimiento de la aplicación finalizada. Si realizo la misma prueba con el ensamblaje, en promedio solo tomará 15 ms. Eso es 66 pps, que es mucho mejor. Sin embargo, esto sigue siendo una gran parte de los recursos asignados por fotograma, por lo que la velocidad de fotogramas será inferior a 60 fps.

¿Dónde puedo ver el registro de depuración para el ensamblado?
Unity , . . , , Unity Log Files .

Busque solo si es necesario.


Podemos hacer una optimización simple: realice una búsqueda solo cuando sea necesario. Mientras iniciamos una nueva búsqueda en cada cuadro en el que se mantiene presionado el botón del mouse. Por lo tanto, la velocidad de fotogramas se subestimará constantemente al arrastrar y soltar. Podemos evitar esto iniciando una nueva búsqueda HexMapEditor.HandleInputsolo cuando realmente estamos tratando con un nuevo punto final. De lo contrario, la ruta visible actual sigue siendo válida.

  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell != currentCell) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } } else if (searchFromCell && searchFromCell != currentCell) { if (searchToCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); } } 

Mostrar etiquetas solo para la ruta


Mostrar marcas de viaje es una operación bastante costosa, especialmente porque usamos un enfoque no optimizado. Realizar esta operación para todas las celdas definitivamente ralentizará la ejecución. Así que saltemos el etiquetado HexGrid.Search.

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; // neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; // neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } 

Necesitamos ver esta información solo para la ruta encontrada. Por lo tanto, después de llegar al punto final, calcularemos el curso y estableceremos las etiquetas de solo aquellas celdas que están en camino.

  if (current == toCell) { current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } break; } 


Mostrar etiquetas solo para celdas de ruta

Ahora incluimos solo etiquetas de celdas entre el inicio y el final. Pero el punto final es lo más importante, también debemos establecer una etiqueta para ello. Puede hacerlo iniciando el ciclo de ruta desde la celda de destino, y no desde la celda frente a ella. En este caso, la iluminación del punto final de rojo cambiará a blanco, por lo que eliminaremos su retroiluminación durante el ciclo.

  fromCell.EnableHighlight(Color.blue); // toCell.EnableHighlight(Color.red); fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); if (current == toCell) { // current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } toCell.EnableHighlight(Color.red); break; } … } 


La información de progreso es más importante para el punto final.

Después de estos cambios, el peor de los casos se reduce a 23 milisegundos en el editor y a 6 milisegundos en el ensamblaje terminado. Estos son 43 pps y 166 pps, mucho mejor.

paquete de la unidad

La busqueda mas inteligente


En la parte anterior, hicimos el procedimiento de búsqueda más inteligente al implementar el algoritmo A * . Sin embargo, en realidad todavía no estamos realizando la búsqueda de la manera más óptima. En cada iteración, calculamos las distancias desde la celda actual a todos sus vecinos. Esto es cierto para las celdas que aún no son o que actualmente son parte del borde de búsqueda. Pero las celdas que ya se han eliminado del borde ya no necesitan ser consideradas, porque ya hemos encontrado el camino más corto a estas celdas. La implementación correcta de A * omite estas celdas, por lo que podemos hacer lo mismo.

Fase de búsqueda de celda


¿Cómo sabemos si una celda ya ha salido del borde? Si bien no podemos determinar esto. Por lo tanto, debe realizar un seguimiento en qué fase de la búsqueda se encuentra la celda. Todavía no ha estado en la frontera, o está en ella ahora, o está en el extranjero. Podemos rastrear esto agregando a una HexCellpropiedad entera simple.

  public int SearchPhase { get; set; } 

Por ejemplo, 0 significa que las celdas aún no han alcanzado, 1 - que la celda está en el borde ahora, y 2 - que ya se ha eliminado del borde.

Golpeando la frontera


En HexGrid.Searchpodemos restablecer todas las celdas a 0 y usar siempre 1 para el borde. O podemos aumentar el número de bordes con cada nueva búsqueda. Gracias a esto, no tendremos que lidiar con el vertido de celdas si cada vez aumentamos el número de bordes en dos.

  int searchFrontierPhase; … void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; … } 

Ahora necesitamos establecer la fase de búsqueda de celdas al agregarlas al borde. El proceso comienza con una celda inicial, que se agrega al borde.

  fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); 

Y también cada vez que agregamos un vecino a la frontera.

  if (neighbor.Distance == int.MaxValue) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } 

Control de fronteras


Hasta ahora, para verificar que la celda aún no se haya agregado al borde, usamos una distancia igual a int.MaxValue. Ahora podemos comparar la fase de búsqueda de celdas con el borde actual.

 // if (neighbor.Distance == int.MaxValue) { if (neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } 

Esto significa que ya no necesitamos restablecer las distancias de las celdas antes de buscar, es decir, tendremos que hacer menos trabajo, lo cual es bueno.

  for (int i = 0; i < cells.Length; i++) { // cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); } 

Saliendo de la frontera


Cuando una celda se elimina del límite, lo denotamos por un aumento en su fase de búsqueda. Esto la coloca más allá de la frontera actual y antes de la próxima.

  while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; … } 

Ahora podemos omitir celdas eliminadas del borde, evitando el cálculo sin sentido y la comparación de distancias.

  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } … } 

En este punto, nuestro algoritmo aún produce los mismos resultados, pero de manera más eficiente. En mi máquina, la búsqueda en el peor de los casos toma 20 ms en el editor y 5 ms en el ensamblado.

También podemos calcular cuántas veces la celda ha sido procesada por el algoritmo, aumentando el contador al calcular la distancia a la celda. Anteriormente, nuestro algoritmo en el peor de los casos calculaba 28,239 distancias. En el algoritmo A * listo para usar, calculamos sus 14.120 distancias. La cantidad disminuyó en un 50%. El grado de impacto de estos indicadores en la productividad depende de los costos al calcular el costo de la mudanza. En nuestro caso, no hay mucho trabajo aquí, por lo que la mejora en el ensamblaje no es muy grande, pero es muy notable en el editor.

paquete de la unidad

Despejando el camino


Al iniciar una nueva búsqueda, primero debemos borrar la visualización de la ruta anterior. Mientras hacemos esto, apague la selección y elimine las etiquetas de cada celda de la cuadrícula. Este es un enfoque muy difícil. Idealmente, necesitamos descartar solo aquellas celdas que fueron parte de la ruta anterior.

Buscar solo


Comencemos por eliminar completamente el código de visualización de Search. Solo necesita realizar una búsqueda de ruta y no tiene que saber qué haremos con esta información.

  void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } // for (int i = 0; i < cells.Length; i++) { // cells[i].SetLabel(null); // cells[i].DisableHighlight(); // } // fromCell.EnableHighlight(Color.blue); fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { // while (current != fromCell) { // int turn = current.Distance / speed; // current.SetLabel(turn.ToString()); // current.EnableHighlight(Color.white); // current = current.PathFrom; // } // toCell.EnableHighlight(Color.red); // break; } … } } 

Para informar que Searchhemos encontrado una manera, devolveremos boolean.

  bool Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { return true; } … } return false; } 

Recuerda el camino


Cuando se encuentra el camino, debemos recordarlo. Gracias a esto, podremos limpiarlo en el futuro. Por lo tanto, rastrearemos los puntos finales y si hay una ruta entre ellos.

  HexCell currentPathFrom, currentPathTo; bool currentPathExists; … public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); } 

Mostrar el camino nuevamente


Podemos usar los datos de búsqueda que grabamos para visualizar nuevamente la ruta. Creemos un nuevo método para esto ShowPath. Recorrerá el ciclo desde el final hasta el comienzo de la ruta, resaltando las celdas y asignando un valor de trazo a sus etiquetas. Para hacer esto, necesitamos saber la velocidad, así que conviértalo en un parámetro. Si no tenemos una ruta, entonces el método simplemente selecciona los puntos finales.

  void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } } currentPathFrom.EnableHighlight(Color.blue); currentPathTo.EnableHighlight(Color.red); } 

Llame a este método FindPathdespués de la búsqueda.

  currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed); 

Barrer


Vemos el camino nuevamente, pero ahora no se está alejando. Para borrarlo, cree un método ClearPath. De hecho, es una copia ShowPath, excepto que deshabilita la selección y las etiquetas, pero no las incluye. Una vez hecho esto, debe borrar los datos de ruta grabados que ya no son válidos.

  void ClearPath () { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { current.SetLabel(null); current.DisableHighlight(); current = current.PathFrom; } current.DisableHighlight(); currentPathExists = false; } currentPathFrom = currentPathTo = null; } 

Con este método, podemos borrar la visualización de la ruta anterior visitando solo las celdas necesarias, el tamaño del mapa ya no es importante. Lo llamaremos FindPathantes de comenzar una nueva búsqueda.

  sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); if (currentPathExists) { ShowPath(speed); } sw.Stop(); 

Además, despejaremos el camino al crear un nuevo mapa.

  public bool CreateMap (int x, int z) { … ClearPath(); if (chunks != null) { for (int i = 0; i < chunks.Length; i++) { Destroy(chunks[i].gameObject); } } … } 

Y también antes de cargar otra tarjeta.

  public void Load (BinaryReader reader, int header) { ClearPath(); … } 

La visualización de la ruta se borra nuevamente, como antes de este cambio. Pero ahora estamos utilizando un enfoque más eficiente y, en el peor de los casos, el tiempo ha disminuido a 14 milisegundos. Suficiente mejora seria solo debido a una limpieza más inteligente. El tiempo de montaje disminuyó a 3 ms, que es 333 pps. Gracias a esto, la búsqueda de rutas es exactamente aplicable en tiempo real.

Ahora que hemos realizado una búsqueda rápida de rutas, podemos eliminar el código de depuración temporal.

  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { // System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); // sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed); // sw.Stop(); // Debug.Log(sw.ElapsedMilliseconds); } 

paquete de la unidad

Parte 18: unidades


  • Colocamos los escuadrones en el mapa.
  • Guardar y cargar escuadrones.
  • Encontramos formas para las tropas.
  • Movemos las unidades.

Ahora que hemos descubierto cómo buscar un camino, ubiquemos los escuadrones en el mapa.


Llegaron refuerzos

Creando escuadrones


Hasta ahora hemos tratado solo con células y sus objetos fijos. Las unidades difieren de ellas en que son móviles. Un escuadrón puede significar cualquier cosa de cualquier escala, desde una persona o vehículo hasta un ejército completo. En este tutorial, nos restringimos a un tipo simple de unidad generalizada. Después de eso, pasaremos a las combinaciones de apoyo de varios tipos de unidades.

Escuadrón prefabricado


Para trabajar con escuadrones, cree un nuevo tipo de componente HexUnit. Por ahora, comencemos con uno vacío MonoBehavioury luego agreguemos funcionalidad.

 using UnityEngine; public class HexUnit : MonoBehaviour { } 

Crea un objeto de juego vacío con este componente, que debería convertirse en un prefabricado. Este será el objeto raíz del escuadrón.


Escuadrón prefabricado.

Agregue un modelo 3D que simboliza el desprendimiento como un objeto secundario. Utilicé un cubo escalado simple para el que creé material azul. El objeto raíz determina el nivel del suelo del desprendimiento, por lo tanto, desplazamos en consecuencia el elemento hijo.



Elemento de cubo secundario

Agregue un colisionador al escuadrón para que sea más fácil seleccionar en el futuro. El colisionador del cubo estándar es bastante adecuado para nosotros, solo haga que el colisionador encaje en una celda.

Crear instancias de escuadrón


Como todavía no tenemos jugabilidad, la creación de unidades se realiza en el modo de edición. Por lo tanto, esto debe abordarse HexMapEditor. Para hacer esto, necesita un prefabricado, así que agregue un campo HexUnit unitPrefaby conéctelo.

  public HexUnit unitPrefab; 


Conectando el prefabricado

Al crear unidades, las colocaremos en la celda debajo del cursor. Hay HandleInputun código para encontrar esta celda al editar un terreno. Ahora también lo necesitamos para los escuadrones, por lo que moveremos el código correspondiente a un método separado.

  HexCell GetCellUnderCursor () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { return hexGrid.GetCell(hit.point); } return null; } 

Ahora podemos usar este método para HandleInputsimplificarlo.

  void HandleInput () { // Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); // RaycastHit hit; // if (Physics.Raycast(inputRay, out hit)) { // HexCell currentCell = hexGrid.GetCell(hit.point); HexCell currentCell = GetCellUnderCursor(); if (currentCell) { … } else { previousCell = null; } } 

A continuación, agregue un nuevo método CreateUnitque también use GetCellUnderCursor. Si hay una celda, crearemos un nuevo escuadrón.

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { Instantiate(unitPrefab); } } 

Para mantener limpia la jerarquía, usemos la cuadrícula como padre para todos los objetos del juego en los escuadrones.

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); } } 

La forma más fácil de agregar HexMapEditorsoporte para crear unidades es presionando una tecla. Cambie el método Updatepara que llame CreateUnitcuando presiona la tecla U. Al igual que con c HandleInput, esto debería suceder si el cursor no está encima del elemento GUI. Primero, verificaremos si debemos editar el mapa, y si no, verificaremos si debemos agregar un escuadrón. Si es así, entonces llame CreateUnit.

  void Update () { // if ( // Input.GetMouseButton(0) && // !EventSystem.current.IsPointerOverGameObject() // ) { // HandleInput(); // } // else { // previousCell = null; // } if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButton(0)) { HandleInput(); return; } if (Input.GetKeyDown(KeyCode.U)) { CreateUnit(); return; } } previousCell = null; } 


Instancia creada de escuadrón

Colocación de tropas


Ahora podemos crear unidades, pero aparecen en el origen del mapa. Necesitamos ponerlos en el lugar correcto. Para esto, es necesario que las tropas sean conscientes de su posición. Por lo tanto, agregamos a la HexUnitpropiedad que Locationdenota la celda que ocupan. Al establecer la propiedad, cambiaremos la posición del escuadrón para que coincida con la posición de la celda.

  public HexCell Location { get { return location; } set { location = value; transform.localPosition = value.Position; } } HexCell location; 

Ahora HexMapEditor.CreateUnitdebo asignar la posición de la celda del escuadrón debajo del cursor. Entonces las unidades estarán donde deberían.

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; } } 


Escuadrones en el mapa

Orientación de la unidad


Hasta ahora, todas las unidades tienen la misma orientación, lo que parece poco natural. Para revivirlos, agréguelos a la HexUnitpropiedad Orientation. Este es un valor flotante que indica la rotación del escuadrón a lo largo del eje Y en grados. Al configurarlo, cambiaremos la rotación del objeto del juego.

  public float Orientation { get { return orientation; } set { orientation = value; transform.localRotation = Quaternion.Euler(0f, value, 0f); } } float orientation; 

En HexMapEditor.CreateUnitasignar una rotación aleatoria de 0 a 360 grados.

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } } 


Diferentes orientaciones de la unidad.

Un escuadrón por celda


Las unidades se ven bien si no se crean en una celda. En este caso, obtenemos un grupo de cubos de aspecto extraño.


Unidades superpuestas

Algunos juegos permiten la colocación de varias unidades en un solo lugar, otros no. Como es más fácil trabajar con un escuadrón por celda, elegiré esta opción. Esto significa que deberíamos crear un nuevo escuadrón solo cuando la celda actual no esté ocupada. Para que pueda averiguarlo, agregue a la HexCellpropiedad estándar Unit.

  public HexUnit Unit { get; set; } 

Usamos esta propiedad HexUnit.Locationpara que la celda sepa si la unidad está en ella.

  public HexCell Location { get { return location; } set { location = value; value.Unit = this; transform.localPosition = value.Position; } } 

Ahora HexMapEditor.CreateUnitpuede verificar si la celda actual está libre.

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { HexUnit unit = Instantiate(unitPrefab); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } } 

Edición de celdas ocupadas


Inicialmente, las unidades se colocan correctamente, pero todo puede cambiar si sus celdas se editan más tarde. Si la altura de la celda cambia, entonces la unidad que la ocupa colgará sobre ella o se sumergirá en ella.


Escuadrones colgados y ahogados

La solución es verificar la posición del escuadrón después de hacer cambios. Para hacer esto, agregue el método a HexUnit. Hasta ahora, solo estamos interesados ​​en la posición del escuadrón, así que pregúntelo nuevamente.

  public void ValidateLocation () { transform.localPosition = location.Position; } 

Debemos coordinar la posición del desprendimiento al actualizar la celda, lo que sucede cuando se llama a los métodos Refreshu RefreshSelfOnlyobjetos HexCell. Por supuesto, esto es necesario solo cuando realmente hay un desprendimiento en la celda.

  void Refresh () { if (chunk) { chunk.Refresh(); … if (Unit) { Unit.ValidateLocation(); } } } void RefreshSelfOnly () { chunk.Refresh(); if (Unit) { Unit.ValidateLocation(); } } 

Eliminando escuadrones


Además de crear unidades, sería útil destruirlas. Por lo tanto, agregue al HexMapEditormétodo DestroyUnit. Debe verificar si hay un destacamento en la celda debajo del cursor, y si es así, destruir el objeto del juego del destacamento.

  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { Destroy(cell.Unit.gameObject); } } 

Tenga en cuenta que, para llegar al escuadrón, pasamos por la celda. Para interactuar con el escuadrón, simplemente mueva el mouse sobre su celda. Por lo tanto, para que esto funcione, el escuadrón no tiene que tener un colisionador. Sin embargo, agregar un colisionador facilita la selección porque bloquea los rayos que de otra manera colisionarían con la celda detrás del escuadrón.

Usemos Updateuna combinación de Shift + U izquierdo para destruir el escuadrón .

  if (Input.GetKeyDown(KeyCode.U)) { if (Input.GetKey(KeyCode.LeftShift)) { DestroyUnit(); } else { CreateUnit(); } return; } 

En el caso en que creamos y destruimos varias unidades, tengamos cuidado y borremos la propiedad al eliminar la unidad. Es decir, borramos explícitamente el enlace de la celda al escuadrón. Agregue al HexUnitmétodo Dieque se ocupa de esto, así como la destrucción de su propio objeto de juego.

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

Llamaremos a este método HexMapEditor.DestroyUnity no destruiremos el escuadrón directamente.

  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { // Destroy(cell.Unit.gameObject); cell.Unit.Die(); } } 

paquete de la unidad

Guardar y cargar escuadrones


Ahora que podemos tener unidades en el mapa, debemos incluirlas en el proceso de guardar y cargar. Podemos abordar esta tarea de dos maneras. El primero es registrar los datos del escuadrón cuando se graba una celda para que los datos de la celda y el escuadrón se mezclen. La segunda forma es guardar los datos de celda y escuadrón por separado. Aunque parezca que el primer enfoque es más fácil de implementar, el segundo nos proporciona datos más estructurados. Si compartimos los datos, será más fácil trabajar con ellos en el futuro.

Seguimiento de la unidad


Para mantener todas las unidades juntas, necesitamos rastrearlas. Haremos esto agregando a la HexGridlista de unidades. Esta lista debe contener todas las unidades en el mapa.

  List<HexUnit> units = new List<HexUnit>(); 

Al crear o cargar un nuevo mapa, necesitamos deshacernos de todas las unidades en el mapa. Para simplificar este proceso, cree un método ClearUnitsque mate a todos en la lista y lo borre.

  void ClearUnits () { for (int i = 0; i < units.Count; i++) { units[i].Die(); } units.Clear(); } 

Llamamos a este método in CreateMapy in Load. Hagámoslo después de limpiar el camino.

  public bool CreateMap (int x, int z) { … ClearPath(); ClearUnits(); … } … public void Load (BinaryReader reader, int header) { ClearPath(); ClearUnits(); … } 

Agregar escuadrones a la cuadrícula


Ahora, al crear nuevas unidades, necesitamos agregarlas a la lista. Vamos a establecer un método para esto AddUnit, que también se ocupará de la ubicación del escuadrón y los parámetros de su objeto padre.

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

Ahora HexMapEditor.CreatUnitserá suficiente llamar AddUnitcon una nueva instancia del destacamento, su ubicación y orientación aleatoria.

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { // HexUnit unit = Instantiate(unitPrefab); // unit.transform.SetParent(hexGrid.transform, false); // unit.Location = cell; // unit.Orientation = Random.Range(0f, 360f); hexGrid.AddUnit( Instantiate(unitPrefab), cell, Random.Range(0f, 360f) ); } } 

Eliminar escuadrones de la cuadrícula


Agregue un método para eliminar el escuadrón y c HexGrid. Simplemente elimina el escuadrón de la lista y ordena que muera.

  public void RemoveUnit (HexUnit unit) { units.Remove(unit); unit.Die(); } 

Llamamos a este método HexMapEditor.DestroyUnit, en lugar de destruir el escuadrón directamente.

  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { // cell.Unit.Die(); hexGrid.RemoveUnit(cell.Unit); } } 

Unidades de ahorro


Como vamos a mantener todas las unidades juntas, debemos recordar qué celdas ocupan. La forma más confiable es guardar las coordenadas de su ubicación. Para hacer esto posible, agregamos los campos X y Z al HexCoordinatesmétodo Saveque lo escribe.

 using UnityEngine; using System.IO; [System.Serializable] public struct HexCoordinates { … public void Save (BinaryWriter writer) { writer.Write(x); writer.Write(z); } } 

El método Savepara HexUnitahora puede registrar las coordenadas y la orientación del escuadrón. Estos son todos los datos de las unidades que tenemos en este momento.

 using UnityEngine; using System.IO; public class HexUnit : MonoBehaviour { … public void Save (BinaryWriter writer) { location.coordinates.Save(writer); writer.Write(orientation); } } 

Como HexGridrastrea las unidades, su método Saveregistrará los datos de las unidades. Primero, escriba el número total de unidades, y luego repártalas todas en un bucle.

  public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } writer.Write(units.Count); for (int i = 0; i < units.Count; i++) { units[i].Save(writer); } } 

Cambiamos los datos almacenados, por lo que aumentaremos el número de versión SaveLoadMenu.Savea 2. El antiguo código de arranque seguirá funcionando, porque simplemente no leerá los datos del escuadrón. Sin embargo, debe aumentar el número de versión para indicar que hay información de la unidad en el archivo.

  void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(2); hexGrid.Save(writer); } } 

Cargando escuadrones


Como HexCoordinateses una estructura, no tiene mucho sentido agregarle el método habitual Load. Hagámoslo un método estático que lea y devuelva coordenadas almacenadas.

  public static HexCoordinates Load (BinaryReader reader) { HexCoordinates c; cx = reader.ReadInt32(); cz = reader.ReadInt32(); return c; } 

Como el número de unidades es variable, no tenemos unidades preexistentes en las que se puedan cargar datos. Podemos crear nuevas instancias de unidades antes de cargar sus datos, pero esto requerirá que HexGridcreamos instancias de nuevas unidades en el momento del arranque. Entonces es mejor dejarlo HexUnit. También usamos el método estático HexUnit.Load. Comencemos simplemente leyendo estos escuadrones. Para leer el valor del flotador de orientación, utilizamos el método BinaryReader.ReadSingle.

¿Por qué soltero?
float , . , double , . Unity .

  public static void Load (BinaryReader reader) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); } 

El siguiente paso es crear una instancia de un nuevo escuadrón. Sin embargo, para esto necesitamos un enlace al prefabricado de la unidad. Para no complicarlo aún, agreguemos un HexUnitmétodo estático para esto .

  public static HexUnit unitPrefab; 

Para configurar este enlace, usémoslo nuevamente HexGrid, como lo hicimos con la textura de ruido. Cuando necesitemos soportar muchos tipos de unidades, pasaremos a una mejor solución.

  public HexUnit unitPrefab; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; CreateMap(cellCountX, cellCountZ); } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; } } 


Pasamos el prefabricado de la unidad.

Después de conectar el campo, ya no necesitamos un enlace directo a HexMapEditor. En cambio, él puede usar HexUnit.unitPrefab.

 // public HexUnit unitPrefab; … void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { hexGrid.AddUnit( Instantiate(HexUnit.unitPrefab), cell, Random.Range(0f, 360f) ); } } 

Ahora podemos crear una instancia del nuevo escuadrón en HexUnit.Load. En lugar de devolverlo, podemos usar las coordenadas y la orientación cargadas para agregarlo a la cuadrícula. Para hacer esto posible, agregue un parámetro HexGrid.

  public static void Load (BinaryReader reader, HexGrid grid) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); grid.AddUnit( Instantiate(unitPrefab), grid.GetCell(coordinates), orientation ); } 

Al final, HexGrid.Loadcontamos el número de unidades y lo usamos para cargar todas las unidades almacenadas, pasándonos a nosotros mismos como argumento adicional.

  public void Load (BinaryReader reader, int header) { … int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } } 

Por supuesto, esto solo funcionará para guardar archivos con una versión no inferior a 2, en versiones más jóvenes no hay unidades para cargar.

  if (header >= 2) { int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } } 

Ahora podemos cargar correctamente los archivos de la versión 2, por lo tanto, SaveLoadMenu.Loadaumente el número de la versión compatible a 2.

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

paquete de la unidad

Movimiento de tropas


Los escuadrones son móviles, por lo que debemos poder moverlos por el mapa. Ya tenemos un código de búsqueda de ruta, pero hasta ahora lo hemos probado solo para lugares arbitrarios. Ahora necesitamos eliminar la antigua interfaz de usuario de prueba y crear una nueva interfaz de usuario para la administración de escuadrones.

Limpieza del editor de mapas


Mover unidades a lo largo de los caminos es parte del juego, no se aplica al editor de mapas. Por lo tanto, eliminaremos HexMapEditortodo el código asociado con la búsqueda de la ruta.

 // HexCell previousCell, searchFromCell, searchToCell; HexCell previousCell; … void HandleInput () { HexCell currentCell = GetCellUnderCursor(); if (currentCell) { if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } if (editMode) { EditCells(currentCell); } // else if ( // Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell // ) { // if (searchFromCell != currentCell) { // if (searchFromCell) { // searchFromCell.DisableHighlight(); // } // searchFromCell = currentCell; // searchFromCell.EnableHighlight(Color.blue); // if (searchToCell) { // hexGrid.FindPath(searchFromCell, searchToCell, 24); // } // } // } // else if (searchFromCell && searchFromCell != currentCell) { // if (searchToCell != currentCell) { // searchToCell = currentCell; // hexGrid.FindPath(searchFromCell, searchToCell, 24); // } // } previousCell = currentCell; } else { previousCell = null; } } 

Después de eliminar este código, ya no tiene sentido dejar el editor activo cuando no estamos en modo de edición. Por lo tanto, en lugar de un campo de seguimiento de modo, simplemente podemos habilitar o deshabilitar el componente HexMapEditor. Además, el editor ahora no tiene que lidiar con las etiquetas de la interfaz de usuario.

 // bool editMode; … public void SetEditMode (bool toggle) { // editMode = toggle; // hexGrid.ShowUI(!toggle); enabled = toggle; } … void HandleInput () { HexCell currentCell = GetCellUnderCursor(); if (currentCell) { if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } // if (editMode) { EditCells(currentCell); // } previousCell = currentCell; } else { previousCell = null; } } 

Como por defecto no estamos en el modo de edición de mapas, en Despertar desactivaremos el editor.

  void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); SetEditMode(false); } 

Usar raycast para buscar la celda actual debajo del cursor es necesario al editar el mapa y para administrar unidades. Quizás en el futuro nos sea útil para otra cosa. Pasemos la lógica de la emisión de rayos HexGrida un nuevo método GetCellcon un parámetro de haz.

  public HexCell GetCell (Ray ray) { RaycastHit hit; if (Physics.Raycast(ray, out hit)) { return GetCell(hit.point); } return null; } 

HexMapEditor.GetCellUniderCursor puede llamar a este método con el haz del cursor.

  HexCell GetCellUnderCursor () { return hexGrid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); } 

Game UI


Para controlar la interfaz de usuario del modo de juego, utilizaremos un nuevo componente. Si bien solo se ocupará de la selección y el movimiento de las unidades. Cree un nuevo tipo de componente para él HexGameUI. Un enlace a la red es suficiente para que él haga su trabajo.

 using UnityEngine; using UnityEngine.EventSystems; public class HexGameUI : MonoBehaviour { public HexGrid grid; } 

Agregue este componente al nuevo objeto del juego en la jerarquía de la interfaz de usuario. No tiene que tener su propio objeto, pero será obvio para nosotros que hay una interfaz de usuario separada para el juego.



Juego UI Object

Agregue un HexGameUImétodo SetEditMode, como en HexMapEditor. La interfaz de usuario del juego debe estar activada cuando no estamos en modo de edición. Además, las etiquetas deben incluirse aquí porque la interfaz de usuario del juego funciona con rutas.

  public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); } 

Agregue el método de interfaz de usuario del juego con la lista de eventos del interruptor de modo de edición. Esto significará que cuando el jugador cambia el modo, se llaman ambos métodos.


Varios métodos de eventos.

Rastrear celda actual


Dependiendo de la situación, HexGameUInecesita saber qué celda está actualmente debajo del cursor. Por lo tanto, le agregamos un campo currentCell.

  HexCell currentCell; 

Cree un método UpdateCurrentCellque use el HexGrid.GetCellhaz del cursor para actualizar este campo.

  void UpdateCurrentCell () { currentCell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); } 

Al actualizar la celda actual, es posible que necesitemos averiguar si ha cambiado. Fuerza para UpdateCurrentCelldevolver esta información.

  bool UpdateCurrentCell () { HexCell cell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); if (cell != currentCell) { currentCell = cell; return true; } return false; } 

Selección de unidad


Antes de mover un escuadrón, debe seleccionarse y rastrearse. Por lo tanto, agregue un campo selectedUnit.

  HexUnit selectedUnit; 

Cuando intentamos hacer una selección, debemos comenzar actualizando la celda actual. Si la celda actual es, entonces la unidad que ocupa esta celda se convierte en la unidad seleccionada. Si no hay unidad en la celda, entonces no se selecciona ninguna unidad. Creemos un método para esto DoSelection.

  void DoSelection () { UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } } 

Nos damos cuenta de la elección de las unidades con un simple clic del mouse. Por lo tanto, agregamos un método Updateque realiza una selección cuando se activa el botón del mouse. Por supuesto, necesitamos ejecutarlo solo cuando el cursor no está sobre el elemento GUI.

  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } } } 

En esta etapa, aprendimos cómo seleccionar una unidad a la vez con el clic de un mouse. Cuando hace clic en una celda vacía, se elimina la selección de cualquier unidad. Pero mientras no recibamos ninguna confirmación visual de esto.

Búsqueda de escuadrones


Cuando se selecciona una unidad, podemos usar su ubicación como punto de partida para encontrar una ruta. Para activar esto, no necesitaremos otro clic del botón del mouse. En su lugar, buscaremos y mostraremos automáticamente el camino entre la posición del escuadrón y la celda actual. Siempre haremos esto Update, excepto cuando se haga la elección. Para hacer esto, cuando tenemos un destacamento, llamamos al método DoPathfinding.

  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { DoPathfinding(); } } } 

DoPathfindingsolo actualiza la celda actual y llama HexGrid.FindPathsi hay un punto final. Nuevamente usamos una velocidad constante de 24.

  void DoPathfinding () { UpdateCurrentCell(); grid.FindPath(selectedUnit.Location, currentCell, 24); } 

Tenga en cuenta que no debemos encontrar una nueva ruta cada vez que actualizamos, sino solo cuando cambia la celda actual.

  void DoPathfinding () { if (UpdateCurrentCell()) { grid.FindPath(selectedUnit.Location, currentCell, 24); } } 


Encontrar un camino para un escuadrón

Ahora vemos los caminos que aparecen cuando mueve el cursor después de seleccionar un escuadrón. Gracias a esto, es obvio qué unidad está seleccionada. Sin embargo, las rutas no siempre se borran correctamente. Primero, borremos la ruta anterior si el cursor está fuera del mapa.

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

Por supuesto, esto requiere que HexGrid.ClearPathsea ​​común, por lo que hacemos un cambio.

  public void ClearPath () { … } 

En segundo lugar, despejaremos el viejo camino al elegir un destacamento.

  void DoSelection () { grid.ClearPath(); UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } } 

Finalmente, despejaremos el camino al cambiar el modo de edición.

  public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); } 

Buscar solo puntos finales válidos


No siempre podemos encontrar el camino, porque a veces es imposible llegar a la celda final. Esto es normal Pero a veces la celda final en sí misma es inaceptable. Por ejemplo, decidimos que los caminos no pueden incluir células submarinas. Pero puede depender de la unidad. Agreguemos a un HexUnitmétodo que nos dice si una celda es un punto final válido. Las células submarinas no lo son.

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

Además, permitimos que solo una unidad permanezca en la celda. Por lo tanto, la celda final no será válida si está ocupada.

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

Usamos este método HexGameUI.DoPathfindingpara ignorar los puntos finales no válidos.

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

Moverse al punto final


Si tenemos una ruta válida, entonces podemos mover el escuadrón al punto final. HexGridsabe cuándo se puede hacer esto. Hacemos que pase esta información en una nueva propiedad de solo lectura HasPath.

  public bool HasPath { get { return currentPathExists; } } 

Para mover un escuadrón, agregue al HexGameUImétodo DoMove. Este método se llamará cuando se emita un comando y si se selecciona una unidad. Por lo tanto, debe verificar si hay una manera y, de ser así, cambiar la ubicación del destacamento. Mientras teletransportamos de inmediato al escuadrón hasta el punto final. En uno de los siguientes tutoriales, haremos que el escuadrón vaya realmente hasta el final.

  void DoMove () { if (grid.HasPath) { selectedUnit.Location = currentCell; grid.ClearPath(); } } 

Usemos el botón del mouse 1 (clic derecho) para enviar el comando. Lo comprobaremos si se selecciona un destacamento. Si no se presiona el botón, buscamos la ruta.

  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { if (Input.GetMouseButtonDown(1)) { DoMove(); } else { DoPathfinding(); } } } } 

¡Ahora podemos mover unidades! Pero a veces se niegan a encontrar el camino a algunas células. En particular, a aquellas celdas en las que solía estar el desprendimiento. Esto sucede porque HexUnitno actualiza la ubicación anterior al configurar una nueva. Para solucionar esto, borraremos el enlace al escuadrón en su ubicación anterior.

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

Evitar escuadrones


Encontrar el camino ahora funciona correctamente y las unidades pueden teletransportarse en el mapa. Aunque no pueden pasar a celdas que ya tienen un escuadrón, se ignoran los destacamentos que se interponen en el camino.


Las unidades en el camino se ignoran. Las

unidades de la misma facción generalmente pueden moverse entre sí, pero hasta ahora no tenemos facciones. Por lo tanto, consideremos todas las unidades como desconectadas entre sí y bloqueando los caminos. Esto se puede implementar saltando las celdas ocupadas HexGrid.Search.

  if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } if (neighbor.IsUnderwater || neighbor.Unit) { continue; } 


Evita destacamentos

unitypackage

Parte 19: Animación de movimiento


  • Movimos las unidades entre las celdas.
  • Visualiza el camino recorrido.
  • Movimos a las tropas a lo largo de las curvas.
  • Obligamos a las tropas a mirar en la dirección del movimiento.

En esta parte, forzaremos a las unidades en lugar de teletransportarse a moverse a lo largo de las pistas.


Escuadrones en camino

Movimiento en el camino


En la parte anterior, agregamos unidades y la capacidad de moverlas. Aunque utilizamos una búsqueda del camino para determinar los puntos finales válidos, después de dar el comando, las tropas simplemente se teletransportaron a la celda final. Para que realmente sigan el camino encontrado, necesitamos rastrear este camino y crear un proceso de animación que obligue al escuadrón a moverse de una celda a otra. Dado que mirar las animaciones es difícil notar cómo se movió el escuadrón, también visualizamos el camino recorrido con la ayuda de artilugios. Pero antes de continuar, debemos corregir el error.

Error con vueltas


Debido a un descuido, calculamos incorrectamente el curso en el que se alcanzará la celda. Ahora determinamos el rumbo dividiendo la distancia total por la velocidad del escuadrónt = d / s , y descartando el resto. El error ocurre cuando para ingresar a la celda necesitas gastar exactamente todos los puntos de movimiento restantes por movimiento. Por ejemplo, cuando cada paso cuesta 1 y la velocidad es 3, entonces podemos mover tres celdas por turno. Sin embargo, con los cálculos existentes, solo podemos dar dos pasos en el primer movimiento, porque para el tercer paso

t = d / s = 3 / 3 = 1 .


Los costos sumados de moverse con movimientos incorrectamente definidos, velocidad 3

Para el cálculo correcto de los movimientos necesitamos mover el borde un paso desde la celda inicial. Podemos hacer esto reduciendo la distancia en 1 antes de calcular el movimiento. Luego, el movimiento para el tercer paso serát = 2 / 3 = 0


Movimientos correctos

Podemos hacer esto cambiando la fórmula de cálculo at = ( d - 1 ) / s .Haremos este cambio a HexGrid.Search.

  bool Search (HexCell fromCell, HexCell toCell, int speed) { … while (searchFrontier.Count > 0) { … int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … } } return false; } 

También cambiamos las marcas de los movimientos.

  void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = (current.Distance - 1) / speed; … } } … } 

Tenga en cuenta que con este enfoque, la ruta de la celda inicial es -1. Esto es normal, porque no lo mostramos, y el algoritmo de búsqueda permanece operativo.

Camino


Moverse por el camino es tarea del escuadrón. Para que pueda hacer esto, necesita saber el camino. Tenemos esta información HexGrid, así que agreguemos un método para obtener la ruta actual en forma de una lista de celdas. Puede tomarlo del grupo de listas y regresar si realmente hay un camino.

  public List<HexCell> GetPath () { if (!currentPathExists) { return null; } List<HexCell> path = ListPool<HexCell>.Get(); return path; } 

La lista se completa siguiendo la ruta de enlace desde la celda final a la inicial, como se hace al visualizar la ruta.

  List<HexCell> path = ListPool<HexCell>.Get(); for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } return path; 

En este caso, necesitamos toda la ruta, que incluye la celda inicial.

  for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } path.Add(currentPathFrom); return path; 

Ahora tenemos el camino en el orden inverso. Podemos trabajar con él, pero no será muy intuitivo. Volteemos la lista para que vaya de principio a fin.

  path.Add(currentPathFrom); path.Reverse(); return path; 

Solicitud de movimiento


Ahora podemos agregar al HexUnitmétodo, ordenándole que siga el camino. Inicialmente, simplemente lo dejamos teletransportarse a la celda final. No devolveremos de inmediato la lista al grupo, porque nos será útil por un tiempo.

 using UnityEngine; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; } … } 

Para solicitar movimiento, lo cambiamos HexGameUI.DoMovepara que llame a un nuevo método con la ruta actual, y no solo establece la ubicación de la unidad.

  void DoMove () { if (grid.HasPath) { // selectedUnit.Location = currentCell; selectedUnit.Travel(grid.GetPath()); grid.ClearPath(); } } 

Visualización del camino


Antes de comenzar a animar al escuadrón, verifiquemos que los caminos sean correctos. Haremos esto ordenando HexUnitrecordar la ruta a lo largo de la cual debe moverse, para que pueda visualizarse usando gizmos.

  List<HexCell> pathToTravel; … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; } 

Agregue un método OnDrawGizmospara mostrar la última ruta a seguir (si existe). Si la unidad aún no se ha movido, la ruta debe ser igual null. Pero debido a la serialización de Unity durante la edición después de la compilación en modo Play, también puede ser una lista vacía.

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } } 

La forma más fácil de mostrar el camino es dibujar una esfera de artilugios para cada celda del camino. Una esfera con un radio de 2 unidades es adecuada para nosotros.

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } for (int i = 0; i < pathToTravel.Count; i++) { Gizmos.DrawSphere(pathToTravel[i].Position, 2f); } } 

Como mostraremos los caminos para el destacamento, podremos ver simultáneamente todos sus últimos caminos.


Gizmos muestra los últimos caminos recorridos.

Para mostrar mejor las conexiones de las celdas, dibujamos varias esferas en un bucle en una línea entre las celdas anteriores y actuales. Para hacer esto, necesitamos comenzar el proceso desde la segunda celda. Las esferas se pueden organizar mediante interpolación lineal con un incremento de 0.1 unidades, de modo que obtenemos diez esferas por segmento.

  for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } } 


Formas más obvias

Deslizarse por el camino


Puedes usar el mismo método para mover unidades. Creemos una rutina para esto. En lugar de dibujar un artilugio, estableceremos la posición del escuadrón. En lugar de incrementar, usaremos 0.1 delta de tiempo, y realizaremos el rendimiento para cada iteración. En este caso, el escuadrón se moverá de una celda a la siguiente en un segundo.

 using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } … } 

Comencemos la rutina al final del método Travel. Pero primero, detendremos todas las corutinas existentes. Por lo tanto, garantizamos que dos corutinas no comenzarán al mismo tiempo, de lo contrario, esto conduciría a resultados muy extraños.

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

Mover una celda por segundo es bastante lento. El jugador durante el juego no querrá esperar tanto. Puedes hacer que la velocidad de movimiento del escuadrón sea una opción de configuración, pero por ahora, usemos una constante. Le asigné un valor de 4 celdas por segundo; es bastante rápido, pero notemos lo que está sucediendo.

  const float travelSpeed = 4f; … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } 

Así como podemos visualizar varios caminos simultáneamente, podemos hacer que varias unidades viajen al mismo tiempo. Desde el punto de vista del estado del juego, el movimiento sigue siendo teletransportador, las animaciones son exclusivamente visuales. Las unidades ocupan instantáneamente la celda final. Incluso puedes encontrar formas y comenzar un nuevo movimiento antes de que lleguen. En este caso, se teletransportan visualmente al comienzo de un nuevo camino. Esto puede evitarse bloqueando unidades o incluso toda la interfaz de usuario mientras se mueven, pero una reacción tan rápida es bastante conveniente al desarrollar y probar movimientos.


Unidades móviles

¿Qué pasa con la diferencia de altura?
, . , . , . , . , Endless Legend, , . , .

Posición después de la compilación


Uno de los inconvenientes de la corutina es que no "sobreviven" cuando se vuelven a compilar en el modo Play. Aunque el estado del juego siempre es cierto, esto puede llevar a que los escuadrones se atasquen en algún lugar de su último camino si la recompilación se inicia mientras todavía se están moviendo. Para mitigar las consecuencias, asegurémonos de que, después de la compilación, las unidades estén siempre en la posición correcta. Esto se puede hacer actualizando su posición en OnEnable.

  void OnEnable () { if (location) { transform.localPosition = location.Position; } } 

paquete de la unidad

Movimiento suave


El movimiento desde el centro hacia el centro de la celda parece demasiado mecanicista y crea cambios bruscos de dirección. Para muchos juegos, esto será normal, pero inaceptable si necesita al menos un movimiento ligeramente realista. Así que cambiemos el movimiento para que se vea un poco más orgánico.

Moviéndose de costilla a costilla


El escuadrón comienza su viaje desde el centro de la celda. Pasa al centro del borde de la celda y luego ingresa a la siguiente celda. En lugar de moverse hacia el centro, puede dirigirse directamente hacia el siguiente borde que debe cruzar. De hecho, la unidad cortará el camino cuando necesite cambiar de dirección. Esto es posible para todas las celdas, excepto los puntos finales de la ruta.


Tres formas de moverse de borde a borde

Vamos a adaptarnos OnDrawGizmosa mostrar las rutas generadas de esta manera. Debe interpolar entre los bordes de las celdas, lo que se puede encontrar promediando las posiciones de las celdas vecinas. Es suficiente para nosotros calcular un borde por iteración, reutilizando el valor de la iteración anterior. Por lo tanto, podemos hacer que el método funcione para la celda inicial, pero en lugar del borde tomamos su posición.

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { // Vector3 a = pathToTravel[i - 1].Position; // Vector3 b = pathToTravel[i].Position; a = b; b = (pathToTravel[i - 1].Position + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } } } 

Para llegar al centro de la celda final, necesitamos usar la posición de la celda como el último punto, no el borde. Puede agregar la verificación de este caso al bucle, pero es un código tan simple que será más obvio simplemente duplicar el código y cambiarlo ligeramente.

  void OnDrawGizmos () { … for (int i = 1; i < pathToTravel.Count; i++) { … } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } } 


Rutas basadas en costillas Las

rutas resultantes son menos como zigzags, y el ángulo de giro máximo se reduce de 120 ° a 90 °. Esto puede considerarse una mejora, por lo que aplicamos los mismos cambios en la rutina TravelPathpara ver cómo se ve en la animación.

  IEnumerator TravelPath () { Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { // Vector3 a = pathToTravel[i - 1].Position; // Vector3 b = pathToTravel[i].Position; a = b; b = (pathToTravel[i - 1].Position + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } 


Moverse con una velocidad cambiante

Después de cortar los ángulos, la longitud de los segmentos de la ruta se volvió dependiente del cambio de dirección. Pero establecemos la velocidad en celdas por segundo. Como resultado, la velocidad de desprendimiento cambia aleatoriamente.

Siguientes curvas


Los cambios instantáneos en la dirección y la velocidad al cruzar los límites de las celdas se ven feos. Mejor usar un cambio gradual de dirección. Podemos agregar apoyo para esto al obligar a las tropas a seguir a lo largo de curvas en lugar de líneas rectas. Puedes usar curvas de Bezier para esto. En particular, podemos tomar curvas de Bezier cuadráticas en las que el centro de las celdas serán los puntos de control intermedios. En este caso, las tangentes de las curvas adyacentes serán imágenes especulares entre sí, es decir, todo el camino se convertirá en una curva suave continua.


Curvas de borde a borde

Cree una clase auxiliar Beziercon un método para obtener puntos en una curva de Bezier cuadrática. Como se explica en el tutorial Curves and Splines , la fórmula se usa para esto( 1 - t ) 2 A + 2 ( 1 - t ) t B + t 2 C donde Un , B y C son los puntos de control, y t es el interpolador.

 using UnityEngine; public static class Bezier { public static Vector3 GetPoint (Vector3 a, Vector3 b, Vector3 c, float t) { float r = 1f - t; return r * r * a + 2f * r * t * b + t * t * c; } } 

¿No debería limitarse GetPoint a 0-1?
0-1, . . , GetPointClamped , t . , GetPointUnclamped .

Para mostrar la trayectoria de la curva OnDrawGizmos, necesitamos rastrear no dos, sino tres puntos. Un punto adicional es el centro de la celda con la que estamos trabajando en la iteración actual, que tiene un índice i - 1, porque el ciclo comienza con 1. Habiendo recibido todos los puntos, podemos reemplazarlo Vector3.Lerppor Bezier.GetPoint.

En las celdas de inicio y fin, en lugar de los puntos final y medio, simplemente podemos usar el centro de la celda.

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } } 


Rutas creadas con curvas de Bezier Una

ruta curva se ve mucho mejor. Aplicamos los mismos cambios TravelPathy vemos cómo se animan las unidades con este enfoque.

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } 


Nos movemos a lo largo de las curvas. La

animación también se suavizó, incluso cuando la velocidad del desprendimiento es inestable. Como las tangentes de la curva de los segmentos adyacentes coinciden, la velocidad es continua. El cambio en la velocidad ocurre gradualmente y ocurre cuando un desprendimiento pasa a través de la celda, disminuyendo la velocidad al cambiar de dirección. Si va derecho, entonces la velocidad permanece constante. Además, el escuadrón comienza y termina su viaje a velocidad cero. Esto imita el movimiento natural, así que déjalo así.

Seguimiento de tiempo


Hasta este punto, comenzamos a iterar sobre cada uno de los segmentos desde 0, continuando hasta llegar a 1. Esto funciona bien cuando aumenta en un valor constante, pero nuestra iteración depende del tiempo delta. Cuando se completa la iteración sobre un segmento, es probable que excedamos 1 en cierta cantidad, dependiendo del delta de tiempo. Esto es invisible a velocidades de cuadro altas, pero puede provocar sacudidas a velocidades de cuadro bajas.

Para evitar la pérdida de tiempo, necesitamos transferir el tiempo restante de un segmento al siguiente. Esto se puede hacer rastreando a lo tlargo de toda la ruta, y no solo en cada segmento. Luego, al final de cada segmento, restaremos 1 de él.

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; float t = 0f; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } t -= 1f; } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (; t < 1f; t += Time.deltaTime * traveSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } 

Si ya estamos haciendo esto, asegurémonos de que el tiempo delta se tenga en cuenta al comienzo de la ruta. Esto significa que comenzaremos a movernos de inmediato y no estaremos inactivos durante un fotograma.

  float t = Time.deltaTime * travelSpeed; 

Además, no terminamos exactamente en el momento en que la ruta debería terminar, sino momentos antes. Aquí, la diferencia también puede depender de la velocidad de fotogramas. Por lo tanto, hagamos que el escuadrón complete el camino exactamente en el punto final.

  IEnumerator TravelPath () { … transform.localPosition = location.Position; } 

paquete de la unidad

Animación de orientación


Las unidades comenzaron a moverse a lo largo de una curva suave, pero no cambiaron la orientación de acuerdo con la dirección del movimiento. Como resultado, parecen deslizarse. Para que el movimiento parezca un movimiento real, necesitamos rotarlos.

Mirando hacia adelante


Como en el tutorial Curvas y Splines , podemos usar la derivada de la curva para determinar la orientación de la unidad. La fórmula para la derivada de una curva de Bezier cuadrática:2 ( ( 1 - t ) ( B - A ) + t ( C - B ) ) .Agregue al Beziermétodo para calcularlo.

  public static Vector3 GetDerivative ( Vector3 a, Vector3 b, Vector3 c, float t ) { return 2f * ((1f - t) * (b - a) + t * (c - b)); } 

El vector derivado se encuentra en una línea recta con la dirección del movimiento. Podemos usar el método Quaternion.LookRotationpara convertirlo en un turno de escuadrón. Lo llevaremos a cabo en cada paso HexUnit.TravelPath.

  transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null; … transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null; 

¿No hay ningún error al comienzo del camino?
, . A y B , . , t=0 , , Quaternion.LookRotation . , , t=0 . . , t>0 .
, t<1 .

En contraste con la posición del destacamento, la no idealidad de su orientación al final del camino no es importante. sin embargo, debemos asegurarnos de que su orientación corresponda a la rotación final. Para hacer esto, después de la finalización, equiparamos su orientación a su rotación en Y.

  transform.localPosition = location.Position; orientation = transform.localRotation.eulerAngles.y; 

Ahora las unidades miran exactamente en la dirección del movimiento, tanto horizontal como verticalmente. Esto significa que se inclinarán hacia adelante y hacia atrás, descendiendo de las laderas y subiéndolos. Para asegurarnos de que siempre estén rectos, forzamos el componente Y del vector de dirección a cero antes de usarlo para determinar la rotación de la unidad.

  Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); … Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); 


Mirando hacia adelante mientras te mueves

Nos fijamos en el punto


A lo largo del camino, las unidades están mirando hacia adelante, pero antes de comenzar a moverse, pueden mirar en la otra dirección. En este caso, cambian instantáneamente su orientación. Será mejor si giran en la dirección del camino antes del inicio del movimiento.

Mirar en la dirección correcta puede ser útil en otras situaciones, así que creemos un método LookAtque obligue al escuadrón a cambiar de orientación para mirar un cierto punto. La rotación requerida se puede establecer utilizando el método Transform.LookAt, primero colocando el punto en la misma posición vertical que el desprendimiento. Después de eso, podemos recuperar la orientación del escuadrón.

  void LookAt (Vector3 point) { point.y = transform.localPosition.y; transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; } 

Para que el desprendimiento realmente gire, convertiremos el método en otra corutina que lo rotará a una velocidad constante. La velocidad de giro también se puede ajustar, pero usaremos la constante nuevamente. La rotación debe ser rápida, aproximadamente 180 ° por segundo.

  const float rotationSpeed = 180f; … IEnumerator LookAt (Vector3 point) { … } 

No es necesario jugar con la aceleración del giro, porque es imperceptible. Será suficiente para nosotros simplemente interpolar entre las dos orientaciones. Desafortunadamente, esto no es tan simple como en el caso de dos números, porque los ángulos son circulares. Por ejemplo, una transición de 350 ° a 10 ° debería dar como resultado una rotación de 20 ° en sentido horario, pero una interpolación simple forzará una rotación de 340 ° en sentido antihorario.

La forma más fácil de crear una rotación correcta es interpolar entre dos cuaterniones mediante interpolación esférica. Esto conducirá al turno más corto. Para hacer esto, obtenemos los cuaterniones del principio y el final, y luego hacemos una transición entre ellos usando Quaternion.Slerp.

  IEnumerator LookAt (Vector3 point) { point.y = transform.localPosition.y; Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); for (float t = Time.deltaTime; t < 1f; t += Time.deltaTime) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; } transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; } 

Esto funcionará, pero la interpolación siempre va de 0 a 1, independientemente del ángulo de rotación. Para garantizar una velocidad angular uniforme, debemos reducir la velocidad de la interpolación a medida que aumenta el ángulo de rotación.

  Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); float angle = Quaternion.Angle(fromRotation, toRotation); float speed = rotationSpeed / angle; for ( float t = Time.deltaTime * speed; t < 1f; t += Time.deltaTime * speed ) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; } 

Conociendo el ángulo, podemos omitir por completo el giro si resulta ser cero.

  float angle = Quaternion.Angle(fromRotation, toRotation); if (angle > 0f) { float speed = rotationSpeed / angle; for ( … ) { … } } 

Ahora podemos agregar la rotación de la unidad TravelPathsimplemente realizando rendimiento antes de mover la LookAtposición de la segunda celda. Unity lanzará automáticamente la rutina LookAty TravelPathesperará a que se complete.

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); float t = Time.deltaTime * travelSpeed; … } 

Si verifica el código, el escuadrón se teletransporta a la celda final, gira allí y luego se teletransporta al comienzo del camino y comienza a moverse desde allí. Esto sucede porque asignamos un valor a una propiedad Locationantes del comienzo de la rutina TravelPath. Para deshacernos de la teletransportación, al principio podemos TravelPathdevolver la posición del desprendimiento a la celda inicial.

  Vector3 a, b, c = pathToTravel[0].Position; transform.localPosition = c; yield return LookAt(pathToTravel[1].Position); 


Gire antes de moverse

Barrer


Habiendo recibido el movimiento que necesitamos, podemos deshacernos del método OnDrawGizmos. Elimínelo o comente en caso de que necesitemos ver rutas en el futuro.

 // void OnDrawGizmos () { // … // } 

Como ya no necesitamos recordar en qué dirección nos movíamos, al final TravelPathpuede liberar la lista de celdas.

  IEnumerator TravelPath () { … ListPool<HexCell>.Add(pathToTravel); pathToTravel = null; } 

¿Qué pasa con las animaciones de escuadrones reales?
, . 3D- . . , . Mecanim, TravelPath .

paquete de la unidad

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


All Articles