Phaser 粒子与 VFX 预算:对象池、优先级和低端机降级要提前定

讲解 Phaser 项目中的粒子和 VFX 管线,覆盖特效优先级、对象池、粒子预算、屏幕密度、性能降级和调试指标。

特效不是越多越爽

Phaser 游戏进入打磨阶段后,团队很容易不断加特效:命中火花、爆炸烟尘、拾取闪光、升级光柱、天气粒子、按钮反馈、Boss 弹幕尾焰。每个特效单独看都不贵,但同屏叠加后,低端手机帧率会掉,画面也会变得难读。更糟的是,特效常常由不同系统直接创建,没有统一预算:武器觉得自己重要,怪物死亡觉得自己重要,UI 奖励也觉得自己重要。结果就是关键预警被普通粒子盖住,玩家看不清危险。

VFX 系统需要像音频和反馈一样有导演。它要知道当前设备性能、屏幕上已有多少粒子、哪些特效优先级高、哪些可以合并、哪些在低端机上应降级。Phaser 的 ParticleEmitter 很方便,但方便不代表可以随处创建。项目层应该有 VfxService,所有玩法系统只发语义请求,比如 enemy_small_deathcritical_hitboss_warning,由 VfxService 决定实际播放方案。

先定义特效优先级

特效可以按功能分级。最高级是玩法关键信息:Boss 预警、玩家受伤、危险区域、交互提示。中级是战斗反馈:暴击、爆炸、技能命中。低级是装饰:落叶、灰尘、金币闪光、环境飘粒。性能紧张时,先砍低级装饰,再降低中级数量,不能砍关键预警。这个规则必须写在配置里,而不是每个特效自己决定。

同一种特效也可以有不同质量版本。爆炸高配版有火光、烟雾、碎片、冲击波;低配版只有闪光和少量烟。VfxService 根据设备档位选择 variant。这样美术可以做丰富效果,程序也能保证低端机不崩。

flowchart TD
  A["玩法系统发 VFX 请求"] --> B["VfxService 读取特效配置"]
  B --> C["BudgetManager 检查粒子数、发射器数、帧率"]
  C --> D{"预算是否足够"}
  D -- "足够" --> E["播放高质量 Variant"]
  D -- "不足" --> F["降级、合并或跳过低优先级"]
  E --> G["EmitterPool / SpritePool"]
  F --> G
  G --> H["Phaser Scene 渲染"]
  H --> I["VfxDebugPanel:统计和热点"]

对象池不只给子弹用

很多团队会给子弹做对象池,却忽略特效。伤害数字、爆炸 Sprite、临时光圈、地面警示、粒子发射器都适合池化。频繁创建销毁 GameObject 会造成 GC 抖动,尤其是连锁爆炸、割草类游戏和三消大连锁。池化时要彻底重置状态:位置、缩放、透明度、tint、动画、事件监听、depth、blendMode、active、visible。

Phaser 粒子发射器本身也要管理。不要每次爆炸都新建一个完整 ParticleEmitterManager。可以预创建常用 emitter 或使用统一粒子对象,按配置发射 burst。若特效需要持续跟随对象,比如燃烧、护盾、毒雾,要在对象死亡或离屏时停止并回收,避免幽灵特效留在场上。

粒子预算要有硬上限

预算可以包括:同时活跃粒子数、发射器数、透明覆盖层数、临时 Sprite 数、每秒新建特效请求数。硬上限不是为了省,而是为了稳定。比如同屏最多 800 个装饰粒子,最多 80 个战斗粒子,最多 12 个高优先级警示。超过预算时,低优先级特效跳过或合并。没有硬上限,极端战斗场景迟早把帧率打穿。

预算还要考虑屏幕密度。远离相机的特效可以不播放,屏幕外特效只保留声音或逻辑结果。小屏幕上同样粒子数量显得更挤,移动端可以按视口面积缩放预算。玩家不会因为少几个灰尘粒子觉得体验差,但会因为掉帧和看不清危险而离开。

一个 VFX 请求模型

下面的代码展示如何让 VfxService 按优先级和预算处理请求。

type VfxPriority = "critical" | "combat" | "decor";

interface VfxRequest {
  key: string;
  x: number;
  y: number;
  priority: VfxPriority;
  estimatedParticles: number;
}

export class VfxBudgetManager {
  private activeParticles = 0;

  constructor(private readonly limits: Record<VfxPriority, number>) {}

  canPlay(req: VfxRequest) {
    if (req.priority === "critical") return true;
    return this.activeParticles + req.estimatedParticles <= this.limits[req.priority];
  }

  reserve(req: VfxRequest) {
    this.activeParticles += req.estimatedParticles;
  }

  release(count: number) {
    this.activeParticles = Math.max(0, this.activeParticles - count);
  }
}

真实项目中,预算会按类别分开,并随帧率动态调整。这里的重点是 VFX 请求先经过预算,而不是直接播放。关键预警永远允许播放,但它也应该尽量轻量,避免“关键”过多变成新问题。

VFX 要服务可读性

特效不是独立艺术品。命中特效不能挡住敌人读招,拾取光效不能盖住陷阱,升级光柱不能遮住确认按钮。每个特效都要考虑层级和持续时间。高亮危险区域通常应在角色和地面之间,伤害火花可以在角色上方,UI 奖励飞行动画在 HUD 层。depth 混乱会让画面失去规则。

颜色也要有语义。红色通常表示危险或伤害,绿色表示治疗,蓝色表示护盾或能量。如果普通装饰也大量使用红色,玩家会误读。美术风格可以丰富,但玩法颜色要克制。VfxService 可以按语义管理 tint 和变体,避免各系统随便选颜色。

动态降级和设备档位

首次启动可以根据设备、分辨率和简单压力测试选择档位:high、medium、low。运行中如果连续几秒帧率低于阈值,降低装饰粒子预算;若恢复稳定,再缓慢恢复。不要每秒来回切档,切换要有滞后。玩家也可以手动选择特效质量,手动设置优先于自动。

低端机降级应尽量保留轮廓。比如爆炸低配版可以保留一次闪光和小范围烟雾,关闭碎片;雨天低配保留背景雨幕,关闭地面涟漪;技能低配保留命中点,关闭长尾粒子。降级不是全部关闭,而是保留信息核心。

调试指标

开发模式应显示活跃粒子数、发射器数、VFX 请求数、被跳过请求、各优先级预算、最贵特效排行。点击某个特效可以查看配置、平均生命周期、估计粒子数、触发来源。很多性能问题来自某个看似不起眼的循环特效,比如火把、毒雾或 UI 背景光。

还可以做 VFX 压力场景:一次性生成 50 个敌人死亡、10 个技能爆炸、天气开启、奖励飞行,观察帧率和降级是否生效。不要等正式关卡中玩家凑出极端组合才发现问题。

上线前检查清单

确认所有特效通过 VfxService 请求;确认特效有优先级和质量变体;确认粒子和临时 Sprite 有对象池;确认预算有硬上限;确认屏幕外和远处特效会跳过或降级;确认关键预警不会被低级特效遮挡;确认颜色语义一致;确认低端机降级保留信息核心;确认调试面板能显示请求和跳过原因;确认压力场景通过。

特效的目标是让游戏更清楚、更有冲击,而不是让画面更吵。Phaser 提供了直接的粒子能力,但项目需要预算、优先级和降级策略。让每一个粒子都有存在理由,VFX 才会成为体验加分项。

特效配置和美术协作

VFX 配置最好由美术和程序共同维护。美术关心颜色、节奏、形状和冲击,程序关心生命周期、粒子数量、层级和性能。配置字段可以包括 duration、particleCount、priority、qualityVariant、blendMode、depthGroup、screenShakeTag。美术可以调整表现参数,程序负责执行边界。不要让美术每次改一点火花都需要程序改代码。

命名也要规范。hit_fire_smallhit_fire_criticalenemy_die_slime 这类名称比 effect_01 更容易维护。特效命名应表达来源和用途,而不是资源文件顺序。日志和调试面板显示这些 key,问题定位会快很多。

与后处理和灯光的关系

粒子不是唯一 VFX。屏幕闪白、描边、颜色校正、热浪扭曲、Light2D 都属于视觉效果预算。后处理通常比普通 Sprite 更贵,尤其在移动端。VfxService 应把后处理请求也纳入优先级。比如 Boss 大招可以短暂启用屏幕扭曲,普通命中不应该每次开后处理。

灯光也要克制。Phaser Light2D 对大量动态光源并不适合低端设备。火把、爆炸、魔法弹都想发光时,需要光源预算。低配模式可以把动态光换成预烘焙亮图或普通 additive Sprite。视觉上保留亮感,性能上更稳。

屏幕录制和压测

VFX 质量很难只看静态截图。建议为典型场景录制短视频:普通战斗、Boss 战、奖励结算、天气叠加、低端机模式。每次大改特效,比较视频和性能指标。视频能暴露闪烁、遮挡、颜色混乱和节奏问题。特别是光效和粒子,静态看好,动起来可能很吵。

压测时不要只看平均 FPS,还要看最差帧和 GC 峰值。一次大爆炸造成 200ms 卡顿,即使平均帧率不错,玩家也会感觉卡。对象池和预算的目标就是削掉这些峰值。

特效和玩法碰撞的边界

多数 VFX 不应该参与玩法碰撞。火花、烟尘、碎片只是表现,不能挡子弹或触发机关。若某个效果有玩法意义,比如毒雾区域、火墙、地雷预警,它应该拆成逻辑区域和视觉表现两部分。逻辑区域负责伤害和状态,VFX 负责显示。这样低配关闭部分粒子时,玩法仍然存在。

这个边界还能避免误解。玩家看到烟雾,以为能遮挡视线,但系统没有对应规则,就会失望。若烟雾只是装饰,颜色和形状不要像可交互区域;若烟雾能遮挡,就要在玩法修正里体现。VFX 语义要和规则一致。

UI 特效也要预算

很多项目只管战斗粒子,忽略 UI 特效。奖励飞行、按钮光晕、抽卡闪光、成就弹窗、红点呼吸,都可能叠加在大厅。UI 特效同样需要优先级和节流。玩家连续领取 20 个奖励时,不要播放 20 条完整飞行轨迹,可以合并成一组。抽卡结果的高光可以强,普通按钮 hover 不应长期占用资源。

UI 层特效还要适配不同分辨率。固定像素大小的粒子在高 DPI 屏幕上可能太小,在低端机上又太贵。使用相对尺寸和质量档位会更稳。VFX 预算不是战斗系统专属,而是全局视觉预算。

特效生命周期和场景切换

Scene 切换时要清理所有挂在旧 Scene 的特效。持续火焰、天气粒子、UI 光效如果没有停止,会在新场景继续占资源或报错。VfxService 可以按 sceneId 管理请求,Scene shutdown 时统一释放。全局特效如转场光效则挂在专门的 overlay scene,不跟玩法 Scene 一起销毁。

生命周期还包括暂停。游戏暂停时,某些特效停住,某些 UI 特效继续播放。规则要明确。战斗粒子暂停,菜单按钮呼吸继续,网络等待转圈继续。不同层级的时间缩放应由 VfxService 读取,而不是每个 emitter 自己判断。

特效生命周期日志也能帮助定位泄漏。离开场景后如果活跃特效数量没有归零,说明有对象没有释放。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页