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