543 lines
17 KiB
C#
543 lines
17 KiB
C#
using System;
|
||
using NBF;
|
||
using UnityEngine;
|
||
|
||
[RequireComponent(typeof(LineRenderer))]
|
||
public class Rope : MonoBehaviour
|
||
{
|
||
[Header("Anchors")] [SerializeField] public Rigidbody startAnchor;
|
||
[SerializeField] public Rigidbody endAnchor;
|
||
|
||
[Header("Physics (Dynamic Nodes, Fixed Segment Len)")] [SerializeField, Min(0.01f), Tooltip("物理每段固定长度(越小越细致越耗)")]
|
||
private float physicsSegmentLen = 0.15f;
|
||
|
||
[SerializeField, Range(2, 200)] private int minPhysicsNodes = 12;
|
||
|
||
[SerializeField, Range(2, 400), Tooltip("物理节点上限(仅用于性能保护;与“最大长度不限制”不是一回事)")]
|
||
private int maxPhysicsNodes = 120;
|
||
|
||
[SerializeField] private float gravityStrength = 2.0f;
|
||
[SerializeField, Range(0f, 1f)] private float velocityDampen = 0.95f;
|
||
|
||
[SerializeField, Range(0.0f, 1.0f), Tooltip("约束修正强度,越大越硬。0.6~0.9 常用")]
|
||
private float stiffness = 0.8f;
|
||
|
||
[SerializeField, Range(1, 80), Tooltip("迭代次数。鱼线 10~30 通常够用")]
|
||
private int iterations = 20;
|
||
|
||
[Header("Length Control (No Min/Max Clamp)")]
|
||
[Tooltip("初始总长度(米)。如果为 0,则用 physicsSegmentLen*(minPhysicsNodes-1) 作为初始长度")]
|
||
[SerializeField, Min(0f)]
|
||
private float initialLength = 0f;
|
||
|
||
[Tooltip("长度变化平滑时间(越小越跟手,越大越稳)")] [SerializeField, Min(0.0001f)]
|
||
private float lengthSmoothTime = 0.15f;
|
||
|
||
[Tooltip("当长度在变化时,额外把速度压掉一些(防抖)。0=不额外处理,1=变化时几乎清速度(建议只在收线生效)")] [SerializeField, Range(0f, 1f)]
|
||
private float lengthChangeVelocityKill = 0.6f;
|
||
|
||
[Tooltip("允许的最小松弛余量(避免目标长度刚好等于锚点距离时抖动)")] [SerializeField, Min(0f)]
|
||
private float minSlack = 0.002f;
|
||
|
||
[Header("Head Segment Clamp")] [Tooltip("第一段(起点->第1节点)允许的最小长度,避免收线时第一段被压到0导致数值炸")] [SerializeField, Min(0.0001f)]
|
||
private float headMinLen = 0.01f;
|
||
|
||
[Header("Node Count Stability")] [SerializeField, Tooltip("节点数切换迟滞(米)。避免长度在临界点抖动导致节点数来回跳 -> 卡顿")]
|
||
private float nodeHysteresis = 0.05f;
|
||
|
||
[Header("Simple Ground/Water Constraint (Cheap)")] [SerializeField]
|
||
private bool constrainToGround = true;
|
||
|
||
[SerializeField] private LayerMask groundMask = ~0;
|
||
[SerializeField, Min(0f)] private float groundRadius = 0.01f;
|
||
[SerializeField, Min(0f)] private float groundCastHeight = 1.0f;
|
||
[SerializeField, Min(0.01f)] private float groundCastDistance = 2.5f;
|
||
|
||
[Header("Render (High Resolution)")] [SerializeField, Min(1), Tooltip("静止时每段物理线段插值加密数量(越大越顺,越耗)")]
|
||
private int renderSubdivisionsIdle = 6;
|
||
|
||
[SerializeField, Min(1), Tooltip("甩动时每段物理线段插值加密数量(动态降LOD以防卡顿)")]
|
||
private int renderSubdivisionsMoving = 2;
|
||
|
||
[SerializeField, Min(0f), Tooltip("平均速度超过该阈值认为在甩动(用于动态降 subdiv)")]
|
||
private float movingSpeedThreshold = 2.0f;
|
||
|
||
[SerializeField, Tooltip("是否使用 Catmull-Rom 平滑(开启更顺,但更耗)")]
|
||
private bool smooth = true;
|
||
|
||
[SerializeField, Min(0.0001f)] private float lineWidth = 0.002f;
|
||
|
||
[Header("Air Drag (Stable)")] [SerializeField, Range(0f, 5f), Tooltip("空气阻力(Y向),指数衰减,越大越不飘")]
|
||
private float airDrag = 0.9f;
|
||
|
||
[SerializeField, Range(0f, 2f), Tooltip("横向额外阻力(XZ),指数衰减,越大越不左右飘")]
|
||
private float airDragXZ = 0.6f;
|
||
|
||
private LineRenderer _lineRenderer;
|
||
|
||
// physics (注意:数组固定为 maxPhysicsNodes,physicsNodes 表示有效长度)
|
||
private int _physicsNodes;
|
||
private Vector3[] _pCurr;
|
||
private Vector3[] _pPrev;
|
||
|
||
// render
|
||
private Vector3[] _rPoints;
|
||
private int _rCountCached = -1;
|
||
|
||
private Vector3 _gravity;
|
||
|
||
// length control runtime
|
||
private float _targetLength;
|
||
private float _currentLength;
|
||
private float _lengthSmoothVel;
|
||
|
||
// rest length head
|
||
private float _headRestLen;
|
||
|
||
// node stability
|
||
private int _lastDesiredNodes = 0;
|
||
|
||
private FRod _rod;
|
||
public void Init(FRod rod) => _rod = rod;
|
||
|
||
private void Awake()
|
||
{
|
||
_lineRenderer = GetComponent<LineRenderer>();
|
||
_gravity = new Vector3(0f, -gravityStrength, 0f);
|
||
|
||
InitLengthSystem();
|
||
AllocateAndInitNodes();
|
||
RebuildRenderBufferIfNeeded(renderSubdivisionsIdle);
|
||
}
|
||
|
||
private void OnValidate()
|
||
{
|
||
renderSubdivisionsIdle = Mathf.Max(renderSubdivisionsIdle, 1);
|
||
renderSubdivisionsMoving = Mathf.Max(renderSubdivisionsMoving, 1);
|
||
iterations = Mathf.Clamp(iterations, 1, 80);
|
||
groundCastDistance = Mathf.Max(groundCastDistance, 0.01f);
|
||
groundCastHeight = Mathf.Max(groundCastHeight, 0f);
|
||
lineWidth = Mathf.Max(lineWidth, 0.0001f);
|
||
|
||
lengthSmoothTime = Mathf.Max(lengthSmoothTime, 0.0001f);
|
||
|
||
physicsSegmentLen = Mathf.Max(physicsSegmentLen, 0.01f);
|
||
minPhysicsNodes = Mathf.Max(minPhysicsNodes, 2);
|
||
maxPhysicsNodes = Mathf.Max(maxPhysicsNodes, minPhysicsNodes);
|
||
|
||
headMinLen = Mathf.Max(headMinLen, 0.0001f);
|
||
nodeHysteresis = Mathf.Max(0f, nodeHysteresis);
|
||
|
||
}
|
||
|
||
private void InitLengthSystem()
|
||
{
|
||
float defaultLen = physicsSegmentLen * (Mathf.Max(minPhysicsNodes, 2) - 1);
|
||
_currentLength = (initialLength > 0f) ? initialLength : defaultLen;
|
||
_targetLength = _currentLength;
|
||
}
|
||
|
||
private void AllocateAndInitNodes()
|
||
{
|
||
_physicsNodes = Mathf.Clamp(ComputeDesiredNodesStable(_currentLength), 2, maxPhysicsNodes);
|
||
|
||
// ✅ 永久分配到最大,运行时不再 new,避免GC卡顿
|
||
_pCurr = new Vector3[maxPhysicsNodes];
|
||
_pPrev = new Vector3[maxPhysicsNodes];
|
||
|
||
Vector3 start = startAnchor ? startAnchor.position : transform.position;
|
||
Vector3 dir = Vector3.down;
|
||
|
||
for (int i = 0; i < _physicsNodes; i++)
|
||
{
|
||
Vector3 pos = start + dir * (physicsSegmentLen * i);
|
||
_pCurr[i] = pos;
|
||
_pPrev[i] = pos;
|
||
}
|
||
|
||
UpdateHeadRestLenFromCurrentLength();
|
||
|
||
if (startAnchor && endAnchor)
|
||
LockAnchorsHard();
|
||
}
|
||
|
||
private int ComputeDesiredNodes(float lengthMeters)
|
||
{
|
||
int desired = Mathf.FloorToInt(Mathf.Max(0f, lengthMeters) / physicsSegmentLen) + 1;
|
||
desired = Mathf.Clamp(desired, minPhysicsNodes, maxPhysicsNodes);
|
||
return desired;
|
||
}
|
||
|
||
private int ComputeDesiredNodesStable(float lengthMeters)
|
||
{
|
||
int desired = ComputeDesiredNodes(lengthMeters);
|
||
|
||
if (_lastDesiredNodes == 0)
|
||
{
|
||
_lastDesiredNodes = desired;
|
||
return desired;
|
||
}
|
||
|
||
if (desired == _lastDesiredNodes)
|
||
return desired;
|
||
|
||
// 边界(lastDesiredNodes 对应的长度临界)
|
||
float boundary = (_lastDesiredNodes - 1) * physicsSegmentLen;
|
||
|
||
// 在临界附近就不切换,避免来回跳
|
||
if (Mathf.Abs(lengthMeters - boundary) < nodeHysteresis)
|
||
return _lastDesiredNodes;
|
||
|
||
_lastDesiredNodes = desired;
|
||
return desired;
|
||
}
|
||
|
||
public void SetTargetLength(float lengthMeters)
|
||
{
|
||
_targetLength = Mathf.Max(0f, lengthMeters);
|
||
}
|
||
|
||
public float GetCurrentLength() => _currentLength;
|
||
public float GetTargetLength() => _targetLength;
|
||
|
||
private void FixedUpdate()
|
||
{
|
||
if (!startAnchor || !endAnchor) return;
|
||
|
||
_gravity.y = -gravityStrength;
|
||
|
||
UpdateLengthSmooth();
|
||
UpdateNodesFromLength(); // ✅ 一次性增减,无GC
|
||
UpdateHeadRestLenFromCurrentLength();
|
||
|
||
// simulate
|
||
Simulate();
|
||
|
||
// 确保端点正确后再迭代
|
||
LockAnchorsHard();
|
||
|
||
// constraints
|
||
for (int it = 0; it < iterations; it++)
|
||
SolveDistanceConstraints_HeadOnly();
|
||
|
||
// 迭代后锁一次足够
|
||
LockAnchorsHard();
|
||
|
||
if (constrainToGround)
|
||
ConstrainToGround();
|
||
|
||
// 约束后再锁一次(保险)
|
||
LockAnchorsHard();
|
||
}
|
||
|
||
private void LateUpdate()
|
||
{
|
||
if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return;
|
||
|
||
// 端点“跟手”:渲染前强行同步 transform,消除慢一拍
|
||
int last = _physicsNodes - 1;
|
||
Vector3 s = startAnchor.transform.position;
|
||
Vector3 e = endAnchor.transform.position;
|
||
|
||
_pCurr[0] = s;
|
||
_pPrev[0] = s;
|
||
_pCurr[last] = e;
|
||
_pPrev[last] = e;
|
||
|
||
DrawHighResLine();
|
||
}
|
||
|
||
private void UpdateLengthSmooth()
|
||
{
|
||
float minFeasible = 0.01f;
|
||
|
||
float desired = Mathf.Max(_targetLength, minFeasible);
|
||
|
||
_currentLength = Mathf.SmoothDamp(
|
||
_currentLength,
|
||
desired,
|
||
ref _lengthSmoothVel,
|
||
lengthSmoothTime,
|
||
Mathf.Infinity,
|
||
Time.fixedDeltaTime
|
||
);
|
||
}
|
||
|
||
private void UpdateNodesFromLength()
|
||
{
|
||
int desired = ComputeDesiredNodesStable(_currentLength);
|
||
desired = Mathf.Clamp(desired, 2, maxPhysicsNodes);
|
||
if (desired == _physicsNodes) return;
|
||
|
||
if (desired > _physicsNodes)
|
||
{
|
||
AddNodesAtStart(desired - _physicsNodes);
|
||
}
|
||
else
|
||
{
|
||
RemoveNodesAtStart(_physicsNodes - desired);
|
||
}
|
||
|
||
_physicsNodes = desired;
|
||
|
||
// 渲染缓存按最大 subdiv 预留即可
|
||
RebuildRenderBufferIfNeeded(renderSubdivisionsIdle);
|
||
}
|
||
|
||
|
||
private void AddNodesAtStart(int addCount)
|
||
{
|
||
if (addCount <= 0) return;
|
||
|
||
int oldCount = _physicsNodes;
|
||
int newCount = Mathf.Min(oldCount + addCount, maxPhysicsNodes);
|
||
addCount = newCount - oldCount;
|
||
if (addCount <= 0) return;
|
||
|
||
// 把 [1..oldCount-1] 整体往后挪 addCount,给 [1..addCount] 腾位置
|
||
// oldCount-1 个元素(包含尾端锚点位,尾端后面会被锁)
|
||
Array.Copy(_pCurr, 1, _pCurr, 1 + addCount, oldCount - 1);
|
||
Array.Copy(_pPrev, 1, _pPrev, 1 + addCount, oldCount - 1);
|
||
|
||
Vector3 s = startAnchor.position;
|
||
|
||
// 方向:用“挪完后的第一个动态点”指向来估计
|
||
Vector3 dir = Vector3.down;
|
||
int firstOld = 1 + addCount;
|
||
if (firstOld < 1 + addCount + (oldCount - 2) && oldCount >= 2)
|
||
{
|
||
Vector3 toOld1 = (_pCurr[firstOld] - s);
|
||
if (toOld1.sqrMagnitude > 1e-6f) dir = toOld1.normalized;
|
||
}
|
||
|
||
// 继承速度
|
||
float dt = Time.fixedDeltaTime;
|
||
Vector3 inheritVel = Vector3.zero;
|
||
if (oldCount >= 2 && firstOld < maxPhysicsNodes)
|
||
{
|
||
inheritVel = (_pCurr[firstOld] - _pPrev[firstOld]) / Mathf.Max(dt, 1e-6f);
|
||
}
|
||
|
||
for (int k = 1; k <= addCount; k++)
|
||
{
|
||
Vector3 pos = s + dir * (physicsSegmentLen * k);
|
||
_pCurr[k] = pos;
|
||
_pPrev[k] = pos - inheritVel * dt;
|
||
}
|
||
|
||
LockAnchorsHard();
|
||
}
|
||
|
||
private void RemoveNodesAtStart(int removeCount)
|
||
{
|
||
if (removeCount <= 0) return;
|
||
|
||
int oldCount = _physicsNodes;
|
||
int newCount = Mathf.Max(oldCount - removeCount, 2);
|
||
removeCount = oldCount - newCount;
|
||
if (removeCount <= 0) return;
|
||
|
||
// 把 [1+removeCount .. 1+removeCount+(newCount-2)-1] 挪到 [1..newCount-2]
|
||
// 中间动态节点数量 = newCount-2
|
||
Array.Copy(_pCurr, 1 + removeCount, _pCurr, 1, newCount - 2);
|
||
Array.Copy(_pPrev, 1 + removeCount, _pPrev, 1, newCount - 2);
|
||
|
||
LockAnchorsHard();
|
||
}
|
||
|
||
private void UpdateHeadRestLenFromCurrentLength()
|
||
{
|
||
int fixedSegCount = Mathf.Max(0, _physicsNodes - 2);
|
||
float baseLen = fixedSegCount * physicsSegmentLen;
|
||
|
||
_headRestLen = _currentLength - baseLen;
|
||
_headRestLen = Mathf.Clamp(_headRestLen, headMinLen, physicsSegmentLen * 1.5f);
|
||
}
|
||
|
||
private void Simulate()
|
||
{
|
||
float dt = Time.fixedDeltaTime;
|
||
float invDt = 1f / Mathf.Max(dt, 1e-6f);
|
||
|
||
// 指数衰减:更稳定好调
|
||
float kY = Mathf.Exp(-airDrag * dt);
|
||
float kXZ = Mathf.Exp(-airDragXZ * dt);
|
||
|
||
for (int i = 1; i < _physicsNodes - 1; i++)
|
||
{
|
||
Vector3 vel = (_pCurr[i] - _pPrev[i]) * invDt;
|
||
|
||
vel.x *= kXZ;
|
||
vel.z *= kXZ;
|
||
vel.y *= kY;
|
||
|
||
vel *= velocityDampen;
|
||
|
||
Vector3 next = _pCurr[i] + vel * dt;
|
||
|
||
_pPrev[i] = _pCurr[i];
|
||
_pCurr[i] = next + _gravity * (dt * dt);
|
||
}
|
||
}
|
||
|
||
private void LockAnchorsHard()
|
||
{
|
||
if (!startAnchor || !endAnchor || _pCurr == null || _pPrev == null || _physicsNodes < 2) return;
|
||
|
||
float dt = Time.fixedDeltaTime;
|
||
Vector3 s = startAnchor.position;
|
||
Vector3 e = endAnchor.position;
|
||
|
||
_pCurr[0] = s;
|
||
_pPrev[0] = s - startAnchor.linearVelocity * dt;
|
||
|
||
int last = _physicsNodes - 1;
|
||
_pCurr[last] = e;
|
||
_pPrev[last] = e - endAnchor.linearVelocity * dt;
|
||
}
|
||
|
||
private void SolveDistanceConstraints_HeadOnly()
|
||
{
|
||
for (int i = 0; i < _physicsNodes - 1; i++)
|
||
{
|
||
float rest = (i == 0) ? _headRestLen : physicsSegmentLen;
|
||
|
||
Vector3 a = _pCurr[i];
|
||
Vector3 b = _pCurr[i + 1];
|
||
|
||
Vector3 delta = b - a;
|
||
float dist = delta.magnitude;
|
||
if (dist < 1e-6f) continue;
|
||
|
||
float diff = (dist - rest) / dist;
|
||
Vector3 corr = delta * diff * stiffness;
|
||
|
||
if (i != 0)
|
||
_pCurr[i] += corr * 0.5f;
|
||
|
||
if (i + 1 != _physicsNodes - 1)
|
||
_pCurr[i + 1] -= corr * 0.5f;
|
||
}
|
||
}
|
||
|
||
private void ConstrainToGround()
|
||
{
|
||
for (int i = 1; i < _physicsNodes - 1; i++)
|
||
{
|
||
Vector3 p = _pCurr[i];
|
||
|
||
if (constrainToGround && groundMask != 0)
|
||
{
|
||
Vector3 origin = p + Vector3.up * groundCastHeight;
|
||
if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask,
|
||
QueryTriggerInteraction.Ignore))
|
||
{
|
||
float minY = hit.point.y + groundRadius;
|
||
if (p.y < minY) p.y = minY;
|
||
}
|
||
}
|
||
|
||
_pCurr[i] = p;
|
||
}
|
||
}
|
||
|
||
private void DrawHighResLine()
|
||
{
|
||
if (_pCurr == null || _physicsNodes < 2) return;
|
||
|
||
_lineRenderer.startWidth = lineWidth;
|
||
_lineRenderer.endWidth = lineWidth;
|
||
|
||
if (!smooth)
|
||
{
|
||
_lineRenderer.positionCount = _physicsNodes;
|
||
// 只传有效段
|
||
// LineRenderer.SetPositions 会读数组的前 positionCount 个
|
||
_lineRenderer.SetPositions(_pCurr);
|
||
return;
|
||
}
|
||
|
||
// 动态降 subdiv:甩动时降低点数,减少CPU开销
|
||
int subdiv = PickRenderSubdivisions();
|
||
RebuildRenderBufferIfNeeded(subdiv);
|
||
|
||
int idx = 0;
|
||
|
||
for (int seg = 0; seg < _physicsNodes - 1; seg++)
|
||
{
|
||
Vector3 p0 = _pCurr[Mathf.Max(seg - 1, 0)];
|
||
Vector3 p1 = _pCurr[seg];
|
||
Vector3 p2 = _pCurr[seg + 1];
|
||
Vector3 p3 = _pCurr[Mathf.Min(seg + 2, _physicsNodes - 1)];
|
||
|
||
for (int s = 0; s < subdiv; s++)
|
||
{
|
||
float t = s / (float)subdiv;
|
||
Vector3 pt = CatmullRom_XZ_LinearY(p0, p1, p2, p3, t);
|
||
_rPoints[idx++] = pt;
|
||
}
|
||
}
|
||
|
||
_rPoints[idx++] = _pCurr[_physicsNodes - 1];
|
||
|
||
_lineRenderer.positionCount = idx;
|
||
_lineRenderer.SetPositions(_rPoints);
|
||
}
|
||
|
||
private int PickRenderSubdivisions()
|
||
{
|
||
int idle = Mathf.Max(1, renderSubdivisionsIdle);
|
||
int moving = Mathf.Max(1, renderSubdivisionsMoving);
|
||
|
||
// 计算平均速度(用 Verlet 差分)
|
||
float dt = Time.fixedDeltaTime;
|
||
float invDt = 1f / Mathf.Max(dt, 1e-6f);
|
||
|
||
float sum = 0f;
|
||
int count = Mathf.Max(1, _physicsNodes - 2);
|
||
for (int i = 1; i < _physicsNodes - 1; i++)
|
||
sum += (_pCurr[i] - _pPrev[i]).magnitude * invDt;
|
||
|
||
float avgSpeed = sum / count;
|
||
|
||
return (avgSpeed > movingSpeedThreshold) ? moving : idle;
|
||
}
|
||
|
||
private void RebuildRenderBufferIfNeeded(int subdiv)
|
||
{
|
||
int targetCount = (_physicsNodes - 1) * subdiv + 1;
|
||
if (_rPoints == null || _rCountCached != targetCount)
|
||
{
|
||
_rPoints = new Vector3[targetCount];
|
||
_rCountCached = targetCount;
|
||
}
|
||
}
|
||
|
||
private static Vector3 CatmullRom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
|
||
{
|
||
float t2 = t * t;
|
||
float t3 = t2 * t;
|
||
|
||
return 0.5f * (
|
||
(2f * p1) +
|
||
(-p0 + p2) * t +
|
||
(2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 +
|
||
(-p0 + 3f * p1 - 3f * p2 + p3) * t3
|
||
);
|
||
}
|
||
|
||
private static Vector3 CatmullRom_XZ_LinearY(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
|
||
{
|
||
Vector3 cr = CatmullRom(p0, p1, p2, p3, t);
|
||
cr.y = Mathf.Lerp(p1.y, p2.y, t);
|
||
return cr;
|
||
}
|
||
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
if (_pCurr == null) return;
|
||
Gizmos.color = Color.yellow;
|
||
for (int i = 0; i < _physicsNodes; i++)
|
||
Gizmos.DrawSphere(_pCurr[i], 0.01f);
|
||
}
|
||
} |