Partes 1-3: malla, colores y alturas de celdaPartes 4-7: baches, ríos y caminosPartes 8-11: agua, accidentes geográficos y murallasPartes 12-15: guardar y cargar, texturas, distanciasPartes 16-19: encontrar el camino, escuadrones de jugadores, animacionesPartes 20-23: niebla de guerra, investigación de mapas, generación de procedimientosPartes 24-27: ciclo del agua, erosión, biomas, mapa cilíndricoParte 12: guardar y cargar
- Rastrea el tipo de terreno en lugar del color.
- Crea un archivo.
- Escribimos los datos en un archivo y luego los leemos.
- Nosotros serializamos los datos de la celda.
- Reduce el tamaño del archivo.
Ya sabemos cómo crear mapas bastante interesantes. Ahora necesita aprender cómo guardarlos.
Cargado desde el archivo test.map .Tipo de terreno
Al guardar un mapa, no necesitamos almacenar todos los datos que rastreamos durante la ejecución de la aplicación. Por ejemplo, solo necesitamos recordar el nivel de altura de la celda. Su posición vertical en sí se toma de estos datos, por lo que no es necesario almacenarlos. En realidad, es mejor si no almacenamos estas métricas calculadas. Por lo tanto, los datos del mapa seguirán siendo correctos, incluso si más tarde decidimos cambiar el desplazamiento de altura. Los datos están separados de su presentación.
Del mismo modo, no necesitamos almacenar el color exacto de la celda. Puedes escribir que la celda es verde. Pero el tono exacto del verde puede cambiar con un cambio en el estilo visual. Para hacer esto, podemos guardar el índice de color, no los colores en sí. De hecho, puede ser suficiente para nosotros almacenar este índice en lugar de colores reales en las celdas en tiempo de ejecución. Esto permitirá más adelante pasar a una visualización más compleja del relieve.
Moviendo una variedad de colores
Si las celdas ya no tienen datos de color, entonces deberían almacenarse en otro lugar. Es más conveniente almacenarlo en
HexMetrics
. Así que agreguemos una variedad de colores.
public static Color[] colors;
Al igual que todos los demás datos globales, como el ruido, podemos inicializar estos colores con
HexGrid
.
public Color[] colors; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; … } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; } }
Y como ahora no asignamos colores directamente a las celdas, eliminaremos el color predeterminado.
Establezca los nuevos colores para que coincidan con la matriz general del editor de mapas hexagonales.
Colores añadidos a la cuadrícula.Refactorización Celular
Elimine el campo de color de
HexCell
. En cambio, almacenaremos el índice. En lugar de un índice de color, usamos un índice de tipo de relieve más general.
La propiedad de color puede usar este índice solo para obtener el color correspondiente. Ahora no está configurado directamente, así que elimine esta parte. En este caso, obtenemos un error de compilación, que solucionaremos pronto.
public Color Color { get { return HexMetrics.colors[terrainTypeIndex]; }
Agregue una nueva propiedad para obtener y establecer un nuevo índice de tipo de elevación.
public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value; Refresh(); } } }
Refactorización del editor
Dentro de
HexMapEditor
todo el código con respecto a los colores. Esto solucionará el error de compilación.
Ahora agregue un campo y un método para controlar el índice de tipo de elevación activo.
int activeTerrainTypeIndex; … public void SetTerrainTypeIndex (int index) { activeTerrainTypeIndex = index; }
Utilizamos este método como reemplazo del método
SelectColor
ahora falta. Conecte los widgets de color en la interfaz de usuario con
SetTerrainTypeIndex
, dejando todo lo demás sin cambios. Esto significa que un índice negativo todavía está en uso y significa que el color no debe cambiar.
Cambie
EditCell
para que el índice de tipo de elevación se asigne a la celda que se está editando.
void EditCell (HexCell cell) { if (cell) { if (activeTerrainTypeIndex >= 0) { cell.TerrainTypeIndex = activeTerrainTypeIndex; } … } }
Aunque eliminamos los datos de color de las celdas, el mapa debería funcionar igual que antes. La única diferencia es que el color predeterminado ahora es el primero en la matriz. En mi caso es amarillo.
El amarillo es el nuevo color predeterminado.paquete de la unidadGuardar datos en un archivo
Para controlar el guardado y la carga del mapa, utilizamos
HexMapEditor
. Crearemos dos métodos que harán esto, y por ahora los dejaremos vacíos.
public void Save () { } public void Load () { }
Agregue dos botones a la IU (
GameObject / UI / Button ). Conéctelos a los botones y asigne las etiquetas apropiadas. Los puse en la parte inferior del panel derecho.
Guardar y cargar botones.Ubicación del archivo
Para almacenar una tarjeta, debe guardarla en algún lugar. Como se hace en la mayoría de los juegos, almacenaremos datos en un archivo. Pero, ¿dónde poner este archivo en el sistema de archivos? La respuesta depende de en qué sistema operativo se esté ejecutando el juego. Cada sistema operativo tiene sus propios estándares para almacenar archivos relacionados con aplicaciones.
No necesitamos conocer estos estándares. Unity conoce el camino correcto que podemos obtener con
Application.persistentDataPath
. Puede verificar cómo será con usted, en el método
Save
, mostrándolo en la consola y presionando el botón en el modo Reproducir.
public void Save () { Debug.Log(Application.persistentDataPath); }
En los sistemas de escritorio, la ruta contendrá el nombre de la empresa y el producto. Esta ruta es utilizada tanto por el editor como por el ensamblado. Los nombres se pueden configurar en
Edición / Configuración del proyecto / Reproductor .
Nombre de la empresa y producto.¿Por qué no puedo encontrar la carpeta Biblioteca en Mac?La carpeta Biblioteca a menudo está oculta. La forma en que se puede mostrar depende de la versión de OS X. Si no tiene una versión anterior, seleccione la carpeta de inicio en el Finder y vaya a Mostrar opciones de vista . Hay una casilla de verificación para la carpeta Biblioteca .
¿Qué hay de WebGL?Los juegos de WebGL no pueden acceder al sistema de archivos del usuario. En cambio, todas las operaciones de archivos se redirigen a un sistema de archivos ubicado en la memoria. Ella es transparente para nosotros. Sin embargo, para guardar los datos, deberá ordenar manualmente la página web para volcar los datos en el almacenamiento del navegador.
Creación de archivos
Para crear un archivo, necesitamos usar clases del espacio de nombres
System.IO
. Por lo tanto, agregamos una declaración de
using
sobre la clase
HexMapEditor
.
using UnityEngine; using UnityEngine.EventSystems; using System.IO; public class HexMapEditor : MonoBehaviour { … }
Primero necesitamos crear la ruta completa al archivo. Usamos
test.map como el
nombre del archivo. Debe agregarse a la ruta de los datos almacenados. Si necesita insertar una barra diagonal inversa o barra diagonal inversa (barra diagonal inversa o barra diagonal inversa) depende de la plataforma. El método
Path.Combine
hará
Path.Combine
.
public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); }
A continuación, necesitamos acceder al archivo en esta ubicación. Hacemos esto usando el método
File.Open
. Dado que queremos escribir datos en este archivo, necesitamos usar su modo de creación. En este caso, se creará un nuevo archivo en la ruta especificada o se reemplazará un archivo existente.
string path = Path.Combine(Application.persistentDataPath, "test.map"); File.Open(path, FileMode.Create);
El resultado de llamar a este método será un flujo de datos abierto asociado con este archivo. Podemos usarlo para escribir datos en un archivo. Y no debemos olvidar cerrar la transmisión cuando ya no la necesitemos.
string path = Path.Combine(Application.persistentDataPath, "test.map"); Stream fileStream = File.Open(path, FileMode.Create); fileStream.Close();
En esta etapa, cuando hace clic en el botón
Guardar , el archivo
test.map se creará en la carpeta especificada como la ruta a los datos almacenados. Si estudia este archivo, estará vacío y tendrá un tamaño de 0 bytes, porque hasta ahora no hemos escrito nada en él.
Escribir en el archivo
Para escribir datos en un archivo, necesitamos una forma de transmitir datos a él. La forma más fácil de hacer esto es con
BinaryWriter
. Estos objetos le permiten escribir datos primitivos en cualquier secuencia.
Cree un nuevo objeto
BinaryWriter
, y nuestro flujo de archivos será su argumento. El escritor de cierre cierra la secuencia que utiliza. Por lo tanto, ya no necesitamos almacenar un enlace directo a la transmisión.
string path = Path.Combine(Application.persistentDataPath, "test.map"); BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Close();
Para transferir datos a una secuencia, podemos usar el método
BinaryWriter.Write
. Existe una variante del método
Write
para todos los tipos primitivos, como entero y flotante. También puede grabar líneas. Tratemos de escribir el entero 123.
BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Write(123); writer.Close();
Haga clic en el botón
Guardar y examine
test.map nuevamente. Ahora su tamaño es de 4 bytes, porque el tamaño entero es de 4 bytes.
¿Por qué mi administrador de archivos muestra que el archivo ocupa más espacio?Porque los sistemas de archivos dividen el espacio en bloques de bytes. No rastrean bytes individuales. Como test.map solo toma cuatro bytes hasta el momento, requiere un bloque de espacio de almacenamiento.
Tenga en cuenta que almacenamos datos binarios, no textos legibles por humanos. Por lo tanto, si abrimos el archivo en un editor de texto, veremos un conjunto de caracteres confusos. Probablemente verá el símbolo
{ seguido de nada o algunos marcadores de posición.
Puede abrir el archivo en un editor hexadecimal. En este caso, veremos
7b 00 00 00 . Estos son cuatro bytes de nuestro entero, mapeados en notación hexadecimal. En números decimales ordinarios, esto es
123 0 0 0 . En binario, el primer byte se parece a
01111011 .
El código ASCII para
{ es 123, por lo que este carácter se puede mostrar en un editor de texto. ASCII 0 es un carácter nulo que no coincide con ningún carácter visible.
Los tres bytes restantes son iguales a cero, porque escribimos un número menor que 256. Si escribiéramos 256, veríamos
00 01 00 00 en el editor hexadecimal.
¿No debería almacenarse 123 como 00 00 00 7b?BinaryWriter
usa el formato little-endian para guardar números. Esto significa que los bytes menos significativos se escriben primero. Este formato fue utilizado por Microsoft en el desarrollo del marco .Net. Probablemente fue elegido porque la CPU Intel usa el formato little-endian.
Una alternativa a esto es big-endian, en la que los bytes más significativos se almacenan primero. Esto corresponde al orden habitual de números en números. 123 es ciento veintitrés porque nos referimos al registro big-endian. Si fuera un little endian, entonces 123 significaría trescientos veintiuno.
Hacemos recursos gratis
Es importante que cerremos escritor. Mientras está abierto, el sistema de archivos bloquea el archivo, evitando que otros procesos escriban en él. Si olvidamos cerrarlo, también nos bloquearemos. Si presionamos el botón Guardar dos veces, la segunda vez no podremos abrir la transmisión.
En lugar de cerrar el escritor manualmente, podemos crear un bloque de
using
para esto. Define el alcance dentro del cual el escritor es válido. Cuando el código ejecutable va más allá de este alcance, el escritor se elimina y el hilo se cierra.
using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(123); }
Esto funcionará porque las clases de escritor y secuencia de archivos implementan la interfaz
IDisposable
. Estos objetos tienen un método
Dispose
, que se llama indirectamente cuando van más allá del alcance del
using
.
La gran ventaja de
using
es que funciona sin importar cómo se ejecute el programa fuera de alcance. Las devoluciones anticipadas, las excepciones y los errores no le molestan. Además, él es muy conciso.
Recuperación de datos
Para leer datos escritos previamente, necesitamos insertar el código en el método
Load
. Como en el caso de guardar, necesitamos crear una ruta y abrir la secuencia del archivo. La diferencia es que ahora abrimos el archivo para leer, no para escribir. Y en lugar de escritor necesitamos
BinaryReader
.
public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open)) ) { } }
En este caso, podemos usar el método
File.OpenRead
para abrir el archivo para leerlo.
using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { }
¿Por qué no podemos usar File.OpenWrite al escribir?Este método crea una secuencia que agrega datos a los archivos existentes, en lugar de reemplazarlos.
Al leer, debemos indicar explícitamente el tipo de datos recibidos. Para leer un entero de una secuencia, necesitamos usar
BinaryReader.ReadInt32
. Este método lee un número entero de 32 bits, es decir, cuatro bytes.
using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { Debug.Log(reader.ReadInt32()); }
Cabe señalar que al recibir
123, será suficiente para que podamos leer un byte. Pero al mismo tiempo, tres bytes pertenecientes a este entero permanecerán en la secuencia. Además, esto no funcionará para números fuera del intervalo 0–255. Por lo tanto, no lo hagas.
paquete de la unidadEscribir y leer datos de mapas
Al guardar datos, una pregunta importante es si se debe usar un formato legible por humanos. Por lo general, los formatos legibles por humanos son JSON, XML y ASCII simple con algún tipo de estructura. Dichos archivos se pueden abrir, interpretar y editar en editores de texto. Además, simplifican el intercambio de datos entre diferentes aplicaciones.
Sin embargo, dichos formatos tienen sus propios requisitos. Los archivos ocuparán más espacio (a veces mucho más) que el uso de datos binarios. También pueden aumentar considerablemente el costo de codificar y decodificar datos, tanto en términos de tiempo de ejecución como de huella de memoria.
En contraste, los datos binarios son compactos y rápidos. Esto es importante cuando se graban grandes cantidades de datos. Por ejemplo, al guardar automáticamente un mapa grande en cada turno del juego. Por lo tanto
Utilizaremos el formato binario. Si puede manejar esto, puede trabajar con formatos más detallados.
¿Qué pasa con la serialización automática?Inmediatamente durante el proceso de serialización de datos de Unity, podemos escribir directamente clases serializadas en la secuencia. Los detalles de la grabación de campos individuales serán ocultos para nosotros. Sin embargo, no podemos serializar directamente las células. Son clases de MonoBehaviour
que contienen datos que no necesitamos guardar. Por lo tanto, necesitamos usar una jerarquía separada de objetos, lo que destruye la simplicidad de la serialización automática. Además, será más difícil soportar futuros cambios de código. Por lo tanto, mantendremos el control total con la serialización manual. Además, nos hará comprender realmente lo que está sucediendo.
Para serializar el mapa, necesitamos almacenar los datos de cada celda. Para guardar y cargar una sola celda, agregue los métodos
Save
y
Load
a
HexCell
. Como necesitan un escritor o lector para trabajar, los agregaremos como parámetros.
using UnityEngine; using System.IO; public class HexCell : MonoBehaviour { … public void Save (BinaryWriter writer) { } public void Load (BinaryReader reader) { } }
Agregue los métodos
Save
y
Load
a
HexGrid
. Estos métodos simplemente omiten todas las celdas llamando a sus métodos
Load
y
Save
.
using UnityEngine; using UnityEngine.UI; using System.IO; public class HexGrid : MonoBehaviour { … public void Save (BinaryWriter writer) { for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } } public void Load (BinaryReader reader) { for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } } }
Si descargamos un mapa, debe actualizarse después de que se hayan cambiado los datos de la celda. Para hacer esto, solo actualice todos los fragmentos.
public void Load (BinaryReader reader) { for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } for (int i = 0; i < chunks.Length; i++) { chunks[i].Refresh(); } }
Finalmente, reemplazamos nuestro código de prueba en
HexMapEditor
con llamadas a los métodos
Save
and
Load
de la cuadrícula, pasando el escritor o lector con ellos.
public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { hexGrid.Save(writer); } } public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { hexGrid.Load(reader); } }
Guardar un tipo de relieve
En la etapa actual, volver a guardar crea un archivo vacío, y la descarga no hace nada. Comencemos gradualmente grabando y cargando solo el índice de tipo de elevación
HexCell
.
Asigne el valor directamente al campo terrenoTypeIndex. No usaremos propiedades. Dado que actualizamos explícitamente todos los fragmentos, no se necesitan llamadas a las propiedades de
Refresh
. Además, dado que guardamos solo los mapas correctos, asumiremos que todos los mapas descargados también son correctos. Por lo tanto, por ejemplo, no verificaremos si el río o la carretera están permitidos.
public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); } public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); }
Al guardar en este archivo, uno tras otro se escribirá el índice del tipo de relieve de todas las celdas. Como el índice es un entero, su tamaño es de cuatro bytes. Mi tarjeta contiene 300 celdas, es decir, el tamaño del archivo será de 1200 bytes.
La carga lee los índices en el mismo orden en que se escriben. Si cambió los colores de las celdas después de guardar, al cargar el mapa, los colores volverán al estado al guardar. Como ya no guardamos nada, el resto de los datos de la celda permanecerán igual. Es decir, la carga cambiará el tipo de terreno, pero no su altura, nivel de agua, características del terreno, etc.
Guardar todo entero
Guardar un índice de tipo de alivio no es suficiente para nosotros. Necesita guardar todos los demás datos. Comencemos con todos los campos enteros. Este es un índice del tipo de relieve, altura de celda, nivel de agua, nivel de ciudad, nivel de granja, nivel de vegetación y el índice de objetos especiales. Deberán leerse en el mismo orden en que se grabaron.
public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); writer.Write(elevation); writer.Write(waterLevel); writer.Write(urbanLevel); writer.Write(farmLevel); writer.Write(plantLevel); writer.Write(specialIndex); } public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); waterLevel = reader.ReadInt32(); urbanLevel = reader.ReadInt32(); farmLevel = reader.ReadInt32(); plantLevel = reader.ReadInt32(); specialIndex = reader.ReadInt32(); }
Intente ahora guardar y cargar el mapa, haciendo cambios entre estas operaciones. Todo lo que incluimos en los datos almacenados se restauró lo mejor que pudimos, excepto la altura de la celda. Esto sucedió porque cuando cambia el nivel de altura, necesita actualizar la posición vertical de la celda. Esto se puede hacer asignándole a la propiedad, y no al campo, el valor de la altura cargada. Pero esta propiedad hace un trabajo adicional que no necesitamos. Por lo tanto, extraigamos el código actualizando la posición de la celda del
RefreshPosition
Elevation
e insertándolo en un método
RefreshPosition
separado. El único cambio que necesita hacer aquí es reemplazar el
value
referencia al campo de
elevation
.
void RefreshPosition () { Vector3 position = transform.localPosition; position.y = elevation * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; }
Ahora podemos llamar al método al establecer la propiedad, así como después de cargar los datos de altura.
public int Elevation { … set { if (elevation == value) { return; } elevation = value; RefreshPosition(); ValidateRivers(); … } } … public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); RefreshPosition(); … }
Después de este cambio, las celdas cambiarán correctamente su altura aparente al cargar.
Guardar todos los datos
La presencia de paredes y ríos entrantes / salientes en la celda se almacena en campos booleanos. Podemos escribirlos simplemente como un número entero. Además, los datos de carreteras son una matriz de seis valores booleanos que podemos escribir con un bucle.
public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); writer.Write(elevation); writer.Write(waterLevel); writer.Write(urbanLevel); writer.Write(farmLevel); writer.Write(plantLevel); writer.Write(specialIndex); writer.Write(walled); writer.Write(hasIncomingRiver); writer.Write(hasOutgoingRiver); for (int i = 0; i < roads.Length; i++) { writer.Write(roads[i]); } }
Las direcciones de los ríos entrantes y salientes se almacenan en los campos de
HexDirection
. El tipo
HexDirection
es una enumeración que se almacena internamente como múltiples valores enteros. Por lo tanto, también podemos serializarlos como un entero utilizando una conversión explícita.
writer.Write(hasIncomingRiver); writer.Write((int)incomingRiver); writer.Write(hasOutgoingRiver); writer.Write((int)outgoingRiver);
Los valores booleanos se leen utilizando el método
BinaryReader.ReadBoolean
. Las direcciones de los ríos son enteras, que debemos convertir de nuevo a
HexDirection
.
public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); RefreshPosition(); waterLevel = reader.ReadInt32(); urbanLevel = reader.ReadInt32(); farmLevel = reader.ReadInt32(); plantLevel = reader.ReadInt32(); specialIndex = reader.ReadInt32(); walled = reader.ReadBoolean(); hasIncomingRiver = reader.ReadBoolean(); incomingRiver = (HexDirection)reader.ReadInt32(); hasOutgoingRiver = reader.ReadBoolean(); outgoingRiver = (HexDirection)reader.ReadInt32(); for (int i = 0; i < roads.Length; i++) { roads[i] = reader.ReadBoolean(); } }
Ahora guardamos todos los datos de celda que son necesarios para el guardado completo y la restauración del mapa.
Esto requiere nueve enteros y nueve valores booleanos por celda. Cada valor booleano toma un byte, por lo que utilizamos un total de 45 bytes por celda. Es decir, una tarjeta con 300 celdas requiere un total de 13,500 bytes.paquete de la unidadReduce el tamaño del archivo
Aunque parece que 13,500 bytes no es mucho para 300 celdas, quizás podamos hacerlo con una cantidad menor. Al final, tenemos control total sobre cómo se serializan los datos. Veamos si hay una forma más compacta de almacenarlos.Reducción de intervalo numérico
Los diferentes niveles e índices celulares se almacenan como un número entero. Sin embargo, usan solo un pequeño rango de valores. Cada uno de ellos definitivamente permanecerá en el rango de 0-255. Esto significa que solo se usará el primer byte de cada entero. Los tres restantes siempre serán cero. No tiene sentido almacenar estos bytes vacíos. Podemos descartarlos escribiendo entero a byte antes de escribir en la secuencia. writer.Write((byte)terrainTypeIndex); writer.Write((byte)elevation); writer.Write((byte)waterLevel); writer.Write((byte)urbanLevel); writer.Write((byte)farmLevel); writer.Write((byte)plantLevel); writer.Write((byte)specialIndex); writer.Write(walled); writer.Write(hasIncomingRiver); writer.Write((byte)incomingRiver); writer.Write(hasOutgoingRiver); writer.Write((byte)outgoingRiver);
Ahora, para devolver estos números, tenemos que usar BinaryReader.ReadByte
. La conversión de byte a entero se realiza implícitamente, por lo que no necesitamos agregar conversiones explícitas. terrainTypeIndex = reader.ReadByte(); elevation = reader.ReadByte(); RefreshPosition(); waterLevel = reader.ReadByte(); urbanLevel = reader.ReadByte(); farmLevel = reader.ReadByte(); plantLevel = reader.ReadByte(); specialIndex = reader.ReadByte(); walled = reader.ReadBoolean(); hasIncomingRiver = reader.ReadBoolean(); incomingRiver = (HexDirection)reader.ReadByte(); hasOutgoingRiver = reader.ReadBoolean(); outgoingRiver = (HexDirection)reader.ReadByte();
Entonces nos deshacemos de tres bytes por entero, lo que ahorra 27 bytes por celda. Ahora gastamos 18 bytes por celda, y solo 5,400 bytes por 300 celdas.Vale la pena señalar que los datos de la tarjeta anterior dejan de tener sentido en esta etapa. Al cargar el antiguo guardado, los datos se mezclan y obtenemos celdas confusas. Esto se debe a que ahora estamos leyendo menos datos. Si leemos más datos que antes, obtendríamos un error al intentar leer más allá del final del archivo.La incapacidad para procesar datos antiguos nos conviene, porque estamos en el proceso de determinar el formato. Pero cuando decidimos el formato de guardado, necesitaremos asegurarnos de que el código futuro siempre pueda leerlo. Incluso si cambiamos el formato, idealmente aún deberíamos poder leer el formato anterior.River Byte Union
En esta etapa, usamos cuatro bytes para almacenar datos del río, dos por dirección. Para cada dirección, almacenamos la presencia del río y la dirección en la que fluye.Parece obvio que no necesitamos almacenar la dirección del río si no es así. Esto significa que las celdas sin un río necesitan dos bytes menos. De hecho, un byte en la dirección del río será suficiente para nosotros, independientemente de su existencia.Tenemos seis direcciones posibles, que se almacenan como números en el intervalo 0-5. Tres bits son suficientes para esto, porque en forma binaria los números del 0 al 5 se parecen a 000, 001, 010, 011, 100, 101 y 110. Es decir, un byte más permanece sin usar cinco bits más. Podemos usar uno de ellos para indicar si existe un río. Por ejemplo, puede usar el octavo bit, que corresponde al número 128.Para hacer esto, le agregaremos 128 antes de convertir la dirección en bytes. Es decir, si tenemos un río que fluye hacia el noroeste, escribiremos 133, que en forma binaria es 10000101. Y si no hay río, simplemente escribimos un byte cero.Al mismo tiempo, cuatro bits más permanecen sin usar, pero esto es normal. Podemos combinar ambas direcciones del río en un byte, pero esto ya será demasiado confuso.
Para decodificar los datos del río, primero tenemos que volver a leer el byte. Si su valor no es inferior a 128, esto significa que hay un río. Para obtener su dirección, resta 128 y luego convierte a HexDirection
.
Como resultado, obtuvimos 16 bytes por celda. La mejora parece no ser grande, pero este es uno de esos trucos que se utilizan para reducir el tamaño de los datos binarios.Guardar carreteras en un byte
Podemos usar un truco similar para comprimir los datos del camino. Tenemos seis valores booleanos que se pueden almacenar en los primeros seis bits de un byte. Es decir, cada dirección de la carretera está representada por un número que es una potencia de dos. Estos son 1, 2, 4, 8, 16 y 32, o en forma binaria 1, 10, 100, 1000, 10000 y 100000.Para crear un byte terminado, necesitamos establecer los bits que corresponden a las direcciones utilizadas de las carreteras. Para obtener la dirección correcta para la dirección, podemos usar el operador <<
. Luego combínelos usando el operador OR bit a bit. Por ejemplo, si se utilizan las carreteras primera, segunda, tercera y sexta, el byte final será 100111. int roadFlags = 0; for (int i = 0; i < roads.Length; i++) {
¿Cómo funciona << funciona?. integer . . integer . , . 1 << n
2 n , .
Para recuperar el valor booleano de la carretera, debe verificar si el bit está configurado. Si es así, enmascare todos los demás bits utilizando el operador AND bit a bit con el número apropiado. Si el resultado no es igual a cero, se establece el bit y existe el camino. int roadFlags = reader.ReadByte(); for (int i = 0; i < roads.Length; i++) { roads[i] = (roadFlags & (1 << i)) != 0; }
Habiendo exprimido seis bytes en uno, recibimos 11 bytes por celda. Con 300 celdas, esto es solo 3,300 bytes. Es decir, después de trabajar un poco con bytes, redujimos el tamaño del archivo en un 75%.Preparándose para el futuro
Antes de declarar nuestro formato de guardado completo, agregamos un detalle más. Antes de guardar los datos del mapa, forzaremos a HexMapEditor
escribir un entero cero. public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(0); hexGrid.Save(writer); } }
Esto agregará cuatro bytes vacíos al comienzo de nuestros datos. Es decir, antes de cargar la tarjeta, tenemos que leer estos cuatro bytes. public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { reader.ReadInt32(); hexGrid.Load(reader); } }
Aunque estos bytes son inútiles hasta ahora, se usan como un encabezado que proporcionará compatibilidad con versiones anteriores en el futuro. Si no hubiéramos agregado estos bytes nulos, entonces el contenido de los primeros bytes dependía de la primera celda del mapa. Por lo tanto, en el futuro sería más difícil para nosotros averiguar qué versión del formato de guardado estamos tratando. Ahora podemos verificar los primeros cuatro bytes. Si están vacíos, entonces estamos tratando con una versión de formato 0. En futuras versiones, será posible agregar algo más allí.Es decir, si el título no es cero, estamos tratando con alguna versión desconocida. Como no podemos averiguar qué datos hay, debemos negarnos a descargar el mapa. using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 0) { hexGrid.Load(reader); } else { Debug.LogWarning("Unknown map format " + header); } }
paquete de la unidadParte 13: gestión de tarjetas
- Creamos nuevas cartas en el modo Jugar.
- Agregue soporte para varios tamaños de tarjeta.
- Agregue el tamaño del mapa a los datos guardados.
- Guarda y carga mapas arbitrarios.
- Mostrar una lista de tarjetas.
En esta parte, agregaremos soporte para varios tamaños de tarjetas, así como guardaremos diferentes archivos.A partir de esta parte, se crearán tutoriales en Unity 5.5.0.El comienzo de la biblioteca de mapas.Crear nuevos mapas
Hasta este punto, creamos la cuadrícula hexagonal solo una vez, al cargar la escena. Ahora haremos posible comenzar un nuevo mapa en cualquier momento. La nueva tarjeta simplemente reemplazará a la actual.En Awake HexGrid
, se inicializan algunas métricas y luego se determina el número de celdas y se crean los fragmentos y celdas necesarios. Al crear un nuevo conjunto de fragmentos y celdas, creamos un nuevo mapa. Dividámonos HexGrid.Awake
en dos partes: el código fuente de inicialización y el método general CreateMap
. void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; CreateMap(); } public void CreateMap () { cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); }
Agregue un botón en la interfaz de usuario para crear un nuevo mapa. Lo hice grande y lo coloqué debajo de los botones guardar y cargar.Nuevo botón de mapa.Conectemos el evento On Click de este botón con el método de CreateMap
nuestro objeto HexGrid
. Es decir, no pasaremos por el Editor de mapas hexadecimales , sino que llamaremos directamente al método de objeto Cuadrícula hexadecimal .Crea un mapa haciendo clic.Borrar datos antiguos
Ahora, cuando hace clic en el botón Nuevo mapa , se creará un nuevo conjunto de fragmentos y celdas. Sin embargo, los antiguos no se eliminan automáticamente. Por lo tanto, como resultado, obtenemos varias mallas de mapa superpuestas entre sí. Para evitar esto, primero tenemos que deshacernos de los objetos viejos. Esto se puede hacer destruyendo todos los fragmentos actuales al principio CreateMap
. public void CreateMap () { if (chunks != null) { for (int i = 0; i < chunks.Length; i++) { Destroy(chunks[i].gameObject); } } … }
¿Podemos reutilizar objetos existentes?, . , . , — , .
¿Es posible destruir elementos secundarios como este en un bucle?Por supuesto .
Especifique el tamaño en celdas en lugar de fragmentos
Mientras establecemos el tamaño del mapa a través de los campos chunkCountX
y el chunkCountZ
objeto HexGrid
. Pero será mucho más conveniente indicar el tamaño del mapa en las celdas. Al mismo tiempo, incluso podemos cambiar el tamaño del fragmento en el futuro sin cambiar el tamaño de las tarjetas. Por lo tanto, cambiemos los roles de la cantidad de celdas y la cantidad de campos de fragmentos.
Esto conducirá a un error de compilación, ya que HexMapCamera
utiliza tamaños de fragmento para limitar su posición . Cambie HexMapCamera.ClampPosition
para que use directamente la cantidad de celdas que aún necesita. Vector3 ClampPosition (Vector3 position) { float xMax = (grid.cellCountX - 0.5f) * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = (grid.cellCountZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); return position; }
Un fragmento tiene un tamaño de 5 por 5 celdas, y los mapas por defecto tienen un tamaño de 4 por 3 fragmentos. Por lo tanto, para mantener las tarjetas iguales, tendremos que usar un tamaño de 20 por 15 celdas. Y aunque hemos asignado valores predeterminados en el código, el objeto de cuadrícula todavía no los usará automáticamente, porque los campos ya existían y estaban predeterminados a 0.Por defecto, la tarjeta tiene un tamaño de 20 por 15.Tamaños de tarjetas personalizados
El siguiente paso será el soporte para crear tarjetas de cualquier tamaño, no solo el tamaño predeterminado. Para hacer esto, agregue HexGrid.CreateMap
X y Z a los parámetros, que reemplazarán el número existente de celdas. En el interior, los Awake
llamaremos con el número actual de celdas. void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; CreateMap(cellCountX, cellCountZ); } public void CreateMap (int x, int z) { … cellCountX = x; cellCountZ = z; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); }
Sin embargo, esto funcionará correctamente solo con el número de celdas que es un múltiplo del tamaño del fragmento. De lo contrario, la división entera creará muy pocos fragmentos. Aunque podemos agregar soporte para fragmentos parcialmente llenos de celdas, prohíbamos el uso de tamaños que no correspondan a fragmentos.Podemos usar el operador %
para calcular el resto de dividir el número de celdas por el número de fragmentos. Si no es igual a cero, existe una discrepancia y no crearemos un nuevo mapa. Y mientras hacemos esto, agreguemos protección contra cero y tamaños negativos. public void CreateMap (int x, int z) { if ( x <= 0 || x % HexMetrics.chunkSizeX != 0 || z <= 0 || z % HexMetrics.chunkSizeZ != 0 ) { Debug.LogError("Unsupported map size."); return; } … }
Nuevo menú de tarjeta
En la etapa actual, el botón Nuevo mapa ya no funciona, porque el método HexGrid.CreateMap
ahora tiene dos parámetros. No podemos conectar directamente los eventos de Unity con tales métodos. Además, para admitir diferentes tamaños de tarjetas, necesitamos algunos botones. En lugar de agregar todos estos botones a la interfaz de usuario principal, creemos un menú emergente separado.Agregue un nuevo lienzo a la escena ( GameObject / UI / Canvas ). Utilizaremos la misma configuración que el lienzo existente, excepto que su orden de clasificación debería ser igual a 1. Gracias a esto, estará en la parte superior de la interfaz de usuario del editor principal. Hice que tanto el lienzo como el sistema de eventos fueran hijos del nuevo objeto de interfaz de usuario para que la jerarquía de la escena permanezca limpia.Menú de lienzo Nuevo mapa.Agregue un panel al Nuevo menú del mapa que cierre toda la pantalla. Es necesario para oscurecer el fondo y no permitir que el cursor interactúe con todo lo demás cuando el menú está abierto. Le di un color uniforme, borrando su imagen de origen y configuré (0, 0, 0, 200) como el color .Configuración de imagen de fondo.Agregue una barra de menú al centro del lienzo, similar a los paneles del Editor de mapas hexadecimales . Creemos una etiqueta y botones claros para sus tarjetas pequeñas, medianas y grandes. También le agregaremos un botón de cancelación en caso de que el jugador cambie de opinión. Una vez que haya terminado de crear el diseño, desactive todo el Menú Nuevo mapa .Nuevo menú de mapa.Para administrar el menú, cree un componente NewMapMenu
y agréguelo al objeto Nuevo menú del mapa del lienzo . Para crear un nuevo mapa, necesitamos acceso al objeto Hex Grid . Por lo tanto, le agregamos un campo común y lo conectamos. using UnityEngine; public class NewMapMenu : MonoBehaviour { public HexGrid hexGrid; }
Componente del nuevo menú del mapa.Apertura y cierre
Podemos abrir y cerrar el menú emergente simplemente activando y desactivando el objeto del lienzo. Agreguemos NewMapMenu
dos métodos comunes para hacer esto. public void Open () { gameObject.SetActive(true); } public void Close () { gameObject.SetActive(false); }
Ahora conecte el botón New Map UI del editor al método Open
en el objeto New Map Menu .Abrir el menú presionando.También conecte el botón Cancelar al método Close
. Esto nos permitirá abrir y cerrar el menú emergente.Crear nuevos mapas
Para crear nuevos mapas, necesitamos llamar al método en el objeto Hex GridCreateMap
. Además, después de eso, necesitamos cerrar el menú emergente. Agregue al NewMapMenu
método que se ocupará de esto, teniendo en cuenta un tamaño arbitrario. void CreateMap (int x, int z) { hexGrid.CreateMap(x, z); Close(); }
Este método no debe ser general, porque todavía no podemos conectarlo directamente a eventos de botón. En su lugar, cree un método por botón que llame CreateMap
con el tamaño especificado. Para un mapa pequeño, utilicé un tamaño de 20 por 15, correspondiente al tamaño predeterminado del mapa. Para la tarjeta central, decidí duplicar este tamaño, obteniendo 40 por 30, y duplicarlo nuevamente para la tarjeta grande. Conecte los botones con los métodos adecuados. public void CreateSmallMap () { CreateMap(20, 15); } public void CreateMediumMap () { CreateMap(40, 30); } public void CreateLargeMap () { CreateMap(80, 60); }
Bloqueo de la cámara
¡Ahora podemos usar el menú emergente para crear nuevos mapas con tres tamaños diferentes! Todo funciona bien, pero debemos cuidar algunos detalles. Cuando el menú Nuevo mapa está activo, ya no podemos interactuar con la interfaz de usuario del editor y editar celdas. Sin embargo, aún podemos controlar la cámara. Idealmente, con el menú abierto, la cámara debería bloquearse.Como solo tenemos una cámara, una solución rápida y pragmática es simplemente agregarle una propiedad estática Locked
. Para un uso generalizado, esta solución no es muy adecuada, pero para nuestra interfaz simple es suficiente. Esto requiere que rastreemos la instancia estática en el interior HexMapCamera
, que se configura cuando la cámara Despierta. static HexMapCamera instance; … void Awake () { instance = this; swivel = transform.GetChild(0); stick = swivel.GetChild(0); }
Una propiedad Locked
puede ser una propiedad booleana estática simple solo con un setter. Todo lo que hace es apagar la instancia HexMapCamera
cuando está bloqueada y encenderla cuando está desbloqueada. public static bool Locked { set { instance.enabled = !value; } }
Ahora NewMapMenu.Open
puede bloquear la cámara y NewMapMenu.Close
desbloquearla. public void Open () { gameObject.SetActive(true); HexMapCamera.Locked = true; } public void Close () { gameObject.SetActive(false); HexMapCamera.Locked = false; }
Mantener la posición correcta de la cámara.
Hay otro problema probable con la cámara. Al crear un nuevo mapa que es más pequeño que el actual, la cámara puede aparecer fuera de los bordes del mapa. Ella permanecerá allí hasta que el jugador intente mover la cámara. Y solo entonces estará limitado por los límites del nuevo mapa.Para resolver este problema, podemos agregar al HexMapCamera
método estático ValidatePosition
. Llamar a un método de AdjustPosition
instancia con un desplazamiento cero obligará a la cámara a moverse a los bordes del mapa. Si la cámara ya está dentro de los bordes del nuevo mapa, permanecerá en su lugar. public static void ValidatePosition () { instance.AdjustPosition(0f, 0f); }
Llame al método dentro NewMapMenu.CreateMap
después de crear un nuevo mapa. void CreateMap (int x, int z) { hexGrid.CreateMap(x, z); HexMapCamera.ValidatePosition(); Close(); }
paquete de la unidadGuardar tamaño de mapa
Aunque podemos crear tarjetas de diferentes tamaños, no se tiene en cuenta al guardar y cargar. Esto significa que cargar un mapa provocará un error o un mapa incorrecto si el tamaño del mapa actual no coincide con el tamaño del mapa cargado.Para resolver este problema, antes de cargar los datos de la celda, necesitamos crear un nuevo mapa del tamaño apropiado. Digamos que tenemos un pequeño mapa guardado. En este caso, todo estará bien si creamos HexGrid.Load
un mapa de 20 por 15 al principio . public void Load (BinaryReader reader) { CreateMap(20, 15); for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } for (int i = 0; i < chunks.Length; i++) { chunks[i].Refresh(); } }
Tamaño de la tarjeta de almacenamiento
Por supuesto, podemos almacenar una tarjeta de cualquier tamaño. Por lo tanto, una solución generalizada será guardar el tamaño del mapa frente a estas celdas. public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } }
Entonces podemos obtener el tamaño verdadero y usarlo para crear un mapa con los tamaños correctos. public void Load (BinaryReader reader) { CreateMap(reader.ReadInt32(), reader.ReadInt32()); … }
Como ahora podemos cargar mapas de diferentes tamaños, nuevamente nos enfrentamos con el problema de la posición de la cámara. Lo resolveremos verificando su posición HexMapEditor.Load
después de cargar el mapa. public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 0) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
Nuevo formato de archivo
Aunque este enfoque funciona con tarjetas que conservaremos en el futuro, no funcionará con las antiguas. Y viceversa: el código de la parte anterior del tutorial no podrá cargar correctamente nuevos archivos de mapa. Para distinguir entre formatos antiguos y nuevos, aumentaremos el valor entero del encabezado. El antiguo formato de guardar sin un tamaño de mapa tenía la versión 0. El nuevo formato con un tamaño de mapa tendrá la versión 1. Por lo tanto, al grabar, HexMapEditor.Save
debe escribir 1 en lugar de 0. public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(1); hexGrid.Save(writer); } }
A partir de ahora, las tarjetas se guardarán como versión 1. Si intentamos abrirlas en el ensamblaje del tutorial anterior, se negarán a cargar e informar sobre un formato de tarjeta desconocido. De hecho, esto sucederá si ya intentamos cargar dicha tarjeta. Debe cambiar el método HexMapEditor.Load
para que acepte la nueva versión. public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 1) { hexGrid.Load(reader); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
Compatibilidad con versiones anteriores
De hecho, si queremos, aún podemos descargar mapas de la versión 0, suponiendo que todos tengan el mismo tamaño 20 por 15. Es decir, el título no tiene que ser 1, también puede ser cero. Como cada versión requiere su propio enfoque, HexMapEditor.Load
debe pasar el encabezado al método HexGrid.Load
. if (header <= 1) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); }
Agregue un HexGrid.Load
título al parámetro y úselo para tomar decisiones sobre acciones adicionales. Si el encabezado no es inferior a 1, debe leer los datos del tamaño de la tarjeta. De lo contrario, usamos el tamaño de tarjeta fija anterior de 20 por 15 y omitimos la lectura de los datos de tamaño. public void Load (BinaryReader reader, int header) { int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } CreateMap(x, z); … }
archivo de mapa versión 0Verificación del tamaño de la tarjeta
Al igual que con la creación de un nuevo mapa, es teóricamente posible que tengamos que cargar un mapa que sea incompatible con el tamaño del fragmento. Cuando esto sucede, debemos interrumpir la descarga de la tarjeta. HexGrid.CreateMap
Ya se niega a crear un mapa y muestra un error en la consola. Para decirle esto al llamador del método, regresemos un bool que indique si se creó el mapa. public bool CreateMap (int x, int z) { if ( x <= 0 || x % HexMetrics.chunkSizeX != 0 || z <= 0 || z % HexMetrics.chunkSizeZ != 0 ) { Debug.LogError("Unsupported map size."); return false; } … return true; }
Ahora, HexGrid.Load
también puede detener la ejecución cuando falla la creación del mapa. public void Load (BinaryReader reader, int header) { int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } if (!CreateMap(x, z)) { return; } … }
Como la carga sobrescribe todos los datos en las celdas existentes, no necesitamos crear un nuevo mapa si se carga un mapa del mismo tamaño. Por lo tanto, este paso se puede omitir. if (x != cellCountX || z != cellCountZ) { if (!CreateMap(x, z)) { return; } }
paquete de la unidadGestión de archivos
Podemos guardar y cargar tarjetas de diferentes tamaños, pero siempre escribimos y leemos test.map . Ahora agregaremos soporte para diferentes archivos.En lugar de guardar o cargar directamente el mapa, utilizamos otro menú emergente que proporciona administración avanzada de archivos. Cree otro lienzo, como en el Menú Nuevo mapa , pero esta vez lo llamaremos Menú Guardar carga . Este menú guardará y cargará mapas, dependiendo del botón presionado para abrirlo.Crearemos el diseño del menú Guardar carga .como si fuera un menú para guardar. Más tarde lo convertiremos dinámicamente en un menú de arranque. Al igual que otro menú, debe tener un fondo y una barra de menú, una etiqueta de menú y un botón de cancelar. Luego agregue una vista de desplazamiento ( GameObject / UI / Scroll View ) al menú para mostrar una lista de archivos. A continuación, insertamos el campo de entrada ( GameObject / UI / Input Field ) para indicar los nombres de las nuevas tarjetas. También necesitamos un botón de acción para guardar el mapa. Y finalmente agregue un botón Eliminar para eliminar tarjetas innecesarias.Diseño Guardar Cargar Menú.Por defecto, la vista de desplazamiento permite el desplazamiento horizontal y vertical, pero solo necesitamos una lista con desplazamiento vertical. Por lo tanto, desactivar el desplazamiento horizontal y desenchufe la barra de desplazamiento horizontal. También configuramos el Tipo de movimiento para bloquear y deshabilitar Inercia para que la lista parezca más restrictiva.Opciones de la lista de archivos.Eliminaremos el elemento secundario Barra de desplazamiento horizontal del objeto Lista de archivos , porque no lo necesitamos. Luego, cambie el tamaño de la barra de desplazamiento vertical para que llegue al final de la lista.Marcador de posición objeto de texto Nombre de la entrada se puede cambiar en sus hijos marcador de posición . Usé texto descriptivo, pero puedes dejarlo en blanco y deshacerte del marcador de posición.Se modificó el diseño del menú.Hemos terminado con el diseño y ahora desactivamos el menú para que, por defecto, esté oculto.Gestión de menú
Para que el menú funcione, necesitamos otro script, en este caso - SaveLoadMenu
. Al igual que NewMapMenu
, necesita un enlace a la cuadrícula, así como métodos Open
y Close
. using UnityEngine; public class SaveLoadMenu : MonoBehaviour { public HexGrid hexGrid; public void Open () { gameObject.SetActive(true); HexMapCamera.Locked = true; } public void Close () { gameObject.SetActive(false); HexMapCamera.Locked = false; } }
Agregue este componente a SaveLoadMenu y dele un enlace al objeto de cuadrícula.Componente SaveLoadMenu.Se abrirá un menú para guardar o cargar. Para simplificar el trabajo, agregue un Open
parámetro booleano al método . Determina si el menú debe estar en modo guardar. Realizaremos un seguimiento de este modo en el campo para saber qué acción realizar más adelante. bool saveMode; public void Open (bool saveMode) { this.saveMode = saveMode; gameObject.SetActive(true); HexMapCamera.Locked = true; }
Ahora combinar los botones Guardar y Cargar objetos Hex Editor de mapas con el método Open
del objeto Guardar Cargar el menú . Verifique el parámetro booleano solo para el botón Guardar .Abrir el menú en modo guardar.Si aún no lo ha hecho, conecte el evento del botón Cancelar al método Close
. Ahora Guardar Cargar menú puede ser abierta y cerrada.Cambio en la apariencia
Creamos el menú como un menú para guardar, pero su modo está determinado por el botón presionado para abrir. Necesitamos cambiar la apariencia del menú dependiendo del modo. En particular, necesitamos cambiar la etiqueta del menú y la etiqueta del botón de acción. Esto significa que necesitaremos enlaces a estas etiquetas. using UnityEngine; using UnityEngine.UI; public class SaveLoadMenu : MonoBehaviour { public Text menuLabel, actionButtonLabel; … }
Conexión con etiquetas.Cuando el menú se abre en modo guardar, usamos las etiquetas existentes, es decir, Guardar mapa para el menú y Guardar para el botón de acción. De lo contrario, estamos en modo de carga, es decir, usamos Load Map y Load . public void Open (bool saveMode) { this.saveMode = saveMode; if (saveMode) { menuLabel.text = "Save Map"; actionButtonLabel.text = "Save"; } else { menuLabel.text = "Load Map"; actionButtonLabel.text = "Load"; } gameObject.SetActive(true); HexMapCamera.Locked = true; }
Ingrese el nombre de la tarjeta
Dejemos la lista de archivos por ahora. El usuario puede especificar el archivo guardado o descargado ingresando el nombre de la tarjeta en el campo de entrada. Para obtener estos datos, necesitamos una referencia al componente InputField
del objeto de entrada de nombre . public InputField nameInput;
Conexión al campo de entrada.No es necesario obligar al usuario a ingresar la ruta completa al archivo de mapa. Bastará solo el nombre de la tarjeta sin la extensión .map . Agreguemos un método que toma la entrada del usuario y crea la ruta correcta para ello. Esto no es posible cuando la entrada está vacía, por lo que en este caso volveremos null
. using UnityEngine; using UnityEngine.UI; using System.IO; public class SaveLoadMenu : MonoBehaviour { … string GetSelectedPath () { string mapName = nameInput.text; if (mapName.Length == 0) { return null; } return Path.Combine(Application.persistentDataPath, mapName + ".map"); } }
¿Qué sucede si el usuario ingresa caracteres no válidos?, . , , .
Content Type . , - , . , , .
Guardar y cargar
Ahora se dedicará a guardar y cargar SaveLoadMenu
. Por lo tanto, nos movemos los métodos Save
y Load
de la HexMapEditor
en SaveLoadMenu
. Ya no tienen que compartirse y funcionarán con el parámetro de ruta en lugar de la ruta fija. void Save (string path) {
Como ahora estamos cargando archivos arbitrarios, sería bueno verificar que el archivo realmente exista, y solo entonces intentar leerlo. Si no es así, arrojamos un error y terminamos la operación. void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } … }
Ahora agregue el método general Action
. Comienza con la obtención de la ruta seleccionada por el usuario. Si hay una ruta, guárdela o cárguela. Luego cierra el menú. public void Action () { string path = GetSelectedPath(); if (path == null) { return; } if (saveMode) { Save(path); } else { Load(path); } Close(); }
Al adjuntar un evento de Botón de acción a este método , podemos guardar y cargar usando nombres de mapas arbitrarios. Como no restablecemos el campo de entrada, el nombre seleccionado permanecerá hasta el próximo guardado o carga. Esto es conveniente para guardar o cargar desde un archivo varias veces seguidas, por lo que no cambiaremos nada.Elementos de la lista de mapas
A continuación, completaremos la lista de archivos con todas las tarjetas que se encuentran en la ruta de almacenamiento de datos. Cuando hace clic en uno de los elementos de la lista, se utilizará como texto en la entrada de nombre . Agregue un SaveLoadMenu
método general para esto. public void SelectItem (string name) { nameInput.text = name; }
Necesitamos algo que sea un elemento de la lista. El botón habitual servirá. Créelo y reduzca la altura a 20 unidades para que no ocupe mucho espacio verticalmente. No debería verse como un botón, por lo que borraremos el enlace de la imagen de origen de su componente de imagen . En este caso, se volverá completamente blanco. Además, nos aseguraremos de que la etiqueta esté alineada a la izquierda y de que haya espacio entre el texto y el lado izquierdo del botón. Habiendo terminado con el diseño del botón, lo convertimos en un prefabricado.El botón es un elemento de la lista.No podemos conectar directamente el evento del botón al Nuevo menú del mapa , porque es un prefabricado y aún no existe en la escena. Por lo tanto, un elemento de menú necesita un enlace al menú para poder invocar un método cuando se hace clic en él SelectItem
. También necesita hacer un seguimiento del nombre de la tarjeta que representa y establecer su texto. Vamos a crear un pequeño componente para esto SaveLoadItem
. using UnityEngine; using UnityEngine.UI; public class SaveLoadItem : MonoBehaviour { public SaveLoadMenu menu; public string MapName { get { return mapName; } set { mapName = value; transform.GetChild(0).GetComponent<Text>().text = value; } } string mapName; public void Select () { menu.SelectItem(mapName); } }
Agregue un componente al elemento del menú y haga que el botón llame a su método Select
.Componente del artículo.Relleno de lista
Para completar la lista, SaveLoadMenu
necesita un enlace al contenido dentro de la ventana gráfica del objeto Lista de archivos . También necesita un enlace al elemento prefabricado. public RectTransform listContent; public SaveLoadItem itemPrefab;
Mezcle el contenido de la lista y prefabricado.Utilizamos un nuevo método para completar esta lista. El primer paso es identificar los archivos de mapas existentes. Para obtener una matriz de todas las rutas de archivos dentro del directorio, podemos usar el método Directory.GetFiles
. Este método tiene un segundo parámetro que le permite filtrar archivos. En nuestro caso, solo se requieren archivos que coincidan con la máscara * .map . void FillList () { string[] paths = Directory.GetFiles(Application.persistentDataPath, "*.map"); }
Lamentablemente, el orden de los archivos no está garantizado. Para mostrarlos en orden alfabético, necesitamos ordenar la matriz con System.Array.Sort
. using UnityEngine; using UnityEngine.UI; using System; using System.IO; public class SaveLoadMenu : MonoBehaviour { … void FillList () { string[] paths = Directory.GetFiles(Application.persistentDataPath, "*.map"); Array.Sort(paths); } … }
A continuación, crearemos instancias prefabricadas para cada elemento de la matriz. Vincula el elemento al menú, establece su nombre de mapa y conviértelo en un elemento secundario del contenido de la lista. Array.Sort(paths); for (int i = 0; i < paths.Length; i++) { SaveLoadItem item = Instantiate(itemPrefab); item.menu = this; item.MapName = paths[i]; item.transform.SetParent(listContent, false); }
Como Directory.GetFiles
devuelve las rutas completas a los archivos, necesitamos borrarlos. Afortunadamente, esto es exactamente lo que hace que el método sea conveniente Path.GetFileNameWithoutExtension
. item.MapName = Path.GetFileNameWithoutExtension(paths[i]);
Antes de mostrar el menú, necesitamos completar una lista. Y dado que es probable que los archivos cambien, debemos hacer esto cada vez que abramos el menú. public void Open (bool saveMode) { … FillList(); gameObject.SetActive(true); HexMapCamera.Locked = true; }
Al volver a llenar la lista, debemos eliminar todos los anteriores antes de agregar nuevos elementos. void FillList () { for (int i = 0; i < listContent.childCount; i++) { Destroy(listContent.GetChild(i).gameObject); } … }
Artículos sin arreglo.Arreglo de puntos
Ahora la lista mostrará elementos, pero se superpondrán y estarán en una mala posición. Para convertirlos en una lista vertical, agregue el componente Grupo de diseño vertical ( Componente / Diseño / Grupo de diseño vertical ) al objeto Contenido de la lista . Para que la disposición funcione correctamente, habilite Ancho del tamaño de control secundario y Expandir fuerza secundaria . Ambas opciones de altura deben estar deshabilitadas.Uso de grupo de diseño vertical.Tenemos una hermosa lista de artículos. Sin embargo, el tamaño del contenido de la lista no se ajusta al número real de elementos. Por lo tanto, la barra de desplazamiento nunca cambia de tamaño. Podemos forzar que Content cambie de tamaño automáticamente agregando un componente Content Size Fitter ( Component / Layout / Content Size Fitter ). Su modo de ajuste vertical debe establecerse en tamaño preferido .Usando ajustador de tamaño de contenido.Ahora con un pequeño número de puntos, la barra de desplazamiento desaparecerá. Y cuando hay demasiados elementos en la lista que no caben en la ventana gráfica, aparece la barra de desplazamiento y tiene un tamaño apropiado.Aparece una barra de desplazamiento.Eliminación de la tarjeta
Ahora podemos trabajar convenientemente con muchos archivos de mapas. Sin embargo, a veces es necesario deshacerse de algunas cartas. Para hacer esto, puede usar el botón Eliminar . Creemos un método para esto y hagamos que el botón lo llame. Si hay una ruta seleccionada, simplemente elimínela con File.Delete
. public void Delete () { string path = GetSelectedPath(); if (path == null) { return; } File.Delete(path); }
Aquí también deberíamos comprobar que estamos trabajando con un archivo realmente existente. Si este no es el caso, entonces no deberíamos intentar eliminarlo, pero esto no conduce a un error. if (File.Exists(path)) { File.Delete(path); }
Después de retirar la tarjeta, no necesitamos cerrar el menú. Esto facilita la eliminación de varios archivos a la vez. Sin embargo, después de la eliminación, debemos borrar la entrada de nombre , así como actualizar la lista de archivos. if (File.Exists(path)) { File.Delete(path); } nameInput.text = ""; FillList();
paquete de la unidadParte 14: texturas en relieve
- Use colores de vértice para crear un mapa splat.
- Crear un activo de textura de matriz.
- Agregar índices de elevación a las mallas.
- Transiciones entre texturas en relieve.
Hasta este momento, utilizamos colores sólidos para las tarjetas para colorear. Ahora aplicaremos la textura.Dibujando texturas.Una mezcla de tres tipos.
Si bien los colores uniformes se distinguen claramente y se adaptan bastante a la tarea, no parecen muy interesantes. El uso de texturas aumentará significativamente el atractivo de los mapas. Por supuesto, para esto tenemos que mezclar texturas, no solo colores. En el tutorial de Rendering 3, Combinación de texturas, hablé sobre cómo mezclar múltiples texturas usando el mapa splat. En nuestros mapas hexagonales, puede usar un enfoque similar.En el tutorial de Rendering 3solo se mezclan cuatro texturas, y con un mapa splat podemos admitir hasta cinco texturas. Por el momento, utilizamos cinco colores diferentes, por lo que esto es muy adecuado para nosotros. Sin embargo, más adelante podemos agregar otros tipos. Por lo tanto, se necesita soporte para un número arbitrario de tipos de alivio. Cuando se utilizan propiedades de textura establecidas explícitamente, esto no es posible, por lo que debe usar una matriz de texturas. Más tarde lo crearemos.Cuando se usan matrices de texturas, de alguna manera necesitamos decirle al sombreador qué texturas mezclar. La mezcla más difícil es necesaria para triángulos angulares, que pueden estar entre tres celdas con su propio tipo de terreno. Por lo tanto, necesitamos un soporte de mezcla entre los tres tipos por triángulo.Usando colores de vértice como mapas Splat
Suponiendo que podamos decirte qué texturas mezclar, podemos usar los colores de vértice para crear un mapa splat para cada triángulo. Dado que en cada caso se utiliza un máximo de tres texturas, solo necesitamos tres canales de color. El rojo representará la primera textura, el verde, el segundo, y el azul, el tercero.Mapa de triángulo Splat.¿La suma del mapa triangular splat siempre es igual a uno?Si . . , (1, 0, 0) , (½, ½, 0) (⅓, ⅓, ⅓) .
Si un triángulo necesita solo una textura, usamos solo el primer canal. Es decir, su color será completamente rojo. En el caso de mezclar entre dos tipos diferentes, usamos el primer y el segundo canal. Es decir, el color del triángulo será una mezcla de rojo y verde. Y cuando se encuentren los tres tipos, será una mezcla de rojo, verde y azul.Tres configuraciones de mapas splat.Utilizaremos estas configuraciones de mapa splat independientemente de qué texturas se mezclen realmente. Es decir, el mapa splat siempre será el mismo. Solo las texturas cambiarán. Cómo hacer esto, lo descubriremos más tarde.Necesitamos cambiar HexGridChunk
para que cree estos mapas splat, en lugar de usar colores de celda. Como a menudo usaremos tres colores, crearemos campos estáticos para ellos. static Color color1 = new Color(1f, 0f, 0f); static Color color2 = new Color(0f, 1f, 0f); static Color color3 = new Color(0f, 0f, 1f);
Centros celulares
Comencemos reemplazando el color del centro de las celdas por defecto. Aquí no se realiza ninguna combinación, por lo que solo usamos el primer color, es decir, rojo. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, color1); … }
Centros rojos de células.Los centros celulares ahora se vuelven rojos. Todos usan la primera de las tres texturas, sin importar cuál sea la textura. Sus mapas splat son los mismos, independientemente del color con el que coloreamos las celdas.Barrio del río
Cambiamos segmentos solo dentro de las celdas sin ríos que fluyan a lo largo de ellas. Necesitamos hacer lo mismo para los segmentos adyacentes a los ríos. En nuestro caso, esto es tanto una tira de costilla como un abanico de triángulos de la costilla. Aquí también, solo el rojo es suficiente para nosotros. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … }
Segmentos rojos adyacentes a los ríos.Ríos
A continuación, debemos cuidar la geometría de los ríos dentro de las celdas. Todos ellos también deberían ponerse rojos. Para empezar, echemos un vistazo al principio y al final de los ríos. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … }
Y luego la geometría que compone las orillas y el lecho del río. He agrupado las llamadas al método de color para que el código sea más fácil de leer. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); terrain.AddTriangle(centerL, m.v1, m.v2);
Ríos rojos a lo largo de las celdas.Costillas
Todos los bordes son diferentes porque están entre celdas que pueden tener diferentes tipos de terreno. Usamos el primer color para el tipo de celda actual y el segundo color para el tipo vecino. Como resultado, el mapa splat se convertirá en un degradado rojo-verde, incluso si ambas celdas son del mismo tipo. Si ambas células usan la misma textura, entonces se convierte en una mezcla de la misma textura en ambos lados. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { TriangulateEdgeStrip(e1, color1, e2, color2, hasRoad); } … }
Costillas rojo-verdes, excluidas las repisas.¿No causaría problemas la transición brusca entre rojo y verde?, , . . splat map, . .
, .
Los bordes con las repisas son un poco más complicados porque tienen vértices adicionales. Afortunadamente, el código de interpolación existente funciona muy bien con los colores del mapa splat. Simplemente use el primer y segundo color, no los colores de las celdas del principio y el final. void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); TriangulateEdgeStrip(begin, color1, e2, c2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(color1, color2, i); TriangulateEdgeStrip(e1, c1, e2, c2, hasRoad); } TriangulateEdgeStrip(e2, c2, end, color2, hasRoad); }
Repisas de costillas rojo-verde.Ángulos
Los ángulos celulares son los más difíciles porque tienen que mezclar tres texturas diferentes. Usamos rojo para el pico inferior, verde para la izquierda y azul para la derecha. Comencemos con las esquinas de un triángulo. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
Esquinas rojo-verde-azul, excepto las repisas.Aquí podemos usar nuevamente el código de interpolación de color existente para esquinas con salientes. La interpolación justa se realiza entre tres, no dos colores. Primero, considere las repisas que no están cerca de los acantilados. void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(color1, color2, i); c4 = HexMetrics.TerraceLerp(color1, color3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadColor(c1, c2, c3, c4); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadColor(c3, c4, color2, color3); }
Columnas de esquina rojo-verde-azul, excepto las repisas a lo largo de los acantilados.Cuando se trata de acantilados, necesitamos usar un método TriangulateBoundaryTriangle
. Este método recibió las celdas de inicio y de izquierda como parámetros. Sin embargo, ahora necesitamos los colores de splat apropiados, que pueden variar según la topología. Por lo tanto, reemplazamos estos parámetros con colores. void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginColor, leftColor, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleColor(c1, c2, boundaryColor); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleColor(c2, leftColor, boundaryColor); }
Cámbielo TriangulateCornerTerracesCliff
para que use los colores correctos. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color3, b); TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } }
Y haz lo mismo para TriangulateCornerCliffTerraces
. void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color2, b); TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } }
Mapa completo de relieve splat.paquete de la unidadMatrices de texturas
Ahora que nuestro terreno tiene un mapa splat, podemos pasar la colección de texturas al sombreador. No podemos simplemente asignar un sombreador a una matriz de texturas de C #, porque la matriz debe existir en la memoria de la GPU como una entidad única. Tendremos que usar un objeto especial Texture2DArray
que haya sido compatible con Unity desde la versión 5.4.¿Todas las GPU admiten matrices de texturas?GPU , .
Unity .
- Direct3D 11/12 (Windows, Xbox One)
- OpenGL Core (Mac OS X, Linux)
- Metal (iOS, Mac OS X)
- OpenGL ES 3.0 (Android, iOS, WebGL 2.0)
- PlayStation 4
El maestro
Desafortunadamente, el soporte de Unity para matrices de texturas en la versión 5.5 es mínimo. No podemos simplemente crear un activo de matriz de textura y asignarle texturas. Tenemos que hacerlo manualmente. Podemos crear una variedad de texturas en el modo Reproducir o crear un activo en el editor. Vamos a crear un activo.¿Por qué crear un activo?, Play . , .
, . Unity . , . , .
Para crear una variedad de texturas, armaremos nuestro propio maestro. Cree un script TextureArrayWizard
y colóquelo dentro de la carpeta Editor . En cambio, MonoBehaviour
debería extender el tipo ScriptableWizard
desde el espacio de nombres UnityEditor
. using UnityEditor; using UnityEngine; public class TextureArrayWizard : ScriptableWizard { }
Podemos abrir el asistente a través de un método estático generalizado ScriptableWizard.DisplayWizard
. Sus parámetros son los nombres de la ventana del asistente y su botón de creación. Llamaremos a este método en un método estático CreateWizard
. static void CreateWizard () { ScriptableWizard.DisplayWizard<TextureArrayWizard>( "Create Texture Array", "Create" ); }
Para acceder al asistente a través del editor, necesitamos agregar este método al menú de Unity. Esto se puede hacer agregando un atributo al método MenuItem
. Añádalo al menú Activos , y más específicamente a los Activos / Crear / Matriz de texturas . [MenuItem("Assets/Create/Texture Array")] static void CreateWizard () { … }
Nuestro mago personalizado.Con el nuevo elemento del menú, puede abrir el menú emergente de nuestro asistente personalizado. No es muy bonito, pero es adecuado para resolver el problema. Sin embargo, todavía está vacío. Para crear una matriz de texturas, necesitamos una matriz de texturas. Agregue un campo general para el maestro. La GUI estándar del asistente lo muestra como lo hace un inspector estándar. public Texture2D[] textures;
Maestro con texturas.Vamos a crear algo
Cuando hace clic en el botón Crear del asistente, desaparece. Además, Unity se queja de que no hay ningún método OnWizardCreate
. Este es el método que se llama cuando se hace clic en el botón Crear, por lo que debemos agregarlo al asistente. void OnWizardCreate () { }
Aquí crearemos nuestra matriz de texturas. Al menos si el usuario agrega texturas al maestro. Si no, no hay nada que crear y el trabajo debe detenerse. void OnWizardCreate () { if (textures.Length == 0) { return; } }
El siguiente paso es solicitar la ubicación para guardar el activo de matriz de textura. Guarde el archivo del panel puede ser abierto por EditorUtility.SaveFilePanelInProject
. Sus parámetros definen el nombre del panel, el nombre de archivo predeterminado, la extensión y la descripción del archivo. Las matrices de textura utilizan la extensión de archivo de activos general . if (textures.Length == 0) { return; } EditorUtility.SaveFilePanelInProject( "Save Texture Array", "Texture Array", "asset", "Save Texture Array" );
SaveFilePanelInProject
devuelve la ruta del archivo seleccionado por el usuario. Si el usuario hizo clic en cancelar en este panel, la ruta será una cadena vacía. Por lo tanto, en este caso, debemos interrumpir el trabajo. string path = EditorUtility.SaveFilePanelInProject( "Save Texture Array", "Texture Array", "asset", "Save Texture Array" ); if (path.Length == 0) { return; }
Crear una variedad de texturas
Si tenemos el camino correcto, podemos avanzar y crear un nuevo objeto Texture2DArray
. Su método de construcción requiere especificar el ancho y la altura de la textura, la longitud de la matriz, el formato de las texturas y la necesidad de texturas mip. Estos parámetros deben ser los mismos para todas las texturas en la matriz. Para configurar el objeto, usamos la primera textura. El usuario debe verificar que todas las texturas tengan el mismo formato. if (path.Length == 0) { return; } Texture2D t = textures[0]; Texture2DArray textureArray = new Texture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount > 1 );
Dado que la matriz de textura es un recurso de GPU único, utiliza los mismos modos de filtrado y plegado para todas las texturas. Aquí nuevamente usamos la primera textura para configurarlo todo. Texture2DArray textureArray = new Texture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount > 1 ); textureArray.anisoLevel = t.anisoLevel; textureArray.filterMode = t.filterMode; textureArray.wrapMode = t.wrapMode;
Ahora podemos copiar la textura de una matriz mediante el método Graphics.CopyTexture
. El método copia datos de textura sin procesar, un nivel de mip a la vez. Por lo tanto, necesitamos recorrer todas las texturas y sus niveles de mip. Los parámetros del método son dos conjuntos que consisten en un recurso de textura, un índice y un nivel mip. Como las texturas originales no son matrices, su índice siempre es cero. textureArray.wrapMode = t.wrapMode; for (int i = 0; i < textures.Length; i++) { for (int m = 0; m < t.mipmapCount; m++) { Graphics.CopyTexture(textures[i], 0, m, textureArray, i, m); } }
En esta etapa, tenemos en memoria la matriz correcta de texturas, pero aún no es un activo. El último paso será llamar AssetDatabase.CreateAsset
con la matriz y su ruta. En este caso, los datos se escribirán en un archivo en nuestro proyecto y aparecerán en la ventana del proyecto. for (int i = 0; i < textures.Length; i++) { … } AssetDatabase.CreateAsset(textureArray, path);
Texturas
Para crear una matriz real de texturas, necesitamos las texturas originales. Aquí hay cinco texturas que coinciden con los colores que usamos hasta ahora. El amarillo se convierte en arena, el verde en hierba, el azul en tierra, el naranja en piedra y el blanco en nieve.Texturas de arena, hierba, tierra, piedra y nieve.Tenga en cuenta que estas texturas no son fotografías de este relieve. Estos son los patrones pseudoaleatorios fáciles que creé usando NumberFlow . Me esforcé por crear tipos de relieve reconocibles y detalles que no entren en conflicto con el relieve poligonal abstracto. El fotorrealismo resultó ser inadecuado para esto. Además, aunque los patrones agregan variabilidad, hay algunas características distintas en ellos que harían que las repeticiones se noten de inmediato.Agregue estas texturas a la matriz maestra, asegurándose de que su orden coincida con los colores. Es decir, primero arena, luego hierba, tierra, piedra y finalmente nieve.Creando una variedad de texturas.Después de crear el activo de matriz de textura, selecciónelo y examínelo en el inspector.Inspector de matriz de textura.Esta es la visualización más simple de una pieza de datos de matriz de textura. Tenga en cuenta que hay un interruptor Is Readable que se enciende inicialmente. Como no necesitamos leer datos de píxeles de la matriz, apáguelo. No podemos hacer esto en el asistente porque no Texture2DArray
hay métodos o propiedades para acceder a este parámetro.(En Unity 5.6, hay un error que estropea las matrices de texturas en ensamblajes en varias plataformas. Puede evitarlo sin deshabilitar Es legible ).También vale la pena señalar que hay un campo Espacio de coloral cual se le asigna el valor 1. Esto significa que se supone que las texturas están en el espacio gamma, lo cual es cierto. Si se suponía que estaban en un espacio lineal, entonces el campo tenía que establecerse en 0. En realidad, el diseñador Texture2DArray
tiene un parámetro adicional para especificar el espacio de color, pero Texture2D
no muestra si está en un espacio lineal o no, por lo tanto, en cualquier caso, debe establecer valor de forma manual.Shader
Ahora que tenemos una variedad de texturas, debemos enseñarle al sombreador cómo trabajar con ella. Por ahora, usamos el sombreador VertexColors para representar el terreno . Como ahora usaremos texturas en lugar de colores, cámbiele el nombre a Terrain . Luego convertimos su parámetro _MainTex en una matriz de texturas y le asignamos un activo. Shader "Custom/Terrain" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } … }
Material en relieve con una variedad de texturas.Para habilitar las matrices de texturas en todas las plataformas que las admiten, debe aumentar el nivel de destino del sombreador de 3.0 a 3.5. #pragma target 3.5
Dado que la variable _MainTex
ahora se refiere a una matriz de texturas, necesitamos cambiar su tipo. El tipo depende de la plataforma de destino y la macro se encargará de esto UNITY_DECLARE_TEX2DARRAY
.
Como en otros sombreadores, para muestrear la textura del relieve necesitamos las coordenadas del mundo XZ. Por lo tanto, agregaremos una posición en el mundo a la estructura de entrada del sombreador de superficie. También eliminamos las coordenadas UV predeterminadas, porque no las necesitamos. struct Input {
Para muestrear una variedad de texturas, necesitamos usar una macro UNITY_SAMPLE_TEX2DARRAY
. Para muestrear una matriz, necesita tres coordenadas. Los dos primeros son coordenadas UV normales. Utilizaremos las coordenadas mundiales XZ escaladas a 0.02. Entonces obtenemos una buena resolución de textura con un aumento total. Las texturas se repetirán aproximadamente cada cuatro celdas.La tercera coordenada se usa como el índice de la matriz de textura, como en una matriz regular. Como las coordenadas son flotantes, antes de indexar la matriz de GPU las redondea. Como hasta que sepamos qué textura se necesita, usemos siempre la primera. Además, el color del vértice no afectará el resultado final, porque es un mapa splat. void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.worldPos.xz * 0.02; fixed4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, float3(uv, 0)); Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Todo se ha convertido en arena.paquete de la unidadSelección de textura
Necesitamos un mapa de relieve que combine los tres tipos en un triángulo. Tenemos una variedad de texturas con una textura para cada tipo de terreno. Tenemos un sombreador que muestrea una variedad de texturas. Pero por ahora, no tenemos forma de decirle al sombreador qué texturas elegir para cada triángulo.Como cada triángulo mezcla hasta tres tipos, necesitamos asociar tres índices con cada triángulo. No podemos almacenar información para triángulos, por lo que tenemos que almacenar índices para vértices. Los tres vértices del triángulo simplemente almacenarán los mismos índices que con el color sólido.Datos de mallas
Podemos usar uno de los conjuntos de la malla UV para almacenar índices. Como se almacenan tres índices en cada vértice, los conjuntos de UV 2D existentes no serán suficientes. Afortunadamente, los conjuntos UV pueden contener hasta cuatro coordenadas. Por lo tanto, agregamos a la HexMesh
segunda lista Vector3
, a la que nos referiremos como tipos de relieve. public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; public bool useTerrainTypes; [NonSerialized] List<Vector3> vertices, terrainTypes;
Habilite los tipos de terreno para el hijo del terreno del prefabricado Hex Grid Chunk .Usamos tipos de alivio.Si es necesario, tomaremos otra lista Vector3
para los tipos de relieve durante la limpieza de la malla. public void Clear () { … if (useTerrainTypes) { terrainTypes = ListPool<Vector3>.Get(); } triangles = ListPool<int>.Get(); }
En el proceso de aplicar los datos de malla, guardamos los tipos de relieve en el tercer conjunto de UV. Debido a esto, no entrarán en conflicto con otros dos conjuntos, si alguna vez decidimos usarlos juntos. public void Apply () { … if (useTerrainTypes) { hexMesh.SetUVs(2, terrainTypes); ListPool<Vector3>.Add(terrainTypes); } hexMesh.SetTriangles(triangles, 0); … }
Para establecer los tipos de relieve del triángulo, usaremos Vector3
. Como los que son iguales para todo el triángulo, solo agregamos los mismos datos tres veces. public void AddTriangleTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); }
Mezclar en quad funciona igual. Los cuatro vértices son del mismo tipo. public void AddQuadTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); }
Fans de triángulos de costillas
Ahora necesitamos agregar tipos a los datos de malla HexGridChunk
. Vamos a comenzar con TriangulateEdgeFan
. Primero, en aras de una mejor legibilidad, separaremos las llamadas a los métodos de vértice y color. Recuerde que con cada llamada a este método, se lo pasamos a él color1
, para que podamos usar este color directamente y no aplicar el parámetro. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { terrain.AddTriangle(center, edge.v1, edge.v2);
Después de los colores, agregamos tipos de relieve. Dado que los tipos en el triángulo pueden ser diferentes, este debería ser un parámetro que reemplace el color. Use este tipo simple para crear Vector3
. Solo los primeros cuatro canales son importantes para nosotros, porque en este caso el mapa splat siempre es rojo. Como los tres componentes del vector deben asignarse, les asignaremos un tipo. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float type) { … Vector3 types; types.x = types.y = types.z = type; terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); }
Ahora necesitamos cambiar todas las llamadas a este método, reemplazando el argumento de color con un índice del tipo de terreno de la celda. Vnesom este cambio TriangulateWithoutRiver
, TriangulateAdjacentToRiver
y TriangulateWithRiverBeginOrEnd
.
En este punto, cuando inicie el modo de reproducción, aparecerán errores que le informarán que los terceros conjuntos de mallas UV están fuera de los límites. Esto sucedió porque todavía no agregamos tipos de relieve a cada triángulo y cuadrante. Entonces sigamos cambiando HexGridChunk
.Rayas de la costilla
Ahora, al crear una franja de borde, necesitamos saber qué tipos de terreno hay en ambos lados. Por lo tanto, los agregamos como parámetros y luego creamos un vector de tipos a cuyos dos canales se les asignan estos tipos. El tercer canal no es importante, por lo que solo hay que compararlo con el primero. Después de agregar los colores, agregue los tipos al quad. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, float type1, EdgeVertices e2, Color c2, float type2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); Vector3 types; types.x = types.z = type1; types.y = type2; terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } }
Ahora necesitamos cambiar los desafíos TriangulateEdgeStrip
. Primero TriangulateAdjacentToRiver
, TriangulateWithRiverBeginOrEnd
y TriangulateWithRiver
debe usar el tipo de celda para ambos lados de la tira elástica.
Luego, el caso más simple de un borde TriangulateConnection
debe usar el tipo de celda para el borde más cercano y el tipo vecino para el borde lejano. Pueden ser iguales o diferentes. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else {
Lo mismo se aplica a lo TriangulateEdgeTerraces
que se dispara tres veces TriangulateEdgeStrip
. Los tipos para las repisas son los mismos. void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); float t1 = beginCell.TerrainTypeIndex; float t2 = endCell.TerrainTypeIndex; TriangulateEdgeStrip(begin, color1, t1, e2, c2, t2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(color1, color2, i); TriangulateEdgeStrip(e1, c1, t1, e2, c2, t2, hasRoad); } TriangulateEdgeStrip(e2, c2, t1, end, color2, t2, hasRoad); }
Ángulos
El caso más simple de un ángulo es un triángulo simple. La celda inferior transfiere el primer tipo, el izquierdo el segundo y el derecho el tercero. Utilizándolos, cree un vector de tipos y agréguelo al triángulo. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); Vector3 types; types.x = bottomCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
Usamos el mismo enfoque en TriangulateCornerTerraces
, solo que aquí creamos un grupo de quad-s. void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(color1, color2, i); c4 = HexMetrics.TerraceLerp(color1, color3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadColor(c1, c2, c3, c4); terrain.AddQuadTerrainTypes(types); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadColor(c3, c4, color2, color3); terrain.AddQuadTerrainTypes(types); }
Al mezclar repisas y acantilados, necesitamos usar TriangulateBoundaryTriangle
. Simplemente dele un parámetro de tipo vector y agréguelo a todos sus triángulos. void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor, Vector3 types ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginColor, leftColor, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleColor(c1, c2, boundaryColor); terrain.AddTriangleTerrainTypes(types); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleColor(c2, leftColor, boundaryColor); terrain.AddTriangleTerrainTypes(types); }
En TriangulateCornerTerracesCliff
crear un vector de tipos basado en las celdas transferidas. Luego agrégalo a un triángulo y pasa TriangulateBoundaryTriangle
. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryColor = Color.Lerp(color1, color3, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } }
Lo mismo vale para TriangulateCornerCliffTerraces
. void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryColor = Color.Lerp(color1, color2, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } }
Ríos
El último método para cambiar es este TriangulateWithRiver
. Como aquí estamos en el centro de la celda, solo tratamos con el tipo de celda actual. Por lo tanto, cree un vector para él y agréguelo a triángulos y quad-s. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangleColor(color1); terrain.AddQuadColor(color1); terrain.AddQuadColor(color1); terrain.AddTriangleColor(color1); Vector3 types; types.x = types.y = types.z = cell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); … }
Tipo de mezcla
En esta etapa, las mallas contienen los índices de elevación necesarios. Todo lo que nos queda es obligar al sombreador del terreno a usarlos. Para que los índices caigan en el sombreador de fragmentos, primero debemos pasarlos a través del sombreador de vértices. Podemos hacer esto en nuestra propia función de vértice, como lo hicimos en el sombreador Estuario . En este caso, agregamos un campo a la estructura de entrada float3 terrain
y lo copiamos v.texcoord2.xyz
. #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 … struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; }; void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); data.terrain = v.texcoord2.xyz; }
Necesitamos muestrear la matriz de textura tres veces por fragmento. Por lo tanto, creemos una función conveniente para crear coordenadas de textura, muestrear una matriz y modular una muestra con un mapa splat para un índice. float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3(IN.worldPos.xz * 0.02, IN.terrain[index]); float4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, uvw); return c * IN.color[index]; } void surf (Input IN, inout SurfaceOutputStandard o) { … }
¿Podemos trabajar con un vector como matriz?Si - color[0]
color.r
. color[1]
color.g
, .
Con esta función, podemos simplemente muestrear la matriz de textura tres veces y combinar los resultados. void surf (Input IN, inout SurfaceOutputStandard o) { // float2 uv = IN.worldPos.xz * 0.02; fixed4 c = GetTerrainColor(IN, 0) + GetTerrainColor(IN, 1) + GetTerrainColor(IN, 2); o.Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Relieve texturizado.Ahora podemos pintar el relieve con texturas. Se mezclan como los colores sólidos. Como utilizamos las coordenadas mundiales como coordenadas UV, no cambian con la altura. Como resultado, a lo largo de los acantilados, las texturas se estiran. Si las texturas son bastante neutrales y muy variables, los resultados serán aceptables. De lo contrario, tenemos grandes estrías feas. Puede intentar ocultarlo con geometría adicional o textura de acantilados, pero en el tutorial no haremos esto.Barrer
Ahora, cuando usemos texturas en lugar de colores, será lógico cambiar el panel del editor. Podemos crear una interfaz hermosa que incluso puede mostrar texturas en relieve, pero me centraré en las abreviaturas que corresponden al estilo del esquema existente.Opciones de socorro.Además, la HexCell
propiedad de color ya no es necesaria, así que elimínela.
También HexGrid
puede eliminar una matriz de colores y código asociado.
Finalmente, tampoco se necesita una variedad de colores HexMetrics
.
paquete de la unidadParte 15: distancias
- Mostrar las líneas de la cuadrícula.
- Cambiar entre los modos de edición y navegación.
- Calcule la distancia entre celdas.
- Encontramos formas de sortear obstáculos.
- Tomamos en cuenta los costos variables de la mudanza.
Habiendo creado mapas de alta calidad, comenzaremos la navegación.El camino más corto no siempre es recto.Visualización de cuadrícula
La navegación en el mapa se realiza moviéndose de una celda a otra. Para llegar a algún lugar, debe pasar por una serie de celdas. Para facilitar la estimación de distancias, agreguemos la opción de mostrar la cuadrícula hexagonal en la que se basa nuestro mapa.Textura de malla
A pesar de las irregularidades de la malla del mapa, la malla subyacente es perfectamente plana. Podemos mostrar esto proyectando un patrón de cuadrícula en un mapa. Esto se puede lograr utilizando una textura de malla repetitiva.Repetición de textura de malla.La textura que se muestra arriba contiene una pequeña parte de la rejilla hexagonal que cubre 2 por 2 celdas. Esta área es rectangular, no cuadrada. Dado que la textura en sí es un cuadrado, el patrón se ve estirado. Al tomar muestras, debemos compensar esto.Proyección de cuadrícula
Para proyectar un patrón de malla, necesitamos agregar una propiedad de textura al sombreador Terrain . Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _GridTex ("Grid Texture", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 }
Material en relieve con textura de malla.Muestra la textura usando las coordenadas XZ del mundo, y luego multiplícala por albedo. Dado que las líneas de la cuadrícula en la textura son grises, esto entrelazará el patrón en el relieve. sampler2D _GridTex; … void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = GetTerrainColor(IN, 0) + GetTerrainColor(IN, 1) + GetTerrainColor(IN, 2); fixed4 grid = tex2D(_GridTex, IN.worldPos.xz); o.Albedo = c.rgb * grid * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Albedo multiplicado por malla fina.Necesitamos escalar el patrón para que coincida con las celdas en el mapa. La distancia entre los centros de las celdas vecinas es de 15, debe duplicarse para subir dos celdas. Es decir, necesitamos dividir las coordenadas de la cuadrícula V entre 30. El radio interno de las celdas es 5√3, y para mover dos celdas a la derecha, necesitamos cuatro veces más. Por lo tanto, es necesario dividir las coordenadas de la cuadrícula U por 20√3. float2 gridUV = IN.worldPos.xz; gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); fixed4 grid = tex2D(_GridTex, gridUV);
El tamaño de malla correcto.Ahora las líneas de la cuadrícula corresponden a las celdas del mapa. Al igual que las texturas en relieve, ignoran la altura, por lo que las líneas se estirarán a lo largo de los acantilados.Proyección sobre celdas con altura.La deformación de la malla generalmente no es tan mala, especialmente cuando se mira un mapa desde una gran distancia.Malla en la distancia.Inclusión de cuadrícula
Aunque mostrar una cuadrícula es conveniente, no siempre es obligatorio. Por ejemplo, debe desactivarlo cuando tome una captura de pantalla. Además, no todos prefieren ver la cuadrícula constantemente. Así que hagámoslo opcional. Agregaremos la directiva multi_compile al sombreador para crear opciones con y sin grilla. Para hacer esto, usaremos la palabra clave GRID_ON
. La compilación del sombreador condicional se describe en el tutorial de Rendering 5, Luces múltiples . #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 #pragma multi_compile _ GRID_ON
Al declarar una variable, grid
primero le asignamos un valor de 1. Como resultado, la cuadrícula se desactivará. Luego, probaremos la textura de la cuadrícula solo para la variante con una palabra clave específica GRID_ON
. fixed4 grid = 1; #if defined(GRID_ON) float2 gridUV = IN.worldPos.xz; gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); grid = tex2D(_GridTex, gridUV); #endif o.Albedo = c.rgb * grid * _Color;
Como la palabra clave GRID_ON
no está incluida en el sombreador del terreno, la cuadrícula desaparecerá. Para habilitarlo nuevamente, agregaremos un interruptor a la interfaz de usuario del editor de mapas. Para que esto sea posible, HexMapEditor
debo obtener un enlace al material del terreno y un método para habilitar o deshabilitar la palabra clave GRID_ON
. public Material terrainMaterial; … public void ShowGrid (bool visible) { if (visible) { terrainMaterial.EnableKeyword("GRID_ON"); } else { terrainMaterial.DisableKeyword("GRID_ON"); } }
Editor de hexágonos de marzo con referencia al material.Agregue un interruptor de cuadrícula a la interfaz de usuario y conéctelo al método ShowGrid
.Interruptor de rejilla.Guardar estado
Ahora en modo Play, podemos cambiar la visualización de la cuadrícula. En la primera prueba, la cuadrícula se apaga inicialmente y se vuelve visible cuando activamos el interruptor. Cuando lo apaga, la cuadrícula desaparecerá nuevamente. Sin embargo, si salimos del modo Reproducir cuando la cuadrícula esté visible, la próxima vez que inicie el modo Reproducir, se volverá a encender, aunque el interruptor esté apagado.Esto se debe a que estamos cambiando la palabra clave para el material de Terreno general . Estamos editando el activo material, por lo que el cambio se guarda en el editor de Unity. No se guardará en la asamblea.Para comenzar siempre el juego sin una grilla, desactivaremos la palabra clave GRID_ON
en Despertar HexMapEditor
. void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); }
paquete de la unidadModo de edición
Si queremos controlar el movimiento en el mapa, entonces necesitamos interactuar con él. Como mínimo, debemos seleccionar la celda como punto de partida de la ruta. Pero cuando hace clic en una celda, se editará. Podemos deshabilitar todas las opciones de edición manualmente, pero esto es inconveniente. Además, no queremos que se realicen cálculos de desplazamiento durante la edición del mapa. Así que agreguemos un interruptor que determina si estamos en modo de edición.Editar interruptor
Agregue al HexMapEditor
campo booleano editMode
, así como el método que lo define. Luego agregue otro interruptor a la interfaz de usuario para controlarlo. Comencemos con el modo de navegación, es decir, el modo de edición estará deshabilitado por defecto. bool editMode; … public void SetEditMode (bool toggle) { editMode = toggle; }
Interruptor de modo de edición.Para realmente deshabilitar la edición, haga que la llamada EditCells
dependa de editMode
. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } if (editMode) { EditCells(currentCell); } previousCell = currentCell; } else { previousCell = null; } }
Etiquetas de depuración
Hasta ahora no tenemos unidades para movernos por el mapa. En cambio, visualizamos distancias de movimiento. Para hacer esto, puede usar etiquetas de celda existentes. Por lo tanto, los haremos visibles cuando el modo de edición esté desactivado. public void SetEditMode (bool toggle) { editMode = toggle; hexGrid.ShowUI(!toggle); }
Como comenzamos con el modo de navegación, las etiquetas predeterminadas deberían estar habilitadas. Actualmente los HexGridChunk.Awake
desactiva, pero ya no debería hacerlo. void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ];
Coordinar etiquetas.Las coordenadas de celda ahora se hacen visibles inmediatamente después de iniciar el modo Play. Pero no necesitamos coordenadas, usamos etiquetas para mostrar distancias. Dado que esto requiere solo un número por celda, puede aumentar el tamaño de la fuente para que puedan leerse mejor. Cambie la prefabricación de la etiqueta de celda hexadecimal para que use una fuente en negrita con tamaño 8.Etiquetas con tamaño de fuente en negrita 8.Ahora, después de iniciar el modo Reproducir, veremos etiquetas grandes. Solo las primeras coordenadas de la celda son visibles, el resto no se coloca en la etiqueta.Etiquetas grandesComo ya no necesitamos las coordenadas, eliminaremos el HexGrid.CreateCell
valor en la asignación label.text
. void CreateCell (int x, int z, int i) { … Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z);
También puede eliminar el interruptor de etiquetas y su método asociado de la interfaz de usuario HexMapEditor.ShowUI
.
El cambio de método ya no existe.paquete de la unidadEncontrar distancias
Ahora que tenemos el modo de navegación etiquetado, podemos comenzar a mostrar distancias. Seleccionaremos una celda y luego mostraremos la distancia desde esta celda a todas las celdas en el mapa.Pantalla de distancia
Para rastrear la distancia a la celda, agregue al HexCell
campo entero distance
. Indicará la distancia entre esta celda y la seleccionada. Por lo tanto, para la celda seleccionada en sí, será cero, para el vecino inmediato es 1, y así sucesivamente. int distance;
Cuando se establece la distancia, debemos actualizar la etiqueta de la celda para mostrar su valor. HexCell
tiene una referencia al RectTransform
objeto UI. Tendremos que llamarlo GetComponent<Text>
para llegar a la celda. Considere lo que Text
hay en el espacio de nombres UnityEngine.UI
, así que úselo al comienzo del guión. void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance.ToString(); }
¿No deberíamos mantener un enlace directo al componente Texto?, . , , , . , .
Establezcamos la propiedad general para recibir y establezca la distancia a la celda, así como actualizar su etiqueta. public int Distance { get { return distance; } set { distance = value; UpdateDistanceLabel(); } }
Agregue al HexGrid
método general FindDistancesTo
con el parámetro de celda. Por ahora, simplemente estableceremos la distancia cero a cada celda. public void FindDistancesTo (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = 0; } }
Si el modo de edición no está habilitado, HexMapEditor.HandleInput
llamamos a un nuevo método con la celda actual. if (editMode) { EditCells(currentCell); } else { hexGrid.FindDistancesTo(currentCell); }
Distancias entre coordenadas
Ahora en modo de navegación, después de tocar una de ellas, todas las celdas muestran cero. Pero, por supuesto, deberían mostrar la verdadera distancia a la celda. Para calcular la distancia a ellos, podemos usar las coordenadas de la celda. Por lo tanto, suponga que HexCoordinates
tiene un método DistanceTo
y úselo HexGrid.FindDistancesTo
. public void FindDistancesTo (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = cell.coordinates.DistanceTo(cells[i].coordinates); } }
Ahora agregue al HexCoordinates
método DistanceTo
. Debe comparar sus propias coordenadas con las coordenadas de otro conjunto. Comencemos solo midiendo X, y restaremos las coordenadas X entre sí. public int DistanceTo (HexCoordinates other) { return x - other.x; }
Como resultado, obtenemos un desplazamiento a lo largo de X en relación con la celda seleccionada. Pero las distancias no pueden ser negativas, por lo que debe devolver la diferencia de coordenadas X módulo. return x < other.x ? other.x - x : x - other.x;
Distancias a lo largo de X.Por lo tanto, obtenemos las distancias correctas solo si tenemos en cuenta solo una dimensión. Pero hay tres dimensiones en una cuadrícula de hexágonos. Entonces, sumemos las distancias para las tres dimensiones y veamos qué nos da. return (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y) + (z < other.z ? other.z - z : z - other.z);
Suma de distancias XYZ.Resulta que tenemos el doble de distancia. Es decir, para obtener la distancia correcta, esta cantidad debe dividirse a la mitad. return ((x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y) + (z < other.z ? other.z - z : z - other.z)) / 2;
Distancias reales¿Por qué la suma es igual al doble de la distancia?, . , (1, −3, 2). . , . . , . .
. paquete de la unidadTrabajar con obstáculos
Las distancias calculadas por nosotros corresponden a las rutas más cortas desde la celda seleccionada a la celda de cada una. No podemos encontrar un camino más corto. Pero se garantiza que estos caminos serán correctos si la ruta no bloquea nada. Los acantilados, el agua y otros obstáculos pueden hacernos andar. Quizás algunas células no se puedan alcanzar en absoluto.Para encontrar un camino alrededor de los obstáculos, necesitamos usar un enfoque diferente en lugar de simplemente calcular la distancia entre las coordenadas. Ya no podemos examinar cada celda individualmente. Tendremos que buscar en el mapa hasta encontrar todas las celdas a las que se pueda llegar.Visualización de búsqueda
La búsqueda de mapas es un proceso iterativo. Para entender lo que estamos haciendo, sería útil ver cada etapa de la búsqueda. Podemos hacer esto convirtiendo el algoritmo de búsqueda en una rutina, para lo cual necesitamos un espacio de búsqueda System.Collections
. La frecuencia de actualización de 60 iteraciones por segundo es lo suficientemente pequeña como para que podamos ver lo que está sucediendo, y buscar en un mapa pequeño no nos llevó demasiado tiempo. public void FindDistancesTo (HexCell cell) { StartCoroutine(Search(cell)); } IEnumerator Search (HexCell cell) { WaitForSeconds delay = new WaitForSeconds(1 / 60f); for (int i = 0; i < cells.Length; i++) { yield return delay; cells[i].Distance = cell.coordinates.DistanceTo(cells[i].coordinates); } }
Debemos asegurarnos de que solo una búsqueda esté activa en un momento dado. Por lo tanto, antes de comenzar una nueva búsqueda, detenemos todas las corutinas. public void FindDistancesTo (HexCell cell) { StopAllCoroutines(); StartCoroutine(Search(cell)); }
Además, debemos completar la búsqueda al cargar un nuevo mapa. public void Load (BinaryReader reader, int header) { StopAllCoroutines(); … }
Breadth-First Search
Incluso antes de comenzar la búsqueda, sabemos que la distancia a la celda seleccionada es cero. Y, por supuesto, la distancia a todos sus vecinos es 1, si se puede llegar a ellos. Entonces podemos echar un vistazo a uno de estos vecinos. Es muy probable que esta celda tenga sus propios vecinos a los que se pueda llegar, y para los cuales aún no se ha calculado la distancia. Si es así, la distancia a estos vecinos debería ser 2. Podemos repetir este proceso para todos los vecinos a una distancia de 1. Después de eso, lo repetimos para todos los vecinos a una distancia de 2. Y así sucesivamente, hasta llegar a todas las celdas.Es decir, primero encontramos todas las celdas a una distancia de 1, luego encontramos todo a una distancia de 2, luego a una distancia de 3, y así sucesivamente, hasta que terminemos. Esto asegura que encontremos la distancia más pequeña a cada celda accesible. Este algoritmo se llama búsqueda de amplitud.Para que funcione, necesitamos saber si ya hemos determinado la distancia a la celda. A menudo, para esto, las celdas se colocan en una colección llamada conjunto listo o cerrado. Pero podemos establecer la distancia a la celda int.MaxValue
para indicar que aún no la hemos visitado. Necesitamos hacer esto para todas las celdas justo antes de realizar una búsqueda. IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } … }
También puede usar esto para ocultar todas las celdas no visitadas cambiando HexCell.UpdateDistanceLabel
. Después de eso, comenzaremos cada búsqueda en un mapa en blanco. void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance == int.MaxValue ? "" : distance.ToString(); }
A continuación, debemos rastrear las celdas que se deben visitar y el orden en que se visitan. Tal colección a menudo se llama borde o conjunto abierto. Solo necesitamos procesar las celdas en el mismo orden en que las conocimos. Puede usar una cola para hacer esto Queue
, que es parte del espacio de nombres System.Collections.Generic
. La celda seleccionada será la primera en colocarse en esta cola y tendrá una distancia de 0. IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); Queue<HexCell> frontier = new Queue<HexCell>(); cell.Distance = 0; frontier.Enqueue(cell);
A partir de este momento, el algoritmo ejecuta el ciclo mientras hay algo en la cola. En cada iteración, la celda frontal se recupera de la cola. frontier.Enqueue(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); }
Ahora tenemos la celda actual, que puede estar a cualquier distancia. Luego, necesitamos agregar a todos sus vecinos a la cola un paso más allá de la celda seleccionada. while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor != null) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); } } }
Pero deberíamos agregar solo aquellas celdas que aún no han recibido una distancia. if (neighbor != null && neighbor.Distance == int.MaxValue) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); }
Amplia búsqueda.Evitar el agua
Después de asegurarnos de que la búsqueda de amplitud encuentra las distancias correctas en el mapa monótono, podemos comenzar a agregar obstáculos. Esto se puede hacer al negarse a agregar celdas a la cola si se cumplen ciertas condiciones.De hecho, ya omitimos algunas celdas: aquellas que no existen y aquellas a las que ya les hemos indicado la distancia. Reescribamos el código para que en este caso omitamos a los vecinos explícitamente. for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor == null || neighbor.Distance != int.MaxValue) { continue; } neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); }
Omitamos también todas las celdas que están debajo del agua. Esto significa que cuando buscamos las distancias más cortas, consideramos solo el movimiento en el suelo. if (neighbor == null || neighbor.Distance != int.MaxValue) { continue; } if (neighbor.IsUnderwater) { continue; }
Distancias sin moverse a través del agua.El algoritmo todavía encuentra las distancias más cortas, pero ahora evita toda el agua. Por lo tanto, las células submarinas nunca ganan distancia, como áreas aisladas de tierra. La celda submarina solo recibe distancia si está seleccionada.Evitar los acantilados
Además, para determinar la posibilidad de visitar a un vecino, podemos usar el tipo de costilla. Por ejemplo, puede hacer que los acantilados bloqueen el camino. Si permite el movimiento en pendientes, entonces las celdas en el otro lado del acantilado aún se pueden alcanzar, solo en otros caminos. Por lo tanto, pueden estar a distancias muy diferentes. if (neighbor.IsUnderwater) { continue; } if (current.GetEdgeType(neighbor) == HexEdgeType.Cliff) { continue; }
Distancias sin cruzar acantilados.paquete de la unidadGastos de viaje
Podemos evitar celdas y bordes, pero estas opciones son binarias. Uno puede imaginar que es más fácil navegar en algunas direcciones que en otras. En este caso, la distancia se mide en trabajo o tiempo.Carreteras rápidas
Será lógico que sea más fácil y rápido viajar en carreteras, así que hagamos que la intersección de bordes con carreteras sea menos costosa. Dado que usamos valores enteros para establecer la distancia, dejaremos el costo de movernos por las carreteras igual a 1, y el costo de cruzar otros bordes aumentaremos a 10. Esta es una gran diferencia que nos permite ver de inmediato si obtenemos los resultados correctos. int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += 10; } neighbor.Distance = distance;
Caminos con distancias equivocadas.Ordenación de borde
Desafortunadamente, resulta que la búsqueda de amplitud no puede funcionar con costos de mudanza variables. Él supone que las celdas se agregan al borde en el orden de distancia creciente, y para nosotros esto ya no es relevante. Necesitamos una cola prioritaria, es decir, una cola que se clasifique a sí misma. No hay colas de prioridad estándar, porque no puede programarlas de tal manera que se adapten a todas las situaciones.Podemos crear nuestra propia cola prioritaria, pero permítanos optimizarla para el futuro tutorial. Por ahora, simplemente reemplazamos la cola con una lista que tendrá un método Sort
. List<HexCell> frontier = new List<HexCell>(); cell.Distance = 0; frontier.Add(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … neighbor.Distance = distance; frontier.Add(neighbor); } }
¿No puedo usar ListPool <HexCell>?, , . , , .
Para que el borde sea correcto, debemos ordenarlo después de agregarle una celda. De hecho, podemos posponer la clasificación hasta que se agreguen todos los vecinos de la celda, pero, repito, hasta que nos interesen las optimizaciones.Queremos ordenar las celdas por distancia. Para hacer esto, debemos llamar al método de clasificación de listas con un enlace al método que realiza esta comparación. frontier.Add(neighbor); frontier.Sort((x, y) => x.Distance.CompareTo(y.Distance));
¿Cómo funciona este método de clasificación?. , . .
frontier.Sort(CompareDistances); … static int CompareDistances (HexCell x, HexCell y) { return x.Distance.CompareTo(y.Distance); }
El borde ordenado sigue siendo incorrecto.Actualización de la frontera
Después de comenzar a ordenar el borde, comenzamos a obtener mejores resultados, pero todavía hay errores. Esto se debe a que cuando se agrega una celda al borde, no necesariamente encontramos la distancia más corta a esta celda. Esto significa que ahora ya no podemos saltear a los vecinos a los que ya se les ha asignado una distancia. En cambio, debemos verificar si hemos encontrado un camino más corto. Si es así, entonces necesitamos cambiar la distancia al vecino, en lugar de agregarlo al borde. HexCell neighbor = current.GetNeighbor(d); if (neighbor == null) { continue; } if (neighbor.IsUnderwater) { continue; } if (current.GetEdgeType(neighbor) == HexEdgeType.Cliff) { continue; } int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += 10; } if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; } frontier.Sort((x, y) => x.Distance.CompareTo(y.Distance));
Las distancias correctas.Ahora que tenemos las distancias correctas, comenzaremos a considerar los costos de mudanza. Puede notar que las distancias a algunas celdas son inicialmente demasiado grandes, pero se corrigen cuando se eliminan del borde. Este enfoque se llama algoritmo de Dijkstra, lleva el nombre del primero inventado por Edsger Dijkstra.Pendientes
No queremos limitarnos a costos diferentes solo para carreteras. Por ejemplo, puede reducir el costo de cruzar bordes planos sin carreteras a 5, dejando las pendientes sin carreteras con un valor de 10. HexEdgeType edgeType = current.GetEdgeType(neighbor); if (edgeType == HexEdgeType.Cliff) { continue; } int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; }
Para superar las pendientes necesitas hacer más trabajo, y las carreteras siempre son rápidas.Objetos de socorro
Podemos agregar costos en presencia de objetos en relieve. Por ejemplo, en muchos juegos es más difícil navegar por los bosques. En este caso, simplemente agregamos todos los niveles de objetos a la distancia. Y aquí nuevamente el camino acelera todo. if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; }
Los objetos se ralentizan si no hay camino.Las paredes
Finalmente, tomemos en cuenta las paredes. Las paredes deben bloquear el movimiento si el camino no las atraviesa. 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; }
Las paredes no nos dejan pasar, debes buscar la puerta.paquete de la unidad