Files
Fishing2/Assets/Scripts/Fishing/New/View/FishingLine/Renderer/FishingNodeRope.cs
2026-04-14 18:07:22 +08:00

403 lines
13 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 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;
}
}
}
}
}