游戏开发论坛

 找回密码
 立即注册
搜索
查看: 2299|回复: 0

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

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2019-6-14 13:48:11 | 显示全部楼层 |阅读模式
v2-924d070cd67c3baccf02a0284ef9cdc4_1200x500.jpg


前言

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

本期原文地址:HexMap 10

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

image001.jpg

1编辑城墙

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

image003.jpg
把城墙放置在边缘连接处

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

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

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

image004.jpg


1.2编辑Toggle

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

image005.jpg

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

image006.jpg

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

image007.jpg
城墙的toggle

2构筑城墙

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

image008.jpg
城墙的预制体子物体

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

2.1城墙管理

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

image009.png

image010.jpg
城墙与特征物管理器建立引用连接

城墙不应该是地形特征物的子物体么?

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

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

image011.jpg

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

image012.jpg

2.2构筑单段城墙

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

image013.png

image014.jpg
较近端和较远端

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

image015.jpg

最简单的城墙就是一个立在连接处之间的四边形,通过插值计算连接处近远顶点获取其底部顶点。

image016.png

城墙应该是多高比较合适?这里设置为与单元格海拔高度的一级相同,在HexMertics里定义。

image017.png

在HexFeatureManager.AddWallSegment方法里使用这个高度推算出第三和第四个顶点,然后添加到城墙的mesh中。

image018.jpg

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

image019.jpg
单边可见的四边形

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

image020.png

image021.jpg
两边都可见的城墙

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

2.3城墙加厚

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

image022.png

要加厚城墙,就要让两个四边形在相反的方向分开,通过far,near这两个值来修正向量。如果要确保墙顶平坦,就需要把Y坐标置零。因为在近远点两个地方都要使用这个方法,所以在HexMetrics添加一个方法来计算向量偏移。

image023.jpg

要城墙在连接处的中心,实际上城墙移动的距离应该是厚度的一半。为确保按预期的移动,还要在乘半之前先将向量标准化。

image024.png

在HexFeatureManager.AddWallSegment里使用这个方法修改四边形的顶点坐标,由于偏移向量是从近到远,所以较近处的四边形减去这个向量,而较远处的四边形加上这个向量。

image025.jpg

image026.jpg
偏移之后的城墙

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

城墙的厚度是一致的么?

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

2.4填充城墙顶部

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

image027.jpg

image028.jpg
封顶之后的城墙

2.5角落连接处的城墙

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

image029.jpg

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

image031.jpg

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

image032.jpg

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

image033.jpg

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

image034.jpg

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

2.6填充缝隙

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

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

image036.jpg

image037.jpg
闭合的城墙

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

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

image038.jpg
不再有阴影间隙了

3城墙阶梯化

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

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

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

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

image040.jpg

image041.jpg
扭曲的城墙

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

3.2城墙与地面的贴合

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

image042.jpg
浮动的城墙

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

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

image043.jpg
下降城墙的位置

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

image044.jpg

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

image045.jpg

image046.jpg
与地面贴合的城墙

3.3城墙顶点扰动修正

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

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

image047.jpg

image048.jpg
未扰动的城墙顶点

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

image049.jpg
更加一致的城墙厚度

4城墙开口

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

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

image050.jpg

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

image051.jpg

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

4.1城墙封边

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

image053.jpg

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

image054.jpg

image055.jpg
闭合城墙的边缘

地图边缘的缺口怎么解决?

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

5让城墙避开悬崖和水体

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

image057.jpg
水里和悬崖上的城墙

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

image058.jpg

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

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

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

image061.jpg

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

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

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

image064.jpg

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

5.2角落连接处的城墙封边

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

image067.jpg

image069.jpg
再一次封住边缘

5.3城墙与相邻悬崖的融合

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

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

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

image071.jpg

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

image072.jpg

image073.jpg
城墙的边缘与悬崖相连了

下一篇教程是More Features.

本期工程地址:Hex-Map-Learning/tree/TerrainFeatures

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

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

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

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

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

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-12-27 03:07

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表