我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!
随着技术的发展和游戏内容越来越多地由算法生成,我们不难想象为每个玩家创造具有独特体验的逼真模拟游戏。
技术突破、耐心和完善的技能将帮助我们实现这一目标,但第一步是理解程序性内容生成。
尽管存在许多地图生成的现成解决方案,但本教程将教你使用JavaScript从头开始制作自己的二维地下城地图生成器。
- 可进入和不可进入的区域(隧道和墙壁)。
- 玩家可以导航的连接路线。
本教程中的算法来自 Random Walk Algorithm,,这是地图生成的最简单的解决方案之一。
接下来,让我们通过地图生成算法来看看它是如何实现的:
- 制作墙壁的二维地图
- 在地图上随机选择一个起点
- 而隧道的数量不是零
- 从最大允许长度中选择一个随机长度
- 随机选择一个方向(右,左,上,下)
- 在那个方向绘制一条隧道,同时避免地图的边缘
- 减少隧道数量并重复while循环
- 返回带有更改的映射
这个循环一直持续到隧道的数量为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
下面的地图说明了这些操作:
现在,将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;
};
\