GAS 里"技能打出去了,特效音效谁来播"这个问题,引擎给出的答案是 GameplayCue:一个挂在 GameplayTag 上的表现层事件。它刻意不管逻辑——能不能放技能是 Ability 的事,放出来长什么样是 Cue 的事。这个切分让策划调表现时不用动玩法代码。基于 UE5.3 源码(Engine/Plugins/Runtime/GameplayAbilities)走一遍派发链路。
从 Tag 到执行:三条入口路径
玩法代码侧永远只调三个函数(AbilitySystemComponent.h):
void ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, ...); // 瞬时
void AddGameplayCueLocal(const FGameplayTag GameplayCueTag, ...); // 持续开始
void RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, ...); // 持续结束
"瞬时/持续"的区分对应特效的生命周期:受击闪光是瞬时,燃烧 debuff 的火焰是持续(开始挂、结束摘)。GameplayCueManager(GameplayCueManager.h)内部把 tag 查成一组 FGameplayCueNotifyData——一个 tag 可以对应多个 Cue 蓝图,"一个事件炸出多种表现"就是这么实现的。查询走 GameplayTag 的层级匹配,所以 GameplayCue.Burn.Fire1 能同时命中注册在 GameplayCue.Burn 上的通用 Cue。
执行时分两条路,由 Cue 资源自身的父类决定:
- FGameplayCueNotify_Static:不开 actor、不起 component,直接在
GameplayCueManager里Execute回调里播放 Niagara/音效。适合一次性、无状态的特效。 - FGameplayCueNotify_Actor:生成一个专门的 Cue actor(默认由
GameplayCueNotifyActor派生),带着持续生命周期,OnExecute/OnActive/OnRemove三个虚函数对应瞬时/开始/结束。适合要挂点、要跟目标、要中途换材质的表现。
选择的关键权衡:Actor Cue 每次触发是一个 SpawnActor + Tick(如果用 TickCondition 开了 tick),高频触发时 GC 和 actor 数都是压力;Static Cue 零对象开销但拿不到目标引用。我的项目里受击火花(每帧可能十几个)用 Static,目标身上的持续光环用 Actor。
本地与网络:谁负责播
多人游戏里 Cue 的难点不在本地,在于谁有权播。UE 的约定:Cue 默认由每个客户端各自在本地执行——服务端不播,客户端收到"这个 tag 该执行了"的信号后自己在本地放。为什么不服务端播?特效播给"谁看得到"取决于表现逻辑(距离、遮挡、画质档位),这些在服务端算既贵又不准;拆到客户端各自执行,相关性判断就只剩一条"这个 Cue 属不属于你关心的对象"。
所以网络路径上,Cue 不靠属性同步,也不走普通 RPC,而是借 GameplayCueManager 的转发:服务端把 Cue 事件塞进 FGameplayCueTranslator 翻译层,最终作为不可靠的数据沿 actor channel 的专用路径发给各客户端,客户端的 ASC 收到后本地 InvokeGameplayCueEvent。不可靠是故意的——一帧的受击火花丢了,下一帧的动画还在跑,用户几乎察觉不到;做成可靠反而会在丢包时堆积重传,特效延迟比丢失更难看。
本地预测(上一篇讲的 FPredictionKey)同样覆盖 Cue:预测技能命中时,预测端会立即 InvokeGameplayCueEvent 把特效放出来,等服务端确认后事件去重。5.x 里这个去重靠 GameplayCueParameters 里的 PredictionKey 匹配,所以自己封装"播 Cue"的辅助函数时,别把 PredictionKey 丢了,否则预测端会播两遍。
为什么这个解耦值得学
第一,表现和逻辑独立演化。技能逻辑改命中判定、表现调粒子参数,互不阻塞,策划可以自己建一堆 BP_GameplayCueNotify 资源,不用改 C++。
第二,网络语义被简化成一条约定:每个客户端本地执行。这丢掉了"谁该看到"的集中式判断,换来相关性成本近乎为零。缺点是如果你想做"全房间同步的一次性演出"(比如 boss 大招震屏),Cue 的不可靠语义不够用,得另找可靠通道。
第三,测试友好。showdebug abilitysystem 里能直接看到 ASC 上挂着的活跃 Cue 列表,表现没播出来先查这里——tag 到了没有、Params 对不对,一眼定位是派发问题还是表现资源问题。
实操清单
- 注册表:
DefaultGame.ini的[/Script/GameplayAbilities.GameplayCueSet]段落,或引擎自动扫描的GameplayCueNotifies目录;手动加的别忘配。 - 自定义参数走
FGameplayCueParameters(Location、Normal、SourceObject 等),Static Cue 里拿原始 hit 信息全靠它。 - 持续 Cue 的清理责任在 ASC 收到 Remove 时,如果表现资源被外部提前销毁(切图、角色死亡),要处理
OnRemove里目标已失效的分支。 - 高频伤害技能别让策划随手挂 Actor Cue,Static + Niagara 的 GPU 粒子足够撑大多数火花。