using UnityEngine; using UnityEngine.Serialization; /// /// 统一物理线缆(移植自 Assets/Prototype/2D线缆物理交互 原型): /// 固定粒子数 + 可变总长度的 Verlet 绳,一条线覆盖旧 Cable(拖拽)/ PhysicCable(闲置)两套状态。 /// 拖拽时按需送线、松手自动回收到最短长度、插入插孔时末端钉住。 /// [RequireComponent(typeof(LineRenderer))] public class PhysicCable : MonoBehaviour { public enum CableState { Hidden, // 收起,不渲染不模拟 Free, // 闲置下垂,自动回收到最短长度 Dragging, // 末端钉在拖拽目标上,按需送线 Plugged, // 末端钉在插孔上 Collapsing // 强制收起:纯运动学插值收拢到出线口,不走拖拽物理 } [FormerlySerializedAs("StartTranform")] public Transform startTransform; // 出线口 [Header("绳体")] [SerializeField] private int pointCount = 40; [SerializeField] private float minLength = 2f; // 闲置时的自然下垂长度 [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.92f; [SerializeField, Range(0.8f, 1f)] private float dragDamping = 0.86f; [SerializeField] private int constraintIterations = 24; [SerializeField, Range(0f, 1f)] private float bendSmooth = 0.45f; // 弯曲刚度(中点平滑) [SerializeField] private float endGravityBoost = 1.35f; // 末端几个点的重力加成,让插头端垂坠 [SerializeField] private float releaseGravityKick = 0.12f; // 松手时立即赋予末端向下速度,强化插头重量感 [Header("收放")] [SerializeField] private float feedRate = 0.45f; // 送线速度 [SerializeField] private float holdRate = 0.14f; // 拖拽/插接中回收速度 [SerializeField] private float retractRate = 0.09f; // 松手后回收速度 [SerializeField] private float lengthMargin = 0.25f; // 需求长度余量 [SerializeField, Range(0f, 1f)] private float dragFollowRate = 0.55f; // 拖拽目标跟随率 [Header("出线口保护段")] [SerializeField] private float stubMinOffset = 0.12f; // 出线口处沿出线方向至少伸出的距离 [Header("插头朝向")] [SerializeField, Range(2, 12)] private int plugTangentSampleCount = 7; [Header("插拔手感")] [SerializeField] private float plugSnapRate = 0.55f; [SerializeField] private float insertImpactDuration = 0.22f; [SerializeField] private float insertShoveDistance = 0.18f; [SerializeField] private float insertShakeAmplitude = 0.025f; [SerializeField] private float insertShakeFrequency = 34f; [SerializeField] private float insertTailKick = 0.05f; [SerializeField] private float unplugBreakDistance = 0.55f; [SerializeField] private float unplugStickExponent = 2.4f; [SerializeField] private float unplugReleaseKick = 0.07f; [Header("可选")] [SerializeField] private Transform reelCenter; // 出线方向参考中心,空则用 startTransform.parent [SerializeField] private Transform floorLimit; // 地面高度参考,空则不启用地面碰撞 [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; // 7/7 PixelCableLine step 色阶: Edge → Band Dark → Band Light // 暗面轻微偏红、亮面轻微偏黄;反光保持克制,避免线缆体积感过强。 [SerializeField] private Color darkColor = new(0.02f, 0.05f, 0.09f, 1f); // Edge Outline,微偏红 [SerializeField] private float darkWidth = 0.33f; // 原型:w+4 [SerializeField] private Color bodyColor = new(0.035f, 0.135f, 0.19f, 1f); // Band Dark [SerializeField] private float bodyWidth = 0.26f; // 原型:w=15px ≈ 0.26wu [SerializeField] private Color midColor = new(0.095f, 0.255f, 0.275f, 1f); // Band Light,微偏黄 [SerializeField] private float midWidth = 0.10f; // 收窄中亮面,减弱体积感 [SerializeField] private float midLift = 0.028f; [SerializeField] private Color highlightColor = new(0.18f, 0.32f, 0.30f, 0.75f); // 高光透明度降低 25% [SerializeField] private float highlightWidth = 0.04f; [SerializeField] private float highlightLift = 0.05f; [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; public Vector3 EndPosition => _pts != null ? _pts[_n - 1].pos : (startTransform != null ? startTransform.position : transform.position); public float CurrentLength => _length; /// /// 0=完全松弛,1=绷直。已扣除送线故意留的余量(1.04×直线距 + lengthMargin), /// 避免「有设计余量就永远不转」;线盘只在真正被拽时才应转动。 /// public float GetTautness(float extraSlackMargin = 0.2f) { if (State != CableState.Dragging && State != CableState.Plugged) return 0f; if (_pts == null || startTransform == null) return 0f; float straightDist = Vector3.Distance(startTransform.position, FlattenZ(EndPosition)); float baselineLength = straightDist * 1.04f + lengthMargin; float extraSlack = _length - baselineLength; if (extraSlack >= extraSlackMargin) return 0f; if (extraSlack <= 0f) return 1f; return 1f - extraSlack / extraSlackMargin; } private struct Particle { public Vector3 pos; public Vector3 prev; } private Particle[] _pts; private int _n; private float _length; private Vector3 _pointer; // 拖拽指针(世界坐标,驱动送线长度) private Vector3 _dragTarget; // 末端钉住目标(重 lerp + 长度钳制,消除拽满时的抖动) private Transform _dock; // 当前插入的插孔 private Vector3 _plugSnapEnd; // 插接吸附目标:渐进收敛到孔心后末端硬钉,保证完全对齐堵住孔 private float _plugAngle = -Mathf.PI / 2f; // 插头朝向(弧度,初始朝下) private bool _unplugResisting; private Vector3 _unplugAnchor; private float _insertImpactTimer; private Vector3 _insertImpactDir; private Vector3 _insertImpactNormal; // 收拢(Collapsing)状态:沿当前线径从尾端逐段吸回出线口 private Vector3[] _collapseStart; private float[] _collapseDistances; private float _collapsePathLength; private float _collapseDuration; private float _collapseElapsed; private LineRenderer _line; // 遗留组件,仅保持禁用(旧场景/CableSystem 仍会引用) private CableMeshRenderer _cableMesh; private Vector3[] _renderA; private Vector3[] _renderB; private const int SmoothIterations = 2; // Chaikin 切角细分次数 private bool _initialized; private void Awake() { _line = GetComponent(); if (_line != null) _line.enabled = false; EnsureMeshRenderer(); } // ------------------------------ 状态 API ------------------------------ public void Hide() { State = CableState.Hidden; _dock = null; _unplugResisting = false; _insertImpactTimer = 0f; SetCableRenderersEnabled(false); } /// 闲置下垂状态;resetShape 时把绳重置为从出线口自然下垂。 public void ShowFree(bool resetShape = false) { EnsureInit(); if (resetShape) ResetHangingShape(); _dock = null; _unplugResisting = false; State = CableState.Free; SetCableRenderersEnabled(true); } public void BeginDrag(Vector3 pointerWorld) { EnsureInit(); if (State == CableState.Hidden) ResetHangingShape(); SyncPlugAngleToTail(); _dock = null; _pointer = FlattenZ(pointerWorld); _dragTarget = _pts[_n - 1].pos; _unplugResisting = false; _insertImpactTimer = 0f; State = CableState.Dragging; SetCableRenderersEnabled(true); } public void SetPointer(Vector3 pointerWorld) { _pointer = ApplyUnplugResistance(FlattenZ(pointerWorld)); } public void EndDrag() { if (State == CableState.Dragging) { State = CableState.Free; _unplugResisting = false; ApplyReleaseGravity(); _plugAngle = -Mathf.PI / 2f; } } public void StartUnplugResistance(Transform socket) { if (socket == null || _pts == null) return; _unplugAnchor = FlattenZ(socket.position); _pointer = _unplugAnchor; _dragTarget = _unplugAnchor; _pts[_n - 1].pos = _unplugAnchor; _pts[_n - 1].prev = _unplugAnchor; _unplugResisting = unplugBreakDistance > 0.01f; } /// 末端钉到插孔;snapStraight 用于读档,把绳直接摆成出线口到插孔的直线。 public void PlugInto(Transform dock, bool snapStraight = false) { EnsureInit(); _dock = dock; _unplugResisting = false; State = CableState.Plugged; SetCableRenderersEnabled(true); if (snapStraight && dock != null) { ResetStraightTo(FlattenZ(dock.position)); } // 吸附目标从当前末端出发,向孔心渐进收敛(保留吸入动画,最终严格钉在孔心) _plugSnapEnd = _pts[_n - 1].pos; if (snapStraight) { _insertImpactTimer = 0f; } else { BeginInsertImpact(dock); } } /// /// 强制收起:不走拖拽物理,保留当前曲线路径并从尾端逐段缩短到 target(出线口)。 /// 用于指令强制收起等需要在极短、精确可控时长内完成的场景,避免拖拽跟随参数(为秒级手动拖拽标定) /// 在几帧内来不及收敛而产生的甩鞭/抖动。 /// public void CollapseTo(Vector3 target, float duration) { EnsureInit(); _collapseStart ??= new Vector3[_n]; _collapseDistances ??= new float[_n]; _collapsePathLength = 0f; _collapseStart[0] = FlattenZ(target); _collapseDistances[0] = 0f; for (int i = 1; i < _n; i++) { _collapseStart[i] = _pts[i].pos; _collapsePathLength += Vector3.Distance(_collapseStart[i - 1], _collapseStart[i]); _collapseDistances[i] = _collapsePathLength; } _collapseDuration = Mathf.Max(0.0001f, duration); _collapseElapsed = 0f; _dock = null; _unplugResisting = false; State = CableState.Collapsing; SetCableRenderersEnabled(true); } // ------------------------------ 插头姿态 ------------------------------ /// 把插头摆到绳末端并按绳向旋转(角度经过平滑与限速)。 public void ApplyPlugPose(Transform plug, Transform plugRoot) { if (_pts == null || plug == null) return; plug.rotation = Quaternion.Euler(0, 0, _plugAngle * Mathf.Rad2Deg - 90f); Vector3 end = _pts[_n - 1].pos; if (plugRoot != null) { Vector3 delta = end - plugRoot.position; delta.z = 0; plug.position += delta; } else { plug.position = new Vector3(end.x, end.y, plug.position.z); } } // ------------------------------ 模拟 ------------------------------ private void FixedUpdate() { 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(); } private void EnsureInit() { if (_initialized) return; _initialized = true; EnsureMeshRenderer(); _n = Mathf.Max(8, pointCount); _pts = new Particle[_n]; int renderCount = _n << SmoothIterations; _renderA = new Vector3[renderCount]; _renderB = new Vector3[renderCount]; ResetHangingShape(); } private void ResetHangingShape() { Vector3 anchor = startTransform.position; float idleLength = GetIdleLength(); for (int i = 0; i < _n; i++) { float t = i / (float)(_n - 1); Vector3 p = anchor + Vector3.down * (t * idleLength) + Vector3.right * (t * 0.02f); _pts[i].pos = p; _pts[i].prev = p; } _length = idleLength; _plugAngle = -Mathf.PI / 2f; } private void ResetStraightTo(Vector3 end) { Vector3 anchor = startTransform.position; float idleLength = GetIdleLength(); for (int i = 0; i < _n; i++) { float t = i / (float)(_n - 1); Vector3 p = Vector3.Lerp(anchor, end, t); _pts[i].pos = p; _pts[i].prev = p; } _length = Mathf.Max(idleLength, Vector3.Distance(anchor, end) * 1.04f + lengthMargin); SyncPlugAngleToTail(); } private void Simulate(float dt) { if (State == CableState.Collapsing) { SimulateCollapse(dt); return; } Vector3 anchor = startTransform.position; Vector3 outletDir = GetOutletDirection(anchor); float idleLength = GetIdleLength(); bool dragging = State == CableState.Dragging; bool plugged = State == CableState.Plugged && _dock != null; bool pinnedEnd = dragging || plugged; Vector3 dock = plugged ? FlattenZ(_dock.position) : Vector3.zero; float lMax = minLength * maxStretch; if (plugged) { // 保证够得着插孔 lMax = Mathf.Max(lMax, Vector3.Distance(dock, anchor) * 1.12f); } // --- 拖拽目标:重 lerp + 按当前绳长钳制(拽满时目标跟着绳长走,消除抖动) Vector3 dragPointer = _pointer; if (dragging) { _dragTarget += (dragPointer - _dragTarget) * dragFollowRate; Vector3 offset = _dragTarget - anchor; float dist = offset.magnitude; float maxRadius = Mathf.Min(lMax, _length) * 0.99f; if (dist > maxRadius) { _dragTarget = anchor + offset / dist * maxRadius; } } // --- 送线 / 回收(长度由原始指针驱动,而非被钳制后的目标) Vector3 endTarget = dragging ? dragPointer : (plugged ? dock : _pts[_n - 1].pos); float needed = Vector3.Distance(endTarget, anchor) * 1.04f + lengthMargin; float targetLength = pinnedEnd ? Mathf.Clamp(needed, idleLength, lMax) : idleLength; float rate = targetLength > _length ? feedRate : (pinnedEnd ? holdRate : retractRate); _length += (targetLength - _length) * rate; float seg = _length / (_n - 1); float g = gravity * dt * dt; float velocityDamping = dragging ? dragDamping : damping; // --- Verlet 积分 for (int i = 1; i < _n; i++) { ref Particle p = ref _pts[i]; Vector3 v = (p.pos - p.prev) * velocityDamping; p.prev = p.pos; float boost = i > _n - 4 ? endGravityBoost : 1f; p.pos += v + Vector3.down * (g * boost); } _pts[0].pos = anchor; _pts[0].prev = anchor; if (dragging) { _pts[_n - 1].pos = _dragTarget; } else if (plugged) { // 吸附目标渐进收敛到孔心后,末端硬钉——软拉会被重力/张力持续拽偏,堵不严孔 Vector3 dockTarget = GetImpactDockTarget(dock, dt); _plugSnapEnd += (dockTarget - _plugSnapEnd) * plugSnapRate; if ((_plugSnapEnd - dock).sqrMagnitude < 1e-4f) _plugSnapEnd = dock; _pts[_n - 1].pos = _plugSnapEnd; _pts[_n - 1].prev = _plugSnapEnd; } // --- 约束迭代 for (int k = 0; k < constraintIterations; k++) { for (int i = 0; i < _n - 1; i++) { ref Particle a = ref _pts[i]; ref Particle b = ref _pts[i + 1]; Vector3 d = b.pos - a.pos; float dist = d.magnitude; if (dist < 1e-6f) dist = 1e-6f; float diff = (dist - seg) / dist; bool pinA = i == 0; bool pinB = i == _n - 2 && pinnedEnd; float wa = pinA ? 0f : (pinB ? 1f : 0.5f); float wb = pinB ? 0f : (pinA ? 1f : 0.5f); a.pos += d * (diff * wa); b.pos -= d * (diff * wb); } // 出线口保护段:靠近出线口的两个点顺着出线方向、横向收紧, // 无论绳的其余部分被拽到哪,出线处都不出现折角或自穿插 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) { for (int i = 1; i < _n - 1; i++) { if (i == _n - 2 && pinnedEnd) continue; Vector3 mid = (_pts[i - 1].pos + _pts[i + 1].pos) * 0.5f; Vector3 correction = (mid - _pts[i].pos) * (bendSmooth * 0.5f); _pts[i].pos += correction; // 约束校正不应转化为下一帧速度,否则线缆会产生橡皮筋式回弹。 _pts[i].prev += correction; } } } // --- 地面(被钉住的拖拽末端豁免,避免钉点与地面互相拉扯) if (floorLimit != null) { float floorY = floorLimit.position.y; for (int i = 1; i < _n; i++) { if (i == _n - 1 && dragging) continue; if (_pts[i].pos.y < floorY) { _pts[i].pos.y = floorY; _pts[i].prev.x += (_pts[i].pos.x - _pts[i].prev.x) * 0.5f; // 摩擦 } } } UpdatePlugAngle(plugged); } /// /// Collapsing 状态沿收线前的曲线逐段截短。线身形状不做整体缩放, /// 只有尾端沿原路径倒退,形成被线盘吸入的观感。 /// private void SimulateCollapse(float dt) { _collapseElapsed += dt; float t = Mathf.Clamp01(_collapseElapsed / _collapseDuration); float eased = t * t * (3f - 2f * t); float remainingLength = _collapsePathLength * (1f - eased); int segment = 1; for (int i = 0; i < _n; i++) { float distance = remainingLength * (i / (float)(_n - 1)); while (segment < _n - 1 && _collapseDistances[segment] < distance) { segment++; } int previous = segment - 1; float segmentLength = _collapseDistances[segment] - _collapseDistances[previous]; float segmentT = segmentLength > 1e-5f ? (distance - _collapseDistances[previous]) / segmentLength : 0f; Vector3 pos = Vector3.Lerp(_collapseStart[previous], _collapseStart[segment], segmentT); _pts[i].pos = pos; _pts[i].prev = pos; } _length = remainingLength; SyncPlugAngleToTail(); } private Vector3 ApplyUnplugResistance(Vector3 rawPointer) { if (!_unplugResisting) return rawPointer; Vector3 pull = rawPointer - _unplugAnchor; pull.z = 0; float dist = pull.magnitude; if (dist >= unplugBreakDistance) { _unplugResisting = false; if (dist > 1e-4f) { KickTail(pull / dist * unplugReleaseKick); } return rawPointer; } float t = Mathf.Clamp01(dist / Mathf.Max(0.001f, unplugBreakDistance)); float eased = Mathf.Pow(t, Mathf.Max(1f, unplugStickExponent)); return Vector3.Lerp(_unplugAnchor, rawPointer, eased); } private void BeginInsertImpact(Transform dock) { if (dock == null || IsInsertImpactDisabled()) return; Vector3 dockPos = FlattenZ(dock.position); Vector3 dir = dockPos - _pts[_n - 1].pos; if (dir.sqrMagnitude < 1e-4f) { dir = _pts[_n - 1].pos - _pts[Mathf.Max(0, _n - 3)].pos; } dir.z = 0; if (dir.sqrMagnitude < 1e-4f) dir = Vector3.right; _insertImpactDir = dir.normalized; _insertImpactNormal = new Vector3(-_insertImpactDir.y, _insertImpactDir.x, 0f); _insertImpactTimer = Mathf.Max(0f, insertImpactDuration); KickTail(_insertImpactDir * insertTailKick); } private bool IsInsertImpactDisabled() { return insertImpactDuration <= 0f && insertTailKick <= 0f && insertShoveDistance <= 0f && insertShakeAmplitude <= 0f; } private Vector3 GetImpactDockTarget(Vector3 dock, float dt) { if (_insertImpactTimer <= 0f) return dock; _insertImpactTimer = Mathf.Max(0f, _insertImpactTimer - dt); float life = Mathf.Clamp01(_insertImpactTimer / Mathf.Max(0.001f, insertImpactDuration)); float elapsed = insertImpactDuration - _insertImpactTimer; float shove = insertShoveDistance * life * life; float shake = Mathf.Sin(elapsed * insertShakeFrequency) * insertShakeAmplitude * life; return dock + _insertImpactDir * shove + _insertImpactNormal * shake; } private void KickTail(Vector3 impulse) { if (_pts == null) return; int start = Mathf.Max(1, _n - 5); for (int i = start; i < _n; i++) { float t = (i - start + 1f) / (_n - start + 1f); _pts[i].prev -= impulse * t; } } private void ApplyReleaseGravity() { if (_pts == null || releaseGravityKick <= 0f) return; int start = Mathf.Max(1, _n - 5); for (int i = start; i < _n; i++) { float weight = (i - start + 1f) / (_n - start + 1f); Vector3 horizontalVelocity = _pts[i].pos - _pts[i].prev; horizontalVelocity.y = 0f; horizontalVelocity.z = 0f; _pts[i].prev = _pts[i].pos - horizontalVelocity + Vector3.up * (releaseGravityKick * weight); } } private void ApplyOutletStub(Vector3 anchor, Vector3 dir) { // P1:横向收紧 0.6,且沿出线方向至少伸出 stubMinOffset Vector3 rel = _pts[1].pos - anchor; float along = Vector3.Dot(rel, dir); Vector3 lateral = (rel - dir * along) * 0.4f; if (along < stubMinOffset) along = stubMinOffset; _pts[1].pos = anchor + dir * along + lateral; // P2:横向收紧 0.2 rel = _pts[2].pos - anchor; along = Vector3.Dot(rel, dir); lateral = (rel - dir * along) * 0.8f; _pts[2].pos = anchor + dir * along + lateral; } private void UpdatePlugAngle(bool plugged) { // 插着时插头精灵隐藏,保持角度即可;吸附只影响位置,不直接驱动朝向。 if (plugged) return; if (State == CableState.Free) { _plugAngle = -Mathf.PI / 2f; return; } if (!TryGetStablePlugTangent(out Vector3 tangent)) return; _plugAngle = Mathf.Atan2(tangent.y, tangent.x); } private bool TryGetStablePlugTangent(out Vector3 tangent) { tangent = Vector3.zero; if (_pts == null || _n < 2) return false; int tailIndex = _n - 1; int sampleCount = Mathf.Clamp(plugTangentSampleCount, 2, _n); int baseIndex = tailIndex - (sampleCount - 1); tangent = _pts[tailIndex].pos - _pts[baseIndex].pos; tangent.z = 0f; // 尾段折叠得过短时方向不可靠,保持上一帧角度,避免 180° 翻转。 float segmentLength = _length / Mathf.Max(1, _n - 1); float minTangentLength = Mathf.Max(0.02f, segmentLength * 1.5f); if (tangent.sqrMagnitude < minTangentLength * minTangentLength) { tangent = Vector3.zero; return false; } tangent.Normalize(); return true; } private void SyncPlugAngleToTail() { if (TryGetStablePlugTangent(out Vector3 tangent)) { _plugAngle = Mathf.Atan2(tangent.y, tangent.x); } } private Vector3 GetOutletDirection(Vector3 anchor) { Transform center = reelCenter != null ? reelCenter : startTransform.parent; if (center != null) { Vector3 radial = anchor - center.position; radial.z = 0; if (radial.sqrMagnitude > 1e-6f) { return radial.normalized; } } return Vector3.down; } private Vector3 FlattenZ(Vector3 worldPos) { worldPos.z = startTransform != null ? startTransform.position.z : transform.position.z; return worldPos; } // ------------------------------ 渲染 ------------------------------ private void Render() { for (int i = 0; i < _n; i++) { _renderA[i] = _pts[i].pos; } int count = _n; Vector3[] src = _renderA; Vector3[] dst = _renderB; for (int it = 0; it < SmoothIterations; it++) { count = Chaikin(src, count, dst); (src, dst) = (dst, src); } // 插接时线缆末端由平头切面改为圆头端帽(线自身的圆头,同旧 LineRenderer),读作"插进孔里" var panelLight = AibisDream.FixSystem.FixPanelSystem.Instance?.PanelLight; _cableMesh.SetEnvironmentTint(panelLight != null ? panelLight.CurrentEnvironmentTint : Color.white); _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++) { if (_pts[i].pos.y < _pts[j].pos.y) j = i; // 原型 y 向下,这里取世界最低点 } 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() { return Mathf.Max(0.1f, minLength * idleLengthScale); } private void EnsureMeshRenderer() { if (_cableMesh != null) return; // 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 Material ResolveMaterial() { if (cableMaterial != null) return cableMaterial; // 直接共用出线口所在线盘 sprite 的材质:与周围环境的受光行为保证完全一致 if (startTransform != null) { var reference = startTransform.GetComponentInParent(); if (reference != null && reference.sharedMaterial != null) { return reference.sharedMaterial; } } Shader shader = Shader.Find("Universal Render Pipeline/2D/Sprite-Lit-Default"); if (shader == null) shader = Shader.Find("Sprites/Default"); return new Material(shader); } private CableMeshRenderer.Band[] BuildBands() { // 提交顺序即画序(后画的盖前画的);硬边四档色阶保留平滑轮廓,只强化高清像素感 return new[] { new CableMeshRenderer.Band(darkColor, darkWidth, 0f), new CableMeshRenderer.Band(bodyColor, bodyWidth, 0f), new CableMeshRenderer.Band(midColor, midWidth, midLift), new CableMeshRenderer.Band(highlightColor, highlightWidth, highlightLift), }; } private void SetCableRenderersEnabled(bool enabled) { EnsureMeshRenderer(); _cableMesh.SetVisible(enabled); } /// /// 一次 Chaikin 切角细分:保留首尾点,每段取 1/4、3/4 两个切分点,输出点数为 2n。 /// private static int Chaikin(Vector3[] src, int count, Vector3[] dst) { int idx = 0; dst[idx++] = src[0]; for (int i = 0; i < count - 1; i++) { dst[idx++] = Vector3.Lerp(src[i], src[i + 1], 0.25f); dst[idx++] = Vector3.Lerp(src[i], src[i + 1], 0.75f); } dst[idx++] = src[count - 1]; return idx; } }