鱼线性能优化

This commit is contained in:
2026-02-25 23:46:50 +08:00
parent ceaef328e3
commit adbd097fd5
2 changed files with 186 additions and 148 deletions

View File

@@ -8,6 +8,9 @@ public class Rope : MonoBehaviour
[Header("Anchors")] [SerializeField] public Rigidbody startAnchor;
[SerializeField] public Rigidbody endAnchor;
/// <summary>鱼线宽度倍数</summary>
public int LineMultiple = 1;
[Header("Physics (Dynamic Nodes, Fixed Segment Len)")] [SerializeField, Min(0.01f), Tooltip("物理每段固定长度(越小越细致越耗)")]
private float physicsSegmentLen = 0.15f;
@@ -65,7 +68,7 @@ public class Rope : MonoBehaviour
[SerializeField, Tooltip("是否使用 Catmull-Rom 平滑(开启更顺,但更耗)")]
private bool smooth = true;
[SerializeField, Min(0.0001f)] private float lineWidth = 0.002f;
[SerializeField, Min(0.0001f)] private float lineWidth = 0.001f;
[Header("Air Drag (Stable)")] [SerializeField, Range(0f, 5f), Tooltip("空气阻力Y向指数衰减越大越不飘")]
private float airDrag = 0.9f;
@@ -75,14 +78,14 @@ public class Rope : MonoBehaviour
private LineRenderer _lineRenderer;
// physics (注意:数组固定为 maxPhysicsNodesphysicsNodes 表示有效长度)
// physics
private int _physicsNodes;
private Vector3[] _pCurr;
private Vector3[] _pPrev;
// render
// render (一次性分配到最大,后续不再 new)
private Vector3[] _rPoints;
private int _rCountCached = -1;
private int _rCapacity;
private Vector3 _gravity;
@@ -97,17 +100,47 @@ public class Rope : MonoBehaviour
// node stability
private int _lastDesiredNodes = 0;
// caches
private Transform _startTr;
private Transform _endTr;
// precomputed
private float _dt;
private float _dt2;
private float _kY;
private float _kXZ;
private FRod _rod;
public void Init(FRod rod) => _rod = rod;
// Catmull t caches只缓存 idle/moving 两档,减少每帧重复乘法)
private struct TCaches
{
public float[] t;
public float[] t2;
public float[] t3;
}
private TCaches _tIdle;
private TCaches _tMoving;
private void Awake()
{
_lineRenderer = GetComponent<LineRenderer>();
_gravity = new Vector3(0f, -gravityStrength, 0f);
_startTr = startAnchor ? startAnchor.transform : null;
_endTr = endAnchor ? endAnchor.transform : null;
InitLengthSystem();
AllocateAndInitNodes();
RebuildRenderBufferIfNeeded(renderSubdivisionsIdle);
// ✅ 渲染点一次性分配到最大: (maxNodes-1)*idle + 1
int maxSubdiv = Mathf.Max(1, renderSubdivisionsIdle);
_rCapacity = (maxPhysicsNodes - 1) * maxSubdiv + 1;
_rPoints = new Vector3[_rCapacity];
BuildTCaches(renderSubdivisionsIdle, ref _tIdle);
BuildTCaches(renderSubdivisionsMoving, ref _tMoving);
}
private void OnValidate()
@@ -127,7 +160,6 @@ public class Rope : MonoBehaviour
headMinLen = Mathf.Max(headMinLen, 0.0001f);
nodeHysteresis = Mathf.Max(0f, nodeHysteresis);
}
private void InitLengthSystem()
@@ -141,7 +173,6 @@ public class Rope : MonoBehaviour
{
_physicsNodes = Mathf.Clamp(ComputeDesiredNodesStable(_currentLength), 2, maxPhysicsNodes);
// ✅ 永久分配到最大,运行时不再 new避免GC卡顿
_pCurr = new Vector3[maxPhysicsNodes];
_pPrev = new Vector3[maxPhysicsNodes];
@@ -181,10 +212,7 @@ public class Rope : MonoBehaviour
if (desired == _lastDesiredNodes)
return desired;
// 边界lastDesiredNodes 对应的长度临界)
float boundary = (_lastDesiredNodes - 1) * physicsSegmentLen;
// 在临界附近就不切换,避免来回跳
if (Mathf.Abs(lengthMeters - boundary) < nodeHysteresis)
return _lastDesiredNodes;
@@ -192,11 +220,7 @@ public class Rope : MonoBehaviour
return desired;
}
public void SetTargetLength(float lengthMeters)
{
_targetLength = Mathf.Max(0f, lengthMeters);
}
public void SetTargetLength(float lengthMeters) => _targetLength = Mathf.Max(0f, lengthMeters);
public float GetCurrentLength() => _currentLength;
public float GetTargetLength() => _targetLength;
@@ -204,29 +228,36 @@ public class Rope : MonoBehaviour
{
if (!startAnchor || !endAnchor) return;
// cache dt
_dt = Time.fixedDeltaTime;
if (_dt < 1e-6f) _dt = 1e-6f;
_dt2 = _dt * _dt;
// gravity
_gravity.y = -gravityStrength;
// drag cachesexp 比较贵,但这里每 FixedUpdate 一次OK
_kY = Mathf.Exp(-airDrag * _dt);
_kXZ = Mathf.Exp(-airDragXZ * _dt);
UpdateLengthSmooth();
UpdateNodesFromLength(); // ✅ 一次性增减无GC
UpdateNodesFromLength();
UpdateHeadRestLenFromCurrentLength();
// simulate
Simulate();
Simulate_VerletFast();
// 确保端点正确后再迭代
// anchors
LockAnchorsHard();
// constraints
for (int it = 0; it < iterations; it++)
SolveDistanceConstraints_HeadOnly();
SolveDistanceConstraints_HeadOnly_Fast();
// 迭代后锁一次足够
LockAnchorsHard();
if (constrainToGround)
ConstrainToGround();
// 约束后再锁一次(保险)
LockAnchorsHard();
}
@@ -234,23 +265,23 @@ public class Rope : MonoBehaviour
{
if (!startAnchor || !endAnchor || _pCurr == null || _physicsNodes < 2) return;
// 端点“跟手”:渲染前强行同步 transform消除慢一拍
int last = _physicsNodes - 1;
Vector3 s = startAnchor.transform.position;
Vector3 e = endAnchor.transform.position;
// 用缓存 transform避免多次属性链
Vector3 s = _startTr.position;
Vector3 e = _endTr.position;
_pCurr[0] = s;
_pPrev[0] = s;
_pCurr[last] = e;
_pPrev[last] = e;
DrawHighResLine();
DrawHighResLine_Fast();
}
private void UpdateLengthSmooth()
{
float minFeasible = 0.01f;
float desired = Mathf.Max(_targetLength, minFeasible);
_currentLength = Mathf.SmoothDamp(
@@ -269,22 +300,12 @@ public class Rope : MonoBehaviour
desired = Mathf.Clamp(desired, 2, maxPhysicsNodes);
if (desired == _physicsNodes) return;
if (desired > _physicsNodes)
{
AddNodesAtStart(desired - _physicsNodes);
}
else
{
RemoveNodesAtStart(_physicsNodes - desired);
}
if (desired > _physicsNodes) AddNodesAtStart(desired - _physicsNodes);
else RemoveNodesAtStart(_physicsNodes - desired);
_physicsNodes = desired;
// 渲染缓存按最大 subdiv 预留即可
RebuildRenderBufferIfNeeded(renderSubdivisionsIdle);
}
private void AddNodesAtStart(int addCount)
{
if (addCount <= 0) return;
@@ -294,35 +315,30 @@ public class Rope : MonoBehaviour
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 s = _startTr ? _startTr.position : startAnchor.position;
// 方向:用“挪完后的第一个动态点”指向来估计
Vector3 dir = Vector3.down;
int firstOld = 1 + addCount;
if (firstOld < 1 + addCount + (oldCount - 2) && oldCount >= 2)
{
Vector3 toOld1 = (_pCurr[firstOld] - s);
if (toOld1.sqrMagnitude > 1e-6f) dir = toOld1.normalized;
}
// 继承速度
float dt = Time.fixedDeltaTime;
Vector3 inheritVel = Vector3.zero;
if (oldCount >= 2 && firstOld < maxPhysicsNodes)
{
inheritVel = (_pCurr[firstOld] - _pPrev[firstOld]) / Mathf.Max(dt, 1e-6f);
Vector3 toOld1 = (_pCurr[firstOld] - s);
float sq = toOld1.sqrMagnitude;
if (sq > 1e-6f) dir = toOld1 / Mathf.Sqrt(sq);
}
// inherit displacement (Verlet)
Vector3 inheritDisp = Vector3.zero;
if (oldCount >= 2 && firstOld < maxPhysicsNodes)
inheritDisp = (_pCurr[firstOld] - _pPrev[firstOld]);
for (int k = 1; k <= addCount; k++)
{
Vector3 pos = s + dir * (physicsSegmentLen * k);
_pCurr[k] = pos;
_pPrev[k] = pos - inheritVel * dt;
_pPrev[k] = pos - inheritDisp; // 保持动感
}
LockAnchorsHard();
@@ -337,8 +353,6 @@ public class Rope : MonoBehaviour
removeCount = oldCount - newCount;
if (removeCount <= 0) return;
// 把 [1+removeCount .. 1+removeCount+(newCount-2)-1] 挪到 [1..newCount-2]
// 中间动态节点数量 = newCount-2
Array.Copy(_pCurr, 1 + removeCount, _pCurr, 1, newCount - 2);
Array.Copy(_pPrev, 1 + removeCount, _pPrev, 1, newCount - 2);
@@ -354,29 +368,27 @@ public class Rope : MonoBehaviour
_headRestLen = Mathf.Clamp(_headRestLen, headMinLen, physicsSegmentLen * 1.5f);
}
private void Simulate()
/// <summary>
/// ✅ 更快的 Verlet去掉 /dt 和 *dt 抵消的无效计算
/// </summary>
private void Simulate_VerletFast()
{
float dt = Time.fixedDeltaTime;
float invDt = 1f / Mathf.Max(dt, 1e-6f);
// 指数衰减:更稳定好调
float kY = Mathf.Exp(-airDrag * dt);
float kXZ = Mathf.Exp(-airDragXZ * dt);
// displacement = curr - prev
// next = curr + displacement*drag*dampen + gravity*dt^2
for (int i = 1; i < _physicsNodes - 1; i++)
{
Vector3 vel = (_pCurr[i] - _pPrev[i]) * invDt;
Vector3 disp = _pCurr[i] - _pPrev[i];
vel.x *= kXZ;
vel.z *= kXZ;
vel.y *= kY;
disp.x *= _kXZ;
disp.z *= _kXZ;
disp.y *= _kY;
vel *= velocityDampen;
disp *= velocityDampen;
Vector3 next = _pCurr[i] + vel * dt;
Vector3 next = _pCurr[i] + disp + _gravity * _dt2;
_pPrev[i] = _pCurr[i];
_pCurr[i] = next + _gravity * (dt * dt);
_pCurr[i] = next;
}
}
@@ -384,21 +396,25 @@ public class Rope : MonoBehaviour
{
if (!startAnchor || !endAnchor || _pCurr == null || _pPrev == null || _physicsNodes < 2) return;
float dt = Time.fixedDeltaTime;
Vector3 s = startAnchor.position;
Vector3 e = endAnchor.position;
Vector3 s = _startTr ? _startTr.position : startAnchor.position;
Vector3 e = _endTr ? _endTr.position : endAnchor.position;
_pCurr[0] = s;
_pPrev[0] = s - startAnchor.linearVelocity * dt;
_pPrev[0] = s - startAnchor.linearVelocity * _dt;
int last = _physicsNodes - 1;
_pCurr[last] = e;
_pPrev[last] = e - endAnchor.linearVelocity * dt;
_pPrev[last] = e - endAnchor.linearVelocity * _dt;
}
private void SolveDistanceConstraints_HeadOnly()
/// <summary>
/// ✅ 约束:减少临时变量、用 sqrMagnitude + invDist
/// </summary>
private void SolveDistanceConstraints_HeadOnly_Fast()
{
for (int i = 0; i < _physicsNodes - 1; i++)
int last = _physicsNodes - 1;
for (int i = 0; i < last; i++)
{
float rest = (i == 0) ? _headRestLen : physicsSegmentLen;
@@ -406,131 +422,151 @@ public class Rope : MonoBehaviour
Vector3 b = _pCurr[i + 1];
Vector3 delta = b - a;
float dist = delta.magnitude;
if (dist < 1e-6f) continue;
float sq = delta.sqrMagnitude;
if (sq < 1e-12f) continue;
float diff = (dist - rest) / dist;
Vector3 corr = delta * diff * stiffness;
float dist = Mathf.Sqrt(sq);
float diff = (dist - rest) / dist; // = 1 - 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;
// i==0 锚点固定last 锚点固定
if (i != 0) _pCurr[i] = a + corr * 0.5f;
if (i + 1 != last) _pCurr[i + 1] = b - corr * 0.5f;
}
}
private void ConstrainToGround()
{
if (groundMask == 0) return;
// RaycastHit 是 struct这里不会 GC
for (int i = 1; i < _physicsNodes - 1; i++)
{
Vector3 p = _pCurr[i];
Vector3 origin = p + Vector3.up * groundCastHeight;
if (constrainToGround && groundMask != 0)
if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask,
QueryTriggerInteraction.Ignore))
{
Vector3 origin = p + Vector3.up * groundCastHeight;
if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, groundCastDistance, groundMask,
QueryTriggerInteraction.Ignore))
{
float minY = hit.point.y + groundRadius;
if (p.y < minY) p.y = minY;
}
float minY = hit.point.y + groundRadius;
if (p.y < minY) p.y = minY;
}
_pCurr[i] = p;
}
}
private void DrawHighResLine()
private void DrawHighResLine_Fast()
{
if (_pCurr == null || _physicsNodes < 2) return;
_lineRenderer.startWidth = lineWidth;
_lineRenderer.endWidth = lineWidth;
float w = lineWidth * LineMultiple;
_lineRenderer.startWidth = w;
_lineRenderer.endWidth = w;
if (!smooth)
{
_lineRenderer.positionCount = _physicsNodes;
// 只传有效段
// LineRenderer.SetPositions 会读数组的前 positionCount 个
_lineRenderer.SetPositions(_pCurr);
return;
}
// 动态降 subdiv甩动时降低点数减少CPU开销
int subdiv = PickRenderSubdivisions();
RebuildRenderBufferIfNeeded(subdiv);
int subdiv = PickRenderSubdivisions_Fast();
TCaches tc = (subdiv == renderSubdivisionsMoving) ? _tMoving : _tIdle;
int needed = (_physicsNodes - 1) * subdiv + 1;
if (needed > _rCapacity)
{
// 理论上不该发生_rCapacity 用 maxNodes & idle 分配)
// 保险扩容一次
_rCapacity = needed;
_rPoints = new Vector3[_rCapacity];
}
int idx = 0;
int last = _physicsNodes - 1;
for (int seg = 0; seg < _physicsNodes - 1; seg++)
for (int seg = 0; seg < last; 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)];
int i0 = seg - 1; if (i0 < 0) i0 = 0;
int i1 = seg;
int i2 = seg + 1;
int i3 = seg + 2; if (i3 > last) i3 = last;
Vector3 p0 = _pCurr[i0];
Vector3 p1 = _pCurr[i1];
Vector3 p2 = _pCurr[i2];
Vector3 p3 = _pCurr[i3];
for (int s = 0; s < subdiv; s++)
{
float t = s / (float)subdiv;
Vector3 pt = CatmullRom_XZ_LinearY(p0, p1, p2, p3, t);
_rPoints[idx++] = pt;
float t = tc.t[s];
float t2 = tc.t2[s];
float t3 = tc.t3[s];
// inline CatmullRom少一次函数调用
Vector3 cr =
0.5f * (
(2f * p1) +
(-p0 + p2) * t +
(2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 +
(-p0 + 3f * p1 - 3f * p2 + p3) * t3
);
// Linear Y
cr.y = p1.y + (p2.y - p1.y) * t;
_rPoints[idx++] = cr;
}
}
_rPoints[idx++] = _pCurr[_physicsNodes - 1];
_rPoints[idx++] = _pCurr[last];
_lineRenderer.positionCount = idx;
_lineRenderer.SetPositions(_rPoints);
}
private int PickRenderSubdivisions()
/// <summary>
/// ✅ 用 sqrMagnitude 比较阈值,避免 sqrt
/// </summary>
private int PickRenderSubdivisions_Fast()
{
int idle = Mathf.Max(1, renderSubdivisionsIdle);
int moving = Mathf.Max(1, renderSubdivisionsMoving);
// 计算平均速度(用 Verlet 差分)
float dt = Time.fixedDeltaTime;
float invDt = 1f / Mathf.Max(dt, 1e-6f);
float thr = movingSpeedThreshold;
float thrSq = (thr * _dt) * (thr * _dt); // 因为我们用 disp = curr-prev单位是米/step所以阈值要乘 dt
float sum = 0f;
float sumSq = 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];
_rCountCached = targetCount;
Vector3 disp = _pCurr[i] - _pPrev[i];
sumSq += disp.sqrMagnitude;
}
float avgSq = sumSq / count;
return (avgSq > thrSq) ? moving : idle;
}
private static Vector3 CatmullRom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
private static void BuildTCaches(int subdiv, ref TCaches caches)
{
float t2 = t * t;
float t3 = t2 * t;
subdiv = Mathf.Max(1, subdiv);
caches.t = new float[subdiv];
caches.t2 = new float[subdiv];
caches.t3 = new float[subdiv];
return 0.5f * (
(2f * p1) +
(-p0 + p2) * t +
(2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 +
(-p0 + 3f * p1 - 3f * p2 + p3) * t3
);
}
private static Vector3 CatmullRom_XZ_LinearY(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
Vector3 cr = CatmullRom(p0, p1, p2, p3, t);
cr.y = Mathf.Lerp(p1.y, p2.y, t);
return cr;
float inv = 1f / subdiv;
for (int s = 0; s < subdiv; s++)
{
float t = s * inv;
float t2 = t * t;
caches.t[s] = t;
caches.t2[s] = t2;
caches.t3[s] = t2 * t;
}
}
private void OnDrawGizmosSelected()