news 2026/9/9 20:31:23

用Python构建六角形世界地图生成器:从坐标到地形

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python构建六角形世界地图生成器:从坐标到地形

简介:这是一款基于 Python 的六角形世界地图生成器,主要面向游戏开发者、桌游爱好者和程序化生成研究者。它能在六边形网格上生成随机世界地图,并支持调节参数模拟各类行星表面,还可将地图分割为任意大小的“领土”单元,适合作为浏览器游戏开发板、DnD 战役或开源游戏的地图底座。压缩包共 34 个文件,以 Python 源代码为核心,涵盖地图生成、河流、地形、边界、常量与工具等模块,同时配有多份 Markdown 说明文档、JavaScript 示例、测试脚本和安装配置文件,整体仅 548KB,轻量易用。该资源已有 1539 人学习,代码注释与目录结构清晰,可通过 requirements 快速安装运行。读者拿到的不只是一套可立即运行的生成器,还能借此理解六边形网格索引、噪声地形与河流模拟、领土划分等关键技术,方便在此基础上扩展自己的奇幻世界设定或游戏原型。 我这两年做独立游戏原型,最头疼的不是玩法,反而是地图生成。方格地图用久了总觉得少了点味道,想试试战棋游戏里那种六角格,但网上的生成器要么绑定桌游平台,要么闭源,想自己控制地形和风格基本没门。后来我索性用Python撸了一个叫hexgen的六角形世界地图生成器,从坐标计算、噪声地形到matplotlib出图,整个流程纯Python实现,跑通之后顺手解决了地形分类和地图导出。这篇文章把设计思路、关键实现和踩过的坑都摊开讲,想折腾六边形地图的朋友可以直接拿去做参考。

1. 为什么是六角形而不是方格

1.1 六边形网格的真正优势

玩过《文明》或者《英雄无敌》的朋友应该能理解,六边形让每个格子到相邻格子的距离更均匀,移动起来没有方格那种“斜向穿近路”的别扭感。对地图生成来说,六边形网格还有一个隐藏优势:地块边界更平滑,生物群系过渡看起来更自然。hexgen这个名字其实就是hex + generator,我一开始就没打算做成重型框架,而是专注于两件事:快速生成一个有地理逻辑的世界地图,以及方便导出成自己能用的数据格式。

方格地图的邻居判断看似简单,实际上存在对角线距离和二倍距离不一致的问题。比如从(0,0)移动到(1,0)需要1点移动力,但沿对角线移动到(1,1)如果也按1点算,很多战术玩法就崩了。六边形没有这种困扰,六个邻居距离完全一致,这也是为什么战棋和策略游戏更偏爱它。做程序化生成时,这种均匀性也简化了地形扩散、寻路和势力划分的逻辑。

1.2 hexgen的核心需求拆解

我把需求拆成四点:第一,支持以六边形为基本单元的网格;第二,能模拟海陆分布和基础地形;第三,可视化结果要直观;第四,代码要短小精悍,方便自己改。基于这四点,我选了Python生态里最顺手的组合:numpy做批量计算,noise库生成柏林噪声,matplotlib负责绘制。如果你只是想快速看效果,这三个依赖已经够了。

这个项目不打算和商业地图编辑器比功能,它解决的是“从零到有”的问题。你拿到一堆六边形格子,怎么把它们变成大陆、海洋、沙漠和森林?hexgen提供了一条最小可行的路径,后续无论是接到游戏引擎还是做可视化分析,都有数据接口可以继续扩展。

2. 地图生成的底层逻辑:从坐标到地形

2.1 六边形坐标系统的三种姿势

实现六边形网格的第一步是选坐标系。最常用的是轴向坐标,用(q, r)两个数代表格子位置。也有很多人用立方体坐标,因为三个坐标相加恒等于0,很多方向计算变得更对称。hexgen内部我用的是轴向坐标,因为存储少一个数,但涉及方向查找时我会临时转成立方体坐标来推邻居。两个坐标系转换很简单:立方体坐标(x, y, z)转轴向(q, r)时,q=x, r=z;反向转换时x=q, z=r, y=-x-z。不要被公式吓到,本质上只是去掉一个冗余维度。

网格半径R决定了生成地图的大小。以(0,0)为中心,所有满足max(abs(q), abs(r), abs(q+r)) <= R的格子共同组成一个大的六边形地图。这一步确定的是可玩范围,半径越大,格子数量越多,后续算法耗时也越高。半径R和格子数不是线性关系,而是接近3R^2+3R+1,所以半径从30涨到60,工作量会变成原来的4倍左右,这点在做大图前要有心理准备。

2.2 地形生成:噪声是骨架,阈值是血肉

世界地图不能是纯随机色块,所以需要噪声。柏林噪声(Perlin Noise)或者Simplex噪声是程序化生成地形最常用的工具。它的特点是连续且各向同性,简单说就是相近坐标得到的值相近,这样生成的高度图看起来才有连续的大陆和海洋。你可以用noise库的pnoise2函数,传入x和y的缩放系数,得到范围在[-1,1]之间的值。缩放系数很关键,数值小,地形起伏变化慢,会出现大洲;数值大,地形破碎,像群岛。用两个不同频率的噪声叠加,能得到更自然的海岸线。

有了高度场之后,地形类型分配本质上是一个查表过程。我习惯把高度分成五档:深海、浅海、沙滩、平原、山地,再额外用一个湿度噪声决定森林、草原和沙漠。这些值不需要太复杂,关键是海平面阈值。海平面抬升一点,大陆面积迅速缩小;降低一点,又可能出现超级大陆。所以我通常把海平面阈值做成参数,方便反复调整。湿度噪声则用来打破单调,否则一片大陆全是绿色,看起来没有层次。

3. 动手实现:写一个可运行的hexgen

3.1 环境准备:先解决Python和依赖

如果你还没装Python,先去官网下载3.9以上的版本。安装时记得勾选“Add Python to PATH”,省得后面在命令行里找不到python。装完之后用pip安装三个包:numpy、matplotlib、noise。如果你在国内,可以用镜像源加速,命令加-i参数,速度会快很多。这个步骤没什么技术含量,但很多新手都在Python安装环节卡住,特别提一句。

pip install numpy matplotlib noise

因为hexgen是纯Python项目,不涉及编译,所以依赖很轻。如果你后续想处理更大规模的数据,可以考虑把numpy换成pandas,但就生成地图这个场景来说,完全没有必要。

3.2 网格生成与坐标转换

接下来写核心类。定义HexGrid,传入半径radius。生成所有满足立方体坐标约束的格子,保存为轴向坐标。这里给出一个最小实现:

class HexGrid: def __init__(self, radius): self.radius = radius self.cells = [] for q in range(-radius, radius + 1): r_min = max(-radius, -q - radius) r_max = min(radius, -q + radius) for r in range(r_min, r_max + 1): self.cells.append((q, r))

注意r_min/r_max这个边界是我实际写错好几次才记住的。如果你用立方体坐标约束max(abs(q), abs(r), abs(q+r)) <= R,也等价,但用双层循环时要小心别把边界算漏。很多六边形实现Bug都出在这个地方,不是缺了边缘的一圈格子,就是多生成了一些超出范围的坐标。

为了把格子画到屏幕上,需要把轴向坐标转成像素坐标。这里我采用尖顶六边形的布局,转换公式如下:

import math def axial_to_pixel(q, r, size): x = size * math.sqrt(3) * (q + r / 2) y = size * 1.5 * r return x, y

如果你希望六边形是平顶的,把x和y的公式换成另一套即可,网上有现成对照表。对世界地图来说,尖顶六边形看起来更像传统的地图块,所以我默认用尖顶。size参数是六边形的外接圆半径,控制每个格子的大小。

3.3 地形生成:从高度图到地块类型

然后生成地图。这里用噪声生成高度场和一个湿度场。为了方便调试,我把随机种子设为固定值,保证每次运行结果一致。这种可复现性在调参时太重要了,不然每次跑出来的图都不一样,你根本不知道是哪个参数影响了大洲轮廓。

import noise def generate_terrain(grid, seed=42): terrain = {} for q, r in grid.cells: x = q * 0.08 y = r * 0.08 height = noise.pnoise2(x, y, octaves=5, persistence=0.6, repeatx=1000, repeaty=1000, base=seed) moisture = noise.pnoise2(q * 0.1 + 100, r * 0.1 + 100, octaves=3, base=seed + 1) height = (height + 1) / 2 moisture = (moisture + 1) / 2 terrain[(q, r)] = assign_biome(height, moisture) return terrain

注意pnoise2默认生成的坐标如果不做缩放,频率会高到完全随机,所以我把q和r乘以0.08。这个系数直接控制大陆尺度,0.08适合半径30左右的图,如果你生成更大地图,可以适当调小。我把moisture的坐标偏移了100,是为了避免和高度的噪声pattern重叠,否则某些区域总是固定地又高又湿,看起来不自然。

地形的分类函数assign_biome,我计划用几个阈值完成:

def assign_biome(height, moisture): if height < 0.35: return 'deep_ocean' elif height < 0.42: return 'shallow_water' elif height < 0.47: if moisture > 0.6: return 'swamp' return 'beach' elif height < 0.7: if moisture < 0.25: return 'desert' elif moisture > 0.65: return 'forest' else: return 'grassland' else: if moisture > 0.6: return 'conifer_forest' return 'mountain'

这些阈值是我多次试验后的默认值,但不是唯一答案。你可以把海平面阈值从0.42往上调到0.55,地图会从少量大陆变成一堆岛链。实际调参时,我建议把高度分布打印出来,先看自己的地图高度集中在哪个区间,再去定阈值,比凭空猜要快得多。

3.4 可视化:让地图真正“长”出来

绘制六边形地图,最简单的方式是用matplotlib的Polygon。每个六边形是一个多边形,填充对应的颜色,再用PatchCollection统一绘制。为了性能,不要一个patch一个patch地add,应该把所有多边形合并到一个集合里,matplotlib的渲染效率会高不少。

import matplotlib.pyplot as plt from matplotlib.patches import Polygon from matplotlib.collections import PatchCollection COLORS = { 'deep_ocean': '#1a5276', 'shallow_water': '#2980b9', 'beach': '#f5cba7', 'grassland': '#82e0aa', 'forest': '#27ae60', 'conifer_forest': '#1e8449', 'desert': '#f9e79f', 'mountain': '#7f8c8d', 'swamp': '#4a7c59', } def draw_map(grid, terrain, size=10, filename='hexmap.png'): fig, ax = plt.subplots(figsize=(12, 10)) patches = [] colors = [] for q, r in grid.cells: x, y = axial_to_pixel(q, r, size) angles = [30 + 60 * i for i in range(6)] points = [(x + size * math.cos(math.radians(a)), y + size * math.sin(math.radians(a))) for a in angles] poly = Polygon(points, closed=True) patches.append(poly) colors.append(COLORS[terrain[(q, r)]]) p = PatchCollection(patches, match_original=False) p.set_facecolor(colors) p.set_edgecolor('black') p.set_linewidth(0.5) ax.add_collection(p) ax.autoscale_view() ax.set_aspect('equal') ax.axis('off') plt.savefig(filename, dpi=150, bbox_inches='tight')

运行之后,你会得到一张可以直接放进地图编辑器里的PNG。控制台打印每个地形类型的数量,可以快速判断分布是否合理。如果你用的是radius=30,格子数大约2700个,跑一次不到一秒钟,体验很顺滑。

4. 参数调优与效果控制

4.1 噪声缩放:决定大陆尺度

很多人第一次跑出来觉得地图太碎片化,其实就是缩放系数太大。你可以把generate_terrain里的x=q0.08改成q0.03,再看效果,大陆会明显聚拢。原理是柏林噪声的采样点间距越小,相邻格子的值越接近,高度变化越平缓。这个参数没有标准答案,跟你的地图半径强相关。我一般先让半径R乘以0.002到0.005,得到一个初始值,再根据输出效果微调。半径30和半径60需要用不同的缩放系数,否则大图看起来要么太平,要么太碎。

4.2 海平面与生物群系阈值

海平面是最能改变地图观感的旋钮。用height<0.35作为深海,0.35到0.42作为浅海,0.42到0.47作为沙滩,这个分布能保证大部分地图有一两块主要大陆。如果想把地图变成荒漠星球,就把湿度阈值整体压低,让更多格子走desert分支。调阈值的时候我强烈建议写一个快速小函数,输入参数后直接打印各地形占比,几秒钟就能判断要不要继续调,比一张张看图效率高很多。

你还可以把海洋和陆地的分界线单独提出来作为变量,比如sea_level=0.42。这样后续做海平面上升效果时,只需要改一个变量。地形分类函数里所有的高度阈值,最好也从硬编码改成参数传递,项目变大之后会感谢自己当初留了这个口子。

4.3 用随机种子控制版本

我保留seed参数,这样每次生成的地图都可以复现。做游戏关卡时,玩家看到的是随机地图,但出了bug需要回滚到特定地图,只要记下seed就能精确定位。这个习惯一定要养成,后期省很多事。举个例子,你调了一晚上参数,终于生成了一个特别漂亮的地图,结果第二天打开程序发现地图变了,如果当时没有记录seed,就只能凭感觉重新调了。所以hexgen的generate_terrain里,base参数直接由seed控制,每次运行相同seed,出来的地形就完全一样。

5. 踩坑记录与排查技巧

5.1 六边形之间出现锯齿缝隙

第一次跑出图时,六边形之间的白边特别明显。原因有两个:一是matplotlib多边形默认有边缘色,二是坐标精度导致相邻多边形没有完全贴合。解决办法是把edgecolor设成很浅的灰色或者黑色,linewidth调成0.3;如果还漏白,可以给每个多边形设置相同edgecolor并适当扩大0.5个单位的包围盒。注意不要用白色边,否则在浅色地块上超级难看。实测linewidth=0.5,edgecolor='black',在dpi=150的PNG里表现最稳。

5.2 noise库的坐标越界警告

noise.pnoise2如果采样点坐标接近repeatx/repeaty设置的值,容易出现越界或者重复pattern。我建议在生成每个格子时,坐标先取模,或者把repeatx和repeaty设成远大于实际坐标范围。我写的例子用的是repeatx=1000, repeaty=1000,对半径100以内的地图足够。如果你生成更大的地图,直接把这两个值再放大。否则某些地图的边角会出现明显的纹理重复,一眼就能看出来是程序生成的。

5.3 大尺寸地图性能问题

半径从30增加到60,格子数从2700涨到大约10800,绘制时间会翻好几倍。瓶颈主要在绘图,不在网格计算。如果你需要快速预览,可以先用半径20的小图调参,最终出图再用大半径。如果真要生成几百半径的超大图,建议把地形数据保存成JSON或CSV,渲染交给游戏引擎,别用matplotlib硬扛。我把生成的地形数据序列化成JSON,每个格子存(q, r, terrain_type),实测半径80的地图序列化后只有几百KB,加载非常快。hexgen的核心价值也在这儿:它先是一个数据生成器,然后才是图片生成器。图片只是用来验证数据长什么样。

现象可能原因解决办法
地图全是碎片噪声缩放太大降低q*0.08的系数
陆地过多或过少海平面阈值偏高或低调整assign_biome里的0.42
颜色区分度不高默认颜色太接近自定义COLORS字典
运行慢半径太大且逐个绘制用PatchCollection合并绘制
边角纹理重复repeatx/repeaty设得太小调大到1000以上

5.4 调参时的备份习惯

最后再分享一个小习惯:我每次调整参数前都会先复制一份原参数,然后只改一个变量。这样如果效果不满意,能清楚知道是哪个因素造成的。hexgen目前核心代码只有几百行,但每次跑出一张像模像样的地图时还是很有成就感。后续我打算给它加上河流走向模拟和道路生成,如果你有兴趣,也可以往这个方向扩展。比如在低洼处累积水流,生成河流和湖泊;或者根据海拔和湿度生成城市选址点,都能让这个六边形世界活起来。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 20:26:03

Pandas不是数据库:Python数据分析与SQL到底怎么分工

1. Pandas到底是什么先直接回答这个标题里的问题&#xff1a;Pandas不是数据库。它是一个数据分析库&#xff0c;运行在Python里&#xff0c;专门用来处理结构化数据。很多人第一次接触Pandas会把它和数据库搞混&#xff0c;主要是因为Pandas里有一个叫DataFrame的东西&#xf…

作者头像 李华
网站建设 2026/9/9 20:25:41

使用助记词与派生路径创建钱包:Fuel SDK 助记词钱包完整指南

使用助记词与派生路径创建钱包&#xff1a;Fuel SDK 助记词钱包完整指南 【免费下载链接】fuels-ts Fuel Network Typescript SDK 项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-ts 本文以 Fuel 官方文档 mnemonic-wallet.md 为骨架&#xff0c;讲解 Fuel Ty…

作者头像 李华
网站建设 2026/9/9 20:25:25

STM32 LCD1602驱动库封装指南:从时序到移植的完整实践

简介&#xff1a;基于STM32的LCD1602基本库是一份轻量驱动源码&#xff0c;面向嵌入式初学者及需要快速在STM32工程中加入字符液晶显示的开发者&#xff0c;适用于设备状态显示、参数查看、简单菜单等场景。压缩包内共2个文件&#xff0c;包含1个c源文件和1个h头文件&#xff0…

作者头像 李华