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(); 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; } /// 设置目标总长度(米)。不做最小/最大长度限制(最小可行由锚点距离决定)。 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; } }