diff --git a/Assets/Scripts/FixSystemNew/BodyModule/CableReel.cs b/Assets/Scripts/FixSystemNew/BodyModule/CableReel.cs
index dcb7845da..b333243fc 100644
--- a/Assets/Scripts/FixSystemNew/BodyModule/CableReel.cs
+++ b/Assets/Scripts/FixSystemNew/BodyModule/CableReel.cs
@@ -53,11 +53,26 @@ namespace AibisDream.FixSystem
UpdateOutletPosition();
}
+ /// 遗留兼容(旧 CableSystem 场景仍在调用)。
public void UpdateRotation()
{
- // 如果PhysicCable激活,不进行旋转
- if (cablePanel == null || cablePanel.PlugRef == null ||
- cablePanel.PhysicCableRef.GetComponent().enabled) return;
+ if (cablePanel == null || cablePanel.PlugRef == null) return;
+ UpdateRotation(true);
+ }
+
+ /// 拖拽/插接中朝拉力方向施加扭矩;闲置时缓动回正。
+ public void UpdateRotation(bool active)
+ {
+ if (cablePanel == null || cablePanel.PlugRef == null) return;
+
+ if (!active)
+ {
+ angularVelocity = 0f;
+ currentAngle = Mathf.LerpAngle(currentAngle, 0f, 4f * Time.deltaTime);
+ transform.rotation = Quaternion.Euler(0, 0, currentAngle);
+ UpdateOutletPosition();
+ return;
+ }
Vector2 center = transform.position;
diff --git a/Assets/Scripts/FixSystemNew/Cable/PhysicCable.cs b/Assets/Scripts/FixSystemNew/Cable/PhysicCable.cs
index 13684eda2..cdaf7b13f 100644
--- a/Assets/Scripts/FixSystemNew/Cable/PhysicCable.cs
+++ b/Assets/Scripts/FixSystemNew/Cable/PhysicCable.cs
@@ -1,104 +1,592 @@
-using UnityEngine;
+using UnityEngine;
using UnityEngine.Serialization;
-[RequireComponent(typeof(PhysicLineSegment))]
+///
+/// 统一物理线缆(移植自 Assets/Prototype/2D线缆物理交互 原型):
+/// 固定粒子数 + 可变总长度的 Verlet 绳,一条线覆盖旧 Cable(拖拽)/ PhysicCable(闲置)两套状态。
+/// 拖拽时按需送线、松手自动回收到最短长度、插入插孔时末端钉住。
+///
+[RequireComponent(typeof(LineRenderer))]
public class PhysicCable : MonoBehaviour
{
- [FormerlySerializedAs("StartTranform")] public Transform startTransform;
-
- public PhysicLineSegment physicLine;
-
- private Transform targetTransform;
- private bool isEnabled;
-
- // Start is called before the first frame update
- void Start()
+ public enum CableState
{
- EnsurePhysicLine();
+ Hidden, // 收起,不渲染不模拟
+ Free, // 闲置下垂,自动回收到最短长度
+ Dragging, // 末端钉在拖拽目标上,按需送线
+ Plugged // 末端钉在插孔上
}
- public void Init(Vector3 endPos)
- {
- EnsurePhysicLine();
- if (physicLine == null || startTransform == null) return;
+ [FormerlySerializedAs("StartTranform")] public Transform startTransform; // 出线口
- physicLine.Initialize(startTransform.position, endPos, GetOutletDirection(endPos));
+ [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] private float gravity = 100f; // 世界单位/s²,向下
+ [SerializeField, Range(0.8f, 1f)] private float damping = 0.965f;
+ [SerializeField] private int constraintIterations = 24;
+ [SerializeField, Range(0f, 1f)] private float bendSmooth = 0.3f; // 弯曲刚度(中点平滑)
+ [SerializeField] private float endGravityBoost = 1.8f; // 末端几个点的重力加成,让插头端垂坠
+
+ [Header("收放")]
+ [SerializeField] private float feedRate = 0.45f; // 送线速度
+ [SerializeField] private float holdRate = 0.06f; // 拖拽/插接中回收速度
+ [SerializeField] private float retractRate = 0.09f; // 松手后回收速度
+ [SerializeField] private float lengthMargin = 0.45f; // 需求长度余量
+
+ [Header("出线口保护段")]
+ [SerializeField] private float stubMinOffset = 0.12f; // 出线口处沿出线方向至少伸出的距离
+
+ [Header("插头朝向")]
+ [SerializeField] private float plugRefArcLength = 0.6f; // 取角度参考点时向回走的弧长
+ [SerializeField] private float plugTurnRate = 0.18f;
+ [SerializeField] private float plugMaxTurnStep = 0.11f; // 每步最大转角(弧度)
+
+ [Header("可选")]
+ [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);
+
+
+ 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;
+
+ private struct Particle
+ {
+ public Vector3 pos;
+ public Vector3 prev;
}
- public void HideCable()
+ private Particle[] _pts;
+ private int _n;
+ private float _length;
+ private Vector3 _pointer; // 拖拽指针(世界坐标,驱动送线长度)
+ private Vector3 _dragTarget; // 末端钉住目标(重 lerp + 长度钳制,消除拽满时的抖动)
+ private Transform _dock; // 当前插入的插孔
+ private float _plugAngle = -Mathf.PI / 2f; // 插头朝向(弧度,初始朝下)
+
+ private LineRenderer _line;
+ private LineRenderer _bodyLine;
+ private LineRenderer _midLine;
+ private LineRenderer _highlightLine;
+ 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()
{
- GetComponent().enabled = false;
+ _line = GetComponent();
+ _defaultSortingLayer = _line.sortingLayerName;
+ _defaultSortingOrder = _line.sortingOrder;
+ _baseWidthMultiplier = _line.widthMultiplier;
+ _baseWidthCurve = _line.widthCurve;
+ _line.enabled = false;
+ EnsurePixelLines();
+ ApplyPixelStyle();
}
- public void ShowCable()
+ // ------------------------------ 状态 API ------------------------------
+
+ public void Hide()
{
- GetComponent().enabled = true;
+ State = CableState.Hidden;
+ _dock = null;
+ if (_line != null) _line.enabled = false;
+ SetPixelLinesEnabled(false);
}
- public void SetEnabled(bool enabled)
+ /// 闲置下垂状态;resetShape 时把绳重置为从出线口自然下垂。
+ public void ShowFree(bool resetShape = false)
{
- isEnabled = enabled;
- GetComponent().enabled = enabled;
+ EnsureInit();
+ if (resetShape) ResetHangingShape();
+ _dock = null;
+ State = CableState.Free;
+ SetCableRenderersEnabled(true);
}
- public void SetTarget(Transform target)
+ public void BeginDrag(Vector3 pointerWorld)
{
- targetTransform = target;
+ EnsureInit();
+ if (State == CableState.Hidden) ResetHangingShape();
+ _dock = null;
+ _pointer = FlattenZ(pointerWorld);
+ _dragTarget = _pts[_n - 1].pos;
+ State = CableState.Dragging;
+ SetCableRenderersEnabled(true);
}
- private void Update()
+ public void SetPointer(Vector3 pointerWorld)
{
- if (isEnabled)
+ _pointer = FlattenZ(pointerWorld);
+ }
+
+ public void EndDrag()
+ {
+ if (State == CableState.Dragging)
{
- EnsurePhysicLine();
- if (physicLine == null) return;
-
- // 更新起点位置
- if (startTransform != null)
- {
- Vector3 fallbackTarget = targetTransform != null ? targetTransform.position : startTransform.position + startTransform.right;
- physicLine.UpdateOutletDirection(GetOutletDirection(fallbackTarget));
- physicLine.UpdateStart(startTransform.position);
- }
-
- // 更新终点位置
- if (targetTransform != null)
- {
- physicLine.UpdatePlug(targetTransform);
- }
+ State = CableState.Free;
}
}
- private Vector3 GetOutletDirection(Vector3 fallbackTarget)
+ /// 末端钉到插孔;snapStraight 用于读档,把绳直接摆成出线口到插孔的直线。
+ public void PlugInto(Transform dock, bool snapStraight = false)
{
- if (startTransform == null)
+ EnsureInit();
+ _dock = dock;
+ State = CableState.Plugged;
+ SetCableRenderersEnabled(true);
+ if (snapStraight && dock != null)
{
- return Vector3.right;
+ ResetStraightTo(FlattenZ(dock.position));
+ }
+ }
+
+ // ------------------------------ 插头姿态 ------------------------------
+
+ /// 把插头摆到绳末端并按绳向旋转(角度经过平滑与限速)。
+ 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);
+ }
+ }
+
+ // ------------------------------ 渲染排序 ------------------------------
+
+ public void SetSorting(string sortingLayerName, int sortingOrder)
+ {
+ // 主线必须保持在出线口/线盘下方;点击时只提高插头本体层级。
+ }
+
+ public void RestoreDefaultSorting()
+ {
+ _line.sortingLayerName = _defaultSortingLayer;
+ _line.sortingOrder = _defaultSortingOrder;
+ }
+
+ // ------------------------------ 模拟 ------------------------------
+
+ private void FixedUpdate()
+ {
+ if (State == CableState.Hidden || _pts == null || startTransform == null) return;
+
+ Simulate(Time.fixedDeltaTime);
+ Render();
+ }
+
+ private void EnsureInit()
+ {
+ if (_initialized) return;
+ _initialized = true;
+
+ if (_line == null) _line = GetComponent();
+ EnsurePixelLines();
+ ApplyPixelStyle();
+
+ _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();
+ }
+
+ 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);
+ }
+
+ private void Simulate(float dt)
+ {
+ 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);
}
- if (startTransform.parent != null)
+ // --- 拖拽目标:重 lerp + 按当前绳长钳制(拽满时目标跟着绳长走,消除抖动)
+ if (dragging)
{
- Vector3 radialDirection = startTransform.position - startTransform.parent.position;
- if (radialDirection.sqrMagnitude > 1e-6f)
+ _dragTarget += (_pointer - _dragTarget) * 0.3f;
+ Vector3 offset = _dragTarget - anchor;
+ float dist = offset.magnitude;
+ float maxRadius = Mathf.Min(lMax, _length) * 0.99f;
+ if (dist > maxRadius)
{
- return radialDirection.normalized;
+ _dragTarget = anchor + offset / dist * maxRadius;
}
}
- Vector3 fallbackDirection = fallbackTarget - startTransform.position;
- if (fallbackDirection.sqrMagnitude > 1e-6f)
+ // --- 送线 / 回收(长度由原始指针驱动,而非被钳制后的目标)
+ Vector3 endTarget = dragging ? _pointer : (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;
+
+ // --- Verlet 积分
+ for (int i = 1; i < _n; i++)
{
- return fallbackDirection.normalized;
+ ref Particle p = ref _pts[i];
+ Vector3 v = (p.pos - p.prev) * damping;
+ p.prev = p.pos;
+ float boost = i > _n - 4 ? endGravityBoost : 1f;
+ p.pos += v + Vector3.down * (g * boost);
}
- return startTransform.right;
+ _pts[0].pos = anchor;
+ _pts[0].prev = anchor;
+
+ if (dragging)
+ {
+ _pts[_n - 1].pos = _dragTarget;
+ }
+ else if (plugged)
+ {
+ _pts[_n - 1].pos += (dock - _pts[_n - 1].pos) * 0.5f;
+ }
+
+ // --- 约束迭代
+ 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);
+
+ // 弯曲刚度:中点平滑(每 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;
+ _pts[i].pos += (mid - _pts[i].pos) * (bendSmooth * 0.5f);
+ }
+ }
+ }
+
+ // --- 地面(被钉住的拖拽末端豁免,避免钉点与地面互相拉扯)
+ 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(anchor, dragging, plugged);
}
- private void EnsurePhysicLine()
+ private void ApplyOutletStub(Vector3 anchor, Vector3 dir)
{
- if (physicLine == null)
+ // 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(Vector3 anchor, bool dragging, bool plugged)
+ {
+ Vector3 end = _pts[_n - 1].pos;
+ float targetAngle;
+
+ if (plugged)
{
- physicLine = transform.GetComponent();
+ // 插着时插头精灵隐藏,保持角度即可
+ targetAngle = _plugAngle;
}
+ else
+ {
+ // 拽紧时绳近似出线口到插头的直线,直接沿这条线取向;
+ // 局部段方向在高张力下噪声大
+ Vector3 toEnd = end - anchor;
+ float taut = toEnd.magnitude / Mathf.Max(0.001f, _length);
+ if (dragging && taut > 0.92f)
+ {
+ targetAngle = Mathf.Atan2(toEnd.y, toEnd.x);
+ }
+ else
+ {
+ // 角度取自向回走固定弧长的参考点;绳被压缩(末端点堆积)时方向不可靠,
+ // 保持上一帧角度而不是乱翻
+ int k = _n - 1;
+ float arc = 0f;
+ while (k > 0 && arc < plugRefArcLength)
+ {
+ arc += Vector3.Distance(_pts[k].pos, _pts[k - 1].pos);
+ k--;
+ }
+ Vector3 refP = _pts[k].pos;
+ targetAngle = Vector3.Distance(end, refP) > plugRefArcLength * 0.3f
+ ? Mathf.Atan2(end.y - refP.y, end.x - refP.x)
+ : _plugAngle;
+ }
+ }
+
+ float da = Mathf.DeltaAngle(_plugAngle * Mathf.Rad2Deg, targetAngle * Mathf.Rad2Deg) * Mathf.Deg2Rad;
+ float step = Mathf.Clamp(da * plugTurnRate, -plugMaxTurnStep, plugMaxTurnStep);
+ _plugAngle += step;
+ }
+
+ 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);
+ }
+
+ for (int i = 0; i < count; i++)
+ {
+ _line.SetPosition(i, src[i]);
+ }
+ RenderPixelBands(src, count);
+ }
+
+ private float GetIdleLength()
+ {
+ return Mathf.Max(0.1f, minLength * idleLengthScale);
+ }
+
+ private void EnsurePixelLines()
+ {
+ if (!usePixelStyle || _line == null) return;
+
+ _bodyLine = EnsurePixelLine(_bodyLine, "Pixel Body");
+ _midLine = EnsurePixelLine(_midLine, "Pixel Mid");
+ _highlightLine = EnsurePixelLine(_highlightLine, "Pixel Highlight");
+ }
+
+ private LineRenderer EnsurePixelLine(LineRenderer renderer, string objectName)
+ {
+ if (renderer != null) return renderer;
+
+ Transform existing = transform.Find(objectName);
+ if (existing != null && existing.TryGetComponent(out LineRenderer existingRenderer))
+ {
+ return existingRenderer;
+ }
+
+ var child = new GameObject(objectName);
+ child.transform.SetParent(transform, false);
+ return child.AddComponent();
+ }
+
+ private void ApplyPixelStyle()
+ {
+ 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;
+ }
+
+ 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]);
+ }
+ }
+
+ ///
+ /// 一次 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;
}
}
diff --git a/Assets/Scripts/FixSystemNew/Cable/Plug.cs b/Assets/Scripts/FixSystemNew/Cable/Plug.cs
index ed6f8390a..aa3f0c9b5 100644
--- a/Assets/Scripts/FixSystemNew/Cable/Plug.cs
+++ b/Assets/Scripts/FixSystemNew/Cable/Plug.cs
@@ -2,7 +2,6 @@
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.Utility;
-using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
@@ -21,7 +20,6 @@ namespace AibisDream.FixSystem
[SerializeField] private Vector3 startPosOffset = new(0.3f, -2f, 0);
private Vector3 _startPos;
- private readonly Quaternion _pickupRotation = Quaternion.Euler(0, 0, 30);
private readonly Quaternion _defaultRotation = Quaternion.Euler(0, 0, 180);
#endregion
@@ -126,7 +124,8 @@ namespace AibisDream.FixSystem
if (eventData is PointerEventData pointerData)
{
- transform.position = CameraKit.GetMouseWorldPos(pointerData.position);
+ // 指针只驱动线缆送线与末端钉住目标,插头本体由绳末端带动(CablePanel.LateUpdate)
+ cablePanel.UpdateDragPointer(CameraKit.GetMouseWorldPos(pointerData.position));
}
}
@@ -144,7 +143,6 @@ namespace AibisDream.FixSystem
TryCloseHighlight();
_isDragging = true;
- AdjustPlugPos();
AudioManager.Instance.PlaySfx("event:/FollowInput/plugpick");
}
@@ -184,18 +182,8 @@ namespace AibisDream.FixSystem
public void ReturnToStartPosition()
{
- transform.rotation = _defaultRotation;
- transform.DOMove(_startPos, 0.1f).OnComplete(() =>
- {
- cablePanel.CableReelRef.ResetRotation();
- cablePanel.CableRef.RestoreDefaultSorting();
- cablePanel.SetCableMode(CableMode.Physic, transform);
- });
- }
-
- private void AdjustPlugPos()
- {
- transform.rotation = _pickupRotation;
+ // 解除末端钉住,线缆自动回收到最短长度,插头挂在绳末端跟着回去
+ cablePanel.ReleasePlug();
}
void OnDrawGizmosSelected()
diff --git a/Assets/Scripts/FixSystemNew/Screen System/FixPanel/CablePanel.cs b/Assets/Scripts/FixSystemNew/Screen System/FixPanel/CablePanel.cs
index 1a7e3910a..c5b5c6f98 100644
--- a/Assets/Scripts/FixSystemNew/Screen System/FixPanel/CablePanel.cs
+++ b/Assets/Scripts/FixSystemNew/Screen System/FixPanel/CablePanel.cs
@@ -1,17 +1,10 @@
-using UnityEngine;
+using UnityEngine;
using DG.Tweening;
using AibisDream.Kit;
using AibisDream.Framework;
namespace AibisDream.FixSystem
{
- public enum CableMode
- {
- None, // 都隐藏(收起状态)
- Visual, // 使用 Cable(拖拽时)
- Physic // 使用 PhysicCable(闲置时)
- }
-
public class CablePanel : MonoBehaviour, IOperatorPanel
{
private const string SortingLayerDragging = "Tools";
@@ -22,7 +15,7 @@ namespace AibisDream.FixSystem
[Header("插线组件索引")]
[SerializeField] private Plug plugRef;
- [SerializeField] private Cable cableRef;
+ [SerializeField] private Cable cableRef; // 旧视觉线缆,已由 PhysicCable 统一取代,Start 时停用
[SerializeField] private PhysicCable physicCableRef;
[SerializeField] private Transform cableRootPos;
[SerializeField] private CableReel cableReelRef;
@@ -48,12 +41,22 @@ namespace AibisDream.FixSystem
private ISocket curSocket;
+ // 收线动画期间插头由 DOTween 驱动,暂停"插头跟随绳末端"
+ private bool suppressPlugFollow;
+
#endregion
private void Start()
{
InitComponentRefs();
InitSystem();
+
+ // 单线方案:旧视觉线缆整体停用,所有状态都走 PhysicCable
+ if (cableRef != null)
+ {
+ cableRef.gameObject.SetActive(false);
+ }
+
InitCableVisualFromPrefab();
}
@@ -63,7 +66,7 @@ namespace AibisDream.FixSystem
plugRef.OnPullUpSocket -= OnPullUpSocket;
plugRef.OnPlugPointerDown -= OnPlugPoinerDown;
}
-
+
private void InitComponentRefs()
{
BodyModuleSystem = FixSystemCenter.SystemDic.Get();
@@ -138,7 +141,6 @@ namespace AibisDream.FixSystem
else
{
curSocket = null;
- SetCableMode(CableMode.Physic);
}
}
@@ -147,7 +149,7 @@ namespace AibisDream.FixSystem
PlugRef.PullUpSocketSilent();
CableReelRef.ResetRotation();
PlugRef.RestoreAtStartPositionImmediate();
- SetCableMode(CableMode.Physic);
+ physicCableRef.ShowFree(true);
isCableRetracted = false;
}
@@ -158,15 +160,14 @@ namespace AibisDream.FixSystem
{
Debug.LogWarning($"[CablePanel] RestorePlugToModule: 未找到模块 {moduleName},插头留在初始位。");
curSocket = null;
- SetCableMode(CableMode.Physic);
+ physicCableRef.ShowFree(true);
return;
}
curSocket = module;
var socketPos = module.GetSocketPos();
PlugRef.RestoreInsertedAt(socketPos);
- CableRef.SetEndPos(socketPos);
- SetCableMode(CableMode.Visual);
+ physicCableRef.PlugInto(socketPos, true);
BodyModuleSystem.CloseModuleSlots(null);
}
@@ -174,7 +175,6 @@ namespace AibisDream.FixSystem
{
if (curSocket == null) return;
- CableRef.SetEndPos(PlugRef.PlugRoot);
curSocket.PlugOut();
curSocket = null;
PlugRef.SetSpriteVisible(true);
@@ -189,10 +189,25 @@ namespace AibisDream.FixSystem
private void Update()
{
- // 获取线缆方向并更新转盘旋转
- if (CableRef != null && CableReelRef != null)
+ // 拖拽/插接时转盘朝拉力方向转动,闲置时缓动回正
+ if (CableReelRef != null && physicCableRef != null
+ && physicCableRef.State != PhysicCable.CableState.Hidden)
{
- CableReelRef.UpdateRotation();
+ bool taut = physicCableRef.State == PhysicCable.CableState.Dragging
+ || physicCableRef.State == PhysicCable.CableState.Plugged;
+ CableReelRef.UpdateRotation(taut);
+ }
+ }
+
+ private void LateUpdate()
+ {
+ if (suppressPlugFollow || physicCableRef == null) return;
+
+ // 闲置/拖拽时插头挂在绳末端,位置与朝向都由绳给出
+ var state = physicCableRef.State;
+ if (state == PhysicCable.CableState.Free || state == PhysicCable.CableState.Dragging)
+ {
+ physicCableRef.ApplyPlugPose(plugRef.transform, plugRef.PlugRoot);
}
}
@@ -215,8 +230,8 @@ namespace AibisDream.FixSystem
{
var isSameSocket = curSocket == socket;
curSocket = socket;
- CableRef.SetEndPos(socket.GetSocketPos());
- CableRef.RestoreDefaultSorting();
+ physicCableRef.PlugInto(socket.GetSocketPos());
+ physicCableRef.RestoreDefaultSorting();
AudioManager.Instance.PlaySfx("event:/FollowInput/plugin");
if (!isSameSocket
@@ -230,9 +245,6 @@ namespace AibisDream.FixSystem
private void OnPullUpSocket()
{
- // 修改线头位置
- CableRef.SetEndPos(PlugRef.PlugRoot);
-
curSocket?.PlugOut();
AudioManager.Instance.PlaySfx("event:/FollowInput/plugout");
@@ -249,17 +261,27 @@ namespace AibisDream.FixSystem
{
if (IsPlugInSocket()) PlugRef.PullUpSocket();
- // 首次拿起时 _end 可能仍是插头 transform,需显式切到线缆连接点
- CableRef.SetEndPos(PlugRef.PlugRoot);
-
- // 点击时切换到视觉线缆
- SetCableMode(CableMode.Visual);
- CableRef.SetSorting(SortingLayerDragging, CableSortingOrderDragging);
+ // 末端钉到拖拽目标,按需送线;指针位置随 OnDrag 持续更新
+ physicCableRef.BeginDrag(PlugRef.transform.position);
+ physicCableRef.SetSorting(SortingLayerDragging, CableSortingOrderDragging);
// 打开插孔
BodyModuleSystem.OpenModuleSlots();
}
+ /// 拖拽中更新指针位置(世界坐标),由 Plug.OnDrag 调用。
+ public void UpdateDragPointer(Vector3 pointerWorldPos)
+ {
+ physicCableRef.SetPointer(pointerWorldPos);
+ }
+
+ /// 松手且未插中插孔:解除钉住,线缆自动回收,插头挂回绳末端。
+ public void ReleasePlug()
+ {
+ physicCableRef.EndDrag();
+ physicCableRef.RestoreDefaultSorting();
+ }
+
// ------------------------------ 系统状态 ------------------------------
public bool IsPlugInSocket()
{
@@ -268,36 +290,6 @@ namespace AibisDream.FixSystem
// ------------------------------ 线缆操作 ------------------------------
- ///
- /// 设置线缆显示模式
- ///
- /// 线缆模式
- /// PhysicCable跟随的目标,默认为Plug
- public void SetCableMode(CableMode mode, Transform followTarget = null)
- {
- switch (mode)
- {
- case CableMode.None:
- CableRef.HideCable();
- PhysicCableRef.SetEnabled(false);
- PhysicCableRef.SetTarget(null);
- break;
-
- case CableMode.Visual:
- CableRef.ShowCable();
- PhysicCableRef.SetEnabled(false);
- PhysicCableRef.SetTarget(null);
- break;
-
- case CableMode.Physic:
- CableRef.HideCable();
- PhysicCableRef.SetEnabled(true);
- PhysicCableRef.Init(PlugRef.transform.position);
- PhysicCableRef.SetTarget(followTarget != null ? followTarget : PlugRef.transform);
- break;
- }
- }
-
///
/// 寻找最接近的Module
///
@@ -322,11 +314,16 @@ namespace AibisDream.FixSystem
BodyModuleSystem.CloseModuleSlots(null);
- PlugRef.transform.DOMove(retractedPos.position, 0.1f).OnComplete(() =>
- {
- ApplyRetractedState();
- isCableRetracted = true;
- });
+ // 收线动画:末端钉在插头上跟着一起飞向转盘,线长自动回收
+ suppressPlugFollow = true;
+ physicCableRef.BeginDrag(PlugRef.transform.position);
+ PlugRef.transform.DOMove(retractedPos.position, 0.1f)
+ .OnUpdate(() => physicCableRef.SetPointer(PlugRef.transform.position))
+ .OnComplete(() =>
+ {
+ ApplyRetractedState();
+ isCableRetracted = true;
+ });
AudioManager.Instance.PlaySfx("event:/ActionFB/mods_close");
AudioManager.Instance.PlaySfx("event:/FollowInput/plugdrop");
@@ -338,9 +335,11 @@ namespace AibisDream.FixSystem
// 确保 Plug 已脱离 Socket(收起状态下通常已脱离,此处为防御性调用)
PlugRef.PullUpSocket();
CableReelRef.ResetRotation();
+ suppressPlugFollow = true;
PlugRef.transform.DOMove(PlugRef.GetStartPos(), 0.1f).OnComplete(() =>
{
- SetCableMode(CableMode.Physic);
+ suppressPlugFollow = false;
+ physicCableRef.ShowFree(true);
isCableRetracted = false;
});
}
@@ -348,7 +347,8 @@ namespace AibisDream.FixSystem
/// 应用收起状态的视觉与组件设置(不含动画、不含 isCableRetracted 更新)
private void ApplyRetractedState()
{
- SetCableMode(CableMode.None);
+ physicCableRef.Hide();
+ suppressPlugFollow = false;
PlugRef.transform.position = retractedPos.position;
PlugRef.transform.rotation = retractedPos.rotation;