• RogueLike地牢生成算法Unity实现


    最近几日闲来无事,后来看到了RogueLike的游戏,就像来试一下地牢生成算法。

    网上看到了一篇文章写的挺好的。后面会有转载,不急哈。

    先看一下我实现的效果图

    生成过程:

    地牢生成算法的思路是:

    1.生成大量随机位置随机大小的房间
    2.通过物理碰撞将房间分散开
    3.通过阈值选取主要的房间
    4.对主房间进行三角剖分运算,得到房间之间的可选路径
    5.使用最小生成树选定房间之间的路径,并且保留少量回圈
    6.确定房间路路线
    7.路上经过的房间划入次要房间中
    8.绘制房间和路线.

    随机生成房间

    需要随机生成房间的初始位置,以及房间的长宽高

    文章中提供了一种思路,在圆形中随机确定一个点,作为房间的初始位置。

    而如何在圆形中随机生成一个点。

    圆中生成随机点

    先从一个三角形中入手

    假设三角形ABC 且|AB| =|AC|。

    在AB中找一个点X,在AC中找一个点Y,并用AX和AY做一个平行四边形XAYZ。那么这个随机点Z就找到了。

     

    那万一Z超出了三角形呢

    没事,超出了三角形,就将点Z以BC翻折到三角形中。

    那我们该怎么运用在圆中呢?

    其实这里去了一个极限,将一个圆细分成无数的等腰三角形

     

    这样,我们只要在圆中选择一个三角形,再从三角形中随机选择一个点。

    这样就能随机生成一个点了。

     

    现在轮到房间大小了

    房间大小可以利用上面的方法,在圆内生成一个点,再用这个点生成一个矩形。

    可以有两种生成方法,如下图。

    分散房间

    这里可以选择使用Unity中自带的Rigidbody分散,

    因为Unity的碰撞不好控制,常常跑出来一堆小数,所以我选择自己写一个Rigidbody。

    逻辑非常简单,只需要判断自己是否覆盖别人的房间,如果覆盖就移动到没有覆盖。

    移动距离计算

    假设房间1 和房间2发生碰撞,

    对于房间1
    
    Offset = pos1-pos2
    
    如果选择竖直方向的移动
    
    需要判断是向上还是向下
    
    方向:dirY = offsetY>0?1:-1;
    
    距离:dis = y1/2+y2/2 – abs(offsetY)
    
    pos1 += vector2.up*dirY*dis
    
    水平方向移动同理。

    然后在FixUpdate中调用Simualeting。

    筛选主房间

    这里没什么好说的了,就是这个筛选房间可以在碰撞之前进行。

    还有就是 item.size.Sum();这个是自己写的一个方法。

    三角剖分

    这里是第一个难点,花了不少时间理解和实现。

    三角剖分就是将多个点连起来生成多个三角形,并且三角形之间没有重叠的部分,或者说是线段没有交叉。

    就是类似下面的图:

    这里实现的是Delaunay三角剖分算法,

    我先走一遍流程。

    首先生成一个三角形(又称超级三角形),这个三角形将所有的点都包含进去。

    例如:

    要实现将所有的点包围起来,我的想法是:

    找到x和y的最大值和最小值,生成一个矩形,利用这个矩形生成一个圆形。

    最后利用这个圆形生成一个三角形,这个三角形内接刚才生成的圆形。

    如图:

    将这个超级三角形放入待确认的三角形列表中。

    开始遍历三角形列表,

    如果当前三角形的外接圆内有点,

    选定一个点,将当前三角形拆分成三个边,让这个点和三个边分别连接成三角形,

    将这三个三角形放入列表中,并且将当前三角形删除。

    如果三角形的外接圆内没有点,那么这个三角形就是Delaunay三角形。

    将三角形放入Delaunay三角形列表中。

    上面的流程看起来挺正确的,

    但似乎忽略了一种情况

    按照刚才的算法,会生成下面的情况:

    这时候应该怎么办?

    不急,先执行另一个三角形看看

    执行后多出来了三个三角形,这里用了蓝色的线标记起来。

    此时有两个三角形是完全相同,(大小相同,位置相同)

    如果将这两个三角形删除,就会得到下面的情况。

    这可太美妙了。(当时理解的时候的想法)

    下面展示一下伪代码

    输入顶点列表
    对顶点的x进行排序
    生成一个超级三角形,并添加到待确定三角形列表中
    遍历顶点
        遍历待确定三角形列表
            如果该点在三角形外接圆中
                将三角形拆分,分成三个边,将三个边添加到缓存区中,将本三角形删除
                继续当前循环
            如果该点在三角形外接圆的右侧,即该点的x>三角形外接圆最右侧
                这时候所有的点都不可能在圆内,
                此时将三角形添加到Delaunay三角形列表中
                将三角形从待确定三角形中移除。
            如果该点不在三角形外接圆右侧,这时候不能确定其他点不在三角形内部
                继续当前循环。
        查找缓存区中相同的边,
        将相同的边根除,即一条边不止出现一次,就将这条边在列表中全部删除。
        将顶点和缓存中所有的边组合成三角形。
    添加剩下的三角形到delaunay三角形列表中。
    算法完成
    删除与超级三角形相关的三角形。
    输出Delaunay三角形列表。

    代码中使用了排序,外接圆右侧是个什么意思

    其实是这样的

    如果没有点1 和点2 ,这时只有点3和点4 ,遍历顺序是3 4,

    判断点3 在圆外时,点4一定也在圆外。

    但是如果点1 2 都存在,那么遍历顺序是1 2 3 4

    此时点1 在圆外,而点2在却圆内。

    这样懂了吧。

    至此三角剖分环节结束了,

    代码参考(代码是真的仅供参考,因为代码能力比较emmmm)

    生成超级三角形

    对顶点列表进行排序

    遍历顶点列表

    遍历三角形列表

    如果顶点在三角形外接圆右侧

    如果顶点在三角形外部,而不是在右侧

    如果顶点在外接圆内部,拆分三角形,将三角形添加缓存区,

    如果已经存在缓存区中,添加到待删除的缓存区。

    根除重复的边(上面讲的是根除三角形,其实同理)

    生成新的三角形,添加到待确认的列表中

    最后合并所有的三角形,然后删除超级三角形相关的三角形。

    要是觉得讲的不好可以去看一些我当时参考的博客

    https://www.cnblogs.com/zhiyishou/p/4430017.html

    https://blog.csdn.net/keneyr/article/details/90316467

    最小生成树算法

    从上面获取的三角形列表,将三角形列表转换成边列表,对边列表进行最小生成树算法,

    这里使用的是Kruskal的最小生成树算法

    我就直接把算法流程画出来,看的方便

    我们先从最小的边开始连接,这时候看边的两端的父亲是否为同一个节点。

    如果没有父亲,那自己就是父亲节点。

    接下来是第二条边,同样因为父亲节点不相同,所以连接起来

    接下来边是1-3这条边,但是节点1和节点3 的父亲是相同,所以不能连接起来。

    然后选择2-4这条边,连接起来。

    剩下的同理

    最小生成树有个性质,树的边数量等于节点数量-1

    所以终止的条件是树的边数量等于节点数量-1;

    下面是代码:

    首先对边的权重进行排序

    对边进行遍历

    判断边的两个节点的父亲是否是同一个节点

    至此最小生成树就结束了

    最后我们要随机几条边到生成的路线中。

    这里的方法比较简陋,就是随机找几个,然后添加进去。

    现在大部分工作已经准备好了,剩下的就是如何讲我们的房间和道路展示出来

    绘制房间和道路

    这里采用了Unity中Tilemap工具来绘制房间和道路。

    按照文章的设想,房间之间的通路是由水平和垂直的路组成的,并不是斜着的路。

    如图

    但是两个房间的路又存在两条,就像moba类游戏中的上路和下路一样,我们应该选择哪一种

    如图:

    虽然很不情愿,但是我还是得使用随机来指定路线,

    接下来又面临一个问题,房间的出口究竟应该设置在哪里?

    如果是中间,这样看起来就不够多样性。那只能选择随机了。没错又是随机,毕竟是随机生成地牢嘛。

    我的想法是在图中红色的区域随机生成一个点,作为两个房间道路的拐角点或者是转折点。

    代码:

    为了方便以后放置门,我们还需要讲房间的出口计算出来

    看图可以看出来房间A的出口坐标

    DoorA.x = room.x+size.x/2

    DoorA.y = Turnpoint.y

    看上去很完美,但是如果出现了下面的情况门口就会出现问题。

    假设拐角点在房间内,按照上面的计算公式,就会出现下面的情况

    为了排除这种情况,有两种做法,

    一、禁止转折点在房间内生成,这个也有问题,我不过多解释。

    二、删除房间B的出口,将转折点修改为真正的出口

    我的选择自然是删除房间B的出口

    看图可以看出转折点的位置是

    Turnpoint.x = Turnpoint.x

    Turnpoint.y = roomb.y+size.y/2;

    再将两点返回

    最后调整一下每个方向应该的值,微调一下代码(这个微调花了我最多时间)

    代码参考

    这里稍微解释一下,代码中有很多这样的语句

    (dir.x > 0) ? offset - 1 : -offset)
    或者
    (dir > 0) ? (roomB.size.y / 2) - 1 : -(roomB.size.y / 2)

    这些 - 1 都是为了能够在Tilemap中绘制图的时候不会出错

     如果我不减1 就会出现

    心目中的6*6

    现实中画出来的6*6 注意:这里指的是如果不 减 1 的情况。

    接下来继续上代码

    将主房间和次方间,以及道路绘制出来

    绘制房间到Tilemap的代码:

    绘制道路到Tilemap代码

    最后生成效果展示

    终于结束了,真的花费了太长的时间,感觉有些不务正业hhh,

    当然问题还是有的,比如,大量的射线检测导致的性能问题,还有道路的选择也有写小问题。

    由于房间之间的碰撞使用的是组件,所以碰撞的顺序常常不相同,即使设置随机种子,房间的位置也会不相同。

    剩下的以后有机会再写,保证不咕咕咕。

    最后,我写了一个月才完成了这些代码,不知道大佬们需要花多久。

    参考文章:

    随机地牢作者的博客:

    https://www.gamasutra.com/blogs/AAdonaac/20150903/252889/Procedural_Dungeon_Generation_Algorithm.php

    随机地牢作者的github:

    https://github.com/adnzzzzZ/blog/issues/7

    随机地牢的国内翻译:

    http://www.gamelook.com.cn/2015/12/239245

    圆内随机点算法:

    https://stackoverflow.com/questions/5837572/generate-a-random-point-within-a-circle-uniformly

    三角剖分算法:

    https://www.cnblogs.com/zhiyishou/p/4430017.html

    https://blog.csdn.net/keneyr/article/details/90316467

    最小生成树算法:

    https://blog.csdn.net/qq_41754350/article/details/81460643

    其他生成算法(未参考):

    https://indienova.com/indie-game-development/roguelike-dungeon-building-algorithm/

    https://indienova.com/indie-game-development/rooms-and-mazes-a-procedural-dungeon-generator/

  • 相关阅读:
    1.1 java变量及数据类型
    3. 软件测试的类型
    2.3 软件测试模型之 敏捷测试
    2.2 软件测试的手段
    2.1 软件测试的阶段
    1.1 软件测试基础概念
    浅谈内联元素inline
    微信内置浏览器清除缓存的方法
    我的package.json清单
    我的gulp.js清单
  • 原文地址:https://www.cnblogs.com/SpiderStranger/p/12403219.html
Copyright © 2020-2023  润新知