feat(cable): 线缆改用 Mesh 渲染并优化插接物理
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -22,8 +22,8 @@ public class PhysicCable : MonoBehaviour
|
||||
[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, 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.965f;
|
||||
[SerializeField] private int constraintIterations = 24;
|
||||
@@ -48,12 +48,30 @@ public class PhysicCable : MonoBehaviour
|
||||
[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);
|
||||
[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;
|
||||
// 三档色阶(描边/主体/顶光),压暗去饱和贴合场景像素环境;原型的第四档高光去掉
|
||||
[SerializeField] private Color shadowColor = new(0.05f, 0.05f, 0.07f, 0.2f);
|
||||
[SerializeField] private float shadowWidth = 0.295f;
|
||||
[SerializeField] private float shadowDrop = 0.12f; // 原型:向下平移 7px
|
||||
[SerializeField] private Color darkColor = new(0.09f, 0.11f, 0.18f, 1f); // 深色描边
|
||||
[SerializeField] private float darkWidth = 0.33f; // 原型:w+4
|
||||
[SerializeField] private Color bodyColor = new(0.24f, 0.30f, 0.44f, 1f); // 主体(低饱和板岩蓝)
|
||||
[SerializeField] private float bodyWidth = 0.26f; // 原型:w=15px ≈ 0.26wu
|
||||
[SerializeField] private Color midColor = new(0.38f, 0.45f, 0.60f, 1f); // 顶光
|
||||
[SerializeField] private float midWidth = 0.143f; // 原型:0.55w
|
||||
[SerializeField] private float midLift = 0.036f; // 原型:上移 0.14w
|
||||
|
||||
[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;
|
||||
@@ -76,32 +94,22 @@ public class PhysicCable : MonoBehaviour
|
||||
private Vector3 _pointer; // 拖拽指针(世界坐标,驱动送线长度)
|
||||
private Vector3 _dragTarget; // 末端钉住目标(重 lerp + 长度钳制,消除拽满时的抖动)
|
||||
private Transform _dock; // 当前插入的插孔
|
||||
private Vector3 _plugSnapEnd; // 插接吸附目标:渐进收敛到孔心后末端硬钉,保证完全对齐堵住孔
|
||||
private float _plugAngle = -Mathf.PI / 2f; // 插头朝向(弧度,初始朝下)
|
||||
|
||||
private LineRenderer _line;
|
||||
private LineRenderer _bodyLine;
|
||||
private LineRenderer _midLine;
|
||||
private LineRenderer _highlightLine;
|
||||
private LineRenderer _line; // 遗留组件,仅保持禁用(旧场景/CableSystem 仍会引用)
|
||||
private CableMeshRenderer _cableMesh;
|
||||
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()
|
||||
{
|
||||
_line = GetComponent<LineRenderer>();
|
||||
_defaultSortingLayer = _line.sortingLayerName;
|
||||
_defaultSortingOrder = _line.sortingOrder;
|
||||
_baseWidthMultiplier = _line.widthMultiplier;
|
||||
_baseWidthCurve = _line.widthCurve;
|
||||
_line.enabled = false;
|
||||
EnsurePixelLines();
|
||||
ApplyPixelStyle();
|
||||
if (_line != null) _line.enabled = false;
|
||||
EnsureMeshRenderer();
|
||||
}
|
||||
|
||||
// ------------------------------ 状态 API ------------------------------
|
||||
@@ -110,8 +118,7 @@ public class PhysicCable : MonoBehaviour
|
||||
{
|
||||
State = CableState.Hidden;
|
||||
_dock = null;
|
||||
if (_line != null) _line.enabled = false;
|
||||
SetPixelLinesEnabled(false);
|
||||
SetCableRenderersEnabled(false);
|
||||
}
|
||||
|
||||
/// <summary>闲置下垂状态;resetShape 时把绳重置为从出线口自然下垂。</summary>
|
||||
@@ -159,6 +166,8 @@ public class PhysicCable : MonoBehaviour
|
||||
{
|
||||
ResetStraightTo(FlattenZ(dock.position));
|
||||
}
|
||||
// 吸附目标从当前末端出发,向孔心渐进收敛(保留吸入动画,最终严格钉在孔心)
|
||||
_plugSnapEnd = _pts[_n - 1].pos;
|
||||
}
|
||||
|
||||
// ------------------------------ 插头姿态 ------------------------------
|
||||
@@ -183,19 +192,6 @@ public class PhysicCable : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------ 渲染排序 ------------------------------
|
||||
|
||||
public void SetSorting(string sortingLayerName, int sortingOrder)
|
||||
{
|
||||
// 主线必须保持在出线口/线盘下方;点击时只提高插头本体层级。
|
||||
}
|
||||
|
||||
public void RestoreDefaultSorting()
|
||||
{
|
||||
_line.sortingLayerName = _defaultSortingLayer;
|
||||
_line.sortingOrder = _defaultSortingOrder;
|
||||
}
|
||||
|
||||
// ------------------------------ 模拟 ------------------------------
|
||||
|
||||
private void FixedUpdate()
|
||||
@@ -203,6 +199,13 @@ public class PhysicCable : MonoBehaviour
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -211,17 +214,13 @@ public class PhysicCable : MonoBehaviour
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
|
||||
if (_line == null) _line = GetComponent<LineRenderer>();
|
||||
EnsurePixelLines();
|
||||
ApplyPixelStyle();
|
||||
EnsureMeshRenderer();
|
||||
|
||||
_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();
|
||||
}
|
||||
@@ -315,7 +314,11 @@ public class PhysicCable : MonoBehaviour
|
||||
}
|
||||
else if (plugged)
|
||||
{
|
||||
_pts[_n - 1].pos += (dock - _pts[_n - 1].pos) * 0.5f;
|
||||
// 吸附目标渐进收敛到孔心后,末端硬钉——软拉会被重力/张力持续拽偏,堵不严孔
|
||||
_plugSnapEnd += (dock - _plugSnapEnd) * 0.5f;
|
||||
if ((_plugSnapEnd - dock).sqrMagnitude < 1e-4f) _plugSnapEnd = dock;
|
||||
_pts[_n - 1].pos = _plugSnapEnd;
|
||||
_pts[_n - 1].prev = _plugSnapEnd;
|
||||
}
|
||||
|
||||
// --- 约束迭代
|
||||
@@ -341,6 +344,20 @@ public class PhysicCable : MonoBehaviour
|
||||
// 无论绳的其余部分被拽到哪,出线处都不出现折角或自穿插
|
||||
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)
|
||||
{
|
||||
@@ -469,11 +486,37 @@ public class PhysicCable : MonoBehaviour
|
||||
(src, dst) = (dst, src);
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
// 插接时线缆末端由平头切面改为圆头端帽(线自身的圆头,同旧 LineRenderer),读作"插进孔里"
|
||||
_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++)
|
||||
{
|
||||
_line.SetPosition(i, src[i]);
|
||||
if (_pts[i].pos.y < _pts[j].pos.y) j = i; // 原型 y 向下,这里取世界最低点
|
||||
}
|
||||
RenderPixelBands(src, count);
|
||||
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()
|
||||
@@ -481,97 +524,67 @@ public class PhysicCable : MonoBehaviour
|
||||
return Mathf.Max(0.1f, minLength * idleLengthScale);
|
||||
}
|
||||
|
||||
private void EnsurePixelLines()
|
||||
private void EnsureMeshRenderer()
|
||||
{
|
||||
if (!usePixelStyle || _line == null) return;
|
||||
if (_cableMesh != null) return;
|
||||
|
||||
_bodyLine = EnsurePixelLine(_bodyLine, "Pixel Body");
|
||||
_midLine = EnsurePixelLine(_midLine, "Pixel Mid");
|
||||
_highlightLine = EnsurePixelLine(_highlightLine, "Pixel Highlight");
|
||||
// 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 LineRenderer EnsurePixelLine(LineRenderer renderer, string objectName)
|
||||
private Material ResolveMaterial()
|
||||
{
|
||||
if (renderer != null) return renderer;
|
||||
if (cableMaterial != null) return cableMaterial;
|
||||
|
||||
Transform existing = transform.Find(objectName);
|
||||
if (existing != null && existing.TryGetComponent(out LineRenderer existingRenderer))
|
||||
// 直接共用出线口所在线盘 sprite 的材质:与周围环境的受光行为保证完全一致
|
||||
if (startTransform != null)
|
||||
{
|
||||
return existingRenderer;
|
||||
var reference = startTransform.GetComponentInParent<SpriteRenderer>();
|
||||
if (reference != null && reference.sharedMaterial != null)
|
||||
{
|
||||
return reference.sharedMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
var child = new GameObject(objectName);
|
||||
child.transform.SetParent(transform, false);
|
||||
return child.AddComponent<LineRenderer>();
|
||||
Shader shader = Shader.Find("Universal Render Pipeline/2D/Sprite-Lit-Default");
|
||||
if (shader == null) shader = Shader.Find("Sprites/Default");
|
||||
return new Material(shader);
|
||||
}
|
||||
|
||||
private void ApplyPixelStyle()
|
||||
private CableMeshRenderer.Band[] BuildBands()
|
||||
{
|
||||
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;
|
||||
// 提交顺序即画序(后画的盖前画的);三档色阶,比原型少一档高光,贴合场景像素密度
|
||||
return new[]
|
||||
{
|
||||
new CableMeshRenderer.Band(shadowColor, shadowWidth, -shadowDrop, skipInTail: true),
|
||||
new CableMeshRenderer.Band(darkColor, darkWidth, 0f),
|
||||
new CableMeshRenderer.Band(bodyColor, bodyWidth, 0f),
|
||||
new CableMeshRenderer.Band(midColor, midWidth, midLift),
|
||||
};
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
EnsureMeshRenderer();
|
||||
_cableMesh.SetVisible(enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user