基于瓦片(Tilemap)的随机地图生成

您所在的位置:网站首页 随机生成角色的游戏 基于瓦片(Tilemap)的随机地图生成

基于瓦片(Tilemap)的随机地图生成

2023-09-21 01:26| 来源: 网络整理| 查看: 265

RougeLike游戏中很常见的元素就是随机地图,最近项目上也需要制作,在此记录一下制作方法。

以下是通过Unity中的Tilemap便可简单实现的随机地图效果。

绘制使用了Unity,但生成方法不局限于Unity,理论上可以搬到任意平台。

1.准备工作

1.1 板块布局

为了避免所有地图都过于相似,我们将大地图细分为了30x30的板块,意味着一个板块内包含了900个瓦片单位。

通过将任意数量的板块排列组合,地图可以呈现出不同的结构。

调整板块数量,可以控制整张地图的规模。在未来地图过大时,也可以通过分区块加载来优化性能。

如果你的地图并不大,或不需要呈现出不同的构造,可跳过此步。

5个板块布局

以上是拥有5个板块的地图能呈现出的部分形态。

这部分可以手动配置,也可以通过程序生成,以下是一段示例的代码。

以上代码中得到的结果为板块的相对位置,我们将其乘30(每个板块的边长),即可得到实际的位置。

1.2 板块布局地图元素分层

根据策划需求,我整理出了地图的结构,按照渲染的顺序,由底至上分出了以下几层。

注:由于我们做的是一个开船的游戏,所以是水面和陆地,如果你是一款主要在陆地上的游戏,可对应为地面和山峰(或其他阻挡区域)。

这个结构并不重要,你可以跳过,唯一重要的是【陆地】层,也就是大多数游戏中的“墙壁”,我在后续也会以墙壁称呼它,这一层决定了地图整体长成什么样子。

地图的结构

以上结构可以根据你的项目需求任意调整。

有了以上信息,就可以开始逐层制作了。

2.程序化生成

2.1 基础地形

先把装饰性元素放一边,整个地图其实都可以简化为“地板”和“墙壁”,也就是可行走区域和不可行走区域,这个数据可以通过两个数字来代表(比如0和1)。

而整个地图的数据,可以存储在一个2维数组中,每个数组元素表示此处是什么地块。

这个数组的大小,应当取决于前一步生成出的布局,通过找出x和y轴最小和最大之间的距离即可得出。

同时,可以将压根无法被看到的区域,写入一个特殊的值(比如-1),在后续的运算中,忽略这些格子,避免花费太多性能用于生成这些根本看不见的区域上。

以下是伪代码:

经过上述步骤,将会得到一个“被掏空”的矩形。

基础地形

其中灰色部分代表不可见区域,蓝色代表可见区域。

接下来,需要在每个区块边缘设置基础的墙壁,只需要检查每个格子距离区块边缘的距离即可,距离决定了墙壁的“厚度”,厚度太小可能会穿帮,导致看到外面的没有生成地形的区域。这个值取决于摄像机的设置,你可以经过计算得出,不过更简单的做法是先随便填一个随后慢慢调整。

同时还要检查相应方向上,是否有其他相邻区块。避免生成的墙壁隔断两个区块。

以下是伪代码:

经过此步骤,地图上会在区块边缘生成一定厚度的墙壁,如下所示:

生成墙壁

2.2 随机摆放的独立墙壁块

这一步可忽略,你可以跳到2.3。

目前中间的区域还是太开阔了,出于美观,我在其中放了一些“岛屿”。

放置的规则也很简单,随机找到任意大小(例如5x5)的空间,然后填入墙壁编号即可

重复以上过程N次,即可得到一大堆杂乱摆放的墙壁块。

(因为制作图片比较麻烦,我这里直接用游戏内的素材进行展示了,将数据绘制出来的方法可在后面看到。)

独立墙壁块

需要注意的是:多个随机写入的墙壁块可能会阻断你的区域,形成一片孤立区域。

被随机墙壁块阻断的情况

一个简单的处理办法是,生成一个宽高都+2的块,写入这些墙壁块之前,检查块中每个格子上是否已经存在墙壁,如果存在则舍弃掉这次,并且在写入时将大小-2。

2.3 随机摆放的独立墙壁块

现在已经有个一个基本的地形了,但边缘太过平整,看起来不像是天然形成的自然景观(海岸线),需要让它有些变化。

为此需要定义一个函数,我称之为“生长”函数,让墙壁就像植物一样从平整的墙面长出来。

这个函数需要对每一个格子进行判断:

首先,它必须是一个空的格子;

其次,它的四个方向上需要有任意一个方向是墙体;

最后,通过一个随机检查(比如有30%的概率)将这个格子变为墙体。

至于为什么只能有一个方向是墙壁,后面会提到。

伪代码如下:

将上述代码重复执行数次,迭代次数越多,生成的越多。我们的地图会变成这样。

(注意,我用到的随机参数并非上述代码中的参数,所以结果可能会看起来不一样。)

随机摆放的独立墙壁块

2.4 平滑

这一步是可选项,你可以跳过。

上面的地图已经有些形状了,但部分地方向外突出的太“尖锐”,看起来不够自然。

因此需要做一些平滑处理,我又声明了一个“平滑”函数,平滑的过程非常简单。

在上一步中,我只用到了那些只相邻一块墙壁的空块,而这一步,则是用到相邻2、3、4块的,代码部分几乎一样。

我在每种相邻情况下,都给予了不同的概率,这可以控制地图的“丰满程度”。

其实这一步可以跟上一步“生长”合并,但根据实际项目需求,我还是拆分为了2个,便于控制地图的生成。总之你可以根据需要将它们合并或者拆分。

最后,同样迭代数次。地图大概会变成这个样子:

(注意,我用到的随机参数并非上述代码中的参数,所以结果可能会看起来不一样。)

独立墙壁块平滑结果

2.5 其他的一些装饰层

剩下的步骤都大同小异,基本上和之前一样:

(1)遍历每个格子

(2)判断这个格子和相邻格子的情况

(3)写入数据

例如,要为墙壁的外轮廓添加一圈阴影,那就:

(1)遍历每个格子

(2)检查相邻格子是不是墙壁

(3)写入“阴影数字”

最终在游戏中呈现的效果如下:

添加装饰层后的效果

3.绘制篇

3.1 Rule Tile

这是个可选的组件,如果不需要,你可以跳过此步。

对于渲染瓦片地图,Unity自带的Tilemap已经可以做到。

但我们可以安装一个Unity的额外扩展包来更方便的完成。在Unity菜单中选择:Window -> Package Manager,并安装“2D Tilemap Extras”这个包。

这个包内提供了“Rule Tile”组件,可以根据规则自动调整Sprite,简单形容就是,当这个块是“转角”的情况下,自动调整为“转角砖块”。

界面如下:

Rule Tile组件

绿色箭头表示此方向上有其他瓦片,红色叉表示没有。

其余部分可以参考此处官方教程。https://learn.unity.com/tutorial/using-rule-tiles

3.2 通过数据进行绘制

在上述步骤中,我们已经得到了一个巨大的数组,包含了地图上每一个格子的数据。

这一步需要将其绘制出来。

对于每一层,我们都需要创建一层独立的Tilemap,并将他们按照顺序进行显示。可以通过调整面向摄像机的轴(2d游戏中一般是Z)调整渲染顺序。

剩下的工作十分简单,读取数组中每一个元素,根据元素的编号,在对应的层上填入格子。

伪代码如下:

你还可以添加额外的绘制规则,例如,当此处的值> 30并且< 50的话,设置为草地瓦片。

3.3 装饰性元素

因为这部分根据项目的不同会有很大变化,因此你需要自己选择合适的方式。

例如:

装饰石头

你可以将这块石头,按照瓦片的尺寸切割成数个瓦片,然后通过某种规则找到合适的位置进行摆放,比如和之前一样遍历每个格子和相邻格子的办法。

例如,你想在墙壁的右侧添加一些1格瓦片大小的装饰,那就遍历每个格子,检查:

(1)它是否是墙壁

(2)它的右侧是否是空地

也可以将其作为独立的贴图,通过SpriteRenderer直接放置在世界空间下的合适位置。

4.附录

性能问题:

本篇文章仅介绍了方法,并未从性能方面去考虑,在不同的项目上,性能的问题可大可小,很难一概而论。

如果你的地图非常大,那么构建一个大数组并逐项遍历速度可能会十分感人,你可能需要将其拆分成许多份。你还可以将数组转化为一张贴图,通过颜色值记录格子的信息,并将运算过程放入GPU中,理论上或许会快很多,但我没有测试过。

参考资料:

这套生成的规则主要参考了元胞自动机。

关于元胞自动机的地图生成,可以参考这篇文章。https://blog.jrheard.com/procedural-dungeon-generation-cellular-automata

除此之外,还有许多程序化生成地图的方式:

波函数坍塌:https://www.procjam.com/tutorials/wfc/

通过噪波图生成:https://www.redblobgames.com/maps/terrain-from-noise/

通过维诺图生成:http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/

不同的方式都有各自的优势,可以根据实际情况进行选择。

最后,感谢阅读!

基于以上方法生成的《深海传奇》四种地貌的随机地图



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3