基于瓦片(Tilemap)的随机地图生成 |
您所在的位置:网站首页 › 随机生成角色的游戏 › 基于瓦片(Tilemap)的随机地图生成 |
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 |