193 lines
6.7 KiB
C#
193 lines
6.7 KiB
C#
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 使用 LineRenderer 绘制绳索,通过简化的质点-弹簧系统模拟绳索形态。
|
||
/// 支持物理模拟(质量、弹性、阻尼、重力),绳索总长保持不变,
|
||
/// 并支持在设定时间内“缓慢拉直”至紧绷状态。
|
||
/// </summary>
|
||
[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>();
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 模拟每帧节点的受力与位置更新
|
||
/// 包括弹簧力、阻尼、重力,并支持缓慢进入“拉直状态”
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每帧执行多轮“段长约束修正”,使总绳索长度保持不变
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新 LineRenderer 可视化节点位置
|
||
/// </summary>
|
||
void DrawLine()
|
||
{
|
||
lineRenderer.positionCount = SegmentCount;
|
||
lineRenderer.SetPositions(positions);
|
||
}
|
||
}
|