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 20: la niebla de la guerra
- Guarde los datos de la celda en la textura.
- Cambiar los tipos de relieve sin triangulación.
- Seguimos la visibilidad.
- Oscurece todo lo invisible.
En esta parte, agregaremos el efecto de niebla de guerra al mapa.
Ahora la serie se creará en Unity 2017.1.0.
Ahora vemos que podemos y no podemos ver.Datos de celda en el sombreador
Muchos juegos de estrategia utilizan el concepto de niebla de guerra. Esto significa que la visión del jugador es limitada. Solo puede ver lo que está cerca de sus unidades o área controlada. Aunque podemos ver el alivio, no sabemos qué está pasando allí. Por lo general, el terreno invisible se vuelve más oscuro. Para darnos cuenta de esto, necesitamos rastrear la visibilidad de la celda y representarla en consecuencia.
La forma más sencilla de cambiar la apariencia de las celdas ocultas es agregar una métrica de visibilidad a los datos de malla. Sin embargo, al mismo tiempo, tendremos que lanzar una nueva triangulación en relieve cuando cambie la visibilidad. Esta es una mala decisión porque la visibilidad cambia constantemente durante el juego.
A menudo se usa la técnica de renderizado sobre la topografía de una superficie translúcida, que oculta parcialmente las celdas invisibles para el jugador. Este método es adecuado para terrenos relativamente planos en combinación con un ángulo de visión limitado. Pero dado que nuestro terreno puede contener alturas y objetos muy diferentes que se pueden ver desde diferentes ángulos, para esto necesitamos una malla altamente detallada que coincida con la forma del terreno. Este método será más costoso que el enfoque más simple mencionado anteriormente.
Otro enfoque es transferir los datos de las celdas al sombreador cuando se procesa por separado de la malla de relieve. Esto nos permitirá realizar la triangulación solo una vez. Los datos de la celda se pueden transferir usando textura. Cambiar la textura es un proceso mucho más simple que triangular el terreno. Además, ejecutar varias muestras de textura adicionales es más rápido que renderizar una sola capa translúcida.
¿Qué pasa con el uso de matrices de sombreadores?También puede transferir datos de celdas al sombreador utilizando una matriz de vectores. Sin embargo, las matrices de sombreadores tienen un límite de tamaño, medido en miles de bytes, y las texturas pueden contener millones de píxeles. Para admitir mapas grandes, utilizaremos texturas.
Gestión de datos de celda
Necesitamos una forma de controlar la textura que contiene los datos de la celda.
HexCellShaderData
un nuevo componente
HexCellShaderData
que hará esto.
using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; }
Al crear o cargar un nuevo mapa, necesitamos crear una nueva textura con el tamaño correcto. Por lo tanto, agregamos un método de inicialización que crea una textura. Utilizamos una textura RGBA sin texturas mip y espacio de color lineal. No necesitamos mezclar datos de celdas, por lo que utilizamos el filtrado de puntos. Además, los datos no deben colapsarse. Cada píxel en la textura contendrá datos de una celda.
public void Initialize (int x, int z) { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; }
¿El tamaño de la textura debe coincidir con el tamaño del mapa?No, solo necesita tener suficientes píxeles para contener todas las celdas. Con una coincidencia exacta con el tamaño del mapa, lo más probable es que se cree una textura con tamaños que no sean potencias de dos (sin potencia de dos, NPOT), y este formato de textura no es el más efectivo. Aunque podemos configurar el código para que funcione con texturas del tamaño de una potencia de dos, esta es una optimización menor, lo que complica el acceso a los datos de la celda.
De hecho, no tenemos que crear una nueva textura cada vez que creamos un nuevo mapa. Es suficiente cambiar el tamaño de la textura si ya existe. Ni siquiera necesitamos verificar si ya tenemos el tamaño correcto, porque
Texture2D.Resize
es lo suficientemente inteligente como para hacer esto por nosotros.
public void Initialize (int x, int z) { if (cellTexture) { cellTexture.Resize(x, z); } else { cellTexture = new Texture2D( cellCountX, cellCountZ, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; } }
En lugar de aplicar los datos de la celda un píxel a la vez, usamos un búfer de color y aplicamos los datos de todas las celdas a la vez. Para hacer esto, usaremos la matriz
Color32
. Si es necesario, crearemos una nueva instancia de matriz al final de
Initialize
. Si ya tenemos una matriz del tamaño correcto. entonces limpiamos su contenido.
Texture2D cellTexture; Color32[] cellTextureData; public void Initialize () { … if (cellTextureData == null || cellTextureData.Length != x * z) { cellTextureData = new Color32[x * z]; } else { for (int i = 0; i < cellTextureData.Length; i++) { cellTextureData[i] = new Color32(0, 0, 0, 0); } } }
¿Qué es color32?Las texturas RGBA estándar sin comprimir contienen píxeles de cuatro bytes. Cada uno de los cuatro canales de color recibe un byte, es decir, tienen 256 valores posibles. Cuando se usa la estructura Unity Color
, sus componentes de punto flotante en el intervalo 0-1 se convierten en bytes en el intervalo 0-255. Al muestrear, la GPU realiza la transformación inversa.
La estructura Color32
funciona directamente con bytes, por lo que ocupan menos espacio y no requieren conversión, lo que aumenta la eficiencia de su uso. Como almacenamos datos de celdas en lugar de colores, será más lógico trabajar directamente con datos de textura sin procesar, y no con Color
.
HexGrid
debería ocuparse de la creación e inicialización de estas celdas en el sombreador. Por lo tanto, agregaremos un campo
cellShaderData
y crearemos un componente dentro de
Awake
.
HexCellShaderData cellShaderData; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); CreateMap(cellCountX, cellCountZ); }
Al crear un nuevo mapa,
cellShaderData
también debe
cellShaderData
.
public bool CreateMap (int x, int z) { … cellCountX = x; cellCountZ = z; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; cellShaderData.Initialize(cellCountX, cellCountZ); CreateChunks(); CreateCells(); return true; }
Edición de datos de celda
Hasta ahora, al cambiar las propiedades de una celda, era necesario actualizar uno o varios fragmentos, pero ahora puede ser necesario actualizar los datos de las celdas. Esto significa que las celdas deben tener un enlace a los datos de la celda en el sombreador. Para hacer esto, agregue una propiedad a
HexCell
.
public HexCellShaderData ShaderData { get; set; }
En
HexGrid.CreateCell
asignaremos un componente de datos de sombreador a esta propiedad.
void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.ShaderData = cellShaderData; … }
Ahora podemos hacer que las celdas actualicen sus datos de sombreador. Si bien no estamos rastreando la visibilidad, podemos usar datos de sombreador para otra cosa. El tipo de relieve de la celda determina la textura utilizada para representarlo. No afecta la geometría de la celda, por lo que podemos almacenar el índice de tipo de elevación en los datos de la celda y no en los datos de la malla. Esto nos permitirá eliminar la necesidad de triangulación al cambiar el tipo de topografía celular.
Agregue un método de
RefreshTerrain
a
RefreshTerrain
para simplificar esta tarea para una celda específica. Dejemos este método vacío por ahora.
public void RefreshTerrain (HexCell cell) { }
Cambie
HexCell.TerrainTypeIndex
para que
HexCell.TerrainTypeIndex
este método y no ordene actualizar los fragmentos.
public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value;
También lo llamaremos en
HexCell.Load
después de recibir el tipo de topografía celular.
public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); RefreshPosition(); … }
Índice de la celda
Para cambiar estas celdas, necesitamos conocer el índice de la celda. La forma más fácil de hacerlo es agregando la propiedad
Index
a
HexCell
. Indicará el índice de la celda en la lista de celdas en el mapa, que corresponde a su índice en las celdas dadas en el sombreador.
public int Index { get; set; }
Este índice ya está en
HexGrid.CreateCell
, así que simplemente
HexGrid.CreateCell
a la celda creada.
void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; … }
Ahora
HexCellShaderData.RefreshTerrain
puede usar este índice para especificar datos de celda. Guardemos el índice de tipo de elevación en el componente alfa de su píxel simplemente convirtiendo el tipo en byte. Esto admitirá hasta 256 tipos de terreno, lo que será suficiente para nosotros.
public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; }
Para aplicar datos a una textura y pasarla a la GPU, debemos llamar a
Texture2D.SetPixels32
y luego a
Texture2D.Apply
. Como en el caso de los fragmentos, pospondremos estas operaciones en
LateUpdate
para que no se puedan realizar más de una vez por cuadro, independientemente del número de celdas cambiadas.
public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; enabled = true; } void LateUpdate () { cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = false; }
Para asegurarse de que los datos se actualizarán después de crear un nuevo mapa, habilite el componente después de la inicialización.
public void Initialize (int x, int z) { … enabled = true; }
Triangulación de índices celulares.
Como ahora almacenamos el índice de tipo de elevación en estas celdas, ya no necesitamos incluirlos en el proceso de triangulación. Pero para usar datos de celda, el sombreador debe saber qué índices usar. Por lo tanto, debe almacenar índices de celda en los datos de malla, reemplazando los índices de tipo de elevación. Además, todavía necesitamos el canal de color de la malla para mezclar celdas cuando se usan estas celdas.
useColors
campos comunes obsoletos
useColors
y
useTerrainTypes
. Reemplácelos con un campo
useCellData
.
Refactorizamos el cambio de nombre de la lista de
cellIndices
de
cellIndices
a
cellIndices
. También
cellWeights
nombre de los
colors
a
cellWeights
: este nombre funcionará mejor.
Cambie
Clear
para que cuando use estas celdas, obtenga dos listas juntas, y no por separado.
public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useCellData) { cellWeights = ListPool<Color>.Get(); cellIndices = ListPool<Vector3>.Get(); }
Realice la misma agrupación en
Apply
.
public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useCellData) { hexMesh.SetColors(cellWeights); ListPool<Color>.Add(cellWeights); hexMesh.SetUVs(2, cellIndices); ListPool<Vector3>.Add(cellIndices); }
AddTriangleColor
todos los
AddTriangleTerrainTypes
AddTriangleColor
y
AddTriangleTerrainTypes
. Reemplácelos con los métodos
AddTriangleCellData
apropiados, que agregan índices y pesos a la vez.
public void AddTriangleCellData ( Vector3 indices, Color weights1, Color weights2, Color weights3 ) { cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellWeights.Add(weights1); cellWeights.Add(weights2); cellWeights.Add(weights3); } public void AddTriangleCellData (Vector3 indices, Color weights) { AddTriangleCellData(indices, weights, weights, weights); }
Haga lo mismo en el método
AddQuad
apropiado.
public void AddQuadCellData ( Vector3 indices, Color weights1, Color weights2, Color weights3, Color weights4 ) { cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellIndices.Add(indices); cellWeights.Add(weights1); cellWeights.Add(weights2); cellWeights.Add(weights3); cellWeights.Add(weights4); } public void AddQuadCellData ( Vector3 indices, Color weights1, Color weights2 ) { AddQuadCellData(indices, weights1, weights1, weights2, weights2); } public void AddQuadCellData (Vector3 indices, Color weights) { AddQuadCellData(indices, weights, weights, weights, weights); }
HexGridChunk Refactoring
En esta etapa, obtenemos muchos errores de compilación en
HexGridChunk
que deben
HexGridChunk
. Pero primero, en aras de la coherencia, cambiamos el nombre de los colores estáticos a pesos.
static Color weights1 = new Color(1f, 0f, 0f); static Color weights2 = new Color(0f, 1f, 0f); static Color weights3 = new Color(0f, 0f, 1f);
Comencemos arreglando
TriangulateEdgeFan
. Solía necesitar un tipo, pero ahora necesita un índice de celda.
AddTriangleColor
código
AddTriangleColor
y
AddTriangleTerrainTypes
con el código
AddTriangleCellData
correspondiente.
void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float index) { terrain.AddTriangle(center, edge.v1, edge.v2); terrain.AddTriangle(center, edge.v2, edge.v3); terrain.AddTriangle(center, edge.v3, edge.v4); terrain.AddTriangle(center, edge.v4, edge.v5); Vector3 indices; indices.x = indices.y = indices.z = index; terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1);
Este método se llama en varios lugares. Vamos a repasarlos y asegurarnos de que el índice de la celda se transfiera allí, y no el tipo de terreno.
TriangulateEdgeFan(center, e, cell.Index);
El siguiente es
TriangulateEdgeStrip
. Aquí todo es un poco más complicado, pero usamos el mismo enfoque. También cambie el nombre de los nombres de los parámetros
c1
y
c2
a
w1
y
w2
.
void TriangulateEdgeStrip ( EdgeVertices e1, Color w1, float index1, EdgeVertices e2, Color w2, float index2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); Vector3 indices; indices.x = indices.z = index1; indices.y = index2; terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2); terrain.AddQuadCellData(indices, w1, w2);
Cambie las llamadas a este método para que se les pase el índice de celda. También mantenemos los nombres de las variables consistentes.
TriangulateEdgeStrip( m, weights1, cell.Index, e, weights1, cell.Index ); … TriangulateEdgeStrip( e1, weights1, cell.Index, e2, weights2, neighbor.Index, hasRoad ); … void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color w2 = HexMetrics.TerraceLerp(weights1, weights2, 1); float i1 = beginCell.Index; float i2 = endCell.Index; TriangulateEdgeStrip(begin, weights1, i1, e2, w2, i2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color w1 = w2; e2 = EdgeVertices.TerraceLerp(begin, end, i); w2 = HexMetrics.TerraceLerp(weights1, weights2, i); TriangulateEdgeStrip(e1, w1, i1, e2, w2, i2, hasRoad); } TriangulateEdgeStrip(e2, w2, i1, end, weights2, i2, hasRoad); }
Ahora pasamos a los métodos de ángulo. Estos cambios son simples, pero deben realizarse en una gran cantidad de código. Primero en
TriangulateCorner
.
void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); Vector3 indices; indices.x = bottomCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; terrain.AddTriangleCellData(indices, weights1, weights2, weights3);
Próximamente en
TriangulateCornerTerraces
.
void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color w3 = HexMetrics.TerraceLerp(weights1, weights2, 1); Color w4 = HexMetrics.TerraceLerp(weights1, weights3, 1); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleCellData(indices, weights1, w3, w4);
Luego en
TriangulateCornerTerracesCliff
.
void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryWeights = Color.Lerp(weights1, weights3, b); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; TriangulateBoundaryTriangle( begin, weights1, left, weights2, boundary, boundaryWeights, indices ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, weights2, right, weights3, boundary, boundaryWeights, indices ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleCellData( indices, weights2, weights3, boundaryWeights );
Y un poco diferente en
TriangulateCornerCliffTerraces
.
void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryWeights = Color.Lerp(weights1, weights2, b); Vector3 indices; indices.x = beginCell.Index; indices.y = leftCell.Index; indices.z = rightCell.Index; TriangulateBoundaryTriangle( right, weights3, begin, weights1, boundary, boundaryWeights, indices ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, weights2, right, weights3, boundary, boundaryWeights, indices ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleCellData( indices, weights2, weights3, boundaryWeights );
Los dos métodos anteriores usan
TriangulateBoundaryTriangle
, que también requiere actualización.
void TriangulateBoundaryTriangle ( Vector3 begin, Color beginWeights, Vector3 left, Color leftWeights, Vector3 boundary, Color boundaryWeights, Vector3 indices ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color w2 = HexMetrics.TerraceLerp(beginWeights, leftWeights, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleCellData(indices, beginWeights, w2, boundaryWeights);
El último método que debe cambiarse es
TriangulateWithRiver
.
void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangle(centerL, m.v1, m.v2); terrain.AddQuad(centerL, center, m.v2, m.v3); terrain.AddQuad(center, centerR, m.v3, m.v4); terrain.AddTriangle(centerR, m.v4, m.v5); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; terrain.AddTriangleCellData(indices, weights1); terrain.AddQuadCellData(indices, weights1); terrain.AddQuadCellData(indices, weights1); terrain.AddTriangleCellData(indices, weights1);
Para que todo funcione, debemos indicar que usaremos los datos de la celda para el elemento secundario del relieve del fragmento prefabricado.
El relieve utiliza datos de celda.En esta etapa, la malla contiene índices de celda en lugar de índices de tipo de elevación. Dado que el sombreador de elevación aún los interpreta como índices de elevación, veremos que la primera celda se representa con la primera textura y así sucesivamente hasta que se alcance la última textura de relieve.
Uso de índices de celda como índices de textura de elevación.No puedo hacer que el código refactorizado funcione. ¿Qué estoy haciendo mal?Hubo un tiempo en que cambiamos una gran cantidad de código de triangulación, por lo que hay una alta probabilidad de errores u omisiones. Si no puede encontrar el error, intente descargar el paquete de esta sección y extraiga los archivos apropiados. Puede importarlos en un proyecto separado y compararlos con su propio código.
Transferir datos de celda a un sombreador
Para usar estas celdas, el sombreador del terreno debe tener acceso a ellas. Esto se puede implementar a través de la propiedad del sombreador. Esto requerirá
HexCellShaderData
establecer la propiedad material del alivio. O podemos hacer que la textura de estas celdas sea visible globalmente para todos los sombreadores. Esto es conveniente porque lo necesitamos en varios sombreadores, por lo que utilizaremos este enfoque.
Después de crear la textura de la celda, llame al método estático
Shader.SetGlobalTexture
para que sea visible globalmente como
_HexCellData .
public void Initialize (int x, int z) { … else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } … }
Al usar la propiedad de sombreador, Unity hace que el tamaño de la textura esté disponible para el sombreador a través de la variable
textureName_TexelSize . Este es un vectorizador de cuatro componentes que contiene valores que son inversos al ancho y alto, así como al ancho y alto en sí. Pero al configurar la textura global, esto no se realiza. Por lo tanto, lo haremos nosotros mismos usando
Shader.SetGlobalVector
después de crear o cambiar el tamaño de la textura.
else { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; Shader.SetGlobalTexture("_HexCellData", cellTexture); } Shader.SetGlobalVector( "_HexCellData_TexelSize", new Vector4(1f / x, 1f / z, x, z) );
Shader Data Access
Cree un nuevo archivo de inclusión de sombreador en la carpeta de materiales llamada
HexCellData . En su interior, definimos variables para obtener información sobre la textura y el tamaño de estas celdas. También creamos una función para obtener los datos de celda para los datos de malla de vértice dados.
sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 GetCellData (appdata_full v) { }
Nuevo archivo de inclusión.Los índices de celda se almacenan en
v.texcoord2
, como fue el caso con los tipos de terreno. Comencemos con el primer índice:
v.texcoord2.x
. Desafortunadamente, no podemos usar directamente el índice para muestrear la textura de estas celdas. Tendremos que convertirlo a coordenadas UV.
El primer paso para crear la coordenada U es dividir el índice de la celda por el ancho de la textura. Podemos hacer esto multiplicándolo por
_HexCellData_TexelSize.x
.
float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; }
El resultado será un número en la forma ZU, donde Z es el índice de la fila y U es la coordenada de la celda U. Podemos extraer la cadena al redondear el número hacia abajo y luego restarlo del número para obtener la coordenada U. float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; }
La coordenada V está dividiendo la línea por la altura de la textura. float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = row * _HexCellData_TexelSize.y; }
Como estamos muestreando la textura, necesitamos usar las coordenadas en los centros de los píxeles, no en sus bordes. De esta forma, garantizamos que se muestrean los píxeles correctos. Por lo tanto, después de dividir por el tamaño de la textura, agregue ½. float4 GetCellData (appdata_full v) { float2 uv; uv.x = (v.texcoord2.x + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; }
Esto nos da las coordenadas UV correctas para el índice de la primera celda almacenada en los datos del vértice. Pero además podemos tener hasta tres índices diferentes. Por lo tanto, haremos que GetCellData
funcione para cualquier índice. Agregue un parámetro entero al mismo index
, que usaremos para acceder al componente del vector con el índice de la celda. float4 GetCellData (appdata_full v, int index) { float2 uv; uv.x = (v.texcoord2[index] + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; }
Ahora que tenemos todas las coordenadas necesarias para estas celdas, podemos tomar muestras _HexCellData
. Como estamos muestreando la textura en el programa de vértices, necesitamos decirle explícitamente al sombreador qué textura de mip debe usar. Esto se puede hacer usando una función tex2Dlod
que requiere las coordenadas de cuatro texturas. Dado que estas celdas no tienen texturas mip, asignamos valores cero a las coordenadas adicionales. float4 GetCellData (appdata_full v, int index) { float2 uv; uv.x = (v.texcoord2[index] + 0.5) * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; uv.y = (row + 0.5) * _HexCellData_TexelSize.y; float4 data = tex2Dlod(_HexCellData, float4(uv, 0, 0)); }
El cuarto componente de datos contiene un índice de tipo de elevación, que almacenamos directamente como bytes. Sin embargo, la GPU lo convirtió automáticamente a un valor de coma flotante en el rango 0-1. Para volver a convertirlo al valor correcto, multiplíquelo por 255. Después de eso, puede devolver los datos. float4 data = tex2Dlod(_HexCellData, float4(uv, 0, 0)); data.w *= 255; return data;
Para usar esta funcionalidad, habilite HexCellData en el sombreador de terreno . Como coloqué este sombreador en Materiales / Terreno , necesito usar la ruta relativa ../HexCellData.cginc . #include "../HexCellData.cginc" UNITY_DECLARE_TEX2DARRAY(_MainTex)
En el programa de vértices, obtenemos datos de celda para los tres índices de celda almacenados en los datos de vértice. Luego asigne data.terrain
sus índices de elevación. void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); // data.terrain = v.texcoord2.xyz; float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.terrain.x = cell0.w; data.terrain.y = cell1.w; data.terrain.z = cell2.w; }
En este punto, el mapa nuevamente comenzó a mostrar el terreno correcto. La gran diferencia es que editar solo tipos de terreno ya no conduce a nuevas triangulaciones. Si durante la edición se cambia cualquier otro dato de celda, la triangulación se realizará como de costumbre.paquete de la unidadVisibilidad
Una vez creada la base de estas celdas, podemos avanzar para admitir la visibilidad. Para hacer esto, usamos el sombreador, las celdas y los objetos que determinan la visibilidad. Tenga en cuenta que el proceso de triangulación no sabe absolutamente nada sobre esto.Shader
Comencemos diciéndole al sombreador Terrain sobre la visibilidad. Recibirá datos de visibilidad del programa de vértices y los pasará al programa de fragmentos utilizando la estructura Input
. Como pasamos tres índices de elevación separados, también pasaremos tres valores de visibilidad. struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float3 visibility; };
Para almacenar la visibilidad, utilizamos el primer componente de estas celdas. void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.terrain.x = cell0.w; data.terrain.y = cell1.w; data.terrain.z = cell2.w; data.visibility.x = cell0.x; data.visibility.y = cell1.x; data.visibility.z = cell2.x; }
Una visibilidad de 0 significa que la celda es actualmente invisible. Si fuera visible, tendría el valor de visibilidad 1. Por lo tanto, podemos oscurecer el terreno multiplicando el resultado GetTerrainColor
por el vector de visibilidad correspondiente. Por lo tanto, modulamos individualmente el color de relieve de cada celda mixta. float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3(IN.worldPos.xz * 0.02, IN.terrain[index]); float4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, uvw); return c * (IN.color[index] * IN.visibility[index]); }
Las células se volvieron negras.¿No podemos combinar la visibilidad en un programa de vértices?, . . . , . , .
La oscuridad completa es un busto para células temporalmente invisibles. Para que podamos ver el alivio, necesitamos aumentar el indicador utilizado para las celdas ocultas. Pasemos de 0–1 a ¼ - 1, lo que se puede hacer usando la función lerp
al final del programa de vértice. void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x; data.visibility.y = cell1.x; data.visibility.z = cell2.x; data.visibility = lerp(0.25, 1, data.visibility); }
Celdas sombreadas.Seguimiento de visibilidad celular
Para que la visibilidad funcione, las celdas deben seguir su visibilidad. Pero, ¿cómo determina una célula si es visible? Podemos hacer esto rastreando el número de entidades que lo ven. Cuando alguien comienza a ver una celda, debe informar esta celda. Y cuando alguien deja de ver la celda, también debe notificárselo. La celda simplemente realiza un seguimiento de la cantidad de observadores, sean cuales sean esas entidades. Si una celda tiene un valor de visibilidad de al menos 1, entonces es visible, de lo contrario es invisible. Para implementar este comportamiento, agregamos HexCell
dos métodos y una propiedad a la variable. public bool IsVisible { get { return visibility > 0; } } … int visibility; … public void IncreaseVisibility () { visibility += 1; } public void DecreaseVisibility () { visibility -= 1; }
A continuación, agregue al HexCellShaderData
método RefreshVisibility
, que hace lo mismo que RefreshTerrain
, solo por el bien de la visibilidad. Guarde los datos en el componente R de las celdas de datos. Como trabajamos con bytes que se convierten en valores 0–1, usamos para indicar visibilidad (byte)255
. public void RefreshVisibility (HexCell cell) { cellTextureData[cell.Index].r = cell.IsVisible ? (byte)255 : (byte)0; enabled = true; }
Llamaremos a este método con visibilidad creciente y decreciente, cambiando el valor entre 0 y 1. public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { ShaderData.RefreshVisibility(this); } } public void DecreaseVisibility () { visibility -= 1; if (visibility == 0) { ShaderData.RefreshVisibility(this); } }
Crear visibilidad de escuadrón
Hagamos que las unidades puedan ver la celda que ocupan. Esto se logra mediante una llamada IncreaseVisibility
a la nueva ubicación de la unidad durante la tarea HexUnit.Location
. También llamamos a la ubicación anterior (si existe) DecreaseVisibility
. public HexCell Location { get { return location; } set { if (location) { location.DecreaseVisibility(); location.Unit = null; } location = value; value.Unit = this; value.IncreaseVisibility(); transform.localPosition = value.Position; } }
Las unidades pueden ver dónde están.¡Finalmente usamos visibilidad! Cuando se agregan a un mapa, las unidades hacen visible su celda. Además, su alcance se teletransporta cuando se traslada a su nueva ubicación. Pero su alcance permanece activo al eliminar unidades del mapa. Para solucionar esto, reduciremos la visibilidad de su ubicación al destruir unidades. public void Die () { if (location) { location.DecreaseVisibility(); } location.Unit = null; Destroy(gameObject); }
Rango de visibilidad
Hasta ahora, solo vemos la celda en la que se encuentra el desprendimiento, y esto limita las posibilidades. Al menos necesitamos ver las células vecinas. En el caso general, las unidades pueden ver todas las celdas dentro de una cierta distancia, que depende de la unidad.Agreguemos al HexGrid
método para encontrar todas las celdas visibles desde una celda teniendo en cuenta el rango. Podemos crear este método duplicando y cambiando Search
. Cambie sus parámetros y haga que devuelva una lista de celdas para las que puede usar el grupo de listas.En cada iteración, la celda actual se agrega a la lista. Ya no hay ninguna celda final, por lo que la búsqueda nunca terminará cuando llegue a este punto. También nos deshacemos de la lógica de los movimientos y el costo de los movimientos. Hacer las propiedadesPathFrom
ya no se les preguntó porque no los necesitamos, y no queremos interferir con el camino a lo largo de la cuadrícula.En cada paso, la distancia simplemente aumenta en 1. Si excede el rango, entonces esta celda se omite. Y no necesitamos una búsqueda heurística, por lo que la inicializamos con un valor de 0. Es decir, en esencia, volvimos al algoritmo de Dijkstra. List<HexCell> GetVisibleCells (HexCell fromCell, int range) { List<HexCell> visibleCells = ListPool<HexCell>.Get(); searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; visibleCells.Add(current);
¿No podemos usar un algoritmo más simple para encontrar todas las celdas dentro del rango?, , .
También agregue HexGrid
métodos IncreaseVisibility
y DecreaseVisibility
. Obtienen la celda y el rango, toman una lista de las celdas correspondientes y aumentan / disminuyen su visibilidad. Cuando terminen, deberían devolver la lista a su grupo. public void IncreaseVisibility (HexCell fromCell, int range) { List<HexCell> cells = GetVisibleCells(fromCell, range); for (int i = 0; i < cells.Count; i++) { cells[i].IncreaseVisibility(); } ListPool<HexCell>.Add(cells); } public void DecreaseVisibility (HexCell fromCell, int range) { List<HexCell> cells = GetVisibleCells(fromCell, range); for (int i = 0; i < cells.Count; i++) { cells[i].DecreaseVisibility(); } ListPool<HexCell>.Add(cells); }
Para utilizar estos métodos se HexUnit
requiere acceso a la cuadrícula, por lo que debe agregarle una propiedad Grid
. public HexGrid Grid { get; set; }
Cuando agrega un escuadrón a una cuadrícula, asignará una cuadrícula a esta propiedad HexGrid.AddUnit
. public void AddUnit (HexUnit unit, HexCell location, float orientation) { units.Add(unit); unit.Grid = this; unit.transform.SetParent(transform, false); unit.Location = location; unit.Orientation = orientation; }
Para empezar, un rango de visibilidad de tres celdas será suficiente. Para hacer esto, agregamos a la HexUnit
constante, que en el futuro siempre puede convertirse en una variable. Luego haremos que el escuadrón invoque métodos para la cuadrícula IncreaseVisibility
y DecreaseVisibility
, transmitiendo también su rango de visibilidad, y no solo vayamos a este lugar. const int visionRange = 3; … public HexCell Location { get { return location; } set { if (location) {
Unidades con rango de visibilidad que pueden superponerse.Visibilidad al mover
Por el momento, el área de visibilidad del escuadrón después del comando de movimiento se teletransporta inmediatamente al punto final. Se habría visto mejor si la unidad y su campo de visibilidad se movieran juntos. El primer paso para esto es que ya no estableceremos la propiedad Location
c HexUnit.Travel
. En cambio, cambiaremos directamente el campo location
, evitando el código de propiedad. Por lo tanto, borraremos manualmente la ubicación anterior y configuraremos una nueva ubicación. La visibilidad permanecerá sin cambios. public void Travel (List<HexCell> path) {
Dentro de las rutinas, TravelPath
reduciremos la visibilidad de la primera celda solo después de la finalización LookAt
. Después de eso, antes de pasar a una nueva celda, aumentaremos la visibilidad desde esta celda. Habiendo terminado con esto, nuevamente reducimos la visibilidad. Finalmente, aumente la visibilidad desde la última celda. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position;
Visibilidad en movimiento.Todo esto funciona, excepto cuando se emite una nueva orden en el momento en que se mueve el destacamento. Esto lleva a la teletransportación, que también debería aplicarse a la visibilidad. Para darnos cuenta de esto, necesitamos rastrear la ubicación actual del escuadrón mientras nos movemos. HexCell location, currentTravelLocation;
Actualizaremos esta ubicación cada vez que toquemos una nueva celda mientras nos movemos, hasta que el escuadrón llegue a la celda final. Entonces debe reiniciarse. IEnumerator TravelPath () { … for (int i = 1; i < pathToTravel.Count; i++) { currentTravelLocation = pathToTravel[i]; a = c; b = pathToTravel[i - 1].Position; c = (b + currentTravelLocation.Position) * 0.5f; Grid.IncreaseVisibility(pathToTravel[i], visionRange); for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); yield return null; } Grid.DecreaseVisibility(pathToTravel[i], visionRange); t -= 1f; } currentTravelLocation = null; … }
Ahora, después de completar el giro, TravelPath
podemos verificar si se conoce la antigua ubicación intermedia de la ruta. En caso afirmativo, debe reducir la visibilidad en esta celda, y no al comienzo del camino. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); Grid.DecreaseVisibility( currentTravelLocation ? currentTravelLocation : pathToTravel[0], visionRange ); … }
También necesitamos corregir la visibilidad después de la compilación que ocurrió durante el movimiento del escuadrón. Si aún se conoce la ubicación intermedia, reduzca la visibilidad en ella y aumente la visibilidad en el punto final, y luego restablezca la ubicación intermedia. void OnEnable () { if (location) { transform.localPosition = location.Position; if (currentTravelLocation) { Grid.IncreaseVisibility(location, visionRange); Grid.DecreaseVisibility(currentTravelLocation, visionRange); currentTravelLocation = null; } } }
paquete de la unidadVisibilidad de carreteras y agua.
Aunque los cambios de color en relieve se basan en la visibilidad, esto no afecta las carreteras y el agua. Se ven demasiado brillantes para las células invisibles. Para aplicar la visibilidad a las carreteras y al agua, necesitamos agregar índices de celda y combinar pesos a sus datos de malla. Por lo tanto, verificaremos los elementos secundarios de Usar datos de celda para ríos , caminos , agua , ribera y estuarios del fragmento prefabricado.Caminos
Comenzaremos desde las carreteras. El método se HexGridChunk.TriangulateRoadEdge
utiliza para crear una pequeña parte de la carretera en el centro de la celda, por lo que necesita un índice de celda. Agregue un parámetro y genere datos de celda para el triángulo. void TriangulateRoadEdge ( Vector3 center, Vector3 mL, Vector3 mR, float index ) { roads.AddTriangle(center, mL, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); Vector3 indices; indices.x = indices.y = indices.z = index; roads.AddTriangleCellData(indices, weights1); }
Otra forma fácil de crear carreteras es TriangulateRoadSegment
. Se usa tanto dentro como entre celdas, por lo que debería funcionar con dos índices diferentes. Para esto, es conveniente utilizar el parámetro del vector índice. Dado que los segmentos de la carretera pueden ser partes de repisas, los pesos también deben pasarse a través de parámetros. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6, Color w1, Color w2, Vector3 indices ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); roads.AddQuadUV(0f, 1f, 0f, 0f); roads.AddQuadUV(1f, 0f, 0f, 0f); roads.AddQuadCellData(indices, w1, w2); roads.AddQuadCellData(indices, w1, w2); }
Ahora pasemos a TriangulateRoad
, que crea caminos dentro de las celdas. También necesita un parámetro de índice. Pasa estos datos a los métodos de carreteras que llama y los agrega a los triángulos que crea. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e, bool hasRoadThroughCellEdge, float index ) { if (hasRoadThroughCellEdge) { Vector3 indices; indices.x = indices.y = indices.z = index; Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment( mL, mC, mR, e.v2, e.v3, e.v4, weights1, weights1, indices ); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); roads.AddTriangleCellData(indices, weights1); roads.AddTriangleCellData(indices, weights1); } else { TriangulateRoadEdge(center, mL, mR, index); } }
Queda por añadir los argumentos de los métodos requeridos TriangulateRoad
, TriangulateRoadEdge
y TriangulateRoadSegment
para corregir todos los errores de compilación. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Index); if (cell.HasRoads) { Vector2 interpolators = GetRoadInterpolators(direction, cell); TriangulateRoad( center, Vector3.Lerp(center, e.v1, interpolators.x), Vector3.Lerp(center, e.v5, interpolators.y), e, cell.HasRoadThroughEdge(direction), cell.Index ); } } … void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge, cell.Index); if (previousHasRiver) { TriangulateRoadEdge(roadCenter, center, mL, cell.Index); } if (nextHasRiver) { TriangulateRoadEdge(roadCenter, mR, center, cell.Index); } } … void TriangulateEdgeStrip ( … ) { … if (hasRoad) { TriangulateRoadSegment( e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4, w1, w2, indices ); } }
Ahora los datos de la malla son correctos y pasaremos al sombreador de carreteras . Necesita un programa de vértices y debe contener HexCellData . #pragma surface surf Standard fullforwardshadows decal:blend vertex:vert #pragma target 3.0 #include "HexCellData.cginc"
Como no mezclamos varios materiales, será suficiente para nosotros pasar un indicador de visibilidad al programa de fragmentos. struct Input { float2 uv_MainTex; float3 worldPos; float visibility; };
Es suficiente que un nuevo programa de vértice reciba datos de dos celdas. Inmediatamente mezclamos su visibilidad, la ajustamos y agregamos a la salida. void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility = lerp(0.25, 1, data.visibility); }
En el programa de fragmentos, solo necesitamos agregar visibilidad al color. void surf (Input IN, inout SurfaceOutputStandard o) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color * ((noise.y * 0.75 + 0.25) * IN.visibility); … }
Caminos con visibilidad.Aguas abiertas
Puede parecer que la visibilidad ya ha afectado el agua, pero esta es solo la superficie de un terreno sumergido en el agua. Comencemos aplicando visibilidad a aguas abiertas. Para esto necesitamos cambiar HexGridChunk.TriangulateOpenWater
. void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … water.AddTriangle(center, c1, c2); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; water.AddTriangleCellData(indices, weights1); if (direction <= HexDirection.SE && neighbor != null) { … water.AddQuad(c1, c2, e1, e2); indices.y = neighbor.Index; water.AddQuadCellData(indices, weights1, weights2); if (direction <= HexDirection.E) { … water.AddTriangle( c2, e2, c2 + HexMetrics.GetWaterBridge(direction.Next()) ); indices.z = nextNeighbor.Index; water.AddTriangleCellData( indices, weights1, weights2, weights3 ); } } }
También necesitamos agregar datos de celdas a los fanáticos de los triángulos cerca de las costas. void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { … water.AddTriangle(center, e1.v1, e1.v2); water.AddTriangle(center, e1.v2, e1.v3); water.AddTriangle(center, e1.v3, e1.v4); water.AddTriangle(center, e1.v4, e1.v5); Vector3 indices; indices.x = indices.y = indices.z = cell.Index; water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); water.AddTriangleCellData(indices, weights1); … }
El sombreador de agua debe cambiarse de la misma manera que el sombreador de carreteras , pero necesita combinar la visibilidad de no dos, sino tres celdas. #pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float3 worldPos; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { float waves = Waves(IN.worldPos.xz, _MainTex); fixed4 c = saturate(_Color + waves); o.Albedo = c.rgb * IN.visibility; … }
Aguas abiertas con visibilidad.Costa y estuario
Para apoyar la costa, necesitamos cambiar nuevamente HexGridChunk.TriangulateWaterShore
. Ya creamos un vector índice, pero solo usamos un índice de celda para aguas abiertas. La costa también necesita un índice vecino, así que cambie el código. Vector3 indices;
Agregue los datos de la celda a los quads y al triángulo de la costa. También pasamos los índices en la llamada TriangulateEstuary
. if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary( e1, e2, cell.IncomingRiver == direction, indices ); } else { … waterShore.AddQuadUV(0f, 0f, 0f, 1f); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); waterShore.AddQuadCellData(indices, weights1, weights2); } HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) { … waterShore.AddTriangleUV( … ); indices.z = nextNeighbor.Index; waterShore.AddTriangleCellData( indices, weights1, weights2, weights3 ); }
Agregue el parámetro necesario TriangulateEstuary
y cuide estas células para la costa y la boca. No olvides que la boca está hecha de trapecio con dos triángulos de la costa a los lados. Nos aseguramos de que los pesos se transfieran en el orden correcto. void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver, Vector3 indices ) { waterShore.AddTriangle(e2.v1, e1.v2, e1.v1); waterShore.AddTriangle(e2.v5, e1.v5, e1.v4); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); waterShore.AddTriangleUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); waterShore.AddTriangleCellData(indices, weights2, weights1, weights1); waterShore.AddTriangleCellData(indices, weights2, weights1, weights1); estuaries.AddQuad(e2.v1, e1.v2, e2.v2, e1.v3); estuaries.AddTriangle(e1.v3, e2.v2, e2.v4); estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5); estuaries.AddQuadUV( new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0f, 0f) ); estuaries.AddTriangleUV( new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(1f, 1f) ); estuaries.AddQuadUV( new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(0f, 1f) ); estuaries.AddQuadCellData( indices, weights2, weights1, weights2, weights1 ); estuaries.AddTriangleCellData(indices, weights1, weights2, weights2); estuaries.AddQuadCellData(indices, weights1, weights2); … }
En el sombreador WaterShore , debe realizar los mismos cambios que en el sombreador Water , mezclando la visibilidad de las tres celdas. #pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float3 worldPos; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); float4 cell2 = GetCellData(v, 2); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { … fixed4 c = saturate(_Color + max(foam, waves)); o.Albedo = c.rgb * IN.visibility; … }
El sombreador de estuario combina la visibilidad de dos celdas, al igual que el sombreador de carretera . Él ya tiene un programa de vértices, porque lo necesitamos para transmitir las coordenadas UV de los ríos. #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float2 riverUV; float3 worldPos; float visibility; }; half _Glossiness; half _Metallic; fixed4 _Color; void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.riverUV = v.texcoord1.xy; float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); o.visibility = cell0.x * v.color.x + cell1.x * v.color.y; o.visibility = lerp(0.25, 1, o.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { … fixed4 c = saturate(_Color + water); o.Albedo = c.rgb * IN.visibility; … }
Costa y estuario con visibilidad.Ríos
Las últimas regiones de agua con las que trabajar son los ríos. Agregue un HexGridChunk.TriangulateRiverQuad
vector de índice al parámetro y agréguelo a la malla para que pueda mantener la visibilidad de dos celdas. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, float v, bool reversed, Vector3 indices ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, v, reversed, indices); } void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float v, bool reversed, Vector3 indices ) { … rivers.AddQuadCellData(indices, weights1, weights2); }
TriangulateWithRiverBeginOrEnd
crea puntos finales de río con un quad y un triángulo en el centro de la celda. Agregue los datos de celda necesarios para esto. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.HasIncomingRiver; Vector3 indices; indices.x = indices.y = indices.z = cell.Index; TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed, indices ); center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); … rivers.AddTriangleCellData(indices, weights1); } }
Ya tenemos estos índices celulares TriangulateWithRiver
, así que los pasamos en la llamada TriangulateRiverQuad
. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.IncomingRiver == direction; TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, 0.4f, reversed, indices ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed, indices ); } }
También agregamos soporte de índice a las cascadas que se vierten en aguas profundas. void TriangulateWaterfallInWater ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float waterY, Vector3 indices ) { … rivers.AddQuadCellData(indices, weights1, weights2); }
Y finalmente, cámbielo TriangulateConnection
para que pase los índices necesarios a los métodos de ríos y cascadas. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (hasRiver) { e2.v3.y = neighbor.StreamBedY; Vector3 indices; indices.x = indices.z = cell.Index; indices.y = neighbor.Index; if (!cell.IsUnderwater) { if (!neighbor.IsUnderwater) { TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction, indices ); } else if (cell.Elevation > neighbor.WaterLevel) { TriangulateWaterfallInWater( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, neighbor.WaterSurfaceY, indices ); } } else if ( !neighbor.IsUnderwater && neighbor.Elevation > cell.WaterLevel ) { TriangulateWaterfallInWater( e2.v4, e2.v2, e1.v4, e1.v2, neighbor.RiverSurfaceY, cell.RiverSurfaceY, cell.WaterSurfaceY, indices ); } } … }
El sombreador River necesita hacer los mismos cambios que el sombreador Road . #pragma surface surf Standard alpha vertex:vert #pragma target 3.0 #include "Water.cginc" #include "HexCellData.cginc" sampler2D _MainTex; struct Input { float2 uv_MainTex; float visibility; }; … void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float4 cell0 = GetCellData(v, 0); float4 cell1 = GetCellData(v, 1); data.visibility = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility = lerp(0.25, 1, data.visibility); } void surf (Input IN, inout SurfaceOutputStandard o) { float river = River(IN.uv_MainTex, _MainTex); fixed4 c = saturate(_Color + river); o.Albedo = c.rgb * IN.visibility; … }
Ríos con visibilidad.paquete de la unidadObjetos y Visibilidad
Ahora la visibilidad funciona para todo el terreno generado por el procedimiento, pero hasta ahora no afecta las características del terreno. Los edificios, granjas y árboles se crean a partir de prefabricados, y no de geometría de procedimiento, por lo que no podemos agregar índices de celda y mezclar pesos con sus vértices. Dado que cada uno de estos objetos pertenece a una sola celda, necesitamos determinar en qué celda están. Si podemos hacer esto, tendremos acceso a los datos de las celdas correspondientes y aplicaremos visibilidad.Ya podemos transformar las posiciones XZ del mundo en índices celulares. Esta transformación se utilizó para editar el terreno y gestionar escuadrones. Sin embargo, el código correspondiente no es trivial. Utiliza operaciones enteras y requiere lógica para trabajar con bordes. Esto no es práctico para un sombreador, por lo que podemos hornear la mayor parte de la lógica en una textura y usarla.Ya estamos usando una textura con un patrón hexagonal para proyectar la cuadrícula sobre la topografía. Esta textura define un área de celda de 2 × 2. Por lo tanto, podemos calcular fácilmente en qué área estamos. Después de eso, puede aplicar una textura que contenga desplazamientos X y Z para las celdas en esta área y usar estos datos para calcular la celda en la que estamos ubicados.Aquí hay una textura similar. El desplazamiento X se almacena en su canal rojo y el desplazamiento Z se almacena en el canal verde. Como cubre el área de celdas 2 × 2, necesitamos desplazamientos de 0 y 2. Tales datos no pueden almacenarse en el canal de color, por lo que los desplazamientos se reducen a la mitad. No necesitamos bordes claros de las celdas, por lo que una pequeña textura es suficiente.La textura de las coordenadas de la cuadrícula.Añade textura al proyecto. Establezca su Modo de ajuste para repetir , al igual que la otra textura de malla. No necesitamos ninguna mezcla, por lo que para el Modo de fusión elegiremos Punto . También apague la compresión para que los datos no se distorsionen. Desactive el modo sRGB para que cuando se procese en modo lineal, no se realicen conversiones de espacio de color. Y finalmente, no necesitamos texturas mip.Opciones de importación de texturas.Sombreador de objetos con visibilidad
Crear un nuevo shader de la función , para añadir soporte para la visibilidad del objeto. Este es un sombreador de superficie simple con un programa de vértice. Agregue HexCellData y pase el indicador de visibilidad al programa de fragmentos y, como de costumbre, considérelo en color. La diferencia aquí es que no podemos usarlo GetCellData
porque los datos de malla requeridos no existen. En cambio, tenemos una posición en el mundo. Pero por ahora, deje la visibilidad igual a 1. Shader "Custom/Feature" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 [NoTilingOffset] _GridCoordinates ("Grid Coordinates", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.0 #include "../HexCellData.cginc" sampler2D _MainTex, _GridCoordinates; half _Glossiness; half _Metallic; fixed4 _Color; struct Input { float2 uv_MainTex; float visibility; }; void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float3 pos = mul(unity_ObjectToWorld, v.vertex); data.visibility = 1; } void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb * IN.visibility; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
Cambie todos los materiales de los objetos para que utilicen el nuevo sombreador y asígneles la textura de las coordenadas de la cuadrícula.Urbano con textura de malla.Acceder a los datos de la celda
Para muestrear la textura de las coordenadas de la cuadrícula en el programa de vértices, nuevamente necesitamos tex2Dlod
un vector de coordenadas de textura de cuatro componentes. Las dos primeras coordenadas son la posición del mundo XZ. Los otros dos son iguales a cero como antes. void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); float3 pos = mul(unity_ObjectToWorld, v.vertex); float4 gridUV = float4(pos.xz, 0, 0); data.visibility = 1; }
Al igual que en el sombreador Terrain , estiramos las coordenadas UV para que la textura tenga la relación de aspecto correcta correspondiente a la cuadrícula de hexágonos. float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0);
Podemos averiguar en qué parte de las celdas 2 × 2 estamos tomando el valor de las coordenadas UV redondeadas hacia abajo. Esto forma la base de las coordenadas de las celdas. float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); float2 cellDataCoordinates = floor(gridUV.xy);
Para encontrar las coordenadas de la celda en la que estamos, agregamos los desplazamientos almacenados en la textura. float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg;
Como parte de la cuadrícula tiene un tamaño de 2 × 2 y los desplazamientos se reducen a la mitad, necesitamos duplicar el resultado para obtener las coordenadas finales. float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg; cellDataCoordinates *= 2;
Ahora tenemos las coordenadas XZ de la cuadrícula de celdas que necesitamos convertir a las coordenadas UV de estas celdas. Esto se puede hacer simplemente moviendo a los centros de los píxeles y luego dividiéndolos en tamaños de textura. Así que agreguemos una función para esto al archivo de inclusión HexCellData que también manejará el muestreo. float4 GetCellData (float2 cellDataCoordinates) { float2 uv = cellDataCoordinates + 0.5; uv.x *= _HexCellData_TexelSize.x; uv.y *= _HexCellData_TexelSize.y; return tex2Dlod(_HexCellData, float4(uv, 0, 0)); }
Ahora podemos utilizar esto en el programa de sombreado de vértices de la función . cellDataCoordinates *= 2; data.visibility = GetCellData(cellDataCoordinates).x; data.visibility = lerp(0.25, 1, data.visibility);
Objetos con visibilidad.Finalmente, la visibilidad afecta a todo el mapa, con la excepción de las unidades que siempre son visibles. Como determinamos la visibilidad de los objetos para cada vértice, para el objeto que cruza el límite de la celda, se mezclará la visibilidad de las celdas que cierra. Pero los objetos son tan pequeños que permanecen constantemente dentro de su celda, incluso teniendo en cuenta la distorsión de las posiciones. Sin embargo, algunos pueden ser parte de los vértices en otra celda. Por lo tanto, nuestro enfoque es barato, pero imperfecto. Esto es más notable en el caso de las paredes, cuya visibilidad varía entre las visibilidades de las celdas vecinas.Muros con visibilidad cambiante.Dado que los segmentos de muro se generan de manera procesal, podemos agregar datos de celda a su malla y usar el enfoque que usamos para el relieve. Desafortunadamente, las torres son prefabricadas, por lo que aún tendremos inconsistencias. En términos generales, el enfoque existente se ve lo suficientemente bueno para la geometría simple que usamos. En el futuro, consideraremos modelos y paredes más detallados, por lo tanto, mejoraremos el método de mezclar su visibilidad.paquete de la unidadParte 21: investigación cartográfica
- Mostramos todo durante la edición.
- Rastreamos las células investigadas.
- Escondemos lo que aún se desconoce.
- Forzamos a las unidades a evitar áreas inexploradas.
En la parte anterior, agregamos la niebla de guerra, que ahora refinaremos para implementar la investigación cartográfica.Estamos listos para explorar el mundo.Mostrar todo el mapa en modo edición
El significado del estudio es que hasta que no se vean las células se consideran desconocidas y, por lo tanto, invisibles. No deben oscurecerse, sino mostrarse en absoluto. Por lo tanto, antes de agregar soporte de investigación, habilitaremos la visibilidad en modo edición.Cambio de visibilidad
Podemos controlar si los sombreadores usan visibilidad usando la palabra clave, como se hizo con la superposición en la cuadrícula. Usemos la palabra clave HEX_MAP_EDIT_MODE para indicar el estado del modo de edición. Dado que varios sombreadores deben conocer esta palabra clave, la definiremos globalmente utilizando métodos estáticos Shader.EnableKeyWord
y Shader.DisableKeyword
. Llamaremos al método apropiado HexGameUI.SetEditMode
cuando cambiemos el modo de edición. public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); if (toggle) { Shader.EnableKeyword("HEX_MAP_EDIT_MODE"); } else { Shader.DisableKeyword("HEX_MAP_EDIT_MODE"); } }
Modo de edición de sombreadores
Cuando se define HEX_MAP_EDIT_MODE , los sombreadores ignorarán la visibilidad. Esto se reduce al hecho de que la visibilidad de la celda siempre se considerará igual a 1. Agreguemos una función para filtrar los datos de las celdas en función de la palabra clave al comienzo del archivo de inclusión HexCellData . sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.x = 1; #endif return data; }
Pasamos a través de esta función el resultado de ambas funciones GetCellData
antes de devolverla. float4 GetCellData (appdata_full v, int index) { … return FilterCellData(data); } float4 GetCellData (float2 cellDataCoordinates) { … return FilterCellData(tex2Dlod(_HexCellData, float4(uv, 0, 0))); }
Para que todo funcione, todos los sombreadores relevantes deben recibir la directiva multi_compilación para crear opciones en caso de que se defina la palabra clave HEX_MAP_EDIT_MODE . Agregue la línea apropiada a los sombreadores Estuario , Característica , Río , Carretera , Terreno , Agua y Orilla de agua , entre la directiva de destino y la primera directiva de inclusión. #pragma multi_compile _ HEX_MAP_EDIT_MODE
Ahora, al cambiar al modo de edición de mapas, la niebla de guerra desaparecerá.paquete de la unidadInvestigación celular
Por defecto, las celdas deben considerarse inexploradas. Se exploran cuando un escuadrón los ve. Después de eso, continúan siendo investigados si un destacamento puede verlos.Seguimiento del estado del estudio
Para agregar soporte para monitorear el estado de los estudios, agregamos a la HexCell
propiedad general IsExplored
. public bool IsExplored { get; set; }
El estado del estudio está determinado por la propia célula. Por lo tanto, esta propiedad solo debe establecerse HexCell
. Para agregar esta restricción, configuraremos el setter como privado. public bool IsExplored { get; private set; }
La primera vez que la visibilidad de la celda se vuelve mayor que cero, la celda comienza a considerarse investigada y, por lo tanto IsExplored
, se debe asignar un valor true
. De hecho, será suficiente para nosotros simplemente marcar la celda como examinada cuando la visibilidad aumenta a 1. Esto debe hacerse antes de la llamada RefreshVisibility
. public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { IsExplored = true; ShaderData.RefreshVisibility(this); } }
Transferencia de estado de investigación a sombreadores
Como en el caso de la visibilidad de las celdas, transferimos su estado de investigación a los sombreadores a través de los datos del sombreador. Al final, es solo otro tipo de visibilidad. HexCellShaderData.RefreshVisibility
almacena el estado de visibilidad en el canal de datos R. Mantengamos el estado del estudio en los datos del canal G. public void RefreshVisibility (HexCell cell) { int index = cell.Index; cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; enabled = true; }
Relieve negro inexplorado
Ahora podemos usar sombreadores para visualizar el estado de la investigación celular. Para asegurarnos de que todo funcione como debería, simplemente hacemos que el terreno inexplorado sea negro. Pero primero, para que el modo de edición funcione, cámbielo FilterCellData
para que filtre los datos de la investigación. float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.xy = 1; #endif return data; }
El sombreador de terreno pasa los datos de visibilidad de las tres celdas posibles al programa de fragmentos. En el caso del estado de investigación, los combinamos en el programa de vértices y transferimos el único valor al programa de fragmentos. Agregue el visibility
cuarto componente a la entrada para que tengamos un lugar para esto. struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float4 visibility; };
Ahora, en el programa de vértices, cuando cambiamos el índice de visibilidad, debemos acceder explícitamente data.visibility.xyz
. void vert (inout appdata_full v, out Input data) { … data.visibility.xyz = lerp(0.25, 1, data.visibility.xyz); }
Después de eso, combinamos los estados del estudio y escribimos el resultado en data.visibility.w
. Esto es similar a combinar la visibilidad en otros sombreadores, pero usando el componente Y de estas celdas. data.visibility.xyz = lerp(0.25, 1, data.visibility.xyz); data.visibility.w = cell0.y * v.color.x + cell1.y * v.color.y + cell2.y * v.color.z;
El estado de la investigación ahora está disponible en el programa de fragmentos a través de IN.visibility.w
. Considéralo en el cálculo del albedo. void surf (Input IN, inout SurfaceOutputStandard o) { … float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
La topografía inexplorada ahora es negra.El alivio de las células inexploradas ahora tiene un color negro. Pero esto aún no ha afectado a objetos, carreteras y agua. Sin embargo, esto es suficiente para asegurarse de que el estudio funcione.Guardar y cargar el estado de la investigación
Ahora que hemos agregado soporte de investigación, debemos asegurarnos de que se tenga en cuenta el estado de la investigación al guardar y cargar mapas. Por lo tanto, necesitamos aumentar la versión de los archivos de mapa a 3. Para hacer estos cambios más convenientes, agreguemos una SaveLoadMenu
constante para esto . const int mapFileVersion = 3;
Utilizaremos esta constante al escribir la versión del archivo Save
y al registrar el soporte de archivos Load
. void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(mapFileVersion); hexGrid.Save(writer); } } void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= mapFileVersion) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
Como paso final, HexCell.Save
registramos el estado del estudio. public void Save (BinaryWriter writer) { … writer.Write(IsExplored); }
Y lo leeremos al final Load
. Después de eso, llamaremos RefreshVisibility
en caso de que el estado del estudio difiera del anterior. public void Load (BinaryReader reader) { … IsExplored = reader.ReadBoolean(); ShaderData.RefreshVisibility(this); }
Para mantener la compatibilidad con los archivos guardados anteriores, debemos omitir la lectura del estado guardado si la versión del archivo es inferior a 3. En este caso, de forma predeterminada, las celdas tendrán el estado "inexplorado". Para hacer esto, necesitamos agregar Load
datos de encabezado como parámetro . public void Load (BinaryReader reader, int header) { … IsExplored = header >= 3 ? reader.ReadBoolean() : false; ShaderData.RefreshVisibility(this); }
Ahora HexGrid.Load
tendrá que pasar los HexCell.Load
datos del encabezado. public void Load (BinaryReader reader, int header) { … for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … }
Ahora, al guardar y cargar mapas, se tendrá en cuenta el estado de exploración de las celdas.paquete de la unidadOcultar celdas desconocidas
En la etapa actual, las células inexploradas están visualmente indicadas por un relieve negro. Pero en realidad, queremos que estas células sean invisibles porque son desconocidas. Podemos hacer que la geometría opaca sea transparente para que no sea visible. Sin embargo, el marco de sombreador de superficie Unity se desarrolló sin esta posibilidad en mente. En lugar de usar una verdadera transparencia, cambiaremos los sombreadores para que coincidan con el fondo, lo que también los hará invisibles.Hacer el alivio realmente negro
Aunque el relieve estudiado es negro, aún podemos reconocerlo porque todavía tiene iluminación especular. Para deshacernos de la iluminación, necesitamos que sea perfectamente negro mate. Para no afectar otras propiedades de la superficie, es más fácil cambiar el color especular a negro. Esto es posible si usa un sombreador de superficie que funciona con especular, pero ahora usamos el metálico estándar. Entonces, comencemos cambiando el sombreador de terreno a especular.Vuelva a colocar la propiedad de color _Metallic en la propiedad _Specular . Por defecto, su valor de color debe ser igual a (0.2, 0.2, 0.2). Por lo tanto, garantizamos que coincidirá con el aspecto de la versión metálica. Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _GridTex ("Grid Texture", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 // _Metallic ("Metallic", Range(0,1)) = 0.0 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) }
También cambie las variables de sombreador correspondientes. El color de los sombreadores de superficie especular se define como fixed3
, así que usémoslo. half _Glossiness;
Cambie la superficie de navegación pragma de Standard a StandardSpecular . Esto obligará a Unity a generar sombreadores usando especular. #pragma surface surf StandardSpecular fullforwardshadows vertex:vert
Ahora la función surf
necesita que el segundo parámetro sea de tipo SurfaceOutputStandardSpecular
. Además, ahora debe asignar el valor no o.Metallic
, pero o.Specular
. void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; // o.Metallic = _Metallic; o.Specular = _Specular; o.Smoothness = _Glossiness; o.Alpha = ca; }
Ahora podemos ocultar los reflejos al considerar el explored
color especular. o.Specular = _Specular * explored;
Terreno inexplorado sin iluminación reflejada.Como puede ver en la imagen, ahora el relieve inexplorado se ve negro mate. Sin embargo, cuando se ve en un ángulo tangente, las superficies se convierten en un espejo, por lo que el relieve comienza a reflejar el entorno, es decir, la caja del cielo.¿Por qué las superficies se convierten en espejos? Las áreas inexploradas aún reflejan el medio ambiente.Para deshacerse de estos reflejos, consideraremos el relieve inexplorado completamente sombreado. Esto se logra asignando un valor al explored
parámetro de oclusión, que usamos como máscara de reflexión. float explored = IN.visibility.w; o.Albedo = c.rgb * grid * _Color * explored; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca;
Inexplorado sin reflejos.Fondo a juego
Ahora que el terreno inexplorado ignora toda la iluminación, debe hacer que coincida con el fondo. Como nuestra cámara siempre se ve desde arriba, el fondo siempre es gris. Para indicarle al sombreador Terrain qué color usar, agregue la propiedad _BackgroundColor , que por defecto es negro. Properties { … _BackgroundColor ("Background Color", Color) = (0,0,0) } … half _Glossiness; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor;
Para usar este color, lo agregaremos como luz emisiva. Esto se o.Emission
logra asignando un valor de color de fondo multiplicado por uno menos explorado. o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored);
Como utilizamos el skybox predeterminado, el color de fondo visible no es el mismo. En general, un gris ligeramente rojizo sería el mejor color. Al configurar el material de relieve, puede usar el código 68615BFF para Color hexadecimal .Material en relieve con fondo de color gris.En general, esto funciona, aunque si sabe dónde mirar, notará siluetas muy débiles. Para que el jugador no pueda verlos, puede asignar un color de fondo uniforme de 68615BFF a la cámara en lugar de skybox.Cámara con un color de fondo uniforme.¿Por qué no eliminar el skybox?, , environmental lighting . , .
Ahora no podemos encontrar la diferencia entre el fondo y las celdas inexploradas. Una topografía alta inexplorada aún puede oscurecer una topografía baja explorada en ángulos de cámara bajos. Además, las partes inexploradas aún proyectan sombras sobre lo explorado. Pero estas pistas mínimas pueden ser descuidadas.Las células inexploradas ya no son visibles.¿Qué pasa si no usa un color de fondo uniforme?, , . . , . , , , UV- .
Ocultar objetos en relieve
Ahora solo tenemos oculta la malla del relieve. El resto del estado del estudio aún no ha afectado.Hasta ahora, solo el alivio está oculto.Ahora vamos a cambiar el sombreado de la función , que es de sombreado opaco como el terreno . Conviértalo en un sombreador especular y agregue el color de fondo. Comencemos con las propiedades. Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 // _Metallic ("Metallic", Range(0,1)) = 0.0 _Specular ("Specular", Color) = (0.2, 0.2, 0.2) _BackgroundColor ("Background Color", Color) = (0,0,0) [NoScaleOffset] _GridCoordinates ("Grid Coordinates", 2D) = "white" {} }
Más superficie de pragma y variables, como antes. #pragma surface surf StandardSpecular fullforwardshadows vertex:vert … half _Glossiness;
visibility
También se requiere un componente más. Como la función combina la visibilidad para cada vértice, solo necesitaba un valor flotante. Ahora necesitamos dos. struct Input { float2 uv_MainTex; float2 visibility; };
Cámbielo vert
para que se use explícitamente para los datos de visibilidad data.visibility.x
y luego asigne el data.visibility.y
valor de los datos del estudio. void vert (inout appdata_full v, out Input data) { … float4 cellData = GetCellData(cellDataCoordinates); data.visibility.x = cellData.x; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cellData.y; }
Cámbielo surf
para que use los nuevos datos, como Terrain . void surf (Input IN, inout SurfaceOutputStandardSpecular o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; float explored = IN.visibility.y; o.Albedo = c.rgb * (IN.visibility.x * explored); // o.Metallic = _Metallic; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored); o.Alpha = ca; }
Objetos ocultos en relieve.Ocultar el agua
El siguiente son los sombreadores Water y Water Shore . Comencemos convirtiéndolos en sombreadores especulares. Sin embargo, no necesitan un color de fondo porque son sombreadores transparentes.Después de la conversión, agregue visibility
un componente más y cámbielo en consecuencia vert
. Ambos sombreadores combinan datos de tres celdas. struct Input { … float2 visibility; }; … void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x * v.color.x + cell1.x * v.color.y + cell2.x * v.color.z; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cell0.y * v.color.x + cell1.y * v.color.y + cell2.y * v.color.z; }
Water y Water Shore realizan surf
diferentes operaciones, pero establecen sus propiedades de superficie de la misma manera. Como son transparentes, tendremos en cuenta explore
en el canal alfa y no estableceremos emisiones. void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.y; o.Albedo = c.rgb * IN.visibility.x; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca * explored; }
Agua oculta.Escondemos estuarios, ríos y caminos.
Todavía tenemos los sombreadores para Estuario , Río y Carretera . Los tres son transparentes y combinan los datos de dos celdas. Cambie todos a especular y luego agréguelos a los visibility
datos de la investigación. struct Input { … float2 visibility; }; … void vert (inout appdata_full v, out Input data) { … data.visibility.x = cell0.x * v.color.x + cell1.x * v.color.y; data.visibility.x = lerp(0.25, 1, data.visibility.x); data.visibility.y = cell0.y * v.color.x + cell1.y * v.color.y; }
Cambie la función de los surf
sombreadores Estuario y Río para que use los nuevos datos. Ambos necesitan hacer los mismos cambios. void surf (Input IN, inout SurfaceOutputStandardSpecular o) { … float explored = IN.visibility.y; fixed4 c = saturate(_Color + water); o.Albedo = c.rgb * IN.visibility.x; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = ca * explored; }
El Shader Road es un poco diferente porque utiliza una métrica de mezcla adicional. void surf (Input IN, inout SurfaceOutputStandardSpecular o) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color * ((noise.y * 0.75 + 0.25) * IN.visibility.x); float blend = IN.uv_MainTex.x; blend *= noise.x + 0.5; blend = smoothstep(0.4, 0.7, blend); float explored = IN.visibility.y; o.Albedo = c.rgb; o.Specular = _Specular * explored; o.Smoothness = _Glossiness; o.Occlusion = explored; o.Alpha = blend * explored; }
Todo esta escondido.paquete de la unidadEvitar las células inexploradas
Aunque todo lo desconocido se oculta visualmente, el estado del estudio no se tiene en cuenta al buscar un camino. Como resultado, se puede ordenar a las unidades que se muevan a través y a través de celdas inexploradas, determinando mágicamente en qué dirección moverse. Necesitamos forzar unidades para evitar células inexploradas.Navega por celdas inexploradas.Los escuadrones determinan el costo de mudarse
Antes de abordar las celdas inexploradas, rehicemos el código para transferir el costo de pasar de HexGrid
a HexUnit
. Esto simplificará el soporte para unidades con diferentes reglas de movimiento.Agregue al HexUnit
método general GetMoveCost
para determinar el costo de la mudanza. Necesita saber qué células se mueven entre ellas, así como la dirección. Copiamos el código correspondiente para los costos de pasar HexGrid.Search
a este método y cambiamos los nombres de las variables. public int GetMoveCost ( HexCell fromCell, HexCell toCell, HexDirection direction) { HexEdgeType edgeType = fromCell.GetEdgeType(toCell); if (edgeType == HexEdgeType.Cliff) { continue; } int moveCost; if (fromCell.HasRoadThroughEdge(direction)) { moveCost = 1; } else if (fromCell.Walled != toCell.Walled) { continue; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += toCell.UrbanLevel + toCell.FarmLevel + toCell.PlantLevel; } }
El método debe devolver el costo de la mudanza. Utilicé el código anterior para omitir movimientos no válidos continue
, pero este enfoque no funcionará aquí. Si el movimiento no es posible, le devolveremos los costos negativos de la mudanza. public int GetMoveCost ( HexCell fromCell, HexCell toCell, HexDirection direction) { HexEdgeType edgeType = fromCell.GetEdgeType(toCell); if (edgeType == HexEdgeType.Cliff) { return -1; } int moveCost; if (fromCell.HasRoadThroughEdge(direction)) { moveCost = 1; } else if (fromCell.Walled != toCell.Walled) { return -1; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += toCell.UrbanLevel + toCell.FarmLevel + toCell.PlantLevel; } return moveCost; }
Ahora necesitamos saber al encontrar el camino, no solo la velocidad, sino también la unidad seleccionada. Cambiar en consecuencia HexGameUI.DoPathFinding
. void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, selectedUnit); } else { grid.ClearPath(); } } }
Como todavía necesitamos acceso a la velocidad del equipo, agregaremos a la HexUnit
propiedad Speed
. Si bien devolverá un valor constante de 24. public int Speed { get { return 24; } }
En HexGrid
cambio, FindPath
y Search
para que puedan trabajar con nuestro nuevo enfoque. public void FindPath (HexCell fromCell, HexCell toCell, HexUnit unit) { ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, unit); ShowPath(unit.Speed); } bool Search (HexCell fromCell, HexCell toCell, HexUnit unit) { int speed = unit.Speed; … }
Ahora eliminaremos del Search
código anterior que determinaba si es posible pasar a la siguiente celda y cuáles son los costos de la mudanza. En cambio, llamaremos HexUnit.IsValidDestination
y HexUnit.GetMoveCost
. Omitiremos la celda si el costo de la mudanza es negativo. for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; }
Evitar áreas inexploradas
Para evitar células inexploradas, es suficiente que nos aseguremos de HexUnit.IsValidDestination
verificar si se examina la célula. public bool IsValidDestination (HexCell cell) { return cell.IsExplored && !cell.IsUnderwater && !cell.Unit; }
Más unidades no podrán llegar a las celdas inexploradas.Como las celdas inexploradas ya no son puntos finales válidos, los escuadrones las evitarán cuando se muevan al punto final. Es decir, las áreas inexploradas actúan como barreras que alargan el camino o incluso lo hacen imposible. Tendremos que acercar las unidades a un terreno desconocido para explorar primero el área.¿Qué pasa si aparece un camino más corto durante el movimiento?. , . .
, , . , .
paquete de la unidadParte 22: Visibilidad mejorada
- Cambia suavemente la visibilidad.
- Use la altura de la celda para determinar el alcance.
- Ocultar el borde del mapa.
Al agregar soporte para la exploración de mapas, mejoraremos los cálculos y las transiciones del alcance.Para ver más, sube más alto.Transiciones de visibilidad
La celda es visible o invisible, porque está dentro del alcance del desprendimiento o no. Incluso si parece que una unidad tarda un tiempo en moverse entre las celdas, su alcance salta de una celda a otra al instante. Como resultado, la visibilidad de las celdas circundantes cambia dramáticamente. El movimiento del escuadrón parece suave, pero los cambios en la visibilidad son repentinos.Idealmente, la visibilidad también debería cambiar sin problemas. Una vez en el campo de visibilidad, las celdas deben iluminarse gradualmente y, dejándolo, oscurecerse gradualmente. ¿O tal vez prefieres las transiciones instantáneas? Agreguemos a la HexCellShaderData
propiedad que cambia las transiciones instantáneas. Por defecto, las transiciones serán suaves. public bool ImmediateMode { get; set; }
Transition Cell Tracking
Incluso cuando se muestran transiciones suaves, los datos de visibilidad verdaderos siguen siendo binarios, es decir, el efecto es solo visual. Esto significa que las transiciones de visibilidad deben tratarse HexCellShaderData
. Le daremos una lista de celdas en las que se realiza la transición. Asegúrese de que en cada inicialización esté vacío. using System.Collections.Generic; using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; Color32[] cellTextureData; List<HexCell> transitioningCells = new List<HexCell>(); public bool ImmediateMode { get; set; } public void Initialize (int x, int z) { … transitioningCells.Clear(); enabled = true; } … }
Por el momento, estamos configurando los datos de la celda RefreshVisibility
directamente. Esto sigue siendo correcto para el modo de transición instantánea, pero cuando está deshabilitado, debemos agregar una celda a la lista de celdas de transición. public void RefreshVisibility (HexCell cell) { int index = cell.Index; if (ImmediateMode) { cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; } else { transitioningCells.Add(cell); } enabled = true; }
La visibilidad ya no parece funcionar porque, por ahora, no estamos haciendo nada con las celdas de la lista.Recorrer las celdas en un bucle
En lugar de establecer instantáneamente los valores correspondientes a 255 o 0, aumentaremos / disminuiremos estos valores gradualmente. La suavidad de la transición depende de la tasa de cambio. No debe ser muy rápido ni muy lento. Un buen compromiso entre hermosas transiciones y la conveniencia del juego es cambiar en un segundo. Vamos a establecer una constante para que esto sea más fácil de cambiar. const float transitionSpeed = 255f;
Ahora en LateUpdate
podemos definir el delta aplicado a los valores. Para hacer esto, multiplique el tiempo delta por la velocidad. Debe ser un número entero porque no sabemos qué tan grande puede ser. Una fuerte caída en la velocidad de fotogramas puede hacer que el delta sea más de 255.Además, necesitamos actualizar mientras haya celdas de transición. Por lo tanto, el código debe incluirse mientras haya algo en la lista. void LateUpdate () { int delta = (int)(Time.deltaTime * transitionSpeed); cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = transitioningCells.Count > 0; }
También teóricamente posible velocidades de cuadro muy altas. En combinación con una baja velocidad de transición, esto puede darnos un delta de 0. Para que se produzca el cambio, forzamos el mínimo delta a 1. int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; }
Una vez recibido el delta, podemos recorrer todas las celdas de transición y actualizar sus datos. Supongamos que tenemos un método para esto UpdateCellData
, cuyos parámetros son la celda y el delta correspondientes. int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; } for (int i = 0; i < transitioningCells.Count; i++) { UpdateCellData(transitioningCells[i], delta); }
En algún momento, la transición celular debería completarse. Suponga que el método devuelve información sobre si la transición aún está en curso. Cuando deja de continuar, podemos eliminar la celda de la lista. Después de eso, debemos disminuir el iterador para no omitir las celdas. for (int i = 0; i < transitioningCells.Count; i++) { if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells.RemoveAt(i--); } }
El orden en que se procesan las celdas de transición no es importante. Por lo tanto, no tenemos que eliminar la celda en el índice actual, lo que obligaría a RemoveAt
todas las celdas a moverse después. En su lugar, movemos la última celda al índice actual y luego eliminamos la última. if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells[i--] = transitioningCells[transitioningCells.Count - 1]; transitioningCells.RemoveAt(transitioningCells.Count - 1); }
Ahora tenemos que crear un método UpdateCellData
. Para hacer su trabajo, necesitará un índice y datos de celda, así que comencemos por obtenerlos. También debe determinar si continuará actualizando la celda. Por defecto, asumiremos que no es necesario. Una vez finalizado el trabajo, es necesario aplicar los datos modificados y devolver el estado "la actualización continúa". bool UpdateCellData (HexCell cell, int delta) { int index = cell.Index; Color32 data = cellTextureData[index]; bool stillUpdating = false; cellTextureData[index] = data; return stillUpdating; }
Actualización de datos de celda
En esta etapa, tenemos una célula que está en proceso de transición o que ya la ha completado. Primero, verifiquemos el estado de la sonda celular. Si se examina la celda, pero su valor G aún no es igual a 255, entonces está en el proceso de transición, por lo que controlaremos esto. bool stillUpdating = false; if (cell.IsExplored && data.g < 255) { stillUpdating = true; } cellTextureData[index] = data;
Para realizar la transición, agregaremos un delta al valor G de la celda. Las operaciones aritméticas no funcionan con bytes, primero se convierten en enteros. Por lo tanto, la suma tendrá el formato entero, que debe convertirse a byte. if (cell.IsExplored && data.g < 255) { stillUpdating = true; int t = data.g + delta; data.g = (byte)t; }
Pero antes de la conversión, debe asegurarse de que el valor no exceda 255. int t = data.g + delta; data.g = t >= 255 ? (byte)255 : (byte)t;
A continuación, debemos hacer lo mismo para la visibilidad, que utiliza el valor de R. if (cell.IsExplored && data.g < 255) { … } if (cell.IsVisible && data.r < 255) { stillUpdating = true; int t = data.r + delta; data.r = t >= 255 ? (byte)255 : (byte)t; }
Dado que la celda puede volverse invisible nuevamente, debemos verificar si es necesario disminuir el valor de R. Esto sucede cuando la celda es invisible, pero R es mayor que cero. if (cell.IsVisible) { if (data.r < 255) { stillUpdating = true; int t = data.r + delta; data.r = t >= 255 ? (byte)255 : (byte)t; } } else if (data.r > 0) { stillUpdating = true; int t = data.r - delta; data.r = t < 0 ? (byte)0 : (byte)t; }
Ahora está UpdateCellData
listo y las transiciones de visibilidad se realizan correctamente.Transiciones de visibilidad.Protección contra elementos de transición duplicados.
Las transiciones funcionan, pero pueden aparecer elementos duplicados en la lista. Esto sucede si el estado de visibilidad de la celda cambia mientras aún está en transición. Por ejemplo, cuando la celda es visible durante el movimiento del escuadrón solo por un corto tiempo.Como resultado de la aparición de elementos duplicados, la transición de celda se actualiza varias veces por cuadro, lo que conduce a transiciones más rápidas y trabajo adicional. Podemos evitar esto comprobando antes de agregar una celda si ya está en la lista. Sin embargo, una búsqueda de lista en cada llamadaRefreshVisibility
costoso, especialmente cuando se realizan múltiples transiciones celulares. En su lugar, usemos otro canal que aún no se haya utilizado para indicar si la celda está en proceso de transición, por ejemplo, el valor B. Al agregar una celda a la lista, le asignaremos el valor 255 y agregaremos solo aquellas celdas cuyo valor no sea igual a 255. public void RefreshVisibility (HexCell cell) { int index = cell.Index; if (ImmediateMode) { cellTextureData[index].r = cell.IsVisible ? (byte)255 : (byte)0; cellTextureData[index].g = cell.IsExplored ? (byte)255 : (byte)0; } else if (cellTextureData[index].b != 255) { cellTextureData[index].b = 255; transitioningCells.Add(cell); } enabled = true; }
Para que esto funcione, necesitamos restablecer el valor de B después de completar la transición celular. bool UpdateCellData (HexCell cell, int delta) { … if (!stillUpdating) { data.b = 0; } cellTextureData[index] = data; return stillUpdating; }
Transiciones sin duplicados.Visibilidad de carga instantánea
Los cambios de visibilidad ahora son siempre graduales, incluso cuando se carga un mapa. Esto es ilógico, porque el mapa describe el estado en el que las celdas ya son visibles, por lo que la transición es inapropiada aquí. Además, realizar transiciones para las muchas celdas visibles de un mapa grande puede ralentizar el juego después de cargarlo. Por lo tanto, antes de cargar celdas y escuadrones, pasemos HexGrid.Load
al modo de transición instantánea. public void Load (BinaryReader reader, int header) { … cellShaderData.ImmediateMode = true; for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … }
Entonces redefinimos la configuración inicial del modo de transición instantánea, sea lo que sea. Tal vez ya esté apagado o haya sido una opción de configuración, por lo que recordaremos el modo inicial y cambiaremos a él después de completar el trabajo. public void Load (BinaryReader reader, int header) { … bool originalImmediateMode = cellShaderData.ImmediateMode; cellShaderData.ImmediateMode = true; … cellShaderData.ImmediateMode = originalImmediateMode; }
paquete de la unidadAlcance dependiente de la altura
Hasta ahora hemos usado un alcance constante de tres para todas las unidades, pero en realidad es más complicado. En el caso general, no podemos ver el objeto por dos razones: algún obstáculo nos impide verlo o el objeto es demasiado pequeño o está muy lejos. En nuestro juego, solo implementamos la limitación del alcance.No podemos ver lo que está en el lado opuesto de la Tierra, porque el planeta nos oculta. Solo podemos ver el horizonte. Como el planeta puede considerarse aproximadamente una esfera, cuanto más alto sea el punto de vista, más superficie podemos ver, es decir, el horizonte depende de la altura.El horizonte depende de la altura del punto de vista.La visibilidad limitada de nuestras unidades imita el efecto del horizonte creado por la curvatura de la Tierra. El alcance de su revisión depende del tamaño del planeta y la escala del mapa. Al menos esa es la explicación lógica. Pero la razón principal para reducir el alcance es la jugabilidad, esta es una limitación llamada niebla de guerra. Sin embargo, al comprender la física subyacente en el campo de visión, podemos concluir que un punto de vista alto debería tener un valor estratégico, ya que aleja el horizonte y le permite mirar los obstáculos más bajos. Pero hasta ahora no lo hemos implementado.Altura para revisión
Para tener en cuenta la altura al determinar el alcance, necesitamos saber la altura. Esta será la altura o nivel habitual de agua, dependiendo de si la celda de tierra o el agua. Agreguemos esto a la HexCell
propiedad. public int ViewElevation { get { return elevation >= waterLevel ? elevation : waterLevel; } }
Pero si la altura afecta el alcance, entonces con un cambio en la altura de visualización de la celda, la situación de visibilidad también puede cambiar. Dado que la celda ha bloqueado o ahora está bloqueando el alcance de varias unidades, no es tan fácil determinar qué se debe cambiar. La célula en sí misma no podrá resolver este problema, así que permítale informar un cambio en la situación HexCellShaderData
. Supongamos que HexCellShaderData
tiene un método para esto ViewElevationChanged
. Lo llamaremos a la asignación HexCell.Elevation
, si es necesario. public int Elevation { get { return elevation; } set { if (elevation == value) { return; } int originalViewElevation = ViewElevation; elevation = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } … } }
Lo mismo vale para WaterLevel
. public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } int originalViewElevation = ViewElevation; waterLevel = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } ValidateRivers(); Refresh(); } }
Restablecer visibilidad
Ahora necesitamos crear un método HexCellShaderData.ViewElevationChanged
. Determinar cómo cambia una situación de visibilidad general es una tarea compleja, especialmente cuando se cambian varias celdas al mismo tiempo. Por lo tanto, no inventaremos ningún truco, sino que simplemente planearemos restablecer la visibilidad de todas las celdas. Agregue un campo booleano para realizar un seguimiento de si hacer esto. Dentro del método, simplemente lo estableceremos en verdadero e incluiremos el componente. Independientemente de la cantidad de celdas que hayan cambiado simultáneamente, esto llevará a un solo reinicio. bool needsVisibilityReset; … public void ViewElevationChanged () { needsVisibilityReset = true; enabled = true; }
Para restablecer los valores de visibilidad de todas las celdas, debe tener acceso a ellas, que HexCellShaderData
no tiene. Así que deleguemos esta responsabilidad HexGrid
. Para hacer esto, debe agregar a la HexCellShaderData
propiedad, que le permitirá hacer referencia a la cuadrícula. Luego podemos usarlo LateUpdate
para solicitar un reinicio. public HexGrid Grid { get; set; } … void LateUpdate () { if (needsVisibilityReset) { needsVisibilityReset = false; Grid.ResetVisibility(); } … }
Pasemos a HexGrid
: establecer el enlace a la cuadrícula HexGrid.Awake
después de crear los datos del sombreador. void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); cellShaderData.Grid = this; CreateMap(cellCountX, cellCountZ); }
HexGrid
También debería obtener un método ResetVisibility
para descartar todas las células. Simplemente haga que recorra todas las celdas del bucle y delegue el restablecimiento a sí mismo. public void ResetVisibility () { for (int i = 0; i < cells.Length; i++) { cells[i].ResetVisibility(); } }
Ahora necesitamos agregar al HexCell
método ResetVisibilty
. Simplemente pondrá a cero la visibilidad y activará la actualización de visibilidad. Esto debe hacerse cuando la visibilidad de la celda es mayor que cero. public void ResetVisibility () { if (visibility > 0) { visibility = 0; ShaderData.RefreshVisibility(this); } }
Después de restablecer todos los datos de visibilidad, HexGrid.ResetVisibility
debe aplicar nuevamente la visibilidad a todos los escuadrones, para lo cual necesita conocer el alcance de cada escuadrón. Supongamos, lo puede conseguir con la propiedad VisionRange
. public void ResetVisibility () { for (int i = 0; i < cells.Length; i++) { cells[i].ResetVisibility(); } for (int i = 0; i < units.Count; i++) { HexUnit unit = units[i]; IncreaseVisibility(unit.Location, unit.VisionRange); } }
Para que esto funcione, refactorizaremos el cambio HexUnit.visionRange
de nombre HexUnit.VisionRange
y lo convertiremos en una propiedad. Si bien recibirá un valor constante de 3, pero en el futuro cambiará. public int VisionRange { get { return 3; } }
Debido a esto, los datos de visibilidad se restablecerán y seguirán siendo correctos después de cambiar la altura de visualización de la celda. Pero es probable que cambiemos las reglas para determinar el alcance y ejecutemos la compilación en modo Play. Para que el alcance cambie de forma independiente, ejecutemos un restablecimiento HexGrid.OnEnable
cuando se detecte la compilación. void OnEnable () { if (!HexMetrics.noiseSource) { … ResetVisibility(); } }
Ahora puede cambiar el código de alcance y ver los resultados, mientras permanece en el modo Reproducir.Expandiendo el horizonte
Se determina el cálculo del alcance HexGrid.GetVisibleCells
. Para que la altura afecte el alcance, simplemente podemos usar la altura de visualización fromCell
redefiniendo temporalmente el área transmitida. Entonces podemos verificar fácilmente si esto funciona. List<HexCell> GetVisibleCells (HexCell fromCell, int range) { … range = fromCell.ViewElevation; fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); … }
Use la altura como alcance.Obstáculos a la visibilidad
La aplicación de una altura de visualización como ámbito solo funciona correctamente cuando todas las demás celdas están a altura cero. Pero si todas las celdas tienen la misma altura que el punto de vista, entonces el campo de visión debería ser cero. Además, las celdas con alturas altas deberían bloquear la visibilidad de las celdas bajas detrás de ellas. Hasta ahora, nada de esto se ha implementado.El alcance no interfiere.La forma más correcta de determinar el alcance sería verificar mediante la emisión de rayos, pero rápidamente se volvería costoso y aún produciría resultados extraños. Necesitamos una solución rápida que genere resultados suficientemente buenos que no tengan que ser perfectos. Además, es importante que las reglas para determinar el alcance sean simples, intuitivas y predecibles para los jugadores.Nuestra solución será la siguiente: al determinar la visibilidad de una celda, agregaremos la altura de visualización de la celda vecina a la distancia cubierta. De hecho, esto reduce el alcance cuando observamos estas celdas, y si se omiten, esto no nos permitirá llegar a las celdas detrás de ellas. int distance = current.Distance + 1; if (distance + neighbor.ViewElevation > range) { continue; }
Las celdas altas bloquean la vista.¿No deberíamos ver células altas en la distancia?, , , . , .
No mires alrededor de las esquinas
Ahora parece que las celdas altas bloquean la vista a baja, pero a veces el alcance penetra a través de ellas, aunque parece que esto no debería ser así. Esto sucede porque el algoritmo de búsqueda todavía encuentra una ruta a estas celdas, sin pasar por las celdas de bloqueo. Como resultado, parece que nuestra área de visibilidad puede sortear obstáculos. Para evitar esto, debemos asegurarnos de que solo se tengan en cuenta las rutas más cortas al determinar la visibilidad de la celda. Esto se puede hacer soltando rutas que se vuelven más largas de lo necesario. HexCoordinates fromCoordinates = fromCell.coordinates; while (searchFrontier.Count > 0) { … for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + 1; if (distance + neighbor.ViewElevation > range || distance > fromCoordinates.DistanceTo(neighbor.coordinates) ) { continue; } … } }
Usamos solo los caminos más cortos.Así que arreglamos la mayoría de los casos obviamente erróneos. Para las celdas cercanas, esto funciona bien, porque solo hay caminos más cortos para llegar a ellas. Las celdas más lejanas tienen más opciones para los caminos; por lo tanto, a largas distancias, todavía puede ocurrir una envolvente de visibilidad. Esto no será un problema si las áreas de visibilidad siguen siendo pequeñas y las diferencias en las alturas adyacentes no son demasiado grandes.Y finalmente, en lugar de reemplazar el campo de visión transmitido, le agregamos la altura de la vista. El campo de visión del escuadrón indica su altura, altitud de vuelo o capacidades de reconocimiento. range += fromCell.ViewElevation;
Ver con un campo de visión completo en un punto de vista bajo.Es decir, las reglas finales de visibilidad se aplican a la visión cuando se mueve a lo largo del camino más corto hacia el campo de visibilidad, teniendo en cuenta la diferencia en la altura de la celda con respecto al punto de vista. Cuando una celda está fuera del alcance, bloquea todos los caminos a través de ella. Como resultado, los puntos de observación altos, desde los cuales nada impide la vista, se vuelven estratégicamente valiosos.¿Qué hay de obstruir la visibilidad de los objetos?, , . , , . .
paquete de la unidadCélulas que no pueden ser exploradas
El último problema con la visibilidad se refiere a los bordes del mapa. El alivio abruptamente y sin transiciones termina, porque las celdas en el borde no tienen vecinos.Borde marcado del mapa.Idealmente, la presentación visual de áreas y bordes inexplorados del mapa debería ser la misma. Podemos lograr esto agregando casos especiales cuando triangulamos bordes, cuando no tienen vecinos, pero esto requerirá una lógica adicional, y tendremos que trabajar con las celdas faltantes. Por lo tanto, tal solución no es trivial. Un enfoque alternativo es forzar que las celdas límite del mapa no se exploren, incluso si están dentro del alcance del escuadrón. Este enfoque es mucho más simple, así que usémoslo. También le permite marcar como inexploradas y otras celdas, lo que facilita la creación de bordes irregulares del mapa. Además, las celdas ocultas en los bordes le permiten crear carreteras y ríos que entran y salen del mapa del río y la carretera, porque sus puntos finales estarán fuera del alcance.Además, con la ayuda de esta solución, puede agregar unidades que entran y salen del mapa.Marcamos las células como investigadas
Para indicar que se puede examinar una celda, agregue a la HexCell
propiedad Explorable
. public bool Explorable { get; set; }
Ahora, una celda puede ser visible si está investigada, por IsVisible
lo que cambiaremos la propiedad para tener esto en cuenta. public bool IsVisible { get { return visibility > 0 && Explorable; } }
Lo mismo se aplica a IsExplored
. Sin embargo, para esto investigamos la propiedad estándar. Necesitamos convertirlo en una propiedad explícita para poder cambiar la lógica de su captador. public bool IsExplored { get { return explored && Explorable; } private set { explored = value; } } … bool explored;
Ocultar el borde del mapa
Puede ocultar el borde de un mapa rectangular en el método HexGrid.CreateCell
. Las células que no están en el borde son investigadas, el resto están sin explorar. void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab); cell.transform.localPosition = position; cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; cell.Explorable = x > 0 && z > 0 && x < cellCountX - 1 && z < cellCountZ - 1; … }
Ahora las cartas se oscurecen alrededor de los bordes, escondiéndose detrás de ellos enormes espacios inexplorados. Como resultado, el tamaño del área de mapas estudiada disminuye en cada dimensión en dos.Borde inexplorado del mapa.¿Es posible hacer que el estado de investigación sea editable?, , . .
Las celdas inexploradas impiden la visibilidad
Finalmente, si la celda no puede ser examinada, entonces debería interferir con la visibilidad. Cambie HexGrid.GetVisibleCells
para tener esto en cuenta. if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase || !neighbor.Explorable ) { continue; }
paquete de la unidadParte 23: generando tierra
- Rellene nuevos mapas con paisajes generados.
- Levantamos tierra sobre el agua, inundamos un poco.
- Controlamos la cantidad de tierra creada, su altura y desniveles.
- Agregamos soporte para varias opciones de configuración para crear mapas variables.
- Lo hacemos para que el mismo mapa pueda generarse nuevamente.
Esta parte del tutorial será el comienzo de una serie sobre la generación de mapas de procedimientos.Esta parte fue creada en Unity 2017.1.0.Uno de los muchos mapas generados.Generación de tarjeta
Aunque podemos crear cualquier mapa, lleva mucho tiempo. Sería conveniente si la aplicación pudiera ayudar al diseñador generando tarjetas para él, que luego puede modificar a su gusto. Puede dar otro paso y deshacerse por completo de crear el diseño manualmente, transfiriendo completamente la responsabilidad de generar el mapa terminado a la aplicación. Debido a esto, el juego se puede jugar cada vez con una nueva tarjeta y cada sesión de juego será diferente. Para que todo esto sea posible, debemos crear un algoritmo de generación de mapas.El tipo de algoritmo de generación que necesita depende del tipo de tarjeta que necesita. No existe un enfoque correcto, siempre hay que buscar un compromiso entre credibilidad y jugabilidad.Para que una carta sea creíble, debe parecer bastante posible y real para el jugador. Esto no significa que el mapa deba verse como parte de nuestro planeta. Puede ser un planeta diferente o una realidad completamente diferente. Pero si debe indicar el alivio de la Tierra, entonces debe parecerse al menos en parte.La jugabilidad está relacionada con la forma en que las cartas corresponden al juego. A veces entra en conflicto con la credibilidad. Por ejemplo, aunque las cadenas montañosas pueden verse hermosas, al mismo tiempo limitan en gran medida el movimiento y la vista de las unidades. Si esto no es deseable, entonces debes prescindir de las montañas, lo que reducirá la credibilidad y limitará la expresividad del juego. O podemos salvar las montañas, pero reducir su impacto en el juego, lo que también puede reducir la credibilidad.Además, se debe considerar la viabilidad. Por ejemplo, puede crear un planeta similar a la Tierra muy realista simulando placas tectónicas, erosión, lluvias, erupciones volcánicas, los efectos de meteoritos y la luna, etc. Pero el desarrollo de dicho sistema requerirá mucho tiempo. Además, puede llevar mucho tiempo generar un planeta así, y los jugadores no querrán esperar unos minutos antes de comenzar un nuevo juego. Es decir, la simulación es una herramienta poderosa, pero tiene un precio.Los juegos a menudo usan compensaciones entre credibilidad, jugabilidad y factibilidad. A veces, estos compromisos son invisibles y parecen completamente normales, y a veces parecen aleatorios, inconsistentes o caóticos, dependiendo de las decisiones tomadas durante el proceso de desarrollo. Esto se aplica no solo a la generación de tarjetas, sino que al desarrollar un generador de tarjetas de procedimiento, debe prestar especial atención a esto. Puedes pasar mucho tiempo creando un algoritmo que genere hermosas tarjetas que resulten inútiles para el juego que estás creando.En esta serie de tutoriales, crearemos un relieve similar a la tierra. Debe parecer interesante, con gran variabilidad y ausencia de grandes áreas homogéneas. La escala de relieve será grande, los mapas cubrirán uno o más continentes, regiones de los océanos o incluso un planeta entero. Necesitamos control sobre la geografía, incluidas las masas de tierra, el clima, el número de regiones y los baches del terreno. En esta parte, sentaremos las bases para la creación de sushi.Comenzar en modo edición
Nos centraremos en el mapa, no en la jugabilidad, por lo que será más conveniente iniciar la aplicación en modo de edición. Gracias a esto, podemos ver inmediatamente las tarjetas. Por lo tanto, cambiaremos HexMapEditor.Awake
estableciendo el modo de edición en verdadero y activando la palabra clave de sombreador de este modo. void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); Shader.EnableKeyword("HEX_MAP_EDIT_MODE"); SetEditMode(true); }
Generador de tarjeta
Dado que se necesita bastante código para generar mapas de procedimientos, no lo agregaremos directamente HexGrid
. En su lugar, crearemos un nuevo componente HexMapGenerator
y HexGrid
no lo sabremos. Esto simplificará la transición a otro algoritmo si lo necesitamos.El generador necesita un enlace a la cuadrícula, por lo que agregaremos un campo general. Además, agregamos un método general GenerateMap
que se ocupará del trabajo del algoritmo. Le daremos las dimensiones del mapa como parámetros, y luego lo forzaremos a usarlo para crear un nuevo mapa vacío. using System.Collections.Generic; using UnityEngine; public class HexMapGenerator : MonoBehaviour { public HexGrid grid; public void GenerateMap (int x, int z) { grid.CreateMap(x, z); } }
Agregue un objeto con un componente a la escena HexMapGenerator
y conéctelo a la cuadrícula.Objeto generador de mapas.Cambiar el menú de un nuevo mapa
Lo cambiaremos NewMapMenu
para que pueda generar tarjetas, no solo crear tarjetas vacías. Controlaremos su funcionalidad a través de un campo booleano generateMaps
, que por defecto tiene un valor true
. Creemos un método general para configurar este campo, como hicimos para cambiar las opciones HexMapEditor
. Agregue el interruptor apropiado al menú y conéctelo al método. bool generateMaps = true; public void ToggleMapGeneration (bool toggle) { generateMaps = toggle; }
Menú de una nueva tarjeta con un interruptor.Dele al menú un enlace al generador de mapas. Luego lo forzaremos a llamar al método GenerateMap
generador si es necesario , y no solo a ejecutar la CreateMap
grilla. public HexMapGenerator mapGenerator; … void CreateMap (int x, int z) { if (generateMaps) { mapGenerator.GenerateMap(x, z); } else { hexGrid.CreateMap(x, z); } HexMapCamera.ValidatePosition(); Close(); }
Conexión al generador.Acceso celular
Para que el generador funcione, necesita acceso a las celdas. Nosotros HexGrid
ya tenemos métodos comunes GetCell
que requieren o vector de posición o coordenadas hexagonales. El generador no necesita trabajar con uno u otro, por lo que agregamos dos métodos convenientes HexGrid.GetCell
que funcionarán con las coordenadas del desplazamiento o índice de la celda. public HexCell GetCell (int xOffset, int zOffset) { return cells[xOffset + zOffset * cellCountX]; } public HexCell GetCell (int cellIndex) { return cells[cellIndex]; }
Ahora HexMapGenerator
puede recibir células directamente. Por ejemplo, después de crear un nuevo mapa, puede usar coordenadas de hierba para establecer la hierba como relieve de la columna central de celdas. public void GenerateMap (int x, int z) { grid.CreateMap(x, z); for (int i = 0; i < z; i++) { grid.GetCell(x / 2, i).TerrainTypeIndex = 1; } }
Columna de hierba en un pequeño mapa.paquete de la unidadHacer sushi
Al generar un mapa, comenzamos completamente sin tierra. Uno puede imaginar que el mundo entero está inundado con un gran océano. Se crea una tierra cuando parte del fondo del océano se empuja tanto que se eleva sobre el agua. Necesitamos decidir cuánta tierra se debe crear de esta manera, dónde aparecerá y qué forma tendrá.Levanta el alivio
Comencemos en pequeño: levante un pedazo de tierra sobre el agua. Creamos para esto un método RaiseTerrain
con un parámetro para controlar el tamaño de la trama. Llame a este método GenerateMap
, reemplazando el código de prueba anterior. Comencemos con un pequeño pedazo de tierra que consta de siete celdas. public void GenerateMap (int x, int z) { grid.CreateMap(x, z);
Hasta ahora, utilizamos el tipo de relieve de "hierba" para denotar la tierra elevada, y el relieve original de "arena" se refiere al océano. Haz que RaiseTerrain
tomemos una celda aleatoria y cambiemos el tipo de relieve hasta que obtengamos la cantidad correcta de tierra.Para obtener una celda aleatoria, agregamos un método GetRandomCell
que determina un índice de celda aleatorio y obtiene la celda correspondiente de la cuadrícula. void RaiseTerrain (int chunkSize) { for (int i = 0; i < chunkSize; i++) { GetRandomCell().TerrainTypeIndex = 1; } } HexCell GetRandomCell () { return grid.GetCell(Random.Range(0, grid.cellCountX * grid.cellCountZ)); }
Siete celdas de sushi al azar.Como al final podemos necesitar muchas celdas aleatorias o recorrer todas las celdas varias veces, hagamos un seguimiento del número de celdas en la celda misma HexMapGenerator
. int cellCount; public void GenerateMap (int x, int z) { cellCount = x * z; … } … HexCell GetRandomCell () { return grid.GetCell(Random.Range(0, cellCount)); }
Creación de un sitio.
Hasta ahora, estamos convirtiendo siete celdas aleatorias en tierra, y pueden estar en cualquier lugar. Lo más probable es que no formen una sola área de tierra. Además, podemos seleccionar las mismas celdas varias veces, por lo que obtenemos menos tierra. Para resolver ambos problemas, sin restricciones, seleccionaremos solo la primera celda. Después de eso, deberíamos seleccionar solo aquellas celdas que están al lado de las seleccionadas anteriormente. Estas restricciones son similares a las limitaciones de la búsqueda de ruta, por lo que utilizamos el mismo enfoque aquí.Agregamos HexMapGenerator
nuestra propia propiedad y el contador de la fase del borde de búsqueda, tal como estaba HexGrid
. HexCellPriorityQueue searchFrontier; int searchFrontierPhase;
Compruebe que la cola de prioridad existe antes de que la necesitemos. public void GenerateMap (int x, int z) { cellCount = x * z; grid.CreateMap(x, z); if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } RaiseTerrain(7); }
Después de crear un nuevo mapa, el límite de búsqueda para todas las celdas es cero. Pero si vamos a buscar celdas en el proceso de generación de mapas, aumentaremos su borde de búsqueda en este proceso. Si realizamos muchas operaciones de búsqueda, pueden estar adelantadas a la fase del límite de búsqueda registrada HexGrid
. Esto puede interferir con la búsqueda de rutas de unidad. Para evitar esto, al final del proceso de generación del mapa, restableceremos la fase de búsqueda de todas las celdas a cero. RaiseTerrain(7); for (int i = 0; i < cellCount; i++) { grid.GetCell(i).SearchPhase = 0; }
Ahora RaiseTerrain
tengo que buscar las celdas apropiadas y no seleccionarlas al azar. Este proceso es muy similar al método de búsqueda en HexGrid
. Sin embargo, no visitaremos las celdas más de una vez, por lo que será suficiente para nosotros aumentar la fase del borde de búsqueda en 1 en lugar de 2. Luego, inicializamos el borde con la primera celda, que se selecciona al azar. Como de costumbre, además de establecer su fase de búsqueda, asignamos su distancia y heurística a cero. void RaiseTerrain (int chunkSize) {
Después de eso, el ciclo de búsqueda nos resultará familiar. Además, para continuar la búsqueda hasta que el borde esté vacío, debemos detenernos cuando el fragmento alcance el tamaño deseado, por lo que lo rastrearemos. En cada iteración, extraeremos la siguiente celda de la cola, estableceremos el tipo de relieve, aumentaremos el tamaño y luego puentearemos a los vecinos de esta celda. Todos los vecinos simplemente se agregan a la frontera si aún no se han agregado allí. No necesitamos hacer ningún cambio o comparación. Una vez completado, debe despejar el borde. searchFrontier.Enqueue(firstCell); int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.TerrainTypeIndex = 1; size += 1; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor && neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = 0; neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } } } searchFrontier.Clear();
Una línea de celdas.Tenemos una sola parcela del tamaño correcto. Será más pequeño solo si no hay un número suficiente de celdas. Debido a la forma en que se llena el borde, la trama siempre consiste en una línea que se extiende hacia el noroeste. Cambia de dirección solo cuando llega al borde del mapa.Conectamos celdas
Las áreas de tierra rara vez se parecen a las líneas, y si lo hacen, no siempre están orientadas de la misma manera. Para cambiar la forma del sitio, necesitamos cambiar las prioridades de las celdas. La primera celda aleatoria se puede usar como centro de la trama. Entonces la distancia a todas las otras celdas será relativa a este punto. Entonces daremos mayor prioridad a las celdas que están más cerca del centro, para que el sitio no crezca como una línea, sino alrededor del centro. searchFrontier.Enqueue(firstCell); HexCoordinates center = firstCell.coordinates; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.TerrainTypeIndex = 1; size += 1; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor && neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = neighbor.coordinates.DistanceTo(center); neighbor.SearchHeuristic = 0; searchFrontier.Enqueue(neighbor); } } }
La acumulación de células.Y, de hecho, ahora nuestras siete celdas están bellamente empaquetadas en un área hexagonal compacta si la celda central no aparece en el borde del mapa. Intentemos ahora usar un tamaño de parcela de 30. RaiseTerrain(30);
Masa de sushi en 30 celdas.Nuevamente obtuvimos la misma forma, aunque no había suficientes celdas para obtener el hexágono correcto. Dado que el radio de la trama es mayor, es más probable que esté cerca del borde del mapa, lo que lo obligará a tomar una forma diferente.Aleatorización de sushi
No queremos que todas las áreas tengan el mismo aspecto, por lo que cambiaremos ligeramente las prioridades de las celdas. Cada vez que agreguemos una celda vecina al borde, si el siguiente número es Random.value
menor que un cierto valor umbral, entonces la heurística de esta celda no se convierte en 0, sino en 1. Usemos el valor 0.5 como umbral, es decir, lo más probable es que afecte a la mitad de las celdas. neighbor.Distance = neighbor.coordinates.DistanceTo(center); neighbor.SearchHeuristic = Random.value < 0.5f ? 1: 0; searchFrontier.Enqueue(neighbor);
Área distorsionada.Al aumentar la búsqueda heurística de la célula, la visitamos más tarde de lo esperado. Al mismo tiempo, otras celdas ubicadas un paso más allá del centro serán visitadas antes, a menos que también aumenten la heurística. Esto significa que si aumentamos la heurística de todas las celdas en un valor, esto no afectará el mapa. Es decir, el umbral 1 no tendrá efecto, como el umbral 0. Y el umbral 0.8 será equivalente a 0.2. Es decir, la probabilidad de 0.5 hace que el proceso de búsqueda sea el más "tembloroso".La cantidad adecuada de oscilación depende del tipo de terreno deseado, así que hagámoslo personalizable. Agregue un campo flotante genérico jitterProbability
con el atributo al generadorRange
limitado en el rango 0-0.5. Vamos a darle un valor predeterminado igual al promedio de este intervalo, es decir, 0.25. Esto nos permitirá configurar el generador en la ventana del inspector de Unity. [Range(0f, 0.5f)] public float jitterProbability = 0.25f;
Probabilidad de fluctuaciones.¿Se puede personalizar en la interfaz de usuario del juego?, . UI, . , UI. , . , .
Ahora, para tomar una decisión sobre cuándo la heurística debe ser igual a 1, usamos la probabilidad en lugar de un valor constante. neighbor.SearchHeuristic = Random.value < jitterProbability ? 1: 0;
Usamos valores heurísticos 0 y 1. Aunque se pueden usar valores más grandes, esto empeorará en gran medida la deformación de las secciones, lo más probable es que las convierta en un montón de rayas.Levantar algo de tierra
No nos limitaremos a la generación de un pedazo de tierra. Por ejemplo, colocamos una llamada RaiseTerrain
dentro de un bucle para obtener cinco secciones. for (int i = 0; i < 5; i++) { RaiseTerrain(30); }
Cinco parcelas de tierra.Aunque ahora estamos generando cinco parcelas de 30 celdas cada una, pero no necesariamente obtenemos exactamente 150 celdas de tierra. Como cada sitio se crea por separado, no se conocen entre sí, por lo que pueden cruzarse. Esto es normal porque puede crear paisajes más interesantes que solo un conjunto de secciones aisladas.Para aumentar la variabilidad de la tierra, también podemos cambiar el tamaño de cada parcela. Agregue dos campos enteros para controlar los tamaños mínimo y máximo de las parcelas. Asigne un intervalo suficientemente grande, por ejemplo, 20-200. Haré que el mínimo estándar sea igual a 30, y el máximo estándar - 100. [Range(20, 200)] public int chunkSizeMin = 30; [Range(20, 200)] public int chunkSizeMax = 100;
Intervalo de dimensionamiento.Usamos estos campos para determinar aleatoriamente el tamaño del área cuando se llama RaiseTerrain
. RaiseTerrain(Random.Range(chunkSizeMin, chunkSizeMax + 1));
Cinco secciones de tamaño aleatorio en el mapa central.Crea suficiente sushi
Si bien no podemos controlar particularmente la cantidad de tierra generada. Aunque podemos agregar la opción de configuración para el número de gráficos, los gráficos en sí son de tamaño aleatorio y pueden superponerse leve o fuertemente. Por lo tanto, el número de sitios no garantiza la recepción en el mapa de la cantidad de tierra requerida. Agreguemos una opción para controlar directamente el porcentaje de tierra expresado como un entero. Dado que el 100% de tierra o agua no es muy interesante, lo limitamos al intervalo 5–95, con un valor de 50 por defecto. [Range(5, 95)] public int landPercentage = 50;
Porcentaje de sushi.Para garantizar la creación de la cantidad correcta de tierra, solo necesitamos continuar elevando áreas del terreno hasta que obtengamos una cantidad suficiente. Para hacer esto, necesitamos controlar el proceso, lo que complicará la generación de tierras. Por lo tanto, reemplacemos el ciclo existente de creación de sitios llamando a un nuevo método CreateLand
. Lo primero que hace este método es calcular la cantidad de celdas que deberían convertirse en tierra. Esta cantidad será nuestra suma total de celdas de sushi. public void GenerateMap (int x, int z) { …
CreateLand
causará RaiseTerrain
hasta que hayamos gastado la cantidad total de células. Para no exceder la cantidad, cambiamos RaiseTerrain
para que reciba la cantidad como un parámetro adicional. Después de terminar el trabajo, debe devolver la cantidad restante.
La cantidad debe disminuir cada vez que la celda se retira del borde y se convierte en tierra. Si después de esto se gasta todo el monto, entonces debemos detener la búsqueda y completar el sitio. Además, esto debe hacerse solo cuando la celda actual aún no está en tierra. while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); if (current.TerrainTypeIndex == 0) { current.TerrainTypeIndex = 1; if (--budget == 0) { break; } } size += 1; … }
Ahora CreateLand
puede levantar tierra hasta que gaste la cantidad total de células. void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); while (landBudget > 0) { landBudget = RaiseTerrain( Random.Range(chunkSizeMin, chunkSizeMax + 1), landBudget ); } }
Exactamente la mitad del mapa se convirtió en tierra.paquete de la unidadTener en cuenta la altura
La tierra no es solo una placa plana, limitada por la costa. Ella tiene una altura cambiante, que contiene colinas, montañas, valles, lagos, etc. Existen grandes diferencias en altura debido a la interacción de placas tectónicas que se mueven lentamente. Aunque no lo simularemos, nuestras áreas de tierra deberían de alguna manera parecerse a esas placas. Los sitios no se mueven, pero pueden cruzarse. Y podemos aprovechar esto.Empuja la tierra hacia arriba
Cada parcela representa una porción de tierra expulsada del fondo del océano. Por lo tanto, aumentemos constantemente la altura de la celda actual RaiseTerrain
y veamos qué sucede. HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.TerrainTypeIndex == 0) { … }
Terreno con alturas.Tenemos las alturas, pero es difícil de ver. Puede hacerlos más legibles si usa su propio tipo de terreno para cada nivel de altura, como capas geográficas. Solo haremos esto para que las alturas sean más notables, por lo que simplemente puede usar el nivel de altura como índice de elevación.¿Qué sucede si la altura excede el número de tipos de terreno?. , .
En lugar de actualizar el tipo de terreno de la celda con cada cambio de altura, creemos un método separado SetTerrainType
para configurar todos los tipos de terreno solo una vez. void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); cell.TerrainTypeIndex = cell.Elevation; } }
Llamaremos a este método después de crear sushi. public void GenerateMap (int x, int z) { … CreateLand(); SetTerrainType(); … }
Ahora no RaiseTerrain
puede lidiar con el tipo de alivio y centrarse en las alturas. Para hacer esto, necesita cambiar su lógica. Si la nueva altura de la celda actual es 1, entonces se ha vuelto más seca, por lo que la suma de las celdas ha disminuido, lo que puede conducir a la finalización del crecimiento del sitio. HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == 1 && --budget == 0) { break; }
Estratificación de las capas.Agregar agua
Indiquemos explícitamente qué celdas son agua o tierra, estableciendo el nivel de agua para todas las celdas en 1. Hagamos esto GenerateMap
antes de crear la tierra. public void GenerateMap (int x, int z) { cellCount = x * z; grid.CreateMap(x, z); if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } for (int i = 0; i < cellCount; i++) { grid.GetCell(i).WaterLevel = 1; } CreateLand(); … }
Ahora para la designación de capas de tierra podemos usar todo tipo de terreno. Todas las células submarinas seguirán siendo arena, al igual que las células terrestres más bajas. Esto se puede hacer restando el nivel del agua de la altura y usando el valor como índice del tipo de relieve. void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); if (!cell.IsUnderwater) { cell.TerrainTypeIndex = cell.Elevation - cell.WaterLevel; } } }
Tierra y agua.Elevar el nivel del agua
No estamos limitados a un nivel de agua. Hagámoslo personalizable usando un campo común con un intervalo de 1 a 5 y un valor predeterminado de 3. Use este nivel al inicializar las celdas. [Range(1, 5)] public int waterLevel = 3; … public void GenerateMap (int x, int z) { … for (int i = 0; i < cellCount; i++) { grid.GetCell(i).WaterLevel = waterLevel; } … }
Nivel de agua 3.Cuando el nivel de agua es 3, obtenemos menos tierra de lo que esperábamos. Esto se debe a que RaiseTerrain
todavía cree que el nivel del agua es 1. Vamos a arreglarlo. HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == waterLevel && --budget == 0) { break; }
Usar niveles de agua más altos conduce a eso. que las células no se convierten en tierra de inmediato. Cuando el nivel del agua es 2, la primera sección permanecerá bajo el agua. El fondo del océano ha subido, pero aún permanece bajo el agua. Un terreno se forma solo en la intersección de al menos dos secciones. Cuanto más alto sea el nivel del agua, más sitios deben cruzar para crear tierra. Por lo tanto, con el aumento del nivel del agua, la tierra se vuelve más caótica. Además, cuando se necesitan más parcelas, es más probable que se crucen en tierras ya existentes, por lo que las montañas serán más comunes y las tierras planas con menos frecuencia, como en el caso de usar parcelas más pequeñas.Los niveles de agua son de 2 a 5, el sushi siempre es del 50%.paquete de la unidadMovimiento vertical
Hasta ahora hemos elevado las parcelas un nivel a la vez, pero no tenemos que limitarnos a esto.Sitios altos
Aunque cada sección aumenta la altura de sus celdas en un nivel, pueden producirse recortes. Esto sucede cuando los bordes de dos secciones se tocan. Esto puede crear acantilados aislados, pero las líneas largas de acantilados serán raras. Podemos aumentar la frecuencia de su aparición aumentando la altura de la trama en más de un paso. Pero esto solo debe hacerse para una cierta proporción de sitios. Si todas las áreas se elevan, será muy difícil moverse a lo largo del terreno. Entonces, hagamos que este parámetro sea personalizable usando un campo de probabilidad con un valor predeterminado de 0.25. [Range(0f, 1f)] public float highRiseProbability = 0.25f;
La probabilidad de un fuerte aumento en las células.Aunque podemos usar cualquier aumento de altura para áreas altas, esto rápidamente se sale de control. La diferencia de altura 2 ya crea acantilados, por lo que es suficiente. Como puede omitir una altura igual al nivel del agua, necesitamos cambiar la forma en que determinamos si una celda se ha convertido en tierra. Si estaba por debajo del nivel del agua, y ahora está al mismo nivel o más alto, entonces creamos una nueva celda terrestre. int rise = Random.value < highRiseProbability ? 2 : 1; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; current.Elevation = originalElevation + rise; if ( originalElevation < waterLevel && current.Elevation >= waterLevel && --budget == 0 ) { break; } size += 1; … }
Las probabilidades de un fuerte aumento en la altura son 0.25, 0.50, 0.75 y 1.Bajar la tierra
La tierra no siempre sube, a veces cae. Cuando la tierra cae lo suficientemente baja, el agua la llena y se pierde. Hasta ahora no estamos haciendo esto. Como solo empujamos las áreas hacia arriba, la tierra generalmente parece un conjunto de áreas bastante redondas mezcladas entre sí. Si a veces bajamos el área, obtenemos formas más variadas.Gran mapa sin sushi hundido.Podemos controlar la frecuencia del hundimiento de la tierra utilizando otro campo de probabilidad. Dado que bajar puede destruir la tierra, la probabilidad de bajar siempre debe ser menor que la probabilidad de subir. De lo contrario, puede tomar mucho tiempo obtener el porcentaje correcto de tierra. Por lo tanto, usemos una probabilidad de disminución máxima de 0.4 con un valor predeterminado de 0.2. [Range(0f, 0.4f)] public float sinkProbability = 0.2f;
Probabilidad de bajar.Bajar el sitio es similar a subir, con algunas diferencias. Por lo tanto, duplicamos el método RaiseTerrain
y cambiamos su nombre a SinkTerrain
. En lugar de determinar la magnitud del aumento, necesitamos un valor de disminución que pueda usar la misma lógica. Al mismo tiempo, las comparaciones para verificar si hemos atravesado la superficie del agua deben ser cambiadas. Además, al bajar el relieve, no estamos limitados a la suma de las celdas. En cambio, cada celda de sushi perdida devuelve la cantidad gastada en ella, por lo que la aumentamos y seguimos trabajando. int SinkTerrain (int chunkSize, int budget) { … int sink = Random.value < highRiseProbability ? 2 : 1; int size = 0; while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; current.Elevation = originalElevation - sink; if ( originalElevation >= waterLevel && current.Elevation < waterLevel
Ahora, en cada iteración dentro, CreateLand
debemos bajar o subir la tierra, dependiendo de la probabilidad de bajar. void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); while (landBudget > 0) { int chunkSize = Random.Range(chunkSizeMin, chunkSizeMax - 1); if (Random.value < sinkProbability) { landBudget = SinkTerrain(chunkSize, landBudget); } else { landBudget = RaiseTerrain(chunkSize, landBudget); } } }
La probabilidad de caída es 0.1, 0.2, 0.3 y 0.4.Altura límite
En la etapa actual, podemos superponer potencialmente muchas secciones, a veces con varios aumentos de altura, algunos de los cuales pueden bajar y luego volver a subir. Al mismo tiempo, podemos crear alturas muy altas, y a veces muy bajas, especialmente cuando se necesita un alto porcentaje de tierra.Enormes alturas al 90% de tierra.Para limitar la altura, agreguemos un mínimo y un máximo personalizados. Un mínimo razonable estará entre −4 y 0, y un máximo aceptable puede estar en el rango de 6-10. Deje que los valores predeterminados sean −2 y 8. Al editar manualmente el mapa, estarán fuera del límite permitido, por lo que puede cambiar el control deslizante del editor de IU o dejarlo como está. [Range(-4, 0)] public int elevationMinimum = -2; [Range(6, 10)] public int elevationMaximum = 8;
Alturas mínimas y máximas.Ahora RaiseTerrain
debemos asegurarnos de que la altura no exceda el máximo permitido. Esto se puede hacer comprobando si las celdas actuales son demasiado altas. Si es así, los omitimos sin cambiar la altura y sin agregar sus vecinos. Esto conducirá al hecho de que las áreas de tierra evitarán áreas que han alcanzado una altura máxima y crecerán a su alrededor. HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; int newElevation = originalElevation + rise; if (newElevation > elevationMaximum) { continue; } current.Elevation = newElevation; if ( originalElevation < waterLevel && newElevation >= waterLevel && --budget == 0 ) { break; } size += 1;
Haga lo mismo en SinkTerrain
, pero para una altura mínima. HexCell current = searchFrontier.Dequeue(); int originalElevation = current.Elevation; int newElevation = current.Elevation - sink; if (newElevation < elevationMinimum) { continue; } current.Elevation = newElevation; if ( originalElevation >= waterLevel && newElevation < waterLevel ) { budget += 1; } size += 1;
Altura limitada con 90% de terreno.Preservación de altitud negativa
En este punto, el código de guardar y cargar no puede manejar alturas negativas porque almacenamos la altura como byte. Un número negativo se convierte cuando se guarda en un positivo grande. Por lo tanto, al guardar y cargar el mapa generado, pueden aparecer mapas muy altos en lugar de las celdas subacuáticas originales.Podemos agregar soporte para alturas negativas almacenándolo como un entero, no como un byte. Sin embargo, todavía no necesitamos soportar múltiples niveles de altura. Además, podemos cambiar el valor almacenado agregando 127. Esto nos permitirá almacenar correctamente las alturas en el rango −127–128 dentro de un byte. Cambiar en HexCell.Save
consecuencia. public void Save (BinaryWriter writer) { writer.Write((byte)terrainTypeIndex); writer.Write((byte)(elevation + 127)); … }
Como cambiamos la forma en que guardamos los datos del mapa, lo aumentamos SaveLoadMenu.mapFileVersion
a 4. const int mapFileVersion = 4;
Y finalmente, cámbielo HexCell.Load
para que reste 127 de las alturas cargadas de los archivos de la versión 4. public void Load (BinaryReader reader, int header) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); if (header >= 4) { elevation -= 127; } … }
paquete de la unidadRecreando el mismo mapa
Ahora podemos crear una amplia variedad de mapas. Al generar cada nuevo resultado será aleatorio. Podemos controlar usando las opciones de configuración solo las características de la tarjeta, pero no la forma más precisa. Pero a veces necesitamos recrear exactamente el mismo mapa nuevamente. Por ejemplo, para compartir un hermoso mapa con un amigo, o comenzar de nuevo después de editarlo manualmente. También es útil en el proceso de desarrollo del juego, así que agreguemos esta función.Usando semillas
Para que el proceso de generación de mapas sea impredecible, utilizamos Random.Range
y Random.value
. Para volver a obtener la misma secuencia de números pseudoaleatoria, debe usar el mismo valor semilla. Ya hemos tomado un enfoque similar antes, en HexMetrics.InitializeHashGrid
. Primero guarda el estado actual del generador de números inicializado con un valor de inicialización específico y luego restaura su estado original. Podemos usar el mismo enfoque para HexMapGenerator.GenerateMap
. Podemos recordar nuevamente el estado anterior y restaurarlo después de la finalización, para no interferir con nada más que use Random
. public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; … Random.state = originalRandomState; }
A continuación, tenemos que poner a disposición la semilla utilizada para generar la última carta. Esto se hace usando un campo entero común. public int seed;
Mostrar semilla.Ahora necesitamos el valor semilla para inicializar Random
. Para crear cartas aleatorias necesitas usar una semilla aleatoria. El enfoque más simple es usar un valor semilla arbitrario para generar Random.Range
. Para que no afecte el estado aleatorio inicial, debemos hacer esto después de guardarlo. public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; seed = Random.Range(0, int.MaxValue); Random.InitState(seed); … }
Dado que después de la finalización restauramos un estado aleatorio, si generamos inmediatamente otra carta, como resultado obtenemos el mismo valor inicial. Además, no sabemos cómo se inicializó el estado aleatorio inicial. Por lo tanto, aunque puede servir como un punto de partida arbitrario, necesitamos algo más para aleatorizarlo con cada llamada.Hay varias formas de inicializar generadores de números aleatorios. En este caso, simplemente puede combinar varios valores arbitrarios que varían en un amplio rango, es decir, la probabilidad de volver a generar la misma tarjeta será baja. Por ejemplo, usamos los 32 bits más bajos del tiempo del sistema, expresados en ciclos, más el tiempo de ejecución actual de la aplicación. Combine estos valores utilizando la operación OR exclusiva bit a bit para que el resultado no sea muy grande. seed = Random.Range(0, int.MaxValue); seed ^= (int)System.DateTime.Now.Ticks; seed ^= (int)Time.unscaledTime; Random.InitState(seed);
El número resultante puede ser negativo, lo que para una semilla de valor público no se ve muy bien. Podemos hacerlo estrictamente positivo mediante el uso de enmascaramiento bit a bit con un valor entero máximo que restablecerá el bit de signo. seed ^= (int)Time.unscaledTime; seed &= int.MaxValue; Random.InitState(seed);
Semilla Reutilizable
Todavía generamos cartas al azar, pero ahora podemos ver qué valor inicial se usó para cada una de ellas. Para recrear el mismo mapa nuevamente, debemos ordenarle al generador que use el mismo valor semilla nuevamente, en lugar de crear uno nuevo. Haremos esto agregando un interruptor usando un campo booleano. public bool useFixedSeed;
Opción de usar una semilla constante.Si se selecciona una semilla constante, simplemente omitimos generar la nueva semilla GenerateMap
. Si no cambiamos manualmente el campo semilla, el resultado será el mismo mapa nuevamente. Random.State originalRandomState = Random.state; if (!useFixedSeed) { seed = Random.Range(0, int.MaxValue); seed ^= (int)System.DateTime.Now.Ticks; seed ^= (int)Time.time; seed &= int.MaxValue; } Random.InitState(seed);
Ahora podemos copiar el valor semilla del mapa que nos gusta y guardarlo en algún lugar, para generarlo nuevamente en el futuro. No olvide que obtendremos la misma tarjeta solo si usamos exactamente los mismos parámetros del generador, es decir, el mismo tamaño de tarjeta, así como todas las demás opciones de configuración. Incluso un pequeño cambio en estas probabilidades puede crear un mapa completamente diferente. Por lo tanto, además de la semilla, debemos recordar todas las configuraciones.Tarjetas grandes con valores semilla 0 y 929396788, parámetros estándar.paquete de la unidad