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;
}
}
}
}
}