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);
}
}