using System; using NBF; using UnityEngine; [RequireComponent(typeof(LineRenderer))] public class Rope : MonoBehaviour { [Header("Anchors")] [SerializeField] public Rigidbody startAnchor; [SerializeField] public Rigidbody endAnchor; /// 鱼线宽度倍数 public int LineMultiple = 1; [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; [SerializeField, Range(1, 8), Tooltip("每隔多少个节点做一次地面检测;越大越省")] private int groundSampleStep = 3; [SerializeField, Tooltip("未采样的点用插值还是直接拷贝邻近采样值")] private bool groundInterpolate = true; [SerializeField, Range(1, 8), Tooltip("每隔多少次FixedUpdate更新一次地面约束")] private int groundUpdateEvery = 2; private int _groundFrameCounter; [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.001f; [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 private int _physicsNodes; private Vector3[] _pCurr; private Vector3[] _pPrev; // render (一次性分配到最大,后续不再 new) private Vector3[] _rPoints; private int _rCapacity; 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; // caches private Transform _startTr; private Transform _endTr; // precomputed private float _dt; private float _dt2; private float _kY; private float _kXZ; private FRod _rod; public void Init(FRod rod) => _rod = rod; // Catmull t caches(只缓存 idle/moving 两档,减少每帧重复乘法) private struct TCaches { public float[] t; public float[] t2; public float[] t3; } private TCaches _tIdle; private TCaches _tMoving; private void Awake() { _lineRenderer = GetComponent(); _gravity = new Vector3(0f, -gravityStrength, 0f); _startTr = startAnchor ? startAnchor.transform : null; _endTr = endAnchor ? endAnchor.transform : null; InitLengthSystem(); AllocateAndInitNodes(); // ✅ 渲染点一次性分配到最大: (maxNodes-1)*idle + 1 int maxSubdiv = Mathf.Max(1, renderSubdivisionsIdle); _rCapacity = (maxPhysicsNodes - 1) * maxSubdiv + 1; _rPoints = new Vector3[_rCapacity]; BuildTCaches(renderSubdivisionsIdle, ref _tIdle); BuildTCaches(renderSubdivisionsMoving, ref _tMoving); } 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); _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; 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; // cache dt _dt = Time.fixedDeltaTime; if (_dt < 1e-6f) _dt = 1e-6f; _dt2 = _dt * _dt; // gravity _gravity.y = -gravityStrength; // drag caches(exp 比较贵,但这里每 FixedUpdate 一次,OK) _kY = Mathf.Exp(-airDrag * _dt); _kXZ = Mathf.Exp(-airDragXZ * _dt); UpdateLengthSmooth(); UpdateNodesFromLength(); UpdateHeadRestLenFromCurrentLength(); Simulate_VerletFast(); // anchors LockAnchorsHard(); // constraints for (int it = 0; it < iterations; it++) SolveDistanceConstraints_HeadOnly_Fast(); LockAnchorsHard(); if (constrainToGround) { _groundFrameCounter++; if (_groundFrameCounter >= groundUpdateEvery) { _groundFrameCounter = 0; ConstrainToGround(); } } LockAnchorsHard(); } private void LateUpdate() { if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return; int last = _physicsNodes - 1; // 用缓存 transform,避免多次属性链 Vector3 s = _startTr.position; Vector3 e = _endTr.position; _pCurr[0] = s; _pPrev[0] = s; _pCurr[last] = e; _pPrev[last] = e; DrawHighResLine_Fast(); } 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; } 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; Array.Copy(_pCurr, 1, _pCurr, 1 + addCount, oldCount - 1); Array.Copy(_pPrev, 1, _pPrev, 1 + addCount, oldCount - 1); Vector3 s = _startTr ? _startTr.position : startAnchor.position; Vector3 dir = Vector3.down; int firstOld = 1 + addCount; if (oldCount >= 2 && firstOld < maxPhysicsNodes) { Vector3 toOld1 = (_pCurr[firstOld] - s); float sq = toOld1.sqrMagnitude; if (sq > 1e-6f) dir = toOld1 / Mathf.Sqrt(sq); } // inherit displacement (Verlet) Vector3 inheritDisp = Vector3.zero; if (oldCount >= 2 && firstOld < maxPhysicsNodes) inheritDisp = (_pCurr[firstOld] - _pPrev[firstOld]); for (int k = 1; k <= addCount; k++) { Vector3 pos = s + dir * (physicsSegmentLen * k); _pCurr[k] = pos; _pPrev[k] = pos - inheritDisp; // 保持动感 } 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; 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); } /// /// ✅ 更快的 Verlet:去掉 /dt 和 *dt 抵消的无效计算 /// private void Simulate_VerletFast() { // displacement = curr - prev // next = curr + displacement*drag*dampen + gravity*dt^2 for (int i = 1; i < _physicsNodes - 1; i++) { Vector3 disp = _pCurr[i] - _pPrev[i]; disp.x *= _kXZ; disp.z *= _kXZ; disp.y *= _kY; disp *= velocityDampen; Vector3 next = _pCurr[i] + disp + _gravity * _dt2; _pPrev[i] = _pCurr[i]; _pCurr[i] = next; } } private void LockAnchorsHard() { if (!startAnchor || !endAnchor || _pCurr == null || _pPrev == null || _physicsNodes < 2) return; Vector3 s = _startTr ? _startTr.position : startAnchor.position; Vector3 e = _endTr ? _endTr.position : endAnchor.position; _pCurr[0] = s; _pPrev[0] = s - startAnchor.linearVelocity * _dt; int last = _physicsNodes - 1; _pCurr[last] = e; _pPrev[last] = e - endAnchor.linearVelocity * _dt; } /// /// ✅ 约束:减少临时变量、用 sqrMagnitude + invDist /// private void SolveDistanceConstraints_HeadOnly_Fast() { int last = _physicsNodes - 1; for (int i = 0; i < last; i++) { float rest = (i == 0) ? _headRestLen : physicsSegmentLen; Vector3 a = _pCurr[i]; Vector3 b = _pCurr[i + 1]; Vector3 delta = b - a; float sq = delta.sqrMagnitude; if (sq < 1e-12f) continue; float dist = Mathf.Sqrt(sq); float diff = (dist - rest) / dist; // = 1 - rest/dist Vector3 corr = delta * (diff * stiffness); // i==0 锚点固定;last 锚点固定 if (i != 0) _pCurr[i] = a + corr * 0.5f; if (i + 1 != last) _pCurr[i + 1] = b - corr * 0.5f; } } // private void ConstrainToGround() // { // if (groundMask == 0) return; // // // RaycastHit 是 struct,这里不会 GC // for (int i = 1; i < _physicsNodes - 1; i++) // { // Vector3 p = _pCurr[i]; // 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 ConstrainToGround() { if (groundMask == 0) return; int last = _physicsNodes - 1; int step = Mathf.Max(1, groundSampleStep); // 记录采样点的“最低允许Y” // 不想分配数组就用局部变量滚动插值 int prevSampleIdx = 1; float prevMinY = SampleMinY(_pCurr[prevSampleIdx]); // 把采样点先处理掉 ApplyMinY(prevSampleIdx, prevMinY); for (int i = 1 + step; i < last; i += step) { float nextMinY = SampleMinY(_pCurr[i]); ApplyMinY(i, nextMinY); if (groundInterpolate) { // 在两个采样点之间插值 minY(视觉更平滑) int a = prevSampleIdx; int b = i; int span = b - a; for (int j = 1; j < span; j++) { int idx = a + j; float t = j / (float)span; float minY = Mathf.Lerp(prevMinY, nextMinY, t); ApplyMinY(idx, minY); } } else { // 直接用 prevMinY 填充中间点(更省) for (int idx = prevSampleIdx + 1; idx < i; idx++) ApplyMinY(idx, prevMinY); } prevSampleIdx = i; prevMinY = nextMinY; } // 尾巴剩余部分用最后一个采样值填 for (int i = prevSampleIdx + 1; i < last; i++) ApplyMinY(i, prevMinY); } private float SampleMinY(Vector3 p) { Vector3 origin = p + Vector3.up * groundCastHeight; if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask, QueryTriggerInteraction.Ignore)) return hit.point.y + groundRadius; // 没命中就不抬(返回极小值) return float.NegativeInfinity; } private void ApplyMinY(int i, float minY) { if (float.IsNegativeInfinity(minY)) return; Vector3 p = _pCurr[i]; if (p.y < minY) p.y = minY; _pCurr[i] = p; } private void DrawHighResLine_Fast() { if (_pCurr == null || _physicsNodes < 2) return; float w = lineWidth * LineMultiple; _lineRenderer.startWidth = w; _lineRenderer.endWidth = w; if (!smooth) { _lineRenderer.positionCount = _physicsNodes; _lineRenderer.SetPositions(_pCurr); return; } int subdiv = PickRenderSubdivisions_Fast(); TCaches tc = (subdiv == renderSubdivisionsMoving) ? _tMoving : _tIdle; int needed = (_physicsNodes - 1) * subdiv + 1; if (needed > _rCapacity) { // 理论上不该发生(_rCapacity 用 maxNodes & idle 分配) // 保险扩容一次 _rCapacity = needed; _rPoints = new Vector3[_rCapacity]; } int idx = 0; int last = _physicsNodes - 1; for (int seg = 0; seg < last; seg++) { int i0 = seg - 1; if (i0 < 0) i0 = 0; int i1 = seg; int i2 = seg + 1; int i3 = seg + 2; if (i3 > last) i3 = last; Vector3 p0 = _pCurr[i0]; Vector3 p1 = _pCurr[i1]; Vector3 p2 = _pCurr[i2]; Vector3 p3 = _pCurr[i3]; for (int s = 0; s < subdiv; s++) { float t = tc.t[s]; float t2 = tc.t2[s]; float t3 = tc.t3[s]; // inline CatmullRom(少一次函数调用) Vector3 cr = 0.5f * ( (2f * p1) + (-p0 + p2) * t + (2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 + (-p0 + 3f * p1 - 3f * p2 + p3) * t3 ); // Linear Y cr.y = p1.y + (p2.y - p1.y) * t; _rPoints[idx++] = cr; } } _rPoints[idx++] = _pCurr[last]; _lineRenderer.positionCount = idx; _lineRenderer.SetPositions(_rPoints); } /// /// ✅ 用 sqrMagnitude 比较阈值,避免 sqrt /// private int PickRenderSubdivisions_Fast() { int idle = Mathf.Max(1, renderSubdivisionsIdle); int moving = Mathf.Max(1, renderSubdivisionsMoving); float thr = movingSpeedThreshold; float thrSq = (thr * _dt) * (thr * _dt); // 因为我们用 disp = curr-prev(单位是米/step),所以阈值要乘 dt float sumSq = 0f; int count = Mathf.Max(1, _physicsNodes - 2); for (int i = 1; i < _physicsNodes - 1; i++) { Vector3 disp = _pCurr[i] - _pPrev[i]; sumSq += disp.sqrMagnitude; } float avgSq = sumSq / count; return (avgSq > thrSq) ? moving : idle; } private static void BuildTCaches(int subdiv, ref TCaches caches) { subdiv = Mathf.Max(1, subdiv); caches.t = new float[subdiv]; caches.t2 = new float[subdiv]; caches.t3 = new float[subdiv]; float inv = 1f / subdiv; for (int s = 0; s < subdiv; s++) { float t = s * inv; float t2 = t * t; caches.t[s] = t; caches.t2[s] = t2; caches.t3[s] = t2 * t; } } private void OnDrawGizmosSelected() { if (_pCurr == null) return; Gizmos.color = Color.yellow; for (int i = 0; i < _physicsNodes; i++) Gizmos.DrawSphere(_pCurr[i], 0.01f); } }