diff --git a/Assets/AssetCaches.asset b/Assets/AssetCaches.asset index aeaf1845f..179c02736 100644 --- a/Assets/AssetCaches.asset +++ b/Assets/AssetCaches.asset @@ -41538,8 +41538,8 @@ MonoBehaviour: Dependencies: [] Tags: - Name: main/plyaer.bundle - Hash: 3f8f7efec7b92de0aa2fee96a290c14b - Size: 378598645 + Hash: e4ec05a4b781eddaae2f07fc44c008f4 + Size: 378599896 Assets: - Path: Assets/ResRaw/Prefabs/Line/fishing line float set.prefab Address: Plyaer/fishing line float set diff --git a/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab b/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab index 5b4df8b76..71d345089 100644 --- a/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab +++ b/Assets/ResRaw/Prefabs/Line/fishing line float set.prefab @@ -50,31 +50,30 @@ MonoBehaviour: physicsSegmentLen: 0.1 minPhysicsNodes: 2 maxPhysicsNodes: 200 - gravityStrength: 2 + gravityStrength: 6 velocityDampen: 0.95 stiffness: 0.8 iterations: 20 initialLength: 0 lengthSmoothTime: 0.15 - lengthChangeVelocityKill: 0.6 + lengthChangeVelocityKill: 0.4 minSlack: 0.002 headMinLen: 0.01 - collisionMask: - serializedVersion: 2 - m_Bits: 24 + nodeHysteresis: 0.05 constrainToGround: 1 groundMask: serializedVersion: 2 - m_Bits: 4513599 + m_Bits: 24 groundRadius: 0.01 groundCastHeight: 1 groundCastDistance: 2.5 - constrainToWater: 0 - waterHeight: 0 - waterRadius: 0.01 - renderSubdivisions: 6 + renderSubdivisionsIdle: 6 + renderSubdivisionsMoving: 2 + movingSpeedThreshold: 2 smooth: 1 - lineWidth: 0.005 + lineWidth: 0.001 + airDrag: 0.2 + airDragXZ: 0.6 --- !u!120 &991521994724602848 LineRenderer: serializedVersion: 2 @@ -663,10 +662,10 @@ MonoBehaviour: m_EditorClassIdentifier: startAnchor: {fileID: 54298866000586118} endAnchor: {fileID: 54679398375713381} - physicsSegmentLen: 0.1 + physicsSegmentLen: 0.2 minPhysicsNodes: 2 maxPhysicsNodes: 120 - gravityStrength: 2 + gravityStrength: 6 velocityDampen: 0.95 stiffness: 0.8 iterations: 20 @@ -675,22 +674,21 @@ MonoBehaviour: lengthChangeVelocityKill: 0.6 minSlack: 0.002 headMinLen: 0.01 - collisionMask: - serializedVersion: 2 - m_Bits: 0 + nodeHysteresis: 0.05 constrainToGround: 1 groundMask: serializedVersion: 2 - m_Bits: 4513599 + m_Bits: 8 groundRadius: 0.01 groundCastHeight: 1 groundCastDistance: 2.5 - constrainToWater: 0 - waterHeight: 0 - waterRadius: 0.01 - renderSubdivisions: 6 + renderSubdivisionsIdle: 6 + renderSubdivisionsMoving: 2 + movingSpeedThreshold: 2 smooth: 1 - lineWidth: 0.005 + lineWidth: 0.001 + airDrag: 0.9 + airDragXZ: 0.6 --- !u!120 &484878994603287356 LineRenderer: serializedVersion: 2 diff --git a/Assets/Scripts/Fishing/Rope/Rope.cs b/Assets/Scripts/Fishing/Rope/Rope.cs index 15b948c6f..c82121df7 100644 --- a/Assets/Scripts/Fishing/Rope/Rope.cs +++ b/Assets/Scripts/Fishing/Rope/Rope.cs @@ -1,4 +1,5 @@ -using NBF; +using System; +using NBF; using UnityEngine; [RequireComponent(typeof(LineRenderer))] @@ -12,16 +13,16 @@ public class Rope : MonoBehaviour [SerializeField, Range(2, 200)] private int minPhysicsNodes = 12; - [SerializeField, Range(2, 400)] [Tooltip("物理节点上限(仅用于性能保护;与“最大长度不限制”不是一回事)")] + [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 常用")] + [SerializeField, Range(0.0f, 1.0f), Tooltip("约束修正强度,越大越硬。0.6~0.9 常用")] private float stiffness = 0.8f; - [SerializeField, Range(1, 80)] [Tooltip("迭代次数。鱼线 10~30 通常够用")] + [SerializeField, Range(1, 80), Tooltip("迭代次数。鱼线 10~30 通常够用")] private int iterations = 20; [Header("Length Control (No Min/Max Clamp)")] @@ -32,7 +33,7 @@ public class Rope : MonoBehaviour [Tooltip("长度变化平滑时间(越小越跟手,越大越稳)")] [SerializeField, Min(0.0001f)] private float lengthSmoothTime = 0.15f; - [Tooltip("当长度在变化时,额外把速度压掉一些(防抖)。0=不额外处理,1=变化时几乎清速度")] [SerializeField, Range(0f, 1f)] + [Tooltip("当长度在变化时,额外把速度压掉一些(防抖)。0=不额外处理,1=变化时几乎清速度(建议只在收线生效)")] [SerializeField, Range(0f, 1f)] private float lengthChangeVelocityKill = 0.6f; [Tooltip("允许的最小松弛余量(避免目标长度刚好等于锚点距离时抖动)")] [SerializeField, Min(0f)] @@ -41,9 +42,8 @@ public class Rope : MonoBehaviour [Header("Head Segment Clamp")] [Tooltip("第一段(起点->第1节点)允许的最小长度,避免收线时第一段被压到0导致数值炸")] [SerializeField, Min(0.0001f)] private float headMinLen = 0.01f; - - [Header("Collision Filter")] [SerializeField, Tooltip("只对这些Layer进行物理检测(Raycast/SphereCast等)。不在这里的层完全不检测。")] - private LayerMask collisionMask = ~0; + [Header("Node Count Stability")] [SerializeField, Tooltip("节点数切换迟滞(米)。避免长度在临界点抖动导致节点数来回跳 -> 卡顿")] + private float nodeHysteresis = 0.05f; [Header("Simple Ground/Water Constraint (Cheap)")] [SerializeField] private bool constrainToGround = true; @@ -53,87 +53,67 @@ public class Rope : MonoBehaviour [SerializeField, Min(0f)] private float groundCastHeight = 1.0f; [SerializeField, Min(0.01f)] private float groundCastDistance = 2.5f; - [SerializeField] private bool constrainToWater = false; - [SerializeField] private float waterHeight = 0f; - [SerializeField, Min(0f)] private float waterRadius = 0.01f; + [Header("Render (High Resolution)")] [SerializeField, Min(1), Tooltip("静止时每段物理线段插值加密数量(越大越顺,越耗)")] + private int renderSubdivisionsIdle = 6; - [Header("Render (High Resolution)")] [SerializeField, Min(1), Tooltip("每段物理线段插值加密的数量(越大越顺,越耗)")] - private int renderSubdivisions = 6; + [SerializeField, Min(1), Tooltip("甩动时每段物理线段插值加密数量(动态降LOD以防卡顿)")] + private int renderSubdivisionsMoving = 2; - - [Header("Air / Wind (For Fishing Line Feel)")] - [SerializeField, Range(0f, 5f), Tooltip("空气线性阻力(越大越不飘,空中更自然)")] - private float airDrag = 0.9f; + [SerializeField, Min(0f), Tooltip("平均速度超过该阈值认为在甩动(用于动态降 subdiv)")] + private float movingSpeedThreshold = 2.0f; - [SerializeField, Range(0f, 2f), Tooltip("横向额外阻力(减少左右飘得太夸张)")] - private float airDragXZ = 0.6f; - - [SerializeField, Tooltip("风方向(世界空间)")] - private Vector3 windDir = new Vector3(1f, 0f, 0f); - - [SerializeField, Range(0f, 10f), Tooltip("基础风强度(m/s 级别的感觉)")] - private float windStrength = 0.3f; - - [SerializeField, Range(0f, 2f), Tooltip("阵风幅度(0=无阵风)")] - private float windGust = 0.25f; - - [SerializeField, Range(0.1f, 5f), Tooltip("阵风频率")] - private float windFreq = 1.2f; - - [Header("Bending (Smooth Curve)")] - [SerializeField, Range(0f, 1f), Tooltip("抗折/弯曲刚度(0=完全不抗折,0.1~0.3 比较像鱼线)")] - private float bendStiffness = 0.18f; - - - - [SerializeField, Tooltip("是否使用 Catmull-Rom 平滑(推荐开启)")] + [SerializeField, Tooltip("是否使用 Catmull-Rom 平滑(开启更顺,但更耗)")] private bool smooth = true; [SerializeField, Min(0.0001f)] private float lineWidth = 0.002f; - private LineRenderer lr; + [Header("Air Drag (Stable)")] [SerializeField, Range(0f, 5f), Tooltip("空气阻力(Y向),指数衰减,越大越不飘")] + private float airDrag = 0.9f; - // physics - private int physicsNodes; - private Vector3[] pCurr; - private Vector3[] pPrev; + [SerializeField, Range(0f, 2f), Tooltip("横向额外阻力(XZ),指数衰减,越大越不左右飘")] + private float airDragXZ = 0.6f; + + private LineRenderer _lineRenderer; + + // physics (注意:数组固定为 maxPhysicsNodes,physicsNodes 表示有效长度) + private int _physicsNodes; + private Vector3[] _pCurr; + private Vector3[] _pPrev; // render - private Vector3[] rPoints; - private int rCountCached = -1; + private Vector3[] _rPoints; + private int _rCountCached = -1; - private Vector3 gravity; + private Vector3 _gravity; // length control runtime - private float targetLength; - private float currentLength; - private float lengthSmoothVel; + private float _targetLength; + private float _currentLength; + private float _lengthSmoothVel; - // Only-head-change trick: - // Total rest length = headRestLen + (physicsNodes - 2) * physicsSegmentLen - private float headRestLen; + // rest length head + private float _headRestLen; + // node stability + private int _lastDesiredNodes = 0; + + private FRod _rod; + public void Init(FRod rod) => _rod = rod; private void Awake() { - lr = GetComponent(); - gravity = new Vector3(0f, -gravityStrength, 0f); + _lineRenderer = GetComponent(); + _gravity = new Vector3(0f, -gravityStrength, 0f); InitLengthSystem(); AllocateAndInitNodes(); - RebuildRenderBufferIfNeeded(); - } - - private FRod _rod; - - public void Init(FRod rod) - { - _rod = rod; + RebuildRenderBufferIfNeeded(renderSubdivisionsIdle); } private void OnValidate() { - renderSubdivisions = Mathf.Max(renderSubdivisions, 1); + renderSubdivisionsIdle = Mathf.Max(renderSubdivisionsIdle, 1); + renderSubdivisionsMoving = Mathf.Max(renderSubdivisionsMoving, 1); iterations = Mathf.Clamp(iterations, 1, 80); groundCastDistance = Mathf.Max(groundCastDistance, 0.01f); groundCastHeight = Mathf.Max(groundCastHeight, 0f); @@ -146,35 +126,33 @@ public class Rope : MonoBehaviour maxPhysicsNodes = Mathf.Max(maxPhysicsNodes, minPhysicsNodes); headMinLen = Mathf.Max(headMinLen, 0.0001f); - - // 如果你希望只用一个mask控制,避免 groundMask 忘了配 - if (groundMask == ~0) - groundMask = collisionMask; + nodeHysteresis = Mathf.Max(0f, nodeHysteresis); + } private void InitLengthSystem() { - // 没有 min/max 长度限制:初始长度只做一个“非负”保障 float defaultLen = physicsSegmentLen * (Mathf.Max(minPhysicsNodes, 2) - 1); - currentLength = (initialLength > 0f) ? initialLength : defaultLen; - targetLength = currentLength; + _currentLength = (initialLength > 0f) ? initialLength : defaultLen; + _targetLength = _currentLength; } private void AllocateAndInitNodes() { - physicsNodes = Mathf.Clamp(ComputeDesiredNodes(currentLength), 2, maxPhysicsNodes); - pCurr = new Vector3[physicsNodes]; - pPrev = new Vector3[physicsNodes]; + _physicsNodes = Mathf.Clamp(ComputeDesiredNodesStable(_currentLength), 2, maxPhysicsNodes); + + // ✅ 永久分配到最大,运行时不再 new,避免GC卡顿 + _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++) + for (int i = 0; i < _physicsNodes; i++) { Vector3 pos = start + dir * (physicsSegmentLen * i); - pCurr[i] = pos; - pPrev[i] = pos; + _pCurr[i] = pos; + _pPrev[i] = pos; } UpdateHeadRestLenFromCurrentLength(); @@ -185,190 +163,195 @@ public class Rope : MonoBehaviour private int ComputeDesiredNodes(float lengthMeters) { - // nodes = floor(length/segLen)+1 int desired = Mathf.FloorToInt(Mathf.Max(0f, lengthMeters) / physicsSegmentLen) + 1; desired = Mathf.Clamp(desired, minPhysicsNodes, maxPhysicsNodes); return desired; } - /// 设置目标总长度(米)。不做最小/最大长度限制(最小可行由锚点距离决定)。 - public void SetTargetLength(float lengthMeters) + private int ComputeDesiredNodesStable(float lengthMeters) { - targetLength = Mathf.Max(0f, lengthMeters); - } + int desired = ComputeDesiredNodes(lengthMeters); - - public float GetCurrentLength() => currentLength; - public float GetTargetLength() => targetLength; - - public float GetAnchorDistance() - { - if (startAnchor != null && endAnchor != null) + if (_lastDesiredNodes == 0) { - return Vector3.Distance(startAnchor.position, endAnchor.position); + _lastDesiredNodes = desired; + return desired; } - return 0; + if (desired == _lastDesiredNodes) + return desired; + + // 边界(lastDesiredNodes 对应的长度临界) + float boundary = (_lastDesiredNodes - 1) * physicsSegmentLen; + + // 在临界附近就不切换,避免来回跳 + if (Mathf.Abs(lengthMeters - boundary) < nodeHysteresis) + return _lastDesiredNodes; + + _lastDesiredNodes = desired; + return desired; } + public void SetTargetLength(float lengthMeters) + { + _targetLength = Mathf.Max(0f, lengthMeters); + } + + public float GetCurrentLength() => _currentLength; + public float GetTargetLength() => _targetLength; private void FixedUpdate() { - if (!startAnchor || !endAnchor) - return; + if (!startAnchor || !endAnchor) return; - gravity.y = -gravityStrength; + _gravity.y = -gravityStrength; - UpdateLengthSmooth(); // 只保证 >= 锚点直线距离 + minSlack - UpdateNodesFromLength(); // 只从头部增/减节点 - UpdateHeadRestLenFromCurrentLength(); // 第一段补余量 => 变化集中在头部 + UpdateLengthSmooth(); + UpdateNodesFromLength(); // ✅ 一次性增减,无GC + UpdateHeadRestLenFromCurrentLength(); + // simulate Simulate(); + // 确保端点正确后再迭代 + LockAnchorsHard(); + + // constraints for (int it = 0; it < iterations; it++) - { SolveDistanceConstraints_HeadOnly(); - SolveBendConstraint(); - LockAnchorsHard(); - } - if (constrainToWater || constrainToGround) - ConstrainToGroundAndWater(); + // 迭代后锁一次足够 + LockAnchorsHard(); + if (constrainToGround) + ConstrainToGround(); + + // 约束后再锁一次(保险) LockAnchorsHard(); } - private void LateUpdate() { - if (!startAnchor || !endAnchor || pCurr == null || physicsNodes < 2) return; + if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return; - int last = physicsNodes - 1; + // 端点“跟手”:渲染前强行同步 transform,消除慢一拍 + int last = _physicsNodes - 1; Vector3 s = startAnchor.transform.position; Vector3 e = endAnchor.transform.position; - pCurr[0] = s; pPrev[0] = s; // ✅ 关键:同步 pPrev - pCurr[last] = e; pPrev[last] = e; // ✅ 关键:同步 pPrev + _pCurr[0] = s; + _pPrev[0] = s; + _pCurr[last] = e; + _pPrev[last] = e; DrawHighResLine(); } private void UpdateLengthSmooth() { - // float anchorDist = Vector3.Distance(startAnchor.position, endAnchor.position); - // float minFeasible = anchorDist + minSlack; float minFeasible = 0.01f; - // ✅ 最小长度 = 起点终点直线距离(+slack),最大不限制 - float desired = Mathf.Max(targetLength, minFeasible); + float desired = Mathf.Max(_targetLength, minFeasible); - float prevLen = currentLength; - - currentLength = Mathf.SmoothDamp( - currentLength, + _currentLength = Mathf.SmoothDamp( + _currentLength, desired, - ref lengthSmoothVel, + ref _lengthSmoothVel, lengthSmoothTime, Mathf.Infinity, Time.fixedDeltaTime ); - - float lenDelta = Mathf.Abs(currentLength - prevLen); - if (lenDelta > 1e-5f && lengthChangeVelocityKill > 0f && pPrev != null) - { - float kill = Mathf.Clamp01(lengthChangeVelocityKill); - for (int i = 1; i < physicsNodes - 1; i++) - pPrev[i] = Vector3.Lerp(pPrev[i], pCurr[i], kill); - } } private void UpdateNodesFromLength() { - int desired = ComputeDesiredNodes(currentLength); - if (desired == physicsNodes) return; + int desired = ComputeDesiredNodesStable(_currentLength); + desired = Mathf.Clamp(desired, 2, maxPhysicsNodes); + if (desired == _physicsNodes) return; - while (physicsNodes < desired) - AddNodeAtStart(); - - while (physicsNodes > desired) - RemoveNodeAtStart(); - - RebuildRenderBufferIfNeeded(); - } - - private void AddNodeAtStart() - { - int newCount = Mathf.Min(physicsNodes + 1, maxPhysicsNodes); - if (newCount == physicsNodes) return; - - Vector3[] newCurr = new Vector3[newCount]; - Vector3[] newPrev = new Vector3[newCount]; - - newCurr[0] = pCurr[0]; - newPrev[0] = pPrev[0]; - - for (int i = 2; i < newCount; i++) + if (desired > _physicsNodes) { - newCurr[i] = pCurr[i - 1]; - newPrev[i] = pPrev[i - 1]; + AddNodesAtStart(desired - _physicsNodes); + } + else + { + RemoveNodesAtStart(_physicsNodes - desired); } - Vector3 s = startAnchor.position; - Vector3 dir = Vector3.down; + _physicsNodes = desired; - if (physicsNodes >= 2) + // 渲染缓存按最大 subdiv 预留即可 + RebuildRenderBufferIfNeeded(renderSubdivisionsIdle); + } + + + 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; + + // 把 [1..oldCount-1] 整体往后挪 addCount,给 [1..addCount] 腾位置 + // oldCount-1 个元素(包含尾端锚点位,尾端后面会被锁) + Array.Copy(_pCurr, 1, _pCurr, 1 + addCount, oldCount - 1); + Array.Copy(_pPrev, 1, _pPrev, 1 + addCount, oldCount - 1); + + Vector3 s = startAnchor.position; + + // 方向:用“挪完后的第一个动态点”指向来估计 + Vector3 dir = Vector3.down; + int firstOld = 1 + addCount; + if (firstOld < 1 + addCount + (oldCount - 2) && oldCount >= 2) { - Vector3 toOld1 = (pCurr[1] - s); + Vector3 toOld1 = (_pCurr[firstOld] - s); if (toOld1.sqrMagnitude > 1e-6f) dir = toOld1.normalized; } - Vector3 pos = s + dir * physicsSegmentLen; - newCurr[1] = pos; - newPrev[1] = pos; + // 继承速度 + float dt = Time.fixedDeltaTime; + Vector3 inheritVel = Vector3.zero; + if (oldCount >= 2 && firstOld < maxPhysicsNodes) + { + inheritVel = (_pCurr[firstOld] - _pPrev[firstOld]) / Mathf.Max(dt, 1e-6f); + } - pCurr = newCurr; - pPrev = newPrev; - physicsNodes = newCount; + for (int k = 1; k <= addCount; k++) + { + Vector3 pos = s + dir * (physicsSegmentLen * k); + _pCurr[k] = pos; + _pPrev[k] = pos - inheritVel * dt; + } LockAnchorsHard(); } - private void RemoveNodeAtStart() + private void RemoveNodesAtStart(int removeCount) { - int newCount = Mathf.Max(physicsNodes - 1, 2); - if (newCount == physicsNodes) return; + if (removeCount <= 0) return; - Vector3[] newCurr = new Vector3[newCount]; - Vector3[] newPrev = new Vector3[newCount]; + int oldCount = _physicsNodes; + int newCount = Mathf.Max(oldCount - removeCount, 2); + removeCount = oldCount - newCount; + if (removeCount <= 0) return; - newCurr[0] = pCurr[0]; - newPrev[0] = pPrev[0]; - - for (int i = 1; i < newCount - 1; i++) - { - newCurr[i] = pCurr[i + 1]; - newPrev[i] = pPrev[i + 1]; - } - - newCurr[newCount - 1] = pCurr[physicsNodes - 1]; - newPrev[newCount - 1] = pPrev[physicsNodes - 1]; - - pCurr = newCurr; - pPrev = newPrev; - physicsNodes = newCount; + // 把 [1+removeCount .. 1+removeCount+(newCount-2)-1] 挪到 [1..newCount-2] + // 中间动态节点数量 = newCount-2 + Array.Copy(_pCurr, 1 + removeCount, _pCurr, 1, newCount - 2); + Array.Copy(_pPrev, 1 + removeCount, _pPrev, 1, newCount - 2); LockAnchorsHard(); } private void UpdateHeadRestLenFromCurrentLength() { - int fixedSegCount = Mathf.Max(0, physicsNodes - 2); + int fixedSegCount = Mathf.Max(0, _physicsNodes - 2); float baseLen = fixedSegCount * physicsSegmentLen; - headRestLen = currentLength - baseLen; - - // 第一段允许在一个合理范围内变动(太长会像橡皮筋,太短会炸) - headRestLen = Mathf.Clamp(headRestLen, headMinLen, physicsSegmentLen * 1.5f); + _headRestLen = _currentLength - baseLen; + _headRestLen = Mathf.Clamp(_headRestLen, headMinLen, physicsSegmentLen * 1.5f); } private void Simulate() @@ -376,91 +359,51 @@ public class Rope : MonoBehaviour float dt = Time.fixedDeltaTime; float invDt = 1f / Mathf.Max(dt, 1e-6f); - // 风方向归一化(避免填了0向量导致NaN) - Vector3 wDir = windDir; - if (wDir.sqrMagnitude < 1e-6f) wDir = Vector3.right; - wDir.Normalize(); + // 指数衰减:更稳定好调 + float kY = Mathf.Exp(-airDrag * dt); + float kXZ = Mathf.Exp(-airDragXZ * dt); - for (int i = 0; i < physicsNodes; i++) + for (int i = 1; i < _physicsNodes - 1; i++) { - // Verlet 速度(由当前位置和上一帧位置推出来) - Vector3 vel = (pCurr[i] - pPrev[i]) * invDt; + Vector3 vel = (_pCurr[i] - _pPrev[i]) * invDt; - // 先做“惯性推进” - Vector3 next = pCurr[i] + (pCurr[i] - pPrev[i]) * velocityDampen; + vel.x *= kXZ; + vel.z *= kXZ; + vel.y *= kY; - // 加速度 = 重力 + 空气阻力 + 风(相对速度) - Vector3 acc = gravity; + vel *= velocityDampen; - // --- 空气阻力(与速度成正比)--- - // drag = -vel * airDrag,并且横向更强一点 - Vector3 drag = -vel * airDrag; - drag.x *= (1f + airDragXZ); - drag.z *= (1f + airDragXZ); - acc += drag; + Vector3 next = _pCurr[i] + vel * dt; - // --- 风(让线在空中不那么“只会垂直掉”)--- - if (i != 0 && i != physicsNodes - 1 && windStrength > 0f) - { - float t = Time.time; - float gust = 1f + Mathf.Sin(t * windFreq + i * 0.35f) * windGust; - - // windVel:风希望空气把线速度拉向这个“风速” - Vector3 windVel = wDir * (windStrength * gust); - - // 相对风:让加速度朝 (windVel - vel) 方向 - // 系数越大,越“被风带着走” - acc += (windVel - vel) * 0.5f; - } - - // Verlet:位置 += acc * dt^2 - pPrev[i] = pCurr[i]; - pCurr[i] = next + acc * (dt * dt); + _pPrev[i] = _pCurr[i]; + _pCurr[i] = next + _gravity * (dt * dt); } - - // 物理步末尾硬锁端点 - LockAnchorsHard(); } - - // private void Simulate() - // { - // float dt = Time.fixedDeltaTime; - // - // for (int i = 0; i < physicsNodes; i++) - // { - // Vector3 v = (pCurr[i] - pPrev[i]) * velocityDampen; - // pPrev[i] = pCurr[i]; - // - // pCurr[i] += v; - // pCurr[i] += gravity * dt; - // } - // - // LockAnchorsHard(); - // } private void LockAnchorsHard() { - if (!startAnchor || !endAnchor || pCurr == null || pPrev == null || physicsNodes < 2) return; + if (!startAnchor || !endAnchor || _pCurr == null || _pPrev == null || _physicsNodes < 2) return; + float dt = Time.fixedDeltaTime; Vector3 s = startAnchor.position; Vector3 e = endAnchor.position; - pCurr[0] = s; - pPrev[0] = s - startAnchor.linearVelocity * dt; + _pCurr[0] = s; + _pPrev[0] = s - startAnchor.linearVelocity * dt; - int last = physicsNodes - 1; - pCurr[last] = e; - pPrev[last] = e - endAnchor.linearVelocity * dt; + int last = _physicsNodes - 1; + _pCurr[last] = e; + _pPrev[last] = e - endAnchor.linearVelocity * dt; } private void SolveDistanceConstraints_HeadOnly() { - for (int i = 0; i < physicsNodes - 1; i++) + for (int i = 0; i < _physicsNodes - 1; i++) { - float rest = (i == 0) ? headRestLen : physicsSegmentLen; + float rest = (i == 0) ? _headRestLen : physicsSegmentLen; - Vector3 a = pCurr[i]; - Vector3 b = pCurr[i + 1]; + Vector3 a = _pCurr[i]; + Vector3 b = _pCurr[i + 1]; Vector3 delta = b - a; float dist = delta.magnitude; @@ -470,60 +413,23 @@ public class Rope : MonoBehaviour Vector3 corr = delta * diff * stiffness; if (i != 0) - pCurr[i] += corr * 0.5f; + _pCurr[i] += corr * 0.5f; - if (i + 1 != physicsNodes - 1) - pCurr[i + 1] -= corr * 0.5f; - } - } - - private void SolveBendConstraint() - { - if (bendStiffness <= 0f) return; - if (physicsNodes < 3) return; - - // bendStiffness 在迭代里用太大很容易爆,先做一个安全钳制 - float kBase = Mathf.Clamp01(bendStiffness); - - for (int i = 1; i < physicsNodes - 1; i++) - { - // 端点不要动(你本来就没动,这里保持) - if (i == 0 || i == physicsNodes - 1) continue; - - Vector3 mid = (pCurr[i - 1] + pCurr[i + 1]) * 0.5f; - - float k = kBase; - if (i <= 2) k *= 1.25f; // 靠近竿尖稍微更“直”一点 - - Vector3 old = pCurr[i]; - Vector3 newPos = Vector3.Lerp(old, mid, k); - - Vector3 delta = newPos - old; - - // ✅ 关键:同样把 pPrev 挪过去,避免“凭空制造速度” - pCurr[i] = newPos; - pPrev[i] += delta; + if (i + 1 != _physicsNodes - 1) + _pCurr[i + 1] -= corr * 0.5f; } } - private void ConstrainToGroundAndWater() + private void ConstrainToGround() { - int groundLayerMask = collisionMask & groundMask; // ✅ 统一过滤:只检测指定层 - - for (int i = 1; i < physicsNodes - 1; i++) + for (int i = 1; i < _physicsNodes - 1; i++) { - Vector3 p = pCurr[i]; + Vector3 p = _pCurr[i]; - if (constrainToWater) - { - float minY = waterHeight + waterRadius; - if (p.y < minY) p.y = minY; - } - - if (constrainToGround && groundLayerMask != 0) + if (constrainToGround && groundMask != 0) { Vector3 origin = p + Vector3.up * groundCastHeight; - if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundLayerMask, + if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask, QueryTriggerInteraction.Ignore)) { float minY = hit.point.y + groundRadius; @@ -531,64 +437,79 @@ public class Rope : MonoBehaviour } } - pCurr[i] = p; + _pCurr[i] = p; } } private void DrawHighResLine() { - if (pCurr == null || physicsNodes < 2) return; + if (_pCurr == null || _physicsNodes < 2) return; - RebuildRenderBufferIfNeeded(); - - lr.startWidth = lineWidth; - lr.endWidth = lineWidth; + _lineRenderer.startWidth = lineWidth; + _lineRenderer.endWidth = lineWidth; if (!smooth) { - lr.positionCount = physicsNodes; - lr.SetPositions(pCurr); + _lineRenderer.positionCount = _physicsNodes; + // 只传有效段 + // LineRenderer.SetPositions 会读数组的前 positionCount 个 + _lineRenderer.SetPositions(_pCurr); return; } + // 动态降 subdiv:甩动时降低点数,减少CPU开销 + int subdiv = PickRenderSubdivisions(); + RebuildRenderBufferIfNeeded(subdiv); + int idx = 0; - for (int seg = 0; seg < physicsNodes - 1; seg++) + for (int seg = 0; seg < _physicsNodes - 1; seg++) { - Vector3 p0 = pCurr[Mathf.Max(seg - 1, 0)]; - Vector3 p1 = pCurr[seg]; - Vector3 p2 = pCurr[seg + 1]; - Vector3 p3 = pCurr[Mathf.Min(seg + 2, physicsNodes - 1)]; + Vector3 p0 = _pCurr[Mathf.Max(seg - 1, 0)]; + Vector3 p1 = _pCurr[seg]; + Vector3 p2 = _pCurr[seg + 1]; + Vector3 p3 = _pCurr[Mathf.Min(seg + 2, _physicsNodes - 1)]; - for (int s = 0; s < renderSubdivisions; s++) + for (int s = 0; s < subdiv; s++) { - float t = s / (float)renderSubdivisions; + float t = s / (float)subdiv; Vector3 pt = CatmullRom_XZ_LinearY(p0, p1, p2, p3, t); - - // 如果水面约束开启:渲染点也夹一下,避免视觉上又穿回去 - if (constrainToWater) - { - float minY = waterHeight + waterRadius; - if (pt.y < minY) pt.y = minY; - } - - rPoints[idx++] = pt; + _rPoints[idx++] = pt; } } - rPoints[idx++] = pCurr[physicsNodes - 1]; + _rPoints[idx++] = _pCurr[_physicsNodes - 1]; - lr.positionCount = idx; - lr.SetPositions(rPoints); + _lineRenderer.positionCount = idx; + _lineRenderer.SetPositions(_rPoints); } - private void RebuildRenderBufferIfNeeded() + private int PickRenderSubdivisions() { - int targetCount = (physicsNodes - 1) * renderSubdivisions + 1; - if (rPoints == null || rCountCached != targetCount) + int idle = Mathf.Max(1, renderSubdivisionsIdle); + int moving = Mathf.Max(1, renderSubdivisionsMoving); + + // 计算平均速度(用 Verlet 差分) + float dt = Time.fixedDeltaTime; + float invDt = 1f / Mathf.Max(dt, 1e-6f); + + float sum = 0f; + int count = Mathf.Max(1, _physicsNodes - 2); + for (int i = 1; i < _physicsNodes - 1; i++) + sum += (_pCurr[i] - _pPrev[i]).magnitude * invDt; + + float avgSpeed = sum / count; + + return (avgSpeed > movingSpeedThreshold) ? moving : idle; + } + + private void RebuildRenderBufferIfNeeded(int subdiv) + { + int targetCount = (_physicsNodes - 1) * subdiv + 1; + if (_rPoints == null || _rCountCached != targetCount) { - rPoints = new Vector3[targetCount]; - rCountCached = targetCount; + _rPoints = new Vector3[targetCount]; + _rCountCached = targetCount; } } @@ -605,22 +526,18 @@ public class Rope : MonoBehaviour ); } - private void OnDrawGizmosSelected() - { - if (pCurr == null) return; - Gizmos.color = Color.yellow; - for (int i = 0; i < pCurr.Length; i++) - Gizmos.DrawSphere(pCurr[i], 0.01f); - } - private static Vector3 CatmullRom_XZ_LinearY(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { - // XZ 做 Catmull-Rom Vector3 cr = CatmullRom(p0, p1, p2, p3, t); - - // Y 不做样条,改成线性(不会过冲) cr.y = Mathf.Lerp(p1.y, p2.y, t); - return cr; } + + private void OnDrawGizmosSelected() + { + if (_pCurr == null) return; + Gizmos.color = Color.yellow; + for (int i = 0; i < _physicsNodes; i++) + Gizmos.DrawSphere(_pCurr[i], 0.01f); + } } \ No newline at end of file