17 KiB
PhysicCable 实现说明
代码路径:
Assets/Scripts/FixSystemNew/Cable/原型来源:Assets/Prototype/2D线缆物理交互/Cable Physics.dc.html相关文档:线缆物理移植分析与方案(侧重渲染移植与问题根因) 更新:2026-07-09
1. 概述
PhysicCable 是维修面板插线玩法的统一物理线缆组件。它用一条 Verlet 绳 覆盖原先两套状态:
| 旧方案 | 行为 |
|---|---|
Cable |
拖拽时的视觉线 |
PhysicCable(旧) |
闲置下垂 |
新方案:固定粒子数 + 可变总长度,同一套模拟处理闲置下垂、拖拽送线、插孔钉住、收线隐藏。
上层协调由 CablePanel 完成;插头交互在 Plug;渲染由 CableMeshRenderer 负责。
Plug(输入) → CablePanel(状态协调) → PhysicCable(物理 + 渲染调度)
↓
CableMeshRenderer(多层 ribbon Mesh)
2. 文件职责
| 文件 | 状态 | 职责 |
|---|---|---|
PhysicCable.cs |
当前主实现 | 状态机、Verlet 模拟、送收线、手感、渲染调度 |
CableMeshRenderer.cs |
当前渲染 | 折线 → 多层色带 Mesh |
Plug.cs |
活跃 | 拖拽/插拔、DOTween 手感、事件上报 |
CablePanel.cs |
活跃 | 插头事件 ↔ 线缆 API、收放线、读档 |
VerletParticle.cs |
遗留 | 旧粒子类,PhysicCable 未使用 |
VerletStick.cs |
遗留 | 旧约束类,PhysicCable 未使用 |
PhysicLineSegment.cs |
遗留 | 旧 LineRenderer 方案,PhysicCable 未使用 |
3. 状态机
public enum CableState
{
Hidden, // 收起:不模拟、不渲染
Free, // 闲置:自然下垂,自动回收到最短长度
Dragging, // 拖拽:末端钉在指针,按需送线
Plugged // 插接:末端钉在插孔
}
3.1 状态切换 API
| 方法 | 进入状态 | 典型调用方 |
|---|---|---|
Hide() |
Hidden |
CablePanel.ApplyRetractedState |
ShowFree(resetShape?) |
Free |
放下线缆、松手未插中 |
BeginDrag(pointer) |
Dragging |
按下插头、收线动画 |
SetPointer(pointer) |
(保持 Dragging) | 拖拽中每帧 |
EndDrag() |
Free |
松手未插中 |
PlugInto(dock, snapStraight?) |
Plugged |
插入模块 |
SetDragAssistTarget / ClearDragAssistTarget |
— | 靠近插孔时软吸附 |
3.2 状态与物理行为
| 状态 | 起点 | 末端 | 线长 _length |
|---|---|---|---|
Hidden |
— | — | 不更新 |
Free |
钉在出线口 | 自由 | 回收到 idleLength |
Dragging |
钉在出线口 | 钉在 _dragTarget |
按指针距离送线/回收 |
Plugged |
钉在出线口 | 钉在孔心 _plugSnapEnd |
保证够到插孔 |
4. 物理核心
4.1 数据结构
private struct Particle { public Vector3 pos; public Vector3 prev; }
private Particle[] _pts; // 固定数量,默认 40
private float _length; // 可变总长度
关键设计:粒子数 _n 固定,相邻粒子目标间距 seg = _length / (_n - 1) 随 _length 变化。因此绳可以「拉长送线」或「缩短回收」,而不改变粒子数量。
闲置长度:
idleLength = minLength * idleLengthScale
4.2 每帧模拟流程(FixedUpdate → Simulate)
1. 送线 / 回收 → 调整 _length
2. Verlet 积分 → 重力 + 阻尼
3. 钉住端点 → 起点 / 拖拽目标 / 插孔
4. 约束迭代 × N → 距离约束 + 出线口保护 + 插接直插 + 弯曲刚度
5. 地面碰撞(可选) → floorLimit
6. 插头朝向 → UpdatePlugAngle
送线 / 回收
float needed = Distance(endTarget, anchor) * 1.04f + lengthMargin;
float targetLength = pinnedEnd ? Clamp(needed, idleLength, lMax) : idleLength;
float rate = targetLength > _length ? feedRate : (pinnedEnd ? holdRate : retractRate);
_length += (targetLength - _length) * rate;
| 场景 | 速率参数 |
|---|---|
| 需要更长 | feedRate(0.45) |
| 末端固定但过长 | holdRate(0.06) |
| 闲置松手 | retractRate(0.09) |
Verlet 积分
Vector3 v = (p.pos - p.prev) * damping;
p.prev = p.pos;
p.pos += v + Vector3.down * (gravity * dt² * endGravityBoost);
pts[0]每帧钉在startTransform(出线口)- 最后 3 个粒子
endGravityBoost加重,插头端更坠
距离约束
对每对相邻粒子,迭代 constraintIterations(默认 24)次,把间距拉回 seg:
- 起点粒子权重 0(完全固定)
- 拖拽/插接时,倒数第二段在末端侧权重偏向固定,减少抖动
附加约束
| 约束 | 方法 | 作用 |
|---|---|---|
| 出线口保护段 | ApplyOutletStub |
P1/P2 沿出线方向伸出、横向收紧,避免出线折角 |
| 插接直插 | Simulate 内 plugged 分支 |
倒数第二点拉向「孔心沿来线后退一节」 |
| 弯曲刚度 | bendSmooth |
每 3 次迭代对中间点做中点平滑 |
| 地面 | floorLimit |
Y 钳制 + 水平摩擦;拖拽末端豁免 |
4.3 出线方向
GetOutletDirection:从 reelCenter(或 startTransform.parent)指向出线口的径向;fallback 为 Vector3.down。
5. 手感系统
物理之上叠加的交互手感,均在 PhysicCable 内实现。
5.1 拖拽防抖
指针经 GetAssistedPointer(吸附)后,再对 _dragTarget 做 lerp * 0.3,并按当前绳长钳制最大半径,避免拽满时末端抖动。
5.2 插孔吸附
SetDragAssistTarget(dock, radius):指针靠近插孔时,混合向孔心偏移(socketAssistStrength + 平滑 blend)。
5.3 拔线阻力
StartUnplugResistance:从已插状态按下插头时,指针被「粘」在孔心;拉出距离超过 unplugBreakDistance 后释放,并 KickTail 给尾段冲量。
5.4 插入冲击
BeginInsertImpact:插入瞬间末段前推(insertShoveDistance)+ 横向抖动(insertShakeAmplitude)+ 尾段 kick(insertTailKick)。
5.5 插头朝向
UpdatePlugAngle:
- 绳拉紧(
taut > 0.92):沿「出线口 → 末端」直线取向 - 松弛:沿末端往回走
plugRefArcLength弧长取参考点方向 - 每帧限速转角(
plugTurnRate、plugMaxTurnStep)
ApplyPlugPose(plug, plugRoot):把插头摆到绳末端并按 _plugAngle 旋转。由 CablePanel.LateUpdate 在渲染后调用,与 PhysicCable.LateUpdate 读同一份点位,避免线头分离。
6. 渲染:CableMeshRenderer
6.1 为何不用 LineRenderer
旧 LineRenderer 已禁用(Awake 里 enabled = false)。多层描边、固定画序、顶光偏移、自遮挡等需求无法用多条 LineRenderer + sorting order 稳定复现(详见移植分析文档)。
6.2 渲染流程(LateUpdate → Render)
物理点 _pts[N]
→ 拷贝到 _renderA
→ Chaikin 切角细分 × 2(N → 4N 点,更圆滑)
→ FindTailStart(自遮挡检测)
→ CableMeshRenderer.UpdateMesh(多层 ribbon + 可选圆头端帽)
Chaikin 一次细分:保留首尾,每段取 1/4、3/4 两个切分点,点数翻倍。
6.3 多层色带(像素风)
BuildBands 提交 4 层,提交顺序 = 画序(后画的盖前画的):
| 带 | 角色 | 特殊 |
|---|---|---|
| Shadow | 落影 | 向下偏移 shadowDrop;尾段重绘时跳过 |
| Dark | 描边 | — |
| Body | 主体 | — |
| Mid | 中间调/顶光 | 向上偏移 midLift |
每层把折线沿法线方向扩成 ribbon(宽度 band.width),顶点色上色,无光照计算。
6.4 自遮挡(tail-over-head)
FindTailStart:找下垂最低点 j;若 j 之后尾段与 j 之前头段空间重叠,返回尾段起始下标;UpdateMesh 把尾段非阴影带再画一遍盖在最上。
6.5 插接圆头
Plugged 时 roundEndCap = true:末端用半圆端帽(CapSegments 细分),视觉上像插进孔里。
6.6 排序
- 线缆 Mesh:
Tools层 /sortingOrder = 47 - 插头 Sprite:
Tools层 /sortingOrder = 48(永远画在线上方) - 收起时插头压到线盘下方(
sortingOrder = 41)
7. 上层协作(CablePanel)
7.1 生命周期
Start:停用旧Cable;按isCableRetracted初始化Hide或ShowFreeLateUpdate:Free/Dragging时ApplyPlugPose+ApplyFeedbackOffset
7.2 典型事件流
按下插头
→ PullUpSocket(若已插)
→ BeginDrag(plug.position)
→ StartUnplugResistance(若刚拔出)
拖拽中
→ SetPointer(mouseWorld)
→ SetDragAssistTarget(靠近模块时)
松手
├─ 插中模块 → Plug DOTween → OnInsertSocket → PlugInto
└─ 未插中 → ReleasePlug → EndDrag
收线
→ BeginDrag + DOTween 插头到 retractedPos
→ Hide + 插头排序降级
7.3 读档
ApplyCableSnapshot(retracted, pluggedModuleName):无动画恢复;插接时用 PlugInto(socket, snapStraight: true) 拉直摆型。
8. 关键参数速查
| 参数 | 默认量级 | 含义 |
|---|---|---|
pointCount |
40 | 粒子数 |
minLength |
2 | 长度基准 |
idleLengthScale |
0.08 | 闲置长度 = minLength × 此值 |
maxStretch |
8 | 最大拉出 = minLength × 此值 |
gravity |
100 | 重力(世界单位/s²) |
damping |
0.965 | 速度衰减 |
constraintIterations |
24 | 约束迭代次数 |
bendSmooth |
0.3 | 弯曲刚度 |
feedRate / holdRate / retractRate |
0.45 / 0.06 / 0.09 | 送线 / 固定回收 / 闲置回收 |
plugSnapRate |
0.55 | 插接末端收敛到孔心 |
socketAssistStrength |
0.62 | 拖拽吸附强度 |
9. 自定义 Mesh 与「3D 做法」的关系
结论:借用了 3D Mesh 的「几何数据结构」,但渲染语义是 2D,不是完整的 3D 线缆方案。
| 维度 | 本项目的 CableMeshRenderer |
典型 3D 线缆 |
|---|---|---|
| 几何 | Mesh:顶点 + 三角形,沿折线法线挤出 ribbon |
同样常用 Mesh(或 LineRenderer 3D) |
| 坐标 | 顶点直接用世界 XY,Z 压平为 0 | 真 3D 空间,有厚度与深度 |
| 着色 | 顶点色 + Sprite/Unlit 材质,不算光照 | 常配合法线、PBR、环境光 |
| 遮挡 | sortingLayer + sortingOrder + 三角形提交顺序 |
深度缓冲(Z-test) |
| 阴影 | 关闭 shadowCasting / receiveShadows |
可投射/接收阴影 |
| 物理 | 2D Verlet 点链,与 Mesh 无刚体绑定 | 可选 MeshCollider / 骨骼等 |
更准确的说法:
- 几何生成类似 3D 管线里「沿路径挤出带状 mesh」的做法(道路、管道、绳索 ribbon 的常见套路)。
- 渲染管线走的是 Unity 2D 排序(
MeshRenderer.sortingLayer),配合无光照顶点色,等价于「用 Mesh 当画布一笔笔画上去」,复刻 HTML 原型的 painter's algorithm。 - 不是把线缆做成场景里的 3D 物体——没有 Z 深度排序、没有光照响应、没有与 3D 碰撞体联动。
这也是 2D 游戏里画复杂形状时的常见折中:Mesh 负责形状,2D 排序负责前后关系。
10. 渲染方案选型:Mesh vs LineRenderer vs Shapes
本节说明为何维修线缆选用自定义 Mesh,以及与项目内另外两种常见「画线」方案的区别。
10.1 一句话定位
| 方案 | 本质 | 本项目中的用途 |
|---|---|---|
| LineRenderer | Unity 内置「一条线 = 一个 Renderer」 | 旧 Cable.cs(已停用)、PhysicLineSegment(遗留) |
| Shapes | 第三方矢量绘图库(组件 + Immediate Mode) | 火山语言粒子连线、波形、ECG 等 |
| 自定义 Mesh | 手写顶点/三角形,单 MeshRenderer |
当前 PhysicCable / CableMeshRenderer |
10.2 LineRenderer
工作方式:给一串世界坐标,Unity 沿路径挤出圆角/圆帽管状线。一条 LineRenderer 对应一种宽度曲线 + 一种材质 + 一种颜色渐变;前后关系靠 sortingLayer / sortingOrder。
旧 Cable.cs 甚至用了两条 LineRenderer(柔性段 + 刚性段),说明单条线难以表达复杂形态。
曾尝试用 4 条子 LineRenderer 模拟像素风分带(阴影/描边/主体/高光),遇到的问题(详见移植分析文档):
| 问题 | 原因 |
|---|---|
| 层之间对不齐 | 各线独立圆角/端帽设置不一致 |
| 无法做「顶光上移」 | 只能同心变窄,无法 shadow 下移、mid 上移 |
| 排序脆弱 | 4 个 Renderer 需同步 order;外部在 37~40 层插入物体会「插进线缆内部」 |
| 自遮挡难做 | 尾段盖头段需再画一遍 → 8 条线 |
| 拖拽时线/插头分层 | 线与插头分属不同 sorting 层,一根线两端遮挡关系无法表达 |
| 外观随灯光漂移 | 共用 Sprite-Lit,受 CablePanelLight 呼吸灯影响 |
适合:简单连线、调试线、少量静态线段。
不适合:固定画序、多层像素风、自遮挡、与维修面板 Sprite 精密咬合的游戏物件级线缆。
10.3 Shapes(Freya Holmér 矢量库)
工作方式:
- 组件模式:
Polyline、Line等挂在物体上(内部也生成 Mesh)。 - Immediate Mode:
ImmediateModeShapeDrawer每帧Draw.Line()/Draw.Polyline()。
项目内参考:ConnectionRenderer(火山语言粒子连线)——Draw.Command(cam) + 透明混合 + 可配发光。
Shapes 擅长:
| 能力 | 说明 |
|---|---|
| 可变粗细 | 每点可不同 Thickness |
| 渐变/虚线/发光 | dash、gradient、glow(配合 Bloom) |
| 连接样式 | Miter / Bevel / Round join |
| 即时绘制 | 大量动态线段,按相机 pass 批处理 |
与 PhysicCable 需求的差距:
Shapes 的模型是每次 Draw = 一种样式的一条线。像素线缆仍需 Draw.Polyline × 4,于是与 LineRenderer 遇到同类问题:
- 多层 = 多次绘制,画序靠调用顺序,与场景 Sprite 的
sortingOrder混用时不如单 Mesh 稳 - 难精确复刻原型的「屏幕空间垂直偏移」(shadow 下移、mid 上移)
- 自遮挡尾段重绘需在同一帧再画一遍,且与面板元素排序协调成本高
- 无「painter's algorithm 线缆」这一层开箱即用抽象
适合:特效连线、波形、可发光/虚线的动态图元。
不适合:外观恒定、多层像素风、自遮挡、与 UI/Sprite 精密咬合的维修线缆。
10.4 自定义 Mesh(CableMeshRenderer)
工作方式:折线点 → 沿法线挤出 ribbon → 顶点色上色 → 按顺序 append 三角形到同一 Mesh。
与「画 4 条线」的本质区别:往同一个 Mesh 里塞 4 层几何,而非 4 个 Renderer / 4 次 Draw。
| 能力 | 实现 |
|---|---|
| 固定画序 | 三角形提交顺序 = 遮挡关系 |
| 单层排序控制 | 对外仅 1 个 MeshRenderer.sortingOrder |
| 分带垂直偏移 | band.lift(shadow 下移、mid 上移) |
| 自遮挡 | 尾段几何追加在同一 Mesh 末尾(FindTailStart) |
| 插接圆头 | 自定义半圆端帽三角形 |
| 平面固定色 | 顶点色 + Unlit,不受呼吸灯影响 |
| 尾段不重画阴影 | skipInTail per band |
代价:每帧手算顶点(当前规模 CPU 压力可忽略),mesh 逻辑约 200 行。
10.5 横向对比
| 维度 | LineRenderer | Shapes | 自定义 Mesh |
|---|---|---|---|
| 抽象层级 | 引擎内置「画线」 | 高级「画矢量」API | 最底层「拼三角形」 |
| 多层描边 | N 个 Renderer | N 次 Draw / N 个组件 | 1 个 Mesh 内多层 |
| 画序/遮挡 | 多个 sortingOrder | Draw 顺序 + sorting | 三角形顺序 |
| 路径平滑 | 自带 corner/cap | join + 可选 Bezier | 自实现 Chaikin |
| 每点不同宽度 | widthCurve(单条) | ✅ 原生 | 自实现 TipTaper |
| 虚线/发光 | ❌ 需自定义 shader | ✅ 很强 | ❌ 需自研 |
| 与 Sprite 2D 排序 | 一般 | 一般 | 最可控 |
| 开发成本 | 最低 | 低~中 | 中 |
| 外观稳定性 | 差(多 Renderer + 受光) | 好(特效向) | 最好(固定平面色) |
| 本项目线缆 | 已淘汰 | 未采用 | 当前方案 |
10.6 选型决策树
要画什么?
│
├─ 简单 / Debug / 一次性连线
│ → LineRenderer
│
├─ 动态特效线(发光、虚线、渐变、大量粒子连线)
│ → Shapes Immediate Mode(如 ConnectionRenderer)
│
└─ 游戏物件、与 UI/Sprite 精密咬合、多层像素风、自遮挡、外观恒定
→ 自定义 Mesh(CableMeshRenderer)
11. 调试建议
- 播放
PeipeiFixScene,展开维修面板,观察Free→Dragging→Plugged过渡 - 调
constraintIterations、bendSmooth看刚性/柔软度 - 调
feedRate/retractRate看送收线节奏 - 自遮挡:让绳下垂重叠,检查尾段是否正确盖在头段上
- 编译后看 Console:
PhysicCable不依赖PhysicLineSegment,后者挂在同一物体上可视为遗留
与 线缆物理移植分析与方案.md 互补:该文档偏「为何要换 Mesh、与原型差在哪」;本文偏「当前代码怎么跑」。