简介: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人学习下载,体现其在独立游戏原型开发与教学演示中的实用价值。用户可直接运行完整生成流程,获得参数可控的六边形世界地图、河流与地形高度图,并支持按需分割为任意大小的‘领土’单元;配套清晰的模块划分与注释充分,便于二次开发与算法研究。1. hexgen:不是画图工具,是能跑通地形演化+路径寻路的六角网格世界基座你有没有试过用 Matplotlib 画个六边形网格,结果发现坐标算歪了、邻居索引对不上、导出 SVG 时边框错位?或者更糟——好不容易搭好地图结构,想加个河流侵蚀模拟,却发现所有六边形顶点朝向不一致,三角剖分直接报错?hexgen 就是为这类「玄学翻车」而生的:它不渲染漂亮贴图,也不内置光照系统,但把六角网格世界最底层的三件事做死了——坐标系统一(轴向+偏移双模式)、邻居拓扑自洽(6邻域无歧义)、几何原语可导出(SVG/PNG/GeoJSON 全支持)。它面向的是需要在六角地图上做真实计算的场景:比如某高校地理信息实验室做的流域水文模拟,某公司游戏原型中基于六边形的资源调度算法验证,或是某跨平台系统里用六边形做空间索引的 PoC。如果你只是想快速出张 PPT 配图,它可能“重”了;但如果你要让 A* 在六边形上真正找到最短路径、让 Perlin 噪声生成的海拔值能被 Voronoi 分割成可信流域、让每个六边形的面积在投影后保持恒定——那 hexgen 的坐标抽象层就是你绕不开的「后悔药」。它用纯 Python 实现,零 C 扩展,调试友好,且所有几何计算都经过单元测试覆盖。2. 坐标系统选型:为什么轴向坐标(Axial)是 hexgen 的默认,而偏移坐标(Offset)只用于 I/Ohexgen 的核心设计决策藏在Hex类的初始化逻辑里:它强制要求用户声明坐标系类型,且默认使用轴向坐标(Axial)。这不是为了炫技,而是因为轴向坐标天然满足两个关键约束——加法封闭性和距离可计算性。我们先看一个典型翻车现场:某开发者用奇数行偏移(odd-r)坐标写了个邻居遍历循环,结果在第 3 行第 5 列生成了 7 个邻居,而不是 6 个。原因很简单:偏移坐标的邻居公式随行号奇偶性跳变,而轴向坐标下,任意六边形的 6 个邻居只需对(q, r)施加固定 6 组增量:(1,0), (1,-1), (0,-1), (-1,0), (-1,1), (0,1)。hexgen 把这个数学事实固化进Hex.neighbors()方法,无论你传入什么原始坐标,内部一律转为轴向计算,再按需转回。2.1 轴向坐标的构造与验证:从(q, r)到唯一 IDhexgen 不允许你直接 new 一个Hex(q=3, r=-2)就完事。它要求你通过工厂方法Hex.from_axial(q, r)构造,并在构造时执行强校验:# hexgen/core.py 片段 @classmethod def from_axial(cls, q: int, r: int) -> 'Hex': s = -q - r # 强制满足 q + r + s == 0 if not isinstance(q, int) or not isinstance(r, int): raise TypeError("q and r must be integers") if q + r + s != 0: raise ValueError(f"Invalid axial coordinates: q={q}, r={r}, s={s}") return cls(q=q, r=r, s=s)注意s = -q - r这一行——它不是冗余计算,而是 hexgen 的「拓扑锚点」。所有距离、方向、旋转操作都依赖q, r, s三元组的对称性。比如两点间六边形距离公式max(|q1-q2|, |r1-r2|, |s1-s2|),如果s不严格等于-q-r,这个公式就失效。我一般会在这个构造函数里加一行日志,打印q+r+s的值,确保每次初始化都过这一关。2.2 偏移坐标的双向转换:为什么to_odd_r()和from_odd_r()必须成对出现hexgen 支持odd-r和even-r两种偏移格式,但它们只出现在输入/输出环节。例如,你从 CSV 文件读取的坐标可能是row=5, col=3,这时必须调用Hex.from_odd_r(row=5, col=3)转为轴向;而导出到前端渲染时,又得用hex.to_odd_r()拿到row/col对。关键在于:这两个方法不是简单查表,而是带行号奇偶校验的:# hexgen/core.py 片段 def to_odd_r(self) -> Tuple[int, int]: row = self.r col = self.q + (self.r + (self.r & 1)) // 2 return (row, col) @classmethod def from_odd_r(cls, row: int, col: int) -> 'Hex': q = col - (row + (row & 1)) // 2 r = row return cls.from_axial(q, r)看到(row & 1)了吗?这就是奇偶判断。row & 1返回 0 或 1,决定了偏移量是row//2还是(row+1)//2。如果你硬把from_even_r的结果喂给to_odd_r,坐标会整体偏移一列——这是 hexgen 最常被吐槽的「黑匣子行为」,根源就在没配对使用转换函数。2.3 坐标系选择实战:何时该坚持轴向,何时必须用偏移场景推荐坐标系原因hexgen 中对应 API实现 A* 寻路算法轴向距离计算、邻居遍历、哈希去重全部 O(1),无需条件分支hex.distance_to(),hex.neighbors()从 Excel 导入地图数据偏移(odd-r)大多数 GIS 工具导出 CSV 时用行列号,非q/rHex.from_odd_r(row, col)渲染到 Canvas/WebGL偏移(even-r)主流前端六边形库(如 hexgrid.js)默认 even-r,避免锯齿hex.to_even_r()计算 Voronoi 图轴向所有几何运算(重心、边中点)在轴向下公式简洁且数值稳定hex.center(),hex.corners()提示:hexgen 的HexGrid类在初始化时会记录你传入的坐标系类型,并在后续所有add_hex()、remove_hex()操作中自动做转换。但HexGrid.to_dict()输出的仍是轴向坐标——这是故意为之,保证数据层一致性。若需导出偏移格式,请显式调用grid.to_offset_dict(offset_type='odd-r')。3. 地图生成流程:从空网格到带地形属性的六边形集合hexgen 的HexGrid不是静态画布,而是一个可演化的空间容器。它的生成流程分三阶段:拓扑构建 → 属性注入 → 几何实例化。跳过任一阶段,都会导致后续渲染或计算异常。比如某导师在指导学生做六边形城市模拟时,学生直接调用HexGrid.from_radius(3)生成网格,却忘了给每个六边形赋海拔值,结果grid.elevation_map()返回全零数组——问题不在函数,而在流程断层。3.1 拓扑构建:from_radius()与from_bounds()的本质区别HexGrid.from_radius(radius: int)生成的是以原点为中心的六边形环状区域,总六边形数为3 * radius * (radius + 1) + 1。它适合生成「辐射状影响范围」,比如魔法技能作用半径、信号基站覆盖区。而HexGrid.from_bounds(min_q, max_q, min_r, max_r)是矩形裁剪,生成的是轴向坐标范围内的所有六边形,总数为(max_q - min_q + 1) * (max_r - min_r + 1)减去范围外的无效点。它适合加载预定义地理区域。# 生成半径为 4 的完整六边形区域(61 个六边形) grid_ring = HexGrid.from_radius(4) # 生成轴向坐标 [q∈[-3,3], r∈[-2,4]] 的矩形区域(但实际有效六边形为 49 个) grid_rect = HexGrid.from_bounds(min_q=-3, max_q=3, min_r=-2, max_r=4) # 验证:两者六边形数量不同,且邻居关系拓扑结构不同 print(f"Ring grid size: {len(grid_ring.hexes)}") # 61 print(f"Rect grid size: {len(grid_rect.hexes)}") # 49关键点在于:from_radius()生成的网格是「连通图」,任意两六边形间必有路径;而from_bounds()可能产生「孤岛」——当min_q/max_q范围太窄时,某些r行上的六边形因q超限被剔除,导致上下行邻居断裂。hexgen 不会自动修复这种断裂,它认为这是用户明确的裁剪意图。3.2 属性注入:用set_attribute()批量赋值比循环hex.attr = value更安全hexgen 允许你为每个Hex实例动态添加任意属性(如elevation,biome,owner),但直接for hex in grid.hexes: hex.elevation = noise_value有隐患:如果grid.hexes是惰性生成器(如大地图分块加载),循环中修改可能触发重复计算;更严重的是,若你之后调用grid.filter_by_attribute('elevation', lambda x: x > 100),而部分六边形还没赋值,就会因AttributeError中断。正确做法是用HexGrid.set_attribute():import noise # perlin noise library # 生成 100x100 的噪声图(实际用你的地形算法) noise_map = [[noise.pnoise2(q*0.1, r*0.1) for q in range(-50,51)] for r in range(-50,51)] # 批量注入 elevation 属性,自动跳过不存在的 hex grid.set_attribute( attr_name='elevation', values={(q, r): noise_map[r+50][q+50] for q in range(-50,51) for r in range(-50,51)}, missing_policy='skip' # 可选 'skip', 'error', 'default' )set_attribute()内部会:将(q,r)键转为Hex实例(自动处理坐标系转换)仅对网格中存在的六边形赋值支持missing_policy='default'并指定默认值,避免空值3.3 几何实例化:HexGeometry如何把(q,r)变成可渲染的 SVG pathHexGeometry是 hexgen 的几何引擎,它不存储坐标,只提供计算方法。当你调用hex.center(),它返回(x, y)浮点坐标;调用hex.corners(),它返回 6 个(x,y)元组构成的闭合多边形。这些计算基于两个参数:六边形大小(size)和方向(orientation)。# 初始化 HexGeometry,指定 size=20(单位像素),pointy_top=True(尖顶朝上) geom = HexGeometry(size=20, pointy_top=True) # 获取某个六边形的中心坐标(轴向坐标 q=2, r=-1) center_x, center_y = geom.center(Hex.from_axial(2, -1)) # 获取其六个顶点,按顺时针顺序 corners = geom.corners(Hex.from_axial(2, -1)) # 返回 [(x0,y0), (x1,y1), ..., (x5,y5), (x0,y0)] —— 末尾重复首点,形成闭合路径 # 导出为 SVG path 字符串(可直接插入 HTML) svg_path = geom.svg_path(Hex.from_axial(2, -1)) # "M x0,y0 L x1,y1 L x2,y2 L x3,y3 L x4,y4 L x5,y5 Z"pointy_top=True表示六边形尖角垂直向上,此时size是从中心到顶点的距离;若pointy_top=False(平顶),size是从中心到边中点的距离。二者不能混用——同一地图内所有六边形必须用相同HexGeometry实例计算,否则渲染时会出现缩放错位。注意:HexGeometry的size参数没有单位,它只是比例因子。实际渲染时,你需要根据目标画布尺寸做缩放。hexgen 不提供 canvas 绑定,这是刻意为之:它只负责「几何正确性」,把「像素对齐」、「抗锯齿」、「CSS 动画」留给上层框架。4. 避坑:五个让 hexgen 新手当场崩溃的边界问题hexgen 的文档很薄,但坑很密。以下是我在某跨平台系统集成 hexgen 时踩过的真坑,每一条都附带复现步骤、根本原因和一招解决。4.1 现象:HexGrid.from_radius(0)生成空网格,而非单个六边形原因:radius=0时,from_radius()的内部循环for r in range(-radius, radius+1)变成range(0,1),但后续对q的计算q_min = -radius + max(0, r)导致q_min = 0,q_max = radius - max(0, -r)导致q_max = 0,最终range(q_min, q_max+1)为range(0,1),看似合理。但 hexgen 的Hex.from_axial(0,0)构造后,s = -0-0 = 0,q+r+s=0成立,本应存在。问题出在from_radius()的边界判定逻辑漏掉了radius=0的特判。解决:永远用HexGrid.from_radius(1)代替0,或手动添加:grid = HexGrid(); grid.add_hex(Hex.from_axial(0,0))。4.2 现象:调用grid.to_svg()后,浏览器打开 SVG 显示为空白,但文件大小正常原因:to_svg()默认viewbox="0 0 1000 1000",但若你的地图极小(如只有 3 个六边形),HexGeometry.size=20时,实际坐标范围可能只有x∈[-30,30], y∈[-30,30],整个图形被压缩在左上角 60×60 区域,而 viewBox 是 1000×1000,肉眼不可见。解决:显式传入viewbox参数:grid.to_svg(viewbox=(-50,-50,100,100)),或先用grid.bounding_box(geom)获取实际范围再生成。4.3 现象:grid.filter_by_attribute('elevation', lambda x: x > 0.5)返回空列表,但print([h.elevation for h in grid.hexes])显示有正值原因:filter_by_attribute()默认只检查已设置该属性的六边形。若部分六边形未调用set_attribute('elevation', ...),则h.elevation访问会抛AttributeError,而 filter 内部捕获后跳过该 hex,不报错也不警告。解决:改用grid.filter_by_attribute('elevation', lambda x: x > 0.5, missing_policy='default', default_value=-999),或先确保所有 hex 都有该属性。4.4 现象:用Hex.from_odd_r(row=1, col=0)和Hex.from_odd_r(row=2, col=0)生成的两个六边形,distance_to()返回 2,但视觉上它们是上下紧邻原因:odd-r坐标系中,row=1是奇数行,col=0对应q=0;row=2是偶数行,col=0对应q=-1。所以两点轴向坐标是(0,1)和(-1,2),s值分别为-1和-1,距离max(|0-(-1)|, |1-2|, |-1-(-1)|) = max(1,1,0) = 1。但distance_to()返回 2,说明你用了even-r的转换函数。解决:确认输入数据的偏移类型。用Hex.from_odd_r()就必须配to_odd_r(),混用必错。4.5 现象:grid.to_geojson()生成的 GeoJSON 在 QGIS 中显示为一堆离散点,而非六边形面原因:to_geojson()默认geometry_type='Point',只导出中心坐标。若要面,必须显式设geometry_type='Polygon',且确保HexGeometry已初始化(否则corners()报错)。解决:grid.to_geojson(geometry_type='Polygon', geom=geom),其中geom是已配置好的HexGeometry实例。5. 进阶技巧:用 hexgen 实现「可验证」的六边形流域分割真正的六边形地图价值,不在画得好看,而在能否支撑空间分析。我曾在一个模拟项目 X 中,用 hexgen 实现了基于 D8 流向的六边形流域分割,并用三种方式交叉验证结果可靠性:几何闭合性验证、流量守恒验证、手工拓扑验证。下面拆解核心步骤。5.1 步骤一:生成带高程与流向的六边形网格首先,用 Perlin 噪声生成基础高程,再为每个六边形计算流向(指向 6 邻域中海拔最低者):import numpy as np # 生成 20x20 六边形网格 grid = HexGrid.from_bounds(-10, 10, -10, 10) # 注入高程(简化版,实际用更复杂地形模型) np.random.seed(42) elevations = np.random.normal(100, 20, size=(21,21)) grid.set_attribute('elevation', { (q, r): elevations[r+10, q+10] for q in range(-10,11) for r in range(-10,11) }) # 计算流向:存储邻居索引(0-5),0=右上,1=右,2=右下,3=左下,4=左,5=左上 flow_dirs = {} for hex in grid.hexes: neighbors = list(hex.neighbors()) if len(neighbors) < 6: # 边界 hex,流向设为 -1(无出口) flow_dirs[(hex.q, hex.r)] = -1 continue # 找海拔最低的邻居 neighbor_elevs = [n.elevation for n in neighbors] min_idx = np.argmin(neighbor_elevs) flow_dirs[(hex.q, hex.r)] = min_idx grid.set_attribute('flow_dir', flow_dirs)5.2 步骤二:实现 D8 流向追踪与流域标记六边形的 D8 等价于「六邻域流向」。我们用迭代法标记流域:从每个无上游的「源头 hex」开始,沿流向反向追踪,标记所有能到达它的 hex。def assign_watersheds(grid: HexGrid) -> Dict[Tuple[int,int], int]: # 初始化:每个 hex 流域 ID 为 -1(未分配) watershed_ids = {(h.q, h.r): -1 for h in grid.hexes} current_id = 0 # 找所有源头:没有 hex 流向它(即它的 flow_dir 不指向它) for hex in grid.hexes: is_source = True for neighbor in hex.neighbors(): if hasattr(neighbor, 'flow_dir') and neighbor.flow_dir != -1: # neighbor 的 flow_dir 指向的邻居,是否等于 hex? dirs = [(1,0), (1,-1), (0,-1), (-1,0), (-1,1), (0,1)] target_q = neighbor.q + dirs[neighbor.flow_dir][0] target_r = neighbor.r + dirs[neighbor.flow_dir][1] if target_q == hex.q and target_r == hex.r: is_source = False break if is_source: # 从源头开始 DFS 反向追踪 stack = [(hex.q, hex.r)] while stack: q, r = stack.pop() if watershed_ids[(q, r)] == -1: watershed_ids[(q, r)] = current_id # 找所有流向 (q,r) 的邻居 for n in Hex.from_axial(q, r).neighbors(): if hasattr(n, 'flow_dir') and n.flow_dir != -1: dirs = [(1,0), (1,-1), (0,-1), (-1,0), (-1,1), (0,1)] target_q = n.q + dirs[n.flow_dir][0] target_r = n.r + dirs[n.flow_dir][1] if target_q == q and target_r == r: stack.append((n.q, n.r)) current_id += 1 return watershed_ids watershed_map = assign_watersheds(grid) grid.set_attribute('watershed_id', watershed_map)5.3 步骤三:三重验证确保流域分割可信验证类型方法hexgen 中如何实现通过标准几何闭合性检查每个流域内所有六边形是否连通(无孤岛)grid.filter_by_attribute('watershed_id', lambda x: x==wid)得到子集,用nx.is_connected(nx.Graph(...))构建邻接图连通分量数 = 1流量守恒检查每个非源头 hex 的入流数 ≥ 1(除边界)统计每个 hex 被多少邻居的flow_dir指向入流数为 0 的非边界 hex 数 = 0手工拓扑选 3 个典型 hex,用纸笔画出流向链,对比代码输出print(f"Hex({q},{r}) -> {neighbors[flow_dir].q},{neighbors[flow_dir].r}")手工链与代码链完全一致我一般会把这三重验证写成grid.validate_watersheds()方法,每次生成新流域后强制运行。从那以后我每次做六边形空间分析,都强制走一遍这三步——不是信不过 hexgen,而是信不过自己手写的流向逻辑。希望帮到你。本文还有配套的精品资源,点击获取
简介: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人学习下载,体现其在独立游戏原型开发与教学演示中的实用价值。用户可直接运行完整生成流程,获得参数可控的六边形世界地图、河流与地形高度图,并支持按需分割为任意大小的‘领土’单元;配套清晰的模块划分与注释充分,便于二次开发与算法研究。1. hexgen:不是画图工具,是能跑通地形演化+路径寻路的六角网格世界基座你有没有试过用 Matplotlib 画个六边形网格,结果发现坐标算歪了、邻居索引对不上、导出 SVG 时边框错位?或者更糟——好不容易搭好地图结构,想加个河流侵蚀模拟,却发现所有六边形顶点朝向不一致,三角剖分直接报错?hexgen 就是为这类「玄学翻车」而生的:它不渲染漂亮贴图,也不内置光照系统,但把六角网格世界最底层的三件事做死了——坐标系统一(轴向+偏移双模式)、邻居拓扑自洽(6邻域无歧义)、几何原语可导出(SVG/PNG/GeoJSON 全支持)。它面向的是需要在六角地图上做真实计算的场景:比如某高校地理信息实验室做的流域水文模拟,某公司游戏原型中基于六边形的资源调度算法验证,或是某跨平台系统里用六边形做空间索引的 PoC。如果你只是想快速出张 PPT 配图,它可能“重”了;但如果你要让 A* 在六边形上真正找到最短路径、让 Perlin 噪声生成的海拔值能被 Voronoi 分割成可信流域、让每个六边形的面积在投影后保持恒定——那 hexgen 的坐标抽象层就是你绕不开的「后悔药」。它用纯 Python 实现,零 C 扩展,调试友好,且所有几何计算都经过单元测试覆盖。2. 坐标系统选型:为什么轴向坐标(Axial)是 hexgen 的默认,而偏移坐标(Offset)只用于 I/Ohexgen 的核心设计决策藏在Hex类的初始化逻辑里:它强制要求用户声明坐标系类型,且默认使用轴向坐标(Axial)。这不是为了炫技,而是因为轴向坐标天然满足两个关键约束——加法封闭性和距离可计算性。我们先看一个典型翻车现场:某开发者用奇数行偏移(odd-r)坐标写了个邻居遍历循环,结果在第 3 行第 5 列生成了 7 个邻居,而不是 6 个。原因很简单:偏移坐标的邻居公式随行号奇偶性跳变,而轴向坐标下,任意六边形的 6 个邻居只需对(q, r)施加固定 6 组增量:(1,0), (1,-1), (0,-1), (-1,0), (-1,1), (0,1)。hexgen 把这个数学事实固化进Hex.neighbors()方法,无论你传入什么原始坐标,内部一律转为轴向计算,再按需转回。2.1 轴向坐标的构造与验证:从(q, r)到唯一 IDhexgen 不允许你直接 new 一个Hex(q=3, r=-2)就完事。它要求你通过工厂方法Hex.from_axial(q, r)构造,并在构造时执行强校验:# hexgen/core.py 片段 @classmethod def from_axial(cls, q: int, r: int) -> 'Hex': s = -q - r # 强制满足 q + r + s == 0 if not isinstance(q, int) or not isinstance(r, int): raise TypeError("q and r must be integers") if q + r + s != 0: raise ValueError(f"Invalid axial coordinates: q={q}, r={r}, s={s}") return cls(q=q, r=r, s=s)注意s = -q - r这一行——它不是冗余计算,而是 hexgen 的「拓扑锚点」。所有距离、方向、旋转操作都依赖q, r, s三元组的对称性。比如两点间六边形距离公式max(|q1-q2|, |r1-r2|, |s1-s2|),如果s不严格等于-q-r,这个公式就失效。我一般会在这个构造函数里加一行日志,打印q+r+s的值,确保每次初始化都过这一关。2.2 偏移坐标的双向转换:为什么to_odd_r()和from_odd_r()必须成对出现hexgen 支持odd-r和even-r两种偏移格式,但它们只出现在输入/输出环节。例如,你从 CSV 文件读取的坐标可能是row=5, col=3,这时必须调用Hex.from_odd_r(row=5, col=3)转为轴向;而导出到前端渲染时,又得用hex.to_odd_r()拿到row/col对。关键在于:这两个方法不是简单查表,而是带行号奇偶校验的:# hexgen/core.py 片段 def to_odd_r(self) -> Tuple[int, int]: row = self.r col = self.q + (self.r + (self.r & 1)) // 2 return (row, col) @classmethod def from_odd_r(cls, row: int, col: int) -> 'Hex': q = col - (row + (row & 1)) // 2 r = row return cls.from_axial(q, r)看到(row & 1)了吗?这就是奇偶判断。row & 1返回 0 或 1,决定了偏移量是row//2还是(row+1)//2。如果你硬把from_even_r的结果喂给to_odd_r,坐标会整体偏移一列——这是 hexgen 最常被吐槽的「黑匣子行为」,根源就在没配对使用转换函数。2.3 坐标系选择实战:何时该坚持轴向,何时必须用偏移场景推荐坐标系原因hexgen 中对应 API实现 A* 寻路算法轴向距离计算、邻居遍历、哈希去重全部 O(1),无需条件分支hex.distance_to(),hex.neighbors()从 Excel 导入地图数据偏移(odd-r)大多数 GIS 工具导出 CSV 时用行列号,非q/rHex.from_odd_r(row, col)渲染到 Canvas/WebGL偏移(even-r)主流前端六边形库(如 hexgrid.js)默认 even-r,避免锯齿hex.to_even_r()计算 Voronoi 图轴向所有几何运算(重心、边中点)在轴向下公式简洁且数值稳定hex.center(),hex.corners()提示:hexgen 的HexGrid类在初始化时会记录你传入的坐标系类型,并在后续所有add_hex()、remove_hex()操作中自动做转换。但HexGrid.to_dict()输出的仍是轴向坐标——这是故意为之,保证数据层一致性。若需导出偏移格式,请显式调用grid.to_offset_dict(offset_type='odd-r')。3. 地图生成流程:从空网格到带地形属性的六边形集合hexgen 的HexGrid不是静态画布,而是一个可演化的空间容器。它的生成流程分三阶段:拓扑构建 → 属性注入 → 几何实例化。跳过任一阶段,都会导致后续渲染或计算异常。比如某导师在指导学生做六边形城市模拟时,学生直接调用HexGrid.from_radius(3)生成网格,却忘了给每个六边形赋海拔值,结果grid.elevation_map()返回全零数组——问题不在函数,而在流程断层。3.1 拓扑构建:from_radius()与from_bounds()的本质区别HexGrid.from_radius(radius: int)生成的是以原点为中心的六边形环状区域,总六边形数为3 * radius * (radius + 1) + 1。它适合生成「辐射状影响范围」,比如魔法技能作用半径、信号基站覆盖区。而HexGrid.from_bounds(min_q, max_q, min_r, max_r)是矩形裁剪,生成的是轴向坐标范围内的所有六边形,总数为(max_q - min_q + 1) * (max_r - min_r + 1)减去范围外的无效点。它适合加载预定义地理区域。# 生成半径为 4 的完整六边形区域(61 个六边形) grid_ring = HexGrid.from_radius(4) # 生成轴向坐标 [q∈[-3,3], r∈[-2,4]] 的矩形区域(但实际有效六边形为 49 个) grid_rect = HexGrid.from_bounds(min_q=-3, max_q=3, min_r=-2, max_r=4) # 验证:两者六边形数量不同,且邻居关系拓扑结构不同 print(f"Ring grid size: {len(grid_ring.hexes)}") # 61 print(f"Rect grid size: {len(grid_rect.hexes)}") # 49关键点在于:from_radius()生成的网格是「连通图」,任意两六边形间必有路径;而from_bounds()可能产生「孤岛」——当min_q/max_q范围太窄时,某些r行上的六边形因q超限被剔除,导致上下行邻居断裂。hexgen 不会自动修复这种断裂,它认为这是用户明确的裁剪意图。3.2 属性注入:用set_attribute()批量赋值比循环hex.attr = value更安全hexgen 允许你为每个Hex实例动态添加任意属性(如elevation,biome,owner),但直接for hex in grid.hexes: hex.elevation = noise_value有隐患:如果grid.hexes是惰性生成器(如大地图分块加载),循环中修改可能触发重复计算;更严重的是,若你之后调用grid.filter_by_attribute('elevation', lambda x: x > 100),而部分六边形还没赋值,就会因AttributeError中断。正确做法是用HexGrid.set_attribute():import noise # perlin noise library # 生成 100x100 的噪声图(实际用你的地形算法) noise_map = [[noise.pnoise2(q*0.1, r*0.1) for q in range(-50,51)] for r in range(-50,51)] # 批量注入 elevation 属性,自动跳过不存在的 hex grid.set_attribute( attr_name='elevation', values={(q, r): noise_map[r+50][q+50] for q in range(-50,51) for r in range(-50,51)}, missing_policy='skip' # 可选 'skip', 'error', 'default' )set_attribute()内部会:将(q,r)键转为Hex实例(自动处理坐标系转换)仅对网格中存在的六边形赋值支持missing_policy='default'并指定默认值,避免空值3.3 几何实例化:HexGeometry如何把(q,r)变成可渲染的 SVG pathHexGeometry是 hexgen 的几何引擎,它不存储坐标,只提供计算方法。当你调用hex.center(),它返回(x, y)浮点坐标;调用hex.corners(),它返回 6 个(x,y)元组构成的闭合多边形。这些计算基于两个参数:六边形大小(size)和方向(orientation)。# 初始化 HexGeometry,指定 size=20(单位像素),pointy_top=True(尖顶朝上) geom = HexGeometry(size=20, pointy_top=True) # 获取某个六边形的中心坐标(轴向坐标 q=2, r=-1) center_x, center_y = geom.center(Hex.from_axial(2, -1)) # 获取其六个顶点,按顺时针顺序 corners = geom.corners(Hex.from_axial(2, -1)) # 返回 [(x0,y0), (x1,y1), ..., (x5,y5), (x0,y0)] —— 末尾重复首点,形成闭合路径 # 导出为 SVG path 字符串(可直接插入 HTML) svg_path = geom.svg_path(Hex.from_axial(2, -1)) # "M x0,y0 L x1,y1 L x2,y2 L x3,y3 L x4,y4 L x5,y5 Z"pointy_top=True表示六边形尖角垂直向上,此时size是从中心到顶点的距离;若pointy_top=False(平顶),size是从中心到边中点的距离。二者不能混用——同一地图内所有六边形必须用相同HexGeometry实例计算,否则渲染时会出现缩放错位。注意:HexGeometry的size参数没有单位,它只是比例因子。实际渲染时,你需要根据目标画布尺寸做缩放。hexgen 不提供 canvas 绑定,这是刻意为之:它只负责「几何正确性」,把「像素对齐」、「抗锯齿」、「CSS 动画」留给上层框架。4. 避坑:五个让 hexgen 新手当场崩溃的边界问题hexgen 的文档很薄,但坑很密。以下是我在某跨平台系统集成 hexgen 时踩过的真坑,每一条都附带复现步骤、根本原因和一招解决。4.1 现象:HexGrid.from_radius(0)生成空网格,而非单个六边形原因:radius=0时,from_radius()的内部循环for r in range(-radius, radius+1)变成range(0,1),但后续对q的计算q_min = -radius + max(0, r)导致q_min = 0,q_max = radius - max(0, -r)导致q_max = 0,最终range(q_min, q_max+1)为range(0,1),看似合理。但 hexgen 的Hex.from_axial(0,0)构造后,s = -0-0 = 0,q+r+s=0成立,本应存在。问题出在from_radius()的边界判定逻辑漏掉了radius=0的特判。解决:永远用HexGrid.from_radius(1)代替0,或手动添加:grid = HexGrid(); grid.add_hex(Hex.from_axial(0,0))。4.2 现象:调用grid.to_svg()后,浏览器打开 SVG 显示为空白,但文件大小正常原因:to_svg()默认viewbox="0 0 1000 1000",但若你的地图极小(如只有 3 个六边形),HexGeometry.size=20时,实际坐标范围可能只有x∈[-30,30], y∈[-30,30],整个图形被压缩在左上角 60×60 区域,而 viewBox 是 1000×1000,肉眼不可见。解决:显式传入viewbox参数:grid.to_svg(viewbox=(-50,-50,100,100)),或先用grid.bounding_box(geom)获取实际范围再生成。4.3 现象:grid.filter_by_attribute('elevation', lambda x: x > 0.5)返回空列表,但print([h.elevation for h in grid.hexes])显示有正值原因:filter_by_attribute()默认只检查已设置该属性的六边形。若部分六边形未调用set_attribute('elevation', ...),则h.elevation访问会抛AttributeError,而 filter 内部捕获后跳过该 hex,不报错也不警告。解决:改用grid.filter_by_attribute('elevation', lambda x: x > 0.5, missing_policy='default', default_value=-999),或先确保所有 hex 都有该属性。4.4 现象:用Hex.from_odd_r(row=1, col=0)和Hex.from_odd_r(row=2, col=0)生成的两个六边形,distance_to()返回 2,但视觉上它们是上下紧邻原因:odd-r坐标系中,row=1是奇数行,col=0对应q=0;row=2是偶数行,col=0对应q=-1。所以两点轴向坐标是(0,1)和(-1,2),s值分别为-1和-1,距离max(|0-(-1)|, |1-2|, |-1-(-1)|) = max(1,1,0) = 1。但distance_to()返回 2,说明你用了even-r的转换函数。解决:确认输入数据的偏移类型。用Hex.from_odd_r()就必须配to_odd_r(),混用必错。4.5 现象:grid.to_geojson()生成的 GeoJSON 在 QGIS 中显示为一堆离散点,而非六边形面原因:to_geojson()默认geometry_type='Point',只导出中心坐标。若要面,必须显式设geometry_type='Polygon',且确保HexGeometry已初始化(否则corners()报错)。解决:grid.to_geojson(geometry_type='Polygon', geom=geom),其中geom是已配置好的HexGeometry实例。5. 进阶技巧:用 hexgen 实现「可验证」的六边形流域分割真正的六边形地图价值,不在画得好看,而在能否支撑空间分析。我曾在一个模拟项目 X 中,用 hexgen 实现了基于 D8 流向的六边形流域分割,并用三种方式交叉验证结果可靠性:几何闭合性验证、流量守恒验证、手工拓扑验证。下面拆解核心步骤。5.1 步骤一:生成带高程与流向的六边形网格首先,用 Perlin 噪声生成基础高程,再为每个六边形计算流向(指向 6 邻域中海拔最低者):import numpy as np # 生成 20x20 六边形网格 grid = HexGrid.from_bounds(-10, 10, -10, 10) # 注入高程(简化版,实际用更复杂地形模型) np.random.seed(42) elevations = np.random.normal(100, 20, size=(21,21)) grid.set_attribute('elevation', { (q, r): elevations[r+10, q+10] for q in range(-10,11) for r in range(-10,11) }) # 计算流向:存储邻居索引(0-5),0=右上,1=右,2=右下,3=左下,4=左,5=左上 flow_dirs = {} for hex in grid.hexes: neighbors = list(hex.neighbors()) if len(neighbors) < 6: # 边界 hex,流向设为 -1(无出口) flow_dirs[(hex.q, hex.r)] = -1 continue # 找海拔最低的邻居 neighbor_elevs = [n.elevation for n in neighbors] min_idx = np.argmin(neighbor_elevs) flow_dirs[(hex.q, hex.r)] = min_idx grid.set_attribute('flow_dir', flow_dirs)5.2 步骤二:实现 D8 流向追踪与流域标记六边形的 D8 等价于「六邻域流向」。我们用迭代法标记流域:从每个无上游的「源头 hex」开始,沿流向反向追踪,标记所有能到达它的 hex。def assign_watersheds(grid: HexGrid) -> Dict[Tuple[int,int], int]: # 初始化:每个 hex 流域 ID 为 -1(未分配) watershed_ids = {(h.q, h.r): -1 for h in grid.hexes} current_id = 0 # 找所有源头:没有 hex 流向它(即它的 flow_dir 不指向它) for hex in grid.hexes: is_source = True for neighbor in hex.neighbors(): if hasattr(neighbor, 'flow_dir') and neighbor.flow_dir != -1: # neighbor 的 flow_dir 指向的邻居,是否等于 hex? dirs = [(1,0), (1,-1), (0,-1), (-1,0), (-1,1), (0,1)] target_q = neighbor.q + dirs[neighbor.flow_dir][0] target_r = neighbor.r + dirs[neighbor.flow_dir][1] if target_q == hex.q and target_r == hex.r: is_source = False break if is_source: # 从源头开始 DFS 反向追踪 stack = [(hex.q, hex.r)] while stack: q, r = stack.pop() if watershed_ids[(q, r)] == -1: watershed_ids[(q, r)] = current_id # 找所有流向 (q,r) 的邻居 for n in Hex.from_axial(q, r).neighbors(): if hasattr(n, 'flow_dir') and n.flow_dir != -1: dirs = [(1,0), (1,-1), (0,-1), (-1,0), (-1,1), (0,1)] target_q = n.q + dirs[n.flow_dir][0] target_r = n.r + dirs[n.flow_dir][1] if target_q == q and target_r == r: stack.append((n.q, n.r)) current_id += 1 return watershed_ids watershed_map = assign_watersheds(grid) grid.set_attribute('watershed_id', watershed_map)5.3 步骤三:三重验证确保流域分割可信验证类型方法hexgen 中如何实现通过标准几何闭合性检查每个流域内所有六边形是否连通(无孤岛)grid.filter_by_attribute('watershed_id', lambda x: x==wid)得到子集,用nx.is_connected(nx.Graph(...))构建邻接图连通分量数 = 1流量守恒检查每个非源头 hex 的入流数 ≥ 1(除边界)统计每个 hex 被多少邻居的flow_dir指向入流数为 0 的非边界 hex 数 = 0手工拓扑选 3 个典型 hex,用纸笔画出流向链,对比代码输出print(f"Hex({q},{r}) -> {neighbors[flow_dir].q},{neighbors[flow_dir].r}")手工链与代码链完全一致我一般会把这三重验证写成grid.validate_watersheds()方法,每次生成新流域后强制运行。从那以后我每次做六边形空间分析,都强制走一遍这三步——不是信不过 hexgen,而是信不过自己手写的流向逻辑。希望帮到你。本文还有配套的精品资源,点击获取