尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

用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)时qx, rz反向转换时xq, zr, y-x-z。不要被公式吓到本质上只是去掉一个冗余维度。网格半径R决定了生成地图的大小。以(0,0)为中心所有满足max(abs(q), abs(r), abs(qr)) R的格子共同组成一个大的六边形地图。这一步确定的是可玩范围半径越大格子数量越多后续算法耗时也越高。半径R和格子数不是线性关系而是接近3R^23R1所以半径从30涨到60工作量会变成原来的4倍左右这点在做大图前要有心理准备。2.2 地形生成噪声是骨架阈值是血肉世界地图不能是纯随机色块所以需要噪声。柏林噪声Perlin Noise或者Simplex噪声是程序化生成地形最常用的工具。它的特点是连续且各向同性简单说就是相近坐标得到的值相近这样生成的高度图看起来才有连续的大陆和海洋。你可以用noise库的pnoise2函数传入x和y的缩放系数得到范围在[-1,1]之间的值。缩放系数很关键数值小地形起伏变化慢会出现大洲数值大地形破碎像群岛。用两个不同频率的噪声叠加能得到更自然的海岸线。有了高度场之后地形类型分配本质上是一个查表过程。我习惯把高度分成五档深海、浅海、沙滩、平原、山地再额外用一个湿度噪声决定森林、草原和沙漠。这些值不需要太复杂关键是海平面阈值。海平面抬升一点大陆面积迅速缩小降低一点又可能出现超级大陆。所以我通常把海平面阈值做成参数方便反复调整。湿度噪声则用来打破单调否则一片大陆全是绿色看起来没有层次。3. 动手实现写一个可运行的hexgen3.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(qr)) 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, seed42): terrain {} for q, r in grid.cells: x q * 0.08 y r * 0.08 height noise.pnoise2(x, y, octaves5, persistence0.6, repeatx1000, repeaty1000, baseseed) moisture noise.pnoise2(q * 0.1 100, r * 0.1 100, octaves3, baseseed 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, size10, filenamehexmap.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, closedTrue) patches.append(poly) colors.append(COLORS[terrain[(q, r)]]) p PatchCollection(patches, match_originalFalse) 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, dpi150, bbox_inchestight)运行之后你会得到一张可以直接放进地图编辑器里的PNG。控制台打印每个地形类型的数量可以快速判断分布是否合理。如果你用的是radius30格子数大约2700个跑一次不到一秒钟体验很顺滑。4. 参数调优与效果控制4.1 噪声缩放决定大陆尺度很多人第一次跑出来觉得地图太碎片化其实就是缩放系数太大。你可以把generate_terrain里的xq0.08改成q0.03再看效果大陆会明显聚拢。原理是柏林噪声的采样点间距越小相邻格子的值越接近高度变化越平缓。这个参数没有标准答案跟你的地图半径强相关。我一般先让半径R乘以0.002到0.005得到一个初始值再根据输出效果微调。半径30和半径60需要用不同的缩放系数否则大图看起来要么太平要么太碎。4.2 海平面与生物群系阈值海平面是最能改变地图观感的旋钮。用height0.35作为深海0.35到0.42作为浅海0.42到0.47作为沙滩这个分布能保证大部分地图有一两块主要大陆。如果想把地图变成荒漠星球就把湿度阈值整体压低让更多格子走desert分支。调阈值的时候我强烈建议写一个快速小函数输入参数后直接打印各地形占比几秒钟就能判断要不要继续调比一张张看图效率高很多。你还可以把海洋和陆地的分界线单独提出来作为变量比如sea_level0.42。这样后续做海平面上升效果时只需要改一个变量。地形分类函数里所有的高度阈值最好也从硬编码改成参数传递项目变大之后会感谢自己当初留了这个口子。4.3 用随机种子控制版本我保留seed参数这样每次生成的地图都可以复现。做游戏关卡时玩家看到的是随机地图但出了bug需要回滚到特定地图只要记下seed就能精确定位。这个习惯一定要养成后期省很多事。举个例子你调了一晚上参数终于生成了一个特别漂亮的地图结果第二天打开程序发现地图变了如果当时没有记录seed就只能凭感觉重新调了。所以hexgen的generate_terrain里base参数直接由seed控制每次运行相同seed出来的地形就完全一样。5. 踩坑记录与排查技巧5.1 六边形之间出现锯齿缝隙第一次跑出图时六边形之间的白边特别明显。原因有两个一是matplotlib多边形默认有边缘色二是坐标精度导致相邻多边形没有完全贴合。解决办法是把edgecolor设成很浅的灰色或者黑色linewidth调成0.3如果还漏白可以给每个多边形设置相同edgecolor并适当扩大0.5个单位的包围盒。注意不要用白色边否则在浅色地块上超级难看。实测linewidth0.5edgecolorblack在dpi150的PNG里表现最稳。5.2 noise库的坐标越界警告noise.pnoise2如果采样点坐标接近repeatx/repeaty设置的值容易出现越界或者重复pattern。我建议在生成每个格子时坐标先取模或者把repeatx和repeaty设成远大于实际坐标范围。我写的例子用的是repeatx1000, repeaty1000对半径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目前核心代码只有几百行但每次跑出一张像模像样的地图时还是很有成就感。后续我打算给它加上河流走向模拟和道路生成如果你有兴趣也可以往这个方向扩展。比如在低洼处累积水流生成河流和湖泊或者根据海拔和湿度生成城市选址点都能让这个六边形世界活起来。本文还有配套的精品资源点击获取
返回列表