Unity贝塞尔曲线解决方案:从数学原理到工程实践

发布时间:2026/7/25 7:16:11

Unity贝塞尔曲线解决方案:从数学原理到工程实践 1. 项目概述为什么我们需要一个专门的贝塞尔曲线解决方案在Unity里做游戏或者交互应用但凡涉及到平滑的路径、流畅的动画轨迹、或者需要用户自定义形状贝塞尔曲线几乎是一个绕不开的话题。Unity引擎本身提供了AnimationCurve和一些基础的Vector3.Lerp方法但对于需要动态生成、编辑、或者进行复杂数学运算比如求切线、法线、等距线、甚至曲线求交的贝塞尔曲线来说原生支持就显得有些力不从心了。你可能试过自己写一个贝塞尔曲线的类从二阶、三阶开始然后很快就会发现一旦需要处理多段连续曲线即样条、循环曲线、或者需要高效的路径跟随逻辑时代码会迅速变得臃肿且难以维护。这就是UnityBezierSolution这类第三方插件或开源方案存在的价值。它不是一个简单的工具函数集合而是一个系统性的解决方案。其核心目标是将贝塞尔曲线的数学原理封装成一套在Unity编辑器和运行时都高效、易用的组件系统。你可以把它理解为一个“贝塞尔曲线引擎”它负责处理从点的定义、曲线的绘制、到物体沿曲线运动、乃至动态修改曲线形状的所有底层逻辑。对于开发者而言我们不再需要反复造轮子去计算曲线上某个点的位置或者为每一段曲线手动编写插值代码而是可以像使用Transform组件一样通过拖拽和配置快速构建出复杂的路径系统。从网络热词的关联来看无论是“Unity地图”、“Unity Timeline”制作过场动画还是“Unity数字孪生”中构建平滑的巡视路径甚至是“Unity UI框架”中设计非线性的动画效果都离不开对曲线运动的精确控制。UnityBezierSolution正是为了满足这些高级且常见的开发需求而生的。它把复杂的数学问题变成了直观的编辑器操作和简洁的API调用。2. 贝塞尔曲线的数学原理从公式到直觉在深入代码之前我们必须先建立对贝塞尔曲线数学原理的直觉理解。很多教程一上来就抛出伯恩斯坦多项式虽然严谨但容易让人望而生畏。我们换个方式从最本质的“线性插值”开始逐步构建。2.1 一阶贝塞尔线性插值一切的起点一阶贝塞尔曲线就是连接两个点P0和P1的直线段。其上任意一点B(t)的位置由参数t0到1决定公式就是最简单的线性插值B(t) (1 - t) * P0 t * P1当t0时点在P0t1时点在P1。这很好理解它描述了一个点从P0匀速直线运动到P1的过程。这是所有高阶贝塞尔曲线的基础运算单元。2.2 二阶贝塞尔二次曲线引入控制点二阶贝塞尔曲线有三个点起点P0终点P2以及一个控制点P1。它的生成过程可以看作是对两条一阶贝塞尔线段进行递归的线性插值。首先在P0-P1线段上根据t找到点Q0Q0 Lerp(P0, P1, t)同时在P1-P2线段上根据t找到点Q1Q1 Lerp(P1, P2, t)最后在Q0-Q1线段上再次根据t进行插值得到最终点B(t)B(t) Lerp(Q0, Q1, t)将这个过程合并就得到了二阶贝塞尔曲线的显式公式B(t) (1-t)^2 * P0 2*t*(1-t) * P1 t^2 * P2这个公式中(1-t)^2,2*t*(1-t),t^2就是二阶伯恩斯坦基函数。控制点P1并不在曲线上但它像一块“磁铁”将直线P0-P2“拉”向自己从而形成弯曲的路径。P1离线段P0-P2越远曲线的弯曲程度就越大。注意理解这个递归插值德卡斯特里奥算法的过程至关重要。它不仅是理解贝塞尔曲线的钥匙其算法思想本身通过递归线性插值来构造复杂曲线也是UnityBezierSolution等库实现曲线求值、分割、升阶等操作的核心逻辑比直接使用多项式公式在数值上更稳定。2.3 三阶贝塞尔三次曲线更丰富的形态三阶贝塞尔曲线有四个点P0, P1, P2, P3。同理它可以通过对三条线段P0-P1, P1-P2, P2-P3进行三层递归线性插值得到。 其公式为B(t) (1-t)^3 * P0 3*t*(1-t)^2 * P1 3*t^2*(1-t) * P2 t^3 * P3三阶曲线拥有两个控制点P1和P2因此可以定义更复杂的曲线形态例如S形曲线。它也是图形学和UI设计中最常用的一种因为它在表达能力和计算复杂度之间取得了很好的平衡。Unity的AnimationCurve在内部可以理解为一系列三阶贝塞尔曲线段的拼接。2.4 高阶贝塞尔与德卡斯特里奥算法当阶数继续升高显式公式会变得非常复杂。但在实际编程中我们几乎从不使用高阶贝塞尔曲线的多项式公式进行计算。取而代之的是德卡斯特里奥算法。这个算法完美地对应了我们上面描述的递归线性插值过程将给定的控制点列表作为第一层。对于当前的参数t对相邻点进行线性插值得到点数减一的新一层点集。重复步骤2直到只剩下一个点这个点就是曲线在参数t处的位置。这个算法的优点是稳定、高效并且适用于任意阶数的贝塞尔曲线。UnityBezierSolution在计算曲线上点的位置、切线、法线时底层很可能就是基于此算法或其变种实现的。2.5 连续性构建复杂路径的关键单个贝塞尔曲线段的能力有限。要创建复杂的路径如一条蜿蜒的河流或角色的巡逻路线我们需要将多段贝塞尔曲线连接起来这就引入了连续性的概念。G0连续位置连续前一段曲线的终点就是后一段曲线的起点。这是最基本的要求。G1连续切线连续在连接点处前后两段曲线的切线方向相同。这保证了路径方向是平滑变化的没有尖角。在视觉上运动物体会平滑转向。C1连续在G1连续的基础上要求切线的模长即速度大小也相同。这对于需要匀速运动的场景很重要。更高阶连续C2曲率连续要求连接点处的曲率也相同这能保证加速度的平滑常用于汽车或相机运动等对运动质量要求极高的场合。UnityBezierSolution通常会提供工具来帮助开发者自动维护这些连续性。例如当你移动一个共享节点时插件可以自动调整相邻控制点的位置以保持G1或C1连续。3. UnityBezierSolution 核心架构与组件解析理解了数学原理我们来看UnityBezierSolution是如何将这些原理工程化封装成一套即插即用的Unity组件系统的。一个典型的UnityBezierSolution项目会包含以下几个核心部分3.1 BezierSpline曲线的骨架BezierSpline贝塞尔样条组件是系统的核心。它代表了一条完整的路径这条路径可以由多段贝塞尔曲线连接而成。你可以将它挂载在一个空的GameObject上。控制点Points样条由一系列控制点定义。在编辑器中这些点通常表现为场景视图中的可拖动小圆球Gizmo。每个控制点是一个Transform这意味着你可以利用Unity完整的变换体系位置、旋转、缩放来操控它也方便与其他对象关联。曲线段Segments每两个相邻的“节点”或称“锚点”之间构成一段贝塞尔曲线。一个拥有N个节点的样条拥有N-1段曲线。BezierSpline负责管理这些曲线段并保证它们之间的连续性。闭合Loop一个重要的布尔属性。当设置为true时样条的最后一个节点会自动与第一个节点连接形成一条闭合环路。这对于制作循环跑道或巡逻边界非常有用。插值模式虽然贝塞尔曲线本身是参数化的但UnityBezierSolution通常会提供额外的抽象层比如“均匀化”处理。因为贝塞尔曲线的参数t并不与曲线长度成正比在曲率大的地方t变化慢曲率小的地方t变化快。高级的样条组件会提供“按距离插值”的选项确保物体沿曲线匀速运动。3.2 BezierWalker沿着曲线运动有了路径下一步就是让物体动起来。BezierWalker组件就是干这个的。你将它挂载到需要沿路径运动的物体如敌人、相机、特效上然后将其Spline属性指向之前创建的BezierSpline。运动控制它提供了丰富的控制参数TravelMode: 可以是单向一次Once、往复PingPong或循环Loop。Speed: 运动速度。RotationMode: 物体如何旋转。可以是“无旋转”、“朝向切线方向”最常用让物体始终面朝前进方向、“朝向切线并保持上方向”等。归一化位置NormalizedTBezierWalker内部维护一个0到1之间的参数代表物体在整条样条上的位置。它负责将这个归一化的位置映射到具体的某一段曲线和该段曲线的局部参数t上然后调用BezierSpline的GetPoint和GetTangent方法来更新物体的Transform。3.3 编辑器扩展可视化与高效编辑UnityBezierSolution的强大一半体现在其编辑器工具上。优秀的可视化能极大提升开发效率。场景视图Gizmo绘制在Scene视图中样条会以平滑的线条绘制出来。控制点、切线手柄都会清晰可见并且支持直接点击拖拽。你可以实时看到修改控制点位置对曲线形状的影响。Inspector自定义面板BezierSpline的Inspector面板通常会集成添加/删除控制点、反转方向、均匀化控制点、设置自动切线等常用操作按钮。你还可以直接输入控制点的世界坐标进行精确调整。切线手柄对于三阶贝塞尔曲线每个节点有两个控制点前一个控制点影响入方向后一个控制点影响出方向。编辑器通常会提供类似Unity动画曲线那样的切线手柄允许你直观地调整曲线的弯曲度和连续性模式如折断、对齐、平滑。3.4 扩展组件与工具函数除了核心组件一个完整的解决方案还会提供一系列周边工具BezierCollider用于2D项目可以根据贝塞尔曲线动态生成EdgeCollider2D或PolygonCollider2D的轮廓让物理引擎能识别这条曲线。BezierMesh沿曲线挤出网格用于生成道路、管道、绳索等模型。采样与查询APIBezierSpline会暴露一系列关键方法供脚本调用GetPoint(float t): 获取曲线上参数t处的位置。GetTangent(float t): 获取该点的切线方向归一化向量。GetNormal(float t): 获取法线方向通常基于某个上向量计算得出。FindNearestPoint(Vector3 worldPos): 查找样条上距离给定世界坐标最近的点及其参数t。这对于实现“物体吸附到路径”或“路径寻路”功能至关重要。EvenlySpacedPoints(int resolution): 生成一系列在曲线上均匀分布的点。这是实现匀速运动或进行精确长度计算的基础。4. 核心代码实现深度剖析现在我们抛开现成的插件从零开始思考如何实现一个BezierSpline的核心功能。这能让我们真正理解UnityBezierSolution这类工具的内部机理。4.1 数据结构设计如何组织控制点首先我们需要定义存储结构。一个直观的设计是使用ListVector3来存储所有控制点。但对于三阶贝塞尔曲线每段曲线需要4个点P0, P1, P2, P3且P3是下一段的P0。为了便于编辑和保持连续性更常见的“节点-控制点”模型是一个ListVector3存储所有节点Anchor Points即曲线实际经过的点。每个节点关联两个控制点Handle一个控制入方向Handle In一个控制出方向Handle Out。 这样对于有N个节点的样条实际有N个节点和2N个控制点首尾节点的某个控制点可能未使用。在代码中我们可以定义一个BezierPoint类来封装一个节点及其两个控制点。[System.Serializable] public class BezierPoint { public Vector3 position; // 节点位置 public Vector3 handleIn; // 入方向控制点相对位置 public Vector3 handleOut; // 出方向控制点相对位置 public BezierPoint(Vector3 pos) { position pos; handleIn pos Vector3.left; // 默认控制点位置 handleOut pos Vector3.right; } }样条类则包含一个ListBezierPoint points。4.2 核心算法实现求值与求导有了数据结构接下来实现核心的GetPoint和GetTangent方法。这里我们采用德卡斯特里奥算法因为它通用、稳定。// 计算单段三阶贝塞尔曲线在参数t处的位置 public static Vector3 GetPoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { // 确保t在[0,1]区间 t Mathf.Clamp01(t); float oneMinusT 1f - t; // 三层线性插值德卡斯特里奥算法 Vector3 q0 Vector3.Lerp(p0, p1, t); Vector3 q1 Vector3.Lerp(p1, p2, t); Vector3 q2 Vector3.Lerp(p2, p3, t); Vector3 r0 Vector3.Lerp(q0, q1, t); Vector3 r1 Vector3.Lerp(q1, q2, t); return Vector3.Lerp(r0, r1, t); } // 计算单段三阶贝塞尔曲线在参数t处的切线一阶导数 // 切线方向即速度方向对t求导可得公式B(t) 3*(1-t)^2*(P1-P0) 6*(1-t)*t*(P2-P1) 3*t^2*(P3-P2) public static Vector3 GetFirstDerivative(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { t Mathf.Clamp01(t); float oneMinusT 1f - t; return 3f * oneMinusT * oneMinusT * (p1 - p0) 6f * oneMinusT * t * (p2 - p1) 3f * t * t * (p3 - p2); }在BezierSpline类中GetPoint方法需要先根据全局的t0到1判断它属于哪一段曲线计算出段索引i和段内局部参数localT然后调用上述单段函数。4.3 均匀化Uniform Sampling与弧长参数化这是实现匀速运动的关键也是难点。贝塞尔曲线的参数t与曲线长度不是线性关系。UnityBezierSolution通常会在内部预先计算一个“弧长表”。预计算在曲线初始化或控制点改变时以很高的分辨率例如每段采样100个点计算曲线上多个点的位置并累加相邻点之间的距离得到一个近似总长度L和一个映射表。这个表记录了(参数t, 累计长度s)的对应关系。查询当需要根据长度s或匀速运动下的时间来获取位置时就在这个弧长表中进行二分查找找到s所在区间然后用线性插值反推出对应的参数t最后再用这个t去调用GetPoint。 这个过程虽然增加了初始化开销但保证了运行时查询的高效和运动的均匀性。BezierWalker组件内部就依赖于此。4.4 编辑器代码让曲线“活”起来编辑器代码让我们的工具变得可用。关键是在OnSceneGUI方法中绘制Gizmo并处理交互。[CustomEditor(typeof(BezierSpline))] public class BezierSplineEditor : Editor { private const float handleSize 0.04f; private const float pickSize 0.06f; private BezierSpline spline; private Transform handleTransform; private Quaternion handleRotation; private int selectedIndex -1; private void OnSceneGUI() { spline target as BezierSpline; handleTransform spline.transform; handleRotation Tools.pivotRotation PivotRotation.Local ? handleTransform.rotation : Quaternion.identity; // 1. 绘制曲线 Vector3 lineStart spline.GetPoint(0f); Handles.color Color.white; int steps spline.CurveCount * 20; for (int i 1; i steps; i) { Vector3 lineEnd spline.GetPoint(i / (float)steps); Handles.DrawLine(lineStart, lineEnd); lineStart lineEnd; } // 2. 绘制控制点并处理拖动 for (int i 0; i spline.pointCount; i) { Vector3 pointWorldPos handleTransform.TransformPoint(spline.GetPointPosition(i)); float size HandleUtility.GetHandleSize(pointWorldPos); // 绘制一个可点击拖动的按钮 if (Handles.Button(pointWorldPos, handleRotation, size * handleSize, size * pickSize, Handles.DotHandleCap)) { selectedIndex i; Repaint(); // 刷新Inspector面板 } if (selectedIndex i) { // 使用Handle工具进行位置编辑 EditorGUI.BeginChangeCheck(); pointWorldPos Handles.DoPositionHandle(pointWorldPos, handleRotation); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(spline, Move Point); spline.SetPointPosition(i, handleTransform.InverseTransformPoint(pointWorldPos)); } } } } }这段简化的代码展示了如何在Scene视图中绘制曲线和可交互的控制点。完整的实现还会包括切线手柄的绘制、连续性的自动维护、以及更多工具按钮。5. 实战应用从简单移动到复杂系统掌握了原理和组件我们来看看UnityBezierSolution能如何应用到真实项目中。5.1 基础应用相机漫游与敌人巡逻这是最直接的应用。为相机创建一个BezierSpline勾勒一条你希望相机运动的路径然后挂上BezierWalker组件。调整速度、循环模式一个平滑的过场动画或漫游镜头就完成了。对于敌人巡逻你可以设置TravelMode为PingPong敌人就会在路径上来回巡逻。通过调整控制点你可以轻松设计出绕柱、爬坡、俯冲等复杂动作而无需编写复杂的向量运算代码。5.2 进阶应用动态路径与程序化内容UnityBezierSolution的强大之处在于其程序化访问能力。动态生成路径你可以在运行时通过代码AddPoint、InsertPoint来动态构建或修改路径。例如在一个塔防游戏中可以根据地图数据实时生成敌人行进路线在一个赛车游戏中可以根据玩家的行驶轨迹动态记录并生成一条“幽灵赛车”的路径。路径跟随与偏移GetPoint和GetTangent给了你曲线上任意点的位置和方向。你可以利用这些信息做更多事。比如让一个物体不仅沿着路径中心走还可以根据某种规则进行横向偏移——想象一下《神庙逃亡》中角色在一条宽度有限的路径上左右移动这本质上就是在路径的切线/法线平面上进行偏移。// 示例在路径法线方向上进行偏移 Vector3 pathPos spline.GetPoint(t); Vector3 tangent spline.GetTangent(t).normalized; Vector3 normal Vector3.Cross(tangent, Vector3.up).normalized; // 假设上方向为世界Y轴 Vector3 finalPos pathPos normal * lateralOffset;基于路径的Mesh生成结合BezierMesh或自己编写网格生成代码你可以用贝塞尔曲线定义一条脊线然后沿着这条线挤出横截面从而生成弯曲的管道、绳索、道路或自定义形状的模型。这对于需要大量程序化生成环境的游戏如赛道、河流非常有用。5.3 与其他系统集成与Timeline集成你可以编写一个PlayableAsset在Timeline剪辑中控制BezierWalker的NormalizedT属性从而将路径运动无缝嵌入到复杂的过场动画序列中与其他动画、音频剪辑同步。与物理系统集成通过BezierCollider或自己根据曲线生成碰撞体可以让物理对象与弯曲的路径发生交互。例如制作一个弯曲的滑梯让小球沿着它滑落。与粒子系统集成将曲线的GetPoint方法作为粒子发射器或力场影响的位置来源可以创建出沿特定轨迹流动的粒子效果如魔法光束、水流轨迹等。6. 性能优化与最佳实践在项目中使用贝塞尔曲线尤其是复杂的样条需要注意性能。6.1 性能瓶颈分析实时采样计算如果在Update中频繁调用GetPoint进行高分辨率采样例如用于每帧绘制且曲线段数很多可能会造成CPU压力。德卡斯特里奥算法本身是O(n)复杂度n为阶数对于三阶曲线是常数时间但采样次数多了依然可观。弧长表重建如果曲线是动态变化的控制点被脚本频繁修改那么每次变化后都需要重建弧长表这是一个O(分辨率*段数)的操作可能成为帧率杀手。Gizmo绘制在编辑器下OnSceneGUI中高分辨率的Handles.DrawLine调用在曲线非常复杂时会影响场景视图的流畅度。6.2 优化策略缓存缓存还是缓存对于静态或变化不频繁的曲线一定要在Awake或Start中预计算好弧长表、均匀采样点等数据避免在Update中进行重复计算。降低运行时采样分辨率对于运动物体BezierWalker每帧可能只需要采样一次。但对于需要实时绘制曲线如游戏中的可见路径指示的情况要权衡视觉效果和性能适当降低绘制时的采样步数。动态曲线更新策略如果曲线必须动态变化考虑以下方案脏标记只有当控制点真正发生改变时才触发弧长表的重建。增量更新如果只是局部微调是否可以只更新受影响曲线段的弧长表部分异步计算如果重建开销实在太大可以考虑将计算放到子线程或分帧进行但要注意数据同步。编辑器优化在OnSceneGUI中可以通过Event.current.type来判断当前是重绘事件还是交互事件。在仅仅是重绘时可以使用精度稍低的绘制或者提供一个“简化显示”的选项。对象池与复用在需要实例化大量沿相同路径运动物体的场景如一波敌人让它们共享同一个BezierSpline的引用和计算好的数据而不是每个物体都持有一份拷贝。6.3 设计最佳实践保持曲线简洁用尽可能少的控制点来表达所需的形状。过多的控制点不仅难以编辑也会增加计算负担。通常用多段低阶三阶曲线连接比用一段高阶曲线要好。善用自动切线在编辑器中对于需要平滑过渡的节点使用“对齐”或“平滑”的切线模式让插件自动计算控制点位置可以快速获得光顺的曲线避免手动调整的繁琐。参数化设计在设计BezierWalker这类组件时将速度、循环模式、旋转模式等属性暴露给设计师在Inspector中调整而不是硬编码在脚本里。这能提升工作流效率。编写辅助工具根据项目需求编写一些自定义的编辑器工具。例如一个“将SpriteRenderer顶点转换为贝塞尔曲线”的工具或者一个“将样条等距离分割并生成路点”的工具能极大提升美术和策划的工作效率。7. 常见问题与调试技巧在实际使用中你肯定会遇到一些“坑”。这里记录一些典型问题和解决方法。7.1 运动不匀速或卡顿症状物体沿曲线运动时忽快忽慢或在某些段落感觉“卡顿”。排查检查是否启用了弧长参数化确保BezierWalker或你自定义的运动逻辑使用的是基于弧长或称为“均匀化”的插值而不是原始的曲线参数t。这是最常见的原因。检查弧长表分辨率如果启用了弧长参数化但运动仍有轻微的不均匀可能是预计算的采样点数量不足。尝试增加BezierSpline的Resolution或类似名称参数。检查帧率与DeltaTime确保运动计算正确乘以了Time.deltaTime以保证运动速度与帧率无关。7.2 曲线在编辑器中显示异常症状曲线不显示、显示为直线或显示破碎。排查检查控制点数据确认控制点List不为空且包含有效数据。有时序列化问题可能导致数据丢失。检查Gizmo绘制代码在OnSceneGUI中确保用于连接线段的lineStart和lineEnd计算正确并且绘制循环没有越界。检查坐标系变换如果你将样条放在了有缩放或旋转的父物体下在GetPoint计算和Gizmo绘制时需要正确处理本地坐标与世界坐标的转换。确保在计算曲线点时使用了Transform.TransformPoint在绘制时使用了正确的矩阵。7.3 物体旋转方向错误症状使用BezierWalker的“朝向切线”模式时物体旋转轴不对例如倒着走或侧着走。排查理解切线方向贝塞尔曲线的切线B(t)是曲线在该点的瞬时速度方向它指向参数t增加的方向。这通常是物体应该面对的正前方。检查旋转计算Quaternion.LookRotation(forward, up)方法需要两个参数前方向量和上方向量。通常我们将归一化的切线作为forward。问题往往出在up向量上。如果使用Vector3.up作为全局上方向当曲线垂直或倒置时旋转就会出错。更稳健的方法是使用“最小扭转”算法或者根据前一帧的旋转平滑过渡。一些高级的BezierWalker实现会提供RotationMode选项来处理这个问题。模型初始朝向确认你的3D模型本身的初始朝向在建模软件中是沿着哪个轴通常是Z轴正方向。LookRotation默认假设模型前向是Vector3.forward(0,0,1)。如果不匹配可能需要在旋转后额外乘一个修正旋转。7.4 动态修改曲线导致的问题症状在运行时通过代码添加、删除或移动控制点后运动物体错位或报错。排查索引越界这是最危险的错误。当你在运动过程中修改了控制点数组之前存储的段索引i或参数t可能失效。在修改曲线结构增删点后必须重置所有BezierWalker的状态如将其NormalizedT重新计算或归零或者确保你的GetPoint方法能安全地处理失效的索引例如Clamp到有效范围。数据同步如果曲线数据在多个地方被修改例如同时有脚本和动画在修改控制点位置需要确保修改是线程安全的如果涉及多线程并且能正确触发弧长表等缓存数据的更新。使用事件通知一个良好的设计是在BezierSpline中定义OnSplineChanged这样的事件。当控制点被修改时触发该事件。所有依赖样条的组件如Walker、Collider、Mesh都订阅这个事件并在事件触发时更新自己的状态。这能有效解耦和避免状态不一致。7.5 内存与序列化问题症状场景保存后重新打开曲线数据丢失或者预制体实例化后曲线数据共享。排查确保数据可序列化BezierPoint类或存储控制点的数据结构必须标记为[System.Serializable]并且其所有字段都是Unity可序列化的类型如Vector3,float, 其他可序列化类。注意引用类型如果使用ListBezierPoint要明白BezierPoint如果是类class那么它的实例是引用。在复制预制体或通过Instantiate实例化时如果直接赋值可能会出现多个样条组件共享同一个List引用的情况修改一个会影响到所有。解决方案通常是在Awake或OnValidate中深度复制一份数据或者将BezierPoint改为结构体struct但结构体在编辑数组元素时有些不便需要根据情况权衡。

相关新闻