CS61B project 2 示例图的地牢地图房间走廊地图生成洞穴地图生成方法

    本文是针对 CS61B 学习的 proj 2 的部分内容的笔记。通过本文可以学习到怎么生成地牢游戏用的洞穴地图和房间走廊地图,主要讲解元胞自动机,房间走廊的暴力生成算法,房间(矩形)范围碰撞检测算法,简单(暴力)的路径生成算法,Prim 求解最小生成树连通房间算法。


成品

Josh 做的例子是这样的:

首先是模仿 Josh 的版本做的矩形房间和通道的版本:具体做法也很简单很暴力。

然后还有一个做的地牢的基于洞穴的版本大致这样(地图应该调平滑点洞穴减少直线的路)。

基本方案是使用 Cellular Automata 演变,然后用某种策略联通不联通的洞穴。(设计多种可走路径才比较趣味)。

 实际上如果对这类游戏感兴趣,可以做一种先生成房间,然后在房间里面做 cave 这种的,这样可能更好玩一些。

讲解从 Cave 开始讲,之后在讲怎么做 josh 的版本。


Cellular Automata

请学习元胞自动机的基本思路,这里摘录一下:

1.每个元胞都依赖其邻居而引发下一个时刻状态的更新。

2. 几个显而易见的应用:(特殊模型的理解性难搞,相似模型才方便用)生命游戏:指定某种规则让 Cell 生长或死亡。澳洲火灾:指定树木火和空地,编写地生树树生火的依概率转变的元胞自动机。其中树生火可以认为是外界火激励(雷电,烟头)。

3. 邻居分布有:摩尔型 Moor 和 冯诺依曼型 Von 和其他类型

要点是:

一次更新全部更新 (全局更新,并行性质)。

边界条件就是涉及倒边界的时候他的邻居取谁,有固定型,周期型,绝热型和映射型这四种。固定就是指定一个值。周期就是回到整个世界的另一边。绝热是取自己。映射是指定取某个地方的。

不废话了。。。好像没有什么好摘录的。

对于初始化的世界,我们可以采用随机的方法。

了解元胞自动机理论可以编写一些地板和墙壁相互转换的规则,比如扩大墙壁堆和扩大墙壁范围的规则(floor 的邻居的墙数超过一定程度就变成墙),清除空心墙的规则(floor 的邻居的墙数为8一定程度就变成墙)。

尝试不同的参数,最终用的方案经过多次演变之后得到一个洞穴地图:

尽管已经很不错了,但是可以发现有一些没有连通起来的洞穴,必须把他们连接起来。


Prim 建路打通

Prim 算法本来是建立 最小生成树/支撑树 的。

这里复习一下,用来打通地图的不联通的域。请复习 dijkstra,prim 和 kruskal 算法(复习成果能看明白下面的文段)。

Kruskal 是边的贪心,每次选一条最短的边(对边集排序的优先队列即可),然后不成环地(使用并查集)接入支撑树。

Prim 算法则是 Dijkstra 的变形和 Kruskal 的无需判环改进。

回想 Dijkstra 的过程,其贪心决策是基于单源点到达目前点的距离,松弛操作也是基于这个原则,每次从 visited 取最小代价的加入 done,然后去松弛其他 visited 和 unvisited 节点。

回顾最小生成树解决的问题是是 Spanning Tree 的总权重,第一个是取松弛源点的时候是从整个支撑树的集合里面随意取的,然后贪心地遍历能连接两个集合的边,这里实际上是两个集合(done 和 unvisited)笛卡尔积的遍历,找最短边的过程。这也是为什么说 Prim 是 Kruskal 的无需判环的改进,因为他实际上还是对边贪心)。Dijkstra 的优化要快速取得贪心的当前 unvisited 里最短路径节点使用 PriorityQueue 优化,Prim 的时空复杂度和 Dijkstra 是一样的。其正确性可以通过这样的归纳法想到:首先起始一个点, visited 点集必然是一颗最小生成树。然后每次都保证添加的边是 visited 这个点集所有点中到 unvisited 中最短的那条(相当于整个集合重权重的增量最小),所以更新后的 visited 点集也必定是一颗对节点们来说最小生成树。本质上和 kruskal 是一样的,只是通过划分集合来防止成环而已,而kruskal只需要排序一次,prim就要用优先队列动态排序。

不过我们不需要做一个最小生成树的决策,所以实际是就简单地建路而已,有兴趣的话可以用发言人的距离来做最小生成树建路。

简单的一种建路方案:

由于在地图里并不是直接对节点进行最短路或者支撑树的操作,实际的节点是一堆代表房间或者墙壁的 Tiles 。

如果我们想要建立一个图的结构,涉及边的构造问题,就算是真的求支撑树或者最短路,也必须要先弄一幅图出来,但是实际上要建这一个底图并不简单,首先我们不能直接搞一个全连接的图,这样后面的算法就算不过来,就涉及到哪些节点连接起来的问题。如果了解拓扑学,我们知道研究曲面的时候可以用三角剖分(Triangulation)来研究,有限元分析里离散建模也常用这种多边形的方法,图形学里就更加是采用多边形来画立体图形的。这里有一种方法 Delaunay Triangulation 可以对点集建立一个基于三角形的图。这个暂且不讲了,如果我们要模拟 Josh 的示例结果,说不定会用上这个算法来建立路径。

首先我们指定地区发言人(或者是房间发言人)作为一个洞穴的代表(用来作为建路的起点和终点),这个房间发言人就依据遍历顺序取房间的左下角好了。

为了简单起见,建路简单模仿这个过程好了,依据区域发言人离最左下角的曼哈顿距离来决定哪两个非联通区域需要建一条路。

先遍历地图,找到地区发言人之后加入到子图中,然后用一个循环从优先队列取两个发言人来,用某个随机决定左右上下的方案建设一条路。(其实优先队列都不用, 只需要一次排序就行的事情。。。)

我采用的随机化方案如下:

具体就是每次决定是向 x 前进还是向 y 前进。不走迂回的路减少代码量。

运行结果:

可以发现原来很多的墙壁的地方又被打通了,看上去洞穴的那种很多连在一起的感觉没有了,实际上这里可以做一部小洞穴全部填成墙的操作保留更多的墙壁完整性,这一点则需要在找到非连通洞穴的时候顺带进行一个计数,如果数量小于某个 threshold 就全部填了,粗暴的做法可能使用更多的内存和运行时间,这里暂时就不研究下去了。


边界墙上开门思路

我这个普通版洞穴地牢设计的是四周都是墙,所以只需要确定一个方向(l,r,t,b)然后遍历对应的那条边随便找一个就行了。

如果是单行的墙,直接开一个门即可,如果是多行的(比如右下角),我限定一个 loop 最多上限次数(防止全部都是多行墙导致死循环)然后再跳出来去打穿这个开一条路给门用。

开路这个直接确定方向后沿用前面 Prim 调用的那个随机选方向开路的方案的 helper 就行了。


权重放置

更多时候需要根据地图上点的特性决定放置某些东西(包括门和宝物陷阱自动寻路AI怪物等),需要对地图建立某种评分机制。为了方便我们可以沿用 Cellular Automata 的思路根据摩尔邻居或者冯诺依曼邻居以及扩展摩尔邻居等来对某些特性评分。

和用卷积核滤波器求相关度相似。

比如要找比较角落(靠近墙)的地方,就求某个邻居墙阈值之上比较高分的点。要找路的话则是墙和地板某种比例范围的,如果是洞穴则是全是地板。具体细节可以通过理论推导也可以编程时调参,具体我们写到这里再回来补充。

这里对全是地板的地方,我们可以补充一个元胞自动机的规则,采用某个大范围的扩展摩尔型邻居,如果邻居中墙的个数过少,我们就建立一些墙,并且下一个循环仍然会扩充墙和地板,这样能避免自动机多次迭代只剩下大片空地的问题。

这个时候都累死了,但是我实际上不想用这个 Cave 的方案,因为他不好看哈哈。josh 的房间和 hallway 做得挺好看的,我决定要来模仿这个!


矩形重叠检测

我们来研究 josh 的示例里面的 room base, 事实上在他的 lab proj 讲解视频里野没有具体说 implementation 的方面,就是讲了一下 hallway 的一些建议。我于是建立一个基本思路就是先生成 rooms,再生成 hallway(也是延续用我的这个 Cave 的思路了,因为实在是懒得思考其他思路的实现。其实我野参考了一些博主,见参考连接的一些先生成 迷宫 再挖 出 rooms 的方案,但是就是懒得去学迷宫的算法,希望之后补上)

为了 room 的运算和传参方便,定义 Rectangle 类

第一步是生成 no overlap 的房间。836. 矩形重叠 - 力扣(LeetCode) (leetcode-cn.com),这个算法概要就是高中的线性规划问题里计算 overlap 的可行域有没有面积。和判断两个水平、垂直线段是否 overlap 是一个道理的。

当然我野参考了一些游戏(主要是 indienova 的参考资料)源码怎么生成的,他有一种是用物理引擎自己的碰撞算法来分离房间(😓),有一种是用判断 rectangle 域内有没有非 nothing 的 tile,然后就是leetcode 836 这种。这两种方案我都试了,具体来说就算判断矩形框内主要的复杂度就根据矩形框的大小,房间比较小的时候还是很稳定的。然后存储矩形框列表,通过 leetcode 836 这种,复杂度依据房间的数量。实测计时生成几百个地图下来两个方案的运行时间大致相同。这里两种方案都给出:

然后就是生成房间的具体代码:

解释这里需要做的事情:

第一是随机化的东西,由于会出现无法生成可放置的时候,我们需要一个大循环,里面 count ++,然后对于不同的尝试次数(th)我限定了不同的随机长宽来减少尝试次数。当然如果实在生成不了就终止吧。


直截了当地建路

接下来我们看怎么联通。这里我决定采用和 cave 方案中 一样的思路来建设 hallwawys。

这里需要用到 existingrooms 列表,因为我们需要一个hallway联通的入口和出口。我仍然采用了一个 随机化的方案,其中 makePath 函数本来想用 Cave 那里用的随机决定 x 或者 y,但是这样导致 hallways 不够好看(随机度过高)。我们提供一个约定条件,生成有特定长度的  Hallways 再转弯。而且我用了 sort 来保证只联通靠近的 rooms 而避免了穿越过多的情况。这一点很重要不然得到的图会很多交错 hallways 很乱。后来我发现梯子型转弯会让方向性太突出。研究了发现简单的是最好的,所以我直接让 hallway 就最多是一横一竖。

很容易就能得到这样的联通。具体的更加好看的错综复杂的值得去学习一些迷宫生成的方法。

调参获得更多好看的图案

最后是放门,这里仍然保留意见就是建立热度地图,可以找一个贫瘠的地方(等放下了更多游戏资源后)或者比较狭窄的地方,也可以找一个很小的 room 来。随机找墙的话就涉及一直找不到的情况。最简单的是随机全图点,然后根据冯诺依曼邻居之类的方案找一个墙,我这里采用的是随机取一个 room,然后在他四周随机找一个方向,然后遍历那条边找一个点放置。当然要用一个计数器记录无效情况,这个时候我们需要循环换一个房间或者方向放置。如果循环次数太多可以再考虑必定能找到的情况,比如遍历 world。

做这个project 的收获应该就是随机性的时候要考虑多次不合法的情况?感觉除了判断房子 overlap 其他和算法数据结构无关。本来为了检测 overlap 优化范围查询我还查询学习了 kd树和替罪羊套树(适用于最近邻和范围查询,可以看邓俊辉学堂在线的计算几何课程慕课),最后由于问题规模不大没有用上。


一些好文欣赏

Room:房间和迷宫:一个地牢生成算法 | indienova 独立游戏

Room:Gamasutra: A Adonaac's Blog - Procedural Dungeon Generation Algorithm

Cave:随机生成 Tile Based 地图之——洞穴 | indienova 独立游戏

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
这里提供一种简易的Unity-2D随机生成地图及障碍物的方法: 1. 创建一个空物体,命名为MapGenerator,将其挂载一个脚本MapGenerator.cs。 2. 在脚本中定义一些公共变量,如地图大小、障碍物数量、障碍物大小等。 3. 在Start()方法中,使用循环生成地图和障碍物。可以使用随机数来生成地图和障碍物的位置和大小。 4. 生成地图时,可以使用Tilemap组件来创建砖块地图。可以使用Tile Palette工具来创建不同的砖块,并在脚本中随机选择一些砖块来生成地图。 5. 生成障碍物时,可以使用SpriteRenderer组件来创建障碍物。可以使用多个Sprite来创建不同的障碍物,并在脚本中随机选择一些Sprite来生成障碍物。 6. 最后,将生成地图和障碍物放置在MapGenerator物体下,并运行游戏即可。 下面是MapGenerator.cs的代码示例: ``` using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; public class MapGenerator : MonoBehaviour { public int mapWidth = 100; public int mapHeight = 100; public int obstacleCount = 20; public float obstacleSize = 1f; public Tilemap tilemap; public TileBase[] tiles; public Sprite[] obstacles; // Start is called before the first frame update void Start() { GenerateMap(); GenerateObstacles(); } void GenerateMap() { for (int x = 0; x < mapWidth; x++) { for (int y = 0; y < mapHeight; y++) { tilemap.SetTile(new Vector3Int(x, y, 0), tiles[Random.Range(0, tiles.Length)]); } } } void GenerateObstacles() { for (int i = 0; i < obstacleCount; i++) { GameObject obstacle = new GameObject("Obstacle"); obstacle.transform.parent = transform; SpriteRenderer sr = obstacle.AddComponent<SpriteRenderer>(); sr.sprite = obstacles[Random.Range(0, obstacles.Length)]; float size = Random.Range(obstacleSize / 2f, obstacleSize); obstacle.transform.localScale = new Vector3(size, size, 1f); float x = Random.Range(0f, mapWidth); float y = Random.Range(0f, mapHeight); obstacle.transform.position = new Vector3(x, y, 0f); } } } ``` 在这个示例中,我们使用了Tilemap组件来生成地图,并使用了SpriteRenderer组件来生成障碍物。你可以根据自己的需求来修改代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值