Files
Fishing2/Assets/Scripts/Fishing/Rope/Rope.cs

626 lines
20 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 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("Collision Filter")] [SerializeField, Tooltip("只对这些Layer进行物理检测Raycast/SphereCast等。不在这里的层完全不检测。")]
private LayerMask collisionMask = ~0;
[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;
[SerializeField] private bool constrainToWater = false;
[SerializeField] private float waterHeight = 0f;
[SerializeField, Min(0f)] private float waterRadius = 0.01f;
[Header("Render (High Resolution)")] [SerializeField, Min(1), Tooltip("每段物理线段插值加密的数量(越大越顺,越耗)")]
private int renderSubdivisions = 6;
[Header("Air / Wind (For Fishing Line Feel)")]
[SerializeField, Range(0f, 5f), Tooltip("空气线性阻力(越大越不飘,空中更自然)")]
private float airDrag = 0.9f;
[SerializeField, Range(0f, 2f), Tooltip("横向额外阻力(减少左右飘得太夸张)")]
private float airDragXZ = 0.6f;
[SerializeField, Tooltip("风方向(世界空间)")]
private Vector3 windDir = new Vector3(1f, 0f, 0f);
[SerializeField, Range(0f, 10f), Tooltip("基础风强度m/s 级别的感觉)")]
private float windStrength = 0.3f;
[SerializeField, Range(0f, 2f), Tooltip("阵风幅度0=无阵风)")]
private float windGust = 0.25f;
[SerializeField, Range(0.1f, 5f), Tooltip("阵风频率")]
private float windFreq = 1.2f;
[Header("Bending (Smooth Curve)")]
[SerializeField, Range(0f, 1f), Tooltip("抗折/弯曲刚度0=完全不抗折0.1~0.3 比较像鱼线)")]
private float bendStiffness = 0.18f;
[SerializeField, Tooltip("是否使用 Catmull-Rom 平滑(推荐开启)")]
private bool smooth = true;
[SerializeField, Min(0.0001f)] private float lineWidth = 0.002f;
private LineRenderer lr;
// physics
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;
// Only-head-change trick:
// Total rest length = headRestLen + (physicsNodes - 2) * physicsSegmentLen
private float headRestLen;
private void Awake()
{
lr = GetComponent<LineRenderer>();
gravity = new Vector3(0f, -gravityStrength, 0f);
InitLengthSystem();
AllocateAndInitNodes();
RebuildRenderBufferIfNeeded();
}
private FRod _rod;
public void Init(FRod rod)
{
_rod = rod;
}
private void OnValidate()
{
renderSubdivisions = Mathf.Max(renderSubdivisions, 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);
// 如果你希望只用一个mask控制避免 groundMask 忘了配
if (groundMask == ~0)
groundMask = collisionMask;
}
private void InitLengthSystem()
{
// 没有 min/max 长度限制:初始长度只做一个“非负”保障
float defaultLen = physicsSegmentLen * (Mathf.Max(minPhysicsNodes, 2) - 1);
currentLength = (initialLength > 0f) ? initialLength : defaultLen;
targetLength = currentLength;
}
private void AllocateAndInitNodes()
{
physicsNodes = Mathf.Clamp(ComputeDesiredNodes(currentLength), 2, maxPhysicsNodes);
pCurr = new Vector3[physicsNodes];
pPrev = new Vector3[physicsNodes];
// 初始从起点往下排
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)
{
// nodes = floor(length/segLen)+1
int desired = Mathf.FloorToInt(Mathf.Max(0f, lengthMeters) / physicsSegmentLen) + 1;
desired = Mathf.Clamp(desired, minPhysicsNodes, maxPhysicsNodes);
return desired;
}
/// <summary>设置目标总长度(米)。不做最小/最大长度限制(最小可行由锚点距离决定)。</summary>
public void SetTargetLength(float lengthMeters)
{
targetLength = Mathf.Max(0f, lengthMeters);
}
public float GetCurrentLength() => currentLength;
public float GetTargetLength() => targetLength;
public float GetAnchorDistance()
{
if (startAnchor != null && endAnchor != null)
{
return Vector3.Distance(startAnchor.position, endAnchor.position);
}
return 0;
}
private void FixedUpdate()
{
if (!startAnchor || !endAnchor)
return;
gravity.y = -gravityStrength;
UpdateLengthSmooth(); // 只保证 >= 锚点直线距离 + minSlack
UpdateNodesFromLength(); // 只从头部增/减节点
UpdateHeadRestLenFromCurrentLength(); // 第一段补余量 => 变化集中在头部
Simulate();
for (int it = 0; it < iterations; it++)
{
SolveDistanceConstraints_HeadOnly();
SolveBendConstraint();
LockAnchorsHard();
}
if (constrainToWater || constrainToGround)
ConstrainToGroundAndWater();
LockAnchorsHard();
}
private void LateUpdate()
{
if (!startAnchor || !endAnchor || pCurr == null || physicsNodes < 2) return;
int last = physicsNodes - 1;
Vector3 s = startAnchor.transform.position;
Vector3 e = endAnchor.transform.position;
pCurr[0] = s; pPrev[0] = s; // ✅ 关键:同步 pPrev
pCurr[last] = e; pPrev[last] = e; // ✅ 关键:同步 pPrev
DrawHighResLine();
}
private void UpdateLengthSmooth()
{
// float anchorDist = Vector3.Distance(startAnchor.position, endAnchor.position);
// float minFeasible = anchorDist + minSlack;
float minFeasible = 0.01f;
// ✅ 最小长度 = 起点终点直线距离(+slack),最大不限制
float desired = Mathf.Max(targetLength, minFeasible);
float prevLen = currentLength;
currentLength = Mathf.SmoothDamp(
currentLength,
desired,
ref lengthSmoothVel,
lengthSmoothTime,
Mathf.Infinity,
Time.fixedDeltaTime
);
float lenDelta = Mathf.Abs(currentLength - prevLen);
if (lenDelta > 1e-5f && lengthChangeVelocityKill > 0f && pPrev != null)
{
float kill = Mathf.Clamp01(lengthChangeVelocityKill);
for (int i = 1; i < physicsNodes - 1; i++)
pPrev[i] = Vector3.Lerp(pPrev[i], pCurr[i], kill);
}
}
private void UpdateNodesFromLength()
{
int desired = ComputeDesiredNodes(currentLength);
if (desired == physicsNodes) return;
while (physicsNodes < desired)
AddNodeAtStart();
while (physicsNodes > desired)
RemoveNodeAtStart();
RebuildRenderBufferIfNeeded();
}
private void AddNodeAtStart()
{
int newCount = Mathf.Min(physicsNodes + 1, maxPhysicsNodes);
if (newCount == physicsNodes) return;
Vector3[] newCurr = new Vector3[newCount];
Vector3[] newPrev = new Vector3[newCount];
newCurr[0] = pCurr[0];
newPrev[0] = pPrev[0];
for (int i = 2; i < newCount; i++)
{
newCurr[i] = pCurr[i - 1];
newPrev[i] = pPrev[i - 1];
}
Vector3 s = startAnchor.position;
Vector3 dir = Vector3.down;
if (physicsNodes >= 2)
{
Vector3 toOld1 = (pCurr[1] - s);
if (toOld1.sqrMagnitude > 1e-6f) dir = toOld1.normalized;
}
Vector3 pos = s + dir * physicsSegmentLen;
newCurr[1] = pos;
newPrev[1] = pos;
pCurr = newCurr;
pPrev = newPrev;
physicsNodes = newCount;
LockAnchorsHard();
}
private void RemoveNodeAtStart()
{
int newCount = Mathf.Max(physicsNodes - 1, 2);
if (newCount == physicsNodes) return;
Vector3[] newCurr = new Vector3[newCount];
Vector3[] newPrev = new Vector3[newCount];
newCurr[0] = pCurr[0];
newPrev[0] = pPrev[0];
for (int i = 1; i < newCount - 1; i++)
{
newCurr[i] = pCurr[i + 1];
newPrev[i] = pPrev[i + 1];
}
newCurr[newCount - 1] = pCurr[physicsNodes - 1];
newPrev[newCount - 1] = pPrev[physicsNodes - 1];
pCurr = newCurr;
pPrev = newPrev;
physicsNodes = newCount;
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);
// 风方向归一化避免填了0向量导致NaN
Vector3 wDir = windDir;
if (wDir.sqrMagnitude < 1e-6f) wDir = Vector3.right;
wDir.Normalize();
for (int i = 0; i < physicsNodes; i++)
{
// Verlet 速度(由当前位置和上一帧位置推出来)
Vector3 vel = (pCurr[i] - pPrev[i]) * invDt;
// 先做“惯性推进”
Vector3 next = pCurr[i] + (pCurr[i] - pPrev[i]) * velocityDampen;
// 加速度 = 重力 + 空气阻力 + 风(相对速度)
Vector3 acc = gravity;
// --- 空气阻力(与速度成正比)---
// drag = -vel * airDrag并且横向更强一点
Vector3 drag = -vel * airDrag;
drag.x *= (1f + airDragXZ);
drag.z *= (1f + airDragXZ);
acc += drag;
// --- 风(让线在空中不那么“只会垂直掉”)---
if (i != 0 && i != physicsNodes - 1 && windStrength > 0f)
{
float t = Time.time;
float gust = 1f + Mathf.Sin(t * windFreq + i * 0.35f) * windGust;
// windVel风希望空气把线速度拉向这个“风速”
Vector3 windVel = wDir * (windStrength * gust);
// 相对风:让加速度朝 (windVel - vel) 方向
// 系数越大,越“被风带着走”
acc += (windVel - vel) * 0.5f;
}
// Verlet位置 += acc * dt^2
pPrev[i] = pCurr[i];
pCurr[i] = next + acc * (dt * dt);
}
// 物理步末尾硬锁端点
LockAnchorsHard();
}
// private void Simulate()
// {
// float dt = Time.fixedDeltaTime;
//
// for (int i = 0; i < physicsNodes; i++)
// {
// Vector3 v = (pCurr[i] - pPrev[i]) * velocityDampen;
// pPrev[i] = pCurr[i];
//
// pCurr[i] += v;
// pCurr[i] += gravity * dt;
// }
//
// LockAnchorsHard();
// }
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 SolveBendConstraint()
{
if (bendStiffness <= 0f) return;
if (physicsNodes < 3) return;
// bendStiffness 在迭代里用太大很容易爆,先做一个安全钳制
float kBase = Mathf.Clamp01(bendStiffness);
for (int i = 1; i < physicsNodes - 1; i++)
{
// 端点不要动(你本来就没动,这里保持)
if (i == 0 || i == physicsNodes - 1) continue;
Vector3 mid = (pCurr[i - 1] + pCurr[i + 1]) * 0.5f;
float k = kBase;
if (i <= 2) k *= 1.25f; // 靠近竿尖稍微更“直”一点
Vector3 old = pCurr[i];
Vector3 newPos = Vector3.Lerp(old, mid, k);
Vector3 delta = newPos - old;
// ✅ 关键:同样把 pPrev 挪过去,避免“凭空制造速度”
pCurr[i] = newPos;
pPrev[i] += delta;
}
}
private void ConstrainToGroundAndWater()
{
int groundLayerMask = collisionMask & groundMask; // ✅ 统一过滤:只检测指定层
for (int i = 1; i < physicsNodes - 1; i++)
{
Vector3 p = pCurr[i];
if (constrainToWater)
{
float minY = waterHeight + waterRadius;
if (p.y < minY) p.y = minY;
}
if (constrainToGround && groundLayerMask != 0)
{
Vector3 origin = p + Vector3.up * groundCastHeight;
if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundLayerMask,
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;
RebuildRenderBufferIfNeeded();
lr.startWidth = lineWidth;
lr.endWidth = lineWidth;
if (!smooth)
{
lr.positionCount = physicsNodes;
lr.SetPositions(pCurr);
return;
}
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 < renderSubdivisions; s++)
{
float t = s / (float)renderSubdivisions;
Vector3 pt = CatmullRom_XZ_LinearY(p0, p1, p2, p3, t);
// 如果水面约束开启:渲染点也夹一下,避免视觉上又穿回去
if (constrainToWater)
{
float minY = waterHeight + waterRadius;
if (pt.y < minY) pt.y = minY;
}
rPoints[idx++] = pt;
}
}
rPoints[idx++] = pCurr[physicsNodes - 1];
lr.positionCount = idx;
lr.SetPositions(rPoints);
}
private void RebuildRenderBufferIfNeeded()
{
int targetCount = (physicsNodes - 1) * renderSubdivisions + 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 void OnDrawGizmosSelected()
{
if (pCurr == null) return;
Gizmos.color = Color.yellow;
for (int i = 0; i < pCurr.Length; i++)
Gizmos.DrawSphere(pCurr[i], 0.01f);
}
private static Vector3 CatmullRom_XZ_LinearY(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
// XZ 做 Catmull-Rom
Vector3 cr = CatmullRom(p0, p1, p2, p3, t);
// Y 不做样条,改成线性(不会过冲)
cr.y = Mathf.Lerp(p1.y, p2.y, t);
return cr;
}
}