news 2026/9/3 3:54:19

Unity+Blender程序化生成六边形星球世界引擎详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity+Blender程序化生成六边形星球世界引擎详解

之前在做类似《文明》风格的六边形地块世界时,最痛苦的不是写单位移动逻辑,而是“地图怎么来、地块怎么生成、改动一块地形后要不要回到建模软件里返工”。后来把整个流程切换到 Unity 程序化生成 + Blender 辅助资产之后,开发节奏明显顺了很多。这篇文章会把 Unity + Blender 打造程序化星球生成器、从零搭建六边形世界引擎的核心思路、坐标算法、Mesh 生成、噪声地形、常见坑点完整拆出来。新手可以按顺序理解整套体系,有基础的开发者可以直接跳去第 5、6 节抄代码和配置。

1. 为什么需要六边形世界引擎?

1.1 从《文明》看六边形地图设计

《文明》系列从第 5 代开始全面转向六边形地块。相比传统方格,六边形一格与周围六格的距离更均匀,移动距离不会因为“走斜线”出现问题。四方向的方格地图里,单位从中心格走到相邻格和对角格,真实距离不一致;八方向网格则让相邻关系变得太宽松,很多时候玩家很难判断边境、控制和城市辐射范围。六边形天然把复杂的地块邻接问题简化成“6 个等距邻居”,这对寻路、范围判断、区域划分和战争迷雾都更加友好。

放到星球生成器里,六边形还有另一个好处:每个地块是同一块 Tile Mesh,内存和绘制上都能复用。只要你有一套轴向坐标转换、一套寻路规则、一套地块数据表,就可以在任何球面或平面表现层上换皮。

1.2 程序化星球生成器的模块拆分

很多初学者一想到“星球生成器”,就以为只是跑一个噪声算法生成山体高度,再塞进 Unity 的 Terrain 系统。但要做到类《文明》那种可以承载国家、城市、单位、资源的地图,其实需要拆成多个相互独立的子模块:

  1. 地块数据层:保存每个格子的轴向坐标、地形类型、海拔、湿度、资源标记。
  2. 六边形算法层:负责坐标换算、邻居访问、距离计算、寻路。
  3. 表现层:负责把地块数据变成可见的地形,例如六棱柱 Tile、Mesh、水面、草地、山脉。
  4. 网络与区块管理:当格子数量很多时,要按区块分页生成、卸载、序列化。
  5. 程序化噪声系统:用 PerlinNoise、SimplexNoise 等控制高度、温度、湿度。
  6. 美术资产层:Blender 或其它 DCC 软件用来制作特殊地貌、建筑、植被和细节模型。

这篇文章的主要目标,是要把前四块打通,让你能在 Unity 工程里看到一个由六边形地块构成的世界原型。

2. 环境准备与版本说明

2.1 Unity 与 Blender 版本配置

本文示例以 Unity 2021.3 LTS 或 Unity 2022.3 LTS 为参考环境,使用内置渲染管线最容易跑通。如果你用的是 URP,代码中的 Mesh 生成逻辑不需要改,只有着色器设置需要额外检查。Blender 这边以 3.x 及以上版本为例,Blender 2.8 以上也可以,核心操作基本一致。

版本不要盲目追新,尤其是 Unity 长期支持版本(LTS)比年度版稳定。程序化生成项目通常要写大量 Editor 调试代码,也会频繁 Build,用 LTS 能少踩很多内部 API 变动带来的坑。

2.2 项目目录结构

为了保证后续扩展,建议在一开始就用清晰的目录。新建 Unity 工程后,在 Assets 下创建这些文件夹:

Assets/ ├─ Scenes/ ├─ Scripts/ │ ├─ Core/ │ ├─ Hex/ │ ├─ Generation/ │ └─ Editor/ ├─ Art/ │ ├─ Models/ │ ├─ Materials/ │ └─ Textures/ ├─ Data/ │ └─ TileConfigs/ └─ Prefabs/

不要把脚本全部丢在根目录。等程序化逻辑逐渐增多,你会发现“核心算法、Unity 生命周期、编辑器工具、数据配置”必须分开,否则排查问题会非常痛苦。

2.3 开发路线建议

程序化星球生成器的复杂度非常高,不建议直接从“完美球面六边形铺满”开始。比较好的路径是:

  1. 先实现平面六边形地图原型,验证地块数据和游戏逻辑。
  2. 再实现 Blender 基础 Tile 资产,导入 Unity 确认单位与缩放。
  3. 然后实现程序化 Mesh 生成和噪声地形。
  4. 最后才处理球面映射、多面体投影、区块加载和性能优化。

3. 六边形网格:坐标系统与基础算法

六边形地图最容易劝退新手的点就是坐标。因为你不能像方格地图一样直接使用(x, y)行列索引。同一个视觉位置,在不同的坐标体系里差异很大。

3.1 三种常见坐标表示

六边形坐标系主要有三种表示法。

  • Offset 坐标:仍然使用行列(col, row),但每隔一行让格子错位半个宽度。优点是接近传统二维数组,缺点是相邻关系需要判断奇偶行,算法并不漂亮。
  • Axial 坐标:使用(q, r),q 代表列方向,r 代表行方向,去掉了奇偶判断,是工程中最推荐的数据层坐标。
  • Cube 坐标:使用三维坐标(x, y, z),满足x + y + z = 0。这个坐标本身是对称的,距离、旋转、邻居算法都极其优雅。

我建议数据层统一使用轴向坐标(q, r),只在计算距离和寻路时临时转成立方体坐标。

3.2 轴向坐标与邻居计算

先准备一个 HexMath 静态工具类。下面这段代码可以作为整个六边形世界引擎的“地基”。

// 文件路径: Assets/Scripts/Hex/HexMath.cs using UnityEngine; public static class HexMath { // 轴向坐标 (q, r) 的六个邻居方向 // 这里按照 pointy-top 布局给出 public static readonly Vector2Int[] AxialDirections = { new Vector2Int(1, 0), new Vector2Int(1, -1), new Vector2Int(0, -1), new Vector2Int(-1, 0), new Vector2Int(-1, 1), new Vector2Int(0, 1) }; public static Vector2Int GetNeighbor(Vector2Int axial, int direction) { int dir = ((direction % 6) + 6) % 6; return axial + AxialDirections[dir]; } public static Vector2Int[] GetNeighbors(Vector2Int axial) { Vector2Int[] neighbors = new Vector2Int[6]; for (int i = 0; i < 6; i++) { neighbors[i] = GetNeighbor(axial, i); } return neighbors; } public static Vector3Int AxialToCube(Vector2Int axial) { int x = axial.x; int z = axial.y; int y = -x - z; return new Vector3Int(x, y, z); } public static int Distance(Vector2Int a, Vector2Int b) { Vector3Int ac = AxialToCube(a); Vector3Int bc = AxialToCube(b); return (Mathf.Abs(ac.x - bc.x) + Mathf.Abs(ac.y - bc.y) + Mathf.Abs(ac.z - bc.z)) / 2; } public static Vector3 AxialToWorld(Vector2Int axial, float tileSize) { float sqrt3 = Mathf.Sqrt(3f); float x = sqrt3 * (axial.x + axial.y * 0.5f) * tileSize; float z = 1.5f * axial.y * tileSize; return new Vector3(x, 0f, z); } }

这里为什么要用AxialToCube?因为在轴向坐标系里,两点距离公式比较绕,用立方体坐标后,距离就是三个坐标差值绝对值的最大值,也等于差值绝对值之和的一半。代码里用的正是立方体坐标的切比雪夫距离变体。

你可能会疑惑方向常量为什么是这个顺序。pointy-top 六边形的一圈邻居,按方向依次是右、右下、左下、左、左上、右上。这个顺序不强求,只要生成邻居时保持一致即可。关键是后续寻路、范围判断都复用同一套方向,别在某个函数里手写另一套易错的偏移量。

3.3 把距离、包围圈和寻路准备好

有了Distance函数,就可以快速实现“单位在半径 N 内能到达哪些格子”的逻辑:

public static List<Vector2Int> GetCellsInRange(Vector2Int center, int range) { List<Vector2Int> results = new List<Vector2Int>(); for (int q = -range; q <= range; q++) { int rMin = Mathf.Max(-range, -q - range); int rMax = Mathf.Min(range, -q + range); for (int r = rMin; r <= rMax; r++) { results.Add(new Vector2Int(center.x + q, center.y + r)); } } return results; }

qr的双层循环是基于立方体坐标推导出来的。你可以暂时不理解数学细节,但需要记住:只要转换成 Cube 坐标后,限制条件是x、y、z的绝对值都小于等于 range。这个实现能保证你得到一个漂亮的六边形范围,而不是一个矩形。

4. 在 Blender 中准备六边形资源

4.1 为什么不是全部在 Unity 中手写?

有人会觉得“都程序化生成了,为什么还需要 Blender?”真正做项目时,Unity 用代码生成 Tile 和地形基础还行,但山脉、废墟、植被、建筑这类复杂度较高的装饰性资产,用 Blender 建模再导入效率高得多。

程序化星球的正确做法通常是混合使用:基础地块网格由 Unity 代码重复生成,特殊地貌、建筑、植物由美术在 Blender 里制作成 Prefab,再按地块数据随机摆放。这样既保证了地图规模,又不牺牲细节表现。

4.2 快速制作六棱柱 Tile

打开 Blender 后,先删除默认的 Cube,按快捷键Shift + A添加一个圆柱体,把它命名为Hex_Tile。在添加圆柱体的面板里,把 Vertices 从默认的 32 改成 6,Radius 可以根据需要填 1,Depth 先填 0.2。

这样得到的六棱柱,非常适合当作最基础的六边形地块。如果你想制作山丘、丘陵等不同高度地形,可以进入编辑模式,按Tab,选择顶面后使用E键挤出,通过上下移动顶点做出起伏。

不过要注意,高度不同的地块如果直接做成不同模型,美术资产量会很大。更推荐的方式是在 Blender 中只制作“六棱柱地块基础底”,高度、颜色、纹理交给 Unity 程序化处理。这样可以用一个 Tile Mesh 实例化几百个地块,再通过缩放或顶点高度生成不同地形效果。

4.3 导出与单位问题

Blender 导出 FBX 时,经常出现“导入到 Unity 后模型变得很大或很小”的现象。这是因为 Blender 与 FBX 的单位换算规则比较特殊。

解决方法如下:

  1. 在 Blender 中选中模型,按N打开右侧边栏,确认“场景单位”中长度单位是米,缩放为 1。
  2. 导出时选择 FBX 格式。
  3. 在 Unity 中选中导入的模型,检查Scale Factor。如果模型边长应该是 1 米,但导入后不是 1,就把 Scale Factor 调成对应比例。

如果你的六边形 Tile 半径本来就是 1,建议在 Blender 中建模时也按真实比例做,不要随便缩小成 0.01。否则 Unity 光照、物理碰撞和程序化寻路都会出现单位不一致的问题。

5. Unity 程序化生成:从 Mesh 到星球

5.1 Unity Mesh 概念简述

在 Unity 中,一个可见模型本质上是 MeshFilter 加 MeshRenderer。MeshFilter 保存网格数据,MeshRenderer 负责绘制。

网格数据由顶点数组、三角形索引数组、UV 数组等组成。顶点数组是空间中的点列表;三角形索引告诉引擎每三个顶点连成一个三角形面;UV 用于采样贴图。要程序化生成一个六边形 Tile,本质上就是自己填充顶点数组和三角形索引数组。

5.2 球面网格方案:Cube Sphere 还是 Icosahedron?

真正要做星球表面无缝六边形网格时,绝大多数方案不会直接在“经纬度球面”上平铺六边形。经纬线在极点会汇聚,导致高纬度的六边形形状被压扁,甚至无法闭合。

业界常用的两个方向:

  • Cube Sphere:把立方体六个面细分,再把每个顶点方向归一化到球面上。这种做法没有极点奇点,纹理和 Tile 逻辑也容易在六个面内展开,但跨面接缝处需要额外处理。
  • Icosahedron 细分:从正二十面体开始,不断细分三角形,再对偶成六边形网格。这种方案更接近真正无缝的球面六边形,像 H3 这样离散全球网格系统正是基于类似的几何理论。

工程上,如果你只做演示或课程原型,使用 Cube Sphere 投影就能得到很不错的星球轮廓。如果你希望达到类似商业策略游戏里“地表无遮挡、格子天然跨边界连通”的效果,则要认真学习二十面体网格生成和对偶算法。

5.3 在 C# 里生成六边形 Mesh

这里先实现一个程序化生成六棱柱 Mesh 的函数。即使最后你要生成球面地块,这个基础 Mesh 也会反复使用。

// 文件路径: Assets/Scripts/Generation/HexMeshBuilder.cs using UnityEngine; public static class HexMeshBuilder { public static Mesh CreateHexPrism(float radius, float thickness) { Mesh mesh = new Mesh(); Vector3[] vertices = new Vector3[14]; Vector2[] uv = new Vector2[14]; // 生成 6 个下半圆环顶点和 6 个上半圆环顶点 for (int i = 0; i < 6; i++) { float angle = Mathf.Deg2Rad * (60f * i + 30f); float x = Mathf.Cos(angle) * radius; float z = Mathf.Sin(angle) * radius; vertices[i] = new Vector3(x, -thickness * 0.5f, z); vertices[i + 6] = new Vector3(x, thickness * 0.5f, z); uv[i] = new Vector2(x / (radius * 2f) + 0.5f, z / (radius * 2f) + 0.5f); uv[i + 6] = uv[i]; } // 底部中心点和顶部中心点 vertices[12] = new Vector3(0f, -thickness * 0.5f, 0f); vertices[13] = new Vector3(0f, thickness * 0.5f, 0f); uv[12] = new Vector2(0.5f, 0.5f); uv[13] = new Vector2(0.5f, 0.5f); int[] triangles = new int[72]; int tri = 0; // 侧面 for (int i = 0; i < 6; i++) { int next = (i + 1) % 6; triangles[tri++] = i; triangles[tri++] = i + 6; triangles[tri++] = next + 6; triangles[tri++] = i; triangles[tri++] = next + 6; triangles[tri++] = next; } // 顶面 for (int i = 0; i < 6; i++) { int next = (i + 1) % 6; triangles[tri++] = 13; triangles[tri++] = i + 6; triangles[tri++] = next + 6; } // 底面 for (int i = 0; i < 6; i++) { int next = (i + 1) % 6; triangles[tri++] = i; triangles[tri++] = 12; triangles[tri++] = next; } mesh.vertices = vertices; mesh.triangles = triangles; mesh.uv = uv; mesh.RecalculateNormals(); mesh.RecalculateBounds(); return mesh; } }

这段代码的核心逻辑是:先准备好 14 个顶点,包括上下各 6 个六边形顶点和上下两个中心点;然后分别生成侧面、顶面、底面的三角形。创建完 Mesh 后,必须调用RecalculateNormals()重新计算法线,否则模型在光照下可能完全是暗的。

如果你只需要地块底盘,可以省略顶面和底面,只生成侧面。但作为策略游戏地块,一个带厚度的六棱柱观感更接近现实中的“格子”。

5.4 噪声驱动海拔:PerlinNoise

地形不能是平的,否则星球就是一颗光滑的台球。Unity 内置了Mathf.PerlinNoise,可以直接用来生成高度图,它是经典的分形噪声基础。

PerlinNoise 接收两个输入值并返回 0 到 1 之间的结果。为了让地形有自然感,不能只采样一次,而要叠加多个不同频率和振幅的噪声,这就是“分形布朗运动”思路。下面的代码可以在运行时给一个地块赋予高度:

public static float SampleHeight(int q, int r, float noiseScale, float amplitude) { float n1 = Mathf.PerlinNoise(q * noiseScale, r * noiseScale); float n2 = Mathf.PerlinNoise(q * noiseScale * 2.1f + 17f, r * noiseScale * 2.1f + 3f); float finalNoise = (n1 + n2 * 0.5f) / 1.5f; return
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 3:53:40

iPhone 7 Plus零循环高容量电池更换指南:从检测到安装全流程

在手机维修的实际项目中&#xff0c;iPhone 7 Plus 电池老化导致续航严重下降是最常见的维修需求之一。很多用户发现手机用了一两年后&#xff0c;原本一天一充变成半天就没电&#xff0c;甚至出现自动关机、电量跳变等问题。这些问题背后通常是电池循环次数过多、容量衰减或电…

作者头像 李华
网站建设 2026/9/3 3:53:28

WSL完全指南:Windows上运行Linux的轻量方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 3:49:02

Linux下Python调用SAP RFC:pyrfc与NW RFC SDK实战指南

简介&#xff1a;Linux环境下&#xff0c;用Python连接SAP系统&#xff0c;常需借助pyrfc与nwrfcsdk。该组件包正面向此类集成开发场景&#xff0c;为需要安装配置SAP NW RFC SDK、并通过Python调用RFC功能的工程师提供了一整套可用文件。压缩包共含27个文件&#xff0c;以头文…

作者头像 李华
网站建设 2026/9/3 3:48:56

AWS GovCloud集成OpenAI等大模型:政府合规云上调用AI的实践指南

这次我们不聊开源模型本地部署&#xff0c;聊一个更“重”的话题&#xff1a;AWS GovCloud 正在把 OpenAI、Meta、Anthropic 这些主流大模型引入政府与公共部门的合规云环境。如果你正在给政务项目、受监管行业或者对外合规要求很高的客户做 AI 技术方案&#xff0c;可以先把这…

作者头像 李华
网站建设 2026/9/3 3:48:10

动漫剧集本地化:从音轨分离到批量压制的工作流搭建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 3:47:51

图像传感器演进:从感光芯片到实体AI的感知底座

过去几年&#xff0c;智能设备的核心竞争点一直在屏幕、算力和通信能力之间轮换&#xff0c;但有一类元器件始终处在幕后&#xff0c;却决定着拍照画质、机器视觉精度和自动驾驶感知上限&#xff0c;它就是图像传感器&#xff0c;也就是我们常说的感光芯片。最近&#xff0c;索…

作者头像 李华