using System; using UnityEngine; namespace AibisDream.FixSystem { [RequireComponent(typeof(LineRenderer))] public class Cable : MonoBehaviour { private Transform _start; private Transform _end; private LineRenderer _lineRenderer; private LineRenderer _stiffLineRenderer; // 用于渲染刚性部分 private Vector3[] _points; // 渲染平滑:物理点数少,直接连线转折生硬,渲染前做切角细分 private Vector3[] _smoothBufferA; private Vector3[] _smoothBufferB; // 配置兜底后的生效值(旧场景/预制体上新字段是 0 时取默认) private float _headStiffLength; private float _headRenderBlend; private int _tailStiffCount; private float _tailStiffLength; private float _tailRenderBlend; private int _smoothIterations; private CablePanel _cablePanel; private RendererSortingState _defaultLineSorting; private RendererSortingState _defaultStiffLineSorting; [SerializeField] private CableConfig config = CableConfig.GeneDefaultConfig(); public void ShowCable() { _lineRenderer.enabled = true; _stiffLineRenderer.enabled = true; } public void HideCable() { _lineRenderer.enabled = false; _stiffLineRenderer.enabled = false; } private void Awake() { ResolveConfig(); _lineRenderer = GetComponent(); // 每次 Chaikin 细分点数翻倍(保留首尾 + 每段两个切分点) int flexCount = config.resolution - config.stiffCount; int smoothedCount = flexCount << _smoothIterations; _lineRenderer.positionCount = smoothedCount; _smoothBufferA = new Vector3[smoothedCount]; _smoothBufferB = new Vector3[smoothedCount]; // 创建第二个LineRenderer用于刚性部分 GameObject stiffObj = new GameObject("StiffLineRenderer"); stiffObj.transform.SetParent(transform); _stiffLineRenderer = stiffObj.AddComponent(); _stiffLineRenderer.positionCount = config.stiffCount; } /// /// 计算生效配置:新加的字段在旧序列化数据里是 0,取兜底默认;并做范围保护。 /// private void ResolveConfig() { _headStiffLength = config.headStiffLength <= 0f ? (config.stiffLength > 0f ? config.stiffLength : 0.055f) : config.headStiffLength; _headRenderBlend = config.headRenderBlend <= 0f ? 0.55f : Mathf.Clamp01(config.headRenderBlend); _tailStiffCount = config.tailStiffCount < 2 ? 2 : config.tailStiffCount; // 至少给物理留 2 个自由点 _tailStiffCount = Mathf.Min(_tailStiffCount, config.resolution - config.stiffCount - 2); _tailStiffCount = Mathf.Max(_tailStiffCount, 2); _tailStiffLength = config.tailStiffLength <= 0f ? 0.024f : config.tailStiffLength; _tailRenderBlend = config.tailRenderBlend <= 0f ? 0.45f : Mathf.Clamp01(config.tailRenderBlend); _smoothIterations = config.smoothIterations <= 0 ? 2 : Mathf.Min(config.smoothIterations, 4); } private void Start() { InitComponentRefs(); InitPoints(); } private void InitComponentRefs() { _cablePanel = transform.GetComponentInParent(); // 复制第一个LineRenderer的属性到第二个 _stiffLineRenderer.startWidth = _lineRenderer.startWidth; _stiffLineRenderer.endWidth = _lineRenderer.startWidth; _stiffLineRenderer.material = _lineRenderer.material; _stiffLineRenderer.startColor = _lineRenderer.startColor; _stiffLineRenderer.endColor = _lineRenderer.startColor; _stiffLineRenderer.numCornerVertices = _lineRenderer.numCornerVertices; _stiffLineRenderer.alignment = _lineRenderer.alignment; _stiffLineRenderer.textureMode = _lineRenderer.textureMode; _stiffLineRenderer.sortingLayerName = _cablePanel.CableReelRef.GetComponent().sortingLayerName; _stiffLineRenderer.sortingOrder = _cablePanel.CableReelRef.GetComponent().sortingOrder - 1; _defaultLineSorting = RendererSortingState.From(_lineRenderer); _defaultStiffLineSorting = RendererSortingState.From(_stiffLineRenderer); // 两端端帽分开处理: // 刚性段贴着出线口,用方口端帽(0)保证切面与出线口齐平不凸出; // 柔性段末端在插头/插孔处,用圆头端帽包进贴图,拖拽转向时不露缝。 _stiffLineRenderer.numCapVertices = 0; if (_lineRenderer.numCapVertices < 1) { _lineRenderer.numCapVertices = 6; } // 连线起止点(末端用 PlugRoot,避免连到插头精灵中心) _start = _cablePanel.CableRootPos; _end = _cablePanel.PlugRef.PlugRoot; } private void InitPoints() { _points = new Vector3[config.resolution]; for (int i = 0; i < config.resolution; i++) { float t = i / (float)(config.resolution - 1); _points[i] = Vector3.Lerp(_start.position, _end.position, t); } } private void FixedUpdate() { DrawLine(); } void DrawLine() { var points = UpdateRope(); Vector3 zOffset = Vector3.forward * -.3f; // 刚性段走向(出线口 -> 接缝点)与接缝方向(接缝点 -> 下一个柔性点) Vector3 stiffDir = (points[config.stiffCount - 1] - points[0]).normalized; Vector3 seamOffset = points[config.stiffCount + 1] - points[config.stiffCount]; Vector3 seamDir = seamOffset.sqrMagnitude > 1e-6f ? seamOffset.normalized : stiffDir; float halfWidth = _lineRenderer.startWidth * 0.5f; // 渲染刚性部分 for (int i = 0; i < config.stiffCount; i++) { _stiffLineRenderer.SetPosition(i, points[i] + zOffset); } // 刚性段末点沿接缝方向多画半个线宽,垫在柔性段圆头下方,避免接缝两侧出现缺口。 // 仅在接缝方向大致顺延刚性段时外延;线松弛折返时跳过, // 否则条带在折角处被斜切,会闪现细长尖刺。 if (Vector3.Dot(stiffDir, seamDir) > 0.2f) { _stiffLineRenderer.SetPosition(config.stiffCount - 1, points[config.stiffCount - 1] + seamDir * halfWidth + zOffset); } // 拷贝柔性段准备渲染 int flexCount = config.resolution - config.stiffCount; for (int i = 0; i < flexCount; i++) { _smoothBufferA[i] = points[i + config.stiffCount] + zOffset; } // 柔性段首点内缩半个线宽:圆头端帽顶点正好落在接缝处, // 不会越过接缝、盖到出线口贴图上(柔性段排序在出线口贴图之上)。 // 内缩量不超过首段长度的一半,防止首段退化成零长度产生扭折。 float insetLimit = Mathf.Min(halfWidth, seamOffset.magnitude * 0.5f); _smoothBufferA[0] += seamDir * insetLimit; if (TryGetHeadDir(out Vector3 headRenderDir)) { BlendHeadRenderControls(flexCount, headRenderDir); } if (TryGetTailDir(out Vector3 tailRenderDir)) { BlendTailRenderControls(flexCount, tailRenderDir); } // Chaikin 切角细分:物理点直接连线在转折处(尤其插头端刚性段与曲线的交界) // 过于生硬,细分把折角切圆;首尾端点保留,两端的进出方向不变。 Vector3[] src = _smoothBufferA; Vector3[] dst = _smoothBufferB; int count = flexCount; for (int it = 0; it < _smoothIterations; it++) { count = Chaikin(src, count, dst); (src, dst) = (dst, src); } for (int i = 0; i < count; i++) { _lineRenderer.SetPosition(i, src[i]); } } /// /// 一次 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; } private void BlendHeadRenderControls(int flexCount, Vector3 headDir) { if (flexCount < 2) return; Vector3 start = _smoothBufferA[0]; Vector3 next = _smoothBufferA[1]; float blendLength = Mathf.Min(_headStiffLength, Vector3.Distance(start, next)); Vector3 tangentTarget = start + headDir * blendLength; _smoothBufferA[1] = Vector3.Lerp(next, tangentTarget, _headRenderBlend); } private void BlendTailRenderControls(int flexCount, Vector3 tailDir) { int endIndex = flexCount - 1; int prevIndex = endIndex - 1; if (prevIndex < 0) return; Vector3 end = _smoothBufferA[endIndex]; Vector3 previous = _smoothBufferA[prevIndex]; Vector3 incoming = previous - end; if (incoming.sqrMagnitude < 1e-6f) return; float angleWeight = Mathf.InverseLerp(-0.1f, 0.7f, Vector3.Dot(incoming.normalized, tailDir)); float blendLength = Mathf.Min(_tailStiffLength, Vector3.Distance(end, previous)); Vector3 tangentTarget = end + tailDir * blendLength; _smoothBufferA[prevIndex] = Vector3.Lerp(previous, tangentTarget, _tailRenderBlend * angleWeight); } Vector3[] UpdateRope() { float t = Mathf.InverseLerp(config.dstMin, config.dstMax, (_start.position - _end.position).magnitude); float F = Mathf.Lerp(config.forceMin, config.forceMax, t); // 计算出线方向(从转盘出线口到起始点) Vector3 dir = (_start.position - _cablePanel.CableReelRef.transform.position).normalized; // 设置刚性部分 for (int i = 0; i < config.stiffCount; i++) { float t_stiff = (float)i / (config.stiffCount - 1); _points[i] = _start.position + dir * (_headStiffLength * t_stiff); } // 设置末端点 _points[^1] = _end.position; // 插头端刚性段:让线先顺着插头轴向(插头中心 -> 线缆接点)伸出一小段再弯曲, // 否则线会以任意角度横着搭在插头尾部,看起来没有"插进去"。 // 末端挂在 Socket 上时接点没有可靠的轴向,保持原有纯物理表现。 bool pinTail = TryGetTailDir(out Vector3 tailDir); int tailPinned = 1; if (pinTail) { tailPinned = _tailStiffCount; for (int j = 0; j < tailPinned; j++) { float tTail = (float)j / (tailPinned - 1); _points[_points.Length - 1 - j] = _end.position + tailDir * (_tailStiffLength * tTail); } } int simEndExclusive = _points.Length - tailPinned; // 只对非刚性部分进行物理模拟 for (int ik = 0; ik < config.k; ik++) { // 确保非刚性部分的第一个点与刚性部分的最后一个点重合 _points[config.stiffCount] = _points[config.stiffCount - 1]; for (int i = config.stiffCount + 1; i < simEndExclusive; i++) { Vector3 offsetPrev = _points[i - 1] - _points[i]; Vector3 offsetNext = _points[i + 1] - _points[i]; Vector3 velocity = offsetPrev.normalized * (offsetPrev.magnitude * F) + offsetNext.normalized * (offsetNext.magnitude * F); _points[i] += velocity * Time.deltaTime / config.k; } for (int i = config.stiffCount + 1; i < simEndExclusive; i++) { _points[i] += Vector3.down * (9.8f * Time.deltaTime) / config.k; } } if (pinTail) { RelaxTailApproach(tailPinned, tailDir); } return _points; } private bool TryGetHeadDir(out Vector3 headDir) { headDir = Vector3.zero; if (_start == null || _cablePanel == null || _cablePanel.CableReelRef == null) return false; Vector3 offset = _start.position - _cablePanel.CableReelRef.transform.position; if (offset.sqrMagnitude < 1e-6f) return false; headDir = offset.normalized; return true; } private void RelaxTailApproach(int tailPinned, Vector3 tailDir) { int firstTailIndex = _points.Length - tailPinned; int approachIndex = firstTailIndex - 1; if (approachIndex <= config.stiffCount) return; Vector3 target = _points[firstTailIndex] + tailDir * _tailStiffLength; _points[approachIndex] = Vector3.Lerp(_points[approachIndex], target, 0.22f); } /// /// 插头轴向:从插头中心指向线缆接点(PlugRoot)、并继续向外的方向。 /// 仅当末端连接点是 PlugRoot 时有效;连接到 Socket 时返回 false。 /// private bool TryGetTailDir(out Vector3 tailDir) { tailDir = Vector3.zero; if (_end == null || _end != _cablePanel.PlugRef.PlugRoot || _end.parent == null) return false; Vector3 offset = _end.position - _end.parent.position; if (offset.sqrMagnitude < 1e-6f) return false; tailDir = offset.normalized; return true; } /// /// 为线缆设置末端点 /// /// 末端点 public void SetEndPos(Transform endPos) { _end = endPos; } public void SetSorting(string sortingLayerName, int sortingOrder) { ApplySorting(_lineRenderer, sortingLayerName, sortingOrder); ApplySorting(_stiffLineRenderer, sortingLayerName, sortingOrder); } public void RestoreDefaultSorting() { _defaultLineSorting.ApplyTo(_lineRenderer); _defaultStiffLineSorting.ApplyTo(_stiffLineRenderer); } public Vector3 GetDirection() { return _points[^1] - _points[^2]; } private static void ApplySorting(LineRenderer lineRenderer, string sortingLayerName, int sortingOrder) { if (lineRenderer == null) return; lineRenderer.sortingLayerName = sortingLayerName; lineRenderer.sortingOrder = sortingOrder; } private readonly struct RendererSortingState { private readonly string _sortingLayerName; private readonly int _sortingOrder; private RendererSortingState(string sortingLayerName, int sortingOrder) { _sortingLayerName = sortingLayerName; _sortingOrder = sortingOrder; } public static RendererSortingState From(LineRenderer lineRenderer) { return new RendererSortingState(lineRenderer.sortingLayerName, lineRenderer.sortingOrder); } public void ApplyTo(LineRenderer lineRenderer) { if (lineRenderer == null) return; lineRenderer.sortingLayerName = _sortingLayerName; lineRenderer.sortingOrder = _sortingOrder; } } } [Serializable] public struct CableConfig { [Tooltip("物理采样点总数。越大曲线越顺滑,插头端/出线口端刚性点也从这里扣除")] public int resolution; [Tooltip("两端距离小于该值时张力取 forceMin(线最松弛)")] public float dstMin; [Tooltip("两端距离大于该值时张力取 forceMax(线完全绷直)。想让线平时带点垂坠感就调大它")] public float dstMax; [Tooltip("松弛时的张力。越小越软、下垂越明显")] public float forceMin; [Tooltip("绷紧时的张力。越大线越像直棍,调小会保留一点弧度")] public float forceMax; [Tooltip("每帧物理迭代次数,影响收敛稳定性,一般不用动")] public int k; [Tooltip("出线口端刚性点数:线离开出线口时保持笔直的采样点数量")] public int stiffCount; [Tooltip("出线口端刚性段长度(世界单位)")] public float stiffLength; [Tooltip("Outlet-side rigid section length. Zero uses the tuned default.")] public float headStiffLength; [Tooltip("Render-only blend for the first soft control point after the outlet-side rigid section.")] public float headRenderBlend; [Tooltip("插头端刚性点数(含末端点,最小 2)。0 = 用默认值 3")] public int tailStiffCount; [Tooltip("插头端刚性段长度:线沿插头轴向笔直伸出多远再开始弯。0 = 用 stiffLength 的 2 倍")] public float tailStiffLength; [Tooltip("Render-only blend for the first soft control point after the plug-side rigid section.")] public float tailRenderBlend; [Tooltip("渲染切角细分次数(1~4):越大转折越圆。0 = 用默认值 2")] public int smoothIterations; public static CableConfig GeneDefaultConfig() { return new CableConfig { resolution = 10, dstMin = 0.1f, dstMax = 1.0f, forceMin = 0.1f, forceMax = 150f, k = 10, stiffCount = 3, stiffLength = 0.1f, headStiffLength = 0f, headRenderBlend = 0f, tailStiffCount = 0, tailStiffLength = 0f, tailRenderBlend = 0f, smoothIterations = 0 }; } } }