Unity实现Townscaper风格有机网格生成全解析引言在独立游戏《Townscaper》中那种看似随意却又和谐统一的建筑布局让无数开发者着迷。这种独特的视觉效果背后隐藏着一套精妙的程序化网格生成算法。本文将带你从零开始在Unity中完整复现这套算法并深入探讨每个技术环节的实现细节。与单纯讲解数学原理不同我们更关注如何在Unity编辑器中可视化地构建和调试这套系统。通过C#脚本与Unity编辑器的无缝集成你可以实时观察网格从基础三角形到最终有机四边形的完整演变过程。1. 环境准备与基础架构1.1 创建Unity项目与基础脚本首先新建一个3D Unity项目创建名为OrganicGridGenerator的C#脚本。这个脚本将作为我们整个生成系统的核心[ExecuteInEditMode] public class OrganicGridGenerator : MonoBehaviour { [Range(2, 12)] public int gridSize 6; [Range(0, 100)] public int randomSeed 42; private ListVector2 points; private ListTriangle triangles; private ListQuad quads; void OnValidate() { Regenerate(); } void Regenerate() { /* 后续填充 */ } void OnDrawGizmos() { // 绘制网格的辅助代码 } }关键点说明ExecuteInEditMode特性允许我们在不运行游戏的情况下看到网格变化OnValidate确保参数修改后自动重新生成使用ListT存储几何数据以提高灵活性1.2 数据结构设计我们需要三种核心数据结构来表示网格元素public struct Point { public Vector2 position; public bool isBoundary; } public struct Triangle { public int a, b, c; public bool isValid; } public struct Quad { public int a, b, c, d; }为什么选择这样的结构Vector2足够表示2D网格位置isBoundary标记用于后续的边缘处理isValid标志控制边的随机剔除2. 核心算法实现2.1 Delaunay三角剖分基础虽然原文提到使用正六边形作为基础但实际项目中我们采用更通用的方法void GenerateDelaunayTriangulation() { points new ListVector2(); triangles new ListTriangle(); // 泊松圆盘采样生成随机点 PoissonDiskSampling.GeneratePoints(gridSize, out points); // 使用Bowyer-Watson算法进行三角剖分 DelaunayTriangulator.Triangulate(points, out triangles); }参数调优建议参数影响推荐值采样半径点密度0.8-1.2排斥次数均匀度20-30边界扩展边缘质量1.5倍范围2.2 随机边剔除与四边形形成这是算法中最关键的一步决定网格的有机感void RandomEdgeRemoval() { System.Random rand new System.Random(randomSeed); quads new ListQuad(); for(int i0; itriangles.Count; i) { if(!triangles[i].isValid) continue; // 寻找共享边的相邻三角形 var neighbors FindAdjacentTriangles(i); if(neighbors.Count 0) continue; // 随机决定是否合并 if(rand.NextDouble() 0.6f) { var quad MergeTriangles(i, neighbors[0]); quads.Add(quad); triangles[i].isValid false; triangles[neighbors[0]].isValid false; } } }提示0.6的合并阈值是个不错的起点值越大四边形越多但可能留下更多未合并的三角形2.3 网格细分技术将剩余三角形和初步四边形统一细分为更小的四边形void SubdivideToQuads() { var newQuads new ListQuad(); // 处理四边形 foreach(var quad in quads) { var center CalculateCenter(quad); int centerIndex points.Count; points.Add(center); newQuads.Add(new Quad(quad.a, quad.b, centerIndex, quad.a)); // 添加其他三个子四边形... } // 处理三角形转换为三个四边形 foreach(var tri in triangles) { if(!tri.isValid) continue; var center CalculateCenter(tri); int centerIndex points.Count; points.Add(center); newQuads.Add(new Quad(tri.a, tri.b, centerIndex, tri.a)); // 添加另外两个子四边形... } quads newQuads; }细分后的网格会明显变得更加密集为后续的松弛步骤做好准备。3. 网格优化与可视化3.1 松弛算法实现松弛步骤让网格看起来更自然void RelaxGrid(int iterations) { for(int i0; iiterations; i) { var newPositions new Vector2[points.Count]; for(int j0; jpoints.Count; j) { if(points[j].isBoundary) continue; Vector2 avg Vector2.zero; var neighbors FindAdjacentPoints(j); foreach(var n in neighbors) avg points[n].position; newPositions[j] avg / neighbors.Count; } // 应用新位置 for(int j0; jpoints.Count; j) if(!points[j].isBoundary) points[j].position newPositions[j]; } }迭代次数的影响3-5次保持原始形状特征10-15次更均匀但可能过度平滑20次完全松弛失去有机感3.2 Unity编辑器可视化利用Gizmos实现实时预览void OnDrawGizmos() { if(points null) return; // 绘制点 Gizmos.color Color.white; foreach(var p in points) Gizmos.DrawSphere(new Vector3(p.position.x, 0, p.position.y), 0.05f); // 绘制四边形 Gizmos.color Color.green; foreach(var q in quads) { DrawQuadGizmo(q); } // 绘制原始三角形调试用 if(showTriangles) { Gizmos.color Color.red; foreach(var t in triangles) { if(t.isValid) DrawTriangleGizmo(t); } } }通过添加这些调试可视化你可以直观地观察算法每个阶段的效果方便参数调整。4. 高级技巧与性能优化4.1 参数化控制系统在Inspector中添加更多控制参数[Header(Generation Parameters)] [Range(0.1f, 2f)] public float pointDensity 1f; [Range(0f, 1f)] public float edgeRemovalChance 0.6f; [Range(1, 5)] public int subdivisionLevels 2; [Header(Relaxation)] [Range(0, 20)] public int relaxationIterations 5; [Range(0f, 1f)] public float boundaryStiffness 0.3f; [Header(Debug)] public bool showTriangles false; public bool showSubdivision true;4.2 性能优化策略对于大型网格这些优化很关键空间分区使用网格或四叉树加速邻域查询并行计算对独立顶点使用Job System进行并行松弛增量更新只重新计算修改部分而非整个网格// 示例使用Unity的Job System并行松弛 public struct RelaxJob : IJobParallelFor { public NativeArrayVector2 positions; [ReadOnly] public NativeMultiHashMapint, int adjacency; public void Execute(int index) { // 实现松弛逻辑... } }4.3 与地形系统集成将生成的网格应用到Unity地形上void GenerateTerrain() { var mesh new Mesh(); // 将2D网格转换为3D网格 var vertices new Vector3[points.Count]; for(int i0; ipoints.Count; i) vertices[i] new Vector3(points[i].position.x, 0, points[i].position.y); // 构建三角形将四边形拆分为两个三角形 var meshTriangles new Listint(); foreach(var q in quads) { meshTriangles.AddRange(new[]{q.a, q.b, q.c}); meshTriangles.AddRange(new[]{q.a, q.c, q.d}); } mesh.vertices vertices; mesh.triangles meshTriangles.ToArray(); mesh.RecalculateNormals(); GetComponentMeshFilter().mesh mesh; }5. 实际应用案例5.1 建筑布局生成基于网格创建有机城镇布局void GenerateBuildings() { foreach(var quad in quads) { var center CalculateQuadCenter(quad); var size CalculateQuadSize(quad); if(size.magnitude minBuildingSize) { var building Instantiate(buildingPrefab, center, Quaternion.identity); building.transform.localScale size * 0.8f; } } }5.2 道路网络生成从网格边派生道路系统void GenerateRoads() { var edges new HashSetEdge(); foreach(var quad in quads) { edges.Add(new Edge(quad.a, quad.b)); edges.Add(new Edge(quad.b, quad.c)); // 添加其他边... } foreach(var edge in edges) { var p1 points[edge.a].position; var p2 points[edge.b].position; GenerateRoadSegment(p1, p2); } }在实际项目中这套系统经过适当调整后可以生成非常自然的城镇布局。一个常见的技巧是根据网格单元的面积决定放置建筑还是开放空间较大的单元适合放置主要建筑而较小的单元则可以作为花园或小路。
Unity里也能玩Townscaper?手把手教你用C#复现它的有机网格生成(附完整源码)
发布时间:2026/5/28 15:38:41
Unity实现Townscaper风格有机网格生成全解析引言在独立游戏《Townscaper》中那种看似随意却又和谐统一的建筑布局让无数开发者着迷。这种独特的视觉效果背后隐藏着一套精妙的程序化网格生成算法。本文将带你从零开始在Unity中完整复现这套算法并深入探讨每个技术环节的实现细节。与单纯讲解数学原理不同我们更关注如何在Unity编辑器中可视化地构建和调试这套系统。通过C#脚本与Unity编辑器的无缝集成你可以实时观察网格从基础三角形到最终有机四边形的完整演变过程。1. 环境准备与基础架构1.1 创建Unity项目与基础脚本首先新建一个3D Unity项目创建名为OrganicGridGenerator的C#脚本。这个脚本将作为我们整个生成系统的核心[ExecuteInEditMode] public class OrganicGridGenerator : MonoBehaviour { [Range(2, 12)] public int gridSize 6; [Range(0, 100)] public int randomSeed 42; private ListVector2 points; private ListTriangle triangles; private ListQuad quads; void OnValidate() { Regenerate(); } void Regenerate() { /* 后续填充 */ } void OnDrawGizmos() { // 绘制网格的辅助代码 } }关键点说明ExecuteInEditMode特性允许我们在不运行游戏的情况下看到网格变化OnValidate确保参数修改后自动重新生成使用ListT存储几何数据以提高灵活性1.2 数据结构设计我们需要三种核心数据结构来表示网格元素public struct Point { public Vector2 position; public bool isBoundary; } public struct Triangle { public int a, b, c; public bool isValid; } public struct Quad { public int a, b, c, d; }为什么选择这样的结构Vector2足够表示2D网格位置isBoundary标记用于后续的边缘处理isValid标志控制边的随机剔除2. 核心算法实现2.1 Delaunay三角剖分基础虽然原文提到使用正六边形作为基础但实际项目中我们采用更通用的方法void GenerateDelaunayTriangulation() { points new ListVector2(); triangles new ListTriangle(); // 泊松圆盘采样生成随机点 PoissonDiskSampling.GeneratePoints(gridSize, out points); // 使用Bowyer-Watson算法进行三角剖分 DelaunayTriangulator.Triangulate(points, out triangles); }参数调优建议参数影响推荐值采样半径点密度0.8-1.2排斥次数均匀度20-30边界扩展边缘质量1.5倍范围2.2 随机边剔除与四边形形成这是算法中最关键的一步决定网格的有机感void RandomEdgeRemoval() { System.Random rand new System.Random(randomSeed); quads new ListQuad(); for(int i0; itriangles.Count; i) { if(!triangles[i].isValid) continue; // 寻找共享边的相邻三角形 var neighbors FindAdjacentTriangles(i); if(neighbors.Count 0) continue; // 随机决定是否合并 if(rand.NextDouble() 0.6f) { var quad MergeTriangles(i, neighbors[0]); quads.Add(quad); triangles[i].isValid false; triangles[neighbors[0]].isValid false; } } }提示0.6的合并阈值是个不错的起点值越大四边形越多但可能留下更多未合并的三角形2.3 网格细分技术将剩余三角形和初步四边形统一细分为更小的四边形void SubdivideToQuads() { var newQuads new ListQuad(); // 处理四边形 foreach(var quad in quads) { var center CalculateCenter(quad); int centerIndex points.Count; points.Add(center); newQuads.Add(new Quad(quad.a, quad.b, centerIndex, quad.a)); // 添加其他三个子四边形... } // 处理三角形转换为三个四边形 foreach(var tri in triangles) { if(!tri.isValid) continue; var center CalculateCenter(tri); int centerIndex points.Count; points.Add(center); newQuads.Add(new Quad(tri.a, tri.b, centerIndex, tri.a)); // 添加另外两个子四边形... } quads newQuads; }细分后的网格会明显变得更加密集为后续的松弛步骤做好准备。3. 网格优化与可视化3.1 松弛算法实现松弛步骤让网格看起来更自然void RelaxGrid(int iterations) { for(int i0; iiterations; i) { var newPositions new Vector2[points.Count]; for(int j0; jpoints.Count; j) { if(points[j].isBoundary) continue; Vector2 avg Vector2.zero; var neighbors FindAdjacentPoints(j); foreach(var n in neighbors) avg points[n].position; newPositions[j] avg / neighbors.Count; } // 应用新位置 for(int j0; jpoints.Count; j) if(!points[j].isBoundary) points[j].position newPositions[j]; } }迭代次数的影响3-5次保持原始形状特征10-15次更均匀但可能过度平滑20次完全松弛失去有机感3.2 Unity编辑器可视化利用Gizmos实现实时预览void OnDrawGizmos() { if(points null) return; // 绘制点 Gizmos.color Color.white; foreach(var p in points) Gizmos.DrawSphere(new Vector3(p.position.x, 0, p.position.y), 0.05f); // 绘制四边形 Gizmos.color Color.green; foreach(var q in quads) { DrawQuadGizmo(q); } // 绘制原始三角形调试用 if(showTriangles) { Gizmos.color Color.red; foreach(var t in triangles) { if(t.isValid) DrawTriangleGizmo(t); } } }通过添加这些调试可视化你可以直观地观察算法每个阶段的效果方便参数调整。4. 高级技巧与性能优化4.1 参数化控制系统在Inspector中添加更多控制参数[Header(Generation Parameters)] [Range(0.1f, 2f)] public float pointDensity 1f; [Range(0f, 1f)] public float edgeRemovalChance 0.6f; [Range(1, 5)] public int subdivisionLevels 2; [Header(Relaxation)] [Range(0, 20)] public int relaxationIterations 5; [Range(0f, 1f)] public float boundaryStiffness 0.3f; [Header(Debug)] public bool showTriangles false; public bool showSubdivision true;4.2 性能优化策略对于大型网格这些优化很关键空间分区使用网格或四叉树加速邻域查询并行计算对独立顶点使用Job System进行并行松弛增量更新只重新计算修改部分而非整个网格// 示例使用Unity的Job System并行松弛 public struct RelaxJob : IJobParallelFor { public NativeArrayVector2 positions; [ReadOnly] public NativeMultiHashMapint, int adjacency; public void Execute(int index) { // 实现松弛逻辑... } }4.3 与地形系统集成将生成的网格应用到Unity地形上void GenerateTerrain() { var mesh new Mesh(); // 将2D网格转换为3D网格 var vertices new Vector3[points.Count]; for(int i0; ipoints.Count; i) vertices[i] new Vector3(points[i].position.x, 0, points[i].position.y); // 构建三角形将四边形拆分为两个三角形 var meshTriangles new Listint(); foreach(var q in quads) { meshTriangles.AddRange(new[]{q.a, q.b, q.c}); meshTriangles.AddRange(new[]{q.a, q.c, q.d}); } mesh.vertices vertices; mesh.triangles meshTriangles.ToArray(); mesh.RecalculateNormals(); GetComponentMeshFilter().mesh mesh; }5. 实际应用案例5.1 建筑布局生成基于网格创建有机城镇布局void GenerateBuildings() { foreach(var quad in quads) { var center CalculateQuadCenter(quad); var size CalculateQuadSize(quad); if(size.magnitude minBuildingSize) { var building Instantiate(buildingPrefab, center, Quaternion.identity); building.transform.localScale size * 0.8f; } } }5.2 道路网络生成从网格边派生道路系统void GenerateRoads() { var edges new HashSetEdge(); foreach(var quad in quads) { edges.Add(new Edge(quad.a, quad.b)); edges.Add(new Edge(quad.b, quad.c)); // 添加其他边... } foreach(var edge in edges) { var p1 points[edge.a].position; var p2 points[edge.b].position; GenerateRoadSegment(p1, p2); } }在实际项目中这套系统经过适当调整后可以生成非常自然的城镇布局。一个常见的技巧是根据网格单元的面积决定放置建筑还是开放空间较大的单元适合放置主要建筑而较小的单元则可以作为花园或小路。