403 lines
13 KiB
C#
403 lines
13 KiB
C#
using System;
|
||
using UnityEngine;
|
||
|
||
namespace NBF
|
||
{
|
||
/// <summary>
|
||
/// 单段鱼线节点模拟(startAnchor -> endAnchor)
|
||
/// </summary>
|
||
[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>();
|
||
_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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|