告别规整格子:用Townscaper的算法思路,为你的独立游戏打造独特有机地形
告别规整格子:用Townscaper的算法思路为独立游戏打造有机地形
在独立游戏开发中,地形生成往往是决定游戏视觉风格的关键因素之一。传统方格或六边形网格虽然易于实现,却难以摆脱机械感,而完全随机的噪声生成又容易失去设计控制。Townscaper通过其标志性的不规则四边形网格,在秩序与随机之间找到了完美平衡——这正是许多独立开发者梦寐以求的"有机感"。
1. 理解Townscaper网格的核心优势
Townscaper的网格之所以令人着迷,在于它同时具备三个特性:视觉不规则性保证自然感,拓扑一致性维持功能可用性,美学可控性确保整体协调。这种"有机网格"特别适合以下场景:
- 岛屿或陆地生成(冒险/生存游戏)
- 村落基底布局(模拟建造/RPG)
- 策略游戏地图(取代传统六边形格)
- 地下城房间连接(roguelike游戏)
与传统方案对比:
| 生成方式 | 优点 | 缺点 |
|---|---|---|
| 规则方格 | 实现简单,寻路高效 | 机械感强,缺乏自然度 |
| 六边形网格 | 移动方向多,视觉改善 | 仍显规整,变化有限 |
| 纯噪声生成 | 高度自然 | 难以控制,拓扑不稳定 |
| Townscaper风格 | 自然感+可控性兼备 | 实现复杂度中等 |
实际项目中,我们曾用Perlin噪声生成岛屿,结果玩家反馈"像融化的塑料"。改用有机网格后,同样的游戏机制获得了"手绘地图般生动"的评价。
2. 算法核心四步实践指南
2.1 基础三角剖分:从六边形开始
Townscaper采用改良的Delaunay三角剖分作为起点。在Unity中实现时,我们推荐从六边形网格出发:
// 六边形网格生成核心代码 void GenerateHexGrid(int size) { float spacing = 1.0f; for (int q = -size; q <= size; q++) { int r1 = Mathf.Max(-size, -q - size); int r2 = Mathf.Min(size, -q + size); for (int r = r1; r <= r2; r++) { Vector3 pos = new Vector3( spacing * (Mathf.Sqrt(3) * q + Mathf.Sqrt(3)/2 * r), spacing * (3f/2 * r), 0 ); CreateCell(pos); } } }这种结构相比随机点生成的三角剖分具有两个优势:
- 密度均匀,避免出现过于狭长的三角形
- 边界形状自然适合地图生成
2.2 智能边剔除:创造有机变异
随机剔除三角形边时,需要加入智能约束以避免不良形态:
bool ShouldMergeTriangles(Triangle t1, Triangle t2) { // 约束1:合并后内角不能小于30度 if(CalculateMinAngle(t1,t2) < 30f) return false; // 约束2:长边优先合并 float edgeLength = GetSharedEdgeLength(t1,t2); if(edgeLength < averageEdgeLength * 0.7f) return false; // 约束3:避免创建凹四边形 if(WouldCreateConcaveQuad(t1,t2)) return false; return true; }实践中建议:
- 使用加权随机而非纯随机选择
- 保留5-15%的三角形不合并以增加变化
- 边界处理要特殊对待以保持闭合性
2.3 细分策略:统一为四边形
细分阶段需要特别注意顶点索引管理。我们采用中点细分法:
void SubdivideQuad(Quad q) { // 计算各边中点 int mAB = GetOrCreateMidpoint(q.a, q.b); int mBC = GetOrCreateMidpoint(q.b, q.c); int mCD = GetOrCreateMidpoint(q.c, q.d); int mDA = GetOrCreateMidpoint(q.d, q.a); // 中心点 int center = GetOrCreateCenterPoint(q); // 生成4个子四边形 AddSubQuad(q.a, mAB, center, mDA); AddSubQuad(mAB, q.b, mBC, center); AddSubQuad(center, mBC, q.c, mCD); AddSubQuad(mDA, center, mCD, q.d); }细分层级建议不超过3级,否则会导致性能问题。对于策略游戏,1级细分通常足够;建造类游戏可能需要2级。
2.4 松弛优化:美学与功能的平衡
松弛(Relaxation)是最具艺术性的步骤。我们改进的版本包含:
void RelaxVertices(int iterations) { for(int i=0; i<iterations; i++) { foreach(var vertex in vertices) { if(vertex.isBoundary) continue; // 加权平均:考虑边长差异 Vector2 newPos = CalculateWeightedAverage(vertex); // 渐进移动避免震荡 vertex.position = Vector2.Lerp( vertex.position, newPos, 0.3f // 松弛系数 ); } } }优化技巧:
- 边界顶点固定或轻微移动
- 对建筑锚点施加额外约束
- 在Unity协程中分帧执行避免卡顿
3. 与游戏系统的深度整合
3.1 建筑摆放解决方案
有机网格对建筑摆放提出新挑战。我们开发了"自适应地基"系统:
- 在四边形内生成凸包碰撞体
- 根据玩家拖拽位置自动选择最佳拟合四边形组
- 动态调整建筑基底多边形
public List<Quad> FindBestFitQuads(Vector2 position, float radius) { var candidates = quads.Where(q => PointToQuadDistance(q, position) < radius ).OrderBy(q => QuadIrregularityScore(q) // 优先选择更规则的四边形 ).Take(5); return GreedyMerge(candidates); // 尝试合并相邻四边形 }3.2 寻路系统适配
将有机网格转换为导航网格的要点:
- 将四边形分解为三角形
- 计算每个三角形的移动成本权重:
float GetPathWeight(Quad q) { float regularity = 1 - (maxAngle - minAngle) / 180f; float sizeFactor = Mathf.Clamp(q.area / avgArea, 0.5f, 2f); return regularity * sizeFactor; } - 使用A*算法时,将顶点作为路径点
3.3 动态编辑与保存
实现玩家地形编辑的关键数据结构:
[System.Serializable] public class OrganicGrid { public List<Vector2> vertices; public List<Quad> quads; public byte[] adjacencyMap; // 用于快速查找邻接关系 public void AddVertex(Vector2 pos) { // 更新时需要同步维护adjacencyMap } public void RemoveQuad(Quad q) { // 处理拓扑变化 } }在测试中,使用JobSystem并行处理网格更新,使万级顶点网格的编辑帧率保持在60FPS以上。
4. 性能优化实战策略
4.1 层级细节管理
采用动态LOD系统:
| LOD级别 | 细分次数 | 适用场景 |
|---|---|---|
| 0 | 0 | 远距离视角/大地图 |
| 1 | 1 | 常规游戏视角 |
| 2 | 2 | 建造模式/特写镜头 |
实现代码框架:
void UpdateLOD() { foreach(var chunk in gridChunks) { float dist = Vector3.Distance( player.position, chunk.bounds.center ); int targetLOD = Mathf.FloorToInt(dist / lodDistance); chunk.SetLOD(targetLOD); } }4.2 内存优化技巧
- 顶点数据压缩:
struct CompressedVertex { ushort x; // 16位精度足够 ushort y; byte neighborCount; byte neighborStartIndex; } - 使用ECS架构处理静态网格
- 四边形索引重用共享边
4.3 批处理与渲染优化
在Unity中的最佳实践组合:
- 合并相同材质的四边形为单个Mesh
- 使用GPU Instancing绘制重复元素
- 实现动态合批的着色器:
// 在着色器中根据顶点ID计算世界位置 float4 GetWorldPosition(uint vertexID) { int quadID = vertexID / 4; int inQuadID = vertexID % 4; return mul(_QuadDataBuffer[quadID].localToWorld, _QuadVertexOffsets[inQuadID]); }经过这些优化,在中等硬件上可实现:
- 生成10万顶点网格 < 200ms
- 60FPS流畅编辑体验(5万顶点级别)
- 内存占用减少40-60%
