做Niagara粒子最有成就感的一刻,是看着自己做出来的导弹划出一道弧线,死死咬住目标,然后在命中的瞬间炸开。前两期我们聊了Niagara的发射器搭建、Sprite渲染和基础拖尾,这期直接上实战:十分钟做一个导弹追踪效果。默认你已经知道怎么创建Niagara System和Emitter、怎么把模块拖进堆栈,如果这些还不熟,建议先回头过一遍前两期的基础内容。
所谓“导弹追踪”,往大里说可以做成导弹、魔法飞弹、跟踪榴弹,往小里说其实就是一件事:粒子每帧都根据目标位置修正自己的飞行方向。这期我会直接从思路开始,把场景参数、发射初始化、逐帧转向、拖尾渲染、命中爆炸完整走一遍,最后把最常见的绕圈、抖动、多目标串线这些坑也一并说清。
1. 追踪效果的本质:让粒子自己决定下一步往哪飞
1.1 直线飞行和追踪飞行的区别
直线飞行太简单了:粒子出生时给一个初速度,后面什么都不用管,它就会沿着直线一直飞。比如默认的Initialize Particle里设置一个Velocity (1000, 0, 0),粒子就会朝X轴方向匀速运动。
追踪效果跟直线飞行的本质区别在于:粒子的速度方向需要每一帧都根据“目标位置”重新计算。也就是说,粒子不能闷着头往前冲,必须持续感知目标在哪,然后调整自己的朝向。听起来像是有“思想”了,但实际上就是一套非常朴素的反馈循环:
每一帧做三件事:
- 拿到粒子的当前位置
- 拿到目标位置(外部传入)
- 计算这两点之间的方向向量,把粒子的速度往这个方向修正
这个循环只要每帧都在跑,粒子就会呈现出“追”的感觉。关键在于“修正”的幅度——修正太快,轨迹会很僵硬;修正太慢,导弹会飞过目标,在目标周围画圈。
1.2 三种常见追踪思路怎么选
我在项目里实际试过几种实现思路,各有取舍,先拉一张表对比:
| 方案 | 实现方式 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| User参数+自定义更新 | 蓝图每帧把目标坐标写入Niagara用户参数,粒子更新时读取并修正速度 | 简单直接,跨网络同步也方便 | 需要外部持续传入目标坐标 | 单目标、多目标的导弹追踪 |
| Object Reader | Niagara直接读取场景中Actor的Transform | 不需要每帧手动传参,目标自己“可见” | 性能开销相对大,只适合CPU模拟,且需要设置好读取通道 | 目标类型固定且数量较少的场景 |
| 碰撞/场景查询 | 通过射线、碰撞事件检测最近目标 | 命中检测天然精准 | 做追踪循环比较复杂,不太适合做平滑转向 | 导弹爆炸检测、命中反馈 |
这期我推荐第一种方案,也就是用User参数传目标坐标。原因是它最直观,也最容易排查问题:你打开Niagara的Debug面板,能看到User.TargetLocation到底变了没有。一旦导弹不追目标,判断起来很快。
1.3 为什么不在Niagara内部直接找目标
有人可能会问:Niagara不是有Object Reader吗?把场景里的目标Actor拖进去,不就能直接在粒子系统里读位置了吗?
理论上可以,但实际用起来有几个麻烦:
- Object Reader依赖目标Actor的可见性和可读通道,万一目标被隐藏、销毁或者不在视口范围内,数据可能读不到
- 它只能配合CPU模拟使用,GPU模拟下很多Data Interface的读取限制很大
- 你把目标“焊死”在粒子系统里,后期想换成多目标轮流追踪,反而不好改
所以除非你的目标就是固定一两个并且永不销毁,否则老老实实用User参数更稳妥。这个在多人联机或者网络同步场景下尤其重要——服务器给客户端传目标坐标,比客户端自己去场景里找Actor更可控。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 场景准备:目标坐标怎么传进Niagara,是我踩过的第一个坑
2.1 先把场景里的目标和发射点摆好
我建议场景里放两个Actor:
- 目标:一个TargetPoint或者一个真正的角色/移动靶子,命名比如
MissileTarget - 发射点:一个明确的空Actor或者场景里的武器挂点,命名比如
MissileSpawn
发射点建议单独用一个Actor,不要偷懒把发射位置直接写死成(0,0,0)。因为后面你可能需要把发射点挂到角色手上、载具机翼下、炮塔口上,单独一个Actor方便随时调整和挂接。
目标则建议用移动的靶子来测试。如果目标站着不动,追踪效果是能跑通,但你看不出追踪逻辑的实时性;目标一旦动起来,才能验证你的转弯算法是不是真的在“追”。
2.2 在Niagara里声明User.TargetLocation
打开你的Niagara System,在System参数面板里新增一个Vector3参数,命名:
text复制User.TargetLocation
命名空间User很关键。Niagara里的参数命名空间不是随便写的,它决定了参数的可见范围。User.命名空间的参数,可以在蓝图里通过Set Niagara Variable直接覆盖,也可以通过Niagara System实例单独设置。如果你把参数建到了Emitter或者Particle命名空间下,蓝图外层访问起来会非常别扭。
2.3 蓝图里每帧把坐标塞进去
在蓝图中拿到Niagara System组件或者Niagara系统实例后,在Tick事件里调用:
text复制Set Niagara Variable (Vector 3D)
变量名: User.TargetLocation
变量值: MissileTarget->GetActorLocation()
如果你用的是Niagara Component(放置在关卡里那种),直接在组件上调用这个函数就行。如果你用的是Spawn System at Location动态生成的实例,记得把返回的NiagaraSystemInstanceHandle存下来,然后通过Set Niagara Variable传参。
这里有个细节:如果是动态生成的实例,必须在Spawn之后立刻传参,否则粒子在初始化的那一帧可能读到默认值(0,0,0),导弹出生瞬间会先朝零点冲一下,再掉头追目标。我一开始就踩过这个,导弹刚出膛的时候总会诡异地抽搐一下,后来发现是出生那一帧User参数还没写进去。
2.4 局部坐标/世界坐标的坑
Niagara系统里有一个很常见的设置:System是Local Space还是World Space。
如果你把System设成了Local Space,那么User.TargetLocation最好也传“相对发射点的局部坐标”,或者你在粒子更新时手动用Transform转换坐标。但如果你的粒子系统直接放在世界空间中,User.TargetLocation直接传世界坐标即可。
我的建议:导弹这种需要在世界空间中飞行和命中目标的效果,统一用World Space。不要混合。否则你会遇到一种玄学:粒子发射出去,方向明明是朝目标的,但飞到一半就偏了,而且偏得毫无规律——大概率就是坐标空间混用了。
3. 导弹出生:Spawn阶段的初始化和发射设置
3.1 发射器选CPU还是GPU
做追踪效果,我优先推荐CPU模拟。原因很简单:追踪逻辑里要访问User.TargetLocation,要在Particle Update里做较多的向量运算和分支判断,CPU模拟的模块支持度和DEBUG友好度都比GPU高很多。GPU模拟适合那种几千上万颗粒子的无脑特效,比如雨、雪、粉尘,不适合这种需要每个粒子都做“大脑计算”的导弹。
发射器创建的时候选Empty,不要选模板。模板带来的额外模块会让你后面排查的时候多很多干扰项。清清爽爽从零搭。
3.2 Burst方式发射导弹
发射方式我建议用Spawn阶段里的Burst Spawn模块,Spawn Count设成1。
为什么是1?因为我们做的是导弹,不是机关枪。一颗导弹就是系统中的一个粒子本体,后续的尾焰、烟雾用子发射器或者额外发射器来做,本体保持简单。
如果你想做“一排导弹齐射”或者“五连发”,可以多生成几个粒子,但要注意:同一发射器下的所有粒子,都共享同一个User.TargetLocation。如果所有导弹追同一个目标,那没问题;如果导弹要分别追不同目标,就不能用同一个发射器里的多个粒子了,这个我在第6章专门讲。
3.3 Initialize Particle模块的初始化参数
在Particle Spawn阶段添加Initialize Particle模块,重点设置这几个属性:
| 属性 | 推荐值 | 说明 |
|---|---|---|
| Lifetime | 5秒 | 给一个稍长的寿命,保证追踪过程不被中断 |
| Position | 发射点位置 | 一般取发射Actor的位置,或者粒子系统组件的位置 |
| Velocity | 从小给一个初速度 | 例如发射方向上的(200, 0, 0),让导弹出生时有个飞行姿态 |
| Sprite Size | 20左右 | 先用一个Sprite调试,之后可以换成Mesh |
| Color | 暖色偏红/橙 | 导弹本体要有发光感 |
还有一点:先把Lifetime给足,调试的时候不要让它中途就自动销毁。等追踪逻辑跑通之后,再回来把Lifetime缩短,配合命中判定处理销毁。
3.4 让导弹“头朝前”:朝向速度的设置
有了速度之后,你可能会发现粒子在飞但“脑袋”不动。Sprite渲染器默认是面向摄像机的(Billboard),所以看不出朝向。如果你用的是Mesh渲染器(比如一个锥体模型代表弹头),那就需要让模型跟随速度方向。
在Mesh Renderer的旋转设置里,勾选“Orient Mesh to Velocity”或者调整Alignment选项,让模型的轴向对准粒子的速度方向。这个细节决定了导弹是“飘”过去的还是“射”过去的,画面感差很多。
4. 转弯逻辑:Update阶段逐帧修正方向,别让导弹原地打转
4.1 模块在堆栈里的顺序会影响结果
Niagara的Particle Update堆栈是从上往下执行的。默认情况下,粒子更新会经过力、阻尼等物理计算,最后才会渲染。如果你想控制速度方向,必须把自定义模块放在速度计算的后面,否则你的修改会被后续的力模块覆盖掉。
一个简单的经验法则:在Particle Update堆栈的底部,新增一个自定义模块,放在Solve Forces and Velocity之后。这样你写进去的速度就是最终生效的速度。
不同Niagara版本里模块名称会有差异,但思路一致:你的转向逻辑,必须是在“所有物理计算完成之后”再执行。
4.2 核心公式:目标方向和速度修正
在自定义模块里,我们需要做这几件事:
- 计算出指向目标的单位向量
- 把当前速度方向往目标方向靠拢
- 对速度大小做控制
用伪代码写出来就是:
text复制ToTarget = User.TargetLocation - Position
Dist = length(ToTarget)
DirToTarget = normalize(ToTarget)
CurrentDir = normalize(Velocity)
Blend = 1 - exp(-TurnSpeed * DeltaTime)
NewDir = normalize(lerp(CurrentDir, DirToTarget, Blend))
Speed = 1200
Velocity = NewDir * Speed
这里TurnSpeed是人为设定的转向强度,数值越大,导弹越“贼”;DeltaTime用来保证帧率不同时转向速度保持一致。
为什么用1 - exp(-TurnSpeed * DeltaTime)而不是直接TurnSpeed * DeltaTime?因为后者在低帧率下可能超过1,导致转向“过冲”。指数形式的修正平滑且永远小于1,相当于做了个隐式的一阶低通滤波,导弹转弯会更顺滑。
4.3 加上转向速率限制,避免导弹抽搐
只做上面这步,你已经能得到一个会转弯的粒子了。但你会发现,如果TurnSpeed设得很大,粒子会在靠近目标的过程中疯狂抖动,看起来像是喝醉了。
原因很简单:你把速度方向直接怼到了目标方向,但目标本身在移动,导致方向向量一直在变,粒子来回修正,视觉上就抖。
解决方式有两个:
- 调低TurnSpeed,让转向更柔和
- 加一个最大角速度限制
最大角速度的意思是:粒子速度方向每帧最多只能转多少度。比如导弹最大转向角是每秒120度,那么你计算NewDir和CurrentDir之间的夹角,如果超过120*DeltaTime,就只转这么多,而不是一步到位。
伪代码可以写成:
text复制AngleBetween = acos(dot(CurrentDir, DirToTarget))
MaxTurn = 150 * DeltaTime // 每秒150度
if AngleBetween > MaxTurn:
// 将CurrentDir朝DirToTarget旋转MaxTurn度
Axis = cross(CurrentDir, DirToTarget)
NewDir = rotateVectorAroundAxis(CurrentDir, Axis, MaxTurn)
else:
NewDir = DirToTarget
这个实现看起来会比简单的lerp多几步,但效果立竿见影。加了最大角速度之后,导弹的轨迹会有一条明显的弧形,而不是像“智能跟踪虫”一样瞬间对准。真实感一下就上来了。
4.4 绕圈问题:为什么导弹总在目标周围画圈
这是追踪效果里最容易翻车的现象。你满怀期待地发射导弹,结果它在目标旁边一圈一圈转,永远不命中。
原因分析起来很简单:导弹的转向能力不够,或者飞行速度太快,导致它到达目标附近时“刹不住车”,冲过头之后再折返,形成圆周运动。
解决办法主要有三种,可以组合使用:
- 降低接近速度:距离目标越近,速度越小。计算目标距离,把它映射成一个0~1之间的因子,距离近时乘上一个较低的系数
- 增大转向能力:提高TurnSpeed和最大角速度,让导弹在近距离能够更激进地修正方向
- 加“预判”:不直接朝向导弹当前帧的目标位置,而是朝向“目标未来位置”——目标当前位置加上目标速度乘以一个提前量
实战中,我用的比较多的是第一种。因为“接近减速”对观感也有好处:导弹快到目标时速度降下来,接着爆炸,观众不会觉得突兀。
我建议把速度设置成由距离控制的曲线:
text复制// 伪代码
Dist = length(User.TargetLocation - Position)
SpeedFactor = clamp(Dist / 800, 0.2, 1.0)
Speed = lerp(MinSpeed, MaxSpeed, SpeedFactor)
意思是:距离800以上全速飞,距离越近速度越低,最低降到20%。这样一个简单的逻辑,就能让导弹稳稳“咬住”目标,不会画圈。
5. 拖尾和火焰:把“会飞的亮点”变成“导弹”的渲染细节
5.1 先让导弹本体有一个发光材质
纯白的小圆点做调试没问题,但你要发出来给别人看,至少得让它像个导弹。我建议先给Sprite做一个简单的发光材质:
- BaseColor用橙色或者暖黄色
- Emissive Color给一个高亮的橙红色
- 开启Soft Particle(软粒子),让粒子和其他几何体相交时边缘羽化
材质做好之后,在Sprite Renderer里指定这个材质。如果你的Niagara版本支持Light Renderer,可以再加一个Point Light,让导弹成为场景里真正发光的光源。不过Light Renderer性能开销不小,如果导弹数量多,建议只保留自发光材质模拟发光,不要真加灯光。
5.2 Ribbon拖尾:让轨迹有“尾巴”
导弹的拖尾用Ribbon渲染器来做最合适。Ribbon会把同一发射器内的历史粒子连接成一条连续的带子,看起来就像导弹拖了一条长长的尾焰或光带。
在发射器上再添加一个Ribbon Renderer,然后设置Ribbon的宽度,建议用“基于粒子的宽度”或随Lifetime缩放的曲线。具体做法:
- Ribbon Width:设一个基础宽度,比如50
- 在粒子更新中,把粒子的Alpha或宽度按照Lifetime归一化后递减,实现“越往尾部越细越透明”
注意Ribbon渲染器对粒子的数量有要求。如果粒子太少,拖尾会变成一段一段的折线。导弹本体只有1个粒子,所以需要配合“历史轨迹”机制,让系统保留一定数量的历史位置;或者用Ribbon渲染器默认的“Trail”功能,让它在粒子运动过程中保留多个历史点。
更稳的方法:拖尾不要和导弹本体放在同一个发射器,而是用独立的发射器专门生成拖尾粒子。
5.3 用子发射器做尾焰和烟雾
这是我最推荐的做法:导弹本体是主发射器,尾焰烟雾是另一个发射器,通过主发射器在Update阶段“定生”出次级粒子。
具体设置:
- 在主发射器的Particle Update里添加Spawn Particles模块
- 每帧生成1~2个次级粒子,位置就在当前粒子的位置
- 次级发射器的粒子上来就给一个很小的反向速度,模拟喷焰向后喷射
- 次级粒子的Lifetime设0.2~0.5秒,Sprite尺寸从大到小,透明度从高到低
这套思路的好处是:导弹本体不需要关心渲染细节,尾焰、烟雾、火花全部由次级粒子承担。以后你想把尾焰从“橙色火团”换成“蓝色粒子”或者“浓烟滚滚”,改次级发射器就行,完全不碰主逻辑。
5.4 CPU和GPU模拟拖尾的性能差异
这里要特别提醒一下:Ribbon渲染器在CPU模拟下表现稳定,因为CPU模拟能提供完整的粒子历史顺序。GPU模拟下Ribbon虽然也能用,但你要想通过模块去对粒子排序、控制条带顺序,限制会多不少。
如果你坚持用GPU模拟做大量导弹齐射,建议拖尾不要用Ribbon,用大量Sprite粒子做烟雾,这样GPU的优势能发挥出来,效果也可以接受。但如果你的目标是“清晰的一根光带”,老老实实用CPU+Ribbon。
6. 命中收场:距离判定、爆炸生成与坐标隔离
6.1 距离判定:别等粒子撞到目标才收场
导弹追踪的最后一步是命中。我们不需要做复杂的物理碰撞,用距离判定就足够了。
在自定义更新模块的尾部,加一个判断:
text复制if Dist < 150:
// 触发命中逻辑
// 生成爆炸
// 销毁自身粒子
当距离小于阈值时,这个粒子就算“命中”。我建议在命中时把粒子的位置直接设置成目标位置,保证爆炸效果出现在目标身上,而不是出现在“距离目标还有150单位”的地方,这个偏差会产生非常严重的穿帮感。
命中后销毁粒子可以用Kill Particles模块,或者直接在自定义模块里将粒子的Lifetime设成一个极小值。后者更柔和,因为Niagara的模块系统会自动把Lifetime耗尽后的粒子移除。
6.2 爆炸效果的生成方式
爆炸我建议用同一个Niagara System里的另一个发射器来做。主发射器命中瞬间,在事件处理器或者简单的条件触发下,让爆炸发射器Burst生成一批粒子。
这里有两个实现思路:
- 使用Niagara的Event机制:主发射器在命中时发送一个Location Event,爆炸发射器订阅这个事件,在事件触发位置生成爆炸粒子
- 更粗暴但好用的方式:在模块里直接“开”一个布尔参数,爆炸发射器的Update模块里检测到这个参数为真,再Burst
Event机制更干净,但调试门槛稍高;粗暴方式适合初学者快速跑通。我个人建议先跑通粗暴版,再考虑优化成Event。
爆炸发射器的粒子不要太多,30~50个Sprite粒子就够。给它们一个随机方向的速度,速度大小从中心向外扩散,再加上0.3~0.6秒的短Lifetime,看起来就是一次干净利落的爆炸。爆炸的颜色建议和导弹尾焰统一色系,避免“导弹橙黄色尾焰,爆炸却是蓝色”的违和感。
6.3 多枚导弹追踪多个目标:参数隔离问题
这是个非常容易踩的坑。我前面强调过,User.TargetLocation是系统级参数,同一个Niagara System实例里的所有粒子共享同一个值。如果你在一个发射器里“Burst 5”生成5颗导弹,它们会全部追向同一个User.TargetLocation。
要让多枚导弹分别追不同目标,有两条路:
- 每颗导弹创建一个独立的Niagara System实例,每个实例单独传User.TargetLocation参数。这种方式最直观,但性能开销稍大,不适合几十发齐射
- 把目标坐标写入粒子属性,而不是系统参数。也就是说,在粒子出生时,通过Spawn阶段把“自己的目标坐标”存储在粒子上一份。这样虽然粒子都在同一个发射器里,但每个粒子的目标坐标不同
第二条路实现起来要借助额外的Int参数和粒子属性复制,复杂度高一些。对小项目来说,我建议直接为每枚导弹Spawn System实例,性能损失几乎可以忽略,而且蓝图逻辑清晰得多。
6.4 发射点和目标都是动态时,追踪系统会怎样
实战里发射点通常是动态的:角色拿着火箭筒旋转、飞机在空中移动。这种情况下,导弹出生位置会跟着发射点移动。如果你的Niagara System放在发射点下方,系统本身位置会变化,可能会导致粒子相对坐标混乱。
我的建议是:使用Spawn System at Location动态生成导弹系统,出生后系统会固定在世界坐标的出生点,发射点继续移动也影响不到已发射的导弹。这样追踪逻辑里的Position、Velocity就全部是稳定的世界坐标,不容易出问题。
7. 调试经验:绕圈、抖动和性能问题这样查
7.1 粒子不动或者胡乱蹦跳,先检查这几项
如果导弹出生后完全不动,优先检查顺序:
- 确认Initialize Particle里给了速度
- 确认Particle Update里的自定义模块不是空内容,也没有被编译器报错
- 确认User.TargetLocation有值,而不是(0,0,0)
如果粒子乱跳,方向毫无规律,多半是坐标空间问题。把System组件的World/Local Space设置和User.TargetLocation的坐标系统一成一致,通常能解决90%这种问题。
7.2 抖动和抖动的区别
我在第4章提到过两种抖动,这里再补充一个排查方法:
- 转向过度导致的抖动,整体轨迹呈锯齿状,把TurnSpeed调低就能缓解
- 帧率不稳定导致的抖动,在高速飞行时最明显,检查DeltaTime的修正是否正确。如果模块里的公式没有乘DeltaTime,低帧率下转向幅度会被放大
还可以加一个阻尼系数:对速度向量的变化做平滑处理,比如:
text复制Velocity = lerp(Velocity, DesiredVelocity, 0.2)
这种方式副作用是会让导弹的反应变“肉”,但能明显降低抖动感。如果调好后手感温吞吞的,就把阻尼系数从0.2改成0.5,找到一个“能追上目标又不抖”的临界点。
7.3 性能:粒子少不等于性能好
追踪效果里粒子本体只有1个,但拖尾、烟雾、爆炸的粒子数量会快速膨胀。一个常见的性能误区是“我只发射一枚导弹,粒子数很少,应该很轻量”。
实际上,Ribbon拖尾会保留大量历史顶点,烟雾发射器如果是Spawning Per Second连续生成,几十秒下来粒子数会非常可观。我的建议是:
- 给初级尾焰粒子设置明确的Lifetime,0.3~0.5秒,不给太长
- 爆炸粒子数量控制在50以内
- 如果不追求极致,把烟雾发射器的生成速率降到每帧0.5以下,甚至每两帧生成一个
- 命中后,把主发射器彻底停止,不要继续发射烟雾粒子
7.4 联动调试的一个小技巧
追踪效果最容易出问题的是“你以为系统没反应,其实参数没传进去”。调试时,我建议在Niagara编辑器的Debug面板上把User.TargetLocation显示出来,然后拖动场景中的目标Actor,看参数是否实时变化。
如果参数在编辑器里实时变,说明数据流是通的,问题出在粒子计算逻辑;如果参数不变,那就是蓝图传参这块没接对,不要再去粒子模块里瞎调了。
另外一个小建议:刚开始不要用玩家角色当调试目标。先放一个手动控制的Actor,把移动速度调慢,这样你能看得清导弹的转向过程。等逻辑稳定了再换成真正的目标,毕竟导弹追一个高速移动的玩家,对算法参数的要求会更高。
这段调试经历我自己走了不少弯路。最痛苦的一次,导弹在目标周围绕了十几圈,我还以为是转向速率不够,把TurnSpeed调到极高,结果导弹直接开始原地“跳大神”了。后来才明白问题是接近速度太快,导弹没有时间完成转向。从那以后,我做的所有追踪类效果都会优先考虑“接近减速”,再回头调转向参数。这套流程跑下来,十分钟做一个能看的导弹追踪效果,是完全够用的。
