diff --git a/Assets/Scripts/Fishing/Rope/Rope.cs b/Assets/Scripts/Fishing/Rope/Rope.cs
index 3bd8a0f25..03a9cf3a8 100644
--- a/Assets/Scripts/Fishing/Rope/Rope.cs
+++ b/Assets/Scripts/Fishing/Rope/Rope.cs
@@ -1,1069 +1,1069 @@
-// 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;
-//
-// [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)]
-// 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;
-//
-// [SerializeField, Range(0, 8), Tooltip("地面约束后,再做几次长度约束,减少 poly 被地面抬长")]
-// private int groundPostConstraintIterations = 2;
-//
-// private int _groundFrameCounter;
-//
-// [Header("Simple Water Float (Cheap)")] [SerializeField, Tooltip("绳子落到水面以下时,是否把节点约束回水面")]
-// private bool constrainToWaterSurface = true;
-//
-// [SerializeField, Tooltip("静态水面高度;如果你后面接波浪水面,可改成采样函数")]
-// private float waterLevelY = 0f;
-//
-// [SerializeField, Min(0f), Tooltip("把线抬到水面上方一点,避免视觉穿插")]
-// private float waterSurfaceOffset = 0.002f;
-//
-// [SerializeField, Range(1, 8), Tooltip("每隔多少个节点做一次水面约束采样;越大越省")]
-// private int waterSampleStep = 2;
-//
-// [SerializeField, Tooltip("未采样节点是否插值水面高度")]
-// private bool waterInterpolate = true;
-//
-// [SerializeField, Range(1, 8), Tooltip("每隔多少次FixedUpdate更新一次水面约束")]
-// private int waterUpdateEvery = 1;
-//
-// [SerializeField, Range(0f, 1f), Tooltip("水面约束抬升强度(每次更新的插值强度),越小越渐进")]
-// private float waterLiftStrength = 0.25f;
-//
-// [SerializeField, Tooltip("startAnchor 在水下时,让其相邻端节点强制跟随 startAnchor,避免被抬到水面导致脱离")]
-// private bool keepStartAdjacentNodeFollow = true;
-//
-// [SerializeField, Range(0, 8), Tooltip("水面约束后,再做几次长度约束,减少局部折角")]
-// private int waterPostConstraintIterations = 2;
-//
-// private int _waterFrameCounter;
-//
-// [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("Performance")] [SerializeField, Tooltip("远端玩家鱼线不可见时,直接停止整条渲染线的模拟与绘制")]
-// private bool cullRemoteRopeWhenInvisible = true;
-//
-// [SerializeField, Tooltip("本地玩家自己的鱼线始终保持完整计算")]
-// private bool localOwnerAlwaysSimulate = true;
-//
-// [SerializeField, Range(1, 60), Tooltip("每隔多少个 FixedUpdate 重新判断一次可见性")]
-// private int visibilityCheckEvery = 10;
-//
-// [SerializeField, Range(0f, 0.5f), Tooltip("屏幕边缘额外留白,避免刚进视野就闪现")]
-// private float visibilityViewportPadding = 0.08f;
-//
-// [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 Transform _cameraTr;
-// private int _visibilityCheckCounter;
-// private bool _isCulledByVisibility;
-// private int _tIdleSubdiv = -1;
-// private int _tMovingSubdiv = -1;
-//
-// private FRod _rod;
-// public void Init(FRod rod)
-// {
-// _rod = rod;
-// if (Application.isPlaying)
-// RefreshVisibilityState(true);
-// }
-//
-// // 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);
-//
-// RefreshAnchorTransforms();
-//
-// InitLengthSystem();
-// AllocateAndInitNodes();
-// EnsureRenderCaches();
-// RefreshVisibilityState(true);
-// }
-//
-// private void OnValidate()
-// {
-// 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);
-//
-// 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);
-//
-// 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);
-// waterSurfaceOffset = Mathf.Max(0f, waterSurfaceOffset);
-// waterLiftStrength = Mathf.Clamp01(waterLiftStrength);
-// waterPostConstraintIterations = Mathf.Clamp(waterPostConstraintIterations, 0, 8);
-// visibilityCheckEvery = Mathf.Clamp(visibilityCheckEvery, 1, 60);
-// visibilityViewportPadding = Mathf.Clamp(visibilityViewportPadding, 0f, 0.5f);
-// }
-//
-// private void RefreshAnchorTransforms()
-// {
-// _startTr = startAnchor ? startAnchor.transform : null;
-// _endTr = endAnchor ? endAnchor.transform : null;
-// }
-//
-// private bool ShouldAlwaysSimulate()
-// {
-// if (!localOwnerAlwaysSimulate)
-// return false;
-//
-// var owner = _rod?.PlayerItem?.Owner;
-// return owner == null || owner.IsSelf;
-// }
-//
-// private Transform GetActiveCameraTransform()
-// {
-// Camera main = BaseCamera.Main;
-// if (main)
-// {
-// _cameraTr = main.transform;
-// return _cameraTr;
-// }
-//
-// if (!_cameraTr)
-// {
-// Camera fallback = Camera.main;
-// if (fallback)
-// _cameraTr = fallback.transform;
-// }
-//
-// return _cameraTr;
-// }
-//
-// private static bool IsViewportPointVisible(Vector3 viewportPoint, float padding)
-// {
-// if (viewportPoint.z <= 0f)
-// return false;
-//
-// return viewportPoint.x >= -padding && viewportPoint.x <= 1f + padding &&
-// viewportPoint.y >= -padding && viewportPoint.y <= 1f + padding;
-// }
-//
-// private bool IsVisibleToMainCamera()
-// {
-// Transform camTr = GetActiveCameraTransform();
-// if (!camTr)
-// return true;
-//
-// Camera cam = camTr.GetComponent();
-// if (!cam)
-// cam = BaseCamera.Main ? BaseCamera.Main : Camera.main;
-// if (!cam)
-// return true;
-//
-// Vector3 start = _startTr ? _startTr.position : (startAnchor ? startAnchor.position : transform.position);
-// Vector3 end = _endTr ? _endTr.position : (endAnchor ? endAnchor.position : transform.position);
-// Vector3 middle = (start + end) * 0.5f;
-// float padding = visibilityViewportPadding;
-//
-// return IsViewportPointVisible(cam.WorldToViewportPoint(start), padding) ||
-// IsViewportPointVisible(cam.WorldToViewportPoint(end), padding) ||
-// IsViewportPointVisible(cam.WorldToViewportPoint(middle), padding);
-// }
-//
-// private void RefreshVisibilityState(bool force = false)
-// {
-// if (!cullRemoteRopeWhenInvisible || ShouldAlwaysSimulate())
-// {
-// _isCulledByVisibility = false;
-// if (_lineRenderer)
-// _lineRenderer.enabled = true;
-// return;
-// }
-//
-// if (!force)
-// {
-// _visibilityCheckCounter++;
-// if (_visibilityCheckCounter < visibilityCheckEvery)
-// return;
-// }
-//
-// _visibilityCheckCounter = 0;
-// bool wasCulled = _isCulledByVisibility;
-// _isCulledByVisibility = !IsVisibleToMainCamera();
-//
-// if (_lineRenderer)
-// _lineRenderer.enabled = !_isCulledByVisibility;
-//
-// if (wasCulled && !_isCulledByVisibility)
-// SyncVisibleStateAfterCulling();
-// }
-//
-// private void SyncVisibleStateAfterCulling()
-// {
-// _currentLength = Mathf.Max(_targetLength, 0.01f);
-// UpdateNodesFromLength();
-// UpdateHeadRestLenFromCurrentLength();
-// ResetNodesBetweenAnchors();
-// LockAnchorsHard();
-// }
-//
-// private void ResetNodesBetweenAnchors()
-// {
-// if (_physicsNodes < 2)
-// return;
-//
-// Vector3 start = _startTr ? _startTr.position : (startAnchor ? startAnchor.position : transform.position);
-// Vector3 end = _endTr ? _endTr.position : (endAnchor ? endAnchor.position : transform.position);
-// int last = _physicsNodes - 1;
-//
-// for (int i = 0; i <= last; i++)
-// {
-// float t = (last > 0) ? i / (float)last : 0f;
-// Vector3 pos = Vector3.Lerp(start, end, t);
-// _pCurr[i] = pos;
-// _pPrev[i] = pos;
-// }
-// }
-//
-// private void EnsureRenderCaches()
-// {
-// int idle = Mathf.Max(1, renderSubdivisionsIdle);
-// if (_tIdleSubdiv != idle)
-// {
-// BuildTCaches(idle, ref _tIdle);
-// _tIdleSubdiv = idle;
-// }
-//
-// int moving = Mathf.Max(1, renderSubdivisionsMoving);
-// if (_tMovingSubdiv != moving)
-// {
-// BuildTCaches(moving, ref _tMoving);
-// _tMovingSubdiv = moving;
-// }
-//
-// int maxSubdiv = Mathf.Max(idle, moving);
-// int neededCapacity = (maxPhysicsNodes - 1) * maxSubdiv + 1;
-// if (_rPoints == null || neededCapacity > _rCapacity)
-// {
-// _rCapacity = neededCapacity;
-// _rPoints = new Vector3[_rCapacity];
-// }
-// }
-//
-// 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.RoundToInt(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;
-// public float GetLengthSmoothVel() => _lengthSmoothVel;
-//
-// public float GetLengthByPoints()
-// {
-// if (!smooth)
-// return GetPhysicsPolylineLength();
-//
-// if (_rPoints == null || _lineRenderer == null) return 0f;
-//
-// int count = _lineRenderer.positionCount;
-// if (count < 2) return 0f;
-//
-// float totalLength = 0f;
-// for (int i = 1; i < count; i++)
-// {
-// Vector3 a = _rPoints[i - 1];
-// Vector3 b = _rPoints[i];
-// totalLength += Vector3.Distance(a, b);
-// }
-//
-// return totalLength;
-// }
-//
-// public float GetPhysicsPolylineLength()
-// {
-// float total = 0f;
-// for (int i = 1; i < _physicsNodes; i++)
-// total += Vector3.Distance(_pCurr[i - 1], _pCurr[i]);
-// return total;
-// }
-//
-// 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={solverRestTotal}, poly={poly}, delta={poly - solverRestTotal}, " +
-// $"maxSegDelta={maxSegDelta}, avgSegDelta={avgSegDelta}"
-// );
-// }
-//
-// private void FixedUpdate()
-// {
-// if (!startAnchor || !endAnchor) return;
-//
-// RefreshAnchorTransforms();
-// RefreshVisibilityState();
-// if (_isCulledByVisibility)
-// return;
-//
-// _dt = Time.fixedDeltaTime;
-// if (_dt < 1e-6f) _dt = 1e-6f;
-// _dt2 = _dt * _dt;
-//
-// _gravity.y = -gravityStrength;
-//
-// _kY = Mathf.Exp(-airDrag * _dt);
-// _kXZ = Mathf.Exp(-airDragXZ * _dt);
-//
-// UpdateLengthSmooth();
-// UpdateNodesFromLength();
-// UpdateHeadRestLenFromCurrentLength();
-//
-// Simulate_VerletFast();
-//
-//
-// for (int it = 0; it < iterations; it++)
-// {
-// LockAnchorsHard();
-// SolveDistanceConstraints_HeadOnly_Fast();
-// }
-//
-// SolveHardDistanceConstraints(hardTightenIterations);
-// LockAnchorsHard();
-//
-// if (constrainToGround)
-// {
-// _groundFrameCounter++;
-// if (_groundFrameCounter >= groundUpdateEvery)
-// {
-// _groundFrameCounter = 0;
-// ConstrainToGround();
-// SolveHardDistanceConstraints(groundPostConstraintIterations);
-// }
-// }
-//
-// if (constrainToWaterSurface)
-// {
-// _waterFrameCounter++;
-// if (_waterFrameCounter >= waterUpdateEvery)
-// {
-// _waterFrameCounter = 0;
-// ConstrainToWaterSurface();
-//
-// // 水面抬升后补几次长度约束,让形状更顺一点
-// SolveHardDistanceConstraints(waterPostConstraintIterations);
-// }
-// }
-//
-// LockAnchorsHard();
-// }
-//
-// private void Update()
-// {
-// if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return;
-//
-// RefreshAnchorTransforms();
-// if (_isCulledByVisibility)
-// return;
-//
-// EnsureRenderCaches();
-//
-// int last = _physicsNodes - 1;
-//
-// Vector3 s = _startTr.position;
-// Vector3 e = _endTr.position;
-//
-// _pCurr[0] = s;
-// _pCurr[last] = e;
-// // _pPrev[0] = s;
-// // _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
-// );
-//
-// // 长度变化时额外压一点速度,减少收放线时抖动
-// float delta = Mathf.Abs(_targetLength - _currentLength);
-// if (delta > 0.0001f && lengthChangeVelocityKill > 0f)
-// {
-// float keep = 1f - Mathf.Clamp01(lengthChangeVelocityKill);
-// for (int i = 1; i < _physicsNodes - 1; i++)
-// {
-// Vector3 curr = _pCurr[i];
-// Vector3 prev = _pPrev[i];
-// Vector3 disp = curr - prev;
-// _pPrev[i] = curr - disp * keep;
-// }
-// }
-// }
-//
-// 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);
-// }
-//
-// 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);
-// }
-//
-// private void Simulate_VerletFast()
-// {
-// 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;
-// }
-//
-// private void SolveDistanceConstraints_HeadOnly_Fast()
-// {
-// SolveDistanceConstraints_HeadOnly_Bidirectional(stiffness);
-// }
-//
-// 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;
-//
-// 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;
-// Vector3 corr = delta * (diff * sweepStiffness);
-//
-// bool aLocked = (i == 0);
-// bool bLocked = (i + 1 == last);
-//
-// if (!aLocked && !bLocked)
-// {
-// _pCurr[i] = a + corr * 0.5f;
-// _pCurr[i + 1] = b - corr * 0.5f;
-// }
-// else if (aLocked && !bLocked)
-// {
-// _pCurr[i + 1] = b - corr; // 首段:node1 吃满
-// }
-// else if (!aLocked)
-// {
-// _pCurr[i] = a + corr; // 尾段:last-1 吃满
-// }
-// // 两边都锁的情况理论上不会出现
-// }
-// }
-//
-// private void ConstrainToGround()
-// {
-// if (groundMask == 0) return;
-//
-// int last = _physicsNodes - 1;
-// int step = Mathf.Max(1, groundSampleStep);
-//
-// 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)
-// {
-// 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
-// {
-// 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;
-//
-// // prev 同步抬上来,避免下一帧又被惯性拉回去造成抖动
-// Vector3 prev = _pPrev[i];
-// if (prev.y < minY) prev.y = minY;
-// _pPrev[i] = prev;
-// }
-// }
-//
-// private void ConstrainToWaterSurface()
-// {
-// int last = _physicsNodes - 1;
-// if (last <= 1) return;
-//
-// int step = Mathf.Max(1, waterSampleStep);
-// float surfaceY = waterLevelY + waterSurfaceOffset;
-// bool startUnderWater = _pCurr[0].y < surfaceY;
-// int startAdjacentIdx = GetStartAdjacentNodeIndex(last);
-//
-// int prevSampleIdx = 1;
-// float prevSurfaceY = surfaceY;
-//
-// ApplyWaterSurface(prevSampleIdx, prevSurfaceY, startUnderWater, startAdjacentIdx);
-//
-// for (int i = 1 + step; i < last; i += step)
-// {
-// float nextSurfaceY = surfaceY;
-// ApplyWaterSurface(i, nextSurfaceY, startUnderWater, startAdjacentIdx);
-//
-// if (waterInterpolate)
-// {
-// 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 y = Mathf.Lerp(prevSurfaceY, nextSurfaceY, t);
-// ApplyWaterSurface(idx, y, startUnderWater, startAdjacentIdx);
-// }
-// }
-// else
-// {
-// for (int idx = prevSampleIdx + 1; idx < i; idx++)
-// ApplyWaterSurface(idx, prevSurfaceY, startUnderWater, startAdjacentIdx);
-// }
-//
-// prevSampleIdx = i;
-// prevSurfaceY = nextSurfaceY;
-// }
-//
-// for (int i = prevSampleIdx + 1; i < last; i++)
-// ApplyWaterSurface(i, prevSurfaceY, startUnderWater, startAdjacentIdx);
-// }
-//
-// private int GetStartAdjacentNodeIndex(int last)
-// {
-// if (last <= 1) return 1;
-//
-// Vector3 s = _pCurr[0];
-// float d1 = (_pCurr[1] - s).sqrMagnitude;
-// float d2 = (_pCurr[last - 1] - s).sqrMagnitude;
-// return d1 <= d2 ? 1 : last - 1;
-// }
-//
-// private void ApplyWaterSurface(int i, float surfaceY, bool startUnderWater, int startAdjacentIdx)
-// {
-// if (keepStartAdjacentNodeFollow && startUnderWater && i == startAdjacentIdx)
-// {
-// Vector3 s = _pCurr[0];
-// _pCurr[i] = s;
-// _pPrev[i] = s;
-// return;
-// }
-//
-// Vector3 p = _pCurr[i];
-// if (p.y < surfaceY)
-// {
-// p.y = Mathf.Lerp(p.y, surfaceY, waterLiftStrength);
-// _pCurr[i] = p;
-//
-// // 渐进同步 prev,削弱向下惯性,避免反复穿透水面
-// Vector3 prev = _pPrev[i];
-// if (prev.y < p.y) prev.y = Mathf.Lerp(prev.y, p.y, waterLiftStrength);
-// _pPrev[i] = prev;
-// }
-// }
-//
-// 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 = 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];
-//
-// Vector3 cr =
-// 0.5f * (
-// (2f * p1) +
-// (-p0 + p2) * t +
-// (2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 +
-// (-p0 + 3f * p1 - 3f * p2 + p3) * t3
-// );
-//
-// // y 也使用平滑曲线,再做单调夹紧;避免垂直时因为线性 y 插值导致切线断裂,看起来像折线。
-// cr.y = ClampMonotonic(cr.y, p0.y, p1.y, p2.y, p3.y);
-//
-// _rPoints[idx++] = cr;
-// }
-// }
-//
-// _rPoints[idx++] = _pCurr[last];
-//
-// _lineRenderer.positionCount = idx;
-// _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);
-// int moving = Mathf.Max(1, renderSubdivisionsMoving);
-//
-// float thr = movingSpeedThreshold;
-// float thrSq = (thr * _dt) * (thr * _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);
-// }
-// }
+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;
+
+ [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)]
+ 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;
+
+ [SerializeField, Range(0, 8), Tooltip("地面约束后,再做几次长度约束,减少 poly 被地面抬长")]
+ private int groundPostConstraintIterations = 2;
+
+ private int _groundFrameCounter;
+
+ [Header("Simple Water Float (Cheap)")] [SerializeField, Tooltip("绳子落到水面以下时,是否把节点约束回水面")]
+ private bool constrainToWaterSurface = true;
+
+ [SerializeField, Tooltip("静态水面高度;如果你后面接波浪水面,可改成采样函数")]
+ private float waterLevelY = 0f;
+
+ [SerializeField, Min(0f), Tooltip("把线抬到水面上方一点,避免视觉穿插")]
+ private float waterSurfaceOffset = 0.002f;
+
+ [SerializeField, Range(1, 8), Tooltip("每隔多少个节点做一次水面约束采样;越大越省")]
+ private int waterSampleStep = 2;
+
+ [SerializeField, Tooltip("未采样节点是否插值水面高度")]
+ private bool waterInterpolate = true;
+
+ [SerializeField, Range(1, 8), Tooltip("每隔多少次FixedUpdate更新一次水面约束")]
+ private int waterUpdateEvery = 1;
+
+ [SerializeField, Range(0f, 1f), Tooltip("水面约束抬升强度(每次更新的插值强度),越小越渐进")]
+ private float waterLiftStrength = 0.25f;
+
+ [SerializeField, Tooltip("startAnchor 在水下时,让其相邻端节点强制跟随 startAnchor,避免被抬到水面导致脱离")]
+ private bool keepStartAdjacentNodeFollow = true;
+
+ [SerializeField, Range(0, 8), Tooltip("水面约束后,再做几次长度约束,减少局部折角")]
+ private int waterPostConstraintIterations = 2;
+
+ private int _waterFrameCounter;
+
+ [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("Performance")] [SerializeField, Tooltip("远端玩家鱼线不可见时,直接停止整条渲染线的模拟与绘制")]
+ private bool cullRemoteRopeWhenInvisible = true;
+
+ [SerializeField, Tooltip("本地玩家自己的鱼线始终保持完整计算")]
+ private bool localOwnerAlwaysSimulate = true;
+
+ [SerializeField, Range(1, 60), Tooltip("每隔多少个 FixedUpdate 重新判断一次可见性")]
+ private int visibilityCheckEvery = 10;
+
+ [SerializeField, Range(0f, 0.5f), Tooltip("屏幕边缘额外留白,避免刚进视野就闪现")]
+ private float visibilityViewportPadding = 0.08f;
+
+ [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 Transform _cameraTr;
+ private int _visibilityCheckCounter;
+ private bool _isCulledByVisibility;
+ private int _tIdleSubdiv = -1;
+ private int _tMovingSubdiv = -1;
+
+ private FRod _rod;
+ public void Init(FRod rod)
+ {
+ _rod = rod;
+ if (Application.isPlaying)
+ RefreshVisibilityState(true);
+ }
+
+ // 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);
+
+ RefreshAnchorTransforms();
+
+ InitLengthSystem();
+ AllocateAndInitNodes();
+ EnsureRenderCaches();
+ RefreshVisibilityState(true);
+ }
+
+ private void OnValidate()
+ {
+ 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);
+
+ 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);
+
+ 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);
+ waterSurfaceOffset = Mathf.Max(0f, waterSurfaceOffset);
+ waterLiftStrength = Mathf.Clamp01(waterLiftStrength);
+ waterPostConstraintIterations = Mathf.Clamp(waterPostConstraintIterations, 0, 8);
+ visibilityCheckEvery = Mathf.Clamp(visibilityCheckEvery, 1, 60);
+ visibilityViewportPadding = Mathf.Clamp(visibilityViewportPadding, 0f, 0.5f);
+ }
+
+ private void RefreshAnchorTransforms()
+ {
+ _startTr = startAnchor ? startAnchor.transform : null;
+ _endTr = endAnchor ? endAnchor.transform : null;
+ }
+
+ private bool ShouldAlwaysSimulate()
+ {
+ if (!localOwnerAlwaysSimulate)
+ return false;
+
+ var owner = _rod?.PlayerItem?.Owner;
+ return owner == null || owner.IsSelf;
+ }
+
+ private Transform GetActiveCameraTransform()
+ {
+ Camera main = BaseCamera.Main;
+ if (main)
+ {
+ _cameraTr = main.transform;
+ return _cameraTr;
+ }
+
+ if (!_cameraTr)
+ {
+ Camera fallback = Camera.main;
+ if (fallback)
+ _cameraTr = fallback.transform;
+ }
+
+ return _cameraTr;
+ }
+
+ private static bool IsViewportPointVisible(Vector3 viewportPoint, float padding)
+ {
+ if (viewportPoint.z <= 0f)
+ return false;
+
+ return viewportPoint.x >= -padding && viewportPoint.x <= 1f + padding &&
+ viewportPoint.y >= -padding && viewportPoint.y <= 1f + padding;
+ }
+
+ private bool IsVisibleToMainCamera()
+ {
+ Transform camTr = GetActiveCameraTransform();
+ if (!camTr)
+ return true;
+
+ Camera cam = camTr.GetComponent();
+ if (!cam)
+ cam = BaseCamera.Main ? BaseCamera.Main : Camera.main;
+ if (!cam)
+ return true;
+
+ Vector3 start = _startTr ? _startTr.position : (startAnchor ? startAnchor.position : transform.position);
+ Vector3 end = _endTr ? _endTr.position : (endAnchor ? endAnchor.position : transform.position);
+ Vector3 middle = (start + end) * 0.5f;
+ float padding = visibilityViewportPadding;
+
+ return IsViewportPointVisible(cam.WorldToViewportPoint(start), padding) ||
+ IsViewportPointVisible(cam.WorldToViewportPoint(end), padding) ||
+ IsViewportPointVisible(cam.WorldToViewportPoint(middle), padding);
+ }
+
+ private void RefreshVisibilityState(bool force = false)
+ {
+ if (!cullRemoteRopeWhenInvisible || ShouldAlwaysSimulate())
+ {
+ _isCulledByVisibility = false;
+ if (_lineRenderer)
+ _lineRenderer.enabled = true;
+ return;
+ }
+
+ if (!force)
+ {
+ _visibilityCheckCounter++;
+ if (_visibilityCheckCounter < visibilityCheckEvery)
+ return;
+ }
+
+ _visibilityCheckCounter = 0;
+ bool wasCulled = _isCulledByVisibility;
+ _isCulledByVisibility = !IsVisibleToMainCamera();
+
+ if (_lineRenderer)
+ _lineRenderer.enabled = !_isCulledByVisibility;
+
+ if (wasCulled && !_isCulledByVisibility)
+ SyncVisibleStateAfterCulling();
+ }
+
+ private void SyncVisibleStateAfterCulling()
+ {
+ _currentLength = Mathf.Max(_targetLength, 0.01f);
+ UpdateNodesFromLength();
+ UpdateHeadRestLenFromCurrentLength();
+ ResetNodesBetweenAnchors();
+ LockAnchorsHard();
+ }
+
+ private void ResetNodesBetweenAnchors()
+ {
+ if (_physicsNodes < 2)
+ return;
+
+ Vector3 start = _startTr ? _startTr.position : (startAnchor ? startAnchor.position : transform.position);
+ Vector3 end = _endTr ? _endTr.position : (endAnchor ? endAnchor.position : transform.position);
+ int last = _physicsNodes - 1;
+
+ for (int i = 0; i <= last; i++)
+ {
+ float t = (last > 0) ? i / (float)last : 0f;
+ Vector3 pos = Vector3.Lerp(start, end, t);
+ _pCurr[i] = pos;
+ _pPrev[i] = pos;
+ }
+ }
+
+ private void EnsureRenderCaches()
+ {
+ int idle = Mathf.Max(1, renderSubdivisionsIdle);
+ if (_tIdleSubdiv != idle)
+ {
+ BuildTCaches(idle, ref _tIdle);
+ _tIdleSubdiv = idle;
+ }
+
+ int moving = Mathf.Max(1, renderSubdivisionsMoving);
+ if (_tMovingSubdiv != moving)
+ {
+ BuildTCaches(moving, ref _tMoving);
+ _tMovingSubdiv = moving;
+ }
+
+ int maxSubdiv = Mathf.Max(idle, moving);
+ int neededCapacity = (maxPhysicsNodes - 1) * maxSubdiv + 1;
+ if (_rPoints == null || neededCapacity > _rCapacity)
+ {
+ _rCapacity = neededCapacity;
+ _rPoints = new Vector3[_rCapacity];
+ }
+ }
+
+ 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.RoundToInt(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;
+ public float GetLengthSmoothVel() => _lengthSmoothVel;
+
+ public float GetLengthByPoints()
+ {
+ if (!smooth)
+ return GetPhysicsPolylineLength();
+
+ if (_rPoints == null || _lineRenderer == null) return 0f;
+
+ int count = _lineRenderer.positionCount;
+ if (count < 2) return 0f;
+
+ float totalLength = 0f;
+ for (int i = 1; i < count; i++)
+ {
+ Vector3 a = _rPoints[i - 1];
+ Vector3 b = _rPoints[i];
+ totalLength += Vector3.Distance(a, b);
+ }
+
+ return totalLength;
+ }
+
+ public float GetPhysicsPolylineLength()
+ {
+ float total = 0f;
+ for (int i = 1; i < _physicsNodes; i++)
+ total += Vector3.Distance(_pCurr[i - 1], _pCurr[i]);
+ return total;
+ }
+
+ 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={solverRestTotal}, poly={poly}, delta={poly - solverRestTotal}, " +
+ $"maxSegDelta={maxSegDelta}, avgSegDelta={avgSegDelta}"
+ );
+ }
+
+ private void FixedUpdate()
+ {
+ if (!startAnchor || !endAnchor) return;
+
+ RefreshAnchorTransforms();
+ RefreshVisibilityState();
+ if (_isCulledByVisibility)
+ return;
+
+ _dt = Time.fixedDeltaTime;
+ if (_dt < 1e-6f) _dt = 1e-6f;
+ _dt2 = _dt * _dt;
+
+ _gravity.y = -gravityStrength;
+
+ _kY = Mathf.Exp(-airDrag * _dt);
+ _kXZ = Mathf.Exp(-airDragXZ * _dt);
+
+ UpdateLengthSmooth();
+ UpdateNodesFromLength();
+ UpdateHeadRestLenFromCurrentLength();
+
+ Simulate_VerletFast();
+
+
+ for (int it = 0; it < iterations; it++)
+ {
+ LockAnchorsHard();
+ SolveDistanceConstraints_HeadOnly_Fast();
+ }
+
+ SolveHardDistanceConstraints(hardTightenIterations);
+ LockAnchorsHard();
+
+ if (constrainToGround)
+ {
+ _groundFrameCounter++;
+ if (_groundFrameCounter >= groundUpdateEvery)
+ {
+ _groundFrameCounter = 0;
+ ConstrainToGround();
+ SolveHardDistanceConstraints(groundPostConstraintIterations);
+ }
+ }
+
+ if (constrainToWaterSurface)
+ {
+ _waterFrameCounter++;
+ if (_waterFrameCounter >= waterUpdateEvery)
+ {
+ _waterFrameCounter = 0;
+ ConstrainToWaterSurface();
+
+ // 水面抬升后补几次长度约束,让形状更顺一点
+ SolveHardDistanceConstraints(waterPostConstraintIterations);
+ }
+ }
+
+ LockAnchorsHard();
+ }
+
+ private void Update()
+ {
+ if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return;
+
+ RefreshAnchorTransforms();
+ if (_isCulledByVisibility)
+ return;
+
+ EnsureRenderCaches();
+
+ int last = _physicsNodes - 1;
+
+ Vector3 s = _startTr.position;
+ Vector3 e = _endTr.position;
+
+ _pCurr[0] = s;
+ _pCurr[last] = e;
+ // _pPrev[0] = s;
+ // _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
+ );
+
+ // 长度变化时额外压一点速度,减少收放线时抖动
+ float delta = Mathf.Abs(_targetLength - _currentLength);
+ if (delta > 0.0001f && lengthChangeVelocityKill > 0f)
+ {
+ float keep = 1f - Mathf.Clamp01(lengthChangeVelocityKill);
+ for (int i = 1; i < _physicsNodes - 1; i++)
+ {
+ Vector3 curr = _pCurr[i];
+ Vector3 prev = _pPrev[i];
+ Vector3 disp = curr - prev;
+ _pPrev[i] = curr - disp * keep;
+ }
+ }
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+
+ private void Simulate_VerletFast()
+ {
+ 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;
+ }
+
+ private void SolveDistanceConstraints_HeadOnly_Fast()
+ {
+ SolveDistanceConstraints_HeadOnly_Bidirectional(stiffness);
+ }
+
+ 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;
+
+ 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;
+ Vector3 corr = delta * (diff * sweepStiffness);
+
+ bool aLocked = (i == 0);
+ bool bLocked = (i + 1 == last);
+
+ if (!aLocked && !bLocked)
+ {
+ _pCurr[i] = a + corr * 0.5f;
+ _pCurr[i + 1] = b - corr * 0.5f;
+ }
+ else if (aLocked && !bLocked)
+ {
+ _pCurr[i + 1] = b - corr; // 首段:node1 吃满
+ }
+ else if (!aLocked)
+ {
+ _pCurr[i] = a + corr; // 尾段:last-1 吃满
+ }
+ // 两边都锁的情况理论上不会出现
+ }
+ }
+
+ private void ConstrainToGround()
+ {
+ if (groundMask == 0) return;
+
+ int last = _physicsNodes - 1;
+ int step = Mathf.Max(1, groundSampleStep);
+
+ 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)
+ {
+ 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
+ {
+ 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;
+
+ // prev 同步抬上来,避免下一帧又被惯性拉回去造成抖动
+ Vector3 prev = _pPrev[i];
+ if (prev.y < minY) prev.y = minY;
+ _pPrev[i] = prev;
+ }
+ }
+
+ private void ConstrainToWaterSurface()
+ {
+ int last = _physicsNodes - 1;
+ if (last <= 1) return;
+
+ int step = Mathf.Max(1, waterSampleStep);
+ float surfaceY = waterLevelY + waterSurfaceOffset;
+ bool startUnderWater = _pCurr[0].y < surfaceY;
+ int startAdjacentIdx = GetStartAdjacentNodeIndex(last);
+
+ int prevSampleIdx = 1;
+ float prevSurfaceY = surfaceY;
+
+ ApplyWaterSurface(prevSampleIdx, prevSurfaceY, startUnderWater, startAdjacentIdx);
+
+ for (int i = 1 + step; i < last; i += step)
+ {
+ float nextSurfaceY = surfaceY;
+ ApplyWaterSurface(i, nextSurfaceY, startUnderWater, startAdjacentIdx);
+
+ if (waterInterpolate)
+ {
+ 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 y = Mathf.Lerp(prevSurfaceY, nextSurfaceY, t);
+ ApplyWaterSurface(idx, y, startUnderWater, startAdjacentIdx);
+ }
+ }
+ else
+ {
+ for (int idx = prevSampleIdx + 1; idx < i; idx++)
+ ApplyWaterSurface(idx, prevSurfaceY, startUnderWater, startAdjacentIdx);
+ }
+
+ prevSampleIdx = i;
+ prevSurfaceY = nextSurfaceY;
+ }
+
+ for (int i = prevSampleIdx + 1; i < last; i++)
+ ApplyWaterSurface(i, prevSurfaceY, startUnderWater, startAdjacentIdx);
+ }
+
+ private int GetStartAdjacentNodeIndex(int last)
+ {
+ if (last <= 1) return 1;
+
+ Vector3 s = _pCurr[0];
+ float d1 = (_pCurr[1] - s).sqrMagnitude;
+ float d2 = (_pCurr[last - 1] - s).sqrMagnitude;
+ return d1 <= d2 ? 1 : last - 1;
+ }
+
+ private void ApplyWaterSurface(int i, float surfaceY, bool startUnderWater, int startAdjacentIdx)
+ {
+ if (keepStartAdjacentNodeFollow && startUnderWater && i == startAdjacentIdx)
+ {
+ Vector3 s = _pCurr[0];
+ _pCurr[i] = s;
+ _pPrev[i] = s;
+ return;
+ }
+
+ Vector3 p = _pCurr[i];
+ if (p.y < surfaceY)
+ {
+ p.y = Mathf.Lerp(p.y, surfaceY, waterLiftStrength);
+ _pCurr[i] = p;
+
+ // 渐进同步 prev,削弱向下惯性,避免反复穿透水面
+ Vector3 prev = _pPrev[i];
+ if (prev.y < p.y) prev.y = Mathf.Lerp(prev.y, p.y, waterLiftStrength);
+ _pPrev[i] = prev;
+ }
+ }
+
+ 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 = 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];
+
+ Vector3 cr =
+ 0.5f * (
+ (2f * p1) +
+ (-p0 + p2) * t +
+ (2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 +
+ (-p0 + 3f * p1 - 3f * p2 + p3) * t3
+ );
+
+ // y 也使用平滑曲线,再做单调夹紧;避免垂直时因为线性 y 插值导致切线断裂,看起来像折线。
+ cr.y = ClampMonotonic(cr.y, p0.y, p1.y, p2.y, p3.y);
+
+ _rPoints[idx++] = cr;
+ }
+ }
+
+ _rPoints[idx++] = _pCurr[last];
+
+ _lineRenderer.positionCount = idx;
+ _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);
+ int moving = Mathf.Max(1, renderSubdivisionsMoving);
+
+ float thr = movingSpeedThreshold;
+ float thrSq = (thr * _dt) * (thr * _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);
+ }
+}