第1-3部分:网格,颜色和像元高度第4-7部分:颠簸,河流和道路第8-11部分:水,地貌和城墙第12-15部分:保存和加载,纹理,距离第16-19部分:找到道路,队员,动画第20-23部分:战争迷雾,地图研究,程序生成第24-27部分:水循环,侵蚀,生物群落,圆柱图第20部分:战争迷雾
- 将单元格数据保存在纹理中。
- 更改浮雕类型而无需三角剖分。
- 我们跟踪可见度。
- 使所有看不见的东西变暗。
在这一部分中,我们将向地图添加战争迷雾效果。
现在,该系列将在Unity 2017.1.0上创建。
现在我们看到可以看到,不能看到。着色器中的单元数据
许多策略游戏都使用战争迷雾概念。 这意味着玩家的视力受到限制。 他只能看到靠近他的单位或控制区域的东西。 尽管我们可以看到救济,但我们不知道那里发生了什么。 通常,看不见的地形会变暗。 为了实现这一点,我们需要跟踪单元的可见性并进行相应渲染。
更改隐藏单元格外观的最简单方法是向网格数据添加可见性度量。 但是,与此同时,我们将不得不开始新的浮雕三角剖分,以改变可见度。 这是一个错误的决定,因为在游戏过程中可见性一直在变化。
通常使用在半透明表面的地形上渲染的技术,该技术部分掩盖了玩家看不见的单元。 该方法适用于相对平坦的地形,并具有有限的视角。 但是,由于我们的地形可以包含高度不同的高度和可以从不同角度查看的对象,因此,我们需要一个高度详细的网格,以匹配地形的形状。 该方法将比上述最简单的方法昂贵。
另一种方法是在与浮雕网格物体分开渲染时将单元的数据传输到着色器。 这将使我们仅执行一次三角剖分。 可以使用纹理传输单元格数据。 更改纹理比对地形进行三角剖分要简单得多。 此外,执行多个其他纹理样本比渲染单个半透明层要快。
使用着色器数组怎么办?您还可以使用向量数组将单元格数据传输到着色器。 但是,着色器阵列有大小限制,以数千个字节为单位,并且纹理可以包含数百万个像素。 为了支持大型地图,我们将使用纹理。
细胞数据管理
我们需要一种方法来控制包含单元格数据的纹理。 让我们创建一个新的
HexCellShaderData
组件来执行此操作。
using UnityEngine; public class HexCellShaderData : MonoBehaviour { Texture2D cellTexture; }
在创建或加载新地图时,我们需要创建具有正确尺寸的新纹理。 因此,我们添加了一个初始化方法来为其创建纹理。 我们使用没有Mip纹理和线性色彩空间的RGBA纹理。 我们不需要混合单元格数据,因此我们使用点过滤。 此外,数据不应折叠。 纹理中的每个像素将包含来自一个单元的数据。
public void Initialize (int x, int z) { cellTexture = new Texture2D( x, z, TextureFormat.RGBA32, false, true ); cellTexture.filterMode = FilterMode.Point; cellTexture.wrapMode = TextureWrapMode.Clamp; }
纹理大小是否应与地图大小匹配?不,它只需要有足够的像素来容纳所有单元。 与地图的大小完全匹配时,很可能会创建大小不是2的幂(非2的幂,NPOT)的纹理,并且这种纹理格式不是最有效的。 尽管我们可以将代码配置为使用2的幂的纹理进行处理,但这是次要的优化,这使对单元数据的访问变得复杂。
实际上,我们不必每次创建新地图时都创建新纹理。 如果纹理已经存在,则足以调整其大小。 我们甚至不需要检查我们是否已经具有合适的尺寸,因为
Texture2D.Resize
足够聪明地为我们做到这一点。
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; } }
我们使用颜色缓冲区并一次应用所有单元的数据,而不是一次应用一个像素的单元数据。 为此,我们将使用
Color32
数组。 如有必要,我们将在
Initialize
的末尾创建一个新的数组实例。 如果我们已经有一个正确大小的数组。 然后我们清除其内容。
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); } } }
什么是color32?标准的未压缩RGBA纹理包含四个字节的像素。 四个颜色通道的每个接收一个字节,即它们具有256个可能的值。 使用Unity Color
结构时,其在区间0–1中的浮点分量将转换为在区间0–255中的字节。 采样时,GPU执行逆变换。
Color32
结构直接与字节一起使用,因此它们占用的空间更少,并且不需要转换,从而提高了使用效率。 由于我们存储的是单元格数据而不是颜色,因此直接使用原始纹理数据而不是使用Color
更加合乎逻辑。
HexGrid
应该处理着色器中这些单元的创建和初始化。 因此,我们将向其添加一个
cellShaderData
字段,并在
Awake
创建一个组件。
HexCellShaderData cellShaderData; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); CreateMap(cellCountX, cellCountZ); }
创建新地图时,还应
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; }
编辑单元格数据
到目前为止,在更改单元格的属性时,有必要更新一个或几个片段,但是现在可能有必要更新单元格的数据。 这意味着单元必须具有指向着色器中单元数据的链接。 为此,向
HexCell
添加一个属性。
public HexCellShaderData ShaderData { get; set; }
在
HexGrid.CreateCell
我们
HexGrid.CreateCell
为该属性分配一个着色器数据组件。
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; … }
现在我们可以使单元更新其着色器数据。 虽然我们不跟踪可见性,但可以将着色器数据用于其他用途。 单元的浮雕类型确定用于渲染它的纹理。 它不会影响像元的几何形状,因此我们可以将高程类型索引存储在像元数据中,而不是存储在网格数据中。 这将使我们在更改单元格拓扑的类型时无需进行三角剖分。
向
RefreshTerrain
添加
HexCellShaderData
方法可简化特定单元格的此任务。 让我们暂时将此方法留空。
public void RefreshTerrain (HexCell cell) { }
更改
HexCell.TerrainTypeIndex
以便它
HexCell.TerrainTypeIndex
此方法,并且不按顺序更新片段。
public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value;
收到单元格的地形类型后,我们还将在
HexCell.Load
中将其
HexCell.Load
。
public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); RefreshPosition(); … }
细胞指数
要更改这些单元格,我们需要知道单元格的索引。 最简单的方法是将
Index
属性添加到
HexCell
。 它将指示贴图的单元格列表中该单元格的索引,该索引与其在着色器中给定单元格中的索引相对应。
public int Index { get; set; }
该索引已经在
HexGrid.CreateCell
,因此只需将其分配给创建的单元格即可。
void CreateCell (int x, int z, int i) { … cell.coordinates = HexCoordinates.FromOffsetCoordinates(x, z); cell.Index = i; cell.ShaderData = cellShaderData; … }
现在
HexCellShaderData.RefreshTerrain
可以使用此索引来指定单元格数据。 通过简单地将类型转换为字节,让我们将高程类型索引保存在其像素的alpha分量中。 这将支持多达256种地形,对我们而言已经足够。
public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; }
要将数据应用于纹理并将其传递给GPU,我们需要调用
Texture2D.SetPixels32
,然后调用
Texture2D.Apply
。 与片段的情况一样,我们将这些操作推迟到
LateUpdate
以使它们每帧最多可以执行一次,而与更改的单元格数量无关。
public void RefreshTerrain (HexCell cell) { cellTextureData[cell.Index].a = (byte)cell.TerrainTypeIndex; enabled = true; } void LateUpdate () { cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = false; }
为确保在创建新映射后将更新数据,请在初始化后启用组件。
public void Initialize (int x, int z) { … enabled = true; }
三角化细胞指数
由于我们现在将高程类型索引存储在这些单元格中,因此不再需要在三角测量过程中包括它们。 但是,为了使用单元数据,着色器必须知道要使用哪些索引。 因此,您需要将单元格索引存储在网格数据中,以替换高程类型索引。 另外,使用这些像元时,我们仍然需要网格的颜色通道来混合像元。
我们
useTerrainTypes
HexMesh
过时的公共字段
useColors
和
useTerrainTypes
。 用一个
useCellData
字段替换它们。
我们将
terrainTypes
列表的重命名重构为
cellIndices
。 让我们也将
colors
重构为
cellWeights
这个名称会更好。
更改
Clear
以便在使用这些单元格时将两个列表放在一起,而不是分开。
public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useCellData) { cellWeights = ListPool<Color>.Get(); cellIndices = ListPool<Vector3>.Get(); }
在
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
和
AddTriangleTerrainTypes
。 将它们替换为适当的
AddTriangleCellData
方法,该方法一次添加索引和权重。
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); }
在适当的
AddQuad
方法中执行相同的
AddQuad
。
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重构
在此阶段,我们在
HexGridChunk
中遇到了许多需要
HexGridChunk
的编译器错误。 但是首先,为了保持一致,我们将静态颜色重构为重物。
static Color weights1 = new Color(1f, 0f, 0f); static Color weights2 = new Color(0f, 1f, 0f); static Color weights3 = new Color(0f, 0f, 1f);
让我们从修复
TriangulateEdgeFan
开始。 他曾经需要一个类型,但是现在他需要一个单元格索引。
AddTriangleColor
相应的
AddTriangleCellData
代码
AddTriangleColor
和
AddTriangleCellData
代码。
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);
在很多地方都调用此方法。 让我们遍历它们,确保将单元格的索引转移到那里,而不是地形的类型。
TriangulateEdgeFan(center, e, cell.Index);
接下来是
TriangulateEdgeStrip
。 这里的一切都比较复杂,但是我们使用相同的方法。 还将参数名称
c1
和
c2
重构为
w1
和
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);
更改对此方法的调用,以便将单元格索引传递给他们。 我们还保持变量名称的一致性。
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); }
现在我们继续角度方法。 这些更改很简单,但是需要使用大量代码进行。 首先是
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);
来到
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);
然后在
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 );
与
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 );
前两种方法使用
TriangulateBoundaryTriangle
,这也需要更新。
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);
最后需要更改的方法是
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);
为了使一切正常工作,我们需要指出,我们将使用单元数据作为预制碎片浮雕的子元素。
救济使用单元格数据。在此阶段,网格包含单元索引而不是高程类型索引。 由于高程着色器仍将它们解释为高程索引,因此我们将看到第一个像元是使用第一个纹理渲染的,依此类推,直到到达最后一个浮雕纹理为止。
使用像元索引作为高程纹理索引。我无法使重构代码正常工作。 我做错了什么?一次,我们更改了大量的三角剖分代码,因此很可能出现错误或疏忽。 如果找不到错误,请尝试从本节下载程序包并提取适当的文件。 您可以将它们导入一个单独的项目中,并与自己的代码进行比较。
将单元格数据传输到着色器
要使用这些单元,地形着色器必须可以访问它们。 可以通过shader属性实现。 这将需要
HexCellShaderData
设置浮雕的材质属性。 或者,我们可以使所有着色器全局可见这些单元的纹理。 这很方便,因为我们需要多个着色器,所以我们将使用此方法。
创建单元纹理后,调用静态
Shader.SetGlobalTexture
方法以使其全局可见为
_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); } … }
使用shader属性时,Unity通过
textureName_TexelSize变量使纹理大小可用于着色器。 这是一个四分量矢量化器,包含与宽度和高度以及宽度和高度本身相反的值。 但是在设置全局纹理时,不会执行此操作。 因此,在创建或调整纹理大小之后,我们将使用
Shader.SetGlobalVector
自己进行处理。
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) );
着色器数据访问
在名为
HexCellData的材质文件夹中创建一个新的着色器包含文件。 在其中,我们定义变量以获取有关这些单元的纹理和大小的信息。 我们还创建了一个函数来获取给定顶点网格数据的像元数据。
sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 GetCellData (appdata_full v) { }
新的包含文件。像地形类型
v.texcoord2
,单元
v.texcoord2
存储在
v.texcoord2
。 让我们从第一个索引
v.texcoord2.x
。 不幸的是,我们不能直接使用索引来采样这些单元的纹理。 我们将不得不将其转换为UV坐标。
创建U坐标的第一步是将单元格索引除以纹理的宽度。 我们可以通过将其乘以
_HexCellData_TexelSize.x
。
float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; }
结果将是ZU形式的数字,其中Z是行索引,U是U单元的坐标。我们可以通过将数字四舍五入然后从数字中减去来获得U坐标来提取字符串。 float4 GetCellData (appdata_full v) { float2 uv; uv.x = v.texcoord2.x * _HexCellData_TexelSize.x; float row = floor(uv.x); uv.x -= row; }
V坐标将线除以纹理的高度。 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; }
由于我们正在采样纹理,因此需要在像素中心而不是边缘使用坐标。这样,我们保证可以对正确的像素进行采样。因此,除以纹理大小后,加½。 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; }
这为顶点数据中存储的第一个单元格的索引提供了正确的UV坐标。但最重要的是,我们最多可以有三个不同的索引。因此,我们将使其GetCellData
适用于任何索引。向其添加一个整数参数index
,我们将使用该参数访问带有单元格索引的向量分量。 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; }
现在我们已经拥有了这些单元格的所有必要坐标,我们可以进行采样_HexCellData
。由于我们正在顶点程序中对纹理进行采样,因此我们需要显式告诉着色器要使用哪个mip纹理。这可以使用tex2Dlod
需要四个纹理坐标的函数来完成。由于这些单元格没有mip纹理,因此我们将零值分配给额外的坐标。 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)); }
第四个数据组件包含一个高程类型索引,我们直接将其存储为字节。但是,GPU会自动将其转换为0-1范围内的浮点值。要将其转换回正确的值,请乘以255。之后,您可以返回数据。 float4 data = tex2Dlod(_HexCellData, float4(uv, 0, 0)); data.w *= 255; return data;
要使用此功能,请在Terrain着色器中启用HexCellData。由于我将此着色器放置在“ 材质/地形”中,因此需要使用相对路径../HexCellData.cginc。 #include "../HexCellData.cginc" UNITY_DECLARE_TEX2DARRAY(_MainTex)
在顶点程序中,我们获取存储在顶点数据中的所有三个像元索引的像元数据。然后分配data.terrain
其海拔指数。 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; }
此时,地图再次开始显示正确的地形。最大的区别在于,仅编辑地形类型将不再导致新的三角剖分。如果在编辑期间更改了其他任何单元格数据,则将照常执行三角剖分。统一包装能见度
创建了这些单元的基础之后,我们可以继续支持可见性。为此,我们使用着色器,单元格本身以及确定可见性的对象。请注意,三角剖分过程对此一无所知。着色器
让我们从告诉地形着色器可见性开始。它将从顶点程序接收可见性数据,并使用该结构将其传递给片段程序Input
。由于我们传递了三个单独的海拔指数,因此我们还将传递三个可见性值。 struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float3 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.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; }
可见性0表示该单元格当前不可见。如果可见,它将具有可见性1的值。因此,我们可以通过将结果乘以GetTerrainColor
相应的可见性矢量来使地形变暗。因此,我们分别调整每个混合单元的浮雕颜色。 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]); }
细胞变成黑色。难道我们不能在顶点程序中结合可见性吗?这也是可能的,在这种情况下,片段程序只需要传输一个可见性指示器。当为参与混合的每个小区传输指标时,三个地形样本将被单独混合。结果,可见细胞将对混合区域做出更大贡献。使用一个指标时,必须首先执行混合,然后应用最终的内插可见性。两种方法都可以使用,但是在视觉上它们是不同的。
完全黑暗是暂时看不见细胞的半身像。为了我们仍然可以看到缓解,我们需要增加用于隐藏单元格的指标。让我们从0–1移至¼– 1,这可以使用lerp
顶点程序末尾的函数来完成。 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); }
阴影细胞。单元可见性跟踪
为了使可见性起作用,单元必须跟踪其可见性。但是细胞如何确定它是否可见?我们可以通过跟踪看到它的实体数量来做到这一点。当某人开始看到一个单元格时,他必须报告该单元格。当某人停止看到牢房时,还必须通知她。该单元只是跟踪观察者的数量,无论这些实体是什么。如果单元格的可见性值至少为1,则它是可见的,否则它是不可见的。为了实现此行为,我们向HexCell
变量添加了两个方法和一个属性。 public bool IsVisible { get { return visibility > 0; } } … int visibility; … public void IncreaseVisibility () { visibility += 1; } public void DecreaseVisibility () { visibility -= 1; }
接下来,添加到HexCellShaderData
method RefreshVisibility
,它与的功能相同RefreshTerrain
,只是为了可见。将数据保存在数据单元的组件R中。由于我们使用的字节将被转换为值0-1,因此我们用来表示可见性(byte)255
。 public void RefreshVisibility (HexCell cell) { cellTextureData[cell.Index].r = cell.IsVisible ? (byte)255 : (byte)0; enabled = true; }
我们将通过增加和减少可见性(在0到1之间更改值)来调用此方法。 public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { ShaderData.RefreshVisibility(this); } } public void DecreaseVisibility () { visibility -= 1; if (visibility == 0) { ShaderData.RefreshVisibility(this); } }
创建班组可见度
让我们使它们能够看到它们所占据的单元。这可以通过IncreaseVisibility
在任务执行期间调用单元的新位置来完成HexUnit.Location
。我们还要求使用旧位置(如果存在)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; } }
单位可以看到他们在哪里。最后,我们使用可见性!单位添加到地图后,其单元格将可见。此外,它们的范围在移动到新位置时会被传送。但是,从地图上删除单位时,它们的作用域仍然有效。为了解决这个问题,我们将在销毁单位时降低其位置的可见性。 public void Die () { if (location) { location.DecreaseVisibility(); } location.Unit = null; Destroy(gameObject); }
能见度范围
到目前为止,我们仅看到分离所在的单元,这限制了可能性。至少我们需要看到相邻的单元格。在一般情况下,单位可以看到一定距离内的所有单元,具体取决于单位。让我们添加到该HexGrid
方法中,以考虑到范围,找到一个单元格中所有可见的单元格。我们可以通过复制和更改来创建此方法Search
。更改其参数,并使其返回可以使用列表池的单元格列表。在每次迭代时,当前单元格都会添加到列表中。最终单元不再存在,因此搜索到此为止将永远不会结束。我们还摆脱了移动的逻辑和移动的成本。制作属性PathFrom
他们不再被问到是因为我们不需要它们,我们也不想干扰沿着网格的路径。在每一步中,距离仅增加1。如果超出范围,则跳过该单元格。而且,我们不需要搜索启发式算法,因此可以将其初始化为0值。也就是说,实质上,我们返回了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);
我们不能使用更简单的算法来查找范围内的所有像元吗?, , .
还要添加HexGrid
方法IncreaseVisibility
和DecreaseVisibility
。他们获取单元格和范围,获取相应单元格的列表,并增加/减少其可见性。完成后,他们应将列表返回到其池中。 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); }
要使用这些方法,HexUnit
需要访问网格,因此请向其添加一个属性Grid
。 public HexGrid Grid { get; set; }
将小队添加到网格时,它将为该属性分配一个网格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; }
首先,三个单元格的可见范围就足够了。为此,我们添加了HexUnit
常量,将来它总是可以变成变量。然后,我们将使小队针对网格IncreaseVisibility
和调用方法,并DecreaseVisibility
传递其可见性范围,而不仅仅是到这个地方。 const int visionRange = 3; … public HexCell Location { get { return location; } set { if (location) {
能见度范围可以重叠的单位。移动时能见度
此刻,移动命令后的班组可见区域立即传送到终点。如果该单元及其可见区域一起移动,则效果会更好。第一步是我们将不再设置属性Location
c HexUnit.Travel
。相反,我们将直接更改字段location
,避免使用属性代码。因此,我们将手动清除旧位置并配置新位置。可见性将保持不变。 public void Travel (List<HexCell> path) {
在协程内部,TravelPath
只有完成后,我们才会降低第一个单元格的可见性LookAt
。之后,在移至新单元之前,我们将增加该单元的可见性。完成此操作后,我们再次降低了可见度。最后,增加最后一个单元格的可见性。 IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position;
移动中的可见性。所有这一切都有效,除了在分队移动时发出新订单时。这导致隐形传态,这也应适用于能见度。为了实现这一点,我们需要在移动时跟踪小队的当前位置。 HexCell location, currentTravelLocation;
每当我们在移动中命中一个新单元时,我们都会更新此位置,直到班长到达最后一个单元为止。然后必须将其重置。 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; … }
现在,在完成上交之后,TravelPath
我们可以检查路径的旧中间位置是否已知。如果是,那么您需要降低此单元格中的可见性,而不是路径的开头。 IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); Grid.DecreaseVisibility( currentTravelLocation ? currentTravelLocation : pathToTravel[0], visionRange ); … }
我们还需要校正小队移动期间重新编译后的可见性。如果中间位置仍然是已知的,则减小其中的可见性并增加端点处的可见性,然后重置中间位置。 void OnEnable () { if (location) { transform.localPosition = location.Position; if (currentTravelLocation) { Grid.IncreaseVisibility(location, visionRange); Grid.DecreaseVisibility(currentTravelLocation, visionRange); currentTravelLocation = null; } } }
统一包装道路和水的可见性
尽管浮雕颜色的更改基于可见性,但这不会影响道路和水。对于看不见的细胞,它们看起来太亮了。若要将可见性应用于道路和水,我们需要在其网格数据中添加像元索引和混合权重。因此,我们将检查预制碎片的河流,道路,水,水岸和河口使用单元数据的子级。道路
我们将从道路开始。该方法HexGridChunk.TriangulateRoadEdge
用于在单元中心创建道路的一小部分,因此它需要一个单元索引。向其添加参数并生成三角形的单元格数据。 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); }
创建道路的另一种简单方法是TriangulateRoadSegment
。它在单元内部和单元之间使用,因此应与两个不同的索引一起使用。为此,使用索引向量参数很方便。由于路段可以是壁架的一部分,因此权重也必须通过参数传递。 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); }
现在,我们继续到TriangulateRoad
,它会在单元内部创建道路。它还需要一个索引参数。他将此数据传递给他调用的道路方法,并将其添加到他创建的三角形中。 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); } }
它仍然添加所需的方法参数TriangulateRoad
,TriangulateRoadEdge
并TriangulateRoadSegment
纠正所有的编译器错误。 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 ); } }
现在网格数据正确,我们将继续使用Road shader 。它需要一个顶点程序,并且必须包含HexCellData。 #pragma surface surf Standard fullforwardshadows decal:blend vertex:vert #pragma target 3.0 #include "HexCellData.cginc"
由于我们没有混合几种材料,因此将一个可见性指标传递给片段程序就足够了。 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); 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) { float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color * ((noise.y * 0.75 + 0.25) * IN.visibility); … }
有能见度的道路。开水
可见性似乎已经影响了水,但这只是浸入水中的地形表面。让我们从对开放水域应用可见性开始。为此,我们需要改变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 ); } } }
我们还需要向靠近海岸的三角形的扇形添加单元数据。 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); … }
需要以与“ 道路”着色器相同的方式更改“ 水”着色器,但是它需要合并的可视性不是两个,而是三个单元。 #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; … }
开阔水面能见度。海岸和河口
为了支持海岸,我们需要再次改变HexGridChunk.TriangulateWaterShore
。我们已经创建了索引向量,但是对于开放水域,我们仅使用了一个细胞索引。海岸也需要邻居索引,因此请更改代码。 Vector3 indices;
将像元数据添加到海岸的四边形和三角形。我们还会在通话中传递索引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 ); }
TriangulateEstuary
向海岸和河口添加必要的参数并照顾这些细胞。不要忘记,嘴是梯形的,侧面有两个三角形的海岸。我们确保权重以正确的顺序传输。 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); … }
在WaterShore着色器中,您需要进行与Water着色器相同的更改,同时混合三个单元的可见性。 #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; … }
谢德河口两个细胞混合的知名度,以及着色路概述。他已经有了一个顶点程序,因为我们需要他来传输河流的UV坐标。 #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; … }
海岸和河口具有可见性。河流
最后可以使用的水域是河流。将HexGridChunk.TriangulateRiverQuad
索引向量添加到参数并将其添加到网格,以便它可以保持两个像元的可见性。 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
创建在单元格中心具有四边形和三角形的河流端点。为此添加必要的单元格数据。 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); } }
我们已经有这些单元格索引TriangulateWithRiver
,因此我们只在调用时传递它们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 ); } }
我们还为倒入深水的瀑布添加了索引支持。 void TriangulateWaterfallInWater ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float waterY, Vector3 indices ) { … rivers.AddQuadCellData(indices, weights1, weights2); }
最后,对其进行更改,TriangulateConnection
以便将必要的索引传递给河流和瀑布方法。 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 ); } } … }
River着色器需要进行与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; … }
有能见度的河流。统一包装对象和可见性
现在,可见性适用于整个过程生成的地形,但是到目前为止,它不影响地形特征。建筑物,农场和树木是由预制件而非程序几何创建的,因此我们无法添加单元索引并将权重与其顶点混合。由于这些对象仅属于一个单元格,因此我们需要确定它们位于哪个单元格中。如果可以做到这一点,那么我们将可以访问相应单元格的数据并应用可见性。我们已经可以将世界的XZ位置转换为单元格索引。此转换用于编辑地形和管理小队。但是,相应的代码并不简单。它使用整数运算,并且需要逻辑来处理边。这对于着色器来说是不切实际的,因此我们可以烘焙纹理中的大部分逻辑并使用它。我们已经在使用具有六边形图案的纹理来将网格投影到地形上。此纹理定义了2×2的单元格区域。因此,我们可以轻松计算出我们在哪个区域。之后,您可以为该区域中的单元格应用包含X和Z偏移量的纹理,并使用此数据来计算我们所在的单元格。这是类似的纹理。 X偏移量存储在其红色通道中,Z偏移量存储在绿色通道中。由于它覆盖2×2单元的面积,因此我们需要从0和2开始的偏移量。此类数据无法存储在颜色通道中,因此偏移量减少了一半。我们不需要透明的单元格边缘,因此小的纹理就足够了。网格坐标的纹理。向项目添加纹理。设置其“ 包裹模式”为“ 重复”,就像其他网格纹理一样。我们不需要任何混合,因此对于混合模式,我们将选择Point。同时关闭压缩,以使数据不失真。关闭sRGB模式,以便在线性模式下渲染时,不执行颜色空间转换。最后,我们不需要MIP纹理。纹理导入选项。具有可见性的对象着色器
创建一个新的特征着色器以向对象添加可见性支持。这是带有顶点程序的简单表面着色器。向其中添加HexCellData并将可见性指示器传递给片段程序,然后像往常一样将其视为彩色。区别在于我们无法使用GetCellData
它,因为所需的网格数据不存在。相反,我们在世界上占有一席之地。但是,暂时将可见度设置为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" }
更改对象的所有材质,以使它们使用新的着色器并为其分配网格坐标的纹理。城市与网格纹理。访问单元格数据
为了在顶点程序中采样网格坐标的纹理,我们再次需要tex2Dlod
一个四分量纹理坐标向量。前两个坐标是XZ世界的位置。如前所述,其他两个等于零。 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; }
与在Terrain着色器中一样,我们拉伸UV坐标,以使纹理具有与六边形网格相对应的正确纵横比。 float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0);
通过取四舍五入的UV坐标值,可以找出2×2单元格中的哪个部分。这构成了细胞坐标的基础。 float4 gridUV = float4(pos.xz, 0, 0); gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); float2 cellDataCoordinates = floor(gridUV.xy);
为了找到我们所在的单元的坐标,我们添加了存储在纹理中的位移。 float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg;
由于网格的一部分大小为2×2,并且偏移量减半,因此我们需要将结果加倍以获得最终坐标。 float2 cellDataCoordinates = floor(gridUV.xy) + tex2Dlod(_GridCoordinates, gridUV).rg; cellDataCoordinates *= 2;
现在我们有了单元格的XZ坐标,我们需要将其转换为这些单元的UV坐标。这可以通过简单地移动到像素的中心,然后将它们划分为纹理大小来完成。因此,我们为此添加一个功能到HexCellData包含文件中,该文件也将处理采样。 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)); }
现在,我们可以在顶点着色器程序使用的功能。 cellDataCoordinates *= 2; data.visibility = GetCellData(cellDataCoordinates).x; data.visibility = lerp(0.25, 1, data.visibility);
具有可见性的对象。最后,可见性会影响整个地图,但始终可见的单位除外。由于我们确定每个顶点的对象的可见性,因此对于穿过单元格边界的对象,将关闭其关闭的单元格的可见性。但是物体是如此之小,以至于即使考虑到位置的变形,它们也始终保留在其内部。但是,有些可能是另一个单元中顶点的一部分。因此,我们的方法便宜但不完善。这在墙壁的情况下最为明显,墙壁的可见性在相邻单元的可见性之间变化。能见度不断变化的墙。由于墙段是按程序生成的,因此我们可以将单元格数据添加到其网格中,并使用用于浮雕的方法。不幸的是,这些塔是预制的,因此我们仍然会有不一致之处。概括而言,对于我们使用的简单几何图形,现有方法看起来足够好。将来,我们将考虑更详细的模型和墙,因此,我们将改进混合其可见性的方法。统一包装第21部分:地图研究
- 我们在编辑过程中显示所有内容。
- 我们跟踪调查的细胞。
- 我们隐藏了仍然未知的东西。
- 我们强迫部队避开未开发的区域。
在上一部分中,我们添加了战争迷雾,现在将对其进行细化以实施地图研究。我们准备探索世界。在编辑模式下显示整个地图
该研究的意义在于,直到看不见细胞之前,都被认为是未知的,因此是不可见的。它们不应被遮盖,但根本不显示。因此,在添加研究支持之前,我们将在编辑模式下启用可见性。可见性切换
我们可以使用关键字控制着色器是否使用可见性,就像在网格上进行覆盖一样。让我们使用HEX_MAP_EDIT_MODE关键字来指示编辑模式的状态。由于一些着色器应该知道此关键字,因此我们将使用静态方法Shader.EnableKeyWord
和进行全局定义Shader.DisableKeyword
。HexGameUI.SetEditMode
更改编辑模式时,我们将调用适当的方法。 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"); } }
编辑模式着色器
当HEX_MAP_EDIT_MODE确定着色器会忽略外观。这归结为一个事实,即单元格可见性将始终被视为等于1。让我们添加一个函数,以根据HexCellData include-file开头的关键字来过滤单元格的数据。 sampler2D _HexCellData; float4 _HexCellData_TexelSize; float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.x = 1; #endif return data; }
GetCellData
在返回它之前,我们将这两个函数的结果传递给该函数。 float4 GetCellData (appdata_full v, int index) { … return FilterCellData(data); } float4 GetCellData (float2 cellDataCoordinates) { … return FilterCellData(tex2Dlod(_HexCellData, float4(uv, 0, 0))); }
为了使一切正常工作,所有的着色器都必须接收multi_compile指令才能创建选项,以防定义了HEX_MAP_EDIT_MODE关键字。在目标指令和第一个include指令之间,将适当的行添加到着色器Estuary,Feature,River,Road,Terrain,Water和Water Shore。 #pragma multi_compile _ HEX_MAP_EDIT_MODE
现在,当切换到地图编辑模式时,战争迷雾将消失。统一包装细胞研究
默认情况下,应将单元格视为未开发单元。当小队看到他们时,他们就会被探索。此后,如果分队可以看到他们,他们将继续接受调查。追踪学习状态
为了增加对监视研究状态的支持,我们在HexCell
常规属性中添加IsExplored
。 public bool IsExplored { get; set; }
研究的状态由细胞本身决定。因此,应仅设置此属性HexCell
。要添加此限制,我们将设置器设置为私有。 public bool IsExplored { get; private set; }
单元格的可见性首次大于零时,便开始考虑对该单元格进行调查,因此IsExplored
应分配一个值true
。实际上,当可见性增加到1时,仅将单元标记为已检查就足够了。这必须在call之前完成RefreshVisibility
。 public void IncreaseVisibility () { visibility += 1; if (visibility == 1) { IsExplored = true; ShaderData.RefreshVisibility(this); } }
将研究状态转移到着色器
与细胞可见性一样,我们通过着色器数据将其研究状态转移到着色器。最后,这只是可见性的另一种类型。HexCellShaderData.RefreshVisibility
将可见性状态存储在数据通道R中。让我们将研究状态保留在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; }
黑色未开发的浮雕
现在,我们可以使用着色器来可视化细胞研究的状态。为了确保一切正常,我们将未开发的地形设为黑色。但是首先,要使编辑模式起作用,请对其进行更改,FilterCellData
以过滤出研究数据。 float4 FilterCellData (float4 data) { #if defined(HEX_MAP_EDIT_MODE) data.xy = 1; #endif return data; }
地形着色器将所有三个可能单元的可见性数据传递给片段程序。在研究状态下,我们将它们合并到顶点程序中,并将唯一的值转移到片段程序中。将visibility
第四个组件添加到输入中,以便为此放置一个位置。 struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; float4 visibility; };
现在,在顶点程序中,当我们更改可见性索引时,必须显式访问data.visibility.xyz
。 void vert (inout appdata_full v, out Input data) { … data.visibility.xyz = lerp(0.25, 1, data.visibility.xyz); }
之后,我们结合研究状态并将结果写入data.visibility.w
。这类似于在其他着色器中合并可见性,但使用这些单元的分量Y。 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;
现在可以通过片段程序获得研究状态IN.visibility.w
。在反照率的计算中考虑它。 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; }
未开发的地形现在为黑色。现在,未开发的单元的浮雕为黑色。但这尚未影响物体,道路和水。但是,这足以确保研究能够进行。保存和加载研究状态
现在我们已经添加了研究支持,我们需要确保在保存和加载地图时考虑到研究状态。因此,我们需要将地图文件的版本增加到3。为了使这些更改更方便,让我们为此添加一个SaveLoadMenu
常量。 const int mapFileVersion = 3;
当向中写入文件版本Save
以及在中检查文件支持时,我们将使用此常量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); } } }
最后一步,HexCell.Save
我们记录研究状态。 public void Save (BinaryWriter writer) { … writer.Write(IsExplored); }
我们将在最后阅读它Load
。此后,RefreshVisibility
如果研究状态与先前的状态不同,我们将打电话给我们。 public void Load (BinaryReader reader) { … IsExplored = reader.ReadBoolean(); ShaderData.RefreshVisibility(this); }
为了保持与旧保存文件的向后兼容性,如果文件版本小于3,则需要跳过读取保存状态。在这种情况下,默认情况下,单元格的状态为“未探索”。为此,我们需要将Load
标头数据添加为参数。 public void Load (BinaryReader reader, int header) { … IsExplored = header >= 3 ? reader.ReadBoolean() : false; ShaderData.RefreshVisibility(this); }
现在HexGrid.Load
,它将必须传入HexCell.Load
标头数据。 public void Load (BinaryReader reader, int header) { … for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … }
现在,在保存和加载地图时,将考虑细胞的探索状态。统一包装隐藏未知的单元格
在当前阶段,未探索的细胞会通过黑色浮雕视觉显示。但实际上,我们希望这些细胞不可见,因为它们是未知的。我们可以使不透明的几何体透明,从而使其不可见。但是,在开发Unity表面着色器框架时并未考虑到这种可能性。代替使用真实的透明度,我们将更改着色器以匹配背景,这也将使它们不可见。使救济真正变黑
尽管研究的浮雕是黑色的,但我们仍然可以识别它,因为它仍然具有镜面照明。要摆脱照明,我们需要使其完全为哑光黑色。为了不影响其他表面特性,最容易将镜面颜色更改为黑色。如果您使用可与高光配合使用的表面着色器,则可以这样做,但是现在我们使用标准金属材质。因此,让我们从将Terrain着色器切换为高光开始。更换颜色属性_Metallic物业_Specular。默认情况下,其颜色值应等于(0.2,0.2,0.2)。因此,我们保证它会与金属版本的外观匹配。 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) }
还更改相应的着色器变量。镜面曲面着色器的颜色定义为fixed3
,因此让我们使用它。 half _Glossiness;
将编译指示曲面从Standard更改为StandardSpecular。这将强制Unity使用高光生成着色器。 #pragma surface surf StandardSpecular fullforwardshadows vertex:vert
现在,该函数surf
需要第二个参数为type SurfaceOutputStandardSpecular
。此外,现在您需要分配的值不是o.Metallic
,而是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; }
现在,我们可以考虑explored
镜面反射的颜色来遮挡高光。 o.Specular = _Specular * explored;
未开发的地形,无反射光。如您在图片中所看到的,现在未开发的浮雕看起来暗淡的黑色。但是,当以切线角度查看时,这些表面会变成一面镜子,因此,浮雕开始反射环境,即天空盒。未开发的区域仍然反映环境。为了消除这些反射,我们将完全屏蔽未开发的浮雕。这是通过explored
为遮挡参数分配一个值来实现的,我们将其用作反射蒙版。 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;
未开发,无思考。配套背景
现在未开发的地形会忽略所有照明,您需要使其与背景匹配。由于我们的相机始终从上方看,因此背景始终为灰色。要告诉地形着色器使用哪种颜色,请添加_BackgroundColor属性,该属性默认为黑色。 Properties { … _BackgroundColor ("Background Color", Color) = (0,0,0) } … half _Glossiness; fixed3 _Specular; fixed4 _Color; half3 _BackgroundColor;
要使用此颜色,我们将其添加为发射光。这是o.Emission
通过指定背景色值乘以探索的一减来实现的。 o.Occlusion = explored; o.Emission = _BackgroundColor * (1 - explored);
由于我们使用默认的天空盒,因此可见的背景颜色实际上并不相同。通常,略带红色的灰色是最好的颜色。设置浮雕材料时,可以将代码68615BFF 用于十六进制颜色。救济材料具有灰色背景色。总的来说,这是可行的,尽管如果您知道从哪里看,您会注意到轮廓很弱。为了使播放器看不到它们,您可以为相机分配统一的背景色68615BFF,而不是向天盒。相机具有统一的背景色。为什么不删除天空盒?, , environmental lighting . , .
现在,我们无法找到背景和未开发单元格之间的差异。较高的未勘探地形仍可以在低摄像机角度掩盖较低的勘探地形。此外,未勘探的零件仍在勘探对象上留下阴影。但是这些最小的线索可以忽略。未探索的单元格不再可见。如果您不使用统一的背景色怎么办?, , . . , . , , , UV- .
隐藏救济对象
现在,我们只隐藏了浮雕的网格。其余的研究状态尚未受影响。到目前为止,只有救济是隐藏的。让我们更改Feature着色器,它是Terrain之类的不透明着色器。将其变成镜面反射着色器,并为其添加背景色。让我们从属性开始。 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" {} }
像以前一样进一步编译杂物表面和变量。 #pragma surface surf StandardSpecular fullforwardshadows vertex:vert … half _Glossiness;
visibility
还需要一个组件。由于“ 要素”结合了每个顶点的可见性,因此仅需要一个浮点值。现在我们需要两个。 struct Input { float2 uv_MainTex; float2 visibility; };
对其进行更改,vert
以使其明确用于可见性数据data.visibility.x
,然后分配data.visibility.y
研究数据的值。 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; }
对其进行更改,surf
以使其使用新数据,例如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; }
隐藏的救济物。隐藏水
接下来是“ 水”和“ 水岸”着色器。首先将它们转换为镜面着色器。但是,它们不需要背景色,因为它们是透明的着色器。转换后,再添加visibility
一个组件并相应地进行更改vert
。两个着色器组合来自三个单元的数据。 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和Water Shore执行surf
不同的操作,但以相同的方式设置其表面属性。由于它们是透明的,因此我们将explore
在Alpha通道中加以考虑,并且不会设置发射。 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; }
隐藏的水。我们隐藏河口,河流和道路
我们还有河口,河流和道路的着色器。这三个都是透明的,并且合并了两个单元格的数据。将它们全部切换为镜面反射,然后将它们添加到visibility
研究数据中。 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; }
更改河口和河流surf
着色器的功能,以使其使用新数据。两者都需要进行相同的更改。 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; }
Shader Road有所不同,因为它使用了额外的混合指标。 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; }
一切都是隐藏的。统一包装避免未开发的细胞
尽管所有未知的内容都在视觉上隐藏了,但是在搜索路径时不会考虑研究状态。结果,可以命令单位在未探索的单元之间移动,神奇地确定了移动的方向。我们需要强迫部队避免未开发的细胞。浏览未开发的单元格。小队决定搬家的成本
之前进入未开发的电池,让我们返工的代码转移成本的运动HexGrid
在HexUnit
。这将简化对具有不同移动规则的单位的支持。添加到确定移动成本的HexUnit
通用方法GetMoveCost
中。他需要知道哪些单元格在它们之间移动以及方向。我们复制相应的代码,以支付从HexGrid.Search
迁移到此方法的成本,并更改变量名称。 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; } }
该方法应返回移动成本。我使用旧代码跳过无效动作continue
,但是这种方法在这里行不通。如果无法移动,那么我们将退回移动的负成本。 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; }
现在我们需要知道何时查找路径,不仅是速度,还包括所选单位。相应地更改HexGameUI.DoPathFinding
。 void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, selectedUnit); } else { grid.ClearPath(); } } }
由于我们仍然需要使用小队速度,因此我们将添加到HexUnit
属性中Speed
。虽然它将返回一个恒定值24。 public int Speed { get { return 24; } }
的HexGrid
变化FindPath
,并且Search
使他们能够与我们的新方法的工作。 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; … }
现在,我们将从Search
确定是否可以移动到下一个单元格以及移动成本是多少的旧代码中删除。相反,我们将调用HexUnit.IsValidDestination
和HexUnit.GetMoveCost
。如果移动成本为负,我们将跳过该单元格。 for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; }
绕过未开发的区域
为了避免未探索的细胞,对于我们来说确保HexUnit.IsValidDestination
检查细胞是否已检查就足够了。 public bool IsValidDestination (HexCell cell) { return cell.IsExplored && !cell.IsUnderwater && !cell.Unit; }
更多单位将无法进入未开发的单元。由于未开发的单元不再是有效的端点,因此小队在移至端点时会避免使用它们。就是说,未开发的区域充当了阻碍路径的障碍,甚至延长了路径的可能性。为了首先探索该地区,我们将不得不使这些单位靠近未知的地形。如果在移动过程中出现一条较短的路径怎么办?. , . .
, , . , .
统一包装第22部分:增强的可见性
- 平稳地更改可见性。
- 使用单元格的高度确定范围。
- 隐藏地图的边缘。
通过增加对地图探索的支持,我们将改善范围的计算和转换。要看得更远,请爬上更高的位置。可见性转换
该单元格是可见的还是不可见的,因为它是否在分离范围之内。即使该单元似乎需要一些时间在单元之间移动,它的视场也会立即在单元之间跳跃。结果,周围细胞的可见性急剧变化。小队的移动似乎很平稳,但是能见度的变化却是突然的。理想情况下,可见性也应平稳变化。进入可见区域后,应逐渐照亮细胞,并使其逐渐变暗。还是您更喜欢即时转换?让我们添加到HexCellShaderData
切换即时转换的属性。默认情况下,过渡是平滑的。 public bool ImmediateMode { get; set; }
过渡细胞追踪
即使显示平滑的过渡,真实的可见性数据仍然保持二进制,即效果只是视觉上的。这意味着必须处理可见性转换HexCellShaderData
。我们将为其提供执行转换的单元格列表。确保在每次初始化时为空。 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; } … }
目前,我们正在RefreshVisibility
直接设置单元格数据。对于即时过渡模式,这仍然是正确的,但是当禁用它时,必须将一个单元格添加到过渡单元格列表中。 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; }
可见性似乎不再起作用,因为到目前为止,我们不对列表中的单元格做任何事情。循环遍历单元格
而不是立即将相应的值设置为255或0,我们将逐渐增大/减小这些值。过渡的平滑度取决于变化率。它不应该很快也不应该很慢。漂亮的过渡与游戏便利性之间的一个很好的折衷是在一秒钟内改变。让我们为此设置一个常量,以使其更容易更改。 const float transitionSpeed = 255f;
现在,LateUpdate
我们可以定义应用于值的增量。为此,将时间增量乘以速度。它必须是整数,因为我们不知道它的大小。帧速率的急剧下降会使增量超过255。此外,在有过渡单元的情况下,我们需要进行更新。因此,当列表中包含某些内容时,应包括该代码。 void LateUpdate () { int delta = (int)(Time.deltaTime * transitionSpeed); cellTexture.SetPixels32(cellTextureData); cellTexture.Apply(); enabled = transitioningCells.Count > 0; }
理论上也可能非常高的帧速率。结合较低的过渡速度,这可以使我们得到0的变化量。要进行更改,我们将变化量最小值最小值设为1。 int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; }
收到增量后,我们可以在所有过渡单元周围循环并更新其数据。假设我们有一个方法UpdateCellData
,其参数是相应的像元和增量。 int delta = (int)(Time.deltaTime * transitionSpeed); if (delta == 0) { delta = 1; } for (int i = 0; i < transitioningCells.Count; i++) { UpdateCellData(transitioningCells[i], delta); }
在某个时候,单元格过渡应该完成。假定该方法返回有关过渡是否仍在进行的信息。当它停止进行时,我们可以从列表中删除该单元格。之后,我们必须减小迭代器,以免跳过单元格。 for (int i = 0; i < transitioningCells.Count; i++) { if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells.RemoveAt(i--); } }
过渡单元的处理顺序并不重要。因此,我们不必删除当前索引处的单元格,这将迫使RemoveAt
所有单元格在其后移动。相反,我们将最后一个单元格移到当前索引,然后删除最后一个。 if (!UpdateCellData(transitioningCells[i], delta)) { transitioningCells[i--] = transitioningCells[transitioningCells.Count - 1]; transitioningCells.RemoveAt(transitioningCells.Count - 1); }
现在我们必须创建一个方法UpdateCellData
。要完成他的工作,他将需要一个索引和单元格数据,因此让我们从获取它们开始。它还应确定是否继续更新单元。默认情况下,我们将假定没有必要。工作完成后,有必要应用更改的数据并返回状态“更新正在继续”。 bool UpdateCellData (HexCell cell, int delta) { int index = cell.Index; Color32 data = cellTextureData[index]; bool stillUpdating = false; cellTextureData[index] = data; return stillUpdating; }
更新单元格数据
在此阶段,我们有一个正在过渡或已经完成的单元。首先,让我们检查单元探针的状态。如果检查了该单元格,但其G值尚未等于255,则它处于转换过程中,因此我们将对其进行监视。 bool stillUpdating = false; if (cell.IsExplored && data.g < 255) { stillUpdating = true; } cellTextureData[index] = data;
为了执行过渡,我们将向该单元格的G值添加一个增量。算术运算不适用于字节,它们首先转换为整数。因此,总和将为整数格式,必须将其转换为字节。 if (cell.IsExplored && data.g < 255) { stillUpdating = true; int t = data.g + delta; data.g = (byte)t; }
但是在转换之前,您需要确保该值不超过255。 int t = data.g + delta; data.g = t >= 255 ? (byte)255 : (byte)t;
接下来,我们需要对可见性进行相同的操作,它使用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; }
由于单元格可以再次变为不可见,因此我们需要检查是否有必要减小R的值。当单元格不可见但R大于零时,会发生这种情况。 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; }
现在已经UpdateCellData
准备就绪,可见性转换已正确执行。可见性转换。防止重复的过渡元素
转换有效,但重复的项目可能会出现在列表中。如果单元格的可见状态在转换过程中发生变化,就会发生这种情况。例如,当小队移动期间仅在短时间内可见该单元格时。由于出现了重复的元素,因此每帧会对单元格过渡进行几次更新,从而加快了过渡速度并增加了工作量。我们可以通过在添加单元格之前检查它是否已经在列表中来防止这种情况。但是,每个电话都会进行列表搜索RefreshVisibility
成本很高,尤其是在执行多个单元转换时。相反,让我们使用另一个尚未用于指示单元格是否处于过渡过程中的通道,例如值B.将单元格添加到列表时,我们将为其分配值255,并仅添加值不等于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; }
为此,我们需要在单元格转换完成后重置B的值。 bool UpdateCellData (HexCell cell, int delta) { … if (!stillUpdating) { data.b = 0; } cellTextureData[index] = data; return stillUpdating; }
没有重复的过渡。即时加载可见性
现在,即使在加载地图时,可见性更改也始终是渐进的。这是不合逻辑的,因为地图描述了已经可见细胞的状态,因此此处的过渡是不合适的。此外,对大型地图的许多可见单元执行过渡可能会减慢加载后的游戏速度。因此,在加载像元和小队之前,让我们切换HexGrid.Load
到即时转换模式。 public void Load (BinaryReader reader, int header) { … cellShaderData.ImmediateMode = true; for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader, header); } … }
因此,我们将重新定义即时过渡模式的初始设置,无论它是什么。也许它已经被关闭,或者有一个配置选项,所以我们会记住初始模式,并在工作完成后切换到它。 public void Load (BinaryReader reader, int header) { … bool originalImmediateMode = cellShaderData.ImmediateMode; cellShaderData.ImmediateMode = true; … cellShaderData.ImmediateMode = originalImmediateMode; }
统一包装高度范围
到目前为止,我们对所有单元都使用了恒定的三个范围,但实际上更复杂。在一般情况下,由于两个原因,我们无法看到对象:某个障碍阻止了我们看到它,或者对象太小或太远。在我们的游戏中,我们仅实现范围限制。我们看不到地球另一侧的事物,因为行星使我们蒙昧。我们只能看到地平线。由于行星可以近似地视为球体,因此视角越高,我们可以看到的表面越多,也就是说,地平线取决于高度。地平线取决于视点的高度。我们单位的能见度有限,模仿了地球曲率产生的地平线效应。他们的审查范围取决于行星的大小和地图的比例。至少这是合乎逻辑的解释。但是缩小范围的主要原因是游戏玩法,这就是所谓的战争迷局。但是,了解了视场的物理原理后,我们可以得出结论,高视点应该具有战略价值,因为它可以移动视野并允许您查看较低的障碍。但是到目前为止,我们还没有实现它。审查高度
在确定范围时要考虑高度,我们需要知道高度。这将是通常的水位或水位,具体取决于陆地细胞还是水。让我们将其添加到HexCell
属性中。 public int ViewElevation { get { return elevation >= waterLevel ? elevation : waterLevel; } }
但是,如果高度影响范围,则随着单元格观察高度的变化,可见性状况也可能发生变化。由于单元格已阻止或正在阻止多个单元的作用域,因此确定需要更改的内容并非那么容易。单元本身将无法解决此问题,因此请让其报告情况的变化HexCellShaderData
。假设您HexCellShaderData
有一种方法ViewElevationChanged
。HexCell.Elevation
如有必要,我们将在分配后称呼它。 public int Elevation { get { return elevation; } set { if (elevation == value) { return; } int originalViewElevation = ViewElevation; elevation = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } … } }
同样的道理WaterLevel
。 public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } int originalViewElevation = ViewElevation; waterLevel = value; if (ViewElevation != originalViewElevation) { ShaderData.ViewElevationChanged(); } ValidateRivers(); Refresh(); } }
重置可见性
现在我们需要创建一个方法HexCellShaderData.ViewElevationChanged
。确定一般可见性情况如何变化是一项复杂的任务,尤其是在同时更改多个单元格时。因此,我们不会提出任何技巧,而只是计划重置所有单元的可见性。添加一个布尔字段以跟踪是否执行此操作。在方法内部,我们将简单地将其设置为true并包含组件。无论同时更改了多少个单元,这都会导致一次复位。 bool needsVisibilityReset; … public void ViewElevationChanged () { needsVisibilityReset = true; enabled = true; }
要重置所有单元格的可见性值,您必须有权访问它们,而您HexCellShaderData
没有访问权限。因此,让我们委派这个责任HexGrid
。为此,您需要添加到HexCellShaderData
属性,该属性允许您引用网格。然后,我们可以使用它LateUpdate
来请求重置。 public HexGrid Grid { get; set; } … void LateUpdate () { if (needsVisibilityReset) { needsVisibilityReset = false; Grid.ResetVisibility(); } … }
让我们继续HexGrid
:HexGrid.Awake
创建着色器数据后,设置到网格的链接。 void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; cellShaderData = gameObject.AddComponent<HexCellShaderData>(); cellShaderData.Grid = this; CreateMap(cellCountX, cellCountZ); }
HexGrid
还应该获得一种ResetVisibility
丢弃所有单元格的方法。只需使其遍历循环中的所有单元,然后将重置委托给它自己即可。 public void ResetVisibility () { for (int i = 0; i < cells.Length; i++) { cells[i].ResetVisibility(); } }
现在我们需要添加该HexCell
方法ResetVisibilty
。它将简单地将可见性归零并触发可见性更新。当单元格可见性大于零时,必须执行此操作。 public void ResetVisibility () { if (visibility > 0) { visibility = 0; ShaderData.RefreshVisibility(this); } }
重置所有可见性数据后,HexGrid.ResetVisibility
他必须再次将可见性应用于所有小队,为此他需要了解每个小队的范围。假设可以使用属性获得它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); } }
为此,我们将重命名HexUnit.visionRange
为重构并将HexUnit.VisionRange
其转换为属性。虽然它将收到3的恒定值,但是将来它将改变。 public int VisionRange { get { return 3; } }
因此,在更改单元格的查看高度后,可见性数据将被重置并保持正确。但是我们可能会更改确定范围的规则,并在“播放”模式下运行重新编译。为了使范围独立更改,让我们HexGrid.OnEnable
在检测到重新编译时进行一次重置。 void OnEnable () { if (!HexMetrics.noiseSource) { … ResetVisibility(); } }
现在,您可以更改范围代码并查看结果,同时保持在“播放”模式下。扩大视野
确定范围的计算HexGrid.GetVisibleCells
。为了使高度影响范围,我们可以通过fromCell
临时重新定义透射区域来简单地使用查看高度。因此,我们可以轻松检查是否可行。 List<HexCell> GetVisibleCells (HexCell fromCell, int range) { … range = fromCell.ViewElevation; fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); … }
使用高度作为范围。可见性障碍
仅当所有其他单元格的高度都为零时,才能将查看高度用作范围。但是,如果所有像元的高度都与视点相同,则视场应为零。此外,高度较高的牢房应遮挡后面的矮牢房的可见性。到目前为止,这还没有实现。范围不干涉。确定范围的最正确方法是通过射线的发射进行检查,但是这种方法很快会变得昂贵,并且仍然会产生奇怪的结果。我们需要一种快速的解决方案,它可以产生足够好的结果,而不必是完美的。另外,重要的是,确定范围的规则对于玩家来说必须简单,直观和可预测。我们的解决方案如下-确定单元格的可见性时,我们会将相邻单元格的可视高度添加到覆盖距离内。实际上,当我们查看这些单元格时,这会缩小范围,并且如果跳过它们,则将不允许我们到达它们后面的单元格。 int distance = current.Distance + 1; if (distance + neighbor.ViewElevation > range) { continue; }
高格挡视线。我们不应该在远处看到高大的细胞吗?, , , . , .
不要四处寻找
现在,似乎高的单元格将视线降低到低点,但有时示波器会穿透它们,尽管似乎不应该如此。发生这种情况是因为搜索算法仍然绕过阻塞单元格找到了这些单元格的路径。结果,看起来我们的可见范围可以绕过障碍。为了避免这种情况,我们需要确保在确定像元可见性时仅考虑最短路径。这可以通过删除比所需时间更长的路径来完成。 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; } … } }
我们仅使用最短的路径。因此,我们修复了大多数明显错误的情况。对于附近的小区,此方法效果很好,因为只有最短的路径可以到达它们。距离更远的单元具有更多的路径选择;因此,在长距离上,可见范围仍然可能发生。如果可见区域保持较小并且相邻高度之间的差异不太大,这将不是问题。最后,我们没有取代透射的视野,而是增加了视野的高度。该小队自己的视野表明其高度,飞行高度或侦察能力。 range += fromCell.ViewElevation;
以低视角查看完整视野。也就是说,考虑到相对于视点的像元高度差异,沿着可见光的最短路径移动时,视觉的最终规则适用于视觉。当单元超出范围时,它将阻止通过它的所有路径。结果,没有任何阻碍视线的高观察点就具有了战略上的价值。统一包装无法探索的细胞
可见性的最后一个问题涉及地图的边缘。浮雕突然过渡并且没有过渡结束,因为边缘的单元没有邻居。地图的标记边缘。理想情况下,未勘探区域和地图边缘的视觉显示应相同。我们可以通过在对边缘进行三角测量时(当它们没有邻居时)添加特殊情况来实现此目的,但这将需要附加的逻辑,并且我们将不得不处理缺少的像元。因此,这种解决方案是不平凡的。一种替代方法是强制地图的边界像元处于探索范围之内,而对其进行探索。这种方法要简单得多,所以让我们使用它。它还允许您将标记为未探索的单元格和其他单元格,从而更容易实现地图不均匀边缘的创建。此外,边缘处的隐藏单元格使您可以创建进入和离开河流和道路地图的道路和河流,因为它们的端点将不在范围内。另外,借助此解决方案,您可以添加进入和离开地图的单位。我们将细胞标记为已调查
要指示可以检查一个单元格,请添加到HexCell
属性中Explorable
。 public bool Explorable { get; set; }
现在,如果一个单元格是经过调查的单元格,则可以看到它,因此IsVisible
我们将更改属性以考虑到这一点。 public bool IsVisible { get { return visibility > 0 && Explorable; } }
同样适用于IsExplored
。但是,为此,我们调查了标准属性。我们需要将其转换为显式属性,以便能够更改其吸气剂的逻辑。 public bool IsExplored { get { return explored && Explorable; } private set { explored = value; } } … bool explored;
隐藏地图的边缘
您可以在方法中隐藏矩形地图的边缘HexGrid.CreateCell
。对不在边缘的单元进行调查,其余所有单元均未开发。 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; … }
现在,卡的边缘变暗,在它们后面隐藏了巨大的未开发空间。结果,研究的地图区域的大小在每个维度上减小了两倍。地图的未开发边缘。未开发的细胞会阻碍可见性
最后,如果无法检查细胞,那么它将干扰可见性。进行更改HexGrid.GetVisibleCells
以考虑到这一点。 if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase || !neighbor.Explorable ) { continue; }
统一包装第23部分:产生土地
- 用生成的景观填充新地图。
- 我们在水上耕种土地,在一些地方充水。
- 我们控制已创建土地的数量,其高度和不平坦度。
- 我们增加了对各种配置选项的支持,以创建变量映射。
- 我们这样做是为了可以再次生成相同的地图。
本教程的这一部分将是过程图生成系列的开始。这部分是在Unity 2017.1.0中创建的。众多生成的地图之一。卡生成
尽管我们可以创建任何地图,但需要花费很多时间。如果该应用程序可以通过为设计师生成卡片来帮助设计师,然后他可以根据自己的喜好对其进行修改,则将很方便。您可以采取另一步,完全摆脱手动创建设计的过程,将生成最终地图的责任完全转移到应用程序。因此,每次可以使用新卡玩游戏,并且每次游戏会话都不同。为了使所有这些成为可能,我们必须创建一个地图生成算法。您需要的生成算法的类型取决于您需要的卡的类型。没有正确的方法,您总是必须在信誉和可玩性之间寻求折衷方案。要使一张纸牌令人信服,对于玩家来说,它必须看起来很有可能且真实。这并不意味着地图应该看起来像我们星球的一部分。它可能是不同的星球,也可能是完全不同的现实。但是,如果它应该表示地球的解脱,那么它必须至少部分类似于地球。可玩性与纸牌如何与游戏玩法相关。有时,它与可信度冲突。例如,尽管山脉看起来很美,但同时它们极大地限制了单位的移动和视野。如果这是不可取的,那么您就必须在没有障碍的情况下做,这会降低信誉并限制游戏的表现力。或者,我们可以拯救山脉,但减少它们对游戏玩法的影响,这也可以降低信誉。另外,必须考虑可行性。例如,您可以通过模拟构造板块,侵蚀,降雨,火山喷发,陨石和月球的影响等来创建一个非常逼真的类地球行星。但是开发这样的系统将需要大量时间。此外,生成这样的星球可能要花费很长时间,并且玩家也不想在开始新游戏之前等待几分钟。也就是说,模拟是一个强大的工具,但它有代价。游戏通常会在可信度,可玩性和可行性之间进行权衡。有时,这种折衷是看不见的,看起来完全是正常的,而有时,它们看起来是随机的,不一致的或混乱的,具体取决于开发过程中做出的决定。这不仅适用于卡生成,而且在开发程序卡生成器时,您需要特别注意这一点。您可能需要花费大量时间来创建一种算法,该算法可以生成漂亮的卡片,这些卡片对于您正在创建的游戏毫无用处。在本教程系列中,我们将创建一个类似土地的浮雕。它看起来应该很有趣,具有很大的可变性并且没有较大的均匀区域。救济规模将很大,地图将覆盖一个或多个大洲,海洋区域,甚至整个星球。我们需要控制地理环境,包括土地质量,气候,区域数量和地形颠簸。在这一部分中,我们将为创建寿司奠定基础。在编辑模式下入门
我们将专注于地图,而不是游戏玩法,因此在编辑模式下启动应用程序会更加方便。因此,我们可以立即看到卡片。因此,我们将通过HexMapEditor.Awake
将编辑模式设置为true并打开此模式的shader关键字来进行更改。 void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); Shader.EnableKeyword("HEX_MAP_EDIT_MODE"); SetEditMode(true); }
卡生成器
由于生成过程图需要大量代码,因此我们不会将其直接添加到中HexGrid
。相反,我们将创建一个新组件HexMapGenerator
,并且HexGrid
将不知道它。如果需要,这将简化向另一种算法的过渡。生成器需要一个到网格的链接,因此我们将向它添加一个常规字段。此外,我们添加了一种通用方法GenerateMap
来处理算法的工作。我们将为它提供地图的尺寸作为参数,然后强制将其用于创建新的空地图。 using System.Collections.Generic; using UnityEngine; public class HexMapGenerator : MonoBehaviour { public HexGrid grid; public void GenerateMap (int x, int z) { grid.CreateMap(x, z); } }
将具有组件的对象添加到场景HexMapGenerator
并将其连接到网格。地图生成器对象。更改新地图的菜单
我们将对其进行更改,NewMapMenu
以便它可以生成卡片,而不仅仅是创建空卡片。我们将通过布尔字段generateMaps
(默认情况下具有一个值)来控制其功能true
。让我们创建一个设置此字段的通用方法,就像我们切换options一样HexMapEditor
。将适当的开关添加到菜单并将其连接到方法。 bool generateMaps = true; public void ToggleMapGeneration (bool toggle) { generateMaps = toggle; }
带有开关的新卡的菜单。在菜单上提供指向地图生成器的链接。然后,如果需要GenerateMap
,我们将强制其调用generator 方法,而不仅仅是执行CreateMap
网格。 public HexMapGenerator mapGenerator; … void CreateMap (int x, int z) { if (generateMaps) { mapGenerator.GenerateMap(x, z); } else { hexGrid.CreateMap(x, z); } HexMapCamera.ValidatePosition(); Close(); }
连接到发电机。单元访问
为了使生成器正常工作,它需要访问单元。我们HexGrid
已经有常用的方法GetCell
,需要或位置矢量,或六边形坐标。生成器不需要使用任何一种,因此我们添加了两种方便的方法HexGrid.GetCell
,它们将与像元的偏移量或索引的坐标一起使用。 public HexCell GetCell (int xOffset, int zOffset) { return cells[xOffset + zOffset * cellCountX]; } public HexCell GetCell (int cellIndex) { return cells[cellIndex]; }
现在它HexMapGenerator
可以直接接收细胞。例如,创建新地图后,他可以使用草坐标将草设置为单元格中间列的浮雕。 public void GenerateMap (int x, int z) { grid.CreateMap(x, z); for (int i = 0; i < z; i++) { grid.GetCell(x / 2, i).TerrainTypeIndex = 1; } }
草的专栏在一张小地图的。统一包装做寿司
生成地图时,我们完全没有土地。可以想象,整个世界被一片巨大的海洋淹没。当海底的一部分被推得太多以至于它上升到水面之上时,就会产生一块土地。我们需要确定应以这种方式创建多少土地,它会出现在何处以及形状如何。减轻压力
让我们从小处开始-在水面上方举起一块土地。为此,我们创建了一个RaiseTerrain
带有参数的方法来控制绘图的大小。在中调用此方法GenerateMap
,以替换先前的测试代码。让我们从由七个单元组成的一小块土地开始。 public void GenerateMap (int x, int z) { grid.CreateMap(x, z);
到目前为止,我们使用“草”类浮雕来表示高地,原始的“沙”类浮雕是指海洋。让我们RaiseTerrain
选择一个随机单元,并更改其救济的类型,直到获得适当数量的土地为止。为了获得随机单元格,我们添加了一种GetRandomCell
确定随机单元格索引并从网格中获取相应单元格的方法。 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)); }
七个随机的寿司室。由于最终我们可能需要大量随机单元格或遍历所有单元格几次,因此让我们跟踪单元格本身中单元格的数量HexMapGenerator
。 int cellCount; public void GenerateMap (int x, int z) { cellCount = x * z; … } … HexCell GetRandomCell () { return grid.GetCell(Random.Range(0, cellCount)); }
创建一个站点
到目前为止,我们正在将七个随机小区变成陆地,它们可以在任何地方。它们很可能没有形成一个单一的土地区域。此外,我们可以多次选择相同的像元,因此可以减少土地面积。为了同时解决这两个问题,我们将仅选择第一个单元格。在那之后,我们应该只选择那些与先前选择的单元格相邻的单元格。这些限制类似于路径搜索的限制,因此我们在此处使用相同的方法。我们添加HexMapGenerator
了自己的属性以及搜索边界阶段的计数器,就像在中一样HexGrid
。 HexCellPriorityQueue searchFrontier; int searchFrontierPhase;
在需要优先级队列之前,请先检查它是否存在。 public void GenerateMap (int x, int z) { cellCount = x * z; grid.CreateMap(x, z); if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } RaiseTerrain(7); }
创建新地图后,所有像元的搜索边界均为零。但是,如果我们要在地图生成过程中搜索像元,那么我们将在此过程中增加它们的搜索边界。如果我们执行许多搜索操作,它们可能会超出记录的搜索边界的阶段HexGrid
。这可能会干扰对单位路径的搜索。为避免这种情况,在地图生成过程结束时,我们会将所有像元的搜索阶段重置为零。 RaiseTerrain(7); for (int i = 0; i < cellCount; i++) { grid.GetCell(i).SearchPhase = 0; }
现在,我RaiseTerrain
必须寻找适当的单元格,而不是随机选择它们。此过程与中的搜索方法非常相似HexGrid
。但是,我们访问单元格不会超过一次,因此,将搜索边界的相位增加1而不是2,就足够了。然后,我们使用随机选择的第一个单元格初始化边界。与往常一样,除了设置其搜索阶段外,我们还将其距离和启发式方法分配为零。 void RaiseTerrain (int chunkSize) {
在那之后,搜索循环将是我们最熟悉的。另外,要继续搜索直到边界为空,我们需要在片段达到所需大小时停止,因此我们将对其进行跟踪。在每次迭代时,我们将从队列中提取下一个单元格,设置其释放类型,增加大小,然后绕过该单元格的邻居。如果尚未将所有邻居添加到边界,则仅将其添加到边界。我们不需要进行任何更改或比较。完成后,您需要清除边框。 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();
一排细胞。我们得到了一个合适大小的地块。仅当没有足够数量的单元格时,它才会更小。由于边界的填充方式,地块始终由西北延伸的线组成。仅当到达地图边缘时,它才会改变方向。我们连接细胞
陆地区域很少像线一样,如果确实如此,它们的方向不一定总是相同。要更改站点的形状,我们需要更改单元的优先级。第一个随机像元可以用作图的中心。然后到所有其他像元的距离将相对于此点。因此,我们将为靠近中心的单元格赋予更高的优先级,因此该站点将不会以一条线的形式增长,而是会以中心为中心。 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); } } }
细胞的积累。实际上,如果中心单元格没有出现在地图边缘,那么现在我们的七个单元格将被精美地包装在一个紧凑的六角形区域中。现在让我们尝试使用30的地块大小。 RaiseTerrain(30);
30格中的寿司块。尽管没有足够的单元格来获得正确的六边形,但我们再次得到了相同的形状。由于图的半径较大,因此它很可能靠近地图的边缘,这将迫使其采取其他形状。寿司随机化
我们不希望所有区域看起来都一样,因此我们将略微更改单元格的优先级。每次我们向边界添加一个相邻的单元格时,如果下一个数字Random.value
小于某个阈值,则此单元格的试探法将变为0,而不是1。让我们使用值0.5作为阈值,也就是说,它很可能会影响一半的单元格。 neighbor.Distance = neighbor.coordinates.DistanceTo(center); neighbor.SearchHeuristic = Random.value < 0.5f ? 1: 0; searchFrontier.Enqueue(neighbor);
扭曲的区域。通过增加单元的搜索启发式,我们使它的访问比预期的晚。同时,距离中心更远一步的其他单元将被更早访问,除非它们也增加了启发式方法。这意味着,如果我们将所有单元的试探法增加一个值,那么这将不会影响地图。也就是说,阈值1不会产生影响,就像阈值0一样。阈值0.8等于0.2。也就是说,概率为0.5会使搜索过程最“发抖”。适当的振荡量取决于所需的地形类型,因此让它可自定义。将jitterProbability
具有属性的通用float字段添加到生成器Range
限制在0-0.5范围内。让我们给它一个默认值,等于该间隔的平均值,即0.25。这将使我们能够在Unity inspector窗口中配置生成器。 [Range(0f, 0.5f)] public float jitterProbability = 0.25f;
波动的可能性。您可以在游戏界面中自定义它吗?, . UI, . , UI. , . , .
现在,要确定启发式算法何时应等于1,我们使用概率而不是常数。 neighbor.SearchHeuristic = Random.value < jitterProbability ? 1: 0;
我们使用0和1的启发式值。尽管可以使用更大的值,但这将大大恶化截面的变形,最有可能将其变成一堆条纹。筹集土地
我们将不仅限于一块土地。例如,我们将一个调用RaiseTerrain
放在循环中以获取五个部分。 for (int i = 0; i < 5; i++) { RaiseTerrain(30); }
五块土地。尽管现在我们正在生成五张图,每张图各有30个像元,但不一定能准确得到150个像元的土地。由于每个站点都是单独创建的,因此它们彼此之间并不了解,因此它们可以相交。这是正常现象,因为它可以创建比一组单独的部分更多的有趣风景。为了增加土地的可变性,我们还可以更改每个地块的大小。添加两个整数字段以控制图的最小和最大大小。给它们分配足够大的时间间隔,例如20-200。我将使标准最小值等于30,使标准最大值等于100。 [Range(20, 200)] public int chunkSizeMin = 30; [Range(20, 200)] public int chunkSizeMax = 100;
上浆间隔。当调用时,我们使用这些字段随机确定区域的大小RaiseTerrain
。 RaiseTerrain(Random.Range(chunkSizeMin, chunkSizeMax + 1));
中间地图上有五个随机大小的部分。制作足够的寿司
虽然我们不能特别控制产生的土地数量。尽管我们可以为图的数量添加配置选项,但图本身的大小是随机的,可能会略有重叠或紧密重叠。因此,地点的数量并不能保证在地图上显示所需土地的数量。让我们添加一个选项来直接控制以整数表示的土地百分比。由于100%的土地或水不是很有趣,因此我们将其限制为5–95,默认值为50。 [Range(5, 95)] public int landPercentage = 50;
寿司的百分比。为了保证创建适量的土地,我们只需要继续提高地形区域,直到获得足够的土地。为此,我们需要控制流程,这会使土地的产生变得复杂。因此,让我们通过调用新方法来替换现有的饲养场周期CreateLand
。此方法要做的第一件事是计算应变为陆地的像元数。这将是我们寿司单元的总和。 public void GenerateMap (int x, int z) { …
CreateLand
RaiseTerrain
直到我们用完所有细胞后才会导致。为了不超过金额,我们进行了更改,RaiseTerrain
以便它接收金额作为附加参数。完成工作后,他必须退还剩余的款项。
每次将牢房从边界移开并转换为土地时,数量应减少。如果这之后全部花光了,那么我们必须停止搜索并完成站点。此外,仅当当前单元尚未着陆时,才应执行此操作。 while (size < chunkSize && searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); if (current.TerrainTypeIndex == 0) { current.TerrainTypeIndex = 1; if (--budget == 0) { break; } } size += 1; … }
现在,它CreateLand
可以耕种土地,直到花完整个牢房。 void CreateLand () { int landBudget = Mathf.RoundToInt(cellCount * landPercentage * 0.01f); while (landBudget > 0) { landBudget = RaiseTerrain( Random.Range(chunkSizeMin, chunkSizeMax + 1), landBudget ); } }
恰好一半的地图变成了土地。统一包装考虑高度
土地不仅是一块平坦的土地,还受到海岸线的限制。她的身高不断变化,其中包括丘陵,山脉,山谷,湖泊等。由于缓慢移动的构造板块的相互作用,存在高度差异。尽管我们不会对其进行模拟,但我们的陆地区域应在某种程度上类似于此类板块。站点不移动,但可能相交。我们可以利用这一点。推高土地
每个地块代表从海底推出的一部分土地。因此,让我们不断增加当前单元格的高度,RaiseTerrain
看看会发生什么。 HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.TerrainTypeIndex == 0) { … }
高地着陆。我们达到了顶峰,但很难看到。如果您对每个高度级别使用自己的地形类型,例如地理分层,则可以使它们更加清晰。我们只会这样做,以使高度更引人注目,因此您可以简单地将高度级别用作高程索引。让我们创建一个单独的方法SetTerrainType
来仅设置一次所有地形类型,而不是随高度的变化来更新单元的地形类型。 void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); cell.TerrainTypeIndex = cell.Elevation; } }
创建寿司后,我们将调用此方法。 public void GenerateMap (int x, int z) { … CreateLand(); SetTerrainType(); … }
现在他RaiseTerrain
不能应付那种救济,而专注于身高。为此,您需要更改其逻辑。如果当前单元格的新高度为1,则它刚好变为陆地,因此单元格的总和已减少,这可能导致该站点的增长完成。 HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == 1 && --budget == 0) { break; }
层的分层。加水
让我们明确指出哪些单元格是水或陆地,将所有单元格的水位设置为1。GenerateMap
在创建陆地之前执行此操作。 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(); … }
现在,对于土地层的指定,我们可以使用所有类型的地形。所有的海底细胞以及最低的陆地细胞都将保持沙子。这可以通过从高度减去水位并将该值用作浮雕类型的指数来完成。 void SetTerrainType () { for (int i = 0; i < cellCount; i++) { HexCell cell = grid.GetCell(i); if (!cell.IsUnderwater) { cell.TerrainTypeIndex = cell.Elevation - cell.WaterLevel; } } }
土地和水。提高水位
我们不仅限于一个水位。让我们使用间隔为1–5且默认值为3的公共字段来自定义它。初始化单元格时使用此级别。 [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; } … }
水位3。当水位为3时,我们得到的土地少于我们的预期。这是因为它RaiseTerrain
仍然认为水位是1。让我们对其进行修复。 HexCell current = searchFrontier.Dequeue(); current.Elevation += 1; if (current.Elevation == waterLevel && --budget == 0) { break; }
使用较高的水位会导致这种情况。牢房不会立即变成土地。当水位为2时,第一部分仍将留在水下。海洋底部已经上升,但仍然留在水下。仅在至少两个部分的相交处形成平台。水位越高,就越需要越多的场地来创造土地。因此,随着水位的升高,土地变得更加混乱。另外,当需要更多的地块时,它们更有可能在已经存在的土地上相交,这就是为什么在使用较小的地块的情况下,山地将变得更常见,而平地则更少。水位为2–5,寿司始终为50%。统一包装垂直运动
到目前为止,我们一次将情节提高了一层,但我们不必局限于此。高地
尽管每个部分将其单元格的高度增加了一个级别,但可能会发生剪裁。当两个部分的边缘接触时,会发生这种情况。这可以创建孤立的悬崖,但是长长的悬崖线将很少见。我们可以通过将图的高度增加一级以上来增加其出现的频率。但这仅需要在一定比例的站点中完成。如果所有区域都升高,则很难沿着地形移动。因此,让我们使用默认值为0.25的概率字段使此参数可自定义。 [Range(0f, 1f)] public float highRiseProbability = 0.25f;
细胞强烈上升的可能性。尽管我们可以在高处使用任何增加的高度,但这很快就失去了控制。高度差2已经产生了悬崖,所以这就足够了。由于您可以跳过等于水位的高度,因此我们需要更改确定单元是否已着陆的方式。如果水位低于水位,而现在又等于或高于水位,则我们创建了一个新的陆地单元。 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; … }
高度急剧增加的概率为0.25、0.50、0.75和1。降低土地
土地并不总是会上升,有时会下降。当土地降到足够低时,水就会填满,土地就会流失。到目前为止,我们还没有这样做。由于我们仅将区域向上推,因此土地通常看起来像是一组相当圆形的区域,混合在一起。如果有时将面积降低,则会得到更多变化的形式。没有沉没的寿司的大地图。我们可以使用另一个概率场来控制地面沉降的频率。由于降低可能会破坏土地,因此降低的可能性应始终低于提高的可能性。否则,可能需要很长时间才能获得正确比例的土地。因此,让我们使用最大降低概率0.4和默认值0.2。 [Range(0f, 0.4f)] public float sinkProbability = 0.2f;
降低的可能性。降低场地类似于升高场地,但有所不同。因此,我们复制该方法RaiseTerrain
并将其名称更改为SinkTerrain
。除了确定上升幅度之外,我们需要一个可以使用相同逻辑的下降值。同时,需要进行比较以检查我们是否已经穿过水面。此外,降低浮雕时,我们不限于单元格的总和。相反,每个丢失的寿司单元都会返回花费在其上的金额,因此我们将其增加并继续工作。 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
现在,在内部的每次迭代中,CreateLand
我们必须根据降低的可能性降低或升高土地。 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); } } }
掉落的概率为0.1、0.2、0.3和0.4。极限高度
在当前阶段,我们可能会重叠许多部分,有时会增加一些高度,其中一些可能会下降然后再次上升。同时,我们可以创建很高的高度,有时还可以创建非常低的高度,尤其是在需要大量土地的情况下。在90%的土地上都很高。为了限制高度,让我们添加一个自定义的最小值和最大值。合理的最小值应介于-4和0之间,可接受的最大值可以在6-10之间。假设默认值为−2和8。当手动编辑地图时,它们将超出允许的限制,因此您可以更改UI编辑器的滑块,也可以保持不变。 [Range(-4, 0)] public int elevationMinimum = -2; [Range(6, 10)] public int elevationMaximum = 8;
最小和最大高度。现在,RaiseTerrain
我们必须确保高度不超过允许的最大值。这可以通过检查当前单元格是否太高来完成。如果是这样,那么我们将跳过它们而不更改其高度并添加其邻居。这将导致以下事实:陆地区域将避开已达到最大高度的区域,并在其周围生长。 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;
在中进行相同的操作SinkTerrain
,但最小高度。 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;
高度限制为90%。负海拔保护
此时,保存和加载代码无法处理负高度,因为我们将高度存储为字节。保存为大正数时将转换为负数。因此,在保存和加载生成的地图时,可能会出现很高的位置,以代替原始的水下单元。我们可以通过将其存储为整数而不是字节来增加对负高度的支持。但是,我们仍然不需要支持多个高度级别。另外,我们可以通过加127来抵消存储的值。这将使我们能够在一个字节内正确存储在-127–128范围内的高度。相应地更改HexCell.Save
。 public void Save (BinaryWriter writer) { writer.Write((byte)terrainTypeIndex); writer.Write((byte)(elevation + 127)); … }
由于我们更改了保存地图数据的方式,因此SaveLoadMenu.mapFileVersion
将其增加到4。 const int mapFileVersion = 4;
最后,对其进行更改,HexCell.Load
以使其从版本4文件加载的高度中减去127。 public void Load (BinaryReader reader, int header) { terrainTypeIndex = reader.ReadByte(); ShaderData.RefreshTerrain(this); elevation = reader.ReadByte(); if (header >= 4) { elevation -= 127; } … }
统一包装重新创建相同的地图
现在我们可以创建各种各样的地图。生成每个新结果时将是随机的。我们只能使用配置选项来控制卡的特性,而不能控制最准确的形式。但是有时我们需要再次重新创建完全相同的地图。例如,与朋友分享精美的地图,或者在手动编辑后重新开始。它在游戏开发过程中也很有用,所以让我们添加此功能。使用种子
为了使地图生成过程不可预测,我们使用Random.Range
和Random.value
。要再次获得相同的伪随机数字序列,您需要使用相同的种子值。在之前,我们已经采用了类似的方法HexMetrics.InitializeHashGrid
。它首先保存用特定种子值初始化的数字生成器的当前状态,然后恢复其原始状态。我们可以对使用相同的方法HexMapGenerator.GenerateMap
。我们可以再次记住旧状态,并在完成后将其还原,以免干扰使用的任何其他状态Random
。 public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; … Random.state = originalRandomState; }
接下来,我们需要提供用于生成最后一张卡片的种子。这是通过使用公共整数字段完成的。 public int seed;
显示种子。现在我们需要种子值来初始化Random
。要创建随机卡片,您需要使用随机种子。最简单的方法是使用任意种子值生成Random.Range
。为了不影响初始随机状态,我们需要在保存后执行此操作。 public void GenerateMap (int x, int z) { Random.State originalRandomState = Random.state; seed = Random.Range(0, int.MaxValue); Random.InitState(seed); … }
由于完成后我们将恢复随机状态,因此如果立即生成另一张卡,结果将获得相同的种子值。另外,我们不知道初始随机状态是如何初始化的。因此,尽管它可以作为任意起点,但是我们需要更多的东西才能在每次调用时将其随机化。有多种初始化随机数生成器的方法。在这种情况下,您可以简单地组合多个在很大范围内变化的任意值,也就是说,重新生成同一张卡的可能性很低。例如,我们使用系统时间的低32位(以周期表示)加上应用程序的当前运行时。使用按位异或运算将这些值组合在一起,以使结果不是很大。 seed = Random.Range(0, int.MaxValue); seed ^= (int)System.DateTime.Now.Ticks; seed ^= (int)Time.unscaledTime; Random.InitState(seed);
结果数字可能为负,这对于公共价值种子而言似乎不是很好。我们可以通过使用最大整数值的按位屏蔽来使它严格为正,这将重置符号位。 seed ^= (int)Time.unscaledTime; seed &= int.MaxValue; Random.InitState(seed);
可重复使用的种子
我们仍然会生成随机卡,但是现在我们可以看到每个卡使用了什么种子值。要再次重新创建相同的地图,我们必须命令生成器再次使用相同的种子值,而不是创建新的种子值。我们将通过使用布尔字段添加一个开关来做到这一点。 public bool useFixedSeed;
选择使用恒定种子。如果选择了常量种子,那么我们只需跳过中的生成新种子GenerateMap
。如果我们不手动更改种子字段,那么结果将再次是同一张地图。 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);
现在,我们可以复制所需地图的种子值并将其保存在某个位置,以便将来再次生成它。不要忘了,只有使用完全相同的生成器参数(即,相同的卡大小以及所有其他配置选项)时,我们才能获得同一张卡。即使这些概率发生很小的变化,也可以创建完全不同的地图。因此,除了种子之外,我们还需要记住所有设置。种子值为0和929396788的大型卡,标准参数。统一包装