
1. 项目概述为什么Unity开发者需要一个计算几何库如果你在Unity里做过稍微复杂一点的游戏逻辑比如生成随机地形、处理复杂的碰撞检测、或者实现一个策略游戏的寻路系统你大概率已经和计算几何打过交道了。你可能自己写过一些向量运算用Physics.Raycast或者Collider.ClosestPoint来处理碰撞但一旦需求变得复杂——比如需要把一个不规则多边形切割成多个凸多边形或者动态生成一片由Voronoi图划分的领土——你就会发现Unity内置的API开始力不从心写出来的代码既臃肿又低效。这就是Computational-geometry这个库的价值所在。它不是一个简单的工具集而是一个系统性的、工业级的计算几何算法库专门为Unity环境优化和封装。它把那些在学术论文和算法竞赛里才能看到的经典几何算法比如Delaunay三角剖分、Voronoi图生成、多边形布尔运算并集、交集、差集、曲线拟合等变成了Unity开发者可以直接调用的、高性能的C#组件。想象一下你不再需要从零开始实现一个健壮的耳切法来三角化一个带洞的多边形也不用担心自己写的线段相交检测在边界条件下崩溃。这个库帮你封装了这些复杂性让你能专注于游戏玩法和创意本身。我最初接触它是因为一个城市建造类项目需要动态划分地块。手动摆放既费时又不自然而用Computational-geometry的Voronoi图生成功能我只需要输入一些随机点就能立刻得到一片看起来非常有机的、由多边形划分的地块网格并且每个地块的顶点、边信息都是结构化的可以直接用于道路生成、区域染色或资源分配。从那以后这个库就成了我工具箱里的常客无论是处理2D精灵的复杂碰撞形状还是为3D地形生成网格的辅助结构它都能派上用场。2. 核心功能模块深度解析Computational-geometry库的功能相当丰富我们可以将其核心模块分为几个大类理解每一类能解决什么问题是高效使用它的关键。2.1 基础几何结构与运算任何复杂的几何算法都建立在坚实的基础之上。这个库提供了一套完整且高效的基础结构。点、向量、线段、多边形这些不是简单的Vector2或Vector3数组。库中定义的Polygon类会严格处理顶点顺序通常是逆时针、确保不自交并且内置了面积计算、重心计算、点包含性测试射线法或环绕数法等方法。Segment线段类则封装了精确的相交检测、点到线段的最近点计算。几何谓词与鲁棒性这是库的精华之一。直接使用浮点数进行几何比较比如判断点是否在线段上会因为浮点误差而导致错误。库内部使用了自适应精度的几何谓词或者精心设计的容差比较确保了算法在绝大多数情况下的鲁棒性。这意味着你的布尔运算不会因为一个微小的数值误差而产生破碎的多边形。凸包算法给定一堆散点快速找到那个最小的凸多边形将其全部包裹起来。库通常实现了经典的Graham扫描法或Andrew单调链算法。这在需要简化碰撞体、计算包围区域时非常有用。2.2 多边形布尔运算裁剪这是使用频率最高的功能之一。它允许你对两个或多个多边形进行像集合一样的操作。并集 (Union)合并两个多边形重叠部分只保留一份轮廓。常用于合并相邻区域。交集 (Intersection)只保留两个多边形重叠的部分。可以用来实现“视野裁剪”——将角色的视野范围多边形与障碍物多边形求交得到实际可见区域。差集 (Difference)从一个多边形中“挖去”与另一个多边形重叠的部分。典型应用是一个房间多边形地板减去多个家具占用的多边形得到可以行走的区域。异或 (XOR)保留两个多边形不重叠的部分。相对少用但在一些特效或特殊关卡设计中可能用到。注意布尔运算对输入多边形的质量要求较高。虽然库有容错处理但确保你的多边形是简单的不自交、顶点顺序一致会得到最好和最稳定的结果。对于带洞的多边形需要按照库约定的格式例如外轮廓逆时针内洞顺时针来定义。2.3 三角剖分与网格化将复杂多边形分解为三角形这是渲染和物理模拟的基石。耳切法 (Ear Clipping)适用于简单多边形无洞的三角剖分。算法思想是不断地“剪掉”多边形的一个“耳朵”由连续三个顶点组成的凸角。库的实现会高效地判断“耳朵”并处理顶点链表。带洞多边形的三角剖分这是难点。库可能需要你将外轮廓和内洞的顶点连接成一个“伪简单多边形”然后再应用耳切法或其他算法如单调多边形剖分。Computational-geometry通常会提供一个统一的接口你只需要传入外轮廓和洞的列表即可。Delaunay三角剖分给定一组平面点集生成一个三角网格并满足Delaunay准则任意三角形的外接圆内不包含其他点。这种剖分能最大化最小角避免出现“瘦长”的三角形在数值计算中更稳定。它是生成Voronoi图的基础。2.4 Voronoi图与Delaunay三角剖分这两者是紧密相关的对偶图。Delaunay三角剖分连接了点而Voronoi图划分了区域。从Delaunay到Voronoi库生成Delaunay三角剖分后可以很容易地推导出Voronoi图。每个Voronoi单元格区域对应一个原始点区域内的任意位置到该点的距离都比到其他点更近。应用场景程序化生成生成自然的地块、细胞、裂纹纹理。用随机点生成Voronoi图每个单元格可以赋予不同的生物群系、资源或领土归属。地图分区将游戏地图划分为多个区域用于AI寻路先在区域间寻路再在区域内寻路、动态加载或事件管理。破碎效果虽然物理破碎更复杂但Voronoi图可以作为生成破碎裂纹形状的优秀起点。2.5 曲线与路径处理贝塞尔曲线、样条曲线等用于平滑运动轨迹、武器弹道或UI动画。贝塞尔曲线求值与分割库提供了标准的三次贝塞尔曲线函数可以获取曲线上任意比例的点位置和切线方向。更重要的是它可能包含曲线扁平化算法即将曲线分割成一系列足够短的线段以便于渲染或碰撞检测。路径偏移与轮廓生成给定一条中心线路径生成其平行线等距线。这可以用来从一条路径生成道路的左右边界或者为线条生成描边。这里涉及到大量几何计算如线段平移、圆弧拐角处理等自己实现非常繁琐。3. 在Unity项目中的实战集成与应用知道有什么工具是一回事知道怎么用是另一回事。下面我们看看如何把这个库真正集成到Unity项目中并解决实际问题。3.1 库的获取与项目设置通常Computational-geometry会以.unitypackage或通过Unity的Package Manager (UPM) 以Git URL的形式提供。导入将包导入项目后你会在Assets目录下看到相关的脚本、示例场景和可能预制体。第一件事是浏览示例场景这是最快的学习途径。命名空间库的所有核心类通常都在一个特定的命名空间下比如ComputationalGeometry或CG。在你的脚本开头需要添加using指令。版本兼容性关注库的更新日志确保其与你使用的Unity版本兼容。一些底层数学库的更新如Mathematics包可能会影响兼容性。3.2 典型应用案例分步实现让我们通过两个具体案例来走一遍完整的实现流程。案例一动态生成随机地图并划分领土假设我们在做一个RTS或4X策略游戏需要随机生成一张地图并用自然边界划分给不同玩家或势力。生成随机点在游戏地图的矩形边界内用泊松圆盘采样算法生成一组随机点。这能保证点与点之间有一个最小距离避免生成过于细碎的Voronoi单元格。你可以先使用库中的基础随机点生成或者用其他算法生成更均匀的点集。// 伪代码思路 ListVector2 points PoissonDiskSampling.GeneratePoints(mapWidth, mapHeight, minDistance);执行Delaunay三角剖分将点集输入库的Delaunay剖分函数。using ComputationalGeometry; DelaunayTriangulation triangulation DelaunayTriangulator.Triangulate(points);生成Voronoi图从Delaunay三角剖分结果中构建Voronoi图。库通常会返回一个VoronoiDiagram对象其中包含所有Voronoi站点原始点和对应的多边形单元格。VoronoiDiagram voronoiDiagram new VoronoiDiagram(triangulation); foreach (VoronoiCell cell in voronoiDiagram.Cells) { Polygon cellPolygon cell.GetPolygon(); // 获取该单元格的多边形表示 // cellPolygon 可能是一个凸多边形也可能是一个凹多边形如果靠近边界 }裁剪到地图边界生成的Voronoi单元格是无限延伸的我们需要用地图的矩形边界多边形去裁剪每一个单元格多边形这里就用到了多边形布尔运算求交。Polygon mapBoundsPolygon CreateRectPolygon(mapWidth, mapHeight); foreach (VoronoiCell cell in voronoiDiagram.Cells) { Polygon clippedPolygon PolygonClipper.Intersect(cell.GetPolygon(), mapBoundsPolygon); // 现在 clippedPolygon 就是在地图范围内的领土多边形 }可视化与数据关联你可以为每个裁剪后的多边形生成一个Mesh用于渲染或者添加一个PolygonCollider2D用于物理交互。同时将这个多边形对象与你游戏中的“领土”、“省份”等数据对象关联起来。案例二实现一个复杂的2D角色视野系统带障碍物遮挡许多潜行或RTS游戏需要计算角色的视野范围并且视野会被墙壁等障碍物阻挡。定义视野范围通常是一个以角色为圆心、一定半径为半径的扇形或圆形。我们可以用一个高精度的多边形例如用很多边近似一个圆来表示这个“理想视野区域”。Polygon idealFOV Polygon.CreateCircle(characterPosition, viewRadius, 60); // 用60条边近似圆获取障碍物信息收集视野范围内所有障碍物的碰撞体如BoxCollider2D, PolygonCollider2D并将它们的形状转换为库能识别的Polygon对象。注意处理障碍物的世界坐标变换。视野裁剪将“理想视野区域”多边形与所有障碍物多边形的并集进行差集运算。但直接与所有障碍物依次做差集会非常慢且可能出错。更优的策略是先将所有障碍物多边形合并成一个大的“阻挡区域”多边形使用多次并集运算。然后用“理想视野区域”减去这个“阻挡区域”。Polygon combinedObstacles obstaclePolygons[0]; for (int i 1; i obstaclePolygons.Count; i) { combinedObstacles PolygonClipper.Union(combinedObstacles, obstaclePolygons[i]); } Polygon actualFOV PolygonClipper.Difference(idealFOV, combinedObstacles);处理结果与优化运算结果actualFOV可能是一个复杂多边形可能带洞也可能被分割成多个独立部分。你需要遍历它的所有轮廓进行渲染。对于动态更新的视野需要考虑性能优化比如使用空间划分四叉树来快速获取潜在障碍物并对视野多边形进行低精度简化道格拉斯-普克算法后再渲染。3.3 性能考量与优化技巧计算几何算法可能是CPU密集型的尤其是在对象很多或多边形很复杂时。缓存结果如果某些几何形状是静态的如关卡中的墙壁预先将其转换为Polygon对象并缓存起来避免每帧都从Collider转换。简化输入在进行复杂运算如布尔运算前用多边形简化算法减少顶点数量。Computational-geometry库可能内置了简化功能或者你可以先自己用道格拉斯-普克算法处理。空间划分对于需要处理大量几何体的场景如数百个障碍物使用四叉树2D或BVH树来快速剔除无关对象只对视野或兴趣范围内的物体进行计算。分帧处理如果视野更新不需要每帧都完全精确可以考虑分几帧来完成计算或者使用上一帧的结果作为近似。关注算法复杂度了解你所调用函数的算法复杂度。例如简单的凸包算法是O(n log n)而多边形布尔运算可能更复杂。避免在Update中处理顶点数上千的多边形。4. 常见问题、调试技巧与进阶使用即使有了强大的库在实际开发中还是会遇到各种问题。这里记录一些我踩过的坑和解决方法。4.1 常见错误与异常处理问题现象可能原因排查与解决思路布尔运算结果为空或错误1. 输入多边形不是简单的自交。2. 顶点顺序不一致有的顺时针有的逆时针。3. 多边形退化面积为零或接近零。4. 浮点精度问题导致算法误判。1. 在运算前调用库的Polygon.IsSimple()进行检查。2. 统一用Polygon.ForceWindingOrder(WindingOrder.CounterClockwise)强制为逆时针。3. 过滤掉顶点距离过近的点顶点去重。4. 尝试轻微缩放如1.001倍或平移多边形避开奇点。Delaunay三角剖分失败或崩溃1. 输入点存在重合点。2. 点共线或几乎共线。3. 点集规模太大递归算法栈溢出如果实现是递归的。1. 对输入点进行去重。2. 加入微小的随机扰动破坏共线性。3. 分批处理点集或寻找库中迭代算法的替代接口。生成的Voronoi图单元格缺失或异常1. 边界点处理问题。2. 从Delaunay到Voronoi的转换逻辑bug。3. 裁剪边界时出错。1. 检查库的文档看是否需要对边界点进行特殊处理。2. 在简单数据集如4个点构成的正方形上测试验证基础功能。3. 先不裁剪查看原始的无限Voronoi图是否正确。性能突然下降1. 输入数据复杂度激增顶点数暴涨。2. 在循环内频繁创建临时Polygon对象引发GC垃圾回收。1. 使用简化算法降低顶点数。2.对象池对于频繁使用的Polygon、ListVector2等对象使用对象池复用避免分配和回收开销。这是Unity性能优化的黄金法则之一。4.2 可视化调试让几何问题无所遁形计算几何的问题光看日志很难定位必须“看到”数据。自定义Debug绘制在OnDrawGizmos或OnDrawGizmosSelected中使用Gizmos.DrawLine,Gizmos.DrawSphere,Handles.DrawPolyLine等函数将你的点、线段、多边形轮廓实时绘制在Scene视图中。void OnDrawGizmosSelected() { if (myPolygon ! null myPolygon.Vertices.Count 1) { Gizmos.color Color.green; for (int i 0; i myPolygon.Vertices.Count; i) { Vector3 start myPolygon.Vertices[i]; Vector3 end myPolygon.Vertices[(i 1) % myPolygon.Vertices.Count]; Gizmos.DrawLine(start, end); } // 绘制顶点 Gizmos.color Color.red; foreach (var vertex in myPolygon.Vertices) { Gizmos.DrawSphere(vertex, 0.1f); } } }分步绘制对于复杂的多步运算将每一步的中间结果用不同颜色绘制出来。例如绘制原始多边形为蓝色绘制障碍物为红色绘制布尔运算后的结果为绿色。这样哪一步出了问题一目了然。使用TextMeshPro显示数据在Game视图里将关键信息如顶点数、面积、算法耗时实时显示在UI上方便在运行时监控。4.3 与Unity其他系统的协同Computational-geometry库生成的几何数据最终需要融入到Unity的生态中。Mesh生成将Polygon转换为Mesh用于渲染。你需要处理顶点、三角形索引和UV。对于带洞的多边形三角剖分是这一步的前提。Unity的Mesh类有SetVertices,SetTriangles,SetUVs等方法。Collider生成2DPolygonCollider2D可以通过SetPath来设置轮廓。注意顶点数限制和凸多边形限制如果是凹多边形需要先分解为凸多边形。3D情况更复杂。通常需要先三角剖分生成Mesh然后使用MeshCollider。对于复杂静态物体考虑在编辑器下预生成。NavMesh如果你想用计算几何划分的区域来影响AI寻路可能需要将生成的多边形边界转换为NavMesh的障碍物NavMeshObstacle或者手动烘焙多个NavMesh区域。Unity的NavMesh系统有对应的API可以操作。Tilemap与2D工具如果你在使用Unity的Tilemap系统计算几何生成的多边形区域可以用来定义可建筑区域、不可通行区域等通过脚本将数据同步到Tilemap的规则瓦片或染色上。4.4 从使用到贡献理解源码与扩展当你对这个库依赖越来越深可能会遇到一些特殊需求库本身不支持或者发现了一些小bug。这时理解其源码结构就很有必要了。阅读关键算法不要试图一开始就读懂所有代码。找到你最常用的功能模块比如PolygonClipper.cs从它的公开接口入手逐步深入。关注它的核心算法是引用了哪篇论文或者实现了哪种经典算法如Greiner-Hormann算法用于多边形裁剪。编写扩展方法C#的扩展方法非常适合为库的现有类添加便捷功能。例如你可以为Polygon类添加一个ToMesh()的扩展方法封装从多边形到Unity Mesh的转换逻辑方便在各个项目中复用。public static class PolygonExtensions { public static Mesh ToMesh(this Polygon polygon) { // 三角剖分创建Mesh... return mesh; } }报告问题与贡献如果你确认发现了bug并且有能力修复可以考虑向开源仓库提交Pull Request。在提交前确保你理解了代码风格并添加了相应的测试用例。5. 性能优化深度实践与内存管理在游戏运行时尤其是移动平台或VR/AR应用中计算几何操作的性能瓶颈可能非常明显。除了之前提到的通用技巧这里还有一些更深层次的优化策略。5.1 算法层面的选择性使用不是所有问题都需要动用最通用的算法。根据输入数据的特性选择更高效的特定算法。凸多边形 vs 凹多边形对于凸多边形许多操作如点包含检测、与其他凸多边形的相交测试有O(log n)或O(n)的专用算法远比处理凹多边形的通用算法快。在数据设计阶段尽量使用凸多边形。如果必须是凹多边形考虑用凸分解算法将其预先分解为多个凸多边形的集合。Computational-geometry库可能本身就包含凸分解算法如Hertel-Mehlhorn算法。近似计算换取速度在某些对精度要求不高的场景如视野的粗略估计、远距离LOD细节层次判断可以使用更简单的几何近似。例如用多边形的最小包围圆或轴对齐包围盒AABB来进行快速但保守的碰撞检测只有在这个粗略检测通过后才进行精确的多边形相交测试。空间哈希与网格化对于大量小型、动态的几何体如数百个飞行的子弹可以将空间划分为均匀网格。每个几何体根据其位置注册到对应的网格单元格中。当需要检测某个区域时只需查询该区域覆盖的少数几个单元格内的对象极大地减少了需要两两检测的对象对数。5.2 内存与GC垃圾回收优化实战在Unity中C#的垃圾回收GC是导致帧率卡顿的常见元凶。计算几何算法容易产生大量的临时Vector2、ListT等托管对象。结构体Struct的使用确保库中的基础类型如Point、Segment是struct而非class。struct是值类型分配在栈上方法退出时自动回收不会增加GC压力。检查你使用的库如果关键类型是class考虑向作者反馈或寻找替代。对象池系统化不要只池化Polygon对象。为算法中频繁使用的ListVector2、Listint用于存储索引、HashSetEdge等集合类型也建立对象池。// 一个简单的ListVector2对象池示例 public static class Vector2ListPool { private static readonly StackListVector2 pool new StackListVector2(); public static ListVector2 Get() { return pool.Count 0 ? pool.Pop() : new ListVector2(64); // 预设容量 } public static void Release(ListVector2 list) { list.Clear(); // 清空内容而非销毁对象 pool.Push(list); } } // 使用 ListVector2 tempVertices Vector2ListPool.Get(); // ... 执行算法填充tempVertices ... Vector2ListPool.Release(tempVertices); // 用完归还重用输出容器许多库的函数签名是Polygon SomeOperation(Polygon a, Polygon b)这意味着每次调用都会返回一个新的Polygon对象。如果可能寻找或封装一个具有void SomeOperation(Polygon a, Polygon b, ref Polygon result)签名的版本允许你传入一个预先分配好的Polygon对象来存储结果避免分配新对象。Array代替List在性能极度敏感且大小固定的循环中使用数组Vector2[]代替ListVector2。数组的访问速度略快且没有List的边界检查开销在非开发版本中。但牺牲了灵活性。5.3 多线程与Job System的探索Unity的C# Job System和Burst编译器可以将计算密集型任务转移到多核CPU上并行执行极大提升性能。但将现有的计算几何代码移植到Job中需要小心。数据准备Job不能访问托管对象如ListT。你需要将几何数据顶点数组转换为原生的NativeArrayT。这本身会带来一次内存分配和复制开销因此只对数据稳定且计算量大的操作有意义。算法并行化并非所有几何算法都容易并行化。像Delaunay三角剖分中的某些增量算法其步骤间存在数据依赖难以并行。但像对大量独立多边形进行相同的布尔运算如每个角色计算自己的视野或者对一个大点集进行空间哈希网格的分配这些任务则是“令人尴尬的并行”非常适合Job System。Burst编译为你的Job结构体添加[BurstCompile]特性可以让Burst编译器将其编译为高度优化的本地代码获得数倍甚至数十倍的性能提升。确保你的Job代码只使用Burst支持的数据类型和数学函数如Unity.Mathematics中的float2,math命名空间下的函数。一个简单的想法是将需要处理的一批多边形数据准备好NativeArray然后调度一个并行执行的Job每个Job处理一个多边形对的布尔运算最后在主线程收集结果。这需要对库的源码有较深的理解和修改能力。6. 与其他Unity生态工具的整合方案一个强大的库不会孤立存在。将Computational-geometry与Unity的其他流行工具和资源结合能发挥出112的效果。6.1 与Shader Graph和VFX Graph的联动计算几何的结果可以驱动视觉效果。数据传递到Shader将生成的多边形顶点数据或Voronoi图的站点坐标通过MaterialPropertyBlock或自定义的Shader变量传递到材质球。在Shader中可以利用这些数据实现基于距离的区域染色、蜂窝状特效、动态溶解边缘等。例如将Voronoi单元格的站点位置和ID传入Shader在片段着色器中计算当前像素到各个站点的距离实现泰森多边形风格的着色。控制粒子系统使用计算几何生成的路径如平滑的贝塞尔曲线作为ParticleSystem的轨迹。或者用Voronoi单元格来控制粒子发射器的密度和范围实现非均匀的粒子分布效果。6.2 与Timeline和动画系统的结合让几何变化动起来。动画顶点通过对多边形顶点进行关键帧动画或程序化变换如噪声位移、正弦波可以创建动态变化的形状再结合上面的Mesh生成就能实现流动的液体边界、呼吸感的护盾等效果。Computational-geometry库负责提供初始形状和关键的布尔运算动画系统负责顶点的逐帧插值。Timeline控制你可以创建自定义的Playable Asset在Timeline轨道上控制几何参数的变化。例如在过场动画中用一个Timeline轨道控制视野多边形的半径和角度实现镜头视野的平滑缩放和旋转。6.3 在URP/HDRP渲染管线中的实践现代可编程渲染管线URP/HDRP提供了更多控制渲染细节的可能性。自定义渲染器特性你可以编写一个ScriptableRendererFeature在渲染流程的特定阶段如渲染不透明物体之后注入你的绘制逻辑。在这里你可以使用CommandBuffer来绘制由计算几何生成的、用于后期处理效果的几何图形如自定义的遮罩形状。体积光与遮罩文章开头热词中提到了“unity urp shader 体积光”。你可以用计算几何生成一个表示光线照射范围的体积形状一个锥体与场景几何体求交后的复杂多面体然后将这个形状的深度或模板信息写入缓冲区在自定义的体积光Shader中读取实现被几何体精确遮挡的体积光效果。这比简单的屏幕空间方法要精确得多。6.4 面向数据技术栈的适配随着Unity DOTS面向数据的技术栈的逐渐成熟高性能计算的需求日益增长。ECS与几何数据在ECS架构中几何数据顶点、边可以作为IComponentData存储在Entity上。你可以编写一个ISystem使用Burst编译的Job来并行处理成千上万个实体的几何运算。例如在一个大规模策略游戏中同时计算所有单位的视野区域。与Unity.Mathematics的协作Computational-geometry库的内部实现如果尚未使用Unity.Mathematics中的float2、float3和math函数你可以考虑在其外围封装一层适配器将Vector2/3转换为float2/3后再调用核心算法以便更好地与Burst和Jobs集成。长远来看一个原生基于Unity.Mathematics的计算几何库会是性能最优的选择。我个人在几个大型项目中深度使用这个库的经验是它的价值不仅仅在于提供了现成的算法更在于它建立了一种“几何思维”。当你遇到一个游戏逻辑问题时你会开始思考“这个问题能不能用一个多边形来表示能不能通过几何运算来解决” 这种思维转变往往能带来更优雅、更高效的解决方案。最后一个小建议是建立一个你自己的“几何工具集”脚本把项目中验证过的、常用的操作如快速创建矩形/圆形多边形、常用的裁剪流程、到Mesh/Collider的转换封装成静态函数这能极大提升你未来的开发效率。