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

543 lines
17 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;
[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.002f;
[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 (注意:数组固定为 maxPhysicsNodesphysicsNodes 表示有效长度)
private int _physicsNodes;
private Vector3[] _pCurr;
private Vector3[] _pPrev;
// render
private Vector3[] _rPoints;
private int _rCountCached = -1;
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;
private FRod _rod;
public void Init(FRod rod) => _rod = rod;
private void Awake()
{
_lineRenderer = GetComponent<LineRenderer>();
_gravity = new Vector3(0f, -gravityStrength, 0f);
InitLengthSystem();
AllocateAndInitNodes();
RebuildRenderBufferIfNeeded(renderSubdivisionsIdle);
}
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);
// ✅ 永久分配到最大,运行时不再 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++)
{
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;
// 边界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;
_gravity.y = -gravityStrength;
UpdateLengthSmooth();
UpdateNodesFromLength(); // ✅ 一次性增减无GC
UpdateHeadRestLenFromCurrentLength();
// simulate
Simulate();
// 确保端点正确后再迭代
LockAnchorsHard();
// constraints
for (int it = 0; it < iterations; it++)
SolveDistanceConstraints_HeadOnly();
// 迭代后锁一次足够
LockAnchorsHard();
if (constrainToGround)
ConstrainToGround();
// 约束后再锁一次(保险)
LockAnchorsHard();
}
private void LateUpdate()
{
if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return;
// 端点“跟手”:渲染前强行同步 transform消除慢一拍
int last = _physicsNodes - 1;
Vector3 s = startAnchor.transform.position;
Vector3 e = endAnchor.transform.position;
_pCurr[0] = s;
_pPrev[0] = s;
_pCurr[last] = e;
_pPrev[last] = e;
DrawHighResLine();
}
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;
// 渲染缓存按最大 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[firstOld] - s);
if (toOld1.sqrMagnitude > 1e-6f) dir = toOld1.normalized;
}
// 继承速度
float dt = Time.fixedDeltaTime;
Vector3 inheritVel = Vector3.zero;
if (oldCount >= 2 && firstOld < maxPhysicsNodes)
{
inheritVel = (_pCurr[firstOld] - _pPrev[firstOld]) / Mathf.Max(dt, 1e-6f);
}
for (int k = 1; k <= addCount; k++)
{
Vector3 pos = s + dir * (physicsSegmentLen * k);
_pCurr[k] = pos;
_pPrev[k] = pos - inheritVel * dt;
}
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;
// 把 [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);
float baseLen = fixedSegCount * physicsSegmentLen;
_headRestLen = _currentLength - baseLen;
_headRestLen = Mathf.Clamp(_headRestLen, headMinLen, physicsSegmentLen * 1.5f);
}
private void Simulate()
{
float dt = Time.fixedDeltaTime;
float invDt = 1f / Mathf.Max(dt, 1e-6f);
// 指数衰减:更稳定好调
float kY = Mathf.Exp(-airDrag * dt);
float kXZ = Mathf.Exp(-airDragXZ * dt);
for (int i = 1; i < _physicsNodes - 1; i++)
{
Vector3 vel = (_pCurr[i] - _pPrev[i]) * invDt;
vel.x *= kXZ;
vel.z *= kXZ;
vel.y *= kY;
vel *= velocityDampen;
Vector3 next = _pCurr[i] + vel * dt;
_pPrev[i] = _pCurr[i];
_pCurr[i] = next + _gravity * (dt * dt);
}
}
private void LockAnchorsHard()
{
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;
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++)
{
float rest = (i == 0) ? _headRestLen : physicsSegmentLen;
Vector3 a = _pCurr[i];
Vector3 b = _pCurr[i + 1];
Vector3 delta = b - a;
float dist = delta.magnitude;
if (dist < 1e-6f) continue;
float diff = (dist - rest) / dist;
Vector3 corr = delta * diff * stiffness;
if (i != 0)
_pCurr[i] += corr * 0.5f;
if (i + 1 != _physicsNodes - 1)
_pCurr[i + 1] -= corr * 0.5f;
}
}
private void ConstrainToGround()
{
for (int i = 1; i < _physicsNodes - 1; i++)
{
Vector3 p = _pCurr[i];
if (constrainToGround && groundMask != 0)
{
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()
{
if (_pCurr == null || _physicsNodes < 2) return;
_lineRenderer.startWidth = lineWidth;
_lineRenderer.endWidth = lineWidth;
if (!smooth)
{
_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++)
{
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 < subdiv; s++)
{
float t = s / (float)subdiv;
Vector3 pt = CatmullRom_XZ_LinearY(p0, p1, p2, p3, t);
_rPoints[idx++] = pt;
}
}
_rPoints[idx++] = _pCurr[_physicsNodes - 1];
_lineRenderer.positionCount = idx;
_lineRenderer.SetPositions(_rPoints);
}
private int PickRenderSubdivisions()
{
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;
}
}
private static Vector3 CatmullRom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5f * (
(2f * p1) +
(-p0 + p2) * t +
(2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 +
(-p0 + 3f * p1 - 3f * p2 + p3) * t3
);
}
private static Vector3 CatmullRom_XZ_LinearY(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
Vector3 cr = CatmullRom(p0, p1, p2, p3, t);
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);
}
}