Unity动态绳索模拟:基于Verlet积分与Line Renderer的实现

📅 2026/8/7 15:13:56
Unity动态绳索模拟:基于Verlet积分与Line Renderer的实现
1. 项目概述从一根静态的线到一根有灵魂的绳子在Unity里画一条线用Line Renderer组件这大概是每个Unity开发者都做过的事情。但如果你想让这条线不再是一条僵硬的、数学意义上的线段而是变成一根有物理感的、会晃动的绳子比如连接两个在场景中移动的物体模拟吊桥的缆绳、钟摆的链条或者角色手中的鞭子事情就变得有趣起来了。这不仅仅是视觉上的美化更是游戏交互和物理沉浸感的关键一环。我最近在做一个需要动态绳索连接的游戏原型Unity自带的物理关节如Hinge Joint, Spring Joint虽然能模拟连接但视觉效果上就是两个物体硬生生地连在一起中间是“隐形”的力。玩家需要看到那根实实在在的、会随着运动弯曲、晃动的绳子。这就是我们这次要解决的问题如何用Unity 2022的Line Renderer结合一点简单的物理模拟创造出一根能实时连接两个移动物体、并拥有自然晃动效果的动态绳子。这个方案的核心思路是我们不依赖Unity的物理引擎去模拟整根绳子的碰撞那会非常消耗性能而是用质点-弹簧模型Verlet积分来模拟绳子的形态再用Line Renderer将这个形态“画”出来。这种方法性能高效效果可控非常适合游戏中的绳索、锁链、鞭子等效果。下面我就把从思路拆解到完整脚本实现的整个过程包括我踩过的坑和优化技巧毫无保留地分享给你。2. 核心思路与方案选型为什么是Verlet积分当我们提到“会晃动的绳子”本质上是在模拟一段柔软的、可弯曲的、有弹性的连续体。在游戏开发中完全真实的物理模拟如有限元分析是不现实的。我们通常采用离散化的方法把连续的绳子看成是一串首尾相连的质点。2.1 常见物理模拟方案对比使用Unity物理关节链为绳子的每个分段创建GameObject并添加Rigidbody和关节如Character Joint连接。这是最“物理”的方法。优点与场景物理交互真实能产生碰撞。缺点性能开销巨大每个质点都是一个GameObject稳定性调试复杂容易产生剧烈抖动或断裂控制不够灵活。使用粒子系统Particle System通过设置粒子的渲染模式为Strip来模拟线条。优点性能通常不错易于实现拖尾等效果。缺点对绳子形态的精确控制比较困难物理模拟逻辑需要额外编写与游戏逻辑耦合度较高。使用Line Renderer 自定义物理更新这正是我们要采用的方法。我们将绳子抽象为一组在内存中计算的质点Vector3位置数组用自定义的算法更新这些质点的位置最后将更新后的位置数组赋值给Line Renderer。优点性能极佳只有一个GameObject和一个Line Renderer组件控制粒度细可以实现各种自定义效果如弹性、风阻、切割与游戏逻辑解耦。缺点需要自己实现物理模拟逻辑不与Unity的碰撞体直接交互但可以通过射线检测等方式间接实现。对于我们“连接两个移动物体”的需求方案3无疑是最佳选择。我们只需要关注连接点两个物体和中间点的运动即可。2.2 Verlet积分简单而强大的位置更新法则在自定义物理更新中Verlet积分算法因其数值稳定性和能量守恒性在无阻尼情况下而被广泛用于绳索、布料模拟。它最大的特点是不直接存储速度而是通过上一帧和当前帧的位置差来隐式地表达速度。其核心公式非常简单新位置 当前位置 (当前位置 - 旧位置) 加速度 * 时间步长²让我们拆解一下(当前位置 - 旧位置)这一项实际上就是速度 * 时间步长的近似。它代表了物体基于之前运动的惯性。加速度 * 时间步长²这是外力如重力在本帧内产生的影响。为什么它适合绳子模拟因为它计算出的运动非常平滑并且很容易加入约束条件——比如我们可以强制要求相邻两个质点之间的距离保持固定这就是绳子的“不可拉伸”约束通过迭代调整质点的位置来满足这个约束从而模拟出绳子的张力和摆动。我们的方案流程图可以概括为每一帧 1. 更新绳子两端质点的位置使其锁定到两个目标物体上。 2. 对中间的所有质点应用Verlet积分计算其新位置主要受重力影响。 3. 进行多次迭代对每一对相邻质点应用“距离约束”让它们保持固定长度。 4. 将最终计算出的所有质点位置数组设置给Line Renderer。 5. 可选处理碰撞或其他外力。3. 组件与场景搭建在开始写代码之前我们需要在Unity编辑器中搭建一个简单的测试场景。这里我建议使用Unity 2022.3 LTS或更高版本以确保API的兼容性。3.1 创建基础场景对象创建空物体作为绳子管理器在场景中创建一个空GameObject命名为RopeManager。这个物体将承载我们编写的核心脚本。创建两个移动目标创建两个Cube或其他任何物体分别命名为StartTarget和EndTarget。为它们添加Rigidbody组件并取消勾选Use Gravity为了避免它们掉出视野方便我们通过脚本或手动拖动来模拟移动。添加Line Renderer组件在RopeManager物体上点击Add Component搜索并添加Line Renderer组件。3.2 配置Line Renderer基础属性初始的Line Renderer可能只是一条白色的细线。我们需要对其进行配置以达到基本的绳子视觉效果。材质Material这是最关键的一步。Line Renderer默认使用的材质可能不适用于所有渲染管线。在Unity内置渲染管线Built-in RP中你可以使用Sprites/Default着色器。在URPUniversal Render Pipeline中你需要一个使用Unlit或Simple Lit着色器并支持TEXTURE2D输入的材质。一个简单的方法是在Project窗口右键 - Create - Material命名为RopeMaterial。选中它在Inspector面板将Shader改为Universal Render Pipeline/Unlit。然后将这个材质拖到Line Renderer的Materials列表的Element 0上。宽度Width在Width属性下你可以通过曲线控制绳子各处的宽度。为了简单我们设置一个恒定宽度。点击Width曲线区域将曲线类型改为Constant常数然后将值设为0.1。你也可以通过startWidth和endWidth直接设置0.1f。顶点Positions暂时不用管我们的脚本会动态设置positionCount和每个顶点的位置。使用世界坐标Use World Space务必勾选此项。因为我们模拟的质点位置是在世界坐标系中计算的Line Renderer也需要在世界坐标系中绘制线条。循环Loop不勾选。我们的绳子是连接两端的开放线段。纹理模式Texture Mode选择Tile平铺可以让绳子的纹理沿着长度方向重复适合表现绳索的编织感。选择Stretch拉伸则会将一整张纹理拉伸到整个绳子长度。根据你的纹理资源决定。一个快速检查清单[ ]RopeManager物体上有Line Renderer组件。[ ]Line Renderer的Material已正确赋值非粉红色。[ ]Width设置为一个合适的值如 0.05 - 0.2。[ ]Use World Space已勾选。[ ]StartTarget和EndTarget物体已就位。4. 核心脚本实现RopeSimulator 详解接下来是重头戏编写驱动这根绳子的C#脚本。我们创建一个名为RopeSimulator.cs的脚本并将其挂载到RopeManager物体上。4.1 变量定义与初始化首先我们需要定义一系列可配置的参数和内部状态变量。using UnityEngine; public class RopeSimulator : MonoBehaviour { [Header(绳子参数)] public Transform startPoint; // 绳子起始端连接的物体 public Transform endPoint; // 绳子末端连接的物体 public int segmentCount 20; // 绳子的分段数质点数量。越多越平滑性能开销越大。 public float ropeLength 5f; // 绳子的自然长度米 public float segmentLength ropeLength / (segmentCount - 1); // 每个分段的长度 [Header(物理参数)] public Vector3 gravity new Vector3(0f, -9.81f, 0f); // 模拟重力 public int constraintIterations 50; // 距离约束的迭代次数。越高绳子越“硬”越不容易拉伸。 public float damping 0.98f; // 速度阻尼用于模拟空气阻力使摆动逐渐停止。 [Header(调试与可视化)] public bool drawGizmos true; // 是否在Scene视图绘制质点Gizmo // 内部状态数组 private Vector3[] _positions; // 当前帧质点位置 private Vector3[] _prevPositions; // 上一帧质点位置 private LineRenderer _lineRenderer; void Start() { // 获取或添加LineRenderer组件 _lineRenderer GetComponentLineRenderer(); if (_lineRenderer null) { _lineRenderer gameObject.AddComponentLineRenderer(); } // 初始化位置数组 _positions new Vector3[segmentCount]; _prevPositions new Vector3[segmentCount]; // 初始化绳子为一条连接startPoint和endPoint的直线 InitializeRopePositions(); // 将初始位置也赋给上一帧位置防止第一帧出现巨大速度 _positions.CopyTo(_prevPositions, 0); // 配置LineRenderer _lineRenderer.positionCount segmentCount; UpdateLineRenderer(); } // 初始化绳子位置为两点间的直线 private void InitializeRopePositions() { if (startPoint null || endPoint null) { Debug.LogError(请为RopeSimulator指定Start Point和End Point); return; } Vector3 startPos startPoint.position; Vector3 endPos endPoint.position; for (int i 0; i segmentCount; i) { float t i / (float)(segmentCount - 1); // 0到1的插值 _positions[i] Vector3.Lerp(startPos, endPos, t); } } }关键点解析segmentCount这个值决定了绳子的视觉平滑度和物理精度。值太小如5绳子看起来会有明显的棱角值太大如100计算量会增加可能影响性能。对于大多数情况15-30是一个不错的范围。constraintIterations这是控制绳子“硬度”和模拟稳定性的最重要参数之一。Verlet积分后的位置可能会违反“固定长度”的约束。我们需要多次迭代地修正所有相邻质点的位置使其距离回归segmentLength。迭代次数越多修正越彻底绳子越不容易被拉长但计算量也越大。通常需要根据绳子长度和运动剧烈程度在10-50之间调整。damping每次位置更新后我们对(当前位置 - 旧位置)这项乘以一个小于1的阻尼系数相当于给速度施加了一个衰减。没有阻尼绳子会永远摆动下去阻尼太小摆动停止很慢阻尼太大接近1绳子会显得沉重、不自然。0.98到0.995是常见的范围。InitializeRopePositions在Start时我们将所有质点均匀地插值在起点和终点之间形成一条直线。这是绳子的初始状态。4.2 物理模拟更新Verlet积分与约束求解核心逻辑在FixedUpdate中执行因为物理模拟最好以固定的时间步长进行这样行为更可预测。void FixedUpdate() { if (startPoint null || endPoint null) return; Simulate(); // 步骤12应用Verlet积分 for (int i 0; i constraintIterations; i) { ApplyConstraint(); // 步骤3多次应用约束 } UpdateLineRenderer(); // 步骤4更新视觉 } // 步骤12根据Verlet积分更新中间质点的位置 private void Simulate() { // 首先处理绳子两端它们的位置完全由目标物体决定 _positions[0] startPoint.position; _positions[segmentCount - 1] endPoint.position; // 计算固定时间步长用于Verlet积分。使用Time.fixedDeltaTime保证稳定性。 float deltaTimeSqr Time.fixedDeltaTime * Time.fixedDeltaTime; // 然后更新中间所有质点的位置 for (int i 1; i segmentCount - 1; i) // 跳过首尾 { Vector3 currentPos _positions[i]; // Verlet积分核心公式新位置 当前位置 (当前位置 - 旧位置) * 阻尼 加速度 * 时间步长² Vector3 velocity (currentPos - _prevPositions[i]) * damping; _prevPositions[i] currentPos; // 在覆盖当前位置前保存为“旧位置” _positions[i] currentPos velocity gravity * deltaTimeSqr; } } // 步骤3应用距离约束确保相邻质点间距等于segmentLength private void ApplyConstraint() { // 约束1连接两端的物体首尾质点位置已被Simulate()锁定 // 约束2中间质点间的距离约束 for (int i 0; i segmentCount - 1; i) { Vector3 point1 _positions[i]; Vector3 point2 _positions[i 1]; float currentDistance Vector3.Distance(point1, point2); float distanceError currentDistance - segmentLength; if (Mathf.Abs(distanceError) 0.0001f) // 避免对微小误差进行不必要的计算 { Vector3 correctionDir (point2 - point1).normalized; float halfCorrection distanceError * 0.5f; // 将误差平分到两个质点上 // 移动两个质点使它们靠近或远离以修正距离 // 注意第一个和最后一个质点i0 或 i1segmentCount-1可能被锁定需要特殊处理 if (i ! 0) // 如果point1不是起点 { _positions[i] correctionDir * halfCorrection; } if (i 1 ! segmentCount - 1) // 如果point2不是终点 { _positions[i 1] - correctionDir * halfCorrection; } } } } // 步骤4将计算好的位置数组传递给Line Renderer private void UpdateLineRenderer() { _lineRenderer.SetPositions(_positions); }代码逻辑深度解析Simulate()方法首先强制将绳子首尾两个质点的位置设置为两个目标物体的位置。这是实现“连接”的关键。对于中间的每一个质点我们应用Verlet积分公式。注意damping因子乘在了速度项(currentPos - _prevPositions[i])上这模拟了能量损失如空气阻力。在更新_positions[i]之前我们将它的当前值保存到_prevPositions[i]。这个顺序很重要它确保了“旧位置”是上一帧的位置。ApplyConstraint()方法我们遍历每一对相邻的质点(i, i1)。计算它们当前的距离currentDistance并与期望的距离segmentLength比较得到distanceError误差。如果误差显著大于一个极小阈值我们就需要修正。修正的方向是沿着两点连线的方向correctionDir。关键技巧误差平分。我们将误差的一半halfCorrection用于移动第一个质点另一半用于反向移动第二个质点。这样能更稳定地将距离拉回期望值避免质点“抖动”。边界处理当修正涉及到绳子两端的质点索引0或segmentCount-1时我们不能移动它们因为它们的位置被目标物体锁定了。所以通过if条件判断只移动非锁定的质点。这意味着当目标物体移动时是距离约束力在“拉动”绳子的其他部分跟随运动从而产生了晃动的效果。迭代的重要性在FixedUpdate中我们对ApplyConstraint()进行了constraintIterations次循环。为什么需要多次因为修正一对质点的距离可能会影响到它相邻质点的距离。例如修正了质点1和2的距离后质点2的位置变了这又导致质点2和3的距离出错。通过多次全局迭代可以逐渐将误差传递并消除到整个绳子上最终使所有分段都近似满足长度约束。迭代次数越多最终结果越精确绳子显得越“硬”。4.3 场景视图调试与优化为了方便在编辑器中调试绳子的物理状态我们可以实现OnDrawGizmos方法如果勾选了drawGizmos。// 在Scene视图中绘制质点的Gizmo便于调试 private void OnDrawGizmos() { if (!drawGizmos || _positions null) return; Gizmos.color Color.cyan; for (int i 0; i _positions.Length; i) { Gizmos.DrawSphere(_positions[i], 0.05f); // 绘制质点小球 if (i _positions.Length - 1) { Gizmos.DrawLine(_positions[i], _positions[i 1]); // 绘制分段连线 } } // 用黄色标记起点和终点 if (startPoint ! null) { Gizmos.color Color.yellow; Gizmos.DrawWireSphere(startPoint.position, 0.1f); } if (endPoint ! null) { Gizmos.color Color.yellow; Gizmos.DrawWireSphere(endPoint.position, 0.1f); } }此外我们还可以添加一些优化和增强功能[Header(高级选项)] public bool enableCollision false; // 是否启用简单碰撞检测 public LayerMask collisionMask; // 可碰撞的层 public float collisionRadius 0.1f; // 质点的碰撞半径 private void Simulate() { // ... [原有代码] ... for (int i 1; i segmentCount - 1; i) { // ... [原有Verlet积分代码] ... // 简单碰撞检测球形检测 if (enableCollision) { HandleCollision(i); } } } private void HandleCollision(int pointIndex) { RaycastHit hit; // 从上一帧位置到当前位置发射一个微小的射线检测是否与场景碰撞体相交 if (Physics.SphereCast(_prevPositions[pointIndex], collisionRadius, (_positions[pointIndex] - _prevPositions[pointIndex]).normalized, out hit, Vector3.Distance(_prevPositions[pointIndex], _positions[pointIndex]), collisionMask)) { // 如果发生碰撞将质点位置推到碰撞点表面并沿法线方向弹开一点 _positions[pointIndex] hit.point hit.normal * collisionRadius; // 也可以在这里计算一个简单的反弹速度更新_prevPositions来模拟反弹 // 一个简单的实现将当前位置相对于碰撞面对称反射 Vector3 reflected Vector3.Reflect(_positions[pointIndex] - hit.point, hit.normal); _positions[pointIndex] hit.point reflected.normalized * collisionRadius; } }注意这里的碰撞检测是一个非常简化的版本。在实际项目中你可能需要更精确的连续碰撞检测CCD或更高效的空间划分算法来处理大量绳子的碰撞否则性能可能成为瓶颈。对于大多数装饰性或功能简单的绳子关闭碰撞是完全可以接受的。5. 参数调优与效果打磨脚本写好了但直接运行可能效果并不理想——绳子可能像橡皮筋一样抖动得太厉害或者像铁链一样僵硬。这就需要我们根据实际需求调整参数。下面是一个参数调优指南参数作用调优方向与技巧segmentCount(分段数)决定绳子的视觉平滑度和物理精度。增加绳子更平滑弯曲更自然但计算量增大。对于静态或慢速移动的绳子15-20足够对于需要快速挥舞的鞭子可能需要25-35。注意分段长度(segmentLength)会随之变化。constraintIterations(约束迭代次数)控制绳子抵抗拉伸的“硬度”和模拟稳定性。增加绳子更不容易被拉长形态更稳定摆动更“紧实”。但过高的值如100可能使绳子过于僵硬且消耗CPU。通常从20开始如果发现绳子被快速拉动时像橡皮筋一样过度拉伸就增加到30-50。如果绳子运动平缓10-20即可。damping(阻尼)模拟空气阻力使摆动能量逐渐衰减。减小(如0.95)能量损失快绳子摆动几下就停了感觉“沉重”。增大(如0.998)能量损失慢绳子会长时间摆动感觉“轻盈”或“诡异”。推荐0.98-0.99能模拟出比较自然的摆动衰减。gravity(重力)影响绳子下垂的程度和摆动方向。默认的(0, -9.81, 0)是真实重力。你可以调小它如(0, -2, 0)让绳子感觉更轻或者调大它让绳子下垂感更强。甚至可以改变方向来模拟水下或有风的环境。ropeLength(绳子长度)绳子的自然静止长度。这个长度是当起点和终点距离小于此长度时绳子呈现的松弛长度。当两点距离大于此长度时绳子会被拉直。调整它会影响绳子的初始张力和摆动幅度。实操心得 调参时不要一次性改动多个参数。先固定其他参数只调整constraintIterations和damping观察绳子在目标物体突然移动后的摆动和静止过程。理想的绳子应该在受拉时迅速绷紧但有轻微弹性摆动时有适当的惯性且能平缓停止。如果出现高频率的微小抖动可能是迭代次数不够或阻尼太小如果绳子反应迟钝可能是阻尼太大。6. 常见问题与排查技巧实录在实际使用中你几乎一定会遇到下面这些问题。这里是我踩过坑后的解决方案。6.1 绳子抖动/抽搐Jitter这是最常见的问题表现为绳子不自然地高频震动。原因1约束迭代次数不足。当目标物体快速移动时一次约束计算无法将巨大的距离误差分摊到所有质点上导致部分分段被过度拉伸在下一次迭代中又猛烈回弹。解决逐步增加constraintIterations每次加5直到抖动明显减轻。但要注意性能。原因2FixedUpdate频率与渲染帧率不匹配。FixedUpdate默认每秒运行50次0.02s间隔。如果你的游戏帧率很高如120FPS每渲染2-3帧才进行一次物理更新绳子运动看起来就会不连贯。解决在Project Settings - Time中适当增加Fixed Timestep如从0.02降到0.01但这会增加物理计算负荷。更优的方案是如果绳子渲染是纯视觉效果可以考虑在Update中调用Simulate但使用Time.deltaTime并乘以一个缩放系数来保证模拟速度稳定不过这需要更小心地处理时间相关计算。原因3数值不稳定。Verlet积分对时间步长敏感。如果Time.fixedDeltaTime不稳定或者重力等参数过大可能导致计算发散。解决确保在FixedUpdate中调用模拟逻辑。检查gravity值是否合理通常y轴为负。可以尝试引入一个更小的“软化”系数到约束计算中不完全修正到精确长度而是修正一部分例如只修正distanceError * 0.8f。6.2 绳子被过度拉伸即使增加了迭代次数绳子在剧烈拉扯下中间部分仍像橡皮筋。原因ApplyConstraint中的误差平分策略在端点被锁定时修正力只能从一端传递。当拉力极大时中间的质点来不及被“拉”过去。解决这是一个固有局限。可以尝试的优化方案动态迭代次数根据绳子当前被拉伸的比例动态增加constraintIterations。更激进的约束在ApplyConstraint中不只修正相邻质点也修正相隔一个质点的距离即i和i2这能更快地将张力传递到整个绳子但会让绳子变得更“硬”。接受一定拉伸在视觉上绳子轻微拉伸是合理的。你可以通过LineRenderer的widthCurve让绳子在被拉伸时变细营造出“绷紧”的视觉效果转移玩家的注意力。6.3 绳子穿透其他物体因为我们没有启用复杂的碰撞检测。解决启用并配置简单碰撞勾选脚本的enableCollision并设置合理的collisionMask和collisionRadius。注意这只是一个近似解决方案对于快速移动的绳子可能仍有穿透。使用触发器将绳子质点视为触发器通过OnTriggerEnter/Stay事件来处理交互逻辑如切割绳子、触发机关而不阻止其运动。后期处理如果绳子只是环境装饰穿透问题可以忽略。或者将绳子渲染在特定层级使其总是在其他物体前面。6.4 性能问题当场景中有大量绳子如上百段时。优化减少segmentCount在视觉可接受的范围内使用最少的分段。降低constraintIterations这是性能消耗的大头。尝试找到效果和性能的平衡点。按需更新如果绳子在屏幕外或静止不动可以跳过物理模拟和渲染更新。通过OnBecameVisible/OnBecameInvisible或自己计算视锥体来判断。使用Job System和Burst Compiler对于高级用户可以将质点的位置计算和约束求解移植到C# Job中利用多核并行计算能极大提升性能。这是Unity DOTS面向数据的技术栈的典型应用场景。6.5 Line Renderer显示异常绳子不显示、显示为粉色材质丢失、或宽度异常。检查清单材质确认Line Renderer上的Material是否有效。粉色意味着材质或着色器丢失。确保使用了正确的ShaderBuilt-in用Sprites/DefaultURP用URP Unlit/Lit。宽度检查startWidth和endWidth或widthCurve的值是否过小如0导致不可见。顶点数确认脚本中_lineRenderer.positionCount segmentCount;这行代码被执行且segmentCount 1。坐标空间再次确认LineRenderer组件的Use World Space已勾选。这是最容易被忽略的一点7. 完整脚本与使用示例最后附上整合了所有功能包括可选碰撞的完整脚本并说明如何使用。// RopeSimulator.cs using UnityEngine; public class RopeSimulator : MonoBehaviour { [Header(连接点)] public Transform startPoint; public Transform endPoint; [Header(绳子几何)] [Range(2, 100)] public int segmentCount 20; public float ropeLength 5f; private float SegmentLength ropeLength / (segmentCount - 1); [Header(物理参数)] public Vector3 gravity new Vector3(0f, -9.81f, 0f); [Range(1, 100)] public int constraintIterations 20; [Range(0.9f, 1.0f)] public float damping 0.99f; [Header(碰撞检测 (可选))] public bool enableCollision false; public LayerMask collisionMask -1; // 默认所有层 public float collisionRadius 0.1f; [Header(调试)] public bool drawGizmos true; // 内部状态 private Vector3[] _positions; private Vector3[] _prevPositions; private LineRenderer _lineRenderer; void Start() { InitializeRope(); } void InitializeRope() { _lineRenderer GetComponentLineRenderer(); if (_lineRenderer null) _lineRenderer gameObject.AddComponentLineRenderer(); _positions new Vector3[segmentCount]; _prevPositions new Vector3[segmentCount]; if (startPoint ! null endPoint ! null) { for (int i 0; i segmentCount; i) { float t i / (float)(segmentCount - 1); _positions[i] Vector3.Lerp(startPoint.position, endPoint.position, t); } } else { Debug.LogWarning(RopeSimulator: Start or End Point not assigned. Initializing at origin.); for (int i 0; i segmentCount; i) { _positions[i] transform.position Vector3.right * i * 0.5f; } } _positions.CopyTo(_prevPositions, 0); _lineRenderer.positionCount segmentCount; UpdateLineRenderer(); } void FixedUpdate() { if (startPoint null || endPoint null) return; Simulate(); for (int i 0; i constraintIterations; i) { ApplyConstraint(); } UpdateLineRenderer(); } void Simulate() { // 锁定端点 if (startPoint ! null) _positions[0] startPoint.position; if (endPoint ! null) _positions[segmentCount - 1] endPoint.position; float deltaTimeSqr Time.fixedDeltaTime * Time.fixedDeltaTime; for (int i 1; i segmentCount - 1; i) { Vector3 currentPos _positions[i]; Vector3 velocity (currentPos - _prevPositions[i]) * damping; _prevPositions[i] currentPos; Vector3 newPos currentPos velocity gravity * deltaTimeSqr; // 简单碰撞检测 if (enableCollision) { Vector3 moveDir (newPos - _prevPositions[i]).normalized; float moveDist Vector3.Distance(_prevPositions[i], newPos); if (Physics.SphereCast(_prevPositions[i], collisionRadius, moveDir, out RaycastHit hit, moveDist, collisionMask)) { // 简单反弹将位置推到碰撞表面并沿法线方向偏移 newPos hit.point hit.normal * collisionRadius; // 可选计算反弹后的速度并更新_prevPositions以模拟反弹物理 // Vector3 reflectDir Vector3.Reflect(velocity.normalized, hit.normal); // _prevPositions[i] newPos - reflectDir * velocity.magnitude * 0.5f; // 0.5f是弹性系数 } } _positions[i] newPos; } } void ApplyConstraint() { for (int i 0; i segmentCount - 1; i) { Vector3 p1 _positions[i]; Vector3 p2 _positions[i 1]; float currentDist Vector3.Distance(p1, p2); float error Mathf.Abs(currentDist - SegmentLength); if (error 0.001f) { Vector3 dir (p2 - p1).normalized; float offset (currentDist - SegmentLength) * 0.5f; if (i ! 0) // 如果p1不是锁定点 { _positions[i] dir * offset; } if (i 1 ! segmentCount - 1) // 如果p2不是锁定点 { _positions[i 1] - dir * offset; } } } } void UpdateLineRenderer() { _lineRenderer.SetPositions(_positions); } void OnDrawGizmos() { if (!drawGizmos || _positions null) return; Gizmos.color Color.cyan; for (int i 0; i _positions.Length; i) { Gizmos.DrawSphere(_positions[i], 0.05f); if (i _positions.Length - 1) { Gizmos.DrawLine(_positions[i], _positions[i 1]); } } } // 提供一个方法在运行时动态设置连接点例如绳子一端突然断开并连接到新物体 public void SetConnectionPoints(Transform newStart, Transform newEnd) { startPoint newStart; endPoint newEnd; // 可选重置绳子位置到新的两点之间直线避免瞬间拉扯 // InitializeRope(); } }使用步骤将上述脚本保存为RopeSimulator.cs。在Unity中将其拖拽到场景中的RopeManager空物体上。在Inspector面板将StartTarget和EndTarget物体分别拖拽到脚本的Start Point和End Point字段。根据需要调整Segment Count、Rope Length、Constraint Iterations和Damping参数。运行游戏。拖动两个目标物体观察绳子动态连接和摆动的效果。你可以复制多个RopeManager为它们分配不同的目标物体轻松创建出吊桥、锁链网等复杂结构。通过调整参数和添加自定义逻辑如切割、燃烧这个基础的绳子模拟器可以衍生出无数有趣的游戏玩法。