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;