Cocos Creator 屏幕震动实现指南:4 种方案从参数到性能的完整对照
【免费下载链接】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
爆炸特效播完,画面抖了一下——在 60 帧的手机上像隔了一层玻璃,在 120 帧的旗舰机上却抖得发毛。问题不在位移,在你没给震动一个统一的数学形状。这篇带你读完 Cocos Creator 里四条可落地的屏幕震动路线,每种方案的关键参数、适用边界和真实开销都会对账清楚,带走一份能直接抄进项目的参数清单。
把需求拆开:屏幕震动要先回答三个问题
震动的偏移该作用在谁身上?作用在相机的位置,画面整体平移,天空盒也跟着动;作用在某个 UI 容器,则只有那层 UI 抖。选错挂点,会出现"背景纹丝不动、角色抖成一团"的割裂感。
衰减曲线谁说了算?用随机数每帧重掷,衰减不可控,两帧之间跳变;用一条确定的曲线(正弦衰减或缓动函数),衰减完全在你手里。这就是"生硬"和"有打击感"的分水岭。
多个震动源怎么排队?一次爆炸、一声炮响、一个 UI 点击同时发生,是叠加、取最强、还是后来者覆盖?不先定规则,震感会互相污染。
方案一:随机位移——三行代码的白噪声震动
一句话原理:每帧在强度范围内掷两个随机数当偏移,强度按固定比例逐帧缩小。
放在帧循环里,它是整个家族里最省事的写法:
// 挂在相机节点的 update 里;duration 建议 0.2~0.4s, // 每帧衰减系数建议 0.88~0.95(帧率越高衰减越快,见避坑清单) onExplode() { this.amp = 0.5; // 初始振幅,UI 点击类建议 0.05~0.15 this.duration = 0.3; } update(dt) { if (this.duration <= 0) return; this.duration -= dt; this.amp *= 0.9; const o = Vec2.create( random(-1, 1) * this.amp, random(-1, 1) * this.amp ); cameraNode.position.set(this.basePos.x + o.x, this.basePos.y + o.y, this.basePos.z); }适用边界:单帧开销是两次随机数加一次赋值,成本几乎为零,适合 2D UI 的低强度点击反馈;别拿它做爆炸——相邻帧数值互不相关,画面会像白噪点一样"刺",而不是"撞"。
方案二:正弦衰减——一次撞击的余震
一句话原理:位移 = 振幅 × sin(相位),振幅按指数函数随时间衰减;正弦保证相邻帧连续,指数保证自然收尾。
这个形状来自生活里最常见的撞击:手机被拍一下,先猛跳,再带两三次越来越小的颤动,最后静止。把它写出来就是这几行,决定效果的核心是频率和衰减速率两个参数:
// 挂在相机节点的 update 里;freq(完整震荡次数/秒)建议 8~15, // decay 建议 6~12:数值越大收得越快,越小余震拖得越长 update(dt) { if (this.t >= this.dur) return; this.t += dt; const k = this.t / this.dur; const env = this.amp * Math.exp(-this.decay * k); // 指数包络 const p = Math.sin(2 * Math.PI * this.freq * this.t); const o = p * env; cameraNode.position.set(this.basePos.x + o, this.basePos.y + o * 0.7, this.basePos.z); }适用边界:每帧两次三角函数加一次指数,成本极低;一条曲线同时描述位置和衰减,参数手感好,适合单次撞击、受击硬直这类"一下就好"的效果。它只给一个方向感,想要"乱"就得上方案三。
方案三:Perlin 噪声——让镜头"活着"地震
一句话原理:把时间当作一维坐标,用引擎自带的 Perlin 噪声采出连续、平滑、看似随机的偏移序列——像心电图,忽高忽低但线不断。
引擎粒子系统里有一个现成的 噪声类:noise(x, y, z, min, max)接受任意三维坐标,返回[min, max]内的连续值。屏幕震动只需要让 x 轴坐标随时间滚动,就能"滚动"出一整条噪声带,y、z 固定为 0 即可:
// this.noise 为 ParticleNoise 实例;t 用 dt 累加(秒) // 时间系数建议 1.5~4(越大抖得越快、越细), // 想要更"野"的质感,把 setOctaves 层数从 1 提到 2~3 update(dt) { this.t += dt; const nx = this.noise.noise(this.t * 2.0, 0, 0, -1, 1); const ny = this.noise.noise(this.t * 2.0, 17.7, 0, -1, 1); // 换种子坐标避免 X/Y 同步 cameraNode.position.set( this.basePos.x + nx * this.amp, this.basePos.y + ny * this.amp, this.basePos.z ); }适用边界:连续性最好、最适合长时间播放(地震、行军、镜头呼吸),但它是四个方案里每帧计算最重的一档:每次采样都要重建一张 512 项的排列表(见 噪声源码 第 64~65 行),高频采样或大 octaves 数会把这个开销放大。持续型环境震动用它,单次冲击别用。
方案四:缓动函数包络——方向明确的"挥拳"
一句话原理:震动强度 = 一条缓动曲线的输出,位移沿一个固定方向推进——它不是"抖",是"撞"。
Cocos 在 缓动函数模块 里内置了全套曲线,elasticOut带回弹、backOut带过冲。把其中一条当包络,再配一个方向向量,就是最干净的单次重击:
// dir 是单位方向向量(比如爆炸在右上就 {x: 0.8, y: 0.6}) // duration 建议 0.25~0.5s,amp 建议 0.3~0.8 个屏幕空间单位 const k = this.t / this.duration; // 0 → 1 const e = elasticOut(1 - k); // 1 → 0,带一次回弹 cameraNode.position.set( this.basePos.x + dir.x * e * this.amp, this.basePos.y + dir.y * e * this.amp, this.basePos.z );适用边界:每帧一次曲线求值,开销与随机法同级;方向明确、收尾干脆,是"一次重击 + 一次回弹"的最佳选择。代价是没有横向的"乱抖",需要乱抖感时把它和方案二叠着跑,而不是单独硬扛。
屏幕震动方案性能对比:开销、内存与上手成本一张表
| 方案 | 每帧计算开销 | 常驻内存 | 上手成本 | 典型场景 |
|---|---|---|---|---|
| 随机位移 | 低 | 低 | 低 | UI 点击、轻微受击 |
| 正弦衰减 | 低 | 低 | 低 | 单次撞击、爆炸余震 |
| Perlin 噪声 | 中(每采样重建排列表) | 中 | 中 | 地震、行军、镜头呼吸 |
| 缓动包络 | 低 | 低 | 低 | 方向明确的单次重击 |
三个加粗结论:
正弦衰减与缓动包络的开销可以忽略不计,是默认首选;Perlin 噪声每帧重建排列表是它的主要成本,多实例或高频采样前先降频;随机位移虽然最省,但"白噪声刺眼"的观感是它用参数修不掉的。
避坑清单:屏幕震动的三个高频翻车点
现象:同一份震动代码,高刷屏上抖得又猛又短,低配机上慢吞吞。根因:衰减系数(如amp *= 0.9)按"每帧"计,帧率一高,单位时间内衰减的帧数就变多。修法:把衰减改写成时间的函数——amp = amp0 * Math.exp(-decay * t),频率用sin(2π·freq·t)里的绝对时间t(秒)驱动,而不是逐帧累乘。
现象:震动一开,相机的跟随逻辑开始抽搐,背景却纹丝不动。根因:震动偏移直接写进了相机本体的位置,和相机跟随/镜头移动写在同一层,两个系统互相覆盖。修法:建一个空的"震动节点"套在相机外面(或反过来让相机做震动节点子节点),震动只改这个空节点的位置,跟随系统照旧写相机的基础位置,两者物理隔离。
现象:爆炸、炮击、UI 点击同时发生,画面抖成筛子。根因:每个震动源各自写相机位置,后写的直接覆盖先写的。修法:引入一个总强度上限:每次触发时计算新强度,若超过上限则只保留更强的那个(或按权重归一化后再合成),并给每个震动源一个优先级,UI 点击永远让位于爆炸。
三步接入:把震动装进你的项目
第一步,搭挂点。在场景里建一个空节点CameraShake作为相机的父节点,记录它的基础位置,震动只改这个节点的position——这是后面所有方案的公共底座。
第二步,接事件。在受击或爆炸回调里写入参数即可,初始振幅建议按上表场景取区间:
shakeNode.shake(0.5, 0.3); // UI 反馈类:amp 0.05~0.15 // 爆炸类:amp 0.4~0.8,duration 0.25~0.5第三步,定仲裁。加一个总强度上限与优先级,多震动源同时触发时按"保留最强 + 归一化"合成,别让画面抖成筛子。
收尾:震动感不是抖出来的
好的屏幕震动,从来不是把位移调大,而是让衰减曲线替位移说话——曲线对了,0.05 的振幅也有千钧之力;曲线错了,1.0 的振幅也只是刺眼。
下一步你可以做三件事:拿方案二在你最常用的机型上把freq从 8 扫到 15,找到手感拐点;把 Perlin 噪声的时间系数从 1.5 调到 4,对比"地震"和"警报"两种质感;给项目加震动仲裁,用一次爆炸 + 一次点击的并发场景验证上限是否生效。
你的项目里,哪种震动效果还没调顺?把它扔出来对一下参数。
【免费下载链接】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),仅供参考