生成种子地图

生成种子地图
MeteorCat在部分生存游戏都会在开局通过生成 seed(种子) 来做地图动态生成, 该篇章就是说明其原理和作用
首先大部分种子值是并没有要求长度(不能为空), 更进一步来说就是只要是字符串就可以作为地图种子(只要能被哈希)
hash(哈希) 就是构建地图种子的核心关键, 通过字符串生成沙盒世界的 伪随机种子(Seed), 转化流程如下
-
把输入字符串全部转为
UTF-8编码字节数组 -
使用哈希算法(FNV1a,MurmurHash3,MD5 等哈希算法)
-
截取哈希结果为 int64 长整型(正负数值)
如果对哈希效率有要求可以采用
MurmurHash3算法, 而如果需要更偏向内部集成的通用性可以采用MD5算法
这就是标准生成种子数值的流程, 以 Python 脚本来实现就是如下生成
1 | #!/usr/bin/env python3 |
相同输入文本将永远算出同一个 seed, 获取到地图随机种子值(2981560909910880916)
那么接下来的问题就是拿到该值有什么作用? 要怎么去使用这个数值?
哈希与噪音
拿到哈希值之后就需要用到另外的概念 哈希噪音(Hash Nosie), 按照网上说法来看
对网格坐标、像素 UV、区块 ID 做哈希运算, 将离散坐标映射到 0~1 均匀分布随机数, 替代传统噪声的预计算置换表
不需要直接看懂, 对于游戏开发来说只需要知道哈希噪音主要作用
-
区块坐标作为哈希输入, 每次加载同一区块的哈希输出完全一致, 地形、植被随机分布不会错乱 -
通过给大地图按照长宽高做切分区块并编号, 然后按照哈希值做种子随机生成确定区块ID是什么地形
简单概括来说, 其实你自己就可以在脑子模拟出来流程, 如下流程来思考即可
-
假设目前需要生成一张
100x100的地图 -
按照
10x10为分隔成不同区块ID(RegionID, 比如 [(0,1),(0,2)…] 的区域列表) -
通过上面提出的全局哈希
seed值, 再通过权重算法对区块赋予海洋/浅滩/沙滩/平原/森林/草地等地形类型
而其中提出的权重算法就是利用到 哈希噪音(Hash Nosie) 处理, 这里为什么不干脆直接用随机数来生成?
-
不可复现: 普通随机数每次运行结果不同, 重进游戏同区块地形会变, 会导致没办法复现具体地形
-
方便流处理: 启动游戏只需要加载玩家视野的区块, 后续玩家走到哪就拿区块坐标实时算出哈希噪声、生成地形
如果是动态生成的沙盒世界, 可以通过 输入坐标(x,z) + 全局种子 就可以输出 权重 决定是 平地、高山、深海 地形
这里通过 Python 来构建伪代码(实际上该代码可以直接运行), 方便直观看到效果
1 | #!/usr/bin/env python3 |
这段 Python 代码运行之后可以获得以下输出
1 | world = width: 100, height: 100 |
只要输入的哈希字符串保持一致, 那么生成地形的地形将永远不会变(要复现地形只需要拿到哈希字符串即可)
噪音算法
上面已经通过 均匀区间权重 实现了简单 2D 像素区块地图/Minecraft 类方块区块/简单 2D 策略地图 地形构建
这种算法实现极其简单且高效, 但是的缺陷也是十分明显: 地形完全随机, 区块之间地形无过渡
这种地形算法的弊端就是会出现相邻区块可能突然从海洋跳到森林(过渡效果突兀)
单纯文字可能没有什么概念, 所以这里按照刚才的 Python 说明这种情况, 这里微调下代码
1 | # 修改之前的权重, 追加火山地形 |
运行之后就会发现某个地形出现一些突兀的情况
1 | region = (0,0), noise = 0.32365443124394727, terrain = 火山坑 |
可以看到火山坑附加没有火山地形, 却突兀的形成了火山喷发之后冷却的形成的火山坑(看起来就有点反常识)
不过部分动态生成地图的游戏可能没有讲究这么多, 所以这种情况是可以被接受, 主要原因如下
-
高效性能: 只需要单次哈希 + 简单数值判断, 无循环、无插值、无多层噪声叠加, 海量区块同时生成也不会卡顿
-
维护成本低: 代码极简, 可以快速迭代修改地形权重
-
而以下游戏玩法不在乎连续地形, 所以直接使用是没问题的
- 像素回合制策略: 每个区块独立关卡格子, 不需要自然连续大陆
- 小型挂机放置游戏: 地图仅做背景装饰, 玩家不会细看地形逻辑
- 极简方块小游戏: 单区块为独立小岛, 区块之间天然隔离, 不存在相邻地形违和问题
但是如果你是打算构建生存建造和追求写实地理逻辑的地形, 那么就需要做更加细致的噪音算法构建
高级算法
上面的简单权重地形算法已经保证构建地形功能, 假设现在编写生存建造游戏, 那么就需要做些高级定制处理, 用到技术如下
-
Worley: 细胞噪音, 地块分割噪声, 把整张地图切割成不规则多边形地块(天然形成独立海岛、大陆分割、湖泊) -
Simplex: 群系类型噪声, 输出范围-1.0~1.0, 用于给每个分割好的地块分配环境类型- 得出数值
-1.0~-0.6: 沙漠 - 得出数值
-0.6~-0.2: 草原 - 得出数值
-0.2~0.3: 森林 - 得出数值
0.3~0.7: 沼泽 - 得出数值
0.7~1.0: 雪地
- 得出数值
这种用到方法就是 多层噪音 合并构建, 通俗来讲构建流程就是以下顺序
-
Worley哈希噪音值首次会地区分配地形, 比如分配大区块为海洋/陆地/湖泊(封闭小型陆地)地貌轮廓 -
Simplex哈希噪音值二次分配地形群系, 比如按照区间来生成沙漠/草原/森林/沼泽/雪地等主要地形 -
最后在群系地块内部, 使用哈希噪声生成细节, 比如
树木、岩石、矿石、野生动物刷新点位
这里的海洋区域指代锁死水域, 也就是封闭地区不允许玩家通过(用于地图边界限制), 如果地图是火山类型可以替换成岩浆
按照地理知识逻辑生成的区块地形:
-
大片连通陆地统一群系
-
海洋只存在海岛小型陆地, 不会穿插大片沙漠森林
-
雪地/冻土/岩浆之类成片分布, 不会零散单个地块出现
首先就是 Worley 地貌轮廓分隔处理, 这里的噪音算法就是之前常见的权重算法
1 | #!/usr/bin/env python3 |
注意:
Worley 算法划分的坐标区块和最上面说得 Region 权重区块不是一个概念
也就是说最开始的 100x100 地图通过划分 10x10 划分 Region ID 和 Worley 通过 scale 划分局域地貌 不是一个概念
这里的 worley_noise 内部 scale = 单元格缩放尺寸/单个 Worley 多边形地块的边长, 核心代码如下
1 | # 这里传入 scale = 12, 代表地图上每 12×12 世界坐标方块将合并成同一个 Worley 细胞单元格 |
上面代码核心作用就以 12 个单位为一个单位, 在 (0~12) 坐标之中的都会获取到相同地貌区块ID(cell_id)
这种方式保证多个地图像素坐落的附近都是相同地貌, 也就形成可以形成独有的地形生态, 举例理解如下
-
坐标 (0~11, 0~11) → 整除 12 后全部 = (0,0) → 同一个单元格
-
坐标 (12~23, 0~11) → 整除 12 后全部 = (1,0) → 下一个独立单元格
对于 scale 的取值, 这是需要自定义来确定
-
scale 越大(比如 20~30)
- 单个 Worley 地块尺寸更大
- 整张地图地块数量变少
- 大陆、海岛、湖泊的面积更大, 大板块地貌
-
scale 越小(比如 6~8)
- 单个地块尺寸很小
- 地图被切割成大量细碎小块
- 生成超多小型湖泊和零散小岛, 从而出现地形破碎
建议取中间值 scale = 12, 然后微调这个值来确定最终地貌信息, 之后就是 simplex 算法处理
注意: 只有
terrain_type为非封锁区域才允许走simplex算法处理
利用 Python 重新构建出整体功能, 我这边规划重写整体脚本:
1 | #!/usr/bin/env python3 |
这里初步输出内容如下, 可以具体测试和改进下具体功能
1 | {'x': 0, 'y': 0, 'scale': 12, 'cell_id': 1601, 'base_terrain': 'land', 'biome': '沼泽', 'resource': '空地'} |
内部其实需要修改的是 get_biome_by_simplex(获取地形再分配) 和 get_block_resource(获取区块资源) - 可以用 excel 调整




