853 lines
32 KiB
C#
853 lines
32 KiB
C#
using UnityEngine;
|
|
using UnityEngine.Serialization;
|
|
|
|
/// <summary>
|
|
/// 统一物理线缆(移植自 Assets/Prototype/2D线缆物理交互 原型):
|
|
/// 固定粒子数 + 可变总长度的 Verlet 绳,一条线覆盖旧 Cable(拖拽)/ PhysicCable(闲置)两套状态。
|
|
/// 拖拽时按需送线、松手自动回收到最短长度、插入插孔时末端钉住。
|
|
/// </summary>
|
|
[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;
|
|
|
|
/// <summary>
|
|
/// 0=完全松弛,1=绷直。已扣除送线故意留的余量(1.04×直线距 + lengthMargin),
|
|
/// 避免「有设计余量就永远不转」;线盘只在真正被拽时才应转动。
|
|
/// </summary>
|
|
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<LineRenderer>();
|
|
if (_line != null) _line.enabled = false;
|
|
EnsureMeshRenderer();
|
|
}
|
|
|
|
// ------------------------------ 状态 API ------------------------------
|
|
|
|
public void Hide()
|
|
{
|
|
State = CableState.Hidden;
|
|
_dock = null;
|
|
_unplugResisting = false;
|
|
_insertImpactTimer = 0f;
|
|
SetCableRenderersEnabled(false);
|
|
}
|
|
|
|
/// <summary>闲置下垂状态;resetShape 时把绳重置为从出线口自然下垂。</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>末端钉到插孔;snapStraight 用于读档,把绳直接摆成出线口到插孔的直线。</summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 强制收起:不走拖拽物理,保留当前曲线路径并从尾端逐段缩短到 target(出线口)。
|
|
/// 用于指令强制收起等需要在极短、精确可控时长内完成的场景,避免拖拽跟随参数(为秒级手动拖拽标定)
|
|
/// 在几帧内来不及收敛而产生的甩鞭/抖动。
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
// ------------------------------ 插头姿态 ------------------------------
|
|
|
|
/// <summary>把插头摆到绳末端并按绳向旋转(角度经过平滑与限速)。</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collapsing 状态沿收线前的曲线逐段截短。线身形状不做整体缩放,
|
|
/// 只有尾端沿原路径倒退,形成被线盘吸入的观感。
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 自遮挡检测(移植自原型):找到下垂最低点 j,若 j 之后的尾段与 j 之前的头段
|
|
/// 空间重叠,返回尾段在平滑点数组里的起始下标(从 j 前两个物理点起),否则 -1。
|
|
/// </summary>
|
|
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<CableMeshRenderer>();
|
|
if (_cableMesh == null) _cableMesh = child.gameObject.AddComponent<CableMeshRenderer>();
|
|
|
|
_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<SpriteRenderer>();
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 一次 Chaikin 切角细分:保留首尾点,每段取 1/4、3/4 两个切分点,输出点数为 2n。
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|