当前位置: 首页 > news >正文

Unity游戏开发实战:手把手教你用C#复刻Townscaper的有机网格生成(附完整源码)

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 List<Vector2> vertices = new List<Vector2>(); private List<int> triangles = new List<int>(); private List<int> quads = new List<int>(); void OnValidate() => GenerateGrid(); void OnDrawGizmos() => DrawDebugVisuals(); void GenerateGrid() { /* 后续实现 */ } void DrawDebugVisuals() { /* 后续实现 */ } }

1.2 核心数据结构设计

我们需要三种基础数据结构来支撑网格生成:

[System.Serializable] public struct GridVertex { public Vector2 position; public bool isBoundary; public List<int> connectedVertices; } [System.Serializable] public struct GridFace { public int[] vertexIndices; public bool isQuad; }

关键设计考虑

  • 使用List<int>而非数组存储连接关系,便于动态修改
  • 分离顶点数据与拓扑关系,方便后续松弛操作
  • 标记边界顶点防止过度变形

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(); // 创建可修改的三角形副本 List<int> workingTris = new List<int>(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() { List<int> newQuads = new List<int>(); Dictionary<string, int> edgeMidpoints = new Dictionary<string, 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 性能优化建议

  1. 对象池技术:在频繁生成网格的场景中,重用List容器而非反复创建
  2. 增量更新:添加[SerializeField] bool autoUpdate控制,避免不必要的计算
  3. 异步生成:对于大型网格,考虑使用UnityEditor.AsyncHTTPRequestJobSystem
// 示例:增量更新实现 [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(List<Vector2> vertices, List<int> quads) { Mesh mesh = new Mesh(); // 顶点处理... // 面片生成... GetComponent<MeshFilter>().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.FromJson<GridData>(json); // 应用数据... }

在项目中使用时,建议将核心算法分离为独立的静态类,方便在不同场景中复用。实际开发中发现,将网格生成分为预处理(三角化)和运行时(变形)两个阶段,可以显著提升性能表现。

http://www.rkmt.cn/news/1410691.html

相关文章:

  • Cortex-M3字节序机制与优化实践
  • Claude vs GPT vs Gemini:系统级工程工作流基准测试深度解析
  • 2026年质量好的自贡非遗传统花灯/LED花灯/户外花灯/国潮花灯实力工厂推荐 - 品牌宣传支持者
  • 别再瞎调了!ACfly飞控ADRC参数整定保姆级指南(附Simulink仿真避坑)
  • HWO系统如何实现0.1G级磁星探测与偏振测量
  • 从手动整理到智能检索:我用AI工具管理素材库的实践
  • 从庞贝到元宇宙:如何用Blender和Unreal Engine 5重建一座2000年前的古城
  • 从‘False’到‘True’:手把手教你修复PyTorch GPU支持,并验证CUDA安装是否真的成功
  • 速腾聚创RS-M1激光雷达开箱实测:从拆箱到上电,手把手教你避坑布线
  • 深入理解ros_control:手把手教你为Gazebo仿真机械臂配置关节轨迹与状态控制器
  • 2026年质量好的激光加工/激光熔覆加工/盐城激光耐高温加工批量采购厂家推荐 - 品牌宣传支持者
  • 为什么你的ChatGPT职业规划总失效?揭秘行业未公开的4层能力断层与2024最新对齐方案
  • Dallas 390/400微控制器连续模式配置指南
  • 临床验证有效率83.6%的AI冥想引导模板(N=1,247 RCT数据):含5种脑波同步频率精准匹配策略
  • 2026年Snyk与GitLab深度集成:DevSecOps实战配置与优化指南
  • 别再只会用COUNT了!Power BI数据分析中这5个DAX计数函数,你用对了吗?
  • MoltsPay:为链上智能体构建多链支付与结算基础设施
  • 用Vite+Vue3+Electron20快速打造一个现代化桌面应用(保姆级配置流程)
  • 别再用高斯滤波了!OpenCV中值滤波实战:3行代码搞定椒盐噪声,附Python完整代码
  • 别再死磕光线追踪了!用Unity/Unreal的IBL环境光探针,5分钟搞定写实级全局光照
  • PRoN算法:基于PageRank的芯片后硅验证信号选择新方法
  • 2026年口碑好的绵阳老房翻新装饰公司/绵阳二手房翻新装饰公司/绵阳全包装饰公司/绵阳新房装饰公司哪家收费合理 - 行业平台推荐
  • 深入解析vue-virtual-scroll-list:高效实现Vue大数据列表渲染的完整指南
  • 每周演示可工作软件:弥合团队鸿沟、重塑敏捷交付的核心实践
  • 在CentOS 7上搞定sentencepiece安装:一个重命名whl文件的小技巧
  • 2026年比较好的泰安断桥铝门窗系统窗/断桥铝门窗阳光房定制主流厂家对比评测 - 品牌宣传支持者
  • 告别编译混乱:手把手教你用DSC文件管理UEFI固件项目(以EDK2 vUDK2018为例)
  • 思源宋体TTF字体:5分钟掌握免费商用中文排版方案
  • Lancet Digit Health(IF=24.1)牛津大学:基于Transformer的心血管病预防性治疗人群筛选
  • Windows下pip升级报错“拒绝访问”?试试这个--user参数,5分钟搞定