当前位置: 首页 > 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.jsqmd.com/news/901316/

相关文章:

  • MathType装完Word里不显示?可能是Office的‘信任中心’在搞鬼,5分钟教你设置好
  • 告别PyCharm红色波浪线:快速修复第三方库识别失败的3种实用方法(含Pythonw.exe选择指南)
  • OpenAPI x-agent-trust扩展:为AI智能体构建API信任机制
  • 2026年质量好的自贡非遗传统花灯/LED花灯/户外花灯/国潮花灯实力工厂推荐 - 品牌宣传支持者
  • MySQL排序规则(Collation)详解:从一次SQL注入报错讲起,如何避免和排查字符集问题
  • Agiwo框架:从工具调用到工作流编排的AI应用架构设计
  • 别再瞎调了!ACfly飞控ADRC参数整定保姆级指南(附Simulink仿真避坑)
  • STM32CubeMX外部中断配置避坑指南:从引脚模式到回调函数,新手常犯的5个错误
  • 从手动整理到智能检索:我用AI工具管理素材库的实践
  • 从庞贝到元宇宙:如何用Blender和Unreal Engine 5重建一座2000年前的古城
  • Nolex:基于本地正则与AI检测的浏览器插件,守护AI交互中的敏感数据安全
  • 从‘调包侠’到‘造轮子’:手把手教你用irGSEA包的思路,打造自己的单细胞分析R包
  • 告别有线烧录:手把手教你用MQTT+HTTP为STM32设备打造无线OTA升级系统(附状态机源码)
  • 使用 Taotoken CLI 工具一键配置多开发环境下的 API 访问密钥
  • 2026年质量好的激光加工/激光熔覆加工/盐城激光耐高温加工批量采购厂家推荐 - 品牌宣传支持者
  • 蓝桥杯单片机DS18B20温度测量:从数据手册到四位小数显示的完整代码解析(含负数处理)
  • 临床验证有效率83.6%的AI冥想引导模板(N=1,247 RCT数据):含5种脑波同步频率精准匹配策略
  • 2026年Snyk与GitLab深度集成:DevSecOps实战配置与优化指南
  • 别再只盯着/etc/shadow了:用Python的crypt库手动生成和验证SHA-512密码密文
  • 别再只会用COUNT了!Power BI数据分析中这5个DAX计数函数,你用对了吗?
  • 2026年母婴抖店代运营公司排名前五专业深度测评 - 羊城派
  • MoltsPay:为链上智能体构建多链支付与结算基础设施
  • 2026年 雨水井模具/污水井模具/阀门井模具/电信井模具/电缆井模具/圆井模具/检查井模具/方井模具/拼装方井模具厂家推荐:质量过硬与工艺精度口碑之选 - 品牌企业推荐师(官方)
  • 避坑指南:欧姆龙NJ/NX系列PLC与得克威尔EX-1100 EtherCAT通信的那些‘坑’与最佳实践
  • RTX51与C51版本兼容性问题解析与解决方案
  • 用Vite+Vue3+Electron20快速打造一个现代化桌面应用(保姆级配置流程)
  • Lua动态代码的魔法:用load函数实现一个简易的‘规则引擎‘(附完整代码)
  • STM32CubeMX实战:用NUCLEO-F303RE实现超低功耗待机(5.8uA)与RTC闹钟精准唤醒
  • 基于Hindsight构建有记忆的客服AI:告别健忘,实现连续对话体验
  • SARscape实战:手把手教你处理.hgt格式SRTM DEM,解决干涉处理报错难题