java编写roguelike_RogueLike地牢生成算法Unity实现
发布日期:2021-06-24 11:12:05 浏览次数:5 分类:技术文章

本文共 3873 字,大约阅读时间需要 12 分钟。

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

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

先看一下我实现的效果图

3a0455ffd897b62439d11921d83f0955.png

生成过程:

9dc670ba8e7c5616b4a1b7d58f57d5f2.gif

地牢生成算法的思路是:

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

随机生成房间

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

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

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

圆中生成随机点

先从一个三角形中入手

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

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

a7badf7f0a8b75901bb309014b0dbf16.png

那万一Z超出了三角形呢

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

ca6a10a92cbabbb57249c4dc106cc969.png

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

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

7da885fa8407c54b04f043b457d0dd1a.png

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

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

d7bed008a32df0bb3c00346b579a73e3.png

现在轮到房间大小了

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

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

a4a28f36f2537d17d37a4efb8f35bc95.png

77e0fa45c39958251a69069baf84f7c9.png

分散房间

这里可以选择使用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

水平方向移动同理。

a395d0f87cbd5e0f4328de80d9177faf.png

706367dd77a1e93ddd92160c73f4af88.png

aaa821586b9df079a7b84a4a63d37ab3.png

然后在FixUpdate中调用Simualeting。

筛选主房间

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

8f8f6545aac895b2653eb736759f101b.png

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

f221c208fec027ab8c3dc9f2dfea9dae.png

三角剖分

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

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

就是类似下面的图:

edac5ad93451e72002c8ce63a14a56d5.png

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

我先走一遍流程。

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

例如:

ec07aa02c008b757870e541b1248bbea.png

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

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

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

如图:

39d709c2791f4be65457776f2aabc134.png

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

开始遍历三角形列表,

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

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

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

d9572ea7e48cb69b385d71b118bd1ebd.png

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

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

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

但似乎忽略了一种情况

2b1045f79acf4aab3371cb0b0c6942a6.png

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

dd6a42648a19111f9abdd78350e82820.png

这时候应该怎么办?

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

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

1a98b6eb05e1cc5121653d6cfd01f34b.png

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

82e7051cb7ac3599a7a8ce3b1575704b.png

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

f48cd6dbceb3ff241ed5cb05d5b0e95d.png

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

下面展示一下伪代码

输入顶点列表

对顶点的x进行排序

生成一个超级三角形,并添加到待确定三角形列表中

遍历顶点

遍历待确定三角形列表

如果该点在三角形外接圆中

将三角形拆分,分成三个边,将三个边添加到缓存区中,将本三角形删除

继续当前循环

如果该点在三角形外接圆的右侧,即该点的x>三角形外接圆最右侧

这时候所有的点都不可能在圆内,

此时将三角形添加到Delaunay三角形列表中

将三角形从待确定三角形中移除。

如果该点不在三角形外接圆右侧,这时候不能确定其他点不在三角形内部

继续当前循环。

查找缓存区中相同的边,

将相同的边根除,即一条边不止出现一次,就将这条边在列表中全部删除。

将顶点和缓存中所有的边组合成三角形。

添加剩下的三角形到delaunay三角形列表中。

算法完成

删除与超级三角形相关的三角形。

输出Delaunay三角形列表。

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

其实是这样的

b1358ee19e98bc8183f4da8bba5634f1.png

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

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

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

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

这样懂了吧。

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

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

生成超级三角形

e46ae099dcc7277967bd6c38b8602eb2.png

对顶点列表进行排序

31bff796388f735ca8cbd071289cfa66.png

遍历顶点列表

遍历三角形列表

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

a07e8cb31d379d77fcbc4dfd8204e147.png

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

c08926aa049c74acbe00b1de8133ad84.png

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

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

a1065a797c4151e696789e6de51cb624.png

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

b93d840d4145b2123c36fbbccd396f6f.png

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

2ae19fcbe4b19d733bdc34cff0fa3b24.png

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

f6b47d3b7925bc0bb6f3497d1d5e8d58.png

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

最小生成树算法

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

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

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

dbf6b43fc0ea20388302be70a098ec63.png

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

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

1a21c12df4b623debc9deb1607aa18c2.png

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

aabde7ce36ef682f4733ab1734b63beb.png

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

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

4db2362db528e7b916fdb5e4a20db5ba.png

剩下的同理

3fdb5fa8532d21ce87357f50fbe47afc.png

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

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

下面是代码:

首先对边的权重进行排序

660bdddf0184ba14a91eefec66c6f1b3.png

对边进行遍历

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

2265250aa9c2a91817018fb29153a2a5.png

至此最小生成树就结束了

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

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

b382b6f795a3c38ad50595702b92df3e.png

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

dd385afd91f1a0cc03d37b35bc884384.png

绘制房间和道路

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

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

如图

8f9b42c75b94aae04047e027819a3346.png

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

如图:

6ca1c5896f520b8cbdbbb8ab2b68ec3e.png

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

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

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

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

a9092d8ac66aeb87f65bfd6b79e277fe.png

代码:

122e8831498d6544c4fcce9d37240991.png

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

6a4242472d90794dc4ae380b19add039.png

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

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

DoorA.y = Turnpoint.y

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

553cec82cddbe65c8d2a0af098c2f4f1.png

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

5c9aefbf7667eaf62464930a14680bf7.png

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

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

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

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

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

Turnpoint.x = Turnpoint.x

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

再将两点返回

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

代码参考

e1609d299b8fbf556fb2eca19b68d9fd.png

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

(dir.x > 0) ? offset - 1 : -offset)

或者

(dir> 0) ? (roomB.size.y / 2) - 1 : -(roomB.size.y / 2)

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

411e5e8ddad928ec43ecf884f84cf441.png

如果我不减1 就会出现

心目中的6*6

3e860bfb2293a43a9bca5d71d0d645d6.png

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

7f6e89bd67ff1a8764aa02f90f99efb7.png

接下来继续上代码

5b642f2b6b9efe414b25e0474b479545.png

848ba336c322fc7f2b53bb28cb159efc.png

b5780424dcea2e99c2f9acc772be7c6a.png

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

8ff4e03695a2a4e7e887652fd6f9cf3c.png

ca6bcad325bbd32fab7053be1af54b15.png

绘制房间到Tilemap的代码:

99db8d654f80eb07aa83461c6638c9d5.png

绘制道路到Tilemap代码

53874123b7edb54667e595a07afdafdd.png

最后生成效果展示

e2d252621bb8eacca54bd1d82ca7b3fb.png

7fefba80da11a77af6c776c5ea57819f.gif

13b5fb29586aaa8d2fd4dc4b379ad054.png

c7582e8f5a5631ebf049830a9478e672.gif

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

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

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

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

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

参考文章:

随机地牢作者的博客:

随机地牢作者的github:

随机地牢的国内翻译:

圆内随机点算法:

三角剖分算法:

最小生成树算法:

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

转载地址:https://blog.csdn.net/weixin_32421193/article/details/114755006 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:java ajax 修改数据库数据库数据库_AJAX 自学练习 无刷新提交并修改数据库数据并显...
下一篇:proto 客户端 JAVA_Kubernetes官方java客户端之五:proto基本操作

发表评论

最新留言

不错!
[***.144.177.141]2024年04月21日 08时30分27秒