前言


在毕业之际,总算是做出了一个关于Minecraft类游戏地形生成的DEMO作为毕业设计,虽然说不上有多高大上,但也算是给 Gameplay 技能栈多点了一个熟练度,了解了下一些关于地形生成的算法。不过由于博主并未透彻研究过Minecraft的源代码,凭着部分别参考资料去猜测地形生成的实现方式,因此这个地形生成算法可以说算是靠自己实践得出的经验。这篇博客便是记录自己经验的一篇博客。

在阅读本篇博客之前,还需要掌握 噪声算法 ,若对噪声算法不了解,可参考博主以前的博客:游戏开发中的噪声算法

github源码地址:https://github.com/KillerAery/Minecraft-Like-TerrainGenerationDemo

程序截图:


参考资料:

[1]. Technical jargon heavy article about how terrain is generated in DwarfCorp.

[2]. 知乎 | Minecraft的地形生成算法是什么?

生成地形高度

一般的地形生成中,地形高度场都是通过2D噪声(输入一个二维坐标,输出一个高度值)来生成的,但是一层噪声往往具有单调的特性(单一的频率Frequenccies 和 振幅Amplitudes),不能满足复杂的自然地形高度:地形可能会有大段连绵、高耸山地,也会有丘陵和蚀坑,更小点的有岩石块,甚至更小的鹅卵石块。

为了模拟出这样的自然噪声特性,我们可以借鉴 分形噪声 的思想,通过使用不同的参数进行多几次不同参数的噪声计算,然后将结果叠加在一起。

在DEMO程序的高度生成中,将使用三层2D噪声进行叠加,其中:

  • 第一层:振幅大,频率小,用于模拟平坦大陆的效果
  • 第二层:振幅一般,频率一般,用于模拟山脉群的效果
  • 第三层:振幅小,频率大,用于模拟小山丘、地面小凹凸的效果

\(Height(x,y)=128∗Noise2D(4x,4y)+64∗Noise2D(8x,8y)+32∗Noise2D(16x,16y)\)

生成生物群落

生物群落(Biome),实际上相当于一个区域的基本地形面貌,例如可分为草地、高原、雪原、沙漠、热带雨林等。影响生物群落的因素可以有很多,包含但不限于:温度、湿度、高度、距离大海的距离、魔力值。如何定义影响因素,完全取决于你的建模。

DEMO程序中的生物群落属性只取决于 温度(Temperature)湿度(Humidity)两个因素,而这两个因素又是分别由不同种子设置的噪声计算得出:

\(\begin{aligned}Temperature(x,y)&=Noise2D(8x,8y) \\ Humidity(x,y)&=Noise2D(8x,8y)\end{aligned}\)

DEMO程序将温度(Temperature)粗略分为热带、温带、寒带,湿度(Humidity)粗略分为干燥、湿润;然后也相应提供了六种不同的生物群落类型:草地、雪地、沙漠、热带雨林、温带树林、寒带针叶林。

模拟雨水侵蚀、生成河流(未完)


DFS思想解决,模拟大雨滴落在地面上砸出一个个小坑的效果。

  1. 模拟一个雨滴,先定义雨滴的质量(比如5000)

  2. 随机砸下来在某个位置,并计算它周边的梯度(下降最急的地方)

  3. 沿着梯度移动雨滴,同时在原位置留下一定质量的水

  4. 继续追踪雨滴进行计算,当雨滴质量衰减到0时或者流进海平面时视为终止

对一定范围内随机模拟多个雨滴,得到的结果将是一个有侵蚀,甚至形成河流的地形。

生成洞穴、裂谷


洞穴生成,实际上基于一层3D噪声(输入一个三维坐标,输出一个噪声值)来完成:

\(Cave(x,y,z)=Noise3D(16x,16y,16z)\)

然后再给定一个阈值,做如下判断:

  • 若噪声值高于阈值,则三维坐标对应方块挖空
  • 若噪声值低于阈值,则三维坐标对应方块保留

当阈值越小,那么更加容易产生洞穴且洞穴规模越来越大。

然而这种洞穴往往是不规则的,显然是不符合裂谷、峡谷这种带有狭长特点的中空地形,对于这类地形可另外使用伸缩变换后的3D坐标参数,此外还应当加入高度因素的影响(例如高度越低,意味着越接近地底,因此赋予更低的阈值),这样也可以形成具有一定深度的裂谷。

生成植被

植被生成,则主要是在计算生成概率,它在DEMO程序中依赖四个因素(温度、湿度、噪声值、随机值):

\(\begin{aligned} Possible_{tree}(x,y) &=N_{tree}+H_{tree}+T_{tree}+R_{tree} \\ N_{tree} &= C_1 \cdot Noise2D(32x,32z) \\ H_{tree} &= C_2 \cdot Humidity(x,y)\\ T_{tree} &=C_3 \cdot |Temperature(x,y)-0.4|\\ R_{tree} &=C_4 \cdot Rand(x,y) \end{aligned}\)

其中,\(C_1\)、\(C_2\)、\(C_3\)、\(C_4\) 分别代表四个因素的权重,四个权重之和为1。

植物生成概率依赖湿度、温度因素很合理,为什么要依赖噪声值、随机值呢?

  • 噪声值:让某些区域的植物分布足够密集,而另一些区域的植物分布可以稀疏甚至无分布,这些区域之间又可以做到植物密度的平滑衔接。
  • 随机值:密集分布区域的植物几乎每一格都会满足生成概率条件,为了避免过于密集,融入一些随机值因素,让分布的树木之间至少有一定的间距。

放置树木(Bezier曲线)

一旦满足生成概率条件,我们就可以根据当前方块的生物群落属性来决定放置什么样的植物(温带草、寒带草、蘑菇、花、寒带树、温带树、热带树...)。

其中树木的放置稍微复杂些,DEMO程序采取了程序化生成而非模板生成的方式来放置树木:

  1. 用一个随机数给出树木的最大高度 \(h_{max}\)

  2. 还需要计算树干每层的树叶半径,这一步主要通过三阶Bezier曲线来计算。三阶Bezier曲线拥有4个控制点(2D坐标),将控制点的 \(x\) 视为树叶半径长,而 \(y\) 视为所处在的树干高度。由于树叶在最底层和最顶层都应该是没有树叶的,这样就可以将第一个控制点和最后一个控制点固定在 \((0,0)\) 和 \((h_{max},0)\) ;而中间两个控制点则可以利用两个随机数作为不同的随机半径\(r_1\)、\(r_2\),分别设置位于 \((\frac{1}{3}h_{max},r_1)\) 和 \((\frac{2}{3}h_{max},r_2)\)。

  3. 在每个单位高度上对贝塞尔曲线上一次采样,从而得到每层树叶的半径值(采样后四舍五入)。

如图所示,当计算出一棵树的随机高度为5时,用于生成树叶的贝塞尔曲线的第一个控制点和第四个控制点分别为\((0,0)\)和\((5,0)\)。接着,中间两个控制点,通过随机数4.5、2.5确定坐标分别为\((1.66667,4.5)\)、\((3.33333,2.5)\)。当树需要计算每层树叶半径时,就可以逐层对该贝塞尔曲线进行采样,共采样6次,对应6层树叶半径,分别为\((0,0)\)、\((1,2.2)\)、\((2,2.6)\)、\((3,2.4)\)、\((4,1.6)\)、\((5,0)\),四舍五入后即为 \((0,0)\)、\((1,2)\)、\((2,3)\)、\((3,2)\)、\((4,2)\)、\((5,0)\)。

生成建筑

生成发展域(元胞自动机模型)

基于元胞自动机模型。

发展域可以理解成一个聚落的势力范围。而生成发展域的大概做法是:

  1. 在某个方块设置聚落的源点

  2. 进行若干轮迭代演化,来演绎聚落的发展(扩展势力范围),其中每轮发展需要根据温度、湿度、崎岖度(周围若干方块高低差)等因素来影响发展域的扩展方向,而且只扩张在势力范围邻接的方块。

温度、湿度越适中、崎岖度越小的方块的代价更低,从而也更容易让聚落范围往这种方块的方向去扩展。

而在DEMO程序实现中,有以下细节:

  • 需要设置一个最高发展度(迭代次数)。
  • 一个发展块设置为3*3个方块,这是因为相同大小的势力范围下,一次添加3*3个方块相比1个方块有着更少的迭代次数。
  • 每一轮迭代都从评估队列里将代价最低的发展块加入聚落的势力范围,然后将与该发展块相邻的发展块加入队列中,并分别进行代价评估(即温度、湿度、崎岖度的综合考量)。

在《DwarfCorp》中,这种元胞自动机模型又可以用于模拟各文明在地图上的势力范围,让文明源点尽可能往条件宜人、土地肥沃且少冲突的区域扩张,通过若干轮迭代后,就能得出一条合理的文明势力边界。

放置建筑(DFS)

放置建筑,主要是基于DFS算法(在某种意义上,用高大上的名词来讲就是波函数坍缩),在前面生成好的发展域内通过DFS算法随机尝试放置预制建筑。

DEMO程序的大致实现:

  1. 在待放置位置队列添加源点位置

  2. 进行若干次循环,每次循环从队列中取出一个位置(以该位置为建筑中心点)尝试放置预制建筑。

    • 若建筑即将放置的区域并不是发展域的子集,则尝试放置失败。
    • 若建筑即将放置的区域是发展域的子集,则尝试放置成功,需将地形进行平整化后再放置建筑。接着,将该位置上下左右四个方向一定offset(需要融入一定的随机数,这样得到的建筑分布就不会过于工整)的位置添加进待放置位置队列。最后,移除发展域相应的区域方块记录(避免重复放置建筑)。

连接道路(A*寻路)

连接道路,主要是基于A*寻路算法,将每个建筑的门口视为目标点,通过寻路算法对所有目标点两两连成一条道路。然而问题在于,道路连接不是简单的寻找最短路,还得模拟出人类聚落主干道、分支路的特性。

DEMO程序的解决方式:

  • 只需简单地修改代价函数,使结点在道路上的开销降低

每次生成完一条道路,需记录道路位置信息,以方便下次寻路查询某个坐标是否位于道路中。

这样,第一条道路虽然总是最短路,但是往后每次连接新道路时,这些寻路算法会相当大可能贴近或者连进原有道路,而不是直接连成最短路。若干条道路生成完毕后,就会显而易见看到干道、分支路的现象了。

优化

地形加载&渲染

有时候可能加载方块太多导致内存不足,需要实现实时自动加载周围区域和卸载过远的区域。

其次,Minecraft类地形中往往有大量方块被其它(上下左右前后共6个)方块所包围,从而不可视。而最初的渲染中,需要把所有存在的方块都渲染出来:

如果对每个方块做可视测试(即检测其上下左右前后是否满足至少有一处无方块),通过测试的才提交渲染队列,于是便有了下图:

为了解决边界问题(最外面的渲染边界的方块无法得知界外的方块信息),于是就采取了加载范围大于渲染范围的方案:

  • 块区(Chunk):基本的地形加载/卸载单位,在X轴、Y轴长度为16,在Z轴(高度轴)长度为256,可容纳16*16*256共65536个方块
  • 加载块区:计算出该块区每个位置的方块属性并存于内存
  • 渲染块区:将该块区里所有应该渲染的方块提交渲染队列

以摄像机的位置为中心,将周围6*6个的块区作为需要加载的块区,而周围5*5个块区作为需要渲染的块区。这样渲染边界的方块也能得知界外方块(因为相邻的块区总是会被加载)的方块信息。

数据存储&查询

一般来说,存储Minecraft类地形数据并不需要记录太多信息,得益于噪声算法的可哈希性,几乎仅需要一个种子和部分特殊方块信息,因为绝大部分方块(正常方块)都可以通过地形生成算法流程便能计算得出方块ID属性,即 \(F(seed,position) = blockID\)。

然而对于被玩家破坏、修改、新增而导致的方块ID属性产生变化,这时候就需要特别额外存储了。

此外,在查询时可以对坐标压缩/解压:Vector3D <=> uint64 (28 bit,28 bit,8 bit),Z轴高度由于最高为256,因此最多占8位。

Minecraft类游戏地形生成机制的更多相关文章

  1. 谈一款MOBA类游戏《码神联盟》的服务端架构设计与实现(更新优化思路)

    注:本文仅用于在博客园学习分享,还在随着项目不断更新和完善中,多有不足,暂谢绝各平台或个人的转载和推广,感谢支持. 一.前言 <码神联盟>是一款为技术人做的开源情怀游戏,每一种编程语言都是 ...

  2. Day3:关于地形生成

    ---恢复内容开始--- 今天桃子好像还是没什么动静,不过媳妇倒是有一点见红~ 希望这是马上要出来的前兆了~ 桃子都已经晃点我俩好多回了~ 已经都快习惯来她这个狼来了的征兆了~ ----------- ...

  3. jvm系列(一):java类的加载机制

    java类的加载机制 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装 ...

  4. hibernate 联合主键生成机制(组合主键XML配置方式)

    hibernate 联合主键生成机制(组合主键XML配置方式)   如果数据库中用多个字段而不仅仅是一个字段作为主键,也就是联合主键,这个时候就可以使用hibernate提供的联合主键生成策略. 具体 ...

  5. 论MOBA类游戏五号位的重要性

    观众朋友们,也许你对题目很好奇,才打开这篇文章.为什么技术圈中会出现游戏类的软文?如果时间充足,可以继续往下看. MOBA 类游戏的兴起,逐渐吞噬游戏市场,以病毒式的扩张方式肆意改变着游戏玩家内心对游 ...

  6. cocos2d-x 消类游戏,类似Diamond dash 设计

    前几天刚刚在学习cocos2d-x,无聊之下自己做了一个类似Diamond dash的消类游戏,今天放到网上来和大家分享一下.我相信Diamond dash这个游戏大家都玩过,游戏的规则是这样的,有一 ...

  7. java class加载机制及对象生成机制

    java class加载机制及对象生成机制 当使用到某个类,但该类还未初始化,未加载到内存中时会经历类加载.链接.初始化三个步骤完成类的初始化.需要注意的是类的初始化和链接的顺序有可能是互换的. Cl ...

  8. jvm系列 (五) ---类的加载机制

    类的加载机制 目录 jvm系列(一):jvm内存区域与溢出 jvm系列(二):垃圾收集器与内存分配策略 jvm系列(三):锁的优化 jvm系列 (四) ---强.软.弱.虚引用 我的博客目录 什么是类 ...

  9. JVM-01:类的加载机制

    本文从 纯洁的微笑的博客 转载 原地址:http://www.ityouknow.com/jvm.html 类的加载机制 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内 ...

  10. 24分钟让AI跑起飞车类游戏

    本文由云+社区发表 作者:WeTest小编 WeTest 导读 本文主要介绍如何让AI在24分钟内学会玩飞车类游戏.我们使用Distributed PPO训练AI,在短时间内可以取得不错的训练效果. ...

随机推荐

  1. C++:获取数组长度

    C/C++中如何获取数组的长度?   如何获取数组的长度 2010-12-15 20:49 C/C++中如何获取数组的长度? 收藏     C.C++中没有提供 直接获取数组长度的函数,对于存放字符串 ...

  2. mac 下 sublime text 运行c++/c 不能使用scanf/cin

    { "cmd": ["g++", "${file}", "-o", "${file_path}/${file_ ...

  3. thinkphp给图片打水印不清晰

    项目中打印条形码的函数,从thinkphp自带的water函数修改而来的. 贴上代码: /** * water2 * 改写thinkphp的water函数更强健的函数,增加了写入位置参数 去掉了alp ...

  4. HDU 1084 - ACM

    题目不难,但是需要对数据进行处理,我的代码有些冗长,希望以后能改进... 主要思路是先算总的时间,然后进行对比,将做同样题数的前一半的人筛选出来. /状态:AC/ Description “Point ...

  5. TypeScript入门(三)面向对象特性

    一.类(Class) 类是ts的核心,使用ts开发时,大部分代码都是写在类里面. 1.类的声明 多个对象有相同的属性和方法,但是状态不同. 声明类的属性和方法时可以加 访问控制符,作用是:类的属性和方 ...

  6. Struts标签库详解【3】

    struts2标签库详解 要在jsp中使用Struts2的标志,先要指明标志的引入.通过jsp的代码的顶部加入以下的代码: <%@taglib prefix="s" uri= ...

  7. bzoj4710 [Jsoi2011]分特产(容斥)

    4710: [Jsoi2011]分特产 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 814  Solved: 527[Submit][Status] ...

  8. Delphi取UTC时间秒

    自格林威治标准时间1970年1月1日00:00:00 至现在经过多少秒数时间模块Uses   DateUtils;当前时间:中国是 +8时区,换成UTC 就要减掉8小时showMessage(intt ...

  9. centos6.4安装GitLab

    参考文章: http://www.pickysysadmin.ca/2013/03/25/how-to-install-gitlab-5-0-on-centos-6/ yum安装redis的方法: h ...

  10. 微服务深入浅出(1)-- SpringBoot

    基于Spring的开发框架,旨在简化配置快速开发,是新一代web开发框架.下面介绍一下常用的几个功能: 1.Spring单元测试 针对DAO层 (1) @RunWith(Spring.class),表 ...