From b0ece3e0f11669b50ce1a5200e41efe80a5a2087 Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Thu, 9 Jul 2026 14:18:47 +0800 Subject: [PATCH] =?UTF-8?q?docs(fixsystem):=20=E8=A1=A5=E5=85=85=20PhysicC?= =?UTF-8?q?able=20=E5=AE=9E=E7=8E=B0=E8=AF=B4=E6=98=8E=E4=B8=8E=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E6=96=B9=E6=A1=88=E5=AF=B9=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- Docs/FixSystem/PhysicCable实现说明.md | 433 ++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 Docs/FixSystem/PhysicCable实现说明.md diff --git a/Docs/FixSystem/PhysicCable实现说明.md b/Docs/FixSystem/PhysicCable实现说明.md new file mode 100644 index 000000000..3e66091f2 --- /dev/null +++ b/Docs/FixSystem/PhysicCable实现说明.md @@ -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 Shapes(Freya 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 精密咬合、多层像素风、自遮挡、外观恒定 + → 自定义 Mesh(CableMeshRenderer) +``` + +--- + +## 11. 调试建议 + +- 播放 `PeipeiFixScene`,展开维修面板,观察 `Free` → `Dragging` → `Plugged` 过渡 +- 调 `constraintIterations`、`bendSmooth` 看刚性/柔软度 +- 调 `feedRate` / `retractRate` 看送收线节奏 +- 自遮挡:让绳下垂重叠,检查尾段是否正确盖在头段上 +- 编译后看 Console:`PhysicCable` 不依赖 `PhysicLineSegment`,后者挂在同一物体上可视为遗留 + +--- + +*与 [线缆物理移植分析与方案.md](./线缆物理移植分析与方案.md) 互补:该文档偏「为何要换 Mesh、与原型差在哪」;本文偏「当前代码怎么跑」。*