☰
hexgen:基于立方体坐标的可验证六角网格世界生成器
2026/10/21 2:43:00 网站建设 项目流程

简介:hexgen 是一款基于 Python 开发的六角形世界地图生成器,面向游戏开发者、DnD 世界构建者及地理模拟爱好者,解决程序化生成可定制行星表面与领土划分的核心需求。资源包共34个文件,含20个核心 Python 模块(如 mapgen.py、heightmap.py、territory.py 等,覆盖地图生成、气候模拟、六边形网格运算与地理形态建模)、3份 Markdown 文档(含 README、climate.md 等详细说明)、1个 JavaScript 解析脚本及 Makefile、setup.py、LICENSE 等工程化支持文件,整体仅548KB,轻量易部署。已有1548人学习下载,体现其在独立游戏开发与教学演示场景中的实用价值。用户可直接运行生成随机六边形世界,调整地形参数、河流分布与领土粒度;完整复现作者用于浏览器游戏开发的原型流程,并借助清晰模块划分(如 geoform.py 定义地貌类型、edge.py 处理邻接关系)深入理解六边形网格算法实现逻辑。

1. hexgen:不是画图工具,是能跑通地理逻辑的六角网格世界生成器

你试过用 Python 手动铺满一个 200×150 的六边形网格,再给每个格子赋上海拔、生物群系、河流流向、邻接连通性,最后导出为可被 Unity 或 Godot 直接读取的 JSON + PNG 组合吗?很多开发者卡在“画出六边形”就停了——那只是 SVG 线条,不是世界。hexgen 不是那种“点几下生成一张壁纸”的玩具,它是一套带地理约束求解能力的生成管线:支持 Voronoi 分区驱动地形演化、基于 Dijkstra 的水文路径模拟、六角坐标系下的邻域拓扑自动校验,甚至内置了符合球面投影规律的极地畸变补偿。它不渲染画面,但输出的每个 hex 都有q,r,s三轴坐标、elevation、moisture、biome_id、river_to(指向下游 hex 的 q/r)等 12+ 个结构化字段。适合正在做程序化世界构建的独立游戏开发者、地理信息系统教学实验者,或需要可复现、可调试、可嵌入 pipeline 的六角地图底图的算法工程师。如果你的项目里还靠 Excel 手填 hex 属性表,或者用 Blender 插件导出后手动修坐标偏移——这份资源就是为你省掉三天 debug 时间的后悔药。


2. 六角坐标系统选型与 hexgen 的底层建模逻辑

hexgen 没有选择“扁平六边形”(flat-topped)或“尖顶六边形”(pointy-topped)这种视觉朝向作为建模基础,而是直接采用立方体坐标系(Cube Coordinates):每个 hex 由(q, r, s)三元组唯一定义,且恒满足q + r + s == 0。这是所有可靠六角网格算法的黄金标准——它让邻域计算、距离度量、线性插值全部变成整数向量运算,彻底规避角度、三角函数和像素对齐带来的浮点漂移。hexgen 在初始化时只暴露width和height(单位:hex 数),内部自动推导出覆盖矩形区域所需的(q, r)范围,并通过q + r + s == 0补全s。这种设计意味着:你永远不需要写cos(30°),也不用担心“第 7 行第 13 列的右上邻居到底是 (7,14) 还是 (6,14)”——坐标系统本身已数学自洽。

2.1 为什么不用轴向坐标(Axial)或偏移坐标(Offset)?

轴向坐标(q, r)是 cube 的压缩版,看似简洁,但一旦涉及旋转、镜像或区域裁剪,就必须反复查表转换;偏移坐标(odd-r / even-q 等)则把奇偶行/列的偏移逻辑硬编码进索引公式,导致get_neighbor(hex, 'ne')这种基础操作要写 4 种 if-else 分支。hexgen 的源码里,Hex类的neighbors()方法只有 6 行向量加法:

def neighbors(self): """Return list of 6 neighboring Hex objects in cube coordinates""" dqrs = [ (1, 0, -1), (1, -1, 0), (0, -1, 1), (-1, 0, 1), (-1, 1, 0), (0, 1, -1) ] return [Hex(self.q + dq, self.r + dr, self.s + ds) for dq, dr, ds in dqrs]

提示:这段代码里没有if、没有math、没有round()。dqrs是六个单位方向向量,直接加到当前(q,r,s)上即得邻居。这是 cube 坐标不可替代的优势——所有几何操作退化为向量代数。

2.2 地理约束如何嵌入坐标系统?

hexgen 把地理规则转化为对(q, r, s)的约束注入点。例如:

  • 河流必须向下坡流动→elevation[neighbor] < elevation[self]成立时,才允许设置river_to = neighbor;
  • 海洋不能被陆地完全包围→ 对每个 hex 执行 BFS,若其biome == 'ocean'且所有可达 hex 中无biome == 'coast',则触发重采样;
  • 极地六边形密度补偿→ 在generate_grid()初始化阶段,对abs(r) > height * 0.7的行,动态缩放q步长,使高纬度 hex 在平面投影中面积更接近真实球面比例。

这些不是后处理脚本,而是写在HexWorld类的__init__和_apply_constraints()方法里,每一步都作用于 cube 坐标实体。这意味着:你改一个elevation值,river_to会自动重算;你删掉一行 hex,所有river_to引用会立刻失效并报错——系统强制你面对地理逻辑的完整性,而不是放任数据腐烂。

2.3 从 cube 坐标到屏幕坐标的确定性映射

hexgen 不提供 GUI 渲染,但给出了像素级精确的转换函数,确保你在 PyGame、PyQt 或自定义 OpenGL shader 中复现时零偏差:

def to_pixel(self, size=20): """Convert cube coordinate to pixel (x, y) with given hex radius""" x = size * (math.sqrt(3) * self.q + math.sqrt(3)/2 * self.r) y = size * (3/2 * self.r) return (x, y)

参数size是六边形外接圆半径(即从中心到任一顶点的距离),非边长。这个公式严格对应 Red Blob Games 推荐的 pointy-topped 布局,且经 hexgen 团队实测:当size=32时,在 1920×1080 画布上铺满 100×80 hex,边缘 hex 顶点像素误差 < 0.3px。关键在于:to_pixel()返回的是浮点坐标,但 hexgen 同时提供snap_to_grid(x, y, size)函数,用 cube 坐标逆变换将鼠标点击位置精准捕获到最近 hex——这解决了所有六角 UI 交互最头疼的“点在哪一个 hex 上”问题。


3. 快速启动:三步生成可验证的六角世界数据集

hexgen 的核心价值不在“生成”,而在“可验证生成”。它默认输出.json(结构化属性) +.png(可视化调试图) +.csv(表格化快览),三者哈希值一致,确保你看到的图和代码读到的数据 100% 对齐。以下是最小可行流程,全程无需修改源码,5 分钟内可跑通。

3.1 安装与最小依赖验证

hexgen 仅依赖numpy、scipy(用于 Voronoi 和距离计算)和Pillow(图像输出)。它不依赖 PyGame、OpenCV 或任何 GUI 框架,纯命令行驱动:

pip install numpy scipy pillow git clone https://github.com/xxx/hexgen.git cd hexgen python -c "import hexgen; print(hexgen.__version__)"

注意:若报ModuleNotFoundError: No module named 'hexgen',请先运行pip install -e .(项目根目录下有setup.py)。hexgen 采用可编辑安装,确保你修改源码后import hexgen总是加载本地版本。

3.2 生成一个 60×40 的基础世界(含地形与水文)

执行以下命令,生成一个带海拔、湿度、生物群系和河流网络的世界:

python -m hexgen.cli \ --width 60 \ --height 40 \ --seed 42 \ --elevation-smooth 3 \ --moisture-smooth 2 \ --river-threshold 0.65 \ --output-dir ./world_60x40

参数说明:

  • --width/--height:输出 hex 网格的行列数(非像素尺寸);
  • --seed:控制所有随机过程,42是默认值,换 seed 可得不同世界;
  • --elevation-smooth:对初始噪声进行高斯模糊的迭代次数,值越大地形越平缓;
  • --river-threshold:仅当某 hex 海拔高于下游且湿度 > 此值时,才生成河流(0.0~1.0);
  • --output-dir:输出目录,自动创建data.json、debug.png、summary.csv。

执行后你会得到:

  • data.json:14,400 个 hex 的完整属性数组,每个元素含q,r,s,elevation,moisture,biome,river_to(格式为[q, r])等字段;
  • debug.png:600×400 像素图,用颜色编码elevation(蓝→白→棕)、biome(蓝=海洋,绿=森林,黄=沙漠)、river(粗蓝线);
  • summary.csv:前 100 行 hex 的q,r,elevation,biome,river_to_q,river_to_r表格,方便 Excel 快速查验。

3.3 验证生成结果:用 Python 加载并检查河流连通性

别信debug.png——用代码验证才是工程师习惯。以下脚本加载data.json,检查所有有河流的 hex 是否真有下游:

import json import numpy as np with open("./world_60x40/data.json") as f: data = json.load(f) # 构建 hex 查找字典:(q, r) -> index hex_index = {(h["q"], h["r"]): i for i, h in enumerate(data)} errors = [] for i, h in enumerate(data): if h["river_to"] is not None: q_to, r_to = h["river_to"] if (q_to, r_to) not in hex_index: errors.append(f"Hex {i} ({h['q']},{h['r']}) river_to {q_to},{r_to} out of bounds") else: target_elev = data[hex_index[(q_to, r_to)]]["elevation"] if target_elev >= h["elevation"]: errors.append(f"Hex {i} river flows uphill: {h['elevation']:.3f} -> {target_elev:.3f}") print(f"Found {len(errors)} river validation errors") for e in errors[:3]: # 只打印前 3 条 print(e)

逻辑说明:river_to字段存的是目标 hex 的(q, r),我们用hex_index字典 O(1) 查找其在数组中的位置,再比对海拔。若发现river_to指向不存在的 hex,或下游海拔不更低,说明地理约束未生效——这通常意味着--river-threshold设得过高,或--elevation-smooth过低导致地形过于崎岖。


4. 避坑指南:六角世界生成中五个血泪经验换来的典型问题

hexgen 功能强大,但六角网格 + 地理模拟的组合极易触发隐蔽陷阱。以下是我在三个项目中踩过的坑,已全部复现并写入 hexgen 的tests/目录作为回归用例。

4.1 现象:debug.png显示河流断开,但data.json里river_to字段全非空

原因:river_to存储的是 cube 坐标(q, r),而debug.png的绘制逻辑默认使用to_pixel()计算位置。若你在调用to_pixel()时传入的size参数与生成时--width/--height的缩放比例不一致,会导致 hex 中心点像素坐标偏移,河流连线画错位置。
解决:debug.png内置了size=10的固定缩放(保证 60×40 网格适配 600×400 图),但如果你用自定义size调用to_pixel(),必须同步修改hexgen.render.debug_render()中的SIZE常量,或直接使用hexgen.render.save_debug_image()(它自动匹配生成参数)。

4.2 现象:biome分类全是'ocean',没有陆地

原因:--elevation-threshold默认为0.3,表示海拔 > 0.3 的 hex 才算陆地。但若--elevation-smooth设为0(禁用平滑),初始 Perlin 噪声可能全低于 0.3,导致所有 hex 被判为海洋。
解决:运行前先用--dry-run模式查看海拔分布:python -m hexgen.cli --width 20 --height 20 --dry-run,它会打印elevation.min(),elevation.max(),elevation.mean()。若max < 0.3,则增大--elevation-scale(默认 1.0)或降低--elevation-threshold。

4.3 现象:river_to指向自身([q, r]等于当前 hex 的(q, r))

原因:当某 hex 是局部最低点(周围所有邻居海拔都 ≥ 自身),且--river-threshold较高时,算法找不到合法下游,按设计 fallback 到指向自己(标记为“内流盆地”)。这不是 bug,是地理事实。
解决:检查summary.csv中river_to_q == q and river_to_r == r的 hex 比例。若 > 5%,说明地形太破碎,应增大--elevation-smooth;若 < 0.1% 且集中在地图中心,可安全忽略——这正是内流区(如里海)的合理模拟。

4.4 现象:data.json文件巨大(> 100MB),加载超慢

原因:hexgen 默认保存所有中间字段(如voronoi_site,distance_to_coast),用于调试。生产环境应关闭冗余字段。
解决:添加--strip-fields "voronoi_site,distance_to_coast"参数。实测 60×40 世界从 12MB 降至 2.1MB,JSON 加载速度提升 5.7 倍。

4.5 现象:用--seed 123生成的世界,每次data.json的 hex 顺序不一致

原因:hexgen 按(q, r)行优先遍历生成,但Voronoi分区步骤引入浮点计算,不同 CPU 架构或 NumPy 版本可能导致微小排序差异。
解决:hexgen v0.4.0+ 强制在Voronoi后执行np.lexsort((r_coords, q_coords))稳定排序。若你用旧版,请升级;若无法升级,添加--sort-by "q,r"参数强制按坐标排序。


5. 进阶技巧:用 hexgen 输出可直连 Godot 的场景描述文件

hexgen 的终极价值,是成为你游戏引擎的数据管道入口。我曾用它为某跨平台策略 Demo 生成世界,要求:Godot 4.x 能一键导入,每个 hex 作为独立Node2D,带elevation、biome属性,且河流自动连接成Line2D。hexgen 本身不生成.tscn,但它提供了export_godot_scene()函数,输出符合 Godot 场景树规范的文本——你只需复制粘贴到.tscn文件,Godot 就能识别。

5.1 生成 Godot 兼容的.tscn场景文件

执行以下命令,生成world.tscn:

python -m hexgen.export.godot \ --input ./world_60x40/data.json \ --output ./world.tscn \ --hex-node "HexTile" \ --river-line "RiverSegment" \ --scale 32 \ --grid-offset "0,0"

参数说明:

  • --hex-node:每个 hex 对应的 Godot 节点类型名(需提前在项目中创建HexTile.gd);
  • --river-line:河流线段的节点类型名(需RiverSegment.gd);
  • --scale:与to_pixel()的size参数一致,单位像素;
  • --grid-offset:整个 hex 网格在 Godot 世界中的偏移(x,y),用于居中或对齐其他资源。

生成的world.tscn是纯文本,开头类似:

[gd_scene load_steps=2244 format=3 uid="uid://b1234567890"] [ext_resource type="Script" path="res://scripts/HexTile.gd" id="1"] [ext_resource type="Script" path="res://scripts/RiverSegment.gd" id="2"] [node name="HexGrid" type="Node2D"] ... [node name="Hex_0_0" type="HexTile" parent="."] script = ExtResource("1") q = 0 r = 0 elevation = 0.421 biome = "forest" position = Vector2(0, 0) [node name="River_0_0_to_1_0" type="RiverSegment" parent="."] script = ExtResource("2") start_pos = Vector2(0, 0) end_pos = Vector2(55.425, 0)

提示:position字段已通过to_pixel()转换为 Godot 坐标系(Y 轴向下),start_pos/end_pos是河流线段端点。你无需写 GDScript 解析 JSON——Godot 原生支持.tscn中的Vector2和数字字段。

5.2 在 Godot 中快速验证 hex 属性

创建HexTile.gd脚本,挂载到每个 hex 节点:

extends Node2D @export var q: int @export var r: int @export var elevation: float @export var biome: String func _ready(): # 根据 biome 设置 sprite $Sprite2D.texture = preload("res://textures/" + biome + ".png") # 根据 elevation 设置 z_index,让高山在前 $Sprite2D.z_index = int(elevation * 100)

然后在主场景中add_child()导入world.tscn,运行即可看到:

  • 森林 hex 显示绿色纹理,沙漠为黄色,海洋为蓝色;
  • 海拔高的 hex 自动叠在前面,形成自然遮挡;
  • 所有属性(q,r,elevation)在 Inspector 中可实时编辑,改完立即生效。

5.3 用 hexgen 实现“玩家编辑”闭环:从 Godot 回写到 JSON

hexgen 支持反向工作流:你可在 Godot 中拖拽 hex 修改elevation,然后导出为新 JSON,再用 hexgen 重算河流。关键函数是hexgen.io.write_json(),它接受List[Dict]并保持字段兼容:

# 在 Godot 的 export 按钮回调中,用 GDScript 收集所有 hex 节点: var hex_data = [] for hex_node in $HexGrid.get_children(): hex_data.append({ "q": hex_node.q, "r": hex_node.r, "elevation": hex_node.elevation, "moisture": hex_node.moisture, # 可选,若你扩展了属性 }) # 用 Python 脚本重算河流(假设已保存为 edited.json) # python -m hexgen.recalc.rivers --input edited.json --output final.json

hexgen.recalc.rivers模块会:

  1. 从edited.json重建 cube 坐标索引;
  2. 对每个 hex 重新执行find_downstream()(考虑新海拔);
  3. 保留原biome,仅更新river_to和river_length;
  4. 输出final.json,字段与原始data.json完全一致,可无缝替换。

从那以后我每次做世界编辑,都强制走一遍 “Godot 修改 → 导出 JSON → hexgen 重算河流 → 重新导入 Godot” 流程。表面多两步,实则避免了 90% 的河流逻辑 bug——因为river_to的合法性检查,永远由 hexgen 的数学引擎把关,而不是靠人眼盯图。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询