Files
Fishing2/Assets/Scripts/Fishing/Rope/Rope.cs
2026-02-25 23:46:50 +08:00

579 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using NBF;
using UnityEngine;
[RequireComponent(typeof(LineRenderer))]
public class Rope : MonoBehaviour
{
[Header("Anchors")] [SerializeField] public Rigidbody startAnchor;
[SerializeField] public Rigidbody endAnchor;
/// <summary>鱼线宽度倍数</summary>
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;
[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;
[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("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 FRod _rod;
public void Init(FRod rod) => _rod = rod;
// Catmull t caches只缓存 idle/moving 两档,减少每帧重复乘法)
private struct TCaches
{
public float[] t;
public float[] t2;
public float[] t3;
}
private TCaches _tIdle;
private TCaches _tMoving;
private void Awake()
{
_lineRenderer = GetComponent<LineRenderer>();
_gravity = new Vector3(0f, -gravityStrength, 0f);
_startTr = startAnchor ? startAnchor.transform : null;
_endTr = endAnchor ? endAnchor.transform : null;
InitLengthSystem();
AllocateAndInitNodes();
// ✅ 渲染点一次性分配到最大: (maxNodes-1)*idle + 1
int maxSubdiv = Mathf.Max(1, renderSubdivisionsIdle);
_rCapacity = (maxPhysicsNodes - 1) * maxSubdiv + 1;
_rPoints = new Vector3[_rCapacity];
BuildTCaches(renderSubdivisionsIdle, ref _tIdle);
BuildTCaches(renderSubdivisionsMoving, ref _tMoving);
}
private void OnValidate()
{
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);
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);
}
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.FloorToInt(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;
private void FixedUpdate()
{
if (!startAnchor || !endAnchor) return;
// cache dt
_dt = Time.fixedDeltaTime;
if (_dt < 1e-6f) _dt = 1e-6f;
_dt2 = _dt * _dt;
// gravity
_gravity.y = -gravityStrength;
// drag cachesexp 比较贵,但这里每 FixedUpdate 一次OK
_kY = Mathf.Exp(-airDrag * _dt);
_kXZ = Mathf.Exp(-airDragXZ * _dt);
UpdateLengthSmooth();
UpdateNodesFromLength();
UpdateHeadRestLenFromCurrentLength();
Simulate_VerletFast();
// anchors
LockAnchorsHard();
// constraints
for (int it = 0; it < iterations; it++)
SolveDistanceConstraints_HeadOnly_Fast();
LockAnchorsHard();
if (constrainToGround)
ConstrainToGround();
LockAnchorsHard();
}
private void LateUpdate()
{
if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return;
int last = _physicsNodes - 1;
// 用缓存 transform避免多次属性链
Vector3 s = _startTr.position;
Vector3 e = _endTr.position;
_pCurr[0] = s;
_pPrev[0] = s;
_pCurr[last] = e;
_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
);
}
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);
}
// inherit displacement (Verlet)
Vector3 inheritDisp = Vector3.zero;
if (oldCount >= 2 && firstOld < maxPhysicsNodes)
inheritDisp = (_pCurr[firstOld] - _pPrev[firstOld]);
for (int k = 1; k <= addCount; k++)
{
Vector3 pos = s + dir * (physicsSegmentLen * k);
_pCurr[k] = pos;
_pPrev[k] = pos - 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);
}
/// <summary>
/// ✅ 更快的 Verlet去掉 /dt 和 *dt 抵消的无效计算
/// </summary>
private void Simulate_VerletFast()
{
// displacement = curr - prev
// next = curr + displacement*drag*dampen + gravity*dt^2
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;
}
/// <summary>
/// ✅ 约束:减少临时变量、用 sqrMagnitude + invDist
/// </summary>
private void SolveDistanceConstraints_HeadOnly_Fast()
{
int last = _physicsNodes - 1;
for (int i = 0; i < last; i++)
{
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; // = 1 - rest/dist
Vector3 corr = delta * (diff * stiffness);
// i==0 锚点固定last 锚点固定
if (i != 0) _pCurr[i] = a + corr * 0.5f;
if (i + 1 != last) _pCurr[i + 1] = b - corr * 0.5f;
}
}
private void ConstrainToGround()
{
if (groundMask == 0) return;
// RaycastHit 是 struct这里不会 GC
for (int i = 1; i < _physicsNodes - 1; i++)
{
Vector3 p = _pCurr[i];
Vector3 origin = p + Vector3.up * groundCastHeight;
if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask,
QueryTriggerInteraction.Ignore))
{
float minY = hit.point.y + groundRadius;
if (p.y < minY) p.y = minY;
}
_pCurr[i] = p;
}
}
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 用 maxNodes & idle 分配)
// 保险扩容一次
_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];
// inline CatmullRom少一次函数调用
Vector3 cr =
0.5f * (
(2f * p1) +
(-p0 + p2) * t +
(2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 +
(-p0 + 3f * p1 - 3f * p2 + p3) * t3
);
// Linear Y
cr.y = p1.y + (p2.y - p1.y) * t;
_rPoints[idx++] = cr;
}
}
_rPoints[idx++] = _pCurr[last];
_lineRenderer.positionCount = idx;
_lineRenderer.SetPositions(_rPoints);
}
/// <summary>
/// ✅ 用 sqrMagnitude 比较阈值,避免 sqrt
/// </summary>
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); // 因为我们用 disp = curr-prev单位是米/step所以阈值要乘 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);
}
}