Files
tools/Unity C++脚本/RopeSimulator.cs
T
2025-08-01 10:50:57 +08:00

193 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}