HexMap学习笔记(九)——地形特征

作者:沈琰 2019-06-06

前言

这期内容为地形添加了一些装饰,但坦白说,使用默认形状拼接的效果并不太好看,大家可以用别的软件制作一些更精细的模型作为替代。

本期原文地址:HexMap9

这篇教程为HexMap系列的第九篇,这部分内容是为地形表面添加一些特征相关的东西,像是建筑和树木之类的。

树木,农地和城市建筑的交融

1添加地形特征功能支持

虽然地形的形状有变化,但幅度不大且看起来了无生气,要使其看起来有活力一些,需要添加一些类似树和建筑的地形特征。这些东西并不属于地形mesh部分,它们是独立的物体,但这不妨碍我们也在三角化地形时添加它们。

HexGridChunk同样也不关心mesh是如何工作的,它只是简单的令其中一个挂有HexMesh脚本的子物体添加三角形或是四边形,那同样也能添加另一个子物体来负责地形特征物的放置。

1.1地形特征管理器

创建一个脚本HexFeatureManager来负责单独一个地图块上地形特征物的管理。使用与HexMesh相同的运作方式,添加Clear()、Apply()和AddFeature()的方法。由于物体要放置在一个具体的地方,所以AddFeature方法里要传入一个坐标参数。

我们先不做任何实际的事,把代码结构搭建起来。


现在可以在HexGridChunk里添加对其的引用,并与其他HexMesh的子物体一样,在三角化处理时包含其方法。


从往每个单元格的中心添加地形特征物开始。


现在我们需要实际的管理器对象,添加另一个子物体到HexGridChunk的预制体中并挂载HexFeatureManager脚本,接着拖入脚本中关联起来。

特征物管理器,添加到chunk的预制体里

1.2地形特征物的预制体

应该创建何种类型的地形特征物?首次测试先使用默认的方块。创建一个较大的方块,设置缩放为(3,3,3),然后转换为预制体,再为其新建一个默认颜色为红色的材质球。删除其碰撞器,因为我们用不到。

特征物方块预制体

我们的地形特征管理器需要获取这个预制体的引用,向HexFeatureManager里添加一个,然后关联起来。坐标信息需要访问Transform组件获取,所以使用其作为引用类型。


将预制体拖入管理器的面板中

1.3实例化地形特征物

设置完成,可以开始添加地形特征物了,即简单的在HexFeatureManager.AddFeature里实例化预制体并设置位置。


特征物实例

现在地形会被方块填充,至少是一半的方块。由于Unity里的Cube坐标原点在方块的中心,所以底部半边的方块会处于地形表面之下,要把方块整个放在地形顶部就需要向上移动方块高度的一半。


位于地形的顶部

如果使用不同的mesh呢?

这个方法是针对Unity默认方块的,如果要使用自定义mesh,有一个比较好的办法是在建模时将坐标原点直接设置在底部,这样就再也不用去修正坐标了。

当然,别忘了单元格的坐标是被噪声扰动过的,所以地形特征物的坐标也需要如此处理一下,这样就去除了网格个规律性。



1.4删除地形特征物

每次地图块刷新时都会创建一个地形特征物,这意味着我们现在一直在同一个坐标上创建越来越多的地形特征物。所以为了防止重复,在地图块被清理时也要删除旧的地形特征物。

一个比较快的方法是创建一个容器物体,并把所有的地形特征物体都设置为其子物体。当Clear()被调用时就删除这个容器物体并创建一个新的,这个容器物体将是管理器的子物体。


一直在创建和销毁物体,效率不是会很低么?

可能感觉是这样的,但现在还没到考虑这个问题的时候。首先要关心的是地形特征物放置坐标的正确性,一旦解决了这个问题,而性能问题成为了瓶颈,接下来就能用相对聪明的方法来解决效率问题。那时可能会在HexFeatureManager.Apply方法里解决,不过那将是之后的教程了。不过就算保持现在这样,效率也没那么糟糕,别忘了我们已经把整个地图分块了。

2地形特征物的摆放

目前把地形特征物放在每个单元格中心的位置,这对于其他空着的单元格可行,但是对于包含河流、道路或者是水下的单元格,看起来就不太对。

特征物可以任意位置放置

所以在添加特征物之前先确认单元格是否干净。


限制放置位置

2.1每个方向一个特征物

每个单元格中只有一个特征物看起来不够,单元格内还有更多的空间。让我们在单元格每个方向上三角形的中心位置都添加一个特征物。

当知道当前方向不是河流部分时在另一个三角化方法里做这件事,但是仍要检测是否处于水下或者是否存在道路,但在这种情况下,只需要关心道路是否穿过当前方向。


特征物没有与河流毗邻

这就生成了更多的地形特征物,它们出现在道路的旁边但还是远离河流。要让特征物能顺着河流出现,我们还需要在TriangulateAdjacentToRiver里添加它们,这一次要求这部分单元格不在水下和不在道路的顶部。


同样也与河流毗邻了

我们能渲染这么多物体么?

更多的特征物会带来更多的drawCalls,但是Unity的动态批处理会帮我们摆脱这个问题。由于特征物都不大,它们的Mesh只会有很少的顶点,这使得它们中的许多可以在一次批处理中组合起来。但如果这会成为一个性能瓶颈,我们就会在之后处理。也可以使用实例化,这类似于使用许多小mesh时的动态批处理。

3特征物的样式

现在所有的地形特征物的朝向都是一样的,这看起来很死板,所以我们要设置随机朝向。


随机朝向

虽然这产生了很多样式类型,但是每当地图块刷新,特征物都会获得一个新的随机朝向。编辑特征物是不应该还要注意是否会让附近的特征物的朝向反复变化,所以我们要换一个方法。

我们有一张噪声纹理图,它是一直相同的,但是其包含的柏林梯度噪声是局部连贯的,这在我们扰动单元格顶点时是需要的,但对旋转来说就不需要连贯的值,所有的旋转都应该是等概率。所以需要的是一个非梯度随机值的纹理,并且不使用双线性过滤采样。这实际上就是一个散列值的网格表,梯度噪声纹理的基本形式。

3.1创建散列表

我们可以使用一个floats类型的数组创建散列表并用随机值填充,这样就不再需要纹理图了。在HexMetrics里添加它,长度设置成256乘256,应该大致够用。


通过数学公式生成的随机值会保持相同的结果,获得的结果值的序列取决于一个随机种子,它默认为当前时间,这就是为什么每次运行时得到的结果序列都不一样。

为了每次运行时特征物在相同位置的旋转相同,我们必须在初始化方法中添加一个seed参数。


现在已经初始化了随机数流,将始终得到相同的序列。因此所有在地图生成之后的其他随机事件也是一样的。可以在初始化随机数发生器之前保存它的状态来防止这种情况的发生。在初始化完成之后将恢复到原来的状态。


散列表的初始化由HexGrid完成,它同时也负责分配噪声纹理。所以在HexGrid.start里和HexGrid.Awake里都添加上初始化,并做一个检测确保不会过多生成。


公共的种子字段可以设置任何值,这里设置的1234。

选择一个随机种子

3.2使用散列表

在HexMetrics里添加采样方法来使用散列表,与噪声纹理的采样相同,使用XZ位置的坐标来获取相对的值。散列表的索引是通过把坐标值取整,再除以散列表的大小获得余数得到。


%是什么?

这是取模操作符,它计算的是除法中的余数,在我们的情况中是整数除法。举个例子,序列-4,-3,-2,-1,0,1,2,3,4分别对3取模,得到的结果就是-1,0,-2,-1,0,1,2,0,1。

当坐标为正值的时候没问题,但是如果坐标值为负时,得到的余数也是负的。可以通过为负值结果加上散列表尺寸来修正这个问题。


现在为每平方单位生成了一个不同的值,但是我们的特征物其实并没有这么密集,它们之间的坐标差距更大。所以在计算下标之前可以通过缩小位置坐标来放大散列表,每4乘4的单位取一个唯一随机值就够了。


现在回到HexFeatureManager.AddFeature里,使用坐标在散列表中采样一个值并用它来设置特征物的旋转。如此一来当我们编辑地形时,地貌物的朝向将保持静止。


3.3特征物的放置阈值

现在特征物的旋转变化明显,但是放置位置的模式依然不变,每个单元格都有7个特征物塞在一起。这里可以通过省略一些特征物来产生变化,通过访问另一个随机值来确认是否放置特征物。

所以现在需要两个哈希值而不是一个,把散列表的数组类型由float改为vector2也行,不过向量运算对于哈希值来说没有意义。所以为此创建一个特殊的结构体,所需要的就是两个float值,探后添加一个静态方法来创建两个随机值。


它不需要序列化么?

我们只在散列表中存储这些结构,散列表也是静态的,因此在重编译时不会被Unity序列化,所以这个结构体也不需要序列化。

在HexMetrics里修改代码,使用这个新的结构体。


现在HexFeatureManager.AddFeature里可以访问到两个哈希值,使用第一个来确认是否添加特征物或者跳过。当值大于等于0.5f的时候就直接跳出,这样大约能排除一半的特征物。第二个值就像之前一样用来决定特征物的旋转。


特征物出现概率平均减少了50%

4绘制地形特征物

我们来让特征物的出现位置可编辑,而不是像现在这样每个单元格都放一点。但是我们并不是来绘制单独的特征物,而是给没个单元格添加一个等级的属性,这个属性控制着地形特征物出现的可能性。其默认值为0,代表特征物体不会出现。

由于红色的方块实在不像是自然产生的地形特征物,所以就将其定义为建筑。它们代表着城市开发区域,添加城市等级的属性到HexCell里。


可能觉得需要确保水下的单元格城市等级为0,但这没有必要。因为已经在水下单元格三角化的时候跳过生成特征物的步骤了。而且之后可能添加一些处于水下的特征物,比如说码头或者船坞之类的。

4.1密度滑动条

在HexMapEditor里添加另一个滑动条组件,让城市等级可编辑。


添加另一个滑动条到UI上并与相应的方法相连,这里把它放在屏幕右边的一个新的UI面板上,防止左边的面板塞得太满。

最大等级设置成多少比较合适?这里还是使用4个等级,分别表示零,低,中,高密度的城市开发区。

表示城市密度的滑动条

4.2阈值调整

现在城市等级可以编辑了,使用这个值来确认是否需要放置地形特征物,为此需要将城市等级作为额外的参数添加到HexFeatureManager.AddFeature里.。这里我们更进一步,把单元格本身当做参数传递,这样在之后会更方便一些。

一个快速使用城市等级的方式是把它乘上0.25并使用这个值作为新的特征物出现阈值,这样一来城市等级每提高一级,地形特征物的出现概率就增加25%。


在HexGridChunk里传递单元格参数。


根据密度放置特征物

5多种地形特征物预制体

光是特征物的出现概率还不足以很明显的区别不同的城市等级,有些单元格里的建筑物数量甚至比预期都少。可以使用不同的预制体来表示不同城市等级,使区别更加明显。

把HexFeatureManager里的featurePerfab字段的类型改成预制体的数组,用城市等级减一的值作为数组的下标。


创建两个额外的特征物预制体并重命名来表示三种不同的城市等级。等级1是低密度,就使用标准尺寸的方块代表小平房。把等级2的预制体缩放设置为(1.5,2,1.5)表示更大一些的双层建筑。等级3则使用(2,5,2)的缩放表示大楼或高层建筑。

为每个城市等级使用不同的预制体

5.1混合预制体

我们不需要严格限制建筑物类型分离,可以像真实世界中的一样稍微混合一下。每个城市等级使用三个阈值表示每种建筑物的出现几率。

对于等级1,设置小平房的出现概率是40%,而其他两种建筑不会出现。所以这组阈值是(0.4,0,0)。

对于等级2,把40%概率出现的建筑类型由小平房改为双层建筑,并额外给予20%的概率出现小平房,大楼依然不会出现,这时的阈值组是(0.2,0.4,0)。

对于等级3,把双层建筑升级为大楼,小平房升级为双层建筑,再给予20%的概率出现小平房。

这里的想法是,随着城市等级的提高,会对现有建筑升级并有概率在空地上新建新的建筑。如果要替换现有建筑,就需要使用相同的哈希值范围。比如哈希值在0-0.4之间是1级小平房的话,那么在这个哈希值范围的建筑在城市等级为3时会变成大楼。

具体来说就是,在城市等级为3时,大楼的哈希值范围为0-0.4,双层建筑为0.4-0.6,小平房为0.6-0.8,如果从高到低来检测,等级3的阈值组就是(0.4,0.6,0.8)。以此类推等级2就变成(0,0.4,0.6),等级1为(0,0,0.4)。

在HexMetrics里用一个二维数组保存这些阈值组,然后新建一个获取特定等级阈值组的方法。由于我们只关心与特征物类型相关联的等级,所以这里忽略了等级0。


接着在HexFeatureManager里添加一个使用城市等级与哈希值来选择预制体的方法。当前城市等级大于0时使用其值减1作为阈值组数组的下标,然后循环遍历阈值组,直到其中一个超过阈值组的哈希值为止,这就表示我们找到了一个预制体,如果没找到就返回null。


这就需要我们重新对预制体的引用进行排列,所以它们是从高密度到低密度排列的。


使用新的方法去获取预制体,如果最后都没找到就跳出方法,否则就像之前一样实例化然后继续。


预制体混合

5.2每级的变体

现在建筑混合效果已经很不错了,但建筑本身依然只是三个不同样子。我们可以把预制体集合与每级城市密度关联起来增加更多的变化,然后在其中随机选择一个。这样就需要用到一个新的随机值,所以在HexHash里添加第三个随机值。


把HexFeatureManager.urbanPrefabs的类型改为数组型的数组,然后在PickPrefab方法中添加一个choice参数,使用它将嵌套数组与该数组的长度相乘并转换为整数,从而为该数组建立下标。


使用第二个哈希值,即b来做出这个选择,相应的旋转的哈希值由b变为c。


在继续往下之前,需要意识到一个问题。Random.value是有几率出现1这个数的,这样就会出现数组越界的问题。为确保这种问题不会发生,把最终得到的哈希值减小一些。


不幸地是Inspector里无法显示数组中的数组,所以我们也没办法在Inspector里用拖入的方式赋值。要解决这个问题就需要创建一个封装嵌套数组的可序列化结构,给它一个专用方法处理参数到索引的转换,并返回预制体。


在HexFeatureManager里使用这些封装结构替代嵌套数组。


现在可以为每个密度级别定义多种建筑物。因为它们是独立的,所以不需要在每个组里添加相同的数量。这里在每个组里都额外添加了一个较长较低的建筑变体。其缩放分别设置为(3.5,3,2),(2.75,1.5,1.5)和(1.75,1,1)。


6其他地形特征物类型

使用目前的设置,可以生成一些看起来还行的城市。但地形特征物并不仅仅包含建筑物,还有植物和农场之类的,让我们把这些也添加到HexCell中。它们并非只能单独存在,是可以混合在一起的。


当然也要在HexMapEditor里添加额外的滑动条组件。


添加到UI面板上并与相应的方法关联。

三个滑动条

HexFeatureManager里也需要添加新的容器。



这里也在每个密度等级给农场和植被设置了两个类型预制体,用的也都是默认的方块。其中农场使用的是浅绿色的材质球,植被使用的深绿色材质球。

农场的方块高度设置为0.1单位,来表示矩形的农田,其缩放分别为第三级(2.5,0.1,2.5)和(3.5,0.1,2),第二级(1.75,0.1,1.75)和(2.5,0.1,1.25),第一级则是(1,0.1,1)和(1.5,0.1,0.75)。

而植物的预制体代表高大的树木和大型灌木,第三级为(1.25,4.5,1.25)和(1.5,3,1.5)。第二级(0.75,3,0.75)和)(1,1.5,1),第一级为(0.5,1.5,0.5)和(0.75,1,0.75)。

6.1地形特征物的选择

每一种特征物类型都要给其自己的哈希值,所以它们有不同的生成模式,这样就可以混合这些特征物了。添加两个额外的值到HexHash中。


HexFeatureManager.PickPrefab现在使用不同的容器工作,添加一个参数以示区分。再一次修改预制体选择的哈希值为D,旋转的值为E。


之前AddFeature里只选择了一个建筑的预制体,现在我们有更多选择。在农田预制体里再选择一个,使用B作为其出现几率的哈希值,类型选择则是使用D。


最终要实例化的预制体是哪个?如果其中一个预制体为null,那么选择不言而喻。但当两个预制体都不为null时,使用哈希值较小的那个。


农场与城市的混合

接下来使用哈希值C对植被的预制体做相同操作。


但是这里不能简单的将代码复制粘贴,当我们最终的选择是农田而不是建筑时,接下来应该将植被的哈希值与农田的进行比较,而不是建筑的哈希。因此我得注意最终使用哪个哈希值,并与使用的那个进行比较。


所有特征物的混合

下一篇教程是Walls

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

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

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

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

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

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

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

商务合作 查看更多

编辑推荐 查看更多