using UnityEngine; /// /// 使用 LineRenderer 绘制绳索,通过简化的质点-弹簧系统模拟绳索形态。 /// 支持物理模拟(质量、弹性、阻尼、重力),绳索总长保持不变, /// 并支持在设定时间内“缓慢拉直”至紧绷状态。 /// [RequireComponent(typeof(LineRenderer))] public class RopeSimulator : MonoBehaviour { [Header("绳索控制点(起点与终点)")] public Transform StartPoint; // 绳索起点,受位置驱动 public Transform EndPoint; // 绳索终点,受位置驱动 [Header("绳索参数")] public int SegmentCount = 20; // 绳索段数(节点数量 = 段数) public float Mass = 1f; // 每个中间节点的质量 public float Spring = 50f; // 弹性系数(控制连接的柔软程度) public float Damping = 5f; // 阻尼系数(用于减缓振荡) public float Gravity = 9.8f; // 重力加速度(作用于每个节点) public float TotalLength = 5f; // 绳索总长(约束节点间距) [Header("拉直开关(true 表示启动拉直动画)")] public bool Straight = false; // 是否处于拉直状态,true 会触发缓慢收紧过程 [Header("速度衰减")] [Range(0f, 1f)] public float SpeedAttenuation = 0.98f; // 控制速度每帧的衰减,防止抖动 [Header("缓慢拉直参数")] public float TightenDuration = 0.2f; // 拉直动画持续时间(单位:秒) // 绘制与模拟用的内部变量 private LineRenderer lineRenderer; private Vector3[] positions; // 当前所有节点的位置 private Vector3[] velocities; // 节点当前速度 private Vector3[] simulatedPositions; // 缓慢拉直前记录的原始模拟位置 // 拉直动画控制变量 private float tightenProgress = 0f; // 拉直进度(0~1) private bool isTightening = false; // 当前是否正在拉直动画中 void Start() { // 初始化 LineRenderer lineRenderer = GetComponent(); lineRenderer.positionCount = SegmentCount; // 初始化节点位置与速度数组 positions = new Vector3[SegmentCount]; velocities = new Vector3[SegmentCount]; simulatedPositions = new Vector3[SegmentCount]; // 初始化所有节点位置(线性插值) for (int i = 0; i < SegmentCount; i++) { float t = (float)i / (SegmentCount - 1); positions[i] = Vector3.Lerp(StartPoint.position, EndPoint.position, t); velocities[i] = Vector3.zero; } } void FixedUpdate() { // 在固定帧率下执行模拟、约束和绘制 Simulate(Time.fixedDeltaTime); ApplyConstraints(); DrawLine(); } /// /// 模拟每帧节点的受力与位置更新 /// 包括弹簧力、阻尼、重力,并支持缓慢进入“拉直状态” /// void Simulate(float dt) { // 如果开启拉直 if (Straight) { // 如果是第一次开始拉直 if (!isTightening) { isTightening = true; tightenProgress = 0f; // 记录当前模拟位置,用于插值过渡 for (int i = 0; i < SegmentCount; i++) simulatedPositions[i] = positions[i]; } // 根据拉直时间推进进度(0~1) if (TightenDuration > 0f) tightenProgress += dt / TightenDuration; else tightenProgress = 1f; // 防止除以0 tightenProgress = Mathf.Clamp01(tightenProgress); // 插值绘制节点向绷直目标靠拢 for (int i = 0; i < SegmentCount; i++) { float t = (float)i / (SegmentCount - 1); Vector3 tightTarget = Vector3.Lerp(StartPoint.position, EndPoint.position, t); positions[i] = Vector3.Lerp(simulatedPositions[i], tightTarget, tightenProgress); velocities[i] = Vector3.zero; } return; // 当前为拉直动画阶段,不执行物理模拟 } else { // 退出拉直状态,恢复常规模拟 isTightening = false; tightenProgress = 0f; } // 常规模拟(弹簧 + 阻尼 + 重力) positions[0] = StartPoint.position; positions[SegmentCount - 1] = EndPoint.position; float restLength = TotalLength / (SegmentCount - 1); // 理想段长 for (int i = 1; i < SegmentCount - 1; i++) { Vector3 force = Vector3.zero; // 计算弹簧力(左右节点) Vector3 left = positions[i - 1]; Vector3 right = positions[i + 1]; Vector3 leftDir = positions[i] - left; Vector3 rightDir = positions[i] - right; force += -Spring * (leftDir.normalized * (leftDir.magnitude - restLength)); force += -Spring * (rightDir.normalized * (rightDir.magnitude - restLength)); // 添加阻尼力 force += -velocities[i] * Damping; // 添加重力 force += Vector3.down * Gravity * Mass; // 更新速度和位置 Vector3 acceleration = force / Mass; velocities[i] += acceleration * dt; velocities[i] *= SpeedAttenuation; positions[i] += velocities[i] * dt; } } /// /// 每帧执行多轮“段长约束修正”,使总绳索长度保持不变 /// void ApplyConstraints() { if (Straight) return; // 绷直中不做物理约束 float segmentLength = TotalLength / (SegmentCount - 1); int iterations = 10; // 越多越稳定 for (int k = 0; k < iterations; k++) { positions[0] = StartPoint.position; positions[SegmentCount - 1] = EndPoint.position; for (int i = 0; i < SegmentCount - 1; i++) { Vector3 p1 = positions[i]; Vector3 p2 = positions[i + 1]; Vector3 delta = p2 - p1; float dist = delta.magnitude; float error = dist - segmentLength; Vector3 correction = delta.normalized * (error * 0.5f); if (i != 0) positions[i] += correction; if (i + 1 != SegmentCount - 1) positions[i + 1] -= correction; } } } /// /// 更新 LineRenderer 可视化节点位置 /// void DrawLine() { lineRenderer.positionCount = SegmentCount; lineRenderer.SetPositions(positions); } }