这次直接将后面的内容一次性讲完啦。其实本来是想一个一个视频的讲的,但是今天才发现我上次准备好的内容没有写然后没有发,所以今天就一起讲了,其实剩下的视频是有三个的,最后一个视频主要是作者的应用,写了一些简单的控制脚本让角色在场景中运动,所以就把前两个视频的原理讲清楚好了。先附上视频原址:https://www.youtube.com/watch?v=NhMriRLb1fs&list=PLFt_AvWsXl0eZgMK_DT5_biRkWXftAOf9&index=7,https://www.youtube.com/watch?v=7RiGikVLS3c&list=PLFt_AvWsXl0eZgMK_DT5_biRkWXftAOf9&index=8,https://www.youtube.com/watch?v=oS0iEGX_FM8&list=PLFt_AvWsXl0eZgMK_DT5_biRkWXftAOf9&index=9。

好了,进入正题。

上一篇文章主要讲怎么让房间与房间连接起来,但是上一篇文章的结尾也讲到了,连接的效果最终只是让房间不孤立,但是并不能让所有的房间都连接在一起,所以首先我们要有一个进阶性的规则来让所有的房间连接起来。

之前在优化地图的时候,我们设置了一个阈值,让房间内的点小于这个阈值的时候就被移除,相反大于阈值的时候就被我们保留了下来,所以我们就可以拿到剩下的所有的房间的信息列表survivingRooms。这里我们怎么判定所有的房间都已经连接在一起了呢?先定义一个mainRoom(主房间),mainRoom就是survivingRooms列表中含有的地图信息点数最大的房间,所以我们先对survivingRooms进行排序,获取到最大的房间并设置其为主房间,当所有的房间都可以找到一条路径(无论是直达还是通过其他的房间简介到达)到达主房间,那么就知道所有的房间现在就都是相同的了。所以根据这个规则我们来写连接所有的房间。

为了方便访问,我们在Room类当中继续添加相应的变量

public bool isAccessibleFromMainRoom;   //是否能够到达主房间
        public bool isMainRoom;                 //是否是主房间

而且我们要修改ConnectRooms函数

        /// <summary>/// 设置可连接属性/// </summary>public void SetAccessibleFromMainRoom(){if (!isAccessibleFromMainRoom){isAccessibleFromMainRoom = true;foreach (Room connectedRoom in connectedRooms){connectedRoom.SetAccessibleFromMainRoom();}}}/// <summary>/// 连接两个房间的方法,其实就是将A房间添加到B房间的连接房间列表,将B房间添加到房间A的房间列表中/// </summary>/// <param name="roomA"></param>/// <param name="roomB"></param>public static void ConnectRooms(Room roomA, Room roomB){if (roomA.isAccessibleFromMainRoom){roomB.SetAccessibleFromMainRoom();}else if (roomB.isAccessibleFromMainRoom){roomA.SetAccessibleFromMainRoom();}roomA.connectedRooms.Add(roomB);roomB.connectedRooms.Add(roomA);}

这两个函数首先是连接两个房间,如果其中有一个房间是能够到达主房间的那么就要去设置另外一个房间的连接到主房间的属性为true,并且当自己这个房间能连接到主房间的时候,说明之前已经与自己相连接的房间也是能够到达主房间,所以要遍历一并设置。

下面我先贴代码,然后再讲逻辑。

    /// <summary>/// 连接最近的房间/// </summary>/// <param name="allRooms"></param>void ConnectClosestRooms(List<Room> allRooms, bool forceAccessibilityFromMainRoom = false){List<Room> roomListA = new List<Room>();List<Room> roomListB = new List<Room>();if (forceAccessibilityFromMainRoom){//如果这次连接操作是必须要与住房间相连接,那么roomListA存放着已经能够连接到主房间的房间列表,roomListB存放无法与主房间连接的房间foreach (Room room in allRooms){if (room.isAccessibleFromMainRoom){roomListB.Add(room);}else{roomListA.Add(room);}}}else{//如果不是强连接主房间,那么就是让房间至少能与其中一个房间相连接。roomListA = allRooms;roomListB = allRooms;}int bestDistance = 0;Coord bestTileA = new Coord();      //房间a与房间b连接的最好也就是最近的两个方块Coord bestTileB = new Coord();  Room bestRoomA = new Room();        //找到相聚最近的两个房间Room bestRoomB = new Room();bool possibleConnectionFound = false;   //可能连接的flag,如果为false,说明本次循环不需要为两个房间创建连接foreach (Room roomA in roomListA){if (!forceAccessibilityFromMainRoom){//如果不是强链接,那么如果已经有房间连接了那么就跳过possibleConnectionFound = false;if (roomA.connectedRooms.Count > 0){continue;}}foreach (Room roomB in roomListB){if (roomA == roomB || roomA.IsConnected(roomB)) continue;//if (roomA.IsConnected(roomB))//{//    //遍历发现房间A已经有连接的房间了,设置变量,然后跳过这个房间//    possibleConnectionFound = false;//    break;//}//2层for循环遍历房间A和房间B的所有边界方块。for (int tileindexA = 0; tileindexA < roomA.edgTiles.Count; tileindexA++){for (int tileIndexB = 0; tileIndexB < roomB.edgTiles.Count; tileIndexB++){Coord tileA = roomA.edgTiles[tileindexA];Coord tileB = roomB.edgTiles[tileIndexB];//计算两个方块的距离的平方int distanceBetweenRooms = (int)(Mathf.Pow((tileA.tileX - tileB.tileX), 2) + Mathf.Pow((tileA.tileY - tileB.tileY), 2));if (distanceBetweenRooms < bestDistance || !possibleConnectionFound){bestDistance = distanceBetweenRooms;possibleConnectionFound = true;bestTileA = tileA;bestTileB = tileB;bestRoomA = roomA;bestRoomB = roomB;}}}}//如果不是强连有房间就可以连接,如果是强链接那么就要等到所有没有与主房间连接的列表遍历完,找到一个能连接主房间并且最近的那个房间。if (possibleConnectionFound && !forceAccessibilityFromMainRoom){CreatePassage(bestRoomA, bestRoomB, bestTileA, bestTileB);}}//找到了一个能与主房间连接的最近的房间,并且连接,连接结束接续执行该函数,并且是要强链接。如果是非强链接就会执行后面的if语句,如果是强链接,但是又没有房间,说明当前所有房间都能 够连接到主房间if (possibleConnectionFound && forceAccessibilityFromMainRoom){CreatePassage(bestRoomA, bestRoomB, bestTileA, bestTileB);ConnectClosestRooms(allRooms, true);}//如果是非强链接,那么已经做完了第一次遍历if (!forceAccessibilityFromMainRoom){ConnectClosestRooms(allRooms, true);}}

这个是对之前的遍历连接所有房间的代码的修改。可以看到函数多了一个需要传进来的参数:forceAccessibilityFromMainRoom,这个bool变量意思就是本次连接房间的函数是否是要强制连接到主房间的,如果是false就是上一篇文章中的做法,只是达到房间不孤立的目的,如果是true,那么本次连接一定会有新的房间列表与主房间连接。不能理解没关系,接着看先就好理解了。跟之前的代码不同的是,这里用了两个房间的列表来存房间的信息,如果forceAccessibilityFromMainRoom是false跟之前一样,所以roomListA,roomListB就是所有的房间的信息,两个foreach循环连接房间,这里就跳过。如果forceAccessibilityFromMainRoom = true,说明这次循环是来连接主房间的,所以roomListB存的是已经能够连接到主房间的列表,roomListA是尚未能够连接到主房间的列表。跟之前一样我们一样要两层的for循环去遍历这两个房间列表,拿到列表中的房间一一处理。原理跟forceAccessibilityFromMainRoom = false的情况是一样的,我们遍历roomListB中所有的房间,然后从RoomListA和RoomList中找到bestRoomA和bestRoomB是相聚最近的两个房间,退出循环之后我们就连接这两个房间,这样一次函数的执行就能让一个互相相连接的小的房间列表与主房间对接上,然后继续执行这个函数,知道找不到房间的时候此时possibleConnectionFound = false,那么说明所有的房间都已经建立起了连接。这样优化过后的连接效果就很不错了。下面是运行出来的效果图:

效果是挺不错的。那么接下来就是替换绿色的线,让其生成出路径来。

首先看这么一张图,这张图是从视频截下来,更好的传达作者的思想(本人愚笨,看个视频的这个地方反复看自己纸上反复写好久才理解,简直了)

这个黑色的线覆盖过的格子用红色的格子来体现,那么我们怎么通过头尾两个点来获取线覆盖到的所有的点呢。根绝头尾点我们可以得到dy = 1,dx = 4,此时可以得到先的函数是y = x / 4 。如果此时y++,那么就要满足条件dy * x/ dx + 0.5 >= 1,这里0.5和1的来源是在定义格子的间距是1,从图中可以看到由于我们起始的时候就处于方形的中心,所以要加上0.5。所以我们每次都对x++知道满足上面的条件的时候y++,既然拿到条件了那么就利用这个公式来编写函数。但是还要考虑其他的情况,比如下面的图所示:

此时|dy| > |dx|就不能像之前的情况通过x++来判断y++,由于dy<0并且|dy| > |dx|,所以要用y--来判定x++,这样做的目的其实很明确,如果用相对较短的一遍来递增或者递减,另外一个变量的变化梯度比较大可能会丢失很多中间的点。

这里直接给出代码再讲解吧。

    /// <summary>/// 创建通道/// </summary>/// <param name="roomA"></param>/// <param name="roomB"></param>/// <param name="tileA"></param>/// <param name="tileB"></param>void CreatePassage(Room roomA, Room roomB, Coord tileA, Coord tileB){Room.ConnectRooms(roomA, roomB);Debug.DrawLine(CoordToWorldPoint(tileA), CoordToWorldPoint(tileB), Color.green, 100);List<Coord> line = GetLine(tileA, tileB);foreach (Coord c in line){DrawCircle(c, 2);}}/// <summary>/// 以一个中心点和半径,将园内的点设置成0/// </summary>/// <param name="c"></param>/// <param name="r"></param>void DrawCircle(Coord c, int r){//从-r到+r,算出距离小于r的点,说明在园内,然后判断是否是在地图信息中避免越界,并且设置为路径for (int x = -r; x <= r; x++){for (int y = -r; y <= r; y++){if (x * x + y * y <= r * r){int drawX = c.tileX + x;int drawY = c.tileY + y;if (IsInMapRange(drawX, drawY)){map[drawX, drawY] = 0;}}}}}/// <summary>/// 将线转化成点/// </summary>/// <param name="from"></param>/// <param name="to"></param>/// <returns></returns>List<Coord> GetLine(Coord from, Coord to){//参考公式: dy * x + dx / 2 >= dx;(推导出来的公式)List<Coord> line = new List<Coord>();int x = from.tileX;int y = from.tileY;int dx = to.tileX - from.tileX;     //两个点的x,y的差值int dy = to.tileY - from.tileY;bool inverted = false;              //是否需要变向的flagint step = Math.Sign(dx);           //获取符号大于零为+1,小于零为-1int gradientStep = Math.Sign(dy);int longest = Mathf.Abs(dx);        //先默认x是较长的一边int shortest = Mathf.Abs(dy);//如果x单位变化的长度小于y的单位变化,那么就反向,如果反向的话就是对y进行操作,加还是减看step的值if (longest < shortest){inverted = true;longest = Mathf.Abs(dy);shortest = Mathf.Abs(dx);step = Math.Sign(dy);gradientStep = Math.Sign(dx);}//根据公式获得梯度积累为长边的一半int gradientAccumulation = longest / 2;for (int i = 0; i < longest; i++){line.Add(new Coord(x, y));if (inverted){y += step;}else{x += step;}gradientAccumulation += shortest;//这里的理解方式我用具体的数值来描述,我怕我讲不清楚/**  假如 线的函数是 y = 0.4x; 假设是点(0,0) 和点(5, 2)算出来的 gradientAccumulation = 5 / 2 = 2(整数),step = 1, *  longest = 5, shortest= 2, gradientStep = 1*  那么当 gradientAccumulation += 2 ,此时gradientAccumulation = 4, y值不加1,  继续*  gradientAccumulation += 2 ,此时gradientAccumulation = 6 > 5, y ++, gradientAccumulation -= 5*  *  假如先的函数是 y = -0.4x + 5, 假设是点(5,2)和点(0,0)算出来的, 所以longest = 5,  shortest = 2, gradientStep = 1,*  step = -1, gradientAccumulation的变化同理*/if (gradientAccumulation >= longest){if (inverted){x += gradientStep;}else{y += gradientStep;}gradientAccumulation -= longest;}}return line;}

我在代码中也注释了一部分的理解,如果下面看不懂的可以结合一下代码,最好能在草稿纸上画画写写,这样不会那么抽象。我们通过传进来之前获取到的两个房间距离最近的点,然后通过点的关系我们可以得到两个点形成的线来获取到所有被线覆盖的格子的信息的列表。拿到这样一个列表之后那么就就将列表中的所有的点都取出来,然后给定一个半径r,让该点周围在半径为r的范围内的地图信息都设置为0。

连接完房间之后就调用创建路径的函数,这样在通过mesh的绘制就能拿到最终房间道路生成出来之后的效果。

最后一个视频是对地图的应用,一种是3D的模式,为墙体添加meshCollider然后写一个简单的player控制脚本,另一种是2D的模式,为地图的边界添加EdgeCollider,具体的应用肯定是要根据每个人自己的需求来,所以最后一个视频就不多做讲述了,那么这个系列的视频就讲完了,最后地图生成出来的效果如下面所示:

最后,因为这个系列的文章是我第一次写博客,所以很多地方的讲述非常的混乱,我希望不会对读者带来不好的影响,很多地方我更加偏向于写注释,可能是个人在看别人的代码的时候觉得读代码和注释比较容易理解。

这里给了完整项目的网盘的连接:https://pan.baidu.com/s/1wT8qM74Ljuo__nQNB1qxag

完结~ 。 piapiapia。

程序洞穴生成六(Procedural Cave Generation)相关推荐

  1. 程序洞穴生成三(Procedural Cave Generation)

    上一篇文章我们已经将地图的信息转换成了模型需要的Meh信息,但是生成的是一个平面的效果,所以接下来就要通过其他的方法生成3D的墙面,将上一篇生成的地图的边界衍生出垂直的墙面. 本篇本章的内容的视频链接 ...

  2. 程序洞穴生成一(Procedural Cave Generation)

    这边博客是我自己学习过程中写的第一篇文章,一方面是想与大家分享好的知识,另一方面是为了监督自己不断的学习,希望能给大家带来好的内容分享. 这篇文章的内容是搬运自Youtube上的Sebastian大佬 ...

  3. GPU Pro2 - 3.Procedural Content Generation on the GPU

    GPU Pro2  - 3.Procedural Content Generation on the GPU 这篇文章着重介绍了基于Brownian 噪声和高度图在GPU中实时生成和渲染无限大地形系统 ...

  4. Procedural Landmass Generation开源库测评

    [博物纳新]是UWA旨在为开发者推荐新颖.易用.有趣的开源项目,帮助大家在项目研发之余发现世界上的热门项目.前沿技术或者令人惊叹的视觉效果,并探索将其应用到自己项目的可行性.很多时候,我们并不知道自己 ...

  5. python随机生成30个8_Python生成六万个随机,唯一的8位数字和数字组成的随机字符串实例...

    上代码: 环境:Python3 import random,string s=string.ascii_letters+string.digits print(s) n={''.join(random ...

  6. ASP程序快速生成Excel文件

    ASP程序快速生成Excel文件 在一个web项目中,要求将数据生成Excel文件保存到本地,最早使用的方法是直接使用Microsoft的Office Web组件,但是总体感觉是慢(微软的通病).然后 ...

  7. 给Android程序员的六个建议

    给Android程序员的六个建议 分类: 安卓相关2015-07-14 23:58 177人阅读 评论(0) 收藏 举报 android程序员 如果你一年前写的代码 , 在现在看来你还感觉写的很不错 ...

  8. Java黑皮书课后题第7章:*7.7(统计个位数的数目)编写一个程序,生成0和9之间的100个随机整数,然后显示每一个数出现的次数

    *7.7(统计个位数的数目)编写一个程序,生成0和9之间的100个随机整数,然后显示每一个数出现的次数 题目 题目描述 破题 代码 运行示例 题目 题目描述 *7.7(统计个位数的数目)编写一个程序, ...

  9. 额,你在main.xml中加了一个id以后,要右键点save,才会将这个id加入到R中,否则是没有的。。。R里的东西是程序自动生成的~~~...

    我修改了 main.xml 然后我在主函数中调用main里面的东西 例如在main.xml加了一个id 我在主函数用 R.id老是调用不出来 如何才能快速的准确的调用出 main.xml里面的东西呢? ...

最新文章

  1. 连接惠普打印机(通过WIFI)
  2. CentOS7.4下DNS服务器软件BIND安装及相关的配置(一)
  3. 通过反射实现IOC功能
  4. Linux 内核详解以及内核缓冲区技术
  5. 每日一皮:重构时总会出现的惊喜.......
  6. Tensorflow 模型加载及部分变量初始化
  7. 推荐系统炼丹笔记:推荐算法特征交叉新方式CAN
  8. 07-CoreData清除所有数据
  9. Mac OS使用技巧之十:Finder的详细使用方法
  10. 配置Windows Server 2008群集
  11. vue项目中的跑马灯的使用
  12. jQuery length和size()区别总结如下:
  13. Game Engine on Vulkan 01-preface [Vulkan游戏引擎开发 01-引言]
  14. 【科研人应该知道的网站】查阅文献+学习+代码+开发+其他——研究生必备学习网站,研究生应该知道的学习网站
  15. More Accurate Question Answering on Freebase阅读笔记
  16. 深圳大学计算机网络实验五:Socket编程
  17. MAXIMO学习笔记
  18. Android手电筒案例
  19. 在 Docker 上搭建 PostGIS 数据库实现空间数据存储及可视化
  20. vue项目的首屏优化策略

热门文章

  1. 微信文件直接打印方法,微信文件怎么直接打印
  2. Selenium 显示等待、隐式等待及流畅等待
  3. 其实你什么都不用担心
  4. 汇川一拖二伺服_汇川技术IS620N伺服(EtherCAT高速总线通信伺服)数码管显示
  5. 用Python分析幸福指数
  6. maya遇到渲染慢渲染卡顿问题怎么办?
  7. 古月居 ROS 21 讲5
  8. 图像显著性论文(1-3)
  9. xstream特殊字符转义问题
  10. [IE技巧] IE8 网页兼容问题报告工具