docs(fixsystem): 补充 PhysicCable 实现说明与渲染方案对比

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-09 14:18:47 +08:00
co-authored by Cursor
parent 8a7b2d0ed2
commit b0ece3e0f1
+433
View File
@@ -0,0 +1,433 @@
# PhysicCable 实现说明
> 代码路径:`Assets/Scripts/FixSystemNew/Cable/`
> 原型来源:`Assets/Prototype/2D线缆物理交互/Cable Physics.dc.html`
> 相关文档:[线缆物理移植分析与方案](./线缆物理移植分析与方案.md)(侧重渲染移植与问题根因)
> 更新: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. 状态机
```csharp
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 数据结构
```csharp
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
```
#### 送线 / 回收
```csharp
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 积分
```csharp
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 稳定复现(详见[移植分析文档](./线缆物理移植分析与方案.md))。
### 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``ShowFree`
- `LateUpdate``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 / 骨骼等 |
更准确的说法:
1. **几何生成**类似 3D 管线里「沿路径挤出带状 mesh」的做法(道路、管道、绳索 ribbon 的常见套路)。
2. **渲染管线**走的是 Unity 2D 排序(`MeshRenderer.sortingLayer`),配合无光照顶点色,等价于「用 Mesh 当画布一笔笔画上去」,复刻 HTML 原型的 painter's algorithm。
3. **不是**把线缆做成场景里的 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** 模拟像素风分带(阴影/描边/主体/高光),遇到的问题(详见[移植分析文档](./线缆物理移植分析与方案.md):
| 问题 | 原因 |
|------|------|
| 层之间对不齐 | 各线独立圆角/端帽设置不一致 |
| 无法做「顶光上移」 | 只能同心变窄,无法 shadow 下移、mid 上移 |
| 排序脆弱 | 4 个 Renderer 需同步 order;外部在 37~40 层插入物体会「插进线缆内部」 |
| 自遮挡难做 | 尾段盖头段需再画一遍 → 8 条线 |
| 拖拽时线/插头分层 | 线与插头分属不同 sorting 层,一根线两端遮挡关系无法表达 |
| 外观随灯光漂移 | 共用 `Sprite-Lit`,受 `CablePanelLight` 呼吸灯影响 |
**适合**:简单连线、调试线、少量静态线段。
**不适合**:固定画序、多层像素风、自遮挡、与维修面板 Sprite 精密咬合的游戏物件级线缆。
### 10.3 ShapesFreya Holmér 矢量库)
**工作方式**:
1. **组件模式**:`Polyline``Line` 等挂在物体上(内部也生成 Mesh)。
2. **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 精密咬合、多层像素风、自遮挡、外观恒定
→ 自定义 MeshCableMeshRenderer
```
---
## 11. 调试建议
- 播放 `PeipeiFixScene`,展开维修面板,观察 `Free``Dragging``Plugged` 过渡
-`constraintIterations``bendSmooth` 看刚性/柔软度
-`feedRate` / `retractRate` 看送收线节奏
- 自遮挡:让绳下垂重叠,检查尾段是否正确盖在头段上
- 编译后看 Console`PhysicCable` 不依赖 `PhysicLineSegment`,后者挂在同一物体上可视为遗留
---
*与 [线缆物理移植分析与方案.md](./线缆物理移植分析与方案.md) 互补:该文档偏「为何要换 Mesh、与原型差在哪」;本文偏「当前代码怎么跑」。*