做电波效果这件事我前后换过三种做法。最早用粒子系统拼一圈粒子沿着轨迹匀速跑远看像一队小光点排队行军看不出连续的能量感后来改成平面加动画贴图形状是死的镜头换个角度就露馅。真正让我定下来的是Shader 配合 LineRenderer这套组合LineRenderer 负责形Shader 负责光两层各自独立调出问题的范围一下就缩小了。这次要聊的就是这套组合怎么落地——从线段的采样密度、宽度曲线到 UV 怎么流动、能量包怎么跑、混合模式怎么设再到我实际调的时候踩过的几个坑。适合已经会用 Unity 基础组件、想做科幻风格预警线、能量传输、雷达扫描、技能范围指示这类内容的人看不需要你会写完整的光照模型能看懂 HLSL 的加减乘除就够。1. 为什么最后我把电波效果交给了 LineRenderer1.1 粒子、网格、LineRenderer 三条路线的横评要把一条会抖动的光带画出来Unity 里至少有三条路。第一条是粒子系统配 Trails 模块优点是现成的、参数多缺点是粒子的尺寸是固定的想要连续光带就得靠大量粒子重叠overdraw 直接起飞而且粒子的切线方向要靠外力控制波形完全不由你说了算。第二条是自己生成带状网格顶点和 UV 全手写可控性最强但动态改段数的时候要重建 Mesh索引缓冲一改就有开销代码量也翻倍。第三条就是 LineRenderer它把沿折线生成带状四边形这件事内置了位置数组一改网格就跟着变UV 也自动铺好剩下的只需要你自己写材质。我实际对比下来LineRenderer 的甜点区非常明确单条连续折线、需要每帧变形、宽度沿路径渐变。这三条它全占。它的短板也很清楚——一个 LineRenderer 只能画一条连续路径做不了分叉截面的形状不能自定义只能靠宽度曲线控制粗细它生成的是面向相机的四边形带没法做真正的管状体积。所以如果你的电波是能量沿着一条管道跑这种需要体积感的还是得回到自建网格。但对于绝大多数平面或近似平面的表现形式LineRenderer 的投入产出比高得离谱。下面这张表是我自己在选型时列的供参考维度粒子系统 Trails自建带状 MeshLineRenderer 自定义 Shader形状可控性低波形靠模拟极高顶点随你写高位置数组直接改连续能量感差需要大量重叠好好段数动态调整无所谓需重建 Mesh有开销改 positionCount 即可UV 自动生成无需手写内置四种模式移动端表现overdraw 高可控可控顶点数决定代码量少多少1.2 把效果拆成几何和光感两层这一条是我调了很久才想明白的电波效果不好看八成不是 Shader 的问题是几何的问题。所以我在调参时会把两层彻底分开看。几何层管的是位置数组、宽度曲线、段数、拐角圆滑度它决定轮廓长什么样光感层管的是 UV 流动、颜色渐变、混合模式、噪声它决定这条线看起来是不是有能量。分开之后调参路径就清楚了。如果轮廓不对别动 Shader先把材质换成纯白无光照的默认材质跑一遍看白模的波形、宽度、末端收口是不是你要的样子。白模顺眼了再上 Shader这时候如果再觉得丑问题一定在光感层范围就缩小了。反过来如果白模就已经打结、锯齿、忽粗忽细那你在 Shader 里加多少噪声都是白搭。还有一个特别容易被忽略的点LineRenderer 的顶点色会写进 COLOR 语义。它的 colorGradient 是沿着线的长度方向采样的这意味着你可以直接在 Inspector 里拖一个从亮到透明的渐变Shader 里读i.color.a就能拿到长度方向的衰减完全不用在 Shader 里算归一化距离。这个特性我一开始没用非要在 Shader 里用 UV.x 做衰减结果纹理一改密度、衰减位置就跟着变白折腾了两天。2. LineRenderer 里那几个真正决定观感的参数2.1 Texture Mode 的四种模式与宽度曲线的联动LineRenderer 的textureMode有四个值Stretch、Tile、DistributePerSegment、RepeatPerSegment。这四项直接决定 UV.x 的分布而 UV.x 又是你 Shader 里所有沿长度方向效果的基础所以必须先搞清楚。Stretch 是把整条线映射到 0 到 1不管线多长UV.x 永远从头 0 到尾 1。好处是沿长度归一化的效果天然成立比如从头到尾的渐隐、从头到尾的颜色过渡。坏处是线一被拉长纹理就被拉伸你贴一条流动条纹上去条纹会被越拉越宽。Tile 是按世界长度重复UV.x 等于世界距离乘以材质的 mainTextureScale.x纹理密度和物理长度挂钩线拉长纹理不会被拉长但代价是 UV.x 会超过 1而且线长变化时密度会跟着变。DistributePerSegment 是按段均分每一段的 UV 覆盖固定区间密度稳定但曲线在段与段之间的疏密变化剧烈时纹理会被挤压。RepeatPerSegment 是每段都重复一遍完整纹理适合那种每一小段都要有一个节点标记的效果做电波基本用不上。我的做法是主能量包用 Tile 模式因为能量包的间距应该跟物理距离挂钩看起来才像是在真实空间里传播尾部的整体渐隐交给 colorGradient因为那是归一化的改线长不影响。这两者配合才能既有物理感又有整体的收口。宽度曲线这边有个细节widthMultiplier乘以曲线值得到的是总宽度也就是说曲线的值等于你要的宽度不是半宽。我之前一直以为曲线值 1 就是左右各 1 米结果线比预期细了一半排查了半天以为是 Shader 的 alpha 写错了。另外电波的末端我建议不要收到 0留个 0.1 到 0.2 的残值收到 0 会留一个针尖一样的头看起来像是断裂而不是消散。2.2 Alignment 在三维场景里的宽度错觉alignment决定线段朝向哪。LineAlignment.View是默认值每条线段都转向相机好处是无论相机怎么转线都不会消失坏处是相机一动整条线像是在跟着转贴着镜头的贴纸感很强。LineAlignment.TransformZ是让线段平躺在物体的本地 XY 平面上朝向本地 Z 轴这个在纯 2D 或者正交相机下是最稳的但在透视相机下斜着看会看到侧面变薄。做电波我有一个经验判断如果这条线是铺在地面上的预警圈用 TransformZ因为地面是平的方向固定如果这条线是漂浮在空中的能量弧用 View因为弧形在三维空间里扭曲用固定朝向会露出背面。另外用 View 的时候要注意透视相机的近大远小——宽度是按世界单位算的远处的光带在屏幕上会明显变细如果希望屏幕上宽度一致得自己在 Shader 里补偿或者干脆换成正交相机做。Corner Vertices 和 Cap Vertices 这两个也要提一句。线段在拐角处是两段分开的四边形拼起来的拐得急就会看到缺口或者亮度断层。Corner Vertices 设 2 到 4 就能把拐角补圆但每个拐角都会多出顶点。电波这种柔和的形态我一般 Corner 设 3Cap 设 2末端封口不留空心。2.3 Position Count 的预算和每帧更新方式段数这件事先给一个采样判据每个波长至少 8 个采样点低于这个数正弦就会被采样成折线肉眼能直接看出锯齿。所以如果线长 10 米、波长 2 米一共 5 个完整波形至少要 40 段实际我会给到 64 到 128。想再密一点就往上加但边际收益很快就没有了。性能上N 个点大概生成 2N 个顶点圆角封口会更多一屏幕 20 条线 × 128 点也就是 5000 个出头的顶点这在移动端完全不是瓶颈。真正的瓶颈是每帧修改 positionCount因为那会重建内部缓冲。所以我的原则是只有在段数真的变了才设置 positionCount其余时候只调SetPositions。更新方式有三种SetPosition(index, pos)逐点设置调用开销按点算SetPositions(Vector3[])一次性传数组数组可以复用positions属性每帧读会分配新数组绝对不能放在 Update 里。我用的都是第二种声明一个成员数组长度跟段数绑定只在段数变化时重建。这个细节在 PC 上看不出来在手机上跑久了 GC 一抖就是一次卡顿。using UnityEngine; using UnityEngine.Rendering; [RequireComponent(typeof(LineRenderer))] public class WaveLineBuilder : MonoBehaviour { [Header(采样)] [Range(16, 512)] public int segments 128; public float length 10f; [Header(波形)] public float amplitude 0.6f; public float wavelength 2f; public float speed 4f; [Range(0f, 4f)] public float decay 0.6f; private LineRenderer lr; private Vector3[] buffer; void Awake() { lr GetComponentLineRenderer(); lr.useWorldSpace false; lr.textureMode LineTextureMode.Tile; lr.alignment LineAlignment.TransformZ; lr.shadowCastingMode ShadowCastingMode.Off; lr.receiveShadows false; EnsureBuffer(); } void EnsureBuffer() { if (buffer ! null buffer.Length segments 1) return; buffer new Vector3[segments 1]; lr.positionCount segments 1; } void Update() { EnsureBuffer(); // 用绝对时间做相位避免 deltaTime 累积误差 float basePhase Time.time * speed; for (int i 0; i segments; i) { float t (float)i / segments; float z t * length; float env Mathf.Pow(1f - t, decay); float phase t * length / wavelength * Mathf.PI * 2f basePhase; buffer[i] new Vector3(Mathf.Sin(phase) * amplitude * env, 0f, z); } lr.SetPositions(buffer); } }3. 波形得靠代码长出来贴图骗不了人3.1 正弦波形的参数化每个数字对应什么观感很多人做电波的第一反应是找一张波纹贴图贴上去这样做的结果是形状固定、只能靠 UV 滚动来假装在动一旦镜头绕着它转或者把它拉长就完全穿帮。真正的电波轮廓本身必须是每帧重新算出来的。而算轮廓最省事的模型就是带包络的正弦。我的参数化是这样组织的沿本地 Z 轴均匀取点横向偏移由sin(t × 线长 / 波长 × 2π 相位)决定再乘一个包络函数。相位里加上Time.time × 速度整条波就沿 Z 轴推着走了。四个参数的分工很明确参数作用调过头的表现amplitude 振幅横向摆动的幅度太大会自交打结太小像一条直线wavelength 波长一个完整波形的长度太短需要极高段数否则锯齿speed 速度相位推进的速率太快会变成高频抖动失去传播感decay 衰减指数包络从起点到尾部的收缩速度太大整条线只剩前一截有波动振幅和波长的比值是关键。我的经验是振幅除以波长不要超过 0.15超过之后曲率半径会小于线宽的一半LineRenderer 生成的四边形在弯得急的地方会自交视觉上表现为局部亮得发白或者出现一个硬折角。这个比例也直接决定了波看起来是轻微颤抖还是疯狂摆动跟参数的含义是统一的。包络我习惯用pow(1 - t, decay)t 是从 0 到 1 的归一化参数起点处包络是 1终点处是 0。decay设 0.5 的时候衰减很缓整条线都在动设 2 的时候只有前半段有明显波动后半段几乎贴着中线看起来像是波刚刚从起点发射出去还没传到后面。这两种观感对应完全不同的场景持续扫描用前者一次性发射用后者。3.2 环形、螺旋和沿路径传播直线正弦只能解决一条线的问题。实际项目里更常见的是圆形预警圈和沿管线传播的能量。环形电波用极坐标改一下就行θ从 0 到 2π半径r R A × sin(θ × n φ)再用cos θ和sin θ转回 XZ 平面。这里的n是圈上总的波数必须是整数否则首尾接不上会看到一个明显的错位接缝——这个坑我在第一次做环形的时候踩得很实在盯着接缝看了半小时才想起来要取整。螺旋就是把 Y 也加上一个线性项y t × height剩下的跟环形一样。这玩意儿在场景里当上升能量柱特别好用配上从下往上的颜色渐变和流动条纹能做出很唬人的效果实际代码就是多了三行。沿路径传播稍微复杂一点先给一条基准路径比如一串控制点连成的折线或者贝塞尔曲线沿路径等距采样得到基准点和切线再求法线方向最后在法线方向上加正弦偏移。这里的坑在于法线的求法——用相邻两点差分得到切线之后跟一个固定的上方向做叉乘得到副法线当切线恰好跟上方向平行时叉乘会退化成一个零向量波形会突然塌掉。标准的处理是判断切线跟上方向的点积接近 ±1 的时候换一个备用上方向。这个细节不处理在某些角度上就会看到波形瞬间消失一帧非常难受。3.3 段数分配的策略段数不是越多越好但也不能一刀切。我习惯按每个波长 10 段来算再给一个下限和一个上限。比如线长 20 米、波长 1 米、20 个完整波形那理论需要 200 段如果线拉到 100 米按比例就要 1000 段这显然不现实。所以实际项目里我会给段数设一个上限比如 256超过上限就自动放大波长让波形变稀疏而不是让采样变粗糙。还有一个变通的做法把采样密度按 t 做非线性分布。靠近起点的地方密集靠近尾部的地方稀疏因为远处的波本来就因为衰减而不明显稀疏采样看不出来。这需要用 t 的映射函数先算出一个非均匀参数再算相位实现上就是在循环里加一行t t * t或者用一个自定义曲线。这个优化在单条线上意义不大但如果你一屏幕要开几十条线能省下不少。4. 一个能干活的电波 Shader 怎么写4.1 用顶点色拿长度衰减别在 Shader 里算前面提过 LineRenderer 会把 colorGradient 写进顶点色这是一个被严重低估的能力。渐变是在 Inspector 里可视化编辑的你可以随手拖一个前段全亮、后段渐隐的渐变或者做一个三段式的明暗明完全不用改 Shader。Shader 里只需要energy * i.color.a一行长度方向的任何分布都交给美术调。要注意的是别把这个衰减乘两次。有些内置 Shader 的模板里已经有顶点色乘法了你自己再乘一遍就会变成平方亮度曲线会比你预期的陡很多。我一般是新建一个空的 Shader 从头写避免继承到不认识的逻辑。另外 colorGradient 的采样是按线段的索引插值的不是按世界距离。所以当段数不均匀的时候渐变的分布也会跟着不均匀。这在电波这种视觉为王的场景里其实无所谓但如果你要做每隔固定米数灭一段这种精确效果就得自己在 Shader 里用 UV.x 算。4.2 横向能量分布和纵向流动的乘法关系电波的光感本质上是两个方向的一维函数相乘。横向UV.y也就是线的宽度方向是一个中间亮两边暗的钟形分布纵向UV.x也就是线的长度方向是一串流动的亮包。这两个相乘就得到了一条光带上一串能量包在跑的效果。横向的钟形很简单edge 1 - abs(v - 0.5) * 2v 等于 0.5 时是 1两端是 0。然后用pow(edge, 边缘锐度)来控制光带的胖瘦——指数越大光越集中在中间那一丝上看起来像激光指数越小整条线均匀发光像霓虹灯管。我一般用 1.5 到 2.5 之间。纵向的亮包用pow(abs(sin(u × 亮包数量 - 时间 × 流速)), 锐度)。取绝对值是因为我们要的是明暗明暗的交替而不是正负交替取幂是为了让亮包尖锐、暗区宽广。这个式子里本可以做得更细用smoothstep加两个阈值可以做出更硬的边界但幂函数在移动端上更便宜而且观感上已经足够。再叠一层高频扫描线0.5 0.5 × sin(v × 密度 时间 × 速度)这是沿着宽度方向的高频条纹模拟全息投影的扫描质感。注意它的频率是 v 方向的跟长度方向的亮包是垂直的两者叠加会出现十字形的细节纹理很讨喜。扫描线会轻微吃掉整体的亮度所以记得在扫描项的 lerp 范围里留一个下限比如lerp(0.88, 1.0, scan)。Shader Wave/EnergyLine { Properties { _ColorCore (Core Color, Color) (0.55, 0.95, 1.0, 1) _ColorEdge (Edge Color, Color) (0.05, 0.25, 0.9, 1) _Intensity (Intensity, Float) 2.0 _EdgePower (Edge Power, Range(0.5, 6)) 1.8 _PulseCount (Pulse Count, Float) 3.0 _ScrollSpeed (Scroll Speed, Float) 2.0 _PulseSharp (Pulse Sharpness, Range(1, 16)) 5.0 _ScanDensity (Scan Density, Float) 30.0 _ScanSpeed (Scan Speed, Float) 6.0 _NoiseStrength (Noise Strength, Range(0, 0.5)) 0.12 } SubShader { Tags { Queue Transparent RenderType Transparent IgnoreProjector True PreviewType Plane } Blend One One ZWrite Off Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc fixed4 _ColorCore; fixed4 _ColorEdge; float _Intensity; float _EdgePower; float _PulseCount; float _ScrollSpeed; float _PulseSharp; float _ScanDensity; float _ScanSpeed; float _NoiseStrength; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; fixed4 color : COLOR; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; fixed4 color : COLOR; }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.uv; o.color v.color; return o; } float Hash21(float2 p) { p frac(p * float2(123.34, 456.21)); p dot(p, p 45.32); return frac(p.x * p.y); } fixed4 frag (v2f i) : SV_Target { float u i.uv.x; float v i.uv.y; // 横向能量分布中心亮、两侧暗 float edge saturate(1.0 - abs(v - 0.5) * 2.0); float core pow(edge, _EdgePower); // 沿长度方向流动的能量包 float flow u * _PulseCount - _Time.y * _ScrollSpeed; float band pow(abs(sin(flow * 3.14159265)), _PulseSharp); // 高频扫描线模拟全息质感 float scan 0.5 0.5 * sin(v * _ScanDensity _Time.y * _ScanSpeed); // 噪声打散规则感 float n Hash21(float2(floor(u * 80.0), floor(_Time.y * 24.0))); float noiseTerm lerp(1.0 - _NoiseStrength, 1.0 _NoiseStrength, n); float energy core * lerp(0.45, 1.0, band); energy * lerp(0.88, 1.0, scan); energy * noiseTerm; energy * i.color.a; fixed3 col lerp(_ColorEdge.rgb, _ColorCore.rgb, core); col * _Intensity; return fixed4(col * energy, energy); } ENDCG } } }4.3 混合模式和渲染队列的选择逻辑电波几乎都是加色混合。Blend One One是纯粹的加色颜色直接叠加Blend SrcAlpha One会让 alpha 参与输出更柔和但多乘了一道 alpha。两者的观感差异在亮的地方特别明显纯加色在多层重叠时会快速饱和到白色而带 alpha 的版本会慢一些。想要越亮越白的激光感就用前者想要柔和晕染就用后者。我自己调的时候是先用 One One 做基础觉得层次不够再换。ZWrite Off是必须的否则同一条线的多层四边形会互相遮挡出现硬边和黑缝。Cull Off是为了防止某些角度看过去线消失因为 LineRenderer 生成的四边形法线方向不一定稳定尤其 View 模式下相机一转朝向就可能反过来。队列用 Transparent 默认的 3000 就够。如果电波需要出现在某些透明特效前面把队列调到 3001 到 3010 之间反之调到 2990。这个数值不要乱设得太大否则会跟粒子系统的默认队列产生难以预料的前后关系。还有一点加色混合的物体彼此之间没有顺序问题加法交换律所以只有在电波和半透明物体混合的时候才需要操心队列。4.4 双层结构和噪声扰动的取舍单条 LineRenderer 配一个 Shader做出来的光带是一个亮度分布。想要真正的辉光感最省事的做法是叠两条 LineRenderer一条宽而暗颜色偏冷负责光晕一条窄而亮颜色偏白负责核心。两条共用同一份位置数据只是 widthMultiplier 和材质不同。这比在 Shader 里堆多级幂函数更可控也更接近真实光学的多层散射。代价是两份顶点数据和两次 Draw Call。我的判断标准是如果场景里电波数量在 5 条以内双层随便用超过 10 条就要开始算账了因为 LineRenderer 本身很难批处理每条都是独立的 Draw Call。这时候要么回到单层然后用亮度倍增挡一下要么把光晕那层换成贴片。噪声这块我建议保守一点。Hash21这种基于 sin 和 frac 的哈希在移动端上不贵但每个像素都算一次仍然有代价。而且噪声强度只要超过 0.2就会出现明显的脏看起来像是贴图分辨率不够而不是有意为之。我用的强度是 0.1 左右只是用来打散扫描线的规则感肉眼几乎看不出噪点但能消除那种完美数学公式的假感。5. 实测踩坑从光带压扁到参数串台5.1 弯道处光带打结的完整排查链路这个现象第一次出现的时候我以为是 Shader 的问题波形摆到最大的地方光带会出现一块特别白的区域形状像个小小的菱形其他地方都正常。排查过程我记一下因为思路比结论有用。第一步把材质换成纯白无光照材质。白块还在说明不是 Shader 的亮度计算问题是几何问题。第二步把振幅调到 0白块消失确认跟波形幅度相关。第三步把线宽减半白块变小但还在说明是几何自交而不是单纯的亮度过高。第四步把段数从 64 加到 512白块还在只是边缘稍微平滑了点——这一步很关键它证明了增加段数不能解决曲率问题因为曲率是几何属性离散化只是近似它。结论就是前面说的曲率半径小于线宽一半的时候LineRenderer 生成的内圈四边形会翻转重叠区域因为加色混合就变成了亮块。解决办法只有三个按优先级排减小振幅、增大波长、减小线宽。我最后的取值是振幅 0.6、波长 4、线宽 0.35振幅波长的比值 0.15曲率半径大约是线宽的三倍安全。顺带说一个相关的现象如果波形在某个区间变化特别剧烈比如你给包络加了一个突变的曲线即使不自交采样点之间也会出现明显的折角。这种时候要么提高那一带的采样密度要么让参数变化本身平滑一点别用阶梯函数。5.2 多条电波参数串台的两种成因场景里放了两条电波A 的速度改了B 也跟着变。这个问题有两个完全不同的成因得先分清。第一种是材质共享。LineRenderer.material会返回一个实例化的材质但如果你是通过sharedMaterial拿到的或者你在编辑器里给两个 LineRenderer 拖了同一个材质资源那 Shader 里的属性就是共享的。用material.SetFloat改一个另一个也会变因为改的其实是同一个材质实例。第二种是 Shader 里用了全局量。_Time是全局的这一点没问题但如果你的 Shader 里有自己定义的全局参数比如通过Shader.SetGlobalFloat设置的那所有用到这个 Shader 的物体都会受影响。正确的解法是MaterialPropertyBlock。它为每个 Renderer 单独存一份属性覆盖不产生材质实例也不会污染共享材质private MaterialPropertyBlock mpb; private static readonly int ScrollSpeedId Shader.PropertyToID(_ScrollSpeed); private static readonly int IntensityId Shader.PropertyToID(_Intensity); void SetWaveParams(float speed, float intensity) { if (mpb null) mpb new MaterialPropertyBlock(); var r GetComponentRenderer(); r.GetPropertyBlock(mpb); mpb.SetFloat(ScrollSpeedId, speed); mpb.SetFloat(IntensityId, intensity); r.SetPropertyBlock(mpb); }注意Shader.PropertyToID要缓存成静态字段别每次调用都拼字符串。另外这个方案在用了 SRP Batcher 的渲染管线下会打断批处理但 LineRenderer 本来就不太能批所以影响不大。如果你的项目对批处理极其敏感那就老老实实给每条线克隆材质然后在OnDestroy里Destroy(material)别让它泄漏。5.3 低端机上的相位抖动和精度衰减还有一个坑更隐蔽运行久了之后波形的流动开始一卡一卡的重启之后又好了。这是浮点精度的问题。Time.time是 float有效位数有限运行到几十分钟之后小数部分的精度就不够了Time.time × speed做正弦的时候输入值的小数部分被吃掉波形就会跳。我在 C# 侧的处理是把相位做模运算超过一个周期就归零float raw Time.time * speed; float phase raw % (Mathf.PI * 2f);Shader 侧同理。_Time.y也是 float长时间运行之后_Time.y × _ScrollSpeed的有效精度下降流动条纹会开始抖动。解决方式是在 C# 里算一个循环的时间通过 MaterialPropertyBlock 传给 Shader超过一定值就减掉一个整数倍的周期视觉上看不出任何变化但精度保住了。另外低端机上还有一个Mathf.Sin的调用次数问题。128 段 × 每帧一次 sin加上包络的 pow在手机上其实还好但如果你一屏幕开了 30 条线就是接近 4000 次三角函数这时候可以考虑用查表或者用预计算的正弦近似。我的做法是给段数设一个按平台的降级表PC 给 256中端手机 96低端 48通过Application.platform或者画质等级来判断这个降级几乎不影响观感因为 48 段在屏幕上看起来只是稍微硬了一点点。6. 把电波做成可复用的组件结构6.1 一个控制器管全套参数做到第二个项目的时候我就把之前散落的代码收成了三层。第一层是波形构建器只负责算位置把Vector3[]填好别的什么都不管。第二层是参数控制器负责速度、亮度、颜色的整体节奏比如电波从启动到最强用 0.3 秒然后匀速这一层是在做动画曲线。第三层是 Shader 参数绑定负责把控制器的输出通过 MaterialPropertyBlock 送进 Shader。这样分层的好处是当策划说能不能让电波发射的时候先顿一下再冲出去我只需要改第二层的时间曲线波形和 Shader 一行都不用动。反过来如果要做波形换成方的只需要改第一层的采样函数另外两层完全不受影响。实际组织的时候我会把波形函数做成一个委托或者虚方法让子类去实现直线、环形、螺旋等不同形态共用的采样逻辑、缓冲复用、段数管理都放在基类里。这样新增一种形态的成本就是写十行数学公式。6.2 性能分级和可见性剔除最后说一个容易被忘掉的优化不可见的时候别更新。LineRenderer 本身有 Renderer可以用OnBecameVisible和OnBecameInvisible来开关Update里的计算。这个在场景里电波很多、镜头视野有限的时候效果明显——我做过一个测试20 条电波里同屏最多 6 条加上剔除之后 CPU 侧的计算开销直接降了一半多。分级这块我的做法是给组件加一个 quality
阅读完成 · 觉得有帮助?