Files
aibis-dream/Docs/FixSystem/PhysicCable实现说明.md
T

17 KiB
Raw Blame History

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 每帧模拟流程(FixedUpdateSimulate

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;
场景 速率参数
需要更长 feedRate0.45
末端固定但过长 holdRate0.06
闲置松手 retractRate0.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(吸附)后,再对 _dragTargetlerp * 0.3,并按当前绳长钳制最大半径,避免拽满时末端抖动。

5.2 插孔吸附

SetDragAssistTarget(dock, radius):指针靠近插孔时,混合向孔心偏移(socketAssistStrength + 平滑 blend)。

5.3 拔线阻力

StartUnplugResistance:从已插状态按下插头时,指针被「粘」在孔心;拉出距离超过 unplugBreakDistance 后释放,并 KickTail 给尾段冲量。

5.4 插入冲击

BeginInsertImpact:插入瞬间末段前推(insertShoveDistance+ 横向抖动(insertShakeAmplitude+ 尾段 kickinsertTailKick)。

5.5 插头朝向

UpdatePlugAngle

  • 绳拉紧(taut > 0.92):沿「出线口 → 末端」直线取向
  • 松弛:沿末端往回走 plugRefArcLength 弧长取参考点方向
  • 每帧限速转角(plugTurnRateplugMaxTurnStep

ApplyPlugPose(plug, plugRoot):把插头摆到绳末端并按 _plugAngle 旋转。由 CablePanel.LateUpdate 在渲染后调用,与 PhysicCable.LateUpdate 读同一份点位,避免线头分离。


6. 渲染:CableMeshRenderer

6.1 为何不用 LineRenderer

LineRenderer 已禁用(Awakeenabled = false)。多层描边、固定画序、顶光偏移、自遮挡等需求无法用多条 LineRenderer + sorting order 稳定复现(详见移植分析文档)。

6.2 渲染流程(LateUpdateRender

物理点 _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 插接圆头

PluggedroundEndCap = true:末端用半圆端帽(CapSegments 细分),视觉上像插进孔里。

6.6 排序

  • 线缆 Mesh:Tools 层 / sortingOrder = 47
  • 插头 Sprite:Tools 层 / sortingOrder = 48(永远画在线上方)
  • 收起时插头压到线盘下方(sortingOrder = 41

7. 上层协作(CablePanel

7.1 生命周期

  • Start:停用旧 Cable;按 isCableRetracted 初始化 HideShowFree
  • LateUpdateFree / DraggingApplyPlugPose + 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
坐标 顶点直接用世界 XYZ 压平为 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 模拟像素风分带(阴影/描边/主体/高光),遇到的问题(详见移植分析文档:

问题 原因
层之间对不齐 各线独立圆角/端帽设置不一致
无法做「顶光上移」 只能同心变窄,无法 shadow 下移、mid 上移
排序脆弱 4 个 Renderer 需同步 order;外部在 37~40 层插入物体会「插进线缆内部」
自遮挡难做 尾段盖头段需再画一遍 → 8 条线
拖拽时线/插头分层 线与插头分属不同 sorting 层,一根线两端遮挡关系无法表达
外观随灯光漂移 共用 Sprite-Lit,受 CablePanelLight 呼吸灯影响

适合:简单连线、调试线、少量静态线段。
不适合:固定画序、多层像素风、自遮挡、与维修面板 Sprite 精密咬合的游戏物件级线缆。

10.3 ShapesFreya Holmér 矢量库)

工作方式:

  1. 组件模式:PolylineLine 等挂在物体上(内部也生成 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 自定义 MeshCableMeshRenderer

工作方式:折线点 → 沿法线挤出 ribbon → 顶点色上色 → 按顺序 append 三角形到同一 Mesh。

与「画 4 条线」的本质区别:往同一个 Mesh 里塞 4 层几何,而非 4 个 Renderer / 4 次 Draw。

能力 实现
固定画序 三角形提交顺序 = 遮挡关系
单层排序控制 对外仅 1 个 MeshRenderer.sortingOrder
分带垂直偏移 band.liftshadow 下移、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,展开维修面板,观察 FreeDraggingPlugged 过渡
  • constraintIterationsbendSmooth 看刚性/柔软度
  • feedRate / retractRate 看送收线节奏
  • 自遮挡:让绳下垂重叠,检查尾段是否正确盖在头段上
  • 编译后看 ConsolePhysicCable 不依赖 PhysicLineSegment,后者挂在同一物体上可视为遗留

线缆物理移植分析与方案.md 互补:该文档偏「为何要换 Mesh、与原型差在哪」;本文偏「当前代码怎么跑」。