diff --git a/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs b/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs index c2b0d299a..91ebde4e3 100644 --- a/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs +++ b/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs @@ -119,7 +119,7 @@ namespace NBF Log.Error($"SetObiRopeStretch={value}"); if (value > 3) { - value -= 0.2f; + // value -= 0.2f; } fishingRope.SetTargetLength(value); } diff --git a/Assets/Scripts/Fishing/Rope/Rope.cs b/Assets/Scripts/Fishing/Rope/Rope.cs index 019fd2a36..15cac5018 100644 --- a/Assets/Scripts/Fishing/Rope/Rope.cs +++ b/Assets/Scripts/Fishing/Rope/Rope.cs @@ -28,6 +28,9 @@ public class Rope : MonoBehaviour [SerializeField, Range(1, 80), Tooltip("迭代次数。鱼线 10~30 通常够用")] private int iterations = 20; + [SerializeField, Range(0, 16), Tooltip("主求解后追加的硬长度约束次数。只负责把 poly 拉回到 rest total,不改变可变长度逻辑")] + private int hardTightenIterations = 2; + [Header("Length Control (No Min/Max Clamp)")] [Tooltip("初始总长度(米)。如果为 0,则用 physicsSegmentLen*(minPhysicsNodes-1) 作为初始长度")] [SerializeField, Min(0f)] @@ -65,6 +68,9 @@ public class Rope : MonoBehaviour [SerializeField, Range(1, 8), Tooltip("每隔多少次FixedUpdate更新一次地面约束")] private int groundUpdateEvery = 2; + [SerializeField, Range(0, 8), Tooltip("地面约束后,再做几次长度约束,减少 poly 被地面抬长")] + private int groundPostConstraintIterations = 2; + private int _groundFrameCounter; [Header("Simple Water Float (Cheap)")] [SerializeField, Tooltip("绳子落到水面以下时,是否把节点约束回水面")] @@ -188,6 +194,7 @@ public class Rope : MonoBehaviour renderSubdivisionsIdle = Mathf.Max(renderSubdivisionsIdle, 1); renderSubdivisionsMoving = Mathf.Max(renderSubdivisionsMoving, 1); iterations = Mathf.Clamp(iterations, 1, 80); + hardTightenIterations = Mathf.Clamp(hardTightenIterations, 0, 16); groundCastDistance = Mathf.Max(groundCastDistance, 0.01f); groundCastHeight = Mathf.Max(groundCastHeight, 0f); lineWidth = Mathf.Max(lineWidth, 0.0001f); @@ -203,6 +210,7 @@ public class Rope : MonoBehaviour groundSampleStep = Mathf.Max(1, groundSampleStep); groundUpdateEvery = Mathf.Max(1, groundUpdateEvery); + groundPostConstraintIterations = Mathf.Clamp(groundPostConstraintIterations, 0, 8); waterSampleStep = Mathf.Max(1, waterSampleStep); waterUpdateEvery = Mathf.Max(1, waterUpdateEvery); @@ -302,11 +310,27 @@ public class Rope : MonoBehaviour public void DebugLength() { + float solverRestTotal = (_physicsNodes - 2) * physicsSegmentLen + _headRestLen; + float poly = GetPhysicsPolylineLength(); + float maxSegDelta = 0f; + float avgSegDelta = 0f; + for (int i = 1; i < _physicsNodes; i++) + { + float rest = (i == 1) ? _headRestLen : physicsSegmentLen; + float segLen = Vector3.Distance(_pCurr[i - 1], _pCurr[i]); + float delta = segLen - rest; + if (delta > maxSegDelta) maxSegDelta = delta; + avgSegDelta += delta; + } + + if (_physicsNodes > 1) + avgSegDelta /= (_physicsNodes - 1); + Debug.Log( $"current={_currentLength}, target={_targetLength}, nodes={_physicsNodes}, " + $"seg={physicsSegmentLen}, head={_headRestLen}, headMin={headMinLen}, " + - $"solverRestTotal={(_physicsNodes - 2) * physicsSegmentLen + _headRestLen}, " + - $"poly={GetPhysicsPolylineLength()}" + $"solverRestTotal={solverRestTotal}, poly={poly}, delta={poly - solverRestTotal}, " + + $"maxSegDelta={maxSegDelta}, avgSegDelta={avgSegDelta}" ); } @@ -336,6 +360,7 @@ public class Rope : MonoBehaviour SolveDistanceConstraints_HeadOnly_Fast(); } + SolveHardDistanceConstraints(hardTightenIterations); LockAnchorsHard(); if (constrainToGround) @@ -345,6 +370,7 @@ public class Rope : MonoBehaviour { _groundFrameCounter = 0; ConstrainToGround(); + SolveHardDistanceConstraints(groundPostConstraintIterations); } } @@ -357,10 +383,7 @@ public class Rope : MonoBehaviour ConstrainToWaterSurface(); // 水面抬升后补几次长度约束,让形状更顺一点 - for (int it = 0; it < waterPostConstraintIterations; it++) - { - SolveDistanceConstraints_HeadOnly_Fast(); - } + SolveHardDistanceConstraints(waterPostConstraintIterations); } } @@ -522,9 +545,37 @@ public class Rope : MonoBehaviour private void SolveDistanceConstraints_HeadOnly_Fast() { - int last = _physicsNodes - 1; + SolveDistanceConstraints_HeadOnly_Bidirectional(stiffness); + } - for (int i = 0; i < last; i++) + private void SolveHardDistanceConstraints(int extraIterations) + { + for (int it = 0; it < extraIterations; it++) + { + LockAnchorsHard(); + SolveDistanceConstraints_HeadOnly_Hard(); + } + } + + private void SolveDistanceConstraints_HeadOnly_Hard() + { + SolveDistanceConstraints_HeadOnly_Bidirectional(1f); + } + + private void SolveDistanceConstraints_HeadOnly_Bidirectional(float combinedStiffness) + { + int last = _physicsNodes - 1; + if (last <= 0) return; + + float clamped = Mathf.Clamp01(combinedStiffness); + float sweepStiffness = (clamped >= 0.999999f) ? 1f : 1f - Mathf.Sqrt(1f - clamped); + SolveDistanceConstraintsSweep_Fast(0, last, 1, last, sweepStiffness); + SolveDistanceConstraintsSweep_Fast(last - 1, -1, -1, last, sweepStiffness); + } + + private void SolveDistanceConstraintsSweep_Fast(int start, int endExclusive, int step, int last, float sweepStiffness) + { + for (int i = start; i != endExclusive; i += step) { float rest = (i == 0) ? _headRestLen : physicsSegmentLen; @@ -537,7 +588,7 @@ public class Rope : MonoBehaviour float dist = Mathf.Sqrt(sq); float diff = (dist - rest) / dist; - Vector3 corr = delta * (diff * stiffness); + Vector3 corr = delta * (diff * sweepStiffness); bool aLocked = (i == 0); bool bLocked = (i + 1 == last); @@ -766,7 +817,8 @@ public class Rope : MonoBehaviour (-p0 + 3f * p1 - 3f * p2 + p3) * t3 ); - cr.y = p1.y + (p2.y - p1.y) * t; + // y 也使用平滑曲线,再做单调夹紧;避免垂直时因为线性 y 插值导致切线断裂,看起来像折线。 + cr.y = ClampMonotonic(cr.y, p0.y, p1.y, p2.y, p3.y); _rPoints[idx++] = cr; } @@ -778,6 +830,18 @@ public class Rope : MonoBehaviour _lineRenderer.SetPositions(_rPoints); } + private static float ClampMonotonic(float value, float p0, float p1, float p2, float p3) + { + bool rising = p0 <= p1 && p1 <= p2 && p2 <= p3; + bool falling = p0 >= p1 && p1 >= p2 && p2 >= p3; + if (!rising && !falling) + return value; + + float min = Mathf.Min(p1, p2); + float max = Mathf.Max(p1, p2); + return Mathf.Clamp(value, min, max); + } + private int PickRenderSubdivisions_Fast() { int idle = Mathf.Max(1, renderSubdivisionsIdle);