using System; using UnityEngine; namespace NBF { /// /// 单段鱼线节点模拟(startAnchor -> endAnchor) /// [RequireComponent(typeof(LineRenderer))] public class FishingNodeRope : MonoBehaviour { [Header("Anchors")] public Rigidbody startAnchor; public Rigidbody endAnchor; [Header("Length")] [SerializeField, Min(0.001f)] private float segmentLength = 0.1f; [SerializeField, Min(0.001f)] private float minLength = 0.02f; [SerializeField] private bool initializeFromAnchorDistance = true; [Header("Simulation")] [SerializeField, Range(1, 60)] private int solverIterations = 14; [SerializeField, Range(0f, 1f)] private float stiffness = 0.85f; [SerializeField, Min(0f)] private float gravityScale = 1f; [SerializeField, Range(0f, 1f)] private float velocityDamping = 0.98f; [Header("Ground Check")] [SerializeField] private bool constrainToGround = true; [SerializeField, Range(1, 16), Tooltip("每 N 个模拟点做一次地面检测")] private int groundSampleStep = 3; [SerializeField] private LayerMask groundMask = ~0; [SerializeField, Min(0f)] private float groundCastHeight = 0.5f; [SerializeField, Min(0.01f)] private float groundCastDistance = 2f; [SerializeField, Min(0f)] private float groundOffset = 0.002f; [Header("Bend Balance")] [SerializeField, Range(0, 8)] private int bendIterations = 2; [SerializeField, Range(0f, 1f)] private float bendBalance = 0.35f; [Header("Render")] [SerializeField, Min(0.0001f)] private float lineWidth = 0.001f; private LineRenderer _lineRenderer; private Vector3[] _pCurr; private Vector3[] _pPrev; private float[] _segmentRestLengths; private int _nodeCount; private bool _initialized; public float Length { get; private set; } private void Awake() { _lineRenderer = GetComponent(); _lineRenderer.startWidth = lineWidth; _lineRenderer.endWidth = lineWidth; _lineRenderer.positionCount = 0; } private void OnValidate() { segmentLength = Mathf.Max(0.001f, segmentLength); minLength = Mathf.Max(0.001f, minLength); solverIterations = Mathf.Clamp(solverIterations, 1, 60); stiffness = Mathf.Clamp01(stiffness); velocityDamping = Mathf.Clamp01(velocityDamping); gravityScale = Mathf.Max(0f, gravityScale); groundSampleStep = Mathf.Max(1, groundSampleStep); groundCastHeight = Mathf.Max(0f, groundCastHeight); groundCastDistance = Mathf.Max(0.01f, groundCastDistance); groundOffset = Mathf.Max(0f, groundOffset); bendIterations = Mathf.Clamp(bendIterations, 0, 8); bendBalance = Mathf.Clamp01(bendBalance); lineWidth = Mathf.Max(0.0001f, lineWidth); if (_lineRenderer != null) { _lineRenderer.startWidth = lineWidth; _lineRenderer.endWidth = lineWidth; } } private void Start() { EnsureInitialized(); } private void FixedUpdate() { if (!EnsureInitialized() || _nodeCount < 2) return; SimulateVerlet(); for (int it = 0; it < solverIterations; it++) { LockAnchorsHard(Time.fixedDeltaTime); SolveDistanceConstraints(stiffness); SolveBendBalance(); } // 最后再硬约束一遍,保证每段长度更贴近 rest length。 LockAnchorsHard(Time.fixedDeltaTime); SolveDistanceConstraints(1f); if (constrainToGround) { ConstrainToGround(); LockAnchorsHard(Time.fixedDeltaTime); SolveDistanceConstraints(1f); } LockAnchorsHard(Time.fixedDeltaTime); } private void LateUpdate() { if (_pCurr == null || _nodeCount < 2 || _lineRenderer == null) return; _lineRenderer.startWidth = lineWidth; _lineRenderer.endWidth = lineWidth; _lineRenderer.positionCount = _nodeCount; _lineRenderer.SetPositions(_pCurr); } public void SetLength(float length) { float target = Mathf.Max(length, minLength); bool firstInit = !_initialized; Length = target; RebuildFromLength(target, keepShapeFromStart: !firstInit); _initialized = true; } public void AddLength(float delta) { if (delta <= 0f) return; SetLength(Length + delta); } public void ReduceLength(float delta) { if (delta <= 0f) return; SetLength(Length - delta); } private bool EnsureInitialized() { if (_initialized) return true; if (!startAnchor || !endAnchor) return false; float initialLength = initializeFromAnchorDistance ? Vector3.Distance(GetStartPos(), GetEndPos()) : Mathf.Max(segmentLength, minLength); SetLength(initialLength); return true; } private Vector3 GetStartPos() => startAnchor ? startAnchor.position : transform.position; private Vector3 GetEndPos() => endAnchor ? endAnchor.position : transform.position; private void RebuildFromLength(float totalLength, bool keepShapeFromStart) { float clampedLength = Mathf.Max(totalLength, minLength); Length = clampedLength; int fullSeg = Mathf.FloorToInt(clampedLength / segmentLength); float rem = clampedLength - fullSeg * segmentLength; bool hasRem = rem > 1e-4f; int segmentCount = Mathf.Max(1, fullSeg + (hasRem ? 1 : 0)); int desiredNodes = segmentCount + 1; float[] newRest = new float[segmentCount]; for (int i = 0; i < segmentCount; i++) newRest[i] = segmentLength; if (hasRem) newRest[segmentCount - 1] = rem; if (_pCurr == null || _pPrev == null || _nodeCount < 2 || !keepShapeFromStart) { BuildLinearNodes(desiredNodes, newRest); return; } int oldNodes = _nodeCount; int oldSegments = oldNodes - 1; int add = Mathf.Max(0, segmentCount - oldSegments); int remove = Mathf.Max(0, oldSegments - segmentCount); Vector3[] newCurr = new Vector3[desiredNodes]; Vector3[] newPrev = new Vector3[desiredNodes]; if (add > 0) { Array.Copy(_pCurr, 1, newCurr, 1 + add, oldNodes - 1); Array.Copy(_pPrev, 1, newPrev, 1 + add, oldNodes - 1); Vector3 s = GetStartPos(); int firstOldIdx = 1 + add; Vector3 dir = GetInitialFillDir(s, firstOldIdx < newCurr.Length ? newCurr[firstOldIdx] : GetEndPos()); Vector3 inheritDisp = Vector3.zero; if (firstOldIdx < newCurr.Length) inheritDisp = newCurr[firstOldIdx] - newPrev[firstOldIdx]; for (int i = 1; i <= add; i++) { Vector3 pos = s + dir * (segmentLength * i); newCurr[i] = pos; newPrev[i] = pos - inheritDisp; } } else if (remove > 0) { int srcStart = 1 + remove; int copyCount = desiredNodes - 1; Array.Copy(_pCurr, srcStart, newCurr, 1, copyCount); Array.Copy(_pPrev, srcStart, newPrev, 1, copyCount); } else { Array.Copy(_pCurr, newCurr, desiredNodes); Array.Copy(_pPrev, newPrev, desiredNodes); } _pCurr = newCurr; _pPrev = newPrev; _segmentRestLengths = newRest; _nodeCount = desiredNodes; LockAnchorsHard(Time.fixedDeltaTime > 0f ? Time.fixedDeltaTime : 0.02f); } private void BuildLinearNodes(int desiredNodes, float[] restLengths) { _nodeCount = Mathf.Max(2, desiredNodes); _pCurr = new Vector3[_nodeCount]; _pPrev = new Vector3[_nodeCount]; _segmentRestLengths = restLengths; Vector3 s = GetStartPos(); Vector3 e = GetEndPos(); Vector3 dir = GetInitialFillDir(s, e); _pCurr[0] = s; _pPrev[0] = s; float traveled = 0f; for (int i = 1; i < _nodeCount - 1; i++) { traveled += _segmentRestLengths[i - 1]; Vector3 pos = s + dir * traveled; _pCurr[i] = pos; _pPrev[i] = pos; } _pCurr[_nodeCount - 1] = e; _pPrev[_nodeCount - 1] = e; } private Vector3 GetInitialFillDir(Vector3 start, Vector3 toward) { Vector3 d = toward - start; if (d.sqrMagnitude < 1e-8f) return Vector3.down; return d.normalized; } private void SimulateVerlet() { float dt = Mathf.Max(Time.fixedDeltaTime, 1e-6f); float dt2 = dt * dt; Vector3 gravity = Physics.gravity * gravityScale; int last = _nodeCount - 1; for (int i = 1; i < last; i++) { Vector3 disp = (_pCurr[i] - _pPrev[i]) * velocityDamping; Vector3 next = _pCurr[i] + disp + gravity * dt2; _pPrev[i] = _pCurr[i]; _pCurr[i] = next; } } private void LockAnchorsHard(float dt) { if (_nodeCount < 2) return; Vector3 s = GetStartPos(); Vector3 e = GetEndPos(); _pCurr[0] = s; _pPrev[0] = startAnchor ? s - startAnchor.linearVelocity * dt : s; int last = _nodeCount - 1; _pCurr[last] = e; _pPrev[last] = endAnchor ? e - endAnchor.linearVelocity * dt : e; } private void SolveDistanceConstraints(float solveStiffness) { int last = _nodeCount - 1; if (last <= 0) return; float k = Mathf.Clamp01(solveStiffness); SolveDistanceSweep(0, last, 1, last, k); SolveDistanceSweep(last - 1, -1, -1, last, k); } private void SolveDistanceSweep(int start, int endExclusive, int step, int last, float k) { for (int i = start; i != endExclusive; i += step) { float rest = _segmentRestLengths[i]; 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 * k); 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; } else if (!aLocked) { _pCurr[i] = a + corr; } } } private void SolveBendBalance() { if (bendIterations <= 0 || bendBalance <= 0f || _nodeCount < 3) return; int last = _nodeCount - 1; for (int it = 0; it < bendIterations; it++) { for (int i = 1; i < last; i++) { Vector3 target = (_pCurr[i - 1] + _pCurr[i + 1]) * 0.5f; _pCurr[i] = Vector3.Lerp(_pCurr[i], target, bendBalance); } } } private void ConstrainToGround() { if (groundMask == 0 || _nodeCount < 3) return; int last = _nodeCount - 1; int step = Mathf.Max(1, groundSampleStep); for (int i = 1; i < last; i += step) { Vector3 p = _pCurr[i]; Vector3 origin = p + Vector3.up * groundCastHeight; if (!Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask, QueryTriggerInteraction.Ignore)) { continue; } float minY = hit.point.y + groundOffset; if (p.y < minY) { p.y = minY; _pCurr[i] = p; Vector3 prev = _pPrev[i]; if (prev.y < minY) prev.y = minY; _pPrev[i] = prev; } } } } }