Unity中的六边形图:第1-3部分

图片

来自翻译者:本文是有关从六边形创建地图的详细(共27个部分)系列教程的第一篇。 这是在教程的最后应该发生的事情。

第1-3部分:网格,颜色和像元高度

第4-7部分:颠簸,河流和道路

第8-11部分:水,地貌和城墙

第12-15部分:保存和加载,纹理,距离

第16-19部分:找到道路,队员,动画

第20-23部分:战争迷雾,地图研究,程序生成

第24-27部分:水循环,侵蚀,生物群落,圆柱图

第1部分:从六边形啮合


目录


  • 将正方形转换为六边形。
  • 三角剖分六边形网格。
  • 我们使用三次坐标。
  • 我们与网格单元进行交互。
  • 创建一个游戏内编辑器。

本教程是有关六角形卡系列的开始。 六边形网用于许多游戏中,尤其是在策略中,包括《奇迹年代3》,《文明5》和《无尽传奇》。 我们将从基础知识开始,逐步添加新功能,并最终创建基于六边形的复杂浮雕。

本教程假定您已经学习了以Procedural Grid开头的Mesh Basics系列。 它是在Unity 5.3.1上创建的。 该系列使用Unity的多个版本。 最后一部分是在Unity 2017.3.0p3上制作的。


六边形的简单映射。

关于六边形


为什么需要六角形? 如果我们需要网格,那么使用正方形是合乎逻辑的。 正方形确实很容易绘制和定位,但它们也有缺点。 先看一个网格的正方形,然后看它的邻居。


广场及其邻居。

该广场总共有八个邻居。 通过交叉正方形的边缘可以实现其中四个。 这些是水平和垂直邻居。 其他四个可以通过交叉正方形的角来实现。 这些是对角邻居。

相邻方形网格单元的中心之间的距离是多少? 如果边长为1,则对于水平和垂直邻居,答案为1。但是对于对角邻居,答案为√2。

两种邻居之间的差异导致困难。 如果我们使用离散运动,那么如何感知沿对角线的运动? 我应该允许它吗? 如何使外观更有机? 不同的游戏因其优点和缺点而使用不同的方法。 一种方法是根本不使用正方形网格,而是使用六边形。


六角形及其邻居。

与正方形不同,六边形没有八个,但有六个邻居。 所有这些邻居都与边相邻,没有角邻居。 也就是说,只有一种类型的邻居,这大大简化了。 当然,六边形网格比正方形更难构建,但是我们可以处理。

在开始之前,我们需要确定六边形的大小。 令边缘长度等于10个单位。 由于六边形由六个等边三角形的圆组成,因此从中心到任意角度的距离也是10。此值确定六边形单元的外半径。


六角形的外半径和内半径。

还有一个内半径,它是从中心到每个边缘的距离。 此参数很重要,因为邻居中心之间的距离等于此值乘以2。 内半径是  f r a c s q r t 3 2  从外半径开始,即在我们的情况下 5 小号q - [R 3  。 为了方便起见,将这些参数放在静态类中。

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

如何得出内半径值?
取六边形的六个三角形之一。 内半径等于该三角形的高度。 可以通过将三角形划分为两个正三角形来获得该高度,然后使用勾股定理。

因此,对于肋骨的长度 Ë 内半径为  sqrte2e/22= sqrt3e2/4=e sqrt3/2\约0.886e

如果已经在执行此操作,那么让我们确定六个角相对于单元中心的位置。 应当注意,有六种方法来定位六边形:向上带有一个尖锐的或平坦的侧面。 我们将把角落。 让我们从这个角度开始,然后顺时针添加其余部分。 将它们放置在XZ平面上,以使六边形位于地面上。


可能的方向。

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

统一包装

网格划分


要构建六边形网格,我们需要网格单元。 为此,创建HexCell组件。 现在,将其留空,因为我们尚未使用任何给定的单元格。

 using UnityEngine; public class HexCell : MonoBehaviour { } 

首先,请创建一个默认的平面对象,向其添加一个单元组件,然后将其全部转换为预制件。


使用平面作为六角形单元的预制件。

现在,让我们进入网络。 让我们用单元宽度,高度和预制的常见变量创建一个简单的组件。 然后将具有此组件的游戏对象添加到场景中。

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


六角网格对象。

让我们从创建规则的正方形网格开始,因为我们已经知道如何执行此操作。 让我们将单元格保存在数组中以便能够访问它们。

由于默认情况下飞机的尺寸为10 x 10单位,因此我们将每个像元移动此数量。

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


平面正方形网格。

这样我们得到了一个美丽的正方形单元格。 但是哪个单元格在哪里? 当然,这很容易验证,但是六角形有一些困难。 如果我们可以同时看到所有单元格的坐标,那将很方便。

坐标显示


通过选择GameObject / UI / Canvas将画布添加到场景中,并使它成为我们的网格对象的子代。 由于此画布仅供参考,因此我们将删除其raycaster组件。 您还可以删除已自动添加到场景中的事件系统对象,因为目前我们不需要它。

将“ 渲染模式”设置为“ 世界空间”,然后将其沿X轴旋转90度,以便画布覆盖网格。 将枢轴和位置设置为零。 给它一个小的垂直偏移,使其内容在顶部。 宽度和高度对我们而言并不重要,因为我们可以自行安排内容。 我们可以将值设置为0,以消除场景窗口中的大矩形。

最后,将“ 动态像素每单位”增加到10。因此,我们保证文本对象将使用足够的纹理分辨率。



六边形网格坐标的画布。

要显示坐标,请创建一个Text对象( GameObject / UI / Text )并将其转换为预制件。 将其锚点和枢轴居中,将大小设置为5 x15。文本的中心也应水平和垂直对齐。 将字体大小设置为4。最后,我们不想使用默认文本,也不会使用RTF 。 另外,对于我们而言,是否打开Raycast Target也无关紧要,因为对于我们的画布而言,仍然不需要。



预制单元标签。

现在我们需要告诉网格有关画布和预制件的信息。 using UnityEngine.UI;将其添加到脚本的using UnityEngine.UI; 以方便地访问UnityEngine.UI.Text类型。 预制标签需要共享变量,可以通过调用GetComponentInChildren找到画布。

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


连接预制标签。

连接标签的预制件后,我们可以创建其实例并显示单元的坐标。 在X和Z之间,插入换行符,使它们出现在单独的行上。

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


坐标显示。

六角位置


现在我们可以直观地识别每个单元格,让我们开始移动它们。 我们知道X方向上相邻六边形单元之间的距离等于内半径的两倍。 我们将使用它。 此外,到下一行像元的距离应比外半径大1.5倍。


相邻六边形的几何形状。

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


我们将六边形之间的距离应用为无偏移。

当然,六边形的序数行并不恰好位于另一个之上。 每行沿X轴偏移内半径的值。 可以通过将X的一半加Z,然后乘以内半径的两倍来获得该值。

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


正确放置六边形会形成菱形网格。

尽管这是将细胞放置在六边形的正确位置的方式,但是我们的网格现在填充菱形而不是矩形。 我们使用矩形网格会更自在,因此让我们让单元恢复运行。 这可以通过将偏移量的一部分移回来完成。 在第二行中,所有单元格都必须后退一步。 为此,我们需要在相乘之前减去Z的整数除以2的结果。

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


六边形在矩形区域中的位置。

统一包装

六边形渲染


正确放置单元格后,我们可以继续显示真实的六边形。 首先,我们需要摆脱平面,因此我们将从单元HexCell移除除HexCell之外的所有组件。


没有更多的飞机了。

像在“ 网格基础”教程中一样,我们使用一个网格来渲染整个网格。 但是,这一次我们不会预先设置所需的顶点和三角形的数量。 相反,我们将使用列表。

创建一个新的HexMesh组件来HexMesh我们的网格。 它将需要一个网格过滤器和渲染器,它具有一个网格以及顶点和三角形的列表。

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

使用此组件为此网格创建一个新的子对象。 他将自动收到一个网格渲染器,但不会为他分配任何材质。 因此,向其添加默认材料。



十六进制网格对象。

HexGrid现在HexGrid能够以他发现画布的相同方式来检索他的六角形网格。

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

唤醒网格后,应命令网格对其单元进行三角剖分。 我们需要确保这将在十六进制网格的Awake组件之后发生。 由于稍后会调用“ Start ”,因此请在此处插入适当的代码。

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

可以随时调用此HexMesh.Triangulate方法,即使以前已对单元格进行了三角剖分。 因此,我们应该从清理旧数据开始。 当遍历所有单元格时,我们分别对它们进行三角剖分。 完成此操作后,我们将生成的顶点和三角形分配给网格,并通过重新计算网格法线来完成。

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

由于六边形由三角形组成,因此让我们创建一个方便的方法来基于三个顶点的位置添加三角形。 它将仅按顺序添加顶点。 他还添加了这些顶点的索引以形成三角形。 在向其添加新顶点之前,第一个顶点的索引等于顶点列表的长度。 添加顶点时不要忘记这一点。

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

现在我们可以对细胞进行三角剖分了。 让我们从第一个三角形开始。 它的第一个峰在六边形的中心。 另外两个顶点是相对于中心的第一角度和第二角度。

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


每个单元格的第一个三角形。

这行得通,所以让我们绕过所有六个三角形。

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

峰可以共享吗?
是的,你可以。 实际上,我们可以做得更好,并且仅使用四个而不是六个三角形进行渲染。 但是放弃这一点,我们将简化我们的工作,这是正确的,因为在以下教程中,一切都会变得更加复杂。 在此阶段优化顶点和三角形将阻碍我们将来。

不幸的是,此过程将导致IndexOutOfRangeException 。 这是因为最后一个三角形试图获得第七个角,该角不存在。 当然,他应该回去并将其用作第一个角的最后一个顶点。 或者我们可以在HexMetrics.corners复制第一个角,以免超出边界。

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


六边形完全。

统一包装

六角坐标


现在,在六边形网格的背景下,让我们再次查看单元的坐标。 Z坐标看起来不错,而X坐标则呈锯齿状。 这是线偏移以覆盖矩形区域的副作用。


用高亮显示的零线偏移坐标。

当使用六边形时,这种偏移坐标不容易处理。 让我们添加一个结构HexCoordinates ,该结构可用于转换为另一个坐标系。 让我们使其可序列化,以便Unity可以存储它,并且它将在Play模式下进行重新编译。 我们还使用公共只读属性使这些坐标不可变。

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

添加静态方法以根据普通偏移坐标创建一组坐标。 现在,我们仅复制这些坐标。

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

我们还添加了方便的字符串转换方法。 默认情况下, ToString方法返回一个结构类型名称,这对我们不是很有用。 我们重新定义它,使其返回一行上的坐标。 我们还将添加一种用于在单独的行上显示坐标的方法,因为我们已经使用了这种方案。

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

现在,我们可以将很多坐标传递给HexCell组件。

 public class HexCell : MonoBehaviour { public HexCoordinates coordinates; } 

更改HexGrid.CreateCell以便它可以使用新坐标。

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

现在,让我们重做这些X坐标,以便它们沿直线轴对齐。 这可以通过取消水平移动来完成。 所得结果通常称为轴向坐标。

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



轴向坐标。

这个二维坐标系使我们能够顺序描述位移在四个方向上的运动。 但是,剩下的两个方向仍然需要特别注意。 这使我们意识到存在第三维。 实际上,如果我们水平翻转X的尺寸,则会得到Y的缺失尺寸。


出现Y测量。

由于X和Y的这些测量值互为镜像副本,因此,如果Z保持恒定,则它们的坐标相加总是得到相同的结果。 实际上,如果将所有三个坐标相加,则我们将始终为零。 如果增加一个坐标,则必须减少另一个坐标。 实际上,这为我们提供了六个可能的运动方向。 这种坐标通常称为三次坐标,因为它们是三维的,并且拓扑类似于立方体。

由于所有坐标的总和为零,因此我们始终可以从其他两个坐标中获取任何坐标。 由于我们已经存储了X和Z坐标,因此我们不需要存储Y坐标。
我们可以添加一个必要时对其求值的属性,并在字符串方法中使用它。

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


三次坐标。

检查员坐标


在播放模式下,选择一个网格单元。 原来,检查器没有显示其坐标,只显示了HexCell.coordinates前缀标签。


检查器不显示坐标。

尽管这没有什么大问题,但是显示坐标会很棒。 Unity不显示坐标,因为它们未标记为可序列化的字段。 要显示它们,必须为X和Z显式指定可序列化的字段。

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


现在显示X和Z坐标,但是可以更改它们。 我们不需要这样做,因为必须固定坐标。 它们在彼此下方显示也不太好。

我们可以做得更好:为HexCoordinates类型定义我们自己的属性抽屉。 创建一个HexCoordinatesDrawer脚本并将其粘贴到Editor文件夹中,因为该脚本仅适用于编辑器。

该类必须扩展UnityEditor.PropertyDrawer并且需要UnityEditor.CustomPropertyDrawer属性才能将其与合适的类型相关联。

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

属性抽屉使用OnGUI方法显示其内容。 此方法允许在屏幕矩形内绘制可序列化的属性数据及其所属字段的标签。

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

我们从属性中提取x和z的值,然后使用它们创建一组新的坐标。 然后使用HexCoordinates.ToString方法在所选位置绘制GUI标签。

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


没有前缀标签的坐标。

这将显示坐标,但是现在我们缺少字段名称。 这些名称通常使用EditorGUI.PrefixLabel方法呈现。 另外,它返回一个对齐的矩形,该矩形与该标签右侧的空间匹配。

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


带标签的坐标。

统一包装

触控单元


如果我们不能与六边形网格互动,那么它就不会很有趣。 最简单的交互是触摸单元格,因此让我们为其添加支持。 现在,我们只需要将此代码直接粘贴到HexGrid 。 当它开始工作时,我们将其移到另一个地方。

要触摸单元格,可以从鼠标光标的位置向场景中发射光线。 我们可以使用与“ 网格变形”教程中相同的方法。

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

到目前为止,代码没有执行任何操作。 我们需要向网格添加碰撞器,以使光束可以碰撞到物体。 因此,我们将给HexMesh对撞机网格HexMesh

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

三角剖分完成后,将网格分配给对撞机。

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

我们不能只使用盒对撞机吗?
我们可以,但是它与网格的轮廓不完全匹配。 是的,我们的网格不会长期保持平坦,但这是以后的教程的主题。

现在我们可以触摸网格了! 但是我们接触哪个单元格呢? 为了找出答案,我们需要将触摸位置转换为六边形的坐标。 这适用于HexCoordinates ,因此我们HexCoordinates声明它具有静态的FromPosition方法。

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

此方法将如何确定哪个坐标属于该位置? 我们可以通过将x除以六边形的水平宽度开始。 并且由于Y坐标是X坐标的镜像,因此负x给出y。

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

当然,如果Z为零,这将为我们提供正确的坐标。 沿Z移动时,我们必须再次移动。每两行,我们必须向左移动一个单位。

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

结果,我们的x和y值在每个像元的中心都是整数。因此,将它们四舍五入到最接近的整数,我们必须获取坐标。我们还计算Z,从而获得最终坐标。

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

结果看起来很有希望,但是这些坐标正确吗?仔细研究,您可能会发现有时我们得到的坐标之和不等于零!让我们打开通知以确保这确实发生了。

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

我们实际上收到了通知。我们如何解决这个错误?它仅在六边形之间的边缘旁边出现。即,四舍五入导致问题。哪个坐标沿错误的方向取整?我们离像元中心越远,则舍入越多。因此,合乎逻辑的是假设舍入的所有坐标都不正确。

然后的解决方案是删除具有最大舍入增量的坐标,然后从其他两个值重新创建它。但是由于我们只需要X和Z,我们就不必费心重新创建Y。

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

六边形着色页


现在我们可以触摸正确的单元格了,真正互动的时机已到。让我们更改进入的每个单元格的颜色。添加HexGrid默认单元格和受影响的单元格自定义颜色。

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


单元格颜色选择。

添加到HexCell常规颜色字段。

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

将其分配HexGrid.CreateCell为默认颜色。

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

我们还需要添加HexMesh颜色信息。

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

现在,在进行三角剖分时,必须将颜色数据添加到每个三角形。为此,我们将创建一个单独的方法。

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

回到HexGrid.TouchCell首先,将单元格的坐标转换为数组的相应索引。对于正方形网格,这将只是X加上Z乘以宽度,但是在我们的示例中,我们还必须添加一半Z的偏移量。然后,我们获取像元,更改其颜色,然后再次对网格进行三角剖分。

我们真的需要重新三角化整个网格吗?
, . . , , . .

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

尽管我们现在可以为细胞着色,但视觉变化尚不可见。这是因为默认情况下,着色器不使用顶点颜色。我们必须自己编写。创建一个新的默认着色器(Assets / Create / Shader / Default Surface Shader)。只需对其进行两项更改。首先,将颜色数据添加到其输入结构。其次,将反照率乘以该颜色。我们只对RGB通道感兴趣,因为材质是不透明的。

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

使用此着色器创建新的材质,然后使网格物体使用此材质。因此,将显示单元格的颜色。


有色细胞。

我得到奇怪的阴影伪影!
Unity . , , Z-. .

统一包装

地图编辑器


现在我们知道如何更改颜色了,让我们创建一个简单的游戏内编辑器。此功能不适用于功能HexGrid,因此我们将其TouchCell转换为带有附加颜色参数的常规方法。同时删除该字段touchedColor

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

创建一个组件,HexMapEditor然后将Update方法移至该组件HandleInput向其添加一个公共字段以引用六边形网格,颜色数组以及一个用于跟踪活动颜色的私有字段。最后,添加选择颜色的通用方法,并使其最初选择第一种颜色。

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

添加另一个画布,这次保留默认设置。向其添加组件HexMapEditor,定义几种颜色并将其连接到六边形网格。这次我们需要一个事件系统对象,它会自动再次创建。


四色六边形地图编辑器。

在画布上添加一个面板以存储颜色选择器(GameObject / UI / Panel)。添加她的切换组(“ 组件” /“ UI” /“切换组”)。将面板缩小,然后将其放在屏幕的一角。


带切换组的彩色面板。

现在,用每种颜色的开关(GameObject / UI / Toggle填充面板只要我们不费心创建复杂的UI,简单的手动配置就足够了。



每种颜色一个开关。

打开第一个开关。还应将切换开关组的所有开关部分都设置为一次只能选择其中之一。最后,将它们连接到SelectColor我们编辑器的方法可以使用On Value Changed事件的“ +” UI按钮完成此操作选择地图编辑器对象,然后从下拉列表中选择所需的方法。


第一开关。

此事件传递一个布尔参数,该参数确定每次更改开关是否打开。但是我们不在乎。相反,我们将必须手动传递与我们要使用的颜色索引相对应的整数参数。因此,将第一个开关的值保留为0,将第二个开关的值保留为1,依此类推。

何时调用switch事件方法?
. , , .

, , . , SelectColor . , .


有几种颜色的着色。

尽管用户界面确实可以工作,但是有一个令人讨厌的细节。要查看它,请移动面板,使其覆盖六边形的网格。选择新颜色时,我们还将为用户界面下的单元格着色。也就是说,我们正在同时与UI和网格进行交互。这是不受欢迎的行为。

可以通过询问事件系统是否确定光标在某个对象上的位置来解决此问题。由于她只知道UI对象,因此这将告诉我们我们正在与UI进行交互。因此,仅在不发生这种情况时,我们才需要自行处理输入。

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

统一包装

第2部分:混合单元格颜色


目录


  • 连接邻居。
  • 在三角形之间插入颜色。
  • 创建混合区域。
  • 简化几何。

在上一部分中,我们奠定了网格的基础,并增加了编辑单元格的功能。每个单元格都有自己的纯色,并且单元格边界处的颜色会发生巨大变化。在本教程中,我们将创建混合相邻单元格颜色的过渡区域。


单元之间的平滑过渡。

相邻单元格


在对单元格的颜色进行平滑之前,我们需要找出哪些单元格彼此相邻。每个单元有六个邻居,可以在基本点的方向上进行识别。我们将得到以下方向:东北,东部,东南,西南,西部和西北。让我们为它们创建一个枚举并将其粘贴到单独的脚本文件中。

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

什么是枚举?
enum , . . , . , .

enum . , integer . , - , integer.


六个邻居,六个方向。

要存储这些邻居,请添加到HexCell阵列中。尽管可以将其设为通用,但可以改为将其设为私有,并提供使用说明的方法访问。我们还使其可序列化,以便在重新编译时不会丢失键。

  [SerializeField] HexCell[] neighbors; 

我们是否需要存储与邻居的所有连接?
, . — , .

现在,邻居数组显示在检查器中。由于每个单元有六个邻居,因此对于我们的十六进制单元预制,我们设置阵列6的大小。


我们的预制件中可容纳六个邻居。

现在,让我们添加一个通用方法以使一个方向上的单元格相邻。由于方向值始终在0到5的范围内,因此我们不需要检查索引是否在数组内。

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

添加方法以指定邻居。

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

邻居的关系是双向的。因此,当在一个方向上设置邻居时,立即在相反方向上设置邻居是合乎逻辑的。

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


邻居朝相反的方向。

当然,这表明我们可以请求对方的指示。我们可以通过为创建扩展方法来实现HexDirection要获得相反的方向,您需要添加到原始方向3。但是,这仅适用于前三个方向,其余方向则必须减去3。

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

什么是扩展方法?
— , - . — , , , . this . , .

? , , , . ? — . , , .

邻居连接


我们可以在中初始化邻居链接HexGrid.CreateCell在从左到右逐行遍历单元格时,我们知道已经创建了哪些单元格。这些是我们可以连接的单元。

最简单的是E – W复合物。每行的第一个单元格都没有东邻。但是其他所有单元格都有它。这些邻居是在我们当前正在使用的单元之前创建的。因此,我们可以连接它们。


创建单元期间从E到W的连接。

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


东部和西部的邻国相连。

我们需要再创建两个双向连接。由于这些是网格的不同线之间的连接,因此我们只能与前一行进行通信。这意味着我们必须完全跳过第一行。

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

由于线条是锯齿形的,因此需要进行不同的处理。首先处理偶数行。由于此类行中的所有单元格在SE上都有一个邻居,因此我们可以将其连接到它。


从NW到SE的连接为偶数线。

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

z&1是做什么的?
&& — , & — . , . 1, 1. , 10101010 & 00001111 00001010 .

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

, , . 0, .

我们可以与SW上的邻居连接,除了每行的第一个没有它的单元格。


从NE到SW的偶数线连接。

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

奇数线遵循相同的逻辑,但镜像相同。完成此过程后,网格中的所有邻居均已连接。

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


所有邻居都已连接。

当然,并非每个小区都精确地连接到六个邻居。网格边界上的像元至少有两个且不超过五个。并且必须考虑到这一点。


每个单元的邻居。

统一包装

混色


混色会使每个单元的三角剖分复杂化。因此,让我们在单独的部分中分离三角剖分代码。由于现在有了方向,所以我们使用它们代替数字索引来指示零件。

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

现在,当我们使用方向时,将很方便地获取与方向的角度,而不执行到索引的转换。

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

为此,您需要添加HexMetrics两个静态方法。另外,这允许我们将角度数组设为私有。

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

三角形上的几种颜色


到目前为止,该方法HexMesh.AddTriangleColor只有一个color参数。它只能创建纯色三角形。让我们创建一个替代方案,为每个顶点支持单独的颜色。

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

现在我们可以开始混合颜色了!让我们首先简单地使用其他两个顶点的邻居颜色。

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

不幸的是,这导致NullReferenceException,因为边界上的像元没有六个邻居。如果没有邻居怎么办?让我们务实一点,并使用单元格本身作为替代。

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

操作员做什么?
null-coalescing operator. , a ?? ba != null ? a : b .

, - Unity . null . .


混合了颜色,但是做错了。

坐标标签到哪里去了?
, UI.

色彩平均


混色有效,但结果显然是错误的。六边形边缘的颜色应该是两个相邻单元格的平均值。

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


在肋骨上混合。
尽管我们在边缘上进行混合,但是仍然获得清晰的颜色边框。发生这种情况是因为六边形的每个顶点都被三个六边形共享。


三个邻居,四个颜色。

这意味着我们还需要考虑上一个方向和下一个方向的邻居。也就是说,我们在两组三个一组中获得四个颜色。

让我们添加HexDirectionExtensions两种添加方法,以方便地转换到上一个和下一个方向。

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

现在我们可以得到所有三个邻居并执行三向混合。

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


混合在角落。

因此,除了网格边界外,我们可以获得正确的颜色过渡。边界单元与丢失的邻居的颜色不一致,因此在这里我们仍然看到清晰的边界。但是,总的来说,我们当前的方法并不能取得良好的结果。我们需要更好的策略。

统一包装

混合区域


在六角形的整个表面上混合会导致模糊的混乱。我们无法清楚地看到单个单元格。通过仅在六边形的边缘附近混合可以大大改善结果。在这种情况下,六边形的内部区域将保留纯色。


带有混合区域的美分连续阴影。

与混合区域相比,固体区域的大小应为多少?不同的分布导致不同的结果。我们将这个区域定义为外半径的一部分。使其等于75%。这将使我们得出两个新指标,总计为100%。

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

通过创建此新的实体填充因子,我们可以编写方法来获取实体内部六边形的角度。

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

现在,对其进行更改HexMesh.Triangulate,以使其使用这些实心着色角度而不是原始角度。现在暂时保持颜色不变。

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


无边的实心六边形。

混合区域的三角剖分


我们需要填充通过减少三角形创建的空白空间。在每个方向上,此空间均为梯形形状。要覆盖它,可以使用四边形(quadangle)。因此,我们将创建添加四边形及其颜色的方法。


梯形肋。

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

我们对其进行重新制作HexMesh.Triangulate,以使三角形接收一种颜色,并且四边形在纯色和两个角度的颜色之间进行混合。

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


与梯形肋混合。

肋骨之间的桥梁


情况越来越好,但工作尚未完成。两个邻居之间的混合颜色被邻居单元污染。为避免这种情况,我们需要从梯形上切掉一些角并将其变成矩形。在那之后,他将在单元与它的邻居之间建立一座桥梁,在两侧留出空隙。


肋骨之间的桥梁。

我们可以找到新的位置v3v4,开始v1v2再沿桥直接移动到小区边缘。桥的位移将是多少?我们可以通过取两个相应角度之间的中点,然后对其应用混合系数来找到它。这样就可以了HexMetrics

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

回到HexMesh,现在添加一个AddQuadColor只需要两种颜色的选项将是合乎逻辑的

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

对其进行更改,Triangulate以便在邻居之间创建适当的混合桥。

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


正确绘制的带有角部空间的桥梁。

填补空白


现在,我们在三个单元的交界处形成了一个三角形间隙。我们通过切掉梯形的三角形边得到了这些间隙。让我们重新获得这些三角形。

首先,考虑连接到先前邻居的三角形。它的第一个顶点具有像元颜色。第二个峰的颜色将是三种颜色的混合。最后一个峰的颜色与桥中间的点相同。

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


几乎一切都准备就绪。

另一个三角形的工作方式相同,除了桥不接触第三个峰,而是接触第二个峰。

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


全彩。

现在我们有了漂亮的混合区域,可以提供任何大小。可以根据需要使边缘变模糊或变清晰。但是您会看到网格边界附近的融合仍然没有正确实现。再一次,我们将其留待以后,现在重点讨论另一个主题。

但是颜色之间的过渡仍然很难看
. . .

统一包装

肋骨融合


看一下我们网格的拓扑。在这里值得注意的是什么形式?如果您不注意边框,那么我们可以区分三种不同类型的表格。有六色六边形,二色矩形和三色三角形。所有这三种颜色都出现在三个单元的交界处。


三种视觉结构。

因此,每两个六角形由一个矩形桥连接。每三个六边形由一个三角形连接。但是,我们执行更复杂的三角剖分。现在,我们使用两个四边形而不是一个来连接一对六边形。为了连接三个六边形,我们使用六个三角形。这太多余了。另外,如果我们要直接连接到一个形状,则不需要任何颜色平均。因此,我们将能够以更少的复杂性,更少的工作和更少的三角形来实现。


比必要的要难。

为什么我们甚至需要这个?
, . , , . , , . , , .

直接桥接


现在,我们的肋骨之间的桥由两个四边形组成。为了将它们扩展到下一个六边形,我们需要将桥的长度加倍。这意味着我们不再需要平均两个角度HexMetrics.GetBridge。相反,我们只需将它们相加,然后乘以混合因子。

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


桥横跨整个长度并且彼此重叠。

桥现在在六边形之间创建直接连接。但是我们仍然为每个连接生成两个四边形,每个方向一个。也就是说,其中只有一个应该在两个单元之间建立桥梁。

让我们首先简化三角测量代码。我们将删除与边缘三角形和颜色混合相关的所有内容。然后移动将桥的四边形添加到新方法的代码。我们将前两个顶点传递给此方法,这样我们就不必重新计算它们。

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

现在我们可以轻松地限制化合物的三角剖分了。首先,我们仅在使用NE连接时才添加网桥。

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


桥接仅在NE方向上。

似乎我们可以通过仅在前三个方向(NE,E和SE)上对它们进行三角测量来覆盖所有化合物。

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


所有内部桥梁和边界桥梁。

我们介绍了两个相邻单元之间的所有连接。但是我们也有一些从牢房通往无处的桥梁。让我们摆脱它们,TriangulateConnection当邻居出去时出去。也就是说,我们不再需要用单元格本身替换丢失的邻居。

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


仅内部桥。

三角关节


现在我们需要再次缩小三角形间隙。让我们对连接到下一个邻居的三角形进行此操作。并且仅当邻居存在时才需要这样做。

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

第三个高峰的位置如何?我插入作为替换v2,但这显然是错误的。由于这些三角形的每个边都连接到桥梁,因此我们可以沿着桥梁走到下一个邻居来找到它。

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


我们将再次进行完全三角剖分。

完成了吗 还没有,因为现在我们正在创建重叠的三角形。由于这三个单元格具有一个共同的三角形连接,因此我们只需要为两个连接添加它们。因此,NE和E.会做。

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

统一包装

第3部分:高度


目录


  • 添加单元格高度。
  • 对斜坡进行三角剖分。
  • 插入壁架。
  • 结合壁架和悬崖。

在本部分的教程中,我们将添加对不同高度的支持,并在它们之间创建特殊的过渡。


高地和壁架。

单元高度


我们将地图分为覆盖平坦区域的单独单元。现在,我们将为每个单元格指定自己的高度。我们将使用离散高程级别将它们存储为中的整数字段HexCell

  public int elevation; 

随后的每个高度可以达到多少?我们可以使用任何值,因此我们将其设置为另一个常量HexMetrics我们将使用五个单元的步骤,以使过渡清晰可见。在实际游戏中,我会使用更小的步骤。

  public const float elevationStep = 5f; 

更换细胞


到目前为止,我们只能更改单元格的颜色,但现在可以更改其高度。因此,该方法对HexGrid.ColorCell我们来说还不够。另外,将来,我们可以添加其他单元格编辑选项,因此我们需要一种新方法。

重命名ColorCellGetCell,使其无需设置单元格的颜色,而是将单元格返回给定位置。由于此方法不会更改任何其他内容,因此我们需要立即对单元格进行三角剖分。

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

现在,编辑器将处理单元格更改。完成工作后,需要再次对网格进行三角测量。为此,添加一个常规方法HexGrid.Refresh

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

进行更改,HexMapEditor以便他可以使用新方法。让我们给他一个新方法EditCell,该方法将处理单元格的所有更改,之后将更新网格。

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

我们可以简单地通过为所需的单元格分配所需的高度级别来更改高度。

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

与颜色一样,我们需要一种用于设置活动高度水平的方法,该方法将与UI关联。要从高度间隔中选择值,我们使用滑块。由于滑块与float一起使用,因此我们的方法需要一个float类型的参数。我们将其转换为整数。

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

在画布上添加一个滑块(GameObject / Create / Slider)并将其放在颜色栏下方。我们将其从下到上垂直放置,以便在视觉上与高度相对应。我们将其限制为整数,并创建一个合适的间隔,例如从0到6。然后,将其On Value Changed事件附加Hex Map EditorSetElevation对象方法必须从动态列表中选择该方法,以便使用滑块的值来调用该方法。



高度滑块。

高度可视化


更改单元格时,我们现在同时设置颜色和高度。尽管在检查器中我们可以看到高度实际上发生了变化,但是三角剖分过程仍然忽略了它。

当我们改变高度时,改变单元的垂直局部位置就足够了。为了方便起见,让我们将该方法HexCell.elevation设为私有,并添加一个常规属性HexCell.Elevation

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

现在,我们可以在编辑高度时更改单元格的垂直位置。

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

当然,这需要对进行一些小的更改HexMapEditor.EditCell

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


不同高度的单元格。

网格对撞机是否会更改以适应新的高度?
Unity mesh collider null. , , null . . ( ) .

现在可以看到像元高度,但是有两个问题。首先。细胞标记消失在凸起的细胞下。其次,单元之间的连接忽略了高度。让我们修复它。

更改单元格标签的位置


当前,仅创建和放置单元格的UI标签一次,此后我们将其遗忘。要更新其垂直位置,我们需要对其进行跟踪。让我们为每个人提供HexCell指向RectTransform其UI标签链接,以便您以后可以对其进行更新。

  public RectTransform uiRect; 

最后分配它们HexGrid.CreateCell

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

现在,我们可以扩展该属性,HexCell.Elevation以便它也可以更改单元格UI的位置。由于六角形网格的画布是旋转的,因此标签需要沿Z轴沿负方向移动,而不是沿Y轴沿正方向移动。

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


带高度的标签。

创建斜坡


现在我们需要将平面单元连接转换为斜率。这是在中完成的HexMesh.TriangulateConnection对于边缘连接,我们需要重新定义桥另一端的高度。

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

对于角接缝,我们需要对通往下一个邻居的桥梁进行相同的处理。

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


考虑到高度的连接。

现在,我们已经支持不同高度的单元格,并且它们之间具有正确的倾斜接头。但是,我们不要就此止步。我们将使这些斜坡更加有趣。

统一包装

肋骨与壁架


笔直的斜坡看起来并不吸引人。我们可以通过添加步骤将它们分为几个步骤。游戏《无尽的传奇》中使用了这种方法。

例如,我们可以在每个坡度上插入两个壁架。结果,一个大坡度变成三个小坡度,在两个小坡度之间。为了对这种方案进行三角测量,我们将必须将每个连接分为五个阶段。


在斜坡上的两个壁架。

我们可以设置坡度HexMetrics的阶数,并据此计算阶数。

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

理想情况下,我们可以简单地沿坡度插值每个步骤。但是,这并不是完全无关紧要的,因为Y坐标只应在奇数阶段改变。否则,我们将无法获得平坦的壁架。让我们为此添加一个特殊的插值方法HexMetrics

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

如果我们知道插值步长的大小,则水平插值很简单。

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

两个值之间的插值如何工作?
ab tt 0, a 。 1, bt - 0 1, ab . : (1t)a+tb

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

要仅在奇数阶段更改Y,我们可以使用 小号ë p + 1 / 2 如果我们使用整数除法,那么它将把1、2、3、4系列变成1、1、2、2。

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

我们还添加一种用于为颜色插入壁架的方法。只需对它们进行插值,就像连接是平坦的一样。

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

三角剖分


随着边缘连接的三角剖分变得越来越复杂,我们从中删除了相应的代码HexMesh.TriangulateConnection并将其放在单独的方法中。在注释中,我将保存源代码以便将来参考。

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

让我们从该过程的第一步开始。我们将使用特殊的插值方法创建第一个四边形。在这种情况下,应创建一个短坡,比原始坡陡。

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


创建窗台的第一步。

现在,我们将立即进入最后一个阶段,跳过两者之间的所有内容。尽管到目前为止形状不规则,但这将完成边缘的连接。

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


创建窗台的最后一步。

可以通过循环添加中间步骤。在每个阶段,前两个顶点成为新的第一个顶点。颜色也一样。在计算了新的向量和颜色之后,添加了另一个四边形。

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


所有中间步骤。

现在,所有边缘关节都有两个壁架,或您在中指定的任何其他壁架HexMetrics.terracesPerSlope当然,在为角部连接创建壁架之前,我们将其留待以后使用。


边缘的所有接缝都有壁架。

统一包装

连接方式


将所有边缘连接点转换为壁架不是一个好主意。只有当高度差只有一个级别时,它们才会看起来不错。但是,两者之间的差异较大,它们之间的间隙较大,因此会形成狭窄的壁架,而且看起来并不漂亮。此外,我们不需要为所有关节创建壁架。

让我们对其进行形式化并定义三种类型的边:平面,坡度和悬崖。让我们为此创建一个枚举。

 public enum HexEdgeType { Flat, Slope, Cliff } 

如何确定我们正在处理的连接类型?为此,我们可以添加一个HexMetrics使用两个高度级别的方法。

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

如果高度相同,那么我们将有一个平坦的肋骨。

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

如果水平差等于一步,则这是一个斜率。上升还是下降都没有关系。在其他所有情况下,我们都会休息一下。

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

我们还添加一种便捷的方法HexCell.GetEdgeType来获取特定方向的单元格边缘类型。

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

我们是否不需要检查该方向上是否存在邻居?
, , . , NullReferenceException . , , - . , . .

, , , . - , NullReferenceException .

仅为斜坡创建壁架


现在我们可以确定连接的类型,我们可以决定是否插入壁架。进行更改,HexMesh.TriangulateConnection以便他仅为斜坡创建壁架。

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

在这一点上,我们可以取消注释先前注释掉的代码,以便它可以处理平面和剪裁。

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


仅在斜坡上创建台阶。

统一包装

壁架


角关节比边缘关节更复杂,因为它们不涉及两个单元,而是涉及三个单元。每个角都连接到三个边缘,可以是平面,斜坡或悬崖。因此,存在许多可能的配置。与肋骨关节一样,我们最好在HexMesh新方法中添加三角剖分。

我们的新方法将需要一个三角形的顶点和相连的单元。为了方便起见,让我们安排连接以了解哪个单元格具有最小的高度。之后,我们可以从左下和右下开始工作。


角接。

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

现在,我TriangulateConnection必须确定哪个单元格最低。首先,我们检查三角测量的单元格是否在其邻域以下或与最低单元格处于同一水平。如果是这样,那么我们可以将其用作最低的单元格。

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

如果最深层检查失败,则意味着下一个邻居是最低单元。为了正确定向,我们必须逆时针旋转三角形。

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

如果第一个测试失败,则需要比较两个相邻的单元格。如果肋骨的相邻位置最低,则需要顺时针旋转,否则-逆时针旋转。

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


逆时针旋转,不转,顺时针旋转。

边坡三角剖分


要知道如何对角度进行三角测量,我们需要了解要处理的边线类型。为了简化此任务,让我们添加HexCell另一种方便的方法来识别任意两个像元之间的斜率。

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

我们使用这种新方法HexMesh.TriangulateCorner来确定左边缘和右边缘的类型。

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

如果两个肋骨都是斜坡,那么我们的左右两边都有壁架。此外,由于下部单元格最低,因此我们知道这些斜率会上升。而且,左右单元具有相同的高度,即,上边缘的连接是平坦的。我们可以将这种情况指定为“斜面-斜面”或MTP。


两个斜率和一个平面SSP,

我们将检查是否处于这种情况,如果是,则将调用新方法TriangulateCornerTerraces之后,我们将从方法中返回。将此检查插入到旧的三角测量代码之前,以便它替换原始的三角形。

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

由于我们内部未做任何事情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 c3 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); Color c4 = HexMetrics.TerraceLerp(beginCell.color, rightCell.color, 1); AddTriangle(begin, v3, v4); AddTriangleColor(beginCell.color, c3, c4); } 


三角形的第一阶段。

我们再次直接进入最后一个阶段。这是形成梯形的四边形。与边缘连接的唯一区别是我们不是在处理两种颜色,而是在处理四种颜色。

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


四边形的最后一个阶段。

它们之间的所有阶段也是四边形。

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


所有阶段。

两种坡度变化


具有两个斜率的情况具有两个不同方向的变化,具体取决于哪个单元是底部。我们可以通过检查左右倾斜和左右倾斜的组合来找到它们。


ATP和MSS。

如果右边缘是平坦的,那么我们应该开始在左侧而不是底部创建壁架。如果左边缘是平坦的,则需要从右边开始。

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

因此,壁架将不间断地绕过这些单元,直到它们到达悬崖或地图的尽头。


坚固的壁架。

统一包装

合并斜坡和悬崖


连接斜坡和悬崖呢?如果我们知道左边缘是斜坡,右边缘是悬崖,那么上边缘将是什么?它不能平坦,但可以是斜坡或悬崖。




SOS和COO。

让我们添加一个新方法来处理所有倾斜悬崖的情况。

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

TriangulateCorner当左边缘为坡度时,应将其称为最后一个选项

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

我们如何对此进行三角剖分?此任务可以分为两个部分:较低和较高。

下半部


下部左侧有壁架,右侧有悬崖。我们需要以某种方式将它们结合起来。最简单的方法是挤压壁架,使其在右上角相遇。这将提高壁架。


壁架的压缩。

但是实际上,我们不希望他们在右下角见面,因为这会干扰上面可能存在的壁架。另外,我们可以处理非常高的悬崖,因此,我们会掉落得非常陡峭并且三角形变细。相反,我们将它们压缩到沿着悬崖的边界点。


边界压缩。

让我们将边界点定位在底部单元格上方的一层。您可以通过基于高度差的插值找到它。

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

为确保正确获取,我们用一个三角形覆盖了整个下部。

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


底部三角形。

将边界放置在正确的位置后,我们可以进行壁架的三角测量。让我们仅从第一阶段开始。

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


压缩的第一阶段。

这次,最后一个阶段也将是一个三角形。

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


压缩的最后阶段。

而且所有中间步骤也是三角形。

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


压缩壁架。

我们不能保持窗台水平吗?
, , , . . , . .

角完成


完成底部操作后,您可以转到顶部。如果上边缘是斜坡,那么我们将再次需要连接壁架和悬崖。因此,让我们将此代码移至单独的方法。

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

现在完成顶部将变得容易。如果我们有一个坡度,则添加边界的旋转三角形。否则,一个简单的三角形就足够了。

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



完全三角剖分这两个部分。

镜像案例


我们研究了“斜坡”的情况。还有两个镜盒,每个镜盒的左侧都有一个悬崖。


OSS和CCA。

我们将使用以前的方法,由于方向的变化会略有差异。我们将其复制TriangulateCornerTerracesCliff并相应地进行更改。

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

将这些案例添加到中TriangulateCorner

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



三角剖分的OSS和CCA。

双崖


剩下的唯一非平面情况是两侧都带有悬崖的下部单元。在这种情况下,上肋可以是任何-平的,倾斜的或悬崖的。我们只对“ cliff-cliff-slope”情况感兴趣,因为它只有壁架。

实际上,“悬崖-悬崖-坡度”有两种不同的版本,具体取决于哪一侧更高。它们是彼此的镜像。让我们将它们指定为OOSP和OOSL。




OOSP和OOSL。

我们可以TriangulateCorner通过调用方法TriangulateCornerCliffTerracesTriangulateCornerTerracesCliff使用不同的单元格旋转来涵盖这两种情况

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

但是,这产生了一个奇怪的三角剖分。这是因为现在我们正在从上到下进行三角剖分。因此,我们的边界被内插为负,这是不正确的。解决方案是始终使用正插值器。

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



三角化的OOSP和OOSL。

扫一扫


我们检查了所有需要特殊处理以确保壁架正确三角剖分的情况。


用壁架完成三角剖分。

我们可以通过TriangulateCorner删除运算符return并使用block来进行清理else

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

最后一块else覆盖所有尚未涉及的所有剩余案例。这些情况是RFP(平面-平面-平面),OOP,LLC和LLC。它们全部被一个三角形覆盖。


.

unitypackage

Source: https://habr.com/ru/post/zh-CN424257/


All Articles