Unity游戏开发实战手把手教你用C#复刻Townscaper的有机网格生成附完整源码在独立游戏《Townscaper》中那种看似随意却又充满秩序感的建筑网格令人着迷。这种独特的视觉效果背后是一套精妙的有机网格生成算法。本文将带你从零开始在Unity中实现这套算法并封装成可直接用于项目的可配置组件。1. 环境准备与基础架构1.1 创建Unity项目与基础脚本首先新建一个3D Unity项目2021.3 LTS或更新版本创建名为OrganicGridGenerator的C#脚本using UnityEngine; using System.Collections.Generic; [ExecuteInEditMode] public class OrganicGridGenerator : MonoBehaviour { [Header(Generation Parameters)] [Range(3, 20)] public int gridSize 8; [Range(0, 100)] public int relaxationIterations 15; public int seed 12345; [Header(Debug Visualization)] public bool showVertices true; public bool showEdges true; public Color gizmoColor Color.cyan; // 数据容器 private ListVector2 vertices new ListVector2(); private Listint triangles new Listint(); private Listint quads new Listint(); void OnValidate() GenerateGrid(); void OnDrawGizmos() DrawDebugVisuals(); void GenerateGrid() { /* 后续实现 */ } void DrawDebugVisuals() { /* 后续实现 */ } }1.2 核心数据结构设计我们需要三种基础数据结构来支撑网格生成[System.Serializable] public struct GridVertex { public Vector2 position; public bool isBoundary; public Listint connectedVertices; } [System.Serializable] public struct GridFace { public int[] vertexIndices; public bool isQuad; }关键设计考虑使用Listint而非数组存储连接关系便于动态修改分离顶点数据与拓扑关系方便后续松弛操作标记边界顶点防止过度变形2. 核心算法实现2.1 初始三角化阶段在GenerateGrid()方法中实现六边形三角化void GenerateInitialTriangulation() { vertices.Clear(); triangles.Clear(); // 六边形顶点生成 float hexRadius gridSize * 0.5f; for (int ring 0; ring gridSize; ring) { int verticesInRing (ring 0) ? 1 : 6 * ring; for (int i 0; i verticesInRing; i) { float angle 2 * Mathf.PI * i / verticesInRing; Vector2 pos new Vector2( hexRadius * ring * Mathf.Cos(angle), hexRadius * ring * Mathf.Sin(angle)); vertices.Add(new GridVertex { position pos, isBoundary (ring gridSize - 1) }); } } // Delaunay三角化简化版 TriangulateHexagonalGrid(); }2.2 边随机剔除与四边形形成void FormQuadrilaterals() { System.Random rng new System.Random(seed); quads.Clear(); // 创建可修改的三角形副本 Listint workingTris new Listint(triangles); while (workingTris.Count 0) { int randomIndex rng.Next(0, workingTris.Count / 3) * 3; int v0 workingTris[randomIndex]; int v1 workingTris[randomIndex 1]; int v2 workingTris[randomIndex 2]; // 查找共享边的相邻三角形 int? adjacentTri FindAdjacentTriangle(v0, v1, v2, workingTris); if (adjacentTri.HasValue) { // 组成四边形 int[] quadVertices MergeTriangles( v0, v1, v2, workingTris[adjacentTri.Value], workingTris[adjacentTri.Value 1], workingTris[adjacentTri.Value 2]); quads.AddRange(quadVertices); // 移除已处理三角形 workingTris.RemoveRange(Mathf.Min(randomIndex, adjacentTri.Value), Mathf.Max(randomIndex, adjacentTri.Value) 3 - Mathf.Min(randomIndex, adjacentTri.Value)); } else { workingTris.RemoveRange(randomIndex, 3); } } }2.3 网格细分技术void SubdivideFaces() { Listint newQuads new Listint(); Dictionarystring, int edgeMidpoints new Dictionarystring, int(); foreach (var face in GetAllFaces()) // 包括剩余三角形 { if (face.isQuad) { // 四边形细分为4个小四边形 int[] subQuads SubdivideQuad(face, edgeMidpoints); newQuads.AddRange(subQuads); } else { // 三角形细分为3个四边形 int[] subQuads SubdivideTriangle(face, edgeMidpoints); newQuads.AddRange(subQuads); } } quads newQuads; }3. 网格优化与松弛3.1 Lloyd松弛算法实现void RelaxVertices() { // 构建邻接关系 UpdateVertexConnections(); for (int iter 0; iter relaxationIterations; iter) { // 计算每个顶点的新位置相邻顶点平均值 Vector2[] newPositions new Vector2[vertices.Count]; for (int i 0; i vertices.Count; i) { if (vertices[i].isBoundary) continue; Vector2 sum Vector2.zero; foreach (int neighbor in vertices[i].connectedVertices) { sum vertices[neighbor].position; } newPositions[i] sum / vertices[i].connectedVertices.Count; } // 应用新位置 for (int i 0; i vertices.Count; i) { if (!vertices[i].isBoundary) { vertices[i].position Vector2.Lerp( vertices[i].position, newPositions[i], 0.5f); // 平滑过渡 } } } }3.2 边界处理技巧void ProcessBoundary() { float maxRadius gridSize * 0.5f; Vector2 center Vector2.zero; foreach (var vertex in vertices) { if (!vertex.isBoundary) continue; Vector2 dir (vertex.position - center).normalized; float currentDist Vector2.Distance(vertex.position, center); float pullFactor Mathf.Clamp01((currentDist - maxRadius * 0.7f) / (maxRadius * 0.3f)); vertex.position Vector2.Lerp( vertex.position, center dir * maxRadius, pullFactor * 0.3f); } }4. 工程化优化与实战技巧4.1 编辑器可视化调试void DrawDebugVisuals() { if (!showVertices !showEdges) return; Gizmos.color gizmoColor; // 绘制顶点 if (showVertices) { foreach (var vertex in vertices) { Gizmos.DrawSphere(vertex.position, 0.1f); } } // 绘制边 if (showEdges) { foreach (var face in GetAllFaces()) { int count face.isQuad ? 4 : 3; for (int i 0; i count; i) { int j (i 1) % count; Gizmos.DrawLine( vertices[face.vertexIndices[i]].position, vertices[face.vertexIndices[j]].position); } } } }4.2 性能优化建议对象池技术在频繁生成网格的场景中重用List容器而非反复创建增量更新添加[SerializeField] bool autoUpdate控制避免不必要的计算异步生成对于大型网格考虑使用UnityEditor.AsyncHTTPRequest或JobSystem// 示例增量更新实现 [SerializeField] bool needsRegeneration; void Update() { if (needsRegeneration) { GenerateGrid(); needsRegeneration false; } }4.3 常见问题排查问题1网格出现裂缝检查边剔除阶段的顶点索引处理确认细分阶段正确计算中点位置问题2松弛后网格变形异常验证边界顶点标记是否正确调整松弛迭代次数通常15-20次为宜问题3性能卡顿在OnValidate中添加防抖逻辑对大网格禁用实时预览// 防抖实现示例 float lastValidateTime; void OnValidate() { if (Time.time - lastValidateTime 0.5f) return; lastValidateTime Time.time; GenerateGrid(); }5. 扩展应用与风格化调整5.1 参数化风格控制添加这些参数到脚本头部[Header(Stylization)] [Range(0f, 1f)] public float organicAmount 0.5f; [Range(0f, 1f)] public float edgeSharpness 0.7f; public AnimationCurve sizeDistribution AnimationCurve.Linear(0, 1, 1, 1);修改松弛算法// 在RelaxVertices()中修改 float organicFactor organicAmount * Mathf.PerlinNoise( vertex.position.x * 0.1f, vertex.position.y * 0.1f); newPositions[i] Vector2.Lerp( vertices[i].position, newPositions[i], organicFactor);5.2 三维化扩展创建配套的三维网格生成器[RequireComponent(typeof(MeshFilter))] public class GridMeshBuilder : MonoBehaviour { public float baseHeight 1f; public float heightVariation 0.3f; public void BuildMesh(ListVector2 vertices, Listint quads) { Mesh mesh new Mesh(); // 顶点处理... // 面片生成... GetComponentMeshFilter().mesh mesh; } }5.3 存档与种子系统public string SaveGridState() { GridData data new GridData { vertices this.vertices, quads this.quads, parameters new GenerationParameters { gridSize this.gridSize, seed this.seed } }; return JsonUtility.ToJson(data); } public void LoadGridState(string json) { GridData data JsonUtility.FromJsonGridData(json); // 应用数据... }在项目中使用时建议将核心算法分离为独立的静态类方便在不同场景中复用。实际开发中发现将网格生成分为预处理三角化和运行时变形两个阶段可以显著提升性能表现。