程序化随机多边形地图生成

字数 2190阅读 2618

前言

最近跟团队想要开发一个开放世界的游戏,这是很有趣的游戏概念,然而参考了《塞尔达传说 荒野之息》的设定后发现,这个游戏的成功很大程度是美工和设计大量的工作,才形成了这个很有趣的大陆,然而我们的团队没有办法手工搭建出这么大的场景,于是我萌生了一个很自然的想法:写一个自动生成还算合理的地形的脚本。

起初我尝试了Perlin噪声直接生成高度图的做法,然而产生的地形太过极端,脚本的开发也进入了僵局,直到我看到了一篇2010年的文章:
原文地址:
http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation

这篇文章是在flash上实现了一个自动随机多边形地图的demo,我跟着这篇文章的思路,基本上复制了一个unity3D的版本,以后可能会再写出一个unreal的版本。

源码github地址

效果图

demo

基础多边形细胞图

既然要产生一个多边形为基础的地图,那么第一步当然是产生一个由多边形填满的二维图,术语上这种图形叫做维诺图(Voronoi Diagram),我更喜欢叫他细胞图。

Voronoi

这种图形的生成原理网上有很多,我参考了这篇文章:

http://blog.csdn.net/k346k346/article/details/52244123

需要注意的是,采用这种方法生成出的图形太过随机,我们需要让他看起来齐整一些,这采用了叫做Lloyd的放松算法,这是一种聚类算法,简单来讲就是经过数次迭代,每次迭代将维诺图的中心点移动到所在多边形的中心,重新计算维诺图。实际实验后发现2次迭代已经完全能达到效果。

数据结构

源代码中的VoronoiElement文件里保存了数据结构,大部分都具有注释,需要解释的Center类代表了维诺图的中心点,也就是Delaunay三角网的顶点,而Corner类代表三角形的外心,也就是图中多边形的交点。这样做的目的事实上产生了两张图的信息,三角图 和多边形图,后期开发时三角图可以用作寻路而多边形图主要用于渲染。

区分陆地和水

第二步我们要为我们的地图添加大陆和水的区别,自然界中的地表水的形成是有很多因素的,然而我们模拟时只能采用随机模拟。

原文中给出了很多模拟的思路,我才用了柏林噪声的方式进行模拟,首先为地图上每一个Corner点计算一个Perlin噪声值,设定一定的参数决定这个噪声值是否能使这个Corner具有水的属性。我才用的方式是比较(PerlinNoise)与(waterScale + waterScale * Distance(地图中心,该点))大小,这样既可以修改waterScale的参数值来修改生成地图的水量多少,同时也确保在地图边缘更容易出现水,让我们的地图看起来像是一个海上的大陆(我默认地图边缘都是水)。

在决定好Corner点的属性后,我们把与水Corner相接的Center(也就是多边形)记为水属性,可以通过Corner的touches链表获取到与其相接的Center。

区分水和大陆

区分海洋、内陆湖、海岸线

首先我们规定与地图边界接壤的多边形为海洋,所有与海洋接触的水均为海洋,与海洋接触的陆地是海岸线,其余的水为湖泊。

这里采用了一种种子填充的算法来计算。实际上这是一种广度优先搜素的策略,我们首先将地图边缘的Center添加到一个Queue队列中,接下来执行一个循环,直到队列中没有剩余元素。循环时每次取出队列的尾部Center,检查与该Center直接接触的临近多边形(这可以在neighbors链表中找到),若临近Center为水且不为海洋,代表这是一个还未处理过的Center,将他置为海洋并加入队列,若临近Center不为水,则置为Coast并加入队列。

在处理完全部的Center后,我们只需把海洋多边形的Corner置为海洋,海岸线多边形的Corner置为海岸线即可。

海岸线、海洋和湖泊

赋予高度

接下来我们为我们的地图添加高度的元素,让我们的地形变得崎岖。

为了地形随机但是合理,我们采用了这样一种策略:

靠近海岸线的地方更容易地势低,岛屿中心的地方更容易地势高,这样是很合理的。

实现方案同样是广度优先搜索,我们为每一个Corener定义了一个elevation的属性,代表距离海岸线的最短距离,每经过一个Corner,这个距离就加上一定的随机值(这个随机值范围越大地形会变得越崎岖)。

在搜索结束后,我们把每个Corner的elevation统一到0~1,作为这个点的高度,而每个Center的高度为多边形所有corner高度的平均值。

需要注意的是,我们在统一Corner高度后需要进行一个额外的操作,我们要计算每个Corner下降梯度最大的方向,这会为以后的河流生成做准备。

高度

河流

在上一步时我们已经计算好每个Corner的下降梯度最大的方向,这样我们的河流生成就简单了,我们只需要给出几个参数:河流的最小生成高度,河流的数量范围。然后我们随机给出河流的起点(确保大于最小高度,以及在陆地上),然后让河流按梯度向下流淌,流过的每个Corener置为河流,直到流入湖泊或海洋,或者经过一定路程

河流
  • 到这里我们的地图已经很好了,但是为了游戏性我们仍然为多边形添加两个特征

湿度和生物群落

我们仍然采用了和自然界相反的过程,我们通过多边形到水源的距离来决定多边形的湿度,计算的方案和高度计算基本一致。

湿度图

有了湿度和高度后我们就可以模拟地形上的植物类型了,这里采用了这样一张图表来进行模拟:

生物群落带
群落分布

读者可以自行修改数据让地形变得更干燥或者湿润

渲染

到目前为止我们都是用图形化调试的方式展示,我们还需要渲染出3D网格。

我采用了Unity自带的Mesh渲染的方式,为一个GameObject添加一个MeshRenderer和MeshFilter组件,修改mesh的顶点以及三角形属性,把所有的Center和Corner都作为顶点,对于每个多边形,让Center顶点和每一条边组成一个三角形网格(这里注意三角形要按顺时针排序顶点,确保法线方向正确),同时为mesh分13个submesh(代表13种群落),根据多边形的生物群落决定将三角形网格分配给哪个submesh(mesh.SetTriangles),为MeshRenderer分配13个材质,完成渲染。

demo

尾声

目前不同群落的材质我只用纯色替代,以后会精致制作每一个材质,实现类似低多边形渲染的效果。

且有很多噪声、融合的计算还没有做,如混合边界等。

文中步骤的图片都是一共500个多边形,而最后的效果图共有2000个多边形,从编译、启动、计算到渲染完成一共耗时20s左右,实际游戏运行中计算耗时会更低,且完全没有用到unity引擎的库所以可以放在多线程中完成。

推荐阅读更多精彩内容