HexMap学习笔记(三)——海拔高度与阶梯连接

作者:沈琰 2019-03-07

系列文章
HexMap学习笔记(一)——创建六边形网格
HexMap学习笔记(二)——单元格颜色混合
HexMap学习笔记(三)——海拔高度与阶梯连接

HexMap学习笔记(四)——不规则化
HexMap学习笔记(五)——更大的地图
HexMap学习笔记(六)——河流
HexMap学习笔记(七)——道路

HexMap学习笔记(八)——水体
HexMap学习笔记(九)——地形特征
HexMap学习笔记(十)——城墙
HexMap学习笔记(十一)——更多种地形特征物

前言

从这篇开始难度陡然提高了不少,不过虽然在三角剖分这块略微复杂一些,但也不算很难,主要就是计算繁琐。大家可以在纸上画一下方便理解。

本篇原文地址:Hex Map 3

本篇难度:★★☆☆☆

海拔高度与阶梯连接

此教程是HexMap的第三部分,这次会添加一个方法让单元格处于不同的海拔高度,并用一个特殊的方法去连接它们。

单元格的高度与阶梯化的连接

1.单元格的高度

我们之前在平坦的区域把地图分成了不同的六边形单元格,现在要给每个单元格加上高度变化。我们将用高度等级来表示,在HexCell里新建一个整数字段表示这个值。


高度等级每一级可以是任何值,在HexMetrics里另外定义一个常量表示。这里用5标准单位作为每层的高度值,变化看起来会比较明显,如果是实际游戏中可以适当减小每层高度。


1.1编辑单元格

之前我们只能编辑单元格的颜色,而现在要添加编辑高度的功能,所以现在HexGrid.ColorCell方法已经不够用了。而且考虑到以后可能给每个单元格添加更多的可编辑选项,这需要一个新的编辑方法。

把ColorCell重命名为GetCell,并把编辑颜色的功能变为返回一个指定位置的单元格。现在不再作任何改变操作,也不再直接三角化每个单元格。


现在由编辑器自己来修改单元格,之后在重新三角化一次。为此添加一个公共方法HexGrid.Refresh()去处理。


修改HexMapEditor里的代码让其调用新方法,添加一个EditCell的新方法负责单元格的编辑,然后刷新网格。


可以简单地为正在编辑的单元格分配一个选定的高度等级来调整单元格的海拔高度。


和颜色一样,需要添加一个方法去设置有效的高度等级,并与UI相关联。在这里用UI里的Slider组件去调整高度等级。由于Slider组件接收的是float类型的值,所以我们的方法也需要一个float类型的参数,然后再转换为整数。


在Canvas上添加一个Slider组件(GameObject/Create/Slider)并调整位置到调色板的下面。设置其为竖直方向,视觉上比较吻合调整高度的组件。限制它的取值上下范围在一个合理的区间,比如说0-6。接着把OnValueChanged事件与HexMapEditor里的SetElevation方法相连。

编辑高度的滑动条组件

1.2高度可视化

现在编辑单元格可以同时改变颜色和高度等级了,你可以在Insperctor里看到高度确实改变了,但是三角化时并未应用。

现在需要在高度变化时修改垂直方向的本地坐标,为了让这一步更简化一些,把HexCell.elevation设为私有类型同时添加一个HexCell.Elevation公共字段。


现在就能在编辑高度时修改垂直坐标。


当然别忘了修改HexMapEditor.EditCell里的赋值。


不同高度的单元格

MeshCollider是否能吻合新的高度地图?

旧版本的Unity需要在设置相同的Mesh信息之前先把MeshCollider设为null,它只是假设Mesh的数据不会改改变,所以只有不同的Mesh(或者null)能触发碰撞器的刷新,现在已经没这个必要了。所以我们现在的方法:在三角化之后重新分配碰撞器的Mesh信息,这是可行的。

现在单元格的高度就明显可见了,但是还有两个问题:

第一:单元格显示坐标的标签在被升高的单元格下面消失了。

第二:单元格的连接没有考虑到高度。

现在我们就来解决这两个问题。

1.3单元格坐标标签复位

就当前情况下,单元格的坐标UI标签只有在开始的时候创建和定位了.然后就没有管了。要更新标签的垂直坐标就得获取引用,所以给每个HexCell一个自己UI标签的RectTransform的引用,以便接下来去更新它。


在HexGrid.CreateCell结尾时给它们赋值。


现在可以扩展HexCell.Elevation的set方法,让其同时校准坐标UI标签的位置。因为Canvas组件之前我们旋转过,所以应该是在Z轴的负方向移动而不是Y轴。


坐标标签与单元格的高度同步

1.4创建倾斜连接

下面把平坦的连接变为倾斜的连接,这一步的方法已经在HexMesh.TriangulateConnection里完成了。在边界相连的情况下需要覆盖高度坐标到连接终点两端。


在角相连的情况下则需要对每个相连接的单元格做一样的操作。


不同高度单元格之间的连接

这样不同海拔高度之间的连接就完成了,可以看到所有的倾斜面都用正确的方式连接在一起,但这还不算完,继续让连接方式变得更有意思一些。

2.阶梯状的边界连接

笔直的倾斜连接看起来没什么意思,我们可以让它分成阶梯状的好几段。《无尽传说》就是其中一个这么做的游戏。

例如我们可以在一个倾斜面上插入两段阶梯,结果就是一个完整的大斜坡被分为三个小斜坡,中间用平坦的部分相连,这需要我们把连接分为五个部分来看。

一个斜坡中插入两段梯形

我们可以在HexMetrics里定义每个斜坡中插入梯形的数量,并由此计算出分成部分的数量。


理想情况下,我们可以简单地沿着斜率对每一步进行插值。这并不完全是不重要的,因为Y坐标只能在奇数步上变化,而不能在偶数步上变化。否则我们就得不到平坦的类似梯田的形状。让我们在HexMetrics中添加一个特殊的插值方法来解决这个问题。


水平方向的插值是笔直向前的,只要我们知道插值的步长就行。


两个值之间的插值是怎么计算的?

两个值 a 和 b 之间的插值需要第三个插入值 t ,当 t 是0,结果就是 a 。当 t 是1时结果就是 b 。当 t 在0和1之间的某个点时, a 和 b 是一个混合比例。所以插值的公式是 (1-t)a+tb

这个公式可以变成 (1-t)a+tb=a-ta+tb=a+t(b-a) 。插值的第三个值相当于向量 (b-a) 上从 a 到 b 的移动,这需要的乘法计算量更少。

为了只在奇数步修改Y的坐标,我们可以使用计算。如果我们只取商的整数部分,相当于把1,2,3,4的序列变为1,1,2,2。


同样添加颜色的阶梯插值计算方法,仅当连接处是平的时候计算插值。


2.1三角剖分

因为边界连接处的三角剖分更复杂了,所以把HexMesh.TriangulateConnection中的相关代码提取出来放到一个新方法中,把原先代码注释掉以便等会作为参考。


先处理第一步,用我们的特殊插值方法创建第一个四边形,会得到一个比原来斜率更陡的短斜坡。


阶梯化的第一步

现在直接跳过其他步骤到最后一步,尽管此时还不是正确形状,但先完成边界连接。


阶梯化的最后一步

中间跳过的步骤可以放在一个循环中,每一步中上一次计算的最后两个顶点作为这一步开始的两个顶点,颜色赋值也是同样的方法。这样一来新的向量和颜色就计算出来了,另外几个四边形也添加进去了。


单元格之间的所有阶梯化步骤

现在每个单元格的边界连接都有两次阶梯变换,或者你也可以在HexMetrice.TerracesPerSlope中修改变换的次数。当然,我们还没有对角落连接进行阶梯化处理,这部分放到后面来完成。

阶梯化连接所有的边界

3连接类型

把所有的边界连接都进行阶梯化处理似乎不太妥当,当两个相邻单元格之间的高度相差不大的情况下看起来还行,但差距较大时这样处理会产生一个狭长的阶梯状大跳跃,这不太好看。另外还有平坦连接情况下就更不需要进行阶梯化处理了。

我们把不同高度的连接情况抽象为三种类型:Flat(平坦),Slope(倾斜),Cliff(陡峭),并为此创建一个新的枚举类型。


为了确认是哪种连接类型,可以在HexMetrice里添加一个方法,基于连个高度等级去获取连接类型。


如果高度是一样的,那自然就是平坦的。


如果高度等级的差值刚好为1,就是倾斜类型。除此之外,无论是大于1还是小于-1,就只能是陡峭类型了。


同样也在HexCell里添加一个便于获取边界类型的方法。



是否需要在所有方向上都要检查存不存在相邻单元格?
你可能最终会在恰好位于地图边界上的方向获取边缘类型。在那种情况下是没有相邻单元格的,然后我们也会得到一个空引用异常。当然可以在方法内就检查这个,然后如果发生这种情况抛出某种异常。但是那种情况下,异常就已经发生了,所以这是多此一举,除非你想抛出一个自定义的异常类型。
需要明确的是,我们只会在我们知道不是处理地图边界的时候才会使用这个方法,如果在某些地方出错了,我们肯定会得到一个空引用异常。

3.1限制只让倾斜类型阶梯化

现在已经能确认,哪种连接类型才需要阶梯化。修改一下HexMesh.TriangulateConnection让它只在处理倾斜类型时候阶梯化。


这里可以重新启用之前注释的代码,负责平坦和陡峭类型的连接情况。


只有倾斜类型会进行阶梯化处理

4.角落连接的阶梯化

角落连接比边界连接更为复杂,因为它连接了三个不同单元格,而不是仅仅两个。每个角连接的三个边界类型可能是任意一种情况,所以可能性很多。因此最好另外添加一个角落专用的阶梯化方法到HexMesh里。

新的方法需要角落三角形的三个顶点和连接的单元格。为了便于管理,我们按顺序整理找到最低高度的单元格,然后从底部开始从左往右阶梯化。

单元格角落上的连接


现在TriangulateConnection里需要确认哪一个是高度最低的单元格。首先检查将被三角化的单元格是否高度低于其相邻单元格,或者就是最低的单元格。在这种情况下我们可以把它当做底部单元格使用。


如果最里面的检测是false,说明另一个相邻单元格是最低高度的单元格。这时需要逆时针转动三角形的参数到正确的方向。


如果第一次高度检测就是false,情况就变成了分辨出两个相邻单元格谁才是最低的。如果边界上的相邻单元格是最低的,就要顺时针旋转参数顺序,否则逆时针旋转。

注:这里删去底下原本直接添加三角形的代码

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

4.1倾斜连接的三角剖分

在去想如何三角化角落连接区域时,先需要知道处理的是哪种边界类型。为简化这一步,在HexCell里添加一个新方法,获取任意两个单元格连接方式。


在HexMesh.TriangulateCorner里使用新方法去确认左右的边界类型。


如果两边都是倾斜类型,就需要在左右两边都进行阶梯化。因为底部单元格是最低的,所以这些倾斜类型都是向上的。此外这也意味着左右两边单元格有相同的高度,所以上端的边界连接类型是平坦的。我们可以定义这种情况叫slope-slope-flat,或者简称SSF。

两个倾斜和一个陡峭类型,SSF

检测是否是这种情况,如果是就调用新方法TriangulateCornerTerraces,然后直接跳出方法。把检测代码放到默认的三角化代码之前,这样就会替代默认的的三角化。


只要没在TriangulateCornerTerraces()里做些什么,这个连接角就会变成一个空洞。是否会变成洞取决于哪个单元格最终成为底部单元格。

出现了一个洞

要填充这个洞我们得穿过缺口连接左右的阶梯,这与边界连接基本是一样的,除了内部添加的双色四边形变成了三色三角形。我们还是首先从第一个三角形开始。


第一步的三角形

再一次直接跳到最后一步,这是一个梯形状的四边形。与边界连接最后一步不同的是,这里四个顶点都是不同的颜色。


最后一步的四边形

在此之间的步骤添加的也是四边形。

所有的步骤

4.2双倾斜连接类型的变体

双倾斜连接类型在不同的朝向上有两个变体,取决于哪一个是底部单元格。我们可以通过检查单元格到左右两个相邻单元格的连接类型是不是倾斜-平坦,和平坦-倾斜类型来找到。


如果右边的边界连是平坦类型,我们就从左边开始阶梯化处理而不是底部。如果左边是平坦类型,就从右边开始。


这样一来阶梯就能没有中断的环绕每个单元格,除非碰到陡峭连接或者地图边界。

连续环绕的阶梯化连接

5.倾斜和陡峭类型的融合

所以当倾斜类型和陡峭类型之间该怎么连接起来?如果我们知道左边的边界连接类型是倾斜类型而右边是陡峭类型,那最上端的单元格的边界连接是什么类型?它肯定不可能是平坦的,但有可能是倾斜和陡峭类型中的任意一个。

SCS和SCC类型

添加一个新方法同时处理这两种情况。


它应当在左边边界连接是倾斜类型的情况下,在右边连接最后一个可能选项中被调用。


所以这块该如何三角化?这个问题要分成两个部分:底部和顶部。

5.1底部部分

底部部分的左边已经阶梯化了,而右边是直接连接的陡峭类型。我们要合并它们最简单的方法就是把阶梯化部分向右上角折叠,这样会让阶梯部分向上越来越细。

折叠阶梯化部分

但事实上并不想这样让它在右边的角落交汇,因为这样可能会干扰可能存在的顶部到其他单元格的阶梯化部分。并且在处理非常高的陡峭连接时,这么做还会产生非常高和薄的三角形,这样会很不好看。所以我们把它折叠到陡峭到倾斜连接的分界点上。

折叠到分界点上

把分界点定位在比底部单元格高一级的海拔高度上,我们可以根据海拔高度差的插值得到。


验证一下边界点是否正确,用一个三角形覆盖它们。


较低的三角形

分界点定位准确时就可以开始三角化阶梯部分了。还是从第一个三角形开始。


折叠的第一个步骤

这一次最后一个步骤也是一个三角形。


折叠的最后一个步骤

之间的步骤也是三角形。


阶梯化的折叠

不能保持阶梯的高度不变么?
通过在起始点和分界点之间插值计算我们的确能保持阶梯部分平坦,而不是总是使用边界点。这就需要使用倾斜四边形填充这个部分。但是这些倾斜四边形并不是在一个平坦的平面上,因为它们左右相连的边的斜率并不一样。最后结果看起来会很乱。

5.2完成相连角

底部部分就完成了,现在来看看顶部部分怎么处理。如果顶部单元格与相邻单元格的边缘连接时倾斜类型,我们就又得去连接阶梯化部分和陡峭部分。为了代码能复用,这里单独抽成一个方法,并把先前的代码移动到它自己的方法中。


现在完成顶部连接就简单许多了,如果顶部边缘连接时倾斜的,就改变参数顺序,添加旋转过的分界点三角形,否则就直接一个简单的三角形就足够了。


完成这两种情况的三角化

5.3镜像情况

现在我们已经解决了倾斜-陡峭情况的阶梯化合并.还有两种镜像的情况,这时它们陡峭连接的部分在左边。

方法和之前是一样的,只在方向问题上有些许不同。复制TriangulateCornerTerracesCliff并修改相应的部分,这里只标注了不同的地方。


在TriangulateCorner里去处理这些情况。


CSS和CSC的三角化也完成了

5.4两个陡峭连接类型的情况

剩余的没有平坦连接的情况就是底部单元格两边的边缘连接都是陡峭类型,这样一来顶部的两个相邻单元格的连接有可能是平坦,倾斜,陡峭中的任意情况。我们只对顶部连接是倾斜时这种情况感兴趣(C-C-S),只有这种情况下要处理连接角的阶梯化。

实际上有两个不同的CCS版本,根据哪边更高来决定,它们互为镜像。把这两种情况定义为CCSR和CCSL。

CCSR和CCSL

我们可以通过不同的参数顺序调用TriangulateCorner里先前写好的TriangulateCornerCliffTerraces和TriangulateCornerTerracesCliff这两个方法去解决这两种情况。


然而这会产生一个奇怪的三角剖分。发生这种情况是因为我们现在是从上至下在进行三角化,这样分界点的插值计算就得到了一个负值。解决方案是保证插值计算的正确性。

CCSR和CCSL三角化完成

5.5清理

现在能处理所有特殊的连接情况,并且所有的阶梯都有正确的三角剖分。

所有情况下的三角化都完成了

我们可以清理一下TriangulateCorner里的代码,用else来替代return。


最后一个else里包含了所有之前没提到的连接情况,分别是FFF,CCF,CCCR,CCCL。它们的角连接都可以用一个三角形表示。

所有一个三角形能表示的情况

下一篇教程是Irregularity。
本期工程地址tank1018702/Hex-Map-Learning
有想系统学习游戏开发的童鞋,欢迎访问http://levelpp.com/。


作者:沈琰
专栏地址:https://zhuanlan.zhihu.com/p/55235863



最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多