前言
一直对随机地图生成的算法挺感兴趣的,以前倒是看过一些文章,但是都没有亲手实践过.纸上得来终觉浅,正好这次说轮到我分享了,就想着趁这个机会自己试试做一个简单的随机地图生成的demo.在B站上也找到了一些讲这方面的教程,最后也是在教学视频的帮助下将demo做出来了.真的是可(cai)喜(de)可(kou)贺(jio),只能说咱B站大学真不是吹的.这期分享就分享下自己对这个算法的理解和一点点小改动吧.
参考链接
根据国际惯例,先放参考链接 参考洪水填充算法实现绝对通畅的随机地图生成器 BeaverJoe 2020-10-05 通过洗牌算法实现随机地图生成器 BeaverJoe 2020-10-12
正文
虽然本文主要用到的是洪水填充算法,但在此之前,我们需要先来了解一下洗牌算法
洗牌算法
洗牌算法是一个很简单但很实用的随机打乱算法,通过此算法可以等概率的拿到数组中的k个元素.其核心思想是通过交换的方式将第n次随机到的元素与数组第n个元素交换(或倒数第n个),然后在下一次随机时缩小样本空间继续这个操作,这样重复k次后,数组的前(或后)k项就是我们需要的等概率拿到的随机元素了. 转换为代码就是:
int[] xipai(int arr[],int k){
for(int i = 0; i < k;i++){
swap(arr[i],arr[Random.Range(i,arr.Length)]);
}
return arr;
}
虽然我们都知道Random是伪随机,但是我们这里不讨论其他更复杂的随机方式,虽然我之前倒是在别人的博客看到一些更接近理论随机性的操作,但是大佬也说了,不管通过什么方式计算机得到的随机数都是伪随机
在这个案例中我们正是通过洗牌算法来实现在地图中随机生成一个障碍物.
for (int i = 0; i < wallCount; i++)
{
int index = UnityEngine.Random.Range(i,_allGrids.Count);
Coord temp = _allGrids[i];
_allGrids[i] = _allGrids[index];
_allGrids[index] = temp;
Vector3 spawnPos = new Vector3(_allGrids[i].x*mapScale,0.1f,_allGrids[i].y*mapScale);
GameObject wall = Instantiate(wallPrefab,spawnPos,Quaternion.Euler(0,0,0));
wall.transform.localScale *= (1-OutlinePercent);
wall.transform.SetParent(tileRoot);
}
通过洗牌算法我们似乎成功的获得了一张具有随机障碍物的地图了,可是当我们运行后就会发现,这种方式生成的地图十分的杂乱无章.有很多路径都是无效的死路. 图1-1 仅仅通过洗牌算法生成的地图 这种情况在障碍填充率高的时候情况更甚. 图1-2 障碍填充较多时仅仅通过洗牌生成的地图
为了解决这个问题,在地图上生成随机的联通路径,我们就此引入洪水填充算法
洪水填充算法
顾名思义,这个算法的思想就是像洪水一样去蔓延填充所有可到达的区域… 咳咳咳,说人话就是从一个起点开始查找其相邻的点,以此类推逐渐扩散直到最后将所有能到达的点全部遍历一遍.看到这得朋友是不是会感觉到一丝很熟悉的感觉,这不是和图的遍历很像嘛,没错,现在实现洪水填充算法大多数用的也是DFS或BFS来实现.考虑到实际应用中应该尽量避免递归,我们采用的也是通过BFS来实现.那么获取到一个起点所有可到达的区域对我们的生成随机的通路有何意义呢?
很容易想到,当填充算法可以遍历到的点的单元格数量不为0且等同于 地图的总格子数 - 障碍物的总格子数时,地图内一定有且仅有一个可通行的区域,正是当前遍历的这个区域.
我们可以得到这样一个思路,规定一个起点后,先通过洗牌算法在除起点外的位置随机"生成"一个障碍物,然后对这个障碍物存在后的地图进行以起点开始的洪水填充,记录所有可到达点的数量,再与理论上的可到达点的数量(即上文提到的地图的总格子数 - 障碍物的总格子数)进行比较,当二者相等时则证明在这个位置生成障碍物是不影响通路的,否则则证明这个位置生成障碍物会导致通路断掉,就不能在此生成,转而去找下一个随机位置.那么代码如下
private void init_map(){
for (int i = 0; i < mapSize.x; i++)
{
for (int j = 0; j < mapSize.y; j++)
{
Vector3 spawnPos = new Vector3(i*mapScale,0,j*mapScale);
GameObject tile = Instantiate(tilePrefab,spawnPos,Quaternion.Euler(90,0,0));
tile.transform.SetParent(tileRoot);
tile.transform.localScale *= (1-OutlinePercent);
_allGrids.Add(new Coord(i,j));
}
}
switch(fullStyle){
case 1:
xipai();
break;
case 2:
hongshui1();
break;
case 3:
hongshui2();
break;
case 4:
hongshui3();
break;
}
}
private void hongshui2(){
bool [,] hasWall = new bool[(int)mapSize.x,(int)mapSize.y];
int currenWallCount = 0;
for(int i = 0; i < _allGrids.Count;i++){
int index = UnityEngine.Random.Range(i,_allGrids.Count);
Coord currenWall = _allGrids[index];
_allGrids[index] = _allGrids[i];
_allGrids[i] = currenWall;
hasWall[currenWall.x,currenWall.y] = true;
currenWallCount++;
if(currenWall != begin && CanCreateWall(hasWall,currenWallCount)){
Vector3 spawnPos = new Vector3(_allGrids[i].x*mapScale,0.1f,_allGrids[i].y*mapScale);
GameObject wall = Instantiate(wallPrefab,spawnPos,Quaternion.Euler(0,0,0));
wall.transform.localScale *= (1-OutlinePercent);
wall.transform.SetParent(tileRoot);
}else{
hasWall[currenWall.x,currenWall.y] = false;
currenWallCount--;
}
if(currenWallCount == wallCount){
break;
}
}
}
private bool CanCreateWall(bool[,] hasWall,int currenWallCount){
Queue<Coord> queue= new Queue<Coord>();
queue.Enqueue(begin);
bool [,] flag = new bool[(int)mapSize.x,(int)mapSize.y];
flag[begin.x,begin.y] = true;
int accessibleCount = 1;
while(queue.Count > 0){
Coord currenTile = queue.Dequeue();
for(int x = -1; x <= 1; x++){
for(int y = -1; y <= 1;y++){
int neighborX = currenTile.x + x;
int neighborY = currenTile.y + y;
if(x==0 || y==0){
if(neighborX >= 0 && neighborY >= 0 && neighborX < mapSize.x && neighborY < mapSize.y){
if(!flag[neighborX,neighborY] && !hasWall[neighborX,neighborY]){
flag[neighborX,neighborY] = true;
accessibleCount++;
queue.Enqueue(new Coord(neighborX,neighborY));
}
}
}
}
}
}
int obsTargetCount = (int)(mapSize.x * mapSize.y) - currenWallCount;
return (accessibleCount == obsTargetCount);
}
效果如下: 起点( 0 , 0 )
图2-1洪水填充生成的地图1 图2-2洪水填充生成的地图2
值得一提的是很明显我们应该在寻找随机点位时将之前那些查找到的不符合要求的位置排除掉,在做这个demo时我是听了思路然后尝试自己先写一个,结果在循环时我的想法是在for循环中将 “i < 要设置障碍物的数量” 设置为循环条件,遇到不符合的位置时我的操作是令 i自减,而我的随机数来源是UnityEngine.Random.Range(i,_allGrids.Count),这将导致一个严重的后果, 是的,我没有缩小随机空间,这将导致当障碍填充率较大时,查找到最后几个点位正确的概率极低,需要花费大量的时间,没错,这正是为什么上文的方法名是hongshui2而不是hongshui1,也是因为这个导致我在运行等待七分钟后被迫从任务管理器中关闭了UnityEditor…
一点改动
可以看到现在的这个效果是能随机生成固定起点的通路了,可是只有起点太不实用了,我还想要一个能够固定终点方法.于是我在判断条件中加入了当前位置不能是终点(currenWall != end) 效果如下: 起点:(0,0) 终点:(9,0) 即从左下角到右下角 图3-1带终点的填充
看上去似乎实现了,然而此时我设置的障碍填充率为1,也就是说此时地图上的障碍物应该是可以达到的最大值才对,而我的障碍物的数量的计算方式为:
maxWallCount = mapSize.x * mapSize.y - (Mathf.Abs(begin.x - end.x) + Mathf.Abs(begin.y - end.y));
很明显这个地图并没有满足这个要求,这是因为当前面随机剩下一些较边缘位置的支路时,先随机到了支路中间的点,而被判断不可生成障碍,而当这些支路的终点被填充后,前面这些点位却不会再被随机到了,便保留了下来,如下图,当黄色部分还是空地时先随机到了蓝色部分,后黄色部分被填充,蓝色部分却因被记录过了不再进入随机的范围,最后导致支路的产生. 图3-2支路的产生 于是我试着将判断方法修改了一下,将返回条件更改为能到达终点,效果如下 图3-3去掉支路后的随机地图 当然,代价就是当障碍填充率低的时候,就像洗牌算法一样,会出现死路… 图3-4出现死路的地图 这里我想到了java中的sort函数,其底层据说是根据数量的不同采用不同的排序方法,那么我们似乎也可以根据填充率的大小来使用不同的方法.
然而我实在是太菜了,不知道应该怎样算出那个合适的分界点
或者将二者结合起来,如果第一个洪水填充后当前地图的障碍量不等于我们需要的障碍量,就在第一次填充后的基础上用第二种方式填充.
由于时间有限这里我没有实现,不知道效果如何,周末有一堆作业得赶了 X _ X
这篇文章只是一个对随机地图生成很简陋的实现,还有很多很多可以改进的点.也有很多前人造好的轮子可以帮助我们,本次分享就到此为止啦,最后给大家推荐一个随机地图的轮子教程,好了我赶作业去了
|