简介:hexgen 是一套基于 Python 3 的六角形世界地图生成器,面向游戏开发者、独立制作人与程序化生成爱好者。它通过参数化六边形网格生成随机行星表面,并能按需将地图分割为任意大小的“领土”,既可用于浏览器游戏的原型验证,也能为 DnD 跑团快速生成战役底图。压缩包共 34 个文件、548KB,代码以 Python 脚本为主(20 个 .py),并配有说明文档、依赖清单、单元测试与文档生成脚本,目录结构清晰,便于快速安装与二次开发。内容上涵盖河流、地形高度、气候、边界、领土等生成模块,示例脚本可一键演示默认效果,调整参数即可得到风格各异的世界地图。从源码中还能看到六边形网格建模、随机地形与气候叠加、领土划分等常见程序化生成思路,配合测试用例和文档可以加深理解;已有 1547 人学习下载,适合希望系统掌握六边形地图生成算法或快速搭建随机地图工具的开发者参考。
1. hexgen:把六角格世界的随机生成变成一条命令的 Python 工具
某开发者在做策略游戏原型时,需要一张六边形格子组成的行政地图,上百个格子手动摆位置、调坐标,一个下午耗进去,最后导出的图还全是锯齿缝隙。后来他改用了一个 Python 小工具 hexgen:指定尺寸和随机种子,就能在几秒内输出一张带地形起伏、海岸线、植被分布的世界地图 PNG。这个项目解决的核心问题不是“画得漂亮”,而是把六边形网格系统、噪声地形生成、生物群落分配这三块地图生成最繁琐的环节封装成一条可复现的流程。对做回合制策略、跑团战役地图、地理模拟实验的开发者来说,hexgen 是一个可以直接拿来改的起点。下文从坐标算法讲到渲染输出,拆清楚每一步怎么算、怎么调,以及实际踩过的坑。
2. 技术选型与整体架构:从噪声采样到六角格渲染的完整链路
2.1 技术栈拆解:坐标系统、噪声、渲染三件套
hexgen 的技术栈非常收敛,没有引入重型游戏引擎或地理信息系统库,核心依赖只有 numpy、Pillow(PIL)和 opensimplex 三个。numpy 负责把高度图数据组织成二维数组,方便做向量化运算和阈值筛选;opensimplex 提供 Simplex 噪声生成,比传统 Perlin 噪声在二维网格上产生的伪随机更自然,方向性伪影更少;Pillow 负责最终把六边形格子绘制成 PNG 图片。
选择 Simplex 噪声而不是单纯用随机数,是因为地图需要“连续起伏”的地形——相邻格子之间高度变化要有相关性,否则会得到一块雪花噪点一样的电视雪花图。opensimplex 这个库体积小、接口简单,seed 可控,非常适合这种脚本型项目。渲染层我一般建议用 Pillow 而不是 matplotlib,前者能精确控制每个像素和多边形填充,输出尺寸不受窗口限制,后者更适合做数据分析可视化,画地图容易出现边框、坐标轴残留,还得额外关闭。
从架构角度看,hexgen 刻意把“生成”和“渲染”分离:噪声生成只输出数值矩阵,不关心地图长什么样;渲染模块只负责把数值变成颜色和多边形,不关心数值怎么来的。这样替换任意一个环节都不会影响其他模块。
2.2 目录结构与模块职责
hexgen 的项目结构很直白,解开压缩包之后你会看到一个典型的 Python 包布局。每个模块职责单一,方便二次开发时替换逻辑。
hexgen/ ├── main.py # 命令行入口,解析参数并串联全流程 ├── generator/ │ ├── __init__.py │ ├── hexgrid.py # 六边形网格轴向坐标定义、邻接与距离计算 │ ├── noise.py # Simplex 噪声高度图生成与归一化 │ ├── climate.py # 温度、湿度模型与生物群落分配 │ └── render.py # 坐标转换、六边形绘制、颜色映射 ├── requirements.txt └── output/ # 生成结果默认输出目录main.py 是整个流程的编排者,它读取用户传入的地图宽度、高度、随机种子、噪声缩放比例等参数,依次调用 generator 包里的函数,最后把渲染结果写入 output 目录。generator 包内部不感知命令行参数,只接收明确的数据对象,这样如果你想把它嵌进 Flask 接口或者游戏服务里,只要调用对应的函数就行。
这种模块划分还有一个好处:调试时可以单独跑某个模块。比如怀疑噪声生成有问题,就直接调用 noise.generate_heightmap 打印矩阵内容,不用从头跑一遍完整流程。
2.3 数据流:从四个参数到一张 PNG 的完整链路
整个生成过程是一条单向数据流,没有回环依赖。当你执行 python main.py --width 80 --height 50 --seed 2024,发生的事情可以拆成五步。
第一步,main.py 使用 argparse 解析命令行参数,得到地图列数 width、行数 height、随机种子 seed、噪声缩放 scale。第二步,noise.py 根据 seed 和 scale 生成一张 width x height 的高度浮点矩阵,值域在 -1.0 到 1.0 之间。第三步,climate.py 结合纬度信息与湿度噪声,生成温度矩阵和湿度矩阵,再按高度、温度、湿度三个维度给每个格子分配生物群落类型,比如海洋、沙漠、草原、森林、雪原。
第四步,hexgrid.py 基于 width 和 height 构建所有六边形格子的轴向坐标(q, r),并计算相邻关系。之所以把坐标构建放在高度图之后,是因为网格的尺寸直接由 width 和 height 决定,但计算本身并不依赖高度值,这一步其实可以提前,放到后面是为了让 main.py 的调用顺序更接近“先算内容再建格子”的直觉。第五步,render.py 把每个格子的轴向坐标转换为屏幕像素坐标,确定中心点,再按六边形半径计算六个顶点,用对应生物群落的颜色填充,最终保存为 PNG。
整个链路里最容易出问题的,是坐标转换时把 q 和 r 弄反,或者忘记处理六边形网格特有的“偏移”对齐方式。这个问题后面避坑章节会专门讲。
3. 核心模块拆解:从坐标计算到植被覆盖的五个关键实现
3.1 坐标系统选型:为什么用轴向坐标而不是 Offset 坐标
六边形网格有三种常见坐标表示:Offset 坐标(offset coordinates)、轴向坐标(axial coordinates)、立方体坐标(cube coordinates)。Offset 坐标最接近人眼观察表格的行列,但邻接计算需要区分奇偶行,容易写错分支;立方体坐标的数学公式最漂亮,却多用一个变量,浪费内存。
hexgen 选用轴向坐标,用 (q, r) 两个整数表示一个六边形。q 表示列方向,r 表示斜向行。轴向坐标本质上是立方体坐标去掉第三个轴 s,因为它满足 q + r + s = 0,所以 s 可以由 q 和 r 推导出来。这样既保留了立方体坐标的简洁公式,又只需要存两个数。
# generator/hexgrid.py AXIAL_DIRECTIONS = [ (1, 0), (1, -1), (0, -1), (-1, 0), (-1, 1), (0, 1) ] def axial_neighbor(coord, direction_index): """返回当前格子在指定方向上的邻居坐标""" q, r = coord dq, dr = AXIAL_DIRECTIONS[direction_index] return (q + dq, r + dr) def hex_distance(a, b): """计算两个轴向坐标之间的距离""" dq = a[0] - b[0] dr = a[1] - b[1] ds = -dq - dr return max(abs(dq), abs(dr), abs(ds))这里的关键是 AXIAL_DIRECTIONS 数组的顺序,我按逆时针方向排列六个方向,顺序固定后就不会出现方向混淆。hex_distance 函数用立方体坐标的切比雪夫距离公式,对轴向坐标同样适用,因为 s 轴已经隐含在 q + r 的关系里。
参数说明里有一个容易忽略的点:邻居坐标的 direction_index 如果越界,函数不会报错,会导致静默错误。我实际开发时会加一个 assert 校验,这里为了代码精简没写,二次开发时建议补上。
对比 Offset 坐标,轴向坐标的最大优势是算法对称性高。距离计算、寻路、视野计算都能直接用简洁公式,而 Offset 坐标需要做奇偶判断。缺点是渲染时不能直接用行列号定位,必须经过一次数学换算,这个换算在渲染模块处理。
3.2 网格构建:生成全部六边形格子并建立邻接表
有了坐标系统,就需要根据地图宽高生成所有有效的六边形坐标。这里用轴向坐标时,宽高定义的是菱形的边界,而不是矩形边界。
# generator/hexgrid.py def build_grid(width, height): """生成 width x height 大小的六边形网格坐标集合""" cells = [] for q in range(width): q_offset = q // 2 for r in range(-q_offset, height - q_offset): cells.append((q, r)) return cells def build_adjacency(cells): """为每个格子建立邻居映射,返回 dict""" cell_set = set(cells) adjacency = {} for cell in cells: neighbors = [] for direction in AXIAL_DIRECTIONS: neighbor = (cell[0] + direction[0], cell[1] + direction[1]) if neighbor in cell_set: neighbors.append(neighbor) adjacency[cell] = neighbors return adjacencybuild_grid 的思路是让每一列 q 在 r 方向产生错位,q_offset 等于 q 整除 2,这样形成的菱形地图在视觉上左右两侧是对称的倒三角边界,边缘格子缺失的形状刚好被地图边框裁掉。这种菱形布局在地图生成中最常见,因为所有格子都在同一个矩形参数空间里,数据矩阵和网格坐标一一对应,没有空洞。
build_adjacency 返回的是一个 dict,键是格子坐标,值是邻居坐标列表。用 set 判断邻居是否存在于地图中,时间复杂度 O(1)。这里有个性能细节:如果地图尺寸很大,比如 200x200,格子数四万,每个格子查六次集合,总共也就二十四万次查询,耗时非常低,不需要额外优化。
开发时要注意:build_grid 生成的是“坐标集合”而不是“有序数组”。如果后面需要按某种顺序遍历格子,比如从地图左上角开始渲染,必须额外排序,因为循环中的生成顺序并不等于视觉上的行优先顺序。我的做法是在渲染模块里用像素坐标做排序。
3.3 高度图生成:用 Simplex 噪声模拟连续地形
地形生成的核心是让相邻格子的高度相关联。直接给每个格子赋随机数,会造成马赛克状的地形,完全没有山脉和盆地的感觉。Simplex 噪声是一种连续噪声函数,输入二维坐标,输出平滑变化的数值。
# generator/noise.py import opensimplex import numpy as np def generate_heightmap(width, height, scale=10.0, seed=42): """生成归一化到[-1, 1]区间的高度图矩阵""" opensimplex.seed(seed) heightmap = np.zeros((height, width)) for y in range(height): for x in range(width): # 将像素坐标除以 scale,让采样点在噪声场中平滑过渡 nx = x / scale ny = y / scale heightmap[y][x] = opensimplex.noise2(nx, ny) return heightmap这段代码有三个关键参数。scale 是噪声缩放比例,值越大,采样点之间的间隔越小,地形起伏越平缓;值越小,地形越破碎。我一般用 8 到 12 之间的值,这样能保证地图上既有大块大陆轮廓,又有局部山地细节。seed 是随机种子,opensimplex.seed 后全局噪声场固定,同一个 seed 必然生成同一个高度图。
这里有一个新手容易踩的坑:opensimplex.seed 是全局的,如果你在循环里多次调用 generate_heightmap 生成不同区域,每次都要重置 seed,否则第二次生成的噪声会接着上一次的流,导致两张图看似不同实则相关。
默认返回的矩阵值域是 -1 到 1 之间,后续生物群落判断需要高度阈值。比如海平面以下设为 -0.05,那低于这个值的格子就标记为海洋。实际生成时你会发现地图边缘和中心的分布并不均匀,因为这本质上是随机噪声,不会天然形成“四周海洋中间大陆”的格局。一个巧妙做法是叠加径向渐变:让中心区域高度略增,边缘略降,这样就能生成类似真实星球的地貌。这个进阶技巧在二次开发里会说。
3.4 生物群落分配:温度、湿度与高度的三重判断
有了高度图,还不能直接决定格子颜色,因为同样是陆地,赤道附近可能是雨林,极地附近可能是雪原。hexgen 简化了真实气候模型,用纬度估算温度,用第二张噪声图模拟湿度,再结合高度查表分配群落。
# generator/climate.py import opensimplex import numpy as np def generate_climate(heightmap, width, height, scale=10.0, seed=42): """生成温度矩阵和湿度矩阵""" opensimplex.seed(seed + 100) temp = np.zeros((height, width)) hum = np.zeros((height, width)) for y in range(height): # 纬度系数:中间行温度高,上下两端温度低,模拟赤道到极地 lat_factor = 1.0 - abs(y - height / 2) / (height / 2) for x in range(width): temp[y][x] = lat_factor + 0.3 * opensimplex.noise2(x / scale, y / scale) hum[y][x] = opensimplex.noise2(x / scale + 5, y / scale + 5) + 0.5 return temp, hum def assign_biome(height, temperature, humidity, sea_level=-0.05): """根据高度、温度、湿度返回生物群落名称""" if height < sea_level: return "ocean" if height > 0.55: return "mountain" if humidity < 0.3: return "desert" if temperature < 0.2: return "tundra" if humidity > 0.7: return "forest" return "grassland"temperature 矩阵由纬度和噪声叠加而成,lat_factor 让地图上下两端冷、中间热,0.3 乘噪声是让温度有一些不规则波动。humidity 矩阵则是把噪声值加 0.5,把值域从 -1 到 1 平移成 0 到 1.5,方便用阈值切分。
assign_biome 的阈值顺序很有讲究,先判断海洋和山脉,再判断其它陆地类型。因为山脉高度值很高,即使温度湿度条件可能像沙漠,也应该优先显示为山脉。如果你把沙漠判断放在山脉前面,海拔极高的地区会被画成沙漠,视觉上就会出现“空中沙漠”的反直觉效果。这个优先级问题是我第一次跑这个项目时翻车最严重的地方。
3.5 渲染输出:把轴向坐标转换成屏幕像素并绘制多边形
渲染模块是整个项目里最容易出现视觉 bug 的地方。必须把轴向坐标 (q, r) 转换成屏幕上的 x, y 像素坐标,然后以中心点绘制出正六边形。
# generator/render.py import math from PIL import Image, ImageDraw def axial_to_pixel(coord, size, origin): """轴向坐标转像素坐标,size 是六边形半径""" q, r = coord x = size * math.sqrt(3) * (q + r / 2) + origin[0] y = size * 1.5 * r + origin[1] return (x, y) def draw_hex_grid(cells, biome_map, size, image_size, color_map): """为每个格子绘制填充多边形""" img = Image.new("RGB", image_size, (20, 30, 40)) draw = ImageDraw.Draw(img) for cell in cells: center = axial_to_pixel(cell, size, (size, size)) points = [] for i in range(6): angle = math.pi / 6 + i * math.pi / 3 px = center[0] + size * math.cos(angle) py = center[1] + size * math.sin(angle) points.append((px, py)) draw.polygon(points, fill=color_map[biome_map[cell]]) return imgaxial_to_pixel 的公式里,x 方向要乘以 sqrt(3),因为六边形水平方向的实际宽度是半径的两倍乘以根号三;y 方向每行递增 1.5 倍半径,这对应六边形纵向的“行高”。origin 参数是左上角偏移量,避免边缘格子绘制到画布外。
draw_hex_grid 中,angle 从 math.pi / 6 开始是为了让六边形有一个“平顶”的边正对着上方,而不是尖顶。这不是必须的,但平顶六边形在视觉上更适合表现地形,因为相邻格子垂直方向对齐更自然。如果你想要尖顶六边形,把起始角度改成 0 即可,同时邻接方向表也要相应旋转。
注意这段代码是逐格子创建多边形再填充,格子里没有缝隙是因为相邻六边形的顶点共用同一套公式计算,不会出现浮点误差累积。但如果你把 size 设置得过大,格子会超出画布边缘,最好预先根据地图宽高计算出 image_size 和合适的 size。常见做法是让 size 等于 ceil(image_width / (width * sqrt(3))),确保整张地图恰好铺满画布。
4. 运行与调参:从环境准备到性能优化的完整流程
4.1 环境准备与依赖安装
在跑 hexgen 之前,先准备好 Python 3.8 以上的环境。项目依赖很简单,用 pip 就能装齐。
pip install numpy pillow opensimplex如果你使用的是虚拟环境,建议先创建并激活环境再安装,避免把依赖装到系统全局 Python 里。我一般会顺手创建一个 requirements.txt 以上的内容已经在压缩包里带了,实际执行时直接用 pip install -r requirements.txt 更省事。
装好依赖后,在项目根目录执行一次简单生成验证环境没问题。
python main.py --width 40 --height 30 --seed 128 --output demo.png这行命令会生成一张 40 列 30 行的六边形地图,如果一切正常,output 目录下会出现 demo.png 文件。打开图片能看到蓝色海洋、绿色草原、灰白色山脉的色块。如果这一步就出问题,优先检查依赖是否安装完整,尤其是 opensimplex 这个库在部分 Python 版本上可能没被正确编译,卸载重装基本能解决。
4.2 命令行参数详解
hexgen 的命令行参数不多,但每个参数对生成效果的影响都是决定性的。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| --width | int | 80 | 地图列数(格子数) |
| --height | int | 50 | 地图行数(格子数) |
| --seed | int | 42 | 随机种子,决定噪声场形状 |
| --scale | float | 10.0 | 噪声缩放比例,越大地形越平缓 |
| --sea-level | float | -0.05 | 海平面高度阈值,调大变海洋变少 |
| --output | str | output/map.png | 输出 PNG 文件路径 |
width 和 height 控制的是“格子数量”而不是“像素尺寸”,输出图片的像素尺寸会自动根据格子数量计算。seed 参数非常关键,同一个 seed 必然生成完全一样的地图,这是实现地图场景复现的基础。scale 参数直接影响地形特征,调小比如到 5,地图会出现密集的岛屿和破碎陆地;调大到 20,则会生成大面积连成一片的大陆。
sea-level 是我后来加的参数,默认 -0.05 是因为 Simplex 噪声的均值在 0 附近,稍微降低一点阈值可以让海洋面积占比略高于陆地,看起来更接近真实星球。如果你想把地图变成全陆地,直接设成 -0.9 即可。
4.3 常见运行报错排查
我实际跑这个项目的过程中,遇到过几个非常典型的报错,这里单独说一下排查思路,因为这些问题在二次开发时几乎必现。
第一种是 ModuleNotFoundError: No module named 'opensimplex'。这通常是因为当前 Python 环境和安装依赖的环境不是同一个。解决方案是检查 pip list 里有没有 opensimplex,没有就重装,装了还找不到就检查虚拟环境是否激活。
第二种是运行正常但输出的 PNG 是一片纯色。这种情况九成是 sea-level 设置太高,导致所有格子全部被判定为海洋。可以用 --sea-level -1.0 跑一次,如果出现陆地,就能确认是阈值问题。
第三种是报错 TypeError: 'NoneType' object is not iterable,这通常发生在 render.py 里遍历 biome_map 时。biome_map 是一个 dict,如果代码在循环里访问了不存在的键,比如某个格子没有通过 assign_biome 赋值,就会取到 None。排查方式是打印 heightmap 里是否有 NaN 值,opensimplex 在某些极端参数下可能返回 NaN,污染了后续判断。
4.4 性能优化:大尺寸地图的生成策略
当 width 和 height 超过 150 时,纯 Python 的双循环开始变得吃力。generate_heightmap 里两层 for 循环逐个采样噪声是最大的瓶颈,因为 opensimplex.noise2 是单点调用,无法直接向量化。
一个常见优化是使用 numpy 的 meshgrid 预生成坐标矩阵,然后用向量化函数去填充循环。opensimplex 官方提供了 OpenSimplex 类的 vectorized 方法,可以接收 numpy 数组批量生成噪声值。改造后的核心逻辑如下。
# generator/noise.py 向量化版本 def generate_heightmap_fast(width, height, scale=10.0, seed=42): opensimplex.seed(seed) x = np.arange(width) / scale y = np.arange(height) / scale X, Y = np.meshgrid(x, y) heightmap = opensimplex.noise2array(X, Y) return heightmapnoise2array 是 opensimplex 提供的批量接口,直接输入二维坐标数组,输出同形状的噪声矩阵。测试下来在 200x200 的地图上,这个版本比双循环快接近一个数量级,渲染反而成了新的瓶颈。
渲染优化的思路是减少 Pillow 绘制调用的数量。ImageDraw.Draw.polygon 对每个格子调用一次,在画面格子数超过两万时会有明显卡顿。常见做法是把相同生物群落的格子合并成一个大区域再绘制,这需要做连通区域合并,实现复杂度会增加不少。如果只是追求快速预览,可以降低图片尺寸,让每个格子半径变小,渲染时间会线性下降。我认为这个项目在 100x100 以下的地图不需要过度优化,超过这个规模再考虑向量化和区域合并方案。
5. 避坑指南:六角格地图生成器开发中的五个真实翻车现场
5.1 坐标轴颠倒导致整张地图水平镜像
现象:生成的地图看起来左右对称,但仔细对比会发现自己期望的“西海岸”出现在东边,整个大陆布局被镜像。
原因:在 axial_to_pixel 函数里,x 坐标计算用了 (r + q / 2) 而不是 (q + r / 2)。轴向坐标的齐次偏移方向被我搞反,导致所有格子沿垂直轴对称翻转。
解决:严格按照公式 x = size * sqrt(3) * (q + r / 2) 书写,然后拿一个已知格子比如 (0, 0) 和 (1, 0) 的手算结果对比,确保 (1, 0) 在 (0, 0) 的右上方而不是右下方。从那以后我每次改完坐标转换都先跑一个 3x3 小地图,用不同颜色标记角落格子,肉眼确认几何关系正确再继续。
5.2 opensimplex.seed 被二次调用导致两张图高度相关
现象:在同一个进程里连续生成两张地图,第一张是大陆,第二张也几乎一模一样,只是细节略有抖动。
原因:opensimplex 的 seed 设置是全局单例状态。第二次调用 generate_heightmap 时没有重置 seed,而噪声生成是从上次的状态继续推进的,导致第二次的高度场只是第一次的高度场平移了一块区域。
解决:每次生成高度图或湿度图之前,都显式调用 opensimplex.seed 并且使用不同的种子偏移值,比如主 seed 是 42,温度图用 142,湿度图用 242。这个方法也被我用在循环生成多张预览图时,确保每张图互不相同。
5.3 生物群落出现“空中沙漠”和“冰封雨林”
现象:高海拔地区画成黄色沙漠,赤道附近的高湿度区域画成白色雪原,乍一看非常违和。
原因:assign_biome 的阈值判断顺序不合理。当时我把 desert 判断放在 mountain 之前,高海拔低温但湿度正常的地方先命中 desert;又把 tundra 判断放在 forest 之前,导致高湿度低温的赤道高山被判定为 tundra。
解决:把 assign_biome 改成先判断高度和海平面,再判断山脉,然后按温度湿度组合判断平原类型。调整顺序后效果符合直觉:山顶是灰色,靠近两极是雪原,赤道低海拔高湿度是森林。这之后我养成了习惯,凡是多重条件的分支判断,先把互斥的强约束条件放在前面。
5.4 六边形之间出现透明缝隙
现象:输出 PNG 里,每个六边形周围有一圈很细的背景色线条,看起来像是拼贴画没对齐。
原因:Pillow 的 polygon 绘制使用浮点坐标会做抗锯齿处理,相邻多边形的共享边如果两边独立计算顶点,浮点舍入误差可能导致 0.5 像素的缝隙。另外 image_size 计算偏小,边缘格子被裁切出边界。
解决:把共享边的顶点计算统一到同一套 axial_to_pixel 和顶点生成函数里,确保相邻格子交点坐标完全一致;同时 image_size 在计算时额外加两个 size 的边距,避免边缘裁切。对于缝隙,最简单的视觉修正是在画布背景色上先铺一层和海洋相同的颜色,这样即使有一点缝隙也不显眼。
5.5 固定 seed 却无法复现同一张地图
现象:用同一个 seed 在同事的电脑上跑,生成的地图和我这边不一样,生物群落分布有明显差异。
原因:numpy 版本不同会影响某些浮点运算的微小结果,但更主要的是不同平台下 opensimplex 的浮点实现可能有差异。另一个隐蔽原因是字典遍历顺序,Python 3.7 之后 dict 保序这不是问题,但在渲染时如果 biome_map 是普通 dict 而 cells 是 set,遍历顺序不稳定,导致绘制顺序不一致。
解决:将 cells 从 set 改为排序列表,按 (q, r) 先排序再传给渲染函数。生成的数值矩阵用 np.save 保存一份作为复现基准,后续迭代时对比偏差。这个坑让我意识到,所谓“可复现”不仅取决于 seed,还取决于代码的确定性,遍历顺序、浮点库版本都要锁定。
6. 二次开发:把 hexgen 改造成更懂你的地图工具
6.1 增加大陆与湖泊的随机扰动
现版本的生物群落完全依赖噪声和阈值,自然生成的大陆往往形状不规则。但如果你想要一个“中心大陆加环形海洋”的世界,可以在高度图上叠加一个径向渐变。
# 在 generate_heightmap 返回前叠加岛屿形状 dx = x - width / 2 dy = y - height / 2 dist = math.sqrt(dx * dx + dy * dy) / (width / 2) heightmap[y][x] = heightmap[y][x] * (1 - dist) + 0.2 * (1 - dist)这个扰动让地图边缘高度下降,中心高度抬升,噪声加入的细节仍然保留,但整体轮廓变成类似大陆漂移后的形状。湖泊生成同理,可以在 assign_biome 前把局部高度调整为低于海平面的值。
6.2 输出 SVG 适配矢量编辑
如果你的目标是让美术同学在矢量软件里二次编辑地图,可以在 render.py 里追加一个 svg 导出函数。把生成多边形顶点坐标的循环搬进文本拼接逻辑,每个格子输出一个 polygon 标签,颜色值用生物群落对应的十六进制码。SVG 文件不需要像素渲染,也不会有圆角缝隙问题,而且地图每个格子都是独立对象,方便后期改色和删除。
我实际做的时候会同时输出 PNG 和 SVG 两份结果,PNG 用于快速预览,SVG 用于交付编辑。需要注意 SVG 文件体积会比 PNG 大不少,尤其是格子数量超过五千时,启动编辑器会有些卡顿。
6.3 参数化批量生成:多地图方案对比
做玩法测试时往往需要一次生成几十张候选地图,然后人工挑选。我在 main.py 外面包了一层批量脚本,传入一个种子列表,循环调用 hexgen 的生成函数,把每张地图存成以 seed 命名的文件。同时生成一个 index.html 把所有图片平铺展示,方便对比不同种子下的地形质量。这个小技巧让我从手动跑几十条命令中解脱出来,也统一了输出命名规范。
过程中最深的体会是,地图生成项目的价值不在于单张图有多好看,而在于能不能稳定复现、快速迭代。从那以后我每次改动核心算法,都强制跑一遍固定 seed 的地图对比新旧版本差异,确认没有破坏既有结果再做下一步。希望帮到你,如果按这篇笔记跑通了,试着把 sea-level 参数降到 -0.8,看看你生成的第一片大陆长什么样。
本文还有配套的精品资源,点击获取