feat(cable): 线缆改用 Mesh 渲染并优化插接物理

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-09 12:54:36 +08:00
co-authored by Cursor
parent f6997e8816
commit 4ded2b8eab
+135 -122
View File
@@ -22,8 +22,8 @@ public class PhysicCable : MonoBehaviour
[Header("绳体")]
[SerializeField] private int pointCount = 40;
[SerializeField] private float minLength = 2f; // 闲置时的自然下垂长度
[SerializeField, Range(0.05f, 1f)] private float idleLengthScale = 0.165f;
[SerializeField] private float maxStretch = 4f; // 最大拉出长度(相对 minLength 的倍数)
[SerializeField, Range(0.05f, 1f)] private float idleLengthScale = 0.08f;
[SerializeField] private float maxStretch = 8f; // 最大拉出长度(相对 minLength 的倍数)
[SerializeField] private float gravity = 100f; // 世界单位/s²,向下
[SerializeField, Range(0.8f, 1f)] private float damping = 0.965f;
[SerializeField] private int constraintIterations = 24;
@@ -48,12 +48,30 @@ public class PhysicCable : MonoBehaviour
[SerializeField] private Transform reelCenter; // 出线方向参考中心,空则用 startTransform.parent
[SerializeField] private Transform floorLimit; // 地面高度参考,空则不启用地面碰撞
[Header("像素风渲染")]
[SerializeField] private bool usePixelStyle = true;
[SerializeField] private Color pixelDark = new(0.13f, 0.18f, 0.4f, 1f);
[SerializeField] private Color pixelBody = new(0.24f, 0.36f, 0.85f, 1f);
[SerializeField] private Color pixelMid = new(0.43f, 0.55f, 0.96f, 1f);
[SerializeField] private Color pixelHighlight = new(0.73f, 0.78f, 1f, 1f);
[Header("外观(单 Mesh 分带,对应原型 pixel 预设)")]
[SerializeField] private Material cableMaterial; // 空则运行时回退 Sprite-Unlit-Default / Sprites-Default
// Tools 层整层高于 FixTop(屏幕 UI Canvas 在 FixTop/100),插头历来在 Tools 上未被遮挡;
// 线缆与插头(Tools/48)成对相邻,剪线等演出覆盖物(Tools/50+)仍可整体盖住两者
[SerializeField] private string sortingLayerName = "Tools";
[SerializeField] private int sortingOrder = 47;
// 三档色阶(描边/主体/顶光),压暗去饱和贴合场景像素环境;原型的第四档高光去掉
[SerializeField] private Color shadowColor = new(0.05f, 0.05f, 0.07f, 0.2f);
[SerializeField] private float shadowWidth = 0.295f;
[SerializeField] private float shadowDrop = 0.12f; // 原型:向下平移 7px
[SerializeField] private Color darkColor = new(0.09f, 0.11f, 0.18f, 1f); // 深色描边
[SerializeField] private float darkWidth = 0.33f; // 原型:w+4
[SerializeField] private Color bodyColor = new(0.24f, 0.30f, 0.44f, 1f); // 主体(低饱和板岩蓝)
[SerializeField] private float bodyWidth = 0.26f; // 原型:w=15px ≈ 0.26wu
[SerializeField] private Color midColor = new(0.38f, 0.45f, 0.60f, 1f); // 顶光
[SerializeField] private float midWidth = 0.143f; // 原型:0.55w
[SerializeField] private float midLift = 0.036f; // 原型:上移 0.14w
[Header("端部收窄(默认关闭:整根线宽与插头/孔匹配)")]
[SerializeField, Range(0f, 0.3f)] private float tipTaperFraction = 0f; // 末端收窄段占全长比例,0 关闭
[SerializeField, Range(0.3f, 1f)] private float tipTaperScale = 0.72f; // 末端最细处宽度比例
[Header("自遮挡(尾段盖头段)")]
[SerializeField] private float overlapMargin = 0.1f; // 原型:m.w + 6px 的余量部分
public CableState State { get; private set; } = CableState.Hidden;
@@ -76,32 +94,22 @@ public class PhysicCable : MonoBehaviour
private Vector3 _pointer; // 拖拽指针(世界坐标,驱动送线长度)
private Vector3 _dragTarget; // 末端钉住目标(重 lerp + 长度钳制,消除拽满时的抖动)
private Transform _dock; // 当前插入的插孔
private Vector3 _plugSnapEnd; // 插接吸附目标:渐进收敛到孔心后末端硬钉,保证完全对齐堵住孔
private float _plugAngle = -Mathf.PI / 2f; // 插头朝向(弧度,初始朝下)
private LineRenderer _line;
private LineRenderer _bodyLine;
private LineRenderer _midLine;
private LineRenderer _highlightLine;
private LineRenderer _line; // 遗留组件,仅保持禁用(旧场景/CableSystem 仍会引用)
private CableMeshRenderer _cableMesh;
private Vector3[] _renderA;
private Vector3[] _renderB;
private const int SmoothIterations = 2; // Chaikin 切角细分次数
private string _defaultSortingLayer;
private int _defaultSortingOrder;
private float _baseWidthMultiplier;
private AnimationCurve _baseWidthCurve;
private bool _initialized;
private void Awake()
{
_line = GetComponent<LineRenderer>();
_defaultSortingLayer = _line.sortingLayerName;
_defaultSortingOrder = _line.sortingOrder;
_baseWidthMultiplier = _line.widthMultiplier;
_baseWidthCurve = _line.widthCurve;
_line.enabled = false;
EnsurePixelLines();
ApplyPixelStyle();
if (_line != null) _line.enabled = false;
EnsureMeshRenderer();
}
// ------------------------------ 状态 API ------------------------------
@@ -110,8 +118,7 @@ public class PhysicCable : MonoBehaviour
{
State = CableState.Hidden;
_dock = null;
if (_line != null) _line.enabled = false;
SetPixelLinesEnabled(false);
SetCableRenderersEnabled(false);
}
/// <summary>闲置下垂状态;resetShape 时把绳重置为从出线口自然下垂。</summary>
@@ -159,6 +166,8 @@ public class PhysicCable : MonoBehaviour
{
ResetStraightTo(FlattenZ(dock.position));
}
// 吸附目标从当前末端出发,向孔心渐进收敛(保留吸入动画,最终严格钉在孔心)
_plugSnapEnd = _pts[_n - 1].pos;
}
// ------------------------------ 插头姿态 ------------------------------
@@ -183,19 +192,6 @@ public class PhysicCable : MonoBehaviour
}
}
// ------------------------------ 渲染排序 ------------------------------
public void SetSorting(string sortingLayerName, int sortingOrder)
{
// 主线必须保持在出线口/线盘下方;点击时只提高插头本体层级。
}
public void RestoreDefaultSorting()
{
_line.sortingLayerName = _defaultSortingLayer;
_line.sortingOrder = _defaultSortingOrder;
}
// ------------------------------ 模拟 ------------------------------
private void FixedUpdate()
@@ -203,6 +199,13 @@ public class PhysicCable : MonoBehaviour
if (State == CableState.Hidden || _pts == null || startTransform == null) return;
Simulate(Time.fixedDeltaTime);
}
private void LateUpdate()
{
// 渲染放 LateUpdate:与插头跟随(CablePanel.LateUpdate)读同一份点位,消除拖拽时线头分离
if (State == CableState.Hidden || _pts == null) return;
Render();
}
@@ -211,17 +214,13 @@ public class PhysicCable : MonoBehaviour
if (_initialized) return;
_initialized = true;
if (_line == null) _line = GetComponent<LineRenderer>();
EnsurePixelLines();
ApplyPixelStyle();
EnsureMeshRenderer();
_n = Mathf.Max(8, pointCount);
_pts = new Particle[_n];
int renderCount = _n << SmoothIterations;
_renderA = new Vector3[renderCount];
_renderB = new Vector3[renderCount];
_line.positionCount = renderCount;
SetPixelLinePositionCount(renderCount);
ResetHangingShape();
}
@@ -315,7 +314,11 @@ public class PhysicCable : MonoBehaviour
}
else if (plugged)
{
_pts[_n - 1].pos += (dock - _pts[_n - 1].pos) * 0.5f;
// 吸附目标渐进收敛到孔心后,末端硬钉——软拉会被重力/张力持续拽偏,堵不严孔
_plugSnapEnd += (dock - _plugSnapEnd) * 0.5f;
if ((_plugSnapEnd - dock).sqrMagnitude < 1e-4f) _plugSnapEnd = dock;
_pts[_n - 1].pos = _plugSnapEnd;
_pts[_n - 1].prev = _plugSnapEnd;
}
// --- 约束迭代
@@ -341,6 +344,20 @@ public class PhysicCable : MonoBehaviour
// 无论绳的其余部分被拽到哪,出线处都不出现折角或自穿插
ApplyOutletStub(anchor, outletDir);
// 插接时把末段摆直指向孔心(移植原型的 plugged e2 约束,方向按来线方向泛化):
// 倒数第二点拉向"孔心沿来线方向后退一节"的位置,末段直插进孔,不斜搭在孔上
if (plugged)
{
Vector3 approach = dock - _pts[_n - 3].pos;
approach.z = 0;
float approachDist = approach.magnitude;
if (approachDist > 1e-4f)
{
Vector3 straightTarget = dock - approach / approachDist * seg;
_pts[_n - 2].pos += (straightTarget - _pts[_n - 2].pos) * 0.4f;
}
}
// 弯曲刚度:中点平滑(每 3 次迭代做一次)
if (bendSmooth > 0f && k % 3 == 0)
{
@@ -469,11 +486,37 @@ public class PhysicCable : MonoBehaviour
(src, dst) = (dst, src);
}
for (int i = 0; i < count; i++)
// 插接时线缆末端由平头切面改为圆头端帽(线自身的圆头,同旧 LineRenderer),读作"插进孔里"
_cableMesh.UpdateMesh(src, count, FindTailStart(count), State == CableState.Plugged);
}
/// <summary>
/// 自遮挡检测(移植自原型):找到下垂最低点 j,若 j 之后的尾段与 j 之前的头段
/// 空间重叠,返回尾段在平滑点数组里的起始下标(从 j 前两个物理点起),否则 -1。
/// </summary>
private int FindTailStart(int smoothedCount)
{
int j = 0;
for (int i = 1; i < _n; i++)
{
_line.SetPosition(i, src[i]);
if (_pts[i].pos.y < _pts[j].pos.y) j = i; // 原型 y 向下,这里取世界最低点
}
RenderPixelBands(src, count);
if (j >= _n - 2) return -1;
float rr = bodyWidth + overlapMargin;
float rrSqr = rr * rr;
bool overlap = false;
for (int s = j + 2; s < _n && !overlap; s += 2)
{
for (int t = 0; t < j - 2 && !overlap; t += 2)
{
if (s - t >= 8 && (_pts[s].pos - _pts[t].pos).sqrMagnitude < rrSqr) overlap = true;
}
}
if (!overlap) return -1;
int j0 = Mathf.Max(0, j - 2);
return Mathf.Clamp(j0 << SmoothIterations, 0, smoothedCount - 2);
}
private float GetIdleLength()
@@ -481,97 +524,67 @@ public class PhysicCable : MonoBehaviour
return Mathf.Max(0.1f, minLength * idleLengthScale);
}
private void EnsurePixelLines()
private void EnsureMeshRenderer()
{
if (!usePixelStyle || _line == null) return;
if (_cableMesh != null) return;
_bodyLine = EnsurePixelLine(_bodyLine, "Pixel Body");
_midLine = EnsurePixelLine(_midLine, "Pixel Mid");
_highlightLine = EnsurePixelLine(_highlightLine, "Pixel Highlight");
// MeshRenderer 不能与本物体上遗留的 LineRenderer 共存,放到子物体;
// 顶点直接用世界坐标,子物体世界变换必须归零
Transform child = transform.Find("Cable Mesh");
if (child == null)
{
child = new GameObject("Cable Mesh").transform;
child.SetParent(transform, false);
}
child.position = Vector3.zero;
child.rotation = Quaternion.identity;
// 继承 GameObject layer:后处理/渲染特性按层过滤时,线缆与周围面板行为一致
child.gameObject.layer = gameObject.layer;
_cableMesh = child.GetComponent<CableMeshRenderer>();
if (_cableMesh == null) _cableMesh = child.gameObject.AddComponent<CableMeshRenderer>();
_cableMesh.Configure(ResolveMaterial(), sortingLayerName, sortingOrder);
_cableMesh.SetBands(BuildBands());
_cableMesh.SetTipTaper(tipTaperFraction, tipTaperScale);
_cableMesh.SetVisible(State != CableState.Hidden);
}
private LineRenderer EnsurePixelLine(LineRenderer renderer, string objectName)
private Material ResolveMaterial()
{
if (renderer != null) return renderer;
if (cableMaterial != null) return cableMaterial;
Transform existing = transform.Find(objectName);
if (existing != null && existing.TryGetComponent(out LineRenderer existingRenderer))
// 直接共用出线口所在线盘 sprite 的材质:与周围环境的受光行为保证完全一致
if (startTransform != null)
{
return existingRenderer;
var reference = startTransform.GetComponentInParent<SpriteRenderer>();
if (reference != null && reference.sharedMaterial != null)
{
return reference.sharedMaterial;
}
}
var child = new GameObject(objectName);
child.transform.SetParent(transform, false);
return child.AddComponent<LineRenderer>();
Shader shader = Shader.Find("Universal Render Pipeline/2D/Sprite-Lit-Default");
if (shader == null) shader = Shader.Find("Sprites/Default");
return new Material(shader);
}
private void ApplyPixelStyle()
private CableMeshRenderer.Band[] BuildBands()
{
if (!usePixelStyle || _line == null) return;
ApplyLineStyle(_line, pixelDark, 1f, _defaultSortingOrder - 3);
ApplyLineStyle(_bodyLine, pixelBody, 0.72f, _defaultSortingOrder - 2);
ApplyLineStyle(_midLine, pixelMid, 0.32f, _defaultSortingOrder - 1);
ApplyLineStyle(_highlightLine, pixelHighlight, 0.14f, _defaultSortingOrder);
}
private void ApplyLineStyle(LineRenderer renderer, Color color, float widthScale, int sortingOrder)
{
if (renderer == null || _line == null) return;
renderer.useWorldSpace = true;
renderer.material = _line.material;
renderer.widthMultiplier = _baseWidthMultiplier * widthScale;
renderer.widthCurve = _baseWidthCurve;
renderer.colorGradient = SolidGradient(color);
renderer.numCornerVertices = 0;
renderer.numCapVertices = 0;
renderer.alignment = _line.alignment;
renderer.textureMode = _line.textureMode;
renderer.sortingLayerName = _defaultSortingLayer;
renderer.sortingOrder = sortingOrder;
}
private static Gradient SolidGradient(Color color)
{
var gradient = new Gradient();
gradient.SetKeys(
new[] { new GradientColorKey(color, 0f), new GradientColorKey(color, 1f) },
new[] { new GradientAlphaKey(color.a, 0f), new GradientAlphaKey(color.a, 1f) });
return gradient;
// 提交顺序即画序(后画的盖前画的);三档色阶,比原型少一档高光,贴合场景像素密度
return new[]
{
new CableMeshRenderer.Band(shadowColor, shadowWidth, -shadowDrop, skipInTail: true),
new CableMeshRenderer.Band(darkColor, darkWidth, 0f),
new CableMeshRenderer.Band(bodyColor, bodyWidth, 0f),
new CableMeshRenderer.Band(midColor, midWidth, midLift),
};
}
private void SetCableRenderersEnabled(bool enabled)
{
_line.enabled = enabled;
SetPixelLinesEnabled(enabled && usePixelStyle);
}
private void SetPixelLinesEnabled(bool enabled)
{
if (_bodyLine != null) _bodyLine.enabled = enabled;
if (_midLine != null) _midLine.enabled = enabled;
if (_highlightLine != null) _highlightLine.enabled = enabled;
}
private void SetPixelLinePositionCount(int count)
{
if (_bodyLine != null) _bodyLine.positionCount = count;
if (_midLine != null) _midLine.positionCount = count;
if (_highlightLine != null) _highlightLine.positionCount = count;
}
private void RenderPixelBands(Vector3[] src, int count)
{
if (!usePixelStyle) return;
SetPixelLinePositionCount(count);
for (int i = 0; i < count; i++)
{
if (_bodyLine != null) _bodyLine.SetPosition(i, src[i]);
if (_midLine != null) _midLine.SetPosition(i, src[i]);
if (_highlightLine != null) _highlightLine.SetPosition(i, src[i]);
}
EnsureMeshRenderer();
_cableMesh.SetVisible(enabled);
}
/// <summary>