欢迎光临散文网 会员登陆 & 注册

HexMap学习笔记(十)——城墙

2019-05-27 17:34 作者:皮皮关做游戏  | 我要投稿

作者:沈琰


前言

第十篇的内容与上一篇从效果上来看差不多,都是增添一类地形特征。但从实现方式上来看,前者是扩展原有逻辑,后者则是嵌入原有逻辑,在此基础上修改。所以从难度上来说还是比上一篇高不少的。

本期原文地址:https://catlikecoding.com/unity/tutorials/hex-map/part-10/


这篇教程是HexMap系列的第十篇,这一次将在单元格之间添加城墙。

1 编辑城墙

要实现城墙编辑的功能,首先要知道把城墙放哪,这里把城墙放在单元格的边缘连接部分上。由于已经把其他地形特征物放在了单元格的内部区域,所以不用担心这两部分的物体会相互穿模干扰。

把城墙放置在边缘连接处

城墙也算是一种地形特征物,尽管比较大。所以像其他特征物一样我们不直接编辑城墙的位置,而是编辑单元格。并非放置单独的城墙段落,而是为整个单元格添加城墙。

1.1 标记单元格是否有城墙属性

在HexCell里添加个Walled属性来表示单元格是否被城墙包裹。用一个简单的toggle来控制,由于城墙是放置在单元格之间的,所以这里需要刷新包含自身在内的所有相邻单元格。

1.2 编辑Toggle

在HexMapEditor里添加一个toggle来控制单元格是否被城墙包围,同样添加一个方法设置这个状态。

与道路和河流不同的是,城墙不会从一个单元格穿过到另一个单元格,而是处于连接部分上。所以不用担心鼠标拖拽产生错误的结果。当城墙的Toggle是选中状态时就直接根据值来改变单元格的城墙状态。

复制一份之前有的UI面板,修改名字个调用方法使其控制单元格的城墙状态,这里把这个新UI面板跟其它地形特征放在一起。

城墙的toggle


2 构筑城墙

由于城墙是根据单元格的轮廓放置的,它没有固定形状,所以不能像其他地形特征物一样用一个简单的预制体来保存。相应的我们需要一个相对于地形的mesh结构,这表示我们的地图块预制体需要添加另一个HexMesh子物体。复制一份其他的子物体,并确保其开启了阴影投射。它不需要任何除顶点和三角形之外的数据,所以HexMesh上的其他选项全都不勾选。

城墙的预制体子物体

城墙是属于城市的地形特征物,所以这里使用城市的材质球。

2.1 城墙管理

由于城墙也是地形特征物,所以它是由HexFeatureManager负责管理。在脚本里添加城墙对象的引用,并调用其Clear和Apply方法。

城墙不应该是地形特征物的子物体么?
当然也可以这么设置,但并不是必要的。因为在预制体的层级界面中只会显示根物体的直接子物体,所以这里选择把城墙设置为HexGridChunk的子物体,为的是方便查看。

现在要在管理器中添加方法去构筑城墙。由于城墙处于单元格的边缘连接处上,所以它需要知道相关的连接处顶点和单元格。HexGridChunk会通过TriangulateConnection调用这个方法,这时单元格才开始三角化,参数里的另一个单元格是其相邻单元格。从这里来看,当前单元格为较近的单元格,而另一个单元格为较远的。

在连接工作完成后,处理角落三角形之前这个时间点,在HexGridChunk.TriangualteConnection里调用这个新方法,然后交由HexFeatureManager来最终决定是否确实需要放置城墙。

2.2 构筑单段城墙

整个城墙会如蛇形蜿蜒一般穿过多个单元格的连接处,段段连接处中就有城墙里的单独一段。从相邻单元格的角度来看,这段城墙从边缘连接处的左边开始到右边结束。在HexFeatureManager里创建一个单独的方法,基于边缘连接处的四个顶点来构筑城墙段。

较近端和较远端

AddWall方法里可以通过边缘连接处的第一个和最后一个顶点来调用这个方法。不过城墙应该只能在有城墙和没城墙的两个单元格之间才能添加,不用考虑谁在外面谁在里面,只要状态不同就行。

现在就可以编辑城墙了,它会显示为一个四边形条状。但是你不是总能看到城墙,每个四边形只有一边可见并且可见的面是朝向被标记为有城墙的单元格。

单边可见的四边形

通过添加向外朝向的四边形来快速解决这个问题。

两边都可见的城墙

现在整段城墙都是可见的了,尽管在三个单元格交界处是空缺的。稍后就会填充这部分。

2.3 城墙加厚

虽然现在城墙两面可见,但是它们没有厚度。现在的城墙薄的像纸一样,使其在某些角度几乎看不见,现在我们来增加城墙的厚度来使其更像实体。在HexMetrics里定义城墙的厚度,这里定义厚度为0.75,一个比较合适的量。

偏移之后的城墙

现在四边形的位置就被移动了,尽管不太明显,但从影子上还是看的出来。

城墙的厚度是一致的么?
如果远近的偏移向量都指向一个方向,那就是的。由于单元格周围呈曲线,情况显然并非如此.这些点的向量要么彼此远离,要么指向对方.因此城墙段的顶点移动是基于一个梯形而不是矩形,最终的结果会比我们之前设置的值要薄一些.此外,由于单元格受到噪声图的扰动,向量之间的角度会发生变化,导致厚度不均匀,我们之后会对其进行改进.

2.4 填充城墙顶部

要让城墙的厚度从上可见就需要在城墙顶部添加一个四边形,最简单的办法就是记录第一个四边形的顶部两个顶点并与第二个四边形的顶部顶点相连,组成一个新的四边形。

封顶之后的城墙


2.5 角落连接处的城墙

城墙剩余的空缺部分在单元格的角落连接处,所以现在要这个部分添加城墙。每个角连接处连接着三个单元格,而每个单元格都可以是有或者没有城墙的状态,所以这就产生了8种可能的结构。

我们只在不同城墙状态的单元格之间摆放城墙,所以排除不用放置城墙的情况,这样相关的结构就减少到了6个。这些结构中都有一个单元格在城墙曲线的内侧,我们把这个单元格看做是使城墙弯曲的轴心,然后从左往右构筑城墙。

新建一个AddWallSegment方法,以相关联的三个单元格与三角形的三个顶点为参数。尽管我们可以直接就在这个方法里三角化,但其实这个方法是另一个AddWallSegment方法的特殊情况。作为轴心的单元格同时扮演两个较近顶点的角色。

下一步新建一个AddWall方法的变体,传入三个单元格与三个顶点作为参数。这个方法的作用是在六种可能的情况中找出哪一个单元格是轴心。因此它要检测所有8种情况,并在其中的六种中调用AddWallSegment方法。

在HexGridChunk.TriangulateCorner方法的最后调用这个方法来添加这段城墙。

角落上的城墙也添加了,但还是有缝隙

2.6 填充缝隙

现在城墙上还有缝隙,这是因为每段城墙的高度不一致。沿着边缘连接处的城墙有固定高度,而角上的城墙位于两个不同的边缘连接处之间。因为每条边都有不同的高度,所以角上就会出现间隙。

要解决这个问题就要修改AddWallSegment方法,使其左右顶部顶点的Y坐标分开。

闭合的城墙

现在城墙是闭合了,但是好像还能在阴影中看到缝隙,这是由于方向光阴影设置里的法线偏移在搞鬼。当其值大于0时,三角形的阴影投射会向着表面法线方向偏移。这是为了防止自投影的现象,但也同时在三角形的朝向位置之间产生间隙。这就会在较薄的几何图形上产生可见的阴影间隙,就像我们的城墙一样。

我们可以通过把法线偏移减少到零来解决这个问题,或者也可以修改城墙mesh渲染器的的阴影投射模式为Two Sided,这样就使阴影投射会同时渲染两边的阴影从而覆盖间隙。

不再有阴影间隙了

3 城墙阶梯化

现在的城墙是相对笔直的,这在平坦的地形上看着没问题,但在于阶梯连接处贴合时就显得很奇怪。在单元格有一级的高度差的城墙两侧就会出现这种情况。

笔直的城墙处于阶梯连接处上的样子

3.1 城墙与边缘连接处的贴合

我们的解决方法是在边缘的每一个分段处都构建一段城墙,而不是只用一段城墙覆盖整个单元格边缘连接处。在边缘处使用的AddWallSegment方法里调用4次AddWall方法。

扭曲的城墙

现在城墙的形状与单元格个边缘相吻合了,看起来比之前好多了。这同样在平坦的地形上产生了更多城墙的变化。

3.2 城墙与地面的贴合

仔细观察阶梯连接处上的城墙,发现了一个问题,城墙是漂浮在地面上的。对于倾斜的平面其实城墙也是漂浮的,不过那种情况下通常不太明显。

浮动的城墙

要解决这个问题最简单的方法就是把城墙往下移动,使城墙在地形较高的那边陷到地形之下,得到我们想要的效果。

要把城墙下移首先是要确认城墙较近边还是较远边的高度更低。我们可以使用较低边的单元格高度作为修正高度,但其实不必这么低。可以把高低边的Y坐标进行0.5的插值算出偏移高度。由于我们的城墙在极少情况下厚度会超出阶梯连接处每格的宽度,所以可以直接用阶梯每格的垂直高度作为城墙的偏移。如果城墙的厚度不同,偏移值可能就需要修改一下。

下降城墙的位置

我们在HexMetrics里添加一个WallLerp方法来负责计算这个插值,它基于TerraceLerp方法并额外对远近顶点的X和Z坐标求平均值。

在HexFeatureManager里使用这个方法确认左右顶点。

与地面贴合的城墙

3.3 城墙顶点扰动修正

现在城墙在不同高度的单元格之间的显示正常了,但还是不能精确吻合扰动顶点后的边缘连接处,尽管由于城墙封闭的原因不大看得出来。这是由于我们是先计算的城墙顶点,后进行的顶点扰动。当这些顶点位于近远边缘顶点之间时,再进行扰动的结果就会有些微不同。

城墙没有完全吻合边缘不是问题,但对城墙的顶点扰动会影响其相对均匀厚度。如果我们使用扰动后的顶点来定位城墙,然后添加非扰动的四边形,城墙的厚度就应该不会有太大变化。

未扰动的城墙顶点

使用这种方法后,城墙不再像以前一样贴合着阶梯连接处了。但反过来看,城墙的锯齿感觉更少了,厚度也更加一致了。

更加一致的城墙厚度

4 城墙开口

目前我们没有考虑河流或者道路穿过城墙的情况,当这种情况发生时,应该在城墙上开一个缺口,这样河流和道路就可以通过了。

向AddWall方法内添加两个布尔参数来表示是否有河流或者道路穿过边缘。虽然我们也可以用不同的方法处理,但现在在这两种情况下都只用删除中间的部分的两段城墙,更方便一些。

现在HexGridChunk.TriangulateConnection方法来提供相应的参数。由于之前这个方法里已经用到了,所以我们将这些参数缓存成布尔值,并在调用时传递。

城墙上的开口,使得道路和河流能够通过

4.1 城墙封边

城墙的末端出现了新的缺口,我们现在要用四边形封住这些缺口。为此在HexFeatureManager中新建一个AddWallCap方法,它的工作原理类似于AddWallSegment,但它只需要一对近-远处顶点,并用其添加一个四边形。

当在AddWall里确认需要一个封边时,在第二和第四组顶点之间添加一个四边形封盖。别忘了调换第四组顶点的参数顺序,否则这个四边形将是从里可见的。

闭合城墙的边缘
地图边缘的缺口怎么解决? 
你也可以额外写点代码在那里盖上城墙。就我个人而言,我会避免把墙放在地图的边缘。通常也不会希望游戏玩法发生在离地图边缘太近的地方。

5 让城墙避开悬崖和水体

最后来考虑一下悬崖和水体上的单元格边缘连接处。悬崖实际上就是巨大的墙,所以在上面再建一堵城墙也没什么意义,并且看起来很难看。此外水下的城墙也没有意义,把海岸线围起来也不太好看。

水里和悬崖上的城墙

我们可以在AddWall方法中通过额外的检测来清除这些不合适的城墙。当两个单元格都不在水下,和他们共享的边缘连接处不是悬崖时。

删除了所有相关的城墙段,但角落连接处的位置还没处理

5.1 清除角落连接处的城墙段

清除角上的城墙需要更多步骤。最容易想到的情况是当轴心上的单元格在水下时,这时就没有任何相邻单元格的角上需要连接。

水下的轴心单元格处理完了

现在再来看其余两个单元格。如果其中一个在水下或者通过悬崖与轴心上的单元格相连,那么沿着这条边上就不会有城墙。当其中至少有一个成立时,那么这三个单元格的角连接处就不会有城墙段。

使用两个bool变量缓存左边和右边的城墙是否存在的结果,这样推理起来更清晰。

所有不正确的角落连接处城墙就处理完了


5.2 角落连接处的城墙封边

当左右两边的单元格上都没有城墙时就算完成了,但如果其中一个方向上有城墙就意味着城墙上会新开一个缺口,所以我们得封住它。

再一次封住边缘

5.3 城墙与相邻悬崖的融合

现在有一种情况下我们的城墙看起来不太对,就是当城墙处于悬崖底部时。因为悬崖不是完全垂直的,所以城墙和悬崖之间留下了一个很窄的缝隙,这个问题当城墙处于悬崖顶部上是不存在的。

城墙与悬崖朝向的地方有缺口

如果城墙一直延伸到悬崖边且不留缝隙看起来就更好一些。我们可以通过在当前城墙的末端和悬崖所在单元格的角落连接处之间添加一段额外的城墙来实现这一点。由于悬崖这边的城墙大部分都是隐藏在悬崖里的,因此可以把这一段的悬崖边上的城墙厚度减少到零,只需要创建一个楔形--两个四边形合于一个点上,一个三角形在上面。为此新建一个AddWallWedge方法,大部分都可以复制AddWallCap方法,然后加入楔形点的参数。这里已经把代码不同的位置标注出来了。

在角落连接处的AddWallSegment方法中,当只有一个方向有城墙,而且城墙所在单元格的海拔高度低于另一边时就调用此方法。那时就是遇到悬崖的时候。

城墙的边缘与悬崖相连了

下一篇教程是:https://catlikecoding.com/unity/tutorials/hex-map/part-11/

本期工程地址:https://link.zhihu.com/?target=https%3A//github.com/tank1018702/Hex-Map-Learning/tree/TerrainFeatures


有意向学习游戏开发线下课程的童鞋,欢迎访问levelpp.com/

HexMap学习笔记(十)——城墙的评论 (共 条)

分享到微博请遵守国家法律