From 4ded2b8eab232a3f059131f10232b918361c2eb9 Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Thu, 9 Jul 2026 12:54:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(cable):=20=E7=BA=BF=E7=BC=86=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20Mesh=20=E6=B8=B2=E6=9F=93=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=8F=92=E6=8E=A5=E7=89=A9=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../Scripts/FixSystemNew/Cable/PhysicCable.cs | 257 +++++++++--------- 1 file changed, 135 insertions(+), 122 deletions(-) diff --git a/Assets/Scripts/FixSystemNew/Cable/PhysicCable.cs b/Assets/Scripts/FixSystemNew/Cable/PhysicCable.cs index cdaf7b13f..012169191 100644 --- a/Assets/Scripts/FixSystemNew/Cable/PhysicCable.cs +++ b/Assets/Scripts/FixSystemNew/Cable/PhysicCable.cs @@ -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(); - _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); } /// 闲置下垂状态;resetShape 时把绳重置为从出线口自然下垂。 @@ -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(); - 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); + } + + /// + /// 自遮挡检测(移植自原型):找到下垂最低点 j,若 j 之后的尾段与 j 之前的头段 + /// 空间重叠,返回尾段在平滑点数组里的起始下标(从 j 前两个物理点起),否则 -1。 + /// + 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(); + if (_cableMesh == null) _cableMesh = child.gameObject.AddComponent(); + + _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(); + if (reference != null && reference.sharedMaterial != null) + { + return reference.sharedMaterial; + } } - var child = new GameObject(objectName); - child.transform.SetParent(transform, false); - return child.AddComponent(); + 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); } ///