修改鱼线逻辑

This commit is contained in:
2026-02-25 23:27:55 +08:00
parent b41ee35cd4
commit ceaef328e3
3 changed files with 277 additions and 362 deletions

View File

@@ -41538,8 +41538,8 @@ MonoBehaviour:
Dependencies: [] Dependencies: []
Tags: Tags:
- Name: main/plyaer.bundle - Name: main/plyaer.bundle
Hash: 3f8f7efec7b92de0aa2fee96a290c14b Hash: e4ec05a4b781eddaae2f07fc44c008f4
Size: 378598645 Size: 378599896
Assets: Assets:
- Path: Assets/ResRaw/Prefabs/Line/fishing line float set.prefab - Path: Assets/ResRaw/Prefabs/Line/fishing line float set.prefab
Address: Plyaer/fishing line float set Address: Plyaer/fishing line float set

View File

@@ -50,31 +50,30 @@ MonoBehaviour:
physicsSegmentLen: 0.1 physicsSegmentLen: 0.1
minPhysicsNodes: 2 minPhysicsNodes: 2
maxPhysicsNodes: 200 maxPhysicsNodes: 200
gravityStrength: 2 gravityStrength: 6
velocityDampen: 0.95 velocityDampen: 0.95
stiffness: 0.8 stiffness: 0.8
iterations: 20 iterations: 20
initialLength: 0 initialLength: 0
lengthSmoothTime: 0.15 lengthSmoothTime: 0.15
lengthChangeVelocityKill: 0.6 lengthChangeVelocityKill: 0.4
minSlack: 0.002 minSlack: 0.002
headMinLen: 0.01 headMinLen: 0.01
collisionMask: nodeHysteresis: 0.05
serializedVersion: 2
m_Bits: 24
constrainToGround: 1 constrainToGround: 1
groundMask: groundMask:
serializedVersion: 2 serializedVersion: 2
m_Bits: 4513599 m_Bits: 24
groundRadius: 0.01 groundRadius: 0.01
groundCastHeight: 1 groundCastHeight: 1
groundCastDistance: 2.5 groundCastDistance: 2.5
constrainToWater: 0 renderSubdivisionsIdle: 6
waterHeight: 0 renderSubdivisionsMoving: 2
waterRadius: 0.01 movingSpeedThreshold: 2
renderSubdivisions: 6
smooth: 1 smooth: 1
lineWidth: 0.005 lineWidth: 0.001
airDrag: 0.2
airDragXZ: 0.6
--- !u!120 &991521994724602848 --- !u!120 &991521994724602848
LineRenderer: LineRenderer:
serializedVersion: 2 serializedVersion: 2
@@ -663,10 +662,10 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
startAnchor: {fileID: 54298866000586118} startAnchor: {fileID: 54298866000586118}
endAnchor: {fileID: 54679398375713381} endAnchor: {fileID: 54679398375713381}
physicsSegmentLen: 0.1 physicsSegmentLen: 0.2
minPhysicsNodes: 2 minPhysicsNodes: 2
maxPhysicsNodes: 120 maxPhysicsNodes: 120
gravityStrength: 2 gravityStrength: 6
velocityDampen: 0.95 velocityDampen: 0.95
stiffness: 0.8 stiffness: 0.8
iterations: 20 iterations: 20
@@ -675,22 +674,21 @@ MonoBehaviour:
lengthChangeVelocityKill: 0.6 lengthChangeVelocityKill: 0.6
minSlack: 0.002 minSlack: 0.002
headMinLen: 0.01 headMinLen: 0.01
collisionMask: nodeHysteresis: 0.05
serializedVersion: 2
m_Bits: 0
constrainToGround: 1 constrainToGround: 1
groundMask: groundMask:
serializedVersion: 2 serializedVersion: 2
m_Bits: 4513599 m_Bits: 8
groundRadius: 0.01 groundRadius: 0.01
groundCastHeight: 1 groundCastHeight: 1
groundCastDistance: 2.5 groundCastDistance: 2.5
constrainToWater: 0 renderSubdivisionsIdle: 6
waterHeight: 0 renderSubdivisionsMoving: 2
waterRadius: 0.01 movingSpeedThreshold: 2
renderSubdivisions: 6
smooth: 1 smooth: 1
lineWidth: 0.005 lineWidth: 0.001
airDrag: 0.9
airDragXZ: 0.6
--- !u!120 &484878994603287356 --- !u!120 &484878994603287356
LineRenderer: LineRenderer:
serializedVersion: 2 serializedVersion: 2

View File

@@ -1,4 +1,5 @@
using NBF; using System;
using NBF;
using UnityEngine; using UnityEngine;
[RequireComponent(typeof(LineRenderer))] [RequireComponent(typeof(LineRenderer))]
@@ -12,16 +13,16 @@ public class Rope : MonoBehaviour
[SerializeField, Range(2, 200)] private int minPhysicsNodes = 12; [SerializeField, Range(2, 200)] private int minPhysicsNodes = 12;
[SerializeField, Range(2, 400)] [Tooltip("物理节点上限(仅用于性能保护;与“最大长度不限制”不是一回事)")] [SerializeField, Range(2, 400), Tooltip("物理节点上限(仅用于性能保护;与“最大长度不限制”不是一回事)")]
private int maxPhysicsNodes = 120; private int maxPhysicsNodes = 120;
[SerializeField] private float gravityStrength = 2.0f; [SerializeField] private float gravityStrength = 2.0f;
[SerializeField, Range(0f, 1f)] private float velocityDampen = 0.95f; [SerializeField, Range(0f, 1f)] private float velocityDampen = 0.95f;
[SerializeField, Range(0.0f, 1.0f)] [Tooltip("约束修正强度越大越硬。0.6~0.9 常用")] [SerializeField, Range(0.0f, 1.0f), Tooltip("约束修正强度越大越硬。0.6~0.9 常用")]
private float stiffness = 0.8f; private float stiffness = 0.8f;
[SerializeField, Range(1, 80)] [Tooltip("迭代次数。鱼线 10~30 通常够用")] [SerializeField, Range(1, 80), Tooltip("迭代次数。鱼线 10~30 通常够用")]
private int iterations = 20; private int iterations = 20;
[Header("Length Control (No Min/Max Clamp)")] [Header("Length Control (No Min/Max Clamp)")]
@@ -32,7 +33,7 @@ public class Rope : MonoBehaviour
[Tooltip("长度变化平滑时间(越小越跟手,越大越稳)")] [SerializeField, Min(0.0001f)] [Tooltip("长度变化平滑时间(越小越跟手,越大越稳)")] [SerializeField, Min(0.0001f)]
private float lengthSmoothTime = 0.15f; private float lengthSmoothTime = 0.15f;
[Tooltip("当长度在变化时额外把速度压掉一些防抖。0=不额外处理1=变化时几乎清速度")] [SerializeField, Range(0f, 1f)] [Tooltip("当长度在变化时额外把速度压掉一些防抖。0=不额外处理1=变化时几乎清速度(建议只在收线生效)")] [SerializeField, Range(0f, 1f)]
private float lengthChangeVelocityKill = 0.6f; private float lengthChangeVelocityKill = 0.6f;
[Tooltip("允许的最小松弛余量(避免目标长度刚好等于锚点距离时抖动)")] [SerializeField, Min(0f)] [Tooltip("允许的最小松弛余量(避免目标长度刚好等于锚点距离时抖动)")] [SerializeField, Min(0f)]
@@ -41,9 +42,8 @@ public class Rope : MonoBehaviour
[Header("Head Segment Clamp")] [Tooltip("第一段(起点->第1节点允许的最小长度避免收线时第一段被压到0导致数值炸")] [SerializeField, Min(0.0001f)] [Header("Head Segment Clamp")] [Tooltip("第一段(起点->第1节点允许的最小长度避免收线时第一段被压到0导致数值炸")] [SerializeField, Min(0.0001f)]
private float headMinLen = 0.01f; private float headMinLen = 0.01f;
[Header("Node Count Stability")] [SerializeField, Tooltip("节点数切换迟滞(米)。避免长度在临界点抖动导致节点数来回跳 -> 卡顿")]
[Header("Collision Filter")] [SerializeField, Tooltip("只对这些Layer进行物理检测Raycast/SphereCast等。不在这里的层完全不检测。")] private float nodeHysteresis = 0.05f;
private LayerMask collisionMask = ~0;
[Header("Simple Ground/Water Constraint (Cheap)")] [SerializeField] [Header("Simple Ground/Water Constraint (Cheap)")] [SerializeField]
private bool constrainToGround = true; private bool constrainToGround = true;
@@ -53,87 +53,67 @@ public class Rope : MonoBehaviour
[SerializeField, Min(0f)] private float groundCastHeight = 1.0f; [SerializeField, Min(0f)] private float groundCastHeight = 1.0f;
[SerializeField, Min(0.01f)] private float groundCastDistance = 2.5f; [SerializeField, Min(0.01f)] private float groundCastDistance = 2.5f;
[SerializeField] private bool constrainToWater = false; [Header("Render (High Resolution)")] [SerializeField, Min(1), Tooltip("静止时每段物理线段插值加密数量(越大越顺,越耗)")]
[SerializeField] private float waterHeight = 0f; private int renderSubdivisionsIdle = 6;
[SerializeField, Min(0f)] private float waterRadius = 0.01f;
[Header("Render (High Resolution)")] [SerializeField, Min(1), Tooltip("每段物理线段插值加密数量(越大越顺,越耗)")] [SerializeField, Min(1), Tooltip("甩动时每段物理线段插值加密数量(动态降LOD以防卡顿")]
private int renderSubdivisions = 6; private int renderSubdivisionsMoving = 2;
[SerializeField, Min(0f), Tooltip("平均速度超过该阈值认为在甩动(用于动态降 subdiv")]
[Header("Air / Wind (For Fishing Line Feel)")] private float movingSpeedThreshold = 2.0f;
[SerializeField, Range(0f, 5f), Tooltip("空气线性阻力(越大越不飘,空中更自然)")]
private float airDrag = 0.9f;
[SerializeField, Range(0f, 2f), Tooltip("横向额外阻力(减少左右飘得太夸张")] [SerializeField, Tooltip("是否使用 Catmull-Rom 平滑(开启更顺,但更耗")]
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; private bool smooth = true;
[SerializeField, Min(0.0001f)] private float lineWidth = 0.002f; [SerializeField, Min(0.0001f)] private float lineWidth = 0.002f;
private LineRenderer lr; [Header("Air Drag (Stable)")] [SerializeField, Range(0f, 5f), Tooltip("空气阻力Y向指数衰减越大越不飘")]
private float airDrag = 0.9f;
// physics [SerializeField, Range(0f, 2f), Tooltip("横向额外阻力XZ指数衰减越大越不左右飘")]
private int physicsNodes; private float airDragXZ = 0.6f;
private Vector3[] pCurr;
private Vector3[] pPrev; private LineRenderer _lineRenderer;
// physics (注意:数组固定为 maxPhysicsNodesphysicsNodes 表示有效长度)
private int _physicsNodes;
private Vector3[] _pCurr;
private Vector3[] _pPrev;
// render // render
private Vector3[] rPoints; private Vector3[] _rPoints;
private int rCountCached = -1; private int _rCountCached = -1;
private Vector3 gravity; private Vector3 _gravity;
// length control runtime // length control runtime
private float targetLength; private float _targetLength;
private float currentLength; private float _currentLength;
private float lengthSmoothVel; private float _lengthSmoothVel;
// Only-head-change trick: // rest length head
// Total rest length = headRestLen + (physicsNodes - 2) * physicsSegmentLen private float _headRestLen;
private float headRestLen;
// node stability
private int _lastDesiredNodes = 0;
private FRod _rod;
public void Init(FRod rod) => _rod = rod;
private void Awake() private void Awake()
{ {
lr = GetComponent<LineRenderer>(); _lineRenderer = GetComponent<LineRenderer>();
gravity = new Vector3(0f, -gravityStrength, 0f); _gravity = new Vector3(0f, -gravityStrength, 0f);
InitLengthSystem(); InitLengthSystem();
AllocateAndInitNodes(); AllocateAndInitNodes();
RebuildRenderBufferIfNeeded(); RebuildRenderBufferIfNeeded(renderSubdivisionsIdle);
}
private FRod _rod;
public void Init(FRod rod)
{
_rod = rod;
} }
private void OnValidate() private void OnValidate()
{ {
renderSubdivisions = Mathf.Max(renderSubdivisions, 1); renderSubdivisionsIdle = Mathf.Max(renderSubdivisionsIdle, 1);
renderSubdivisionsMoving = Mathf.Max(renderSubdivisionsMoving, 1);
iterations = Mathf.Clamp(iterations, 1, 80); iterations = Mathf.Clamp(iterations, 1, 80);
groundCastDistance = Mathf.Max(groundCastDistance, 0.01f); groundCastDistance = Mathf.Max(groundCastDistance, 0.01f);
groundCastHeight = Mathf.Max(groundCastHeight, 0f); groundCastHeight = Mathf.Max(groundCastHeight, 0f);
@@ -146,35 +126,33 @@ public class Rope : MonoBehaviour
maxPhysicsNodes = Mathf.Max(maxPhysicsNodes, minPhysicsNodes); maxPhysicsNodes = Mathf.Max(maxPhysicsNodes, minPhysicsNodes);
headMinLen = Mathf.Max(headMinLen, 0.0001f); headMinLen = Mathf.Max(headMinLen, 0.0001f);
nodeHysteresis = Mathf.Max(0f, nodeHysteresis);
// 如果你希望只用一个mask控制避免 groundMask 忘了配
if (groundMask == ~0)
groundMask = collisionMask;
} }
private void InitLengthSystem() private void InitLengthSystem()
{ {
// 没有 min/max 长度限制:初始长度只做一个“非负”保障
float defaultLen = physicsSegmentLen * (Mathf.Max(minPhysicsNodes, 2) - 1); float defaultLen = physicsSegmentLen * (Mathf.Max(minPhysicsNodes, 2) - 1);
currentLength = (initialLength > 0f) ? initialLength : defaultLen; _currentLength = (initialLength > 0f) ? initialLength : defaultLen;
targetLength = currentLength; _targetLength = _currentLength;
} }
private void AllocateAndInitNodes() private void AllocateAndInitNodes()
{ {
physicsNodes = Mathf.Clamp(ComputeDesiredNodes(currentLength), 2, maxPhysicsNodes); _physicsNodes = Mathf.Clamp(ComputeDesiredNodesStable(_currentLength), 2, maxPhysicsNodes);
pCurr = new Vector3[physicsNodes];
pPrev = new Vector3[physicsNodes]; // ✅ 永久分配到最大,运行时不再 new避免GC卡顿
_pCurr = new Vector3[maxPhysicsNodes];
_pPrev = new Vector3[maxPhysicsNodes];
// 初始从起点往下排
Vector3 start = startAnchor ? startAnchor.position : transform.position; Vector3 start = startAnchor ? startAnchor.position : transform.position;
Vector3 dir = Vector3.down; Vector3 dir = Vector3.down;
for (int i = 0; i < physicsNodes; i++) for (int i = 0; i < _physicsNodes; i++)
{ {
Vector3 pos = start + dir * (physicsSegmentLen * i); Vector3 pos = start + dir * (physicsSegmentLen * i);
pCurr[i] = pos; _pCurr[i] = pos;
pPrev[i] = pos; _pPrev[i] = pos;
} }
UpdateHeadRestLenFromCurrentLength(); UpdateHeadRestLenFromCurrentLength();
@@ -185,190 +163,195 @@ public class Rope : MonoBehaviour
private int ComputeDesiredNodes(float lengthMeters) private int ComputeDesiredNodes(float lengthMeters)
{ {
// nodes = floor(length/segLen)+1
int desired = Mathf.FloorToInt(Mathf.Max(0f, lengthMeters) / physicsSegmentLen) + 1; int desired = Mathf.FloorToInt(Mathf.Max(0f, lengthMeters) / physicsSegmentLen) + 1;
desired = Mathf.Clamp(desired, minPhysicsNodes, maxPhysicsNodes); desired = Mathf.Clamp(desired, minPhysicsNodes, maxPhysicsNodes);
return desired; return desired;
} }
/// <summary>设置目标总长度(米)。不做最小/最大长度限制(最小可行由锚点距离决定)。</summary> private int ComputeDesiredNodesStable(float lengthMeters)
public void SetTargetLength(float lengthMeters)
{ {
targetLength = Mathf.Max(0f, lengthMeters); int desired = ComputeDesiredNodes(lengthMeters);
}
if (_lastDesiredNodes == 0)
public float GetCurrentLength() => currentLength;
public float GetTargetLength() => targetLength;
public float GetAnchorDistance()
{
if (startAnchor != null && endAnchor != null)
{ {
return Vector3.Distance(startAnchor.position, endAnchor.position); _lastDesiredNodes = desired;
return desired;
} }
return 0; if (desired == _lastDesiredNodes)
return desired;
// 边界lastDesiredNodes 对应的长度临界)
float boundary = (_lastDesiredNodes - 1) * physicsSegmentLen;
// 在临界附近就不切换,避免来回跳
if (Mathf.Abs(lengthMeters - boundary) < nodeHysteresis)
return _lastDesiredNodes;
_lastDesiredNodes = desired;
return desired;
} }
public void SetTargetLength(float lengthMeters)
{
_targetLength = Mathf.Max(0f, lengthMeters);
}
public float GetCurrentLength() => _currentLength;
public float GetTargetLength() => _targetLength;
private void FixedUpdate() private void FixedUpdate()
{ {
if (!startAnchor || !endAnchor) if (!startAnchor || !endAnchor) return;
return;
gravity.y = -gravityStrength; _gravity.y = -gravityStrength;
UpdateLengthSmooth(); // 只保证 >= 锚点直线距离 + minSlack UpdateLengthSmooth();
UpdateNodesFromLength(); // 只从头部增/减节点 UpdateNodesFromLength(); // ✅ 一次性增减无GC
UpdateHeadRestLenFromCurrentLength(); // 第一段补余量 => 变化集中在头部 UpdateHeadRestLenFromCurrentLength();
// simulate
Simulate(); Simulate();
// 确保端点正确后再迭代
LockAnchorsHard();
// constraints
for (int it = 0; it < iterations; it++) for (int it = 0; it < iterations; it++)
{
SolveDistanceConstraints_HeadOnly(); SolveDistanceConstraints_HeadOnly();
SolveBendConstraint();
LockAnchorsHard();
}
if (constrainToWater || constrainToGround) // 迭代后锁一次足够
ConstrainToGroundAndWater(); LockAnchorsHard();
if (constrainToGround)
ConstrainToGround();
// 约束后再锁一次(保险)
LockAnchorsHard(); LockAnchorsHard();
} }
private void LateUpdate() private void LateUpdate()
{ {
if (!startAnchor || !endAnchor || pCurr == null || physicsNodes < 2) return; if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return;
int last = physicsNodes - 1; // 端点“跟手”:渲染前强行同步 transform消除慢一拍
int last = _physicsNodes - 1;
Vector3 s = startAnchor.transform.position; Vector3 s = startAnchor.transform.position;
Vector3 e = endAnchor.transform.position; Vector3 e = endAnchor.transform.position;
pCurr[0] = s; pPrev[0] = s; // ✅ 关键:同步 pPrev _pCurr[0] = s;
pCurr[last] = e; pPrev[last] = e; // ✅ 关键:同步 pPrev _pPrev[0] = s;
_pCurr[last] = e;
_pPrev[last] = e;
DrawHighResLine(); DrawHighResLine();
} }
private void UpdateLengthSmooth() private void UpdateLengthSmooth()
{ {
// float anchorDist = Vector3.Distance(startAnchor.position, endAnchor.position);
// float minFeasible = anchorDist + minSlack;
float minFeasible = 0.01f; float minFeasible = 0.01f;
// ✅ 最小长度 = 起点终点直线距离(+slack),最大不限制 float desired = Mathf.Max(_targetLength, minFeasible);
float desired = Mathf.Max(targetLength, minFeasible);
float prevLen = currentLength; _currentLength = Mathf.SmoothDamp(
_currentLength,
currentLength = Mathf.SmoothDamp(
currentLength,
desired, desired,
ref lengthSmoothVel, ref _lengthSmoothVel,
lengthSmoothTime, lengthSmoothTime,
Mathf.Infinity, Mathf.Infinity,
Time.fixedDeltaTime 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() private void UpdateNodesFromLength()
{ {
int desired = ComputeDesiredNodes(currentLength); int desired = ComputeDesiredNodesStable(_currentLength);
if (desired == physicsNodes) return; desired = Mathf.Clamp(desired, 2, maxPhysicsNodes);
if (desired == _physicsNodes) return;
while (physicsNodes < desired) if (desired > _physicsNodes)
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]; AddNodesAtStart(desired - _physicsNodes);
newPrev[i] = pPrev[i - 1]; }
else
{
RemoveNodesAtStart(_physicsNodes - desired);
} }
Vector3 s = startAnchor.position; _physicsNodes = desired;
Vector3 dir = Vector3.down;
if (physicsNodes >= 2) // 渲染缓存按最大 subdiv 预留即可
RebuildRenderBufferIfNeeded(renderSubdivisionsIdle);
}
private void AddNodesAtStart(int addCount)
{
if (addCount <= 0) return;
int oldCount = _physicsNodes;
int newCount = Mathf.Min(oldCount + addCount, maxPhysicsNodes);
addCount = newCount - oldCount;
if (addCount <= 0) return;
// 把 [1..oldCount-1] 整体往后挪 addCount给 [1..addCount] 腾位置
// oldCount-1 个元素(包含尾端锚点位,尾端后面会被锁)
Array.Copy(_pCurr, 1, _pCurr, 1 + addCount, oldCount - 1);
Array.Copy(_pPrev, 1, _pPrev, 1 + addCount, oldCount - 1);
Vector3 s = startAnchor.position;
// 方向:用“挪完后的第一个动态点”指向来估计
Vector3 dir = Vector3.down;
int firstOld = 1 + addCount;
if (firstOld < 1 + addCount + (oldCount - 2) && oldCount >= 2)
{ {
Vector3 toOld1 = (pCurr[1] - s); Vector3 toOld1 = (_pCurr[firstOld] - s);
if (toOld1.sqrMagnitude > 1e-6f) dir = toOld1.normalized; if (toOld1.sqrMagnitude > 1e-6f) dir = toOld1.normalized;
} }
Vector3 pos = s + dir * physicsSegmentLen; // 继承速度
newCurr[1] = pos; float dt = Time.fixedDeltaTime;
newPrev[1] = pos; Vector3 inheritVel = Vector3.zero;
if (oldCount >= 2 && firstOld < maxPhysicsNodes)
{
inheritVel = (_pCurr[firstOld] - _pPrev[firstOld]) / Mathf.Max(dt, 1e-6f);
}
pCurr = newCurr; for (int k = 1; k <= addCount; k++)
pPrev = newPrev; {
physicsNodes = newCount; Vector3 pos = s + dir * (physicsSegmentLen * k);
_pCurr[k] = pos;
_pPrev[k] = pos - inheritVel * dt;
}
LockAnchorsHard(); LockAnchorsHard();
} }
private void RemoveNodeAtStart() private void RemoveNodesAtStart(int removeCount)
{ {
int newCount = Mathf.Max(physicsNodes - 1, 2); if (removeCount <= 0) return;
if (newCount == physicsNodes) return;
Vector3[] newCurr = new Vector3[newCount]; int oldCount = _physicsNodes;
Vector3[] newPrev = new Vector3[newCount]; int newCount = Mathf.Max(oldCount - removeCount, 2);
removeCount = oldCount - newCount;
if (removeCount <= 0) return;
newCurr[0] = pCurr[0]; // 把 [1+removeCount .. 1+removeCount+(newCount-2)-1] 挪到 [1..newCount-2]
newPrev[0] = pPrev[0]; // 中间动态节点数量 = newCount-2
Array.Copy(_pCurr, 1 + removeCount, _pCurr, 1, newCount - 2);
for (int i = 1; i < newCount - 1; i++) Array.Copy(_pPrev, 1 + removeCount, _pPrev, 1, newCount - 2);
{
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(); LockAnchorsHard();
} }
private void UpdateHeadRestLenFromCurrentLength() private void UpdateHeadRestLenFromCurrentLength()
{ {
int fixedSegCount = Mathf.Max(0, physicsNodes - 2); int fixedSegCount = Mathf.Max(0, _physicsNodes - 2);
float baseLen = fixedSegCount * physicsSegmentLen; float baseLen = fixedSegCount * physicsSegmentLen;
headRestLen = currentLength - baseLen; _headRestLen = _currentLength - baseLen;
_headRestLen = Mathf.Clamp(_headRestLen, headMinLen, physicsSegmentLen * 1.5f);
// 第一段允许在一个合理范围内变动(太长会像橡皮筋,太短会炸)
headRestLen = Mathf.Clamp(headRestLen, headMinLen, physicsSegmentLen * 1.5f);
} }
private void Simulate() private void Simulate()
@@ -376,91 +359,51 @@ public class Rope : MonoBehaviour
float dt = Time.fixedDeltaTime; float dt = Time.fixedDeltaTime;
float invDt = 1f / Mathf.Max(dt, 1e-6f); float invDt = 1f / Mathf.Max(dt, 1e-6f);
// 风方向归一化避免填了0向量导致NaN // 指数衰减:更稳定好调
Vector3 wDir = windDir; float kY = Mathf.Exp(-airDrag * dt);
if (wDir.sqrMagnitude < 1e-6f) wDir = Vector3.right; float kXZ = Mathf.Exp(-airDragXZ * dt);
wDir.Normalize();
for (int i = 0; i < physicsNodes; i++) for (int i = 1; i < _physicsNodes - 1; i++)
{ {
// Verlet 速度(由当前位置和上一帧位置推出来) Vector3 vel = (_pCurr[i] - _pPrev[i]) * invDt;
Vector3 vel = (pCurr[i] - pPrev[i]) * invDt;
// 先做“惯性推进” vel.x *= kXZ;
Vector3 next = pCurr[i] + (pCurr[i] - pPrev[i]) * velocityDampen; vel.z *= kXZ;
vel.y *= kY;
// 加速度 = 重力 + 空气阻力 + 风(相对速度) vel *= velocityDampen;
Vector3 acc = gravity;
// --- 空气阻力(与速度成正比)--- Vector3 next = _pCurr[i] + vel * dt;
// drag = -vel * airDrag并且横向更强一点
Vector3 drag = -vel * airDrag;
drag.x *= (1f + airDragXZ);
drag.z *= (1f + airDragXZ);
acc += drag;
// --- 风(让线在空中不那么“只会垂直掉”)--- _pPrev[i] = _pCurr[i];
if (i != 0 && i != physicsNodes - 1 && windStrength > 0f) _pCurr[i] = next + _gravity * (dt * dt);
{
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() private void LockAnchorsHard()
{ {
if (!startAnchor || !endAnchor || pCurr == null || pPrev == null || physicsNodes < 2) return; if (!startAnchor || !endAnchor || _pCurr == null || _pPrev == null || _physicsNodes < 2) return;
float dt = Time.fixedDeltaTime; float dt = Time.fixedDeltaTime;
Vector3 s = startAnchor.position; Vector3 s = startAnchor.position;
Vector3 e = endAnchor.position; Vector3 e = endAnchor.position;
pCurr[0] = s; _pCurr[0] = s;
pPrev[0] = s - startAnchor.linearVelocity * dt; _pPrev[0] = s - startAnchor.linearVelocity * dt;
int last = physicsNodes - 1; int last = _physicsNodes - 1;
pCurr[last] = e; _pCurr[last] = e;
pPrev[last] = e - endAnchor.linearVelocity * dt; _pPrev[last] = e - endAnchor.linearVelocity * dt;
} }
private void SolveDistanceConstraints_HeadOnly() private void SolveDistanceConstraints_HeadOnly()
{ {
for (int i = 0; i < physicsNodes - 1; i++) for (int i = 0; i < _physicsNodes - 1; i++)
{ {
float rest = (i == 0) ? headRestLen : physicsSegmentLen; float rest = (i == 0) ? _headRestLen : physicsSegmentLen;
Vector3 a = pCurr[i]; Vector3 a = _pCurr[i];
Vector3 b = pCurr[i + 1]; Vector3 b = _pCurr[i + 1];
Vector3 delta = b - a; Vector3 delta = b - a;
float dist = delta.magnitude; float dist = delta.magnitude;
@@ -470,60 +413,23 @@ public class Rope : MonoBehaviour
Vector3 corr = delta * diff * stiffness; Vector3 corr = delta * diff * stiffness;
if (i != 0) if (i != 0)
pCurr[i] += corr * 0.5f; _pCurr[i] += corr * 0.5f;
if (i + 1 != physicsNodes - 1) if (i + 1 != _physicsNodes - 1)
pCurr[i + 1] -= corr * 0.5f; _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() private void ConstrainToGround()
{ {
int groundLayerMask = collisionMask & groundMask; // ✅ 统一过滤:只检测指定层 for (int i = 1; i < _physicsNodes - 1; i++)
for (int i = 1; i < physicsNodes - 1; i++)
{ {
Vector3 p = pCurr[i]; Vector3 p = _pCurr[i];
if (constrainToWater) if (constrainToGround && groundMask != 0)
{
float minY = waterHeight + waterRadius;
if (p.y < minY) p.y = minY;
}
if (constrainToGround && groundLayerMask != 0)
{ {
Vector3 origin = p + Vector3.up * groundCastHeight; Vector3 origin = p + Vector3.up * groundCastHeight;
if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundLayerMask, if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask,
QueryTriggerInteraction.Ignore)) QueryTriggerInteraction.Ignore))
{ {
float minY = hit.point.y + groundRadius; float minY = hit.point.y + groundRadius;
@@ -531,64 +437,79 @@ public class Rope : MonoBehaviour
} }
} }
pCurr[i] = p; _pCurr[i] = p;
} }
} }
private void DrawHighResLine() private void DrawHighResLine()
{ {
if (pCurr == null || physicsNodes < 2) return; if (_pCurr == null || _physicsNodes < 2) return;
RebuildRenderBufferIfNeeded(); _lineRenderer.startWidth = lineWidth;
_lineRenderer.endWidth = lineWidth;
lr.startWidth = lineWidth;
lr.endWidth = lineWidth;
if (!smooth) if (!smooth)
{ {
lr.positionCount = physicsNodes; _lineRenderer.positionCount = _physicsNodes;
lr.SetPositions(pCurr); // 只传有效段
// LineRenderer.SetPositions 会读数组的前 positionCount 个
_lineRenderer.SetPositions(_pCurr);
return; return;
} }
// 动态降 subdiv甩动时降低点数减少CPU开销
int subdiv = PickRenderSubdivisions();
RebuildRenderBufferIfNeeded(subdiv);
int idx = 0; int idx = 0;
for (int seg = 0; seg < physicsNodes - 1; seg++) for (int seg = 0; seg < _physicsNodes - 1; seg++)
{ {
Vector3 p0 = pCurr[Mathf.Max(seg - 1, 0)]; Vector3 p0 = _pCurr[Mathf.Max(seg - 1, 0)];
Vector3 p1 = pCurr[seg]; Vector3 p1 = _pCurr[seg];
Vector3 p2 = pCurr[seg + 1]; Vector3 p2 = _pCurr[seg + 1];
Vector3 p3 = pCurr[Mathf.Min(seg + 2, physicsNodes - 1)]; Vector3 p3 = _pCurr[Mathf.Min(seg + 2, _physicsNodes - 1)];
for (int s = 0; s < renderSubdivisions; s++) for (int s = 0; s < subdiv; s++)
{ {
float t = s / (float)renderSubdivisions; float t = s / (float)subdiv;
Vector3 pt = CatmullRom_XZ_LinearY(p0, p1, p2, p3, t); Vector3 pt = CatmullRom_XZ_LinearY(p0, p1, p2, p3, t);
_rPoints[idx++] = pt;
// 如果水面约束开启:渲染点也夹一下,避免视觉上又穿回去
if (constrainToWater)
{
float minY = waterHeight + waterRadius;
if (pt.y < minY) pt.y = minY;
}
rPoints[idx++] = pt;
} }
} }
rPoints[idx++] = pCurr[physicsNodes - 1]; _rPoints[idx++] = _pCurr[_physicsNodes - 1];
lr.positionCount = idx; _lineRenderer.positionCount = idx;
lr.SetPositions(rPoints); _lineRenderer.SetPositions(_rPoints);
} }
private void RebuildRenderBufferIfNeeded() private int PickRenderSubdivisions()
{ {
int targetCount = (physicsNodes - 1) * renderSubdivisions + 1; int idle = Mathf.Max(1, renderSubdivisionsIdle);
if (rPoints == null || rCountCached != targetCount) int moving = Mathf.Max(1, renderSubdivisionsMoving);
// 计算平均速度(用 Verlet 差分)
float dt = Time.fixedDeltaTime;
float invDt = 1f / Mathf.Max(dt, 1e-6f);
float sum = 0f;
int count = Mathf.Max(1, _physicsNodes - 2);
for (int i = 1; i < _physicsNodes - 1; i++)
sum += (_pCurr[i] - _pPrev[i]).magnitude * invDt;
float avgSpeed = sum / count;
return (avgSpeed > movingSpeedThreshold) ? moving : idle;
}
private void RebuildRenderBufferIfNeeded(int subdiv)
{
int targetCount = (_physicsNodes - 1) * subdiv + 1;
if (_rPoints == null || _rCountCached != targetCount)
{ {
rPoints = new Vector3[targetCount]; _rPoints = new Vector3[targetCount];
rCountCached = targetCount; _rCountCached = targetCount;
} }
} }
@@ -605,22 +526,18 @@ public class Rope : MonoBehaviour
); );
} }
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) 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); Vector3 cr = CatmullRom(p0, p1, p2, p3, t);
// Y 不做样条,改成线性(不会过冲)
cr.y = Mathf.Lerp(p1.y, p2.y, t); cr.y = Mathf.Lerp(p1.y, p2.y, t);
return cr; return cr;
} }
private void OnDrawGizmosSelected()
{
if (_pCurr == null) return;
Gizmos.color = Color.yellow;
for (int i = 0; i < _physicsNodes; i++)
Gizmos.DrawSphere(_pCurr[i], 0.01f);
}
} }