From 6e918bb1a662096b901e983cfa107982e571e8d6 Mon Sep 17 00:00:00 2001 From: "Bob.Song" <605277374@qq.com> Date: Sun, 12 Apr 2026 23:40:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=96=E6=B6=88=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Scripts/Fishing/Rope/Rope.cs | 2138 +++++++++++++-------------- 1 file changed, 1069 insertions(+), 1069 deletions(-) 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); + } +}