关于随机地图生成

本文介绍了如何利用散列函数生成随机数,结合地形噪声技术构建基础地形,并通过控制点权重分配实现特定结构。通过多步骤优化,包括添加多个控制点、使用Lloyd算法优化,最终形成具有地理特征的地图。此外,还详细阐述了如何通过扫描和连接像素格子来识别和构建地图中的区域和内部孔洞。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一直很想做一个模拟现实世界的游戏,最近开始动手了!既然是一个世界那么地图自然是第一个要解决的问题,网上有很多生成地图的教程,但找不到一个完整的文章。经过几天的研究总结了一些问题。

第一步:散列函数

想在计算机中生成随机数并不容易,而且完全随机的数字对生成地图并没有什么帮助,最开始我想过使用无理数的小数部分来充当随机数列,但计算无理数是一个非常耗时的过程。拜读多位博主的博文后我找到一个不错的散列函数,其核心思想是使用移位、精度溢出来产生散列值。如下(java/c++通用)

int hash(int key) { 
	key = ~key + (key << 15);
    key = key^(key >>> 12); 
	key = key + (key << 2); 
	key = key^(key >>> 4); 
	key = key * 2057;
	key = key^(key >>> 16); 
	return key;
}

然而生成高程时有x,y两个参数,当需要更多数据时需要更多参数,这时可以使用hash(x+hash(y))、hash(seed+hash(x+hash(y))这样的嵌套方式来获得散列值。

第二步:生成基础地形

网上对于地形生成搜索出来的全是关于value噪声、柏林噪声来获得高程图,效果如下

(下面的图绿色为高度0.5以上的地块高度越高颜色越浅,蓝色为高度0.5以下的地块越低颜色越深)

虽然有了高低错落、零零散散的自然美感,但这显然无法称之为地图。

我需要使用一些其他的方法来使它在更大尺度上具有一定的结构,比如在图上选一个点,以此点为中心,在一定范围内距离越远高度越低,一言以蔽之就是画一个圆。如下(黄色点为控制点的中心位置)我们使用第二张图片的高程为基础高程(要求:最大值为1权重1/2)第一张图片为辅助高程(要求:最大值为1权重1/3),剩下1/6的权重使用连续度较差的柏林噪声,重新生成图片:

控制点权重1/2的原因是该处必须保证生成陆地,由于控制点中心的高程为1,权重是1/2,得到的高度至少是海面高度0.5,即使其他高程全部为0也可以保证陆地在控制点处生成。当然也可以增加噪声层数,只要保证权重合理,高程映射到[0,1]之间即可。

现在我们添加多个控制点

这些圆的中心坐标是随机生成的,但位置被限制在x∈[width/4,3*width/4],y∈[height/4,3*height/4],防止控制点靠近边缘,生成后使用lloyd算法松弛一次,避免控制点拥挤在一起。

注意这些圆有大有小,这表示他们的控制范围,也是通过种子随机生成的。

以上所有的图像均使用相同的种子生成

再看看其他种子的图像

地图有了下一步需要分析地图中的图块,比如随机找一个陆地位置,寻找一个岛屿由于地图中的每个像素都是一个方块,我们可以从左到右从下到上逐行扫描地图,将同一类型的格子合并到一个多边形中,如图

每个格子初始有5个点,从左下角开始逆时针一圈回到左下角得到一个格子的轮廓,这个轮廓使用双向链表保存,便于后面搜索和修改。

扫描时使用一个数组保存扫描线上每个格子右上角顶点的引用(称a数组),使用另一个数组保存每个顶点所属的区域(称b数组),只要当前格子扫描完成便将对应的信息填入对应数组

当扫描到B时从a数组中取得A右上角的顶点,向前回溯一个找到A的右下角,新建两个顶点并连接到此处,完成操作

当扫描到C时通过a数组获取A右上角顶点的引用,新建两个顶点并连接到这个节点上,完成操作

扫描到D时,通过b数组发现左侧下方和左下均是本区域的节点,直接修改B左上角节点的坐标到D右上角坐标,完成操作

扫描到E时发现左和下不是相同区域所以它当前是独立的区域,但到达F时发现左与下是相同类型区域,需要合并。1、从a数组中拿到E的右上角顶点,向前回溯一个找到E的右下角,再通过a数组找到H右上角,向后移动一个顶点得到H的左上角。2、E从右下角拆开,H从左上角拆开,重新连接(不太容易描述,H与E均是逆时针连接,将边界顶点正确连接即可)。3、将E所属区域的多边形终点删除,之后将其尾节点连接到首节点之后,由于已经从E右下角拆开并连接到H的左上角,现在E原先所属区域的边界已经与H所属区域合并合。4、删除E所属的区域信息,保留H所属区域。5、替换B数组中所有E数组所属区域为本区域。6、由于左和下均是本区域,所以按照D的添加方式添加H点,完成操作

扫描到K时发现左和下均是本区域,但左下角的L(需要单独一个变量保存左下角所属区域)不属于本区域,此时判断本区域内部出现孔洞,1、使用类似E的方式连接J和M。2、新建一个链表将M左上角作为起点J右下角作为终点保存在本区域的内部孔洞列表中。3、新建一个顶点坐标为K的右上角。4、将此顶点连接到M的右上角之后,完成操作。

这样造成内部孔洞的顺序是顺时针存储的。如果需要保持与外轮廓相同的旋转方向需要额外操作。

通过上面的算法我们得到各个区域的外轮廓和内部孔洞。以便于更多操作。

### 随机地图生成算法概述 随机地图生成是一种常见的游戏开发技术,用于创建程序化的内容。这不仅增加了游戏的重玩价值,还减少了设计师的工作量。对于二维网格地图而言,一种流行的方法是通过迷宫生成算法来构建连通的地图结构。 #### 使用深度优先搜索(DFS)实现随机地图生成 为了简化问题并提供清晰的理解,下面展示了一个基于深度优先搜索 (Depth First Search, DFS) 的简单随机地图生成器。此方法适用于生成具有单一路径特性的迷宫状地图[^1]。 ```typescript // 定义方向常量 const DIRECTIONS = [ { dx: 0, dy: -2 }, // 上 { dx: 2, dy: 0 }, // 右 { dx: 0, dy: 2 }, // 下 { dx: -2, dy: 0 } // 左 ]; function generateMaze(width: number, height: number): string[][] { let maze: string[][] = Array.from({ length: height }, () => Array(width).fill('W')); // 'W' 表示墙 function carvePassagesFrom(cx: number, cy: number) { shuffle(DIRECTIONS); for (let dir of DIRECTIONS) { const nx = cx + dir.dx; const ny = cy + dir.dy; if (nx >= 0 && nx < width && ny >= 0 && ny < height && maze[ny][nx] === 'W') { maze[cy + Math.floor(dir.dy / 2)][cx + Math.floor(dir.dx / 2)] = ' '; // 打开墙壁 maze[ny][nx] = ' '; carvePassagesFrom(nx, ny); } } } // Fisher-Yates Shuffle Algorithm to randomize directions order function shuffle(array: any[]) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } // 开始挖掘通道 carvePassagesFrom(1, 1); return maze; } console.log(generateMaze(21, 21).map(row => row.join('')).join('\n')); ``` 这段代码实现了基本的随机迷宫生成逻辑,其中`generateMaze()`函数接受宽度和高度参数作为输入,并返回一个由字符组成的数组表示的地图。“W”代表不可穿越的墙体,“ ”(空格)则为空旷区域或走廊。该过程始于左上角的一个单元格,并利用递归的方式向四个可能的方向之一扩展直到无法继续为止。每当找到新的未访问过的邻居节点时,在当前节点与其之间移除障碍物形成连接。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值