Cocos 屏幕震动完整指南:5 种从线性插值到物理模拟的打击感实现方案
【免费下载链接】cocos-engineCocos simplifies game creation and distribution with Cocos Creator, a free, open-source, cross-platform game engine. Empowering millions of developers to create high-performance, engaging 2D/3D games and instant web entertainment.项目地址: https://gitcode.com/GitHub_Trending/co/cocos-engine
在 cocos-engine 这类跨平台游戏引擎里做战斗反馈,屏幕震动是成本最低、感知最强的手段之一:一次爆炸、一脚重击,画面抖一下,"打中了"的信号就传给了玩家。本文基于 Cocos 引擎的真实模块(噪声类、刚体组件、游戏时间管理),给出 5 种由浅入深的屏幕震动实现思路,并附上选型对照表和参数经验值,帮助你在打击感与性能之间快速找到平衡点。
原理先行:震动是一条受控的位移曲线,而不是随机抖屏
很多"假震动"的根源是把震动理解成了"每帧给相机加一个随机数"。随机数每帧独立取值,相邻帧之间没有关联,画面会呈现出电击般的跳动,这就是生硬感的来源。
更准确的理解是:震动的本质是一条随时间衰减的位移曲线。想象一根被拨动的音叉:初始振幅最大,之后按固定节奏来回摆动,振幅越来越小,最后归零。好的屏幕震动就是模拟这种"起振—衰减—归零"的过程——用一个强度包络控制"现在该抖多大",再用一个平滑变化的驱动源(插值、噪声或弹簧)决定"这一帧往哪个方向抖"。
这个认知会直接决定你后面所有代码的写法:先算强度,再算方向,两者相乘得到最终位移。
分层实现:从 5 行代码到物理模拟
用线性插值快速实现基础震动
入门方案只做一件事:让强度从触发值平滑降到 0,每帧乘一个随机方向。
import { Vec2 } from 'cc'; class LinearShake { intensity = 0; trigger (power: number) { this.intensity = power; } update (dt: number): Vec2 { const off = new Vec2( (Math.random() * 2 - 1) * this.intensity, (Math.random() * 2 - 1) * this.intensity ); this.intensity += (0 - this.intensity) * Math.min(dt * 6, 1); return off; } }方向仍是逐帧随机的,所以连续帧之间会有跳变。它适合一次性点击反馈、低配机型的兜底方案,不需要任何额外依赖。
用 ParticleNoise 实现连续噪声震动
Cocos 自带一个 Perlin 风格噪声类 ParticleNoise(源自粒子系统模块),noise(x, y, z, min, max)返回平滑连续的值。把时间当作坐标喂给它,就得到一条天然平滑的曲线:相邻帧的值彼此相关,抖动就有了"惯性"。
import { Vec2 } from 'cc'; import { ParticleNoise } from 'cocos/particle/noise'; class NoiseShake { private noise = new ParticleNoise(); private t = 0; intensity = 0; update (dt: number): Vec2 { this.t += dt; const x = this.noise.noise(this.t * 8, 0, 0, -1, 1); const y = this.noise.noise(0, this.t * 8, 0, -1, 1); this.intensity += (0 - this.intensity) * Math.min(dt * 4, 1); return new Vec2(x * this.intensity, y * this.intensity); } }时间系数8就是震动频率,改大改小即可换挡。爆炸、地震这类需要"晃得自然"的场景首选它,单次调用开销极低,移动端毫无压力。
物理模拟震动:让刚体替你算衰减
如果你震的对象本来就有刚体(比如被踢飞的箱子、震动的镜头代理节点),可以直接借用物理引擎:施加一个冲击冲量,阻尼负责衰减,完全不用手写衰减逻辑。
import { Vec3 } from 'cc'; import type { RigidBody } from 'cocos/physics/framework/components/rigid-body'; function impulseShake (body: RigidBody, dir: Vec3, power: number) { const force = new Vec3(dir.x, dir.y, dir.z).multiply(power); body.applyForce(force); body.linearDamping = 0.85; // 控制衰减速度,值越大停得越快 }参考 RigidBody 组件源码 中applyForce的签名,还可传入相对作用点制造扭转。只推荐给"震动的对象本来就参与物理模拟"的场景,纯为抖个屏幕而引入刚体,开销明显偏高。
多频叠加:做出"主震 + 余震"的层次感
单一频率的噪声听起来像"嗡嗡",叠加多个频率就有了层次:低频给大范围晃动,高频给细碎的毛刺感,很像真实地震中先晃后颤的过程。
import { Vec2 } from 'cc'; import { ParticleNoise } from 'cocos/particle/noise'; function multiFreqOffset (n: ParticleNoise, t: number, amp: number): Vec2 { const main = n.noise(t * 2, 0, 0, -1, 1); // 低频:大晃动 const sub = n.noise(t * 10, 0, 0, -1, 1); // 中频:节奏感 const fine = n.noise(t * 28, 0, 0, -1, 1); // 高频:细碎毛刺 const v = (0.6 * main + 0.3 * sub + 0.1 * fine) * amp; return new Vec2(v, -v * 0.8); }权重0.6 / 0.3 / 0.1是偏保守的起点,先保证低频主导。剧情演出、大 BOSS 登场这类需要"有呼吸感"的镜头用它;成本只是多了两次噪声求值,仍属轻量级。
选型对比:一张表看清开销与适用场景
| 方案 | 计算开销 | 效果质感 | 适用场景 | 设备建议 |
|---|---|---|---|---|
| 线性插值 | 最低 | 生硬,有跳变 | 点击反馈、低配兜底 | 全设备 |
| 单频噪声 | 低 | 平滑自然 | 爆炸、通用冲击 | 全设备 |
| 多频叠加 | 中低 | 层次丰富 | 剧情演出、大型事件 | 中端及以上 |
| 物理模拟 | 较高 | 真实联动 | 对象本就参与物理 | 中高端,低端慎用 |
| 实时动态源(多源混合+池化) | 取决于源数量 | 可控性最强 | 弹幕、连续打击 | 做好对象池后全设备可用 |
一句话结论:默认选单频噪声,需要层次感升级多频,涉及刚体才考虑物理方案。
调试与参数经验
让震动在不同设备上表现一致 ⏱️
震动曲线一旦和帧率耦合(比如"每帧衰减 5%"),高刷屏上就会衰减得比低帧率设备快一倍。正确做法是让所有时间推进都乘以真实帧间隔dt(参考 Game 时间管理 的deltaTime),而不是每帧写死一个比例。触发时刻的对齐可以用game.totalTime取统一时间基准。
多震动源冲突如何处理 🔀
子弹连续命中时,如果每个源都各自覆盖相机位移,后触发的会直接"吃掉"先前的效果。推荐一个统一的震动管理器:新震动画进列表,每帧累加所有源的位移,强度低于阈值的回收进对象池,避免高频创建对象。
import { Vec2 } from 'cc'; import { LinearShake } from './linear-shake'; // 上文基础震动类 class ShakeManager { private free: LinearShake[] = []; active: LinearShake[] = []; add (power: number) { const s = this.free.pop() ?? new LinearShake(); s.trigger(power); this.active.push(s); } update (dt: number): Vec2 { const off = new Vec2(); for (const s of this.active) off.add(s.update(dt)); for (let i = this.active.length - 1; i >= 0; i--) if (this.active[i].intensity < 0.01) this.free.push(this.active.splice(i, 1)[0]); return off; } }同方向多源叠加时记得设一个总强度上限(clamp),防止五个源同时触发把画面抖出屏幕外。
强度与衰减的取值区间(经验值)
以"位移单位 = 世界单位"为量纲,下面这组区间在多数项目里可以直接作为调参起点:
- 爆炸/重击:初始强度 3.5~5.0,包络衰减较快(2~3 秒内归零);
- UI 点击反馈:初始强度 0.8~1.2,衰减要干脆,避免"余味太长"显得廉价;
- 剧情长镜头:初始强度 1.5~2.5,衰减放慢,配低频噪声更贴合叙事节奏。
频率上同理:time * 2左右是低频(大地震),time * 8~10是中频(通用),time * 24~32是高频(细碎)。先定包络(多久停),再定频率(怎么晃),顺序反了很难收敛。
进阶话题 🚀
方向性震动:3D 场景里常只想让画面在 XY 平面晃、保持纵深稳定。做法是把噪声映射到 X、Y 两个正交轴(如noise(t*2.3, 0, 0)与noise(0, t*1.8, 0)),第三个轴固定为 0,天然得到 2.5D 效果。
距离衰减:震源到相机的距离越远,强度越低。取1 / (1 + k * distance)这类平滑衰减即可,近处爆炸才值得"满震"。
帧率自适应:除了用dt驱动外,还可在帧耗时超阈值时自动降低高频分量权重——高频毛刺对感知贡献小,砍掉它几乎无感,却能省下一次噪声求值。
收尾:五条可执行清单
- 新项目默认用单频噪声震动,把线性插值版留给低端机降级分支。
- 所有时间推进一律乘
dt,杜绝"每帧写死衰减比例"这类帧率耦合写法。 - 多源震动走统一管理器 + 对象池,并给总位移设上限。
- 调参顺序固定为:先包络(强度与衰减时长)后频率(时间系数),用经验区间做起点而非从零猜。
- 把相机位移逻辑抽成独立组件,方便在真机(尤其中低端安卓机)上逐方案 A/B 观察表现。
【免费下载链接】cocos-engineCocos simplifies game creation and distribution with Cocos Creator, a free, open-source, cross-platform game engine. Empowering millions of developers to create high-performance, engaging 2D/3D games and instant web entertainment.项目地址: https://gitcode.com/GitHub_Trending/co/cocos-engine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考