如何编写自己的程序地下城地图生成器使用随机漫步算法

1,256 阅读5分钟

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!

随着技术的发展和游戏内容越来越多地由算法生成,我们不难想象为每个玩家创造具有独特体验的逼真模拟游戏。

技术突破、耐心和完善的技能将帮助我们实现这一目标,但第一步是理解程序性内容生成。

尽管存在许多地图生成的现成解决方案,但本教程将教你使用JavaScript从头开始制作自己的二维地下城地图生成器。

  1. 可进入和不可进入的区域(隧道和墙壁)。
  2. 玩家可以导航的连接路线。

本教程中的算法来自 Random Walk Algorithm,,这是地图生成的最简单的解决方案之一。

接下来,让我们通过地图生成算法来看看它是如何实现的:

  1. 制作墙壁的二维地图
  2. 在地图上随机选择一个起点
  3. 而隧道的数量不是零
  4. 从最大允许长度中选择一个随机长度
  5. 随机选择一个方向(右,左,上,下)
  6. 在那个方向绘制一条隧道,同时避免地图的边缘
  7. 减少隧道数量并重复while循环
  8. 返回带有更改的映射

这个循环一直持续到隧道的数量为0。

算法代码

由于地图由隧道和壁单元组成,我们可以将其描述为二维数组中的0和1,如下所示:

map = [[1,1,1,1,0],
       [1,0,0,0,0],
       [1,0,1,1,1],       
       [1,0,0,0,1],       
       [1,1,1,0,1]]

由于每个单元格都在一个二维数组中,我们可以通过知道它的行和列来访问它的值,例如map [row] [column]。

在编写算法之前,需要一个助手函数,它接受字符和维度作为参数,并返回一个二维数组。

createArray(num, dimensions) {
    var array = [];    
    for (var i = 0; i < dimensions; i++) { 
      array.push([]);      
      for (var j = 0; j < dimensions; j++) {  
         array[i].push(num);      
      }    
    }    
    return array;  
}

要实现随机行走算法,需要设置地图的尺寸(宽度和高度)、变量maxtunnels和变量maxlength。

createMap(){
 let dimensions = 5,     
 maxTunnels = 3, 
 maxLength = 3;

接下来,使用预定义的helper函数创建一个二维数组(一维数组)。

let map = createArray(1, dimensions);

设置随机列和随机行,为第一条隧道创建随机起点。

let currentRow = Math.floor(Math.random() * dimensions),       
    currentColumn = Math.floor(Math.random() * dimensions);

为了避免对角线转弯的复杂性,算法需要指定水平和垂直方向。每个单元格都位于一个二维数组中,可以用它的行和列来标识。因此,方向可以定义为对列号和行号进行减法和/或加法。

例如,要访问2附近的一个单元格,可以执行以下操作:

  • 向上,从这行减去1 [1] [2]
  • 往下,对[3] [2]行加1
  • 向右,向列[2] [3]加1
  • 向左,从它的列[2] [1]减去1

下面的地图说明了这些操作:

1_P1AfxAKl6SAQMgn8SONUGQ

现在,将directions变量设置为以下值,算法将在创建每个隧道之前选择这些值:

let directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];

最后,初始化randomDirection变量来保存方向数组中的一个随机值,并将lastDirection变量设置为一个空数组来保存以前的randomDirection值。

注意:lastDirection数组在第一次循环中是空的,因为没有以前的randomDirection值。

let lastDirection = [], 
    randomDirection;

接下来,确保maxTunnel不是零,并且维度和maxlengthvalue已经被接收。继续寻找随机方向,直到你找到一个与lastDirection不相反或不相同的方向。这个do while循环有助于防止重写最近绘制的隧道或连续绘制两个隧道。

例如,如果lastTurn为[0,1],则do while循环将阻止函数向前移动,直到randomDirection设置为非[0,1]或相反的值[0,-1]。

do {         
randomDirection = directions[Math.floor(Math.random() * directions.length)];      
} while ((randomDirection[0] === -lastDirection[0] &&    
          randomDirection[1] === -lastDirection[1]) || 
         (randomDirection[0] === lastDirection[0] &&  
          randomDirection[1] === lastDirection[1]));

在do while循环中,有两个主要条件被|| (OR)符号除。条件的第一部分也包括两个条件。第一个检查randomDirection的第一项是否与lastDirection的第一项相反。第二个检查randomDirection的第二项是否与最后一个回合的第二项相反。

举例来说,如果lastDirection是[0,1],randomDirection是[0,-1],条件的第一部分检查randomDirection[0] === - lastDirection[0]),它等于0 === - 0,并且为真。

然后,它检查if (randomDirection[1] === - lastDirection[1]),它等于(-1 === -1),也为真。因为两个条件都为真,算法会返回去寻找另一个randomDirection。

条件的第二部分检查两个数组的第一个和第二个值是否相同。

在选择一个满足条件的randomDirection之后,设置一个变量从maxLength中随机选择一个长度。将tunnelLength变量设为0,作为服务器的迭代器。

let randomLength = Math.ceil(Math.random() * maxLength),       
    tunnelLength = 0;

当隧道长度小于randomLength时,通过将cell的值从1变为0来创建隧道。如果在循环中隧道碰到了地图的边缘,循环就会中断。

while (tunnelLength < randomLength) { 
 if(((currentRow === 0) && (randomDirection[0] === -1))||  
    ((currentColumn === 0) && (randomDirection[1] === -1))|| 
    ((currentRow === dimensions — 1) && (randomDirection[0] ===1))||
 ((currentColumn === dimensions — 1) && (randomDirection[1] === 1)))   
 { break; }

否则使用currentRow和currentColumn将映射的当前单元格设置为零。通过设置currentRow和currentColumn在即将到来的循环迭代中需要它们的位置,在randomDirection数组中添加值。现在,增加tunnelLength迭代器。

else{ 
  map[currentRow][currentColumn] = 0; 
  currentRow += randomDirection[0];
  currentColumn += randomDirection[1]; 
  tunnelLength++; 
 } 
}

在循环形成一个隧道或通过撞击地图的边缘而破裂后,检查隧道是否至少有一个街区长。如果是这样,设置lastDirection为randomDirection并递减maxTunnels,然后返回使用另一个randomDirection创建另一个隧道。

if (tunnelLength) { 
 lastDirection = randomDirection; 
 maxTunnels--; 
}

这个IF语句防止for循环碰到map的边缘,并且没有形成一个至少一个单元格的通道来递减maxTunnel并更改lastDirection。当这种情况发生时,算法会继续寻找另一个随机方向。

当它完成隧道的绘制并且maxTunnels为0时,返回所有的回合和隧道。

}
 return map;
};

\