news 2026/10/21 21:57:46

六边形网格生成器hexgen:地理空间建模与游戏AI的高效底座

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
六边形网格生成器hexgen:地理空间建模与游戏AI的高效底座

简介:hexgen 是一款基于 Python 开发的六角形世界地图生成器,面向游戏开发者、桌游(如 DnD)设计者及地理模拟爱好者,用于快速生成可定制的随机行星表面地图,并支持按需划分领土单元,解决手工绘制六边形网格地图效率低、参数不可控的问题。资源包共34个文件,含20个核心 Python 模块(如 mapgen.py、hex.py、heightmap.py、territory.py 等,覆盖地图生成、地形建模、河流模拟与气候逻辑)、3份 Markdown 文档(含 README、climate.md 等说明)、1个 JS 解析脚本及 Makefile、setup.py 等工程化支持文件,整体仅548KB,轻量易部署。已有1548人学习下载,适合中高级 Python 用户实践地理算法、理解六边形网格数据结构与程序化内容生成(PCG)技术。用户可直接运行示例、调试高度图与气候模块、复用 territory 划分逻辑,或基于清晰的模块化设计(如 geoform.py 抽象地貌形态、edge.py 处理邻接关系)进行二次开发。

1. 六边形地图不是“画着玩”的玩具:它让策略游戏AI训练快3倍、地理模拟误差降40%

你有没有试过在Unity里拖拽一个正方形网格做地形编辑器,结果发现单位移动路径总卡在斜角、视野计算像在解几何题?某高校地理信息实验室去年用传统方形格网建模城市热岛扩散,仿真跑完才发现——边缘区域的热传导被网格对齐强行“拉直”,和实测红外图对不上。而他们换上 hexgen 生成的六角形世界地图后,同一套扩散算法在保持计算量不变的前提下,空间插值误差直接压到 3.7%。这不是美术风格选择,是数学结构带来的物理合理性跃迁:每个六边形有6个等距邻接单元,无方向偏置,天然适配蜂窝通信建模、战术单位覆盖分析、甚至细胞组织生长模拟。hexgen 不是地图渲染器,它是把「空间关系」翻译成可计算对象的编译器——输入经纬度范围或自定义边界多边形,输出带拓扑关系的 GeoJSON + 可序列化的 Python 对象树,连海拔、生物群系、资源丰度这些属性字段都预留了 slot。适合三类人:正在写 Roguelike 游戏逻辑的独立开发者、需要轻量级地理空间原型的科研人员、以及被方形网格坑过至少两次的 GIS 工程师。


2. 技术栈与架构设计:为什么选 Python 而不是 Rust 或 WebAssembly

hexgen 的技术选型不是“因为 Python 简单”,而是被三个硬约束逼出来的:第一,必须无缝对接 GDAL/OGR 和 Shapely 这类地理空间计算库,它们的 Python 绑定成熟度远超其他语言;第二,用户要能直接在 Jupyter Notebook 里调试地图生成参数,比如实时拖动cell_size=500滑块看六边形密度变化;第三,最终产物要能喂给 PyTorch 训练空间感知模型,中间不能有二进制格式转换损耗。所以整个架构是三层洋葱模型:最外层是 CLI 和 Jupyter 插件接口(hexgen-cli),中间是核心生成引擎(hexgen.core),最内层是地理空间抽象层(hexgen.geo)。没有 Web 服务层,不依赖数据库——所有状态都在内存对象里流转,生成一张 10km×10km 范围、50m 分辨率的地图,全程在 320MB 内存里完成,耗时 1.8 秒(i7-11800H 实测)。

2.1 核心模块拆解:从经纬度到六边形对象的完整链路

整个生成流程分四步:投影变换 → 六边形铺排 → 边界裁剪 → 属性注入。关键不在算法多炫酷,而在每一步都保留可逆性。比如投影变换不用pyproj.transform直接转 WGS84 到 Web Mercator,而是先构建CRS对象并缓存Transformer实例:

# hexgen/geo/projection.py from pyproj import CRS, Transformer def get_transformer(src_crs: str, dst_crs: str) -> Transformer: """返回线程安全的复用型坐标转换器""" key = f"{src_crs}_{dst_crs}" if key not in _transformer_cache: _transformer_cache[key] = Transformer.from_crs( CRS.from_user_input(src_crs), CRS.from_user_input(dst_crs), always_xy=True # 强制 x,y 顺序,避免 lat/lon 混淆 ) return _transformer_cache[key]

提示:always_xy=True是血泪经验。某次用+proj=utm +zone=50时没加这句,生成的六边形全歪向东南——因为 UTM 默认按 lon/lat 输入,但 Shapely 多边形顶点是 x/y 顺序,错位导致整个网格旋转 90°。

2.2 六边形铺排算法:避开“中心点偏移”的玄学陷阱

六边形网格有两种主流铺法:轴向坐标(Axial)和偏移坐标(Offset)。hexgen 用的是优化过的双轴向坐标系统,核心是HexGrid类的generate_cells()方法:

# hexgen/core/grid.py class HexGrid: def generate_cells(self, bounds: Tuple[float, float, float, float]) -> List[HexCell]: min_x, min_y, max_x, max_y = bounds # 步骤1:计算六边形外接圆半径(决定单元大小) radius = self.cell_size / (2 * math.sin(math.pi / 6)) # cell_size 是边长 # 步骤2:确定起始偏移量,确保左下角必有单元中心 start_x = min_x + radius * math.cos(math.pi / 6) start_y = min_y + radius * 0.5 cells = [] # 步骤3:双层循环铺排(注意 y 方向步长是 radius * 1.5) for row in range(int((max_y - start_y) / (radius * 1.5)) + 1): y = start_y + row * radius * 1.5 # 奇偶行 x 偏移不同,实现蜂窝错位 x_offset = 0 if row % 2 == 0 else radius * math.cos(math.pi / 6) for col in range(int((max_x - start_x - x_offset) / (radius * math.cos(math.pi / 6) * 2)) + 1): x = start_x + x_offset + col * radius * math.cos(math.pi / 6) * 2 # 步骤4:生成 HexCell 对象,含 q,r 坐标和几何体 cell = HexCell(q=col, r=row, center=(x, y), radius=radius) if self._is_in_bounds(cell, bounds): cells.append(cell) return cells

这段代码的关键在于y步长用radius * 1.5而非radius * √3——后者是理论值,但浮点误差累积会导致第 100 行开始出现 0.3 像素偏移,最终让边界裁剪漏掉整行单元。1.5是经过 2000 次随机范围测试后找到的稳定值,牺牲 0.02% 理论精度换来 100% 可预测的铺排结果。

2.3 边界裁剪:用 Shapely 做布尔运算,但绝不调buffer(0)

很多项目用polygon.buffer(0)修复无效几何体,但在 hexgen 里这是禁用操作。原因:buffer(0)会重采样顶点,导致六边形与裁剪边界的交点位置漂移,后续做空间连接(如“哪些单元覆盖某条河流”)时匹配失败。hexgen 的裁剪逻辑是:

# hexgen/geo/cutter.py def clip_hexes(hexes: List[HexCell], boundary: Polygon) -> List[HexCell]: clipped = [] for cell in hexes: # 1. 先用 bounding box 快速排除(80% 单元在此步过滤) if not boundary.envelope.intersects(cell.polygon.envelope): continue # 2. 精确裁剪:只取 intersection,不 merge 不 buffer intersection = cell.polygon.intersection(boundary) if not intersection.is_empty and intersection.area > cell.polygon.area * 0.1: # 3. 保留原始 cell 对象,仅替换 geometry 字段 clipped_cell = replace(cell, polygon=intersection) clipped.append(clipped_cell) return clipped

area > cell.polygon.area * 0.1这个阈值是硬核设定:小于 10% 面积的碎片单元(如被山脉轮廓切出的细长三角)直接丢弃,避免后续分析中因微小面积引发数值不稳定。


3. 快速上手:从安装到生成第一张可交互地图

hexgen 不走 pip install 那套——因为地理空间库的二进制依赖太敏感。官方推荐用 conda 创建隔离环境,再通过源码安装确保版本锁死。

3.1 环境搭建:绕过 GDAL 编译地狱的三步法

某开发者曾在 Ubuntu 22.04 上为 GDAL 装依赖折腾 7 小时,最后发现只需一条命令:

# 创建专用环境(Python 3.9 是当前验证最稳的版本) conda create -n hexgen-env python=3.9 conda activate hexgen-env # 用 conda-forge 安装地理空间栈(自动解决 PROJ/GDAL 依赖链) conda install -c conda-forge gdal shapely pyproj fiona # 最后才用 pip 装 hexgen(避免 pip 强制重装 conda 已装的包) pip install git+https://github.com/xxx/hexgen.git@v0.4.2

注意:如果conda install报PackagesNotFoundError,说明 channel 未启用。运行conda config --add channels conda-forge && conda config --set channel_priority strict后重试。

3.2 生成标准六边形地图:CLI 命令逐参数解析

生成一张覆盖北京五环内的六边形地图,命令如下:

hexgen generate \ --bounds "116.1,39.7,116.6,40.0" \ --cell-size 500 \ --crs "EPSG:4326" \ --output-format geojson \ --output-path beijing_hex.geojson \ --with-elevation

参数含义:

  • --bounds:WGS84 经纬度范围,格式"min_lon,min_lat,max_lon,max_lat",不是屏幕坐标!输反会生成空地图。
  • --cell-size:六边形边长(单位:米),不是直径。设 500 表示每个单元覆盖约 0.11 km² 区域。
  • --crs:输入坐标的参考系,必须和--bounds单位一致。若用 UTM 坐标,此处填"EPSG:32650"。
  • --with-elevation:触发 SRTM 高程数据下载(需联网),生成的 GeoJSON 中每个feature.properties会多出"elevation_m": 45.2字段。

3.3 在 Jupyter 中动态调试:实时看参数如何影响网格形态

这是 hexgen 最被低估的能力。新建 notebook,运行:

# 加载核心模块 from hexgen.core.generator import HexGenerator from hexgen.geo.boundary import load_boundary_from_wkt # 定义北京五环多边形(简化版 WKT) beijing_ring = "POLYGON((116.1 39.7,116.6 39.7,116.6 40.0,116.1 40.0,116.1 39.7))" # 创建生成器(不立即执行) gen = HexGenerator( bounds=load_boundary_from_wkt(beijing_ring), cell_size=300, crs="EPSG:4326" ) # 查看预估单元数(不触发实际计算) print(f"预计生成 {gen.estimate_cell_count()} 个六边形单元") # 生成并可视化(需要 matplotlib) hexes = gen.generate() hexes.plot() # 自动调用 matplotlib,显示六边形网格叠加边界

gen.estimate_cell_count()的实现很巧妙:它用边界面积除以单个六边形面积再乘 1.2(预留 20% 边界损耗),10ms 内返回结果,比真跑一遍快 200 倍——这是为防止用户设cell_size=1导致内存爆掉的后悔药。


4. 常见问题排查:那些让你怀疑人生却只需改一行代码的坑

hexgen 的报错信息都带上下文定位,但有些现象表面看是 bug,实则是空间概念误用。以下是真实踩坑记录,按发生频率排序:

4.1 现象:生成的 GeoJSON 在 QGIS 里显示为空白图层

原因:--bounds输入了屏幕像素坐标(如"0,0,1000,800")而非经纬度。QGIS 加载时因坐标系不匹配拒绝渲染。
解决:用gdalinfo -so beijing_hex.geojson检查 CRS。若显示UNKNOWN,说明输入--crs错误或--bounds格式非法。正确做法是先用ogr2ogr -f CSV -lco GEOMETRY=AS_XY input.shp /vsistdout/ | head -5查原始数据坐标范围。

4.2 现象:--with-elevation报错HTTPError: 404 Client Error

原因:SRTM 数据服务器只提供 1°×1° 瓦片,若--bounds跨越多个整数经纬度(如"115.9,39.6,116.7,40.1"),hexgen 会尝试下载 4 个瓦片,其中部分可能不存在。
解决:缩小--bounds到单个整数度范围内,或手动下载 SRTM 数据放本地:wget https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL1.003/2000.02.11/SRTMGL1_S39E116.SRTMGL1.hgt.zip,解压后用--elevation-path /path/to/SRTMGL1_S39E116.hgt指定。

4.3 现象:生成的地图在 Leaflet 中缩放时六边形“抖动”

原因:Leaflet 默认用 Web Mercator 投影(EPSG:3857),但 hexgen 输出的是 WGS84(EPSG:4326)坐标。浏览器渲染时对经纬度做球面投影,导致小尺寸六边形顶点计算失真。
解决:生成时强制输出 Web Mercator 坐标:hexgen generate --bounds "116.1,39.7,116.6,40.0" --crs "EPSG:3857" --output-crs "EPSG:3857"。注意--bounds值需先用pyproj转成米制坐标,否则会错位。

4.4 现象:hexes.plot()报AttributeError: 'NoneType' object has no attribute 'plot'

原因:matplotlib 后端未设置。在 Linux 服务器或无 GUI 环境中,默认 backend 为Agg,不支持plt.show(),但 hexgen 的plot()方法内部仍尝试调用。
解决:在脚本开头加两行:

import matplotlib matplotlib.use('Agg') # 强制用非交互后端 import matplotlib.pyplot as plt

或者生成 PNG 而非弹窗:hexes.plot(save_path="beijing.png")。

4.5 现象:用--cell-size 1000生成的地图,feature.properties.area字段全是 0

原因:area字段默认不计算,需显式开启。CLI 没暴露该开关,但 Python API 支持:
解决:在生成后手动计算:

for hex_cell in hexes: hex_cell.polygon = hex_cell.polygon.buffer(0) # 先修复几何有效性 hex_cell.properties["area_m2"] = hex_cell.polygon.area

5. 二次开发实战:给六边形注入业务逻辑的三种可靠姿势

hexgen 的设计哲学是「生成即完成,不碰业务」。它输出的HexCell对象是纯数据容器,所有业务规则必须由你注入。我经历过三次大改版:第一次给游戏加资源点,第二次做疫情传播模拟,第三次接入 IoT 传感器网络。每次都在HexCell上挂新字段,但方法越来越稳——现在总结出三种生产环境验证过的模式。

5.1 模式一:属性扩展(适合静态标签,如生物群系、土壤类型)

这是最轻量的方式。hexgen 的HexCell使用dataclasses,支持运行时添加字段:

from dataclasses import fields from hexgen.core.cell import HexCell # 定义新属性(必须是 typing.Optional,否则序列化失败) HexCell.biome = None HexCell.soil_type = None # 批量注入 for cell in hexes: # 根据经纬度查气候数据库(示例用伪代码) cell.biome = climate_db.query(cell.center[0], cell.center[1]).biome_name cell.soil_type = soil_map.get_soil_at(cell.center) # 序列化时自动包含新字段 import json with open("beijing_enhanced.geojson", "w") as f: json.dump([cell.to_dict() for cell in hexes], f)

关键点:to_dict()方法会遍历fields(HexCell)并递归序列化,所以新字段必须是 JSON 可序列化类型(str/int/float/list/dict)。别用datetime或自定义类,否则json.dump报错。

5.2 模式二:行为绑定(适合动态计算,如单位移动成本、视野遮挡)

当属性依赖实时状态(如“当前是否有雾”),用@property绑定方法更安全:

class SmartHexCell(HexCell): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._weather_state = "clear" # 可被外部更新 @property def movement_cost(self) -> float: """根据天气和地形动态计算移动代价""" base = 1.0 if self._weather_state == "fog": base *= 1.8 if self.biome == "swamp": base *= 2.5 return round(base, 2) def update_weather(self, new_state: str): self._weather_state = new_state # 替换原对象 enhanced_hexes = [SmartHexCell(**cell.to_dict()) for cell in hexes]

提示:不要重写__init__传参逻辑。to_dict()返回的是字典,SmartHexCell(**dict)能完美继承所有原始字段,包括q,r,center等。

5.3 模式三:空间索引加速(适合高频查询,如“找最近3个医疗点”)

当 hexgen 地图变大(>10 万单元),用for循环遍历找邻近单元会卡死。必须上 R-tree:

from rtree import index import numpy as np # 构建空间索引(一次初始化,永久复用) idx = index.Index() for pos, cell in enumerate(hexes): # R-tree 要求 (minx, miny, maxx, maxy) 边界框 bounds = cell.polygon.bounds # 返回 (minx, miny, maxx, maxy) idx.insert(pos, bounds) # 查询:找中心点 (116.3,39.85) 周围 5km 内的所有单元 query_point = (116.3, 39.85) # 先粗筛:用点缓冲区快速获取候选 candidate_ids = list(idx.intersection( (query_point[0]-0.05, query_point[1]-0.05, query_point[0]+0.05, query_point[1]+0.05) )) # 再精筛:计算球面距离(用 haversine) def haversine_dist(lon1, lat1, lon2, lat2): # ... 标准 haversine 实现 pass nearby = [] for cid in candidate_ids: dist = haversine_dist(query_point[0], query_point[1], hexes[cid].center[0], hexes[cid].center[1]) if dist <= 5000: # 5km nearby.append((dist, hexes[cid])) # 按距离排序,取前3 nearby.sort(key=lambda x: x[0]) top3 = [cell for dist, cell in nearby[:3]]

这个模式的核心是「粗筛+精筛」两阶段。R-tree 的intersection在 10 万单元下只要 0.02 秒,而纯for循环要 1.7 秒——差 85 倍。

从那以后我每次接到空间查询需求,都强制走一遍 R-tree 初始化流程,哪怕只有 100 个单元也照做。因为当业务方说“再加 10 倍数据量”时,你不会想重写整个查询模块。希望帮到你。

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

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

临时文件自动化清理工具:从扫描策略到误删事故复盘

今天聊一个我自己都在用的工具&#xff1a;临时文件自动化工具。 说个典型场景。某天我登上一台个人开发机&#xff0c;顺手看了一眼磁盘占用&#xff0c; /tmp 已经占了 31G&#xff0c;用户缓存目录也有十几 G。第一反应当然是手动清理&#xff0c;但删了不到十分钟我就意…

作者头像 李华
网站建设 2026/10/21 21:52:50

自动化与脚本实战:从重复劳动到高效运维的必经之路

从“自动化与脚本”说起&#xff1a;把重复劳动交给机器&#xff0c;是技术人最划算的投资 做技术这些年&#xff0c;我见过太多人把时间耗在“明明能一键完成&#xff0c;却非要手动折腾”的事情上。批量改文件名、每天刷新报表、定时备份数据、跨系统同步信息……这些活儿单看…

作者头像 李华
网站建设 2026/10/21 21:52:25

知识工作浏览器插件集:从采集到输出的低摩擦工作流

先给结论&#xff1a;知识工作的效率瓶颈&#xff0c;往往不在打字速度&#xff0c;而在信息从“看到”变成“可用”的那段链条。我手上的这套knowledge-work-plugins&#xff0c;是一组围绕知识工作者日常场景的浏览器扩展集合&#xff1a;网页里读到一段值得留存的内容&#…

作者头像 李华
网站建设 2026/10/21 21:52:01

模板匹配实现银行卡号识别:从原理到OpenCV落地实践

最近在复盘手头这个模拟项目X&#xff1a;从一张银行卡照片里自动把卡号读出来。技术选型的时候&#xff0c;我第一个想到的竟然不是深度学习方案&#xff0c;而是计算机视觉里的老牌方法——模板匹配。模板匹配做银行卡号识别&#xff0c;听起来确实有点复古&#xff0c;但真正…

作者头像 李华
网站建设 2026/10/21 21:51:22

AIGC十年演进复盘:从玩具到生产工具的技术逻辑与落地经验

2015年我刚开始接触AI生成内容的时候&#xff0c;圈子里最流行的玩法还是让模型续写小说。续出来的东西大多数时候像梦话&#xff0c;偶尔冒出一两句惊为天人&#xff0c;但没人真敢把它当生产力用。十年过去&#xff0c;AIGC这个词已经从技术圈的术语变成了几乎人人都聊过的话…

作者头像 李华
网站建设 2026/10/21 21:49:44

CPU大小核调度错配怎么办?强制锁大核提升性能的实操指南

你有没有遇到过一种很奇怪的情况&#xff1a;电脑配置明明不低&#xff0c;CPU核心也不少&#xff0c;但跑某款游戏或者编译工程的时候&#xff0c;就是卡得让人血压飙升。打开任务管理器一看&#xff0c;GPU占用率很低&#xff0c;CPU总占用率也不算高&#xff0c;再仔细看看每…

作者头像 李华