From adbd097fd5197a0f46298293c349f032901f6dee Mon Sep 17 00:00:00 2001 From: BobSong <605277374@qq.com> Date: Wed, 25 Feb 2026 23:46:50 +0800 Subject: [PATCH] =?UTF-8?q?=E9=B1=BC=E7=BA=BF=E6=80=A7=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Line/fishing line float set.prefab | 6 +- Assets/Scripts/Fishing/Rope/Rope.cs | 328 ++++++++++-------- 2 files changed, 186 insertions(+), 148 deletions(-) diff --git a/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab b/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab index 71d345089..0ba4526bc 100644 --- a/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab +++ b/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab @@ -47,13 +47,14 @@ MonoBehaviour: m_EditorClassIdentifier: startAnchor: {fileID: 0} endAnchor: {fileID: 54298866000586118} + LineMultiple: 1 physicsSegmentLen: 0.1 minPhysicsNodes: 2 maxPhysicsNodes: 200 gravityStrength: 6 velocityDampen: 0.95 stiffness: 0.8 - iterations: 20 + iterations: 10 initialLength: 0 lengthSmoothTime: 0.15 lengthChangeVelocityKill: 0.4 @@ -662,13 +663,14 @@ MonoBehaviour: m_EditorClassIdentifier: startAnchor: {fileID: 54298866000586118} endAnchor: {fileID: 54679398375713381} + LineMultiple: 1 physicsSegmentLen: 0.2 minPhysicsNodes: 2 maxPhysicsNodes: 120 gravityStrength: 6 velocityDampen: 0.95 stiffness: 0.8 - iterations: 20 + iterations: 10 initialLength: 0 lengthSmoothTime: 0.15 lengthChangeVelocityKill: 0.6 diff --git a/Assets/Scripts/Fishing/Rope/Rope.cs b/Assets/Scripts/Fishing/Rope/Rope.cs index c82121df7..083610996 100644 --- a/Assets/Scripts/Fishing/Rope/Rope.cs +++ b/Assets/Scripts/Fishing/Rope/Rope.cs @@ -8,6 +8,9 @@ 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; @@ -65,7 +68,7 @@ public class Rope : MonoBehaviour [SerializeField, Tooltip("是否使用 Catmull-Rom 平滑(开启更顺,但更耗)")] private bool smooth = true; - [SerializeField, Min(0.0001f)] private float lineWidth = 0.002f; + [SerializeField, Min(0.0001f)] private float lineWidth = 0.001f; [Header("Air Drag (Stable)")] [SerializeField, Range(0f, 5f), Tooltip("空气阻力(Y向),指数衰减,越大越不飘")] private float airDrag = 0.9f; @@ -75,14 +78,14 @@ public class Rope : MonoBehaviour private LineRenderer _lineRenderer; - // physics (注意:数组固定为 maxPhysicsNodes,physicsNodes 表示有效长度) + // physics private int _physicsNodes; private Vector3[] _pCurr; private Vector3[] _pPrev; - // render + // render (一次性分配到最大,后续不再 new) private Vector3[] _rPoints; - private int _rCountCached = -1; + private int _rCapacity; private Vector3 _gravity; @@ -97,17 +100,47 @@ public class Rope : MonoBehaviour // 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(); - RebuildRenderBufferIfNeeded(renderSubdivisionsIdle); + + // ✅ 渲染点一次性分配到最大: (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() @@ -127,7 +160,6 @@ public class Rope : MonoBehaviour headMinLen = Mathf.Max(headMinLen, 0.0001f); nodeHysteresis = Mathf.Max(0f, nodeHysteresis); - } private void InitLengthSystem() @@ -141,7 +173,6 @@ public class Rope : MonoBehaviour { _physicsNodes = Mathf.Clamp(ComputeDesiredNodesStable(_currentLength), 2, maxPhysicsNodes); - // ✅ 永久分配到最大,运行时不再 new,避免GC卡顿 _pCurr = new Vector3[maxPhysicsNodes]; _pPrev = new Vector3[maxPhysicsNodes]; @@ -181,10 +212,7 @@ public class Rope : MonoBehaviour if (desired == _lastDesiredNodes) return desired; - // 边界(lastDesiredNodes 对应的长度临界) float boundary = (_lastDesiredNodes - 1) * physicsSegmentLen; - - // 在临界附近就不切换,避免来回跳 if (Mathf.Abs(lengthMeters - boundary) < nodeHysteresis) return _lastDesiredNodes; @@ -192,11 +220,7 @@ public class Rope : MonoBehaviour return desired; } - public void SetTargetLength(float lengthMeters) - { - _targetLength = Mathf.Max(0f, lengthMeters); - } - + public void SetTargetLength(float lengthMeters) => _targetLength = Mathf.Max(0f, lengthMeters); public float GetCurrentLength() => _currentLength; public float GetTargetLength() => _targetLength; @@ -204,29 +228,36 @@ public class Rope : MonoBehaviour { 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(); // ✅ 一次性增减,无GC + UpdateNodesFromLength(); UpdateHeadRestLenFromCurrentLength(); - // simulate - Simulate(); + Simulate_VerletFast(); - // 确保端点正确后再迭代 + // anchors LockAnchorsHard(); // constraints for (int it = 0; it < iterations; it++) - SolveDistanceConstraints_HeadOnly(); + SolveDistanceConstraints_HeadOnly_Fast(); - // 迭代后锁一次足够 LockAnchorsHard(); if (constrainToGround) ConstrainToGround(); - // 约束后再锁一次(保险) LockAnchorsHard(); } @@ -234,23 +265,23 @@ public class Rope : MonoBehaviour { if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return; - // 端点“跟手”:渲染前强行同步 transform,消除慢一拍 int last = _physicsNodes - 1; - Vector3 s = startAnchor.transform.position; - Vector3 e = endAnchor.transform.position; + + // 用缓存 transform,避免多次属性链 + Vector3 s = _startTr.position; + Vector3 e = _endTr.position; _pCurr[0] = s; _pPrev[0] = s; _pCurr[last] = e; _pPrev[last] = e; - DrawHighResLine(); + DrawHighResLine_Fast(); } private void UpdateLengthSmooth() { float minFeasible = 0.01f; - float desired = Mathf.Max(_targetLength, minFeasible); _currentLength = Mathf.SmoothDamp( @@ -269,22 +300,12 @@ public class Rope : MonoBehaviour desired = Mathf.Clamp(desired, 2, maxPhysicsNodes); if (desired == _physicsNodes) return; - if (desired > _physicsNodes) - { - AddNodesAtStart(desired - _physicsNodes); - } - else - { - RemoveNodesAtStart(_physicsNodes - desired); - } + if (desired > _physicsNodes) AddNodesAtStart(desired - _physicsNodes); + else RemoveNodesAtStart(_physicsNodes - desired); _physicsNodes = desired; - - // 渲染缓存按最大 subdiv 预留即可 - RebuildRenderBufferIfNeeded(renderSubdivisionsIdle); } - private void AddNodesAtStart(int addCount) { if (addCount <= 0) return; @@ -294,35 +315,30 @@ public class Rope : MonoBehaviour 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 s = _startTr ? _startTr.position : 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); + 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 - inheritVel * dt; + _pPrev[k] = pos - inheritDisp; // 保持动感 } LockAnchorsHard(); @@ -337,8 +353,6 @@ public class Rope : MonoBehaviour 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); @@ -354,29 +368,27 @@ public class Rope : MonoBehaviour _headRestLen = Mathf.Clamp(_headRestLen, headMinLen, physicsSegmentLen * 1.5f); } - private void Simulate() + /// + /// ✅ 更快的 Verlet:去掉 /dt 和 *dt 抵消的无效计算 + /// + private void Simulate_VerletFast() { - float dt = Time.fixedDeltaTime; - float invDt = 1f / Mathf.Max(dt, 1e-6f); - - // 指数衰减:更稳定好调 - float kY = Mathf.Exp(-airDrag * dt); - float kXZ = Mathf.Exp(-airDragXZ * dt); - + // displacement = curr - prev + // next = curr + displacement*drag*dampen + gravity*dt^2 for (int i = 1; i < _physicsNodes - 1; i++) { - Vector3 vel = (_pCurr[i] - _pPrev[i]) * invDt; + Vector3 disp = _pCurr[i] - _pPrev[i]; - vel.x *= kXZ; - vel.z *= kXZ; - vel.y *= kY; + disp.x *= _kXZ; + disp.z *= _kXZ; + disp.y *= _kY; - vel *= velocityDampen; + disp *= velocityDampen; - Vector3 next = _pCurr[i] + vel * dt; + Vector3 next = _pCurr[i] + disp + _gravity * _dt2; _pPrev[i] = _pCurr[i]; - _pCurr[i] = next + _gravity * (dt * dt); + _pCurr[i] = next; } } @@ -384,21 +396,25 @@ public class Rope : MonoBehaviour { if (!startAnchor || !endAnchor || _pCurr == null || _pPrev == null || _physicsNodes < 2) return; - float dt = Time.fixedDeltaTime; - Vector3 s = startAnchor.position; - Vector3 e = endAnchor.position; + Vector3 s = _startTr ? _startTr.position : startAnchor.position; + Vector3 e = _endTr ? _endTr.position : endAnchor.position; _pCurr[0] = s; - _pPrev[0] = s - startAnchor.linearVelocity * dt; + _pPrev[0] = s - startAnchor.linearVelocity * _dt; int last = _physicsNodes - 1; _pCurr[last] = e; - _pPrev[last] = e - endAnchor.linearVelocity * dt; + _pPrev[last] = e - endAnchor.linearVelocity * _dt; } - private void SolveDistanceConstraints_HeadOnly() + /// + /// ✅ 约束:减少临时变量、用 sqrMagnitude + invDist + /// + private void SolveDistanceConstraints_HeadOnly_Fast() { - for (int i = 0; i < _physicsNodes - 1; i++) + int last = _physicsNodes - 1; + + for (int i = 0; i < last; i++) { float rest = (i == 0) ? _headRestLen : physicsSegmentLen; @@ -406,131 +422,151 @@ public class Rope : MonoBehaviour Vector3 b = _pCurr[i + 1]; Vector3 delta = b - a; - float dist = delta.magnitude; - if (dist < 1e-6f) continue; + float sq = delta.sqrMagnitude; + if (sq < 1e-12f) continue; - float diff = (dist - rest) / dist; - Vector3 corr = delta * diff * stiffness; + float dist = Mathf.Sqrt(sq); + float diff = (dist - rest) / dist; // = 1 - 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; + // 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 (constrainToGround && groundMask != 0) + if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask, + QueryTriggerInteraction.Ignore)) { - 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; - } + float minY = hit.point.y + groundRadius; + if (p.y < minY) p.y = minY; } _pCurr[i] = p; } } - private void DrawHighResLine() + private void DrawHighResLine_Fast() { if (_pCurr == null || _physicsNodes < 2) return; - _lineRenderer.startWidth = lineWidth; - _lineRenderer.endWidth = lineWidth; + float w = lineWidth * LineMultiple; + _lineRenderer.startWidth = w; + _lineRenderer.endWidth = w; if (!smooth) { _lineRenderer.positionCount = _physicsNodes; - // 只传有效段 - // LineRenderer.SetPositions 会读数组的前 positionCount 个 _lineRenderer.SetPositions(_pCurr); return; } - // 动态降 subdiv:甩动时降低点数,减少CPU开销 - int subdiv = PickRenderSubdivisions(); - RebuildRenderBufferIfNeeded(subdiv); + 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 < _physicsNodes - 1; seg++) + for (int seg = 0; seg < last; 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)]; + 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 = s / (float)subdiv; - Vector3 pt = CatmullRom_XZ_LinearY(p0, p1, p2, p3, t); - _rPoints[idx++] = pt; + 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[_physicsNodes - 1]; + _rPoints[idx++] = _pCurr[last]; _lineRenderer.positionCount = idx; _lineRenderer.SetPositions(_rPoints); } - private int PickRenderSubdivisions() + /// + /// ✅ 用 sqrMagnitude 比较阈值,避免 sqrt + /// + private int PickRenderSubdivisions_Fast() { 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 thr = movingSpeedThreshold; + float thrSq = (thr * _dt) * (thr * _dt); // 因为我们用 disp = curr-prev(单位是米/step),所以阈值要乘 dt - float sum = 0f; + float sumSq = 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; + Vector3 disp = _pCurr[i] - _pPrev[i]; + sumSq += disp.sqrMagnitude; } + + float avgSq = sumSq / count; + + return (avgSq > thrSq) ? moving : idle; } - private static Vector3 CatmullRom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) + private static void BuildTCaches(int subdiv, ref TCaches caches) { - float t2 = t * t; - float t3 = t2 * t; + subdiv = Mathf.Max(1, subdiv); + caches.t = new float[subdiv]; + caches.t2 = new float[subdiv]; + caches.t3 = new float[subdiv]; - 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; + 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()