引言
哈喽大家好,我是亿元程序员,一位有着 8 年游戏行业经验的主程。
前几天,项目经理丢过来一个游戏视频:一颗球跟着音乐不断弹跳,脚下的平台从远处飞来,踩中以后放大消失,整个画面有很强的空间纵深感。
经理的要求也很直接:
把这个音乐球球玩法嵌进我们自己的项目。
我看完视频,又看了一眼手里的工程。
视频是 3D 游戏,我们的项目却是一个纯 2D 项目。
我们项目是一个纯 2D 项目,为了一个独立玩法,再加入模型、材质、灯光、3D 相机和一整套空间逻辑,改动实在有点大。
于是我很诚实地告诉经理:
“这个是 3D 效果,我们现在的项目是 2D,直接做不了。”
本以为事情到这里就结束了,没想到经理沉默几秒,又补了一句:
“那能不能用 2D 做一个?”
字越少,需求越大。
既然不能真的搭一个 3D 世界,那就只剩下一条路:
用 2D 把玩家的眼睛骗过去。
言归正传,本期文章,我们就使用 Cocos Creator 3.8.7,不用 3D 模型,不用透视相机,只靠一张背景、一个平台、一颗球和四个定位点,实现一个伪 3D 的音乐球球。
先分析一下
做一个 3D 游戏,通常需要建模、相机、灯光和碰撞。
但先别急着打开建模软件。
我们把视频暂停,一帧一帧观察,会发现这个玩法真正需要表现的内容并不多。
平台部分:
- 1. 平台从画面远处向近处移动。
- 2. 平台越靠近镜头,看起来越大。
- 3. 远处的跑道很窄,近处的跑道很宽。
- 4. 近处的平台能够遮挡远处的平台。
球的部分:
- 1. 球一直待在画面下方的固定区域。
- 2. 平台不断向球靠近。
- 3. 每个平台到达固定位置时,球正好落下。
- 4. 平台被踩中以后放大并消失。
把项目改成 3D?
有的小伙伴可能就蠢蠢欲动了:为什么不直接把项目改成支持3D ?
这里先说明一下,我们不是不能使用 3D。
如果项目本身就是 3D,或者玩法需要自由旋转相机、真实空间碰撞和多角度观察,那么直接使用 3D 当然更合适。
但当前需求有几个特点:
- 原项目是纯 2D 项目。
- 音乐球球只是其中一个独立玩法(副玩法)。
- 游戏镜头始终固定,不需要自由旋转。
- 玩家只会从正面看到平台从远处靠近。
- 为这个玩法单独准备模型、材质和灯光,开发成本偏高。
换句话说,经理要的是看起来像 3D 的效果,并没有要求我们真的建模。
既然镜头固定,玩家判断纵深主要依赖近大远小、透视收拢和前后遮挡,那么我们只要把这些视觉线索补齐即可。
资源准备
本次实战我们直接从视频里面截图,拿到我们所需要的资源。

整个示例只需要三张图片:
-
background.png:游戏背景,尺寸为750 × 1334。 -
platform.png:带透明通道的平台图片。 -
ball.png:带透明通道的球图片。
背景负责提供远处的地平线、跑道和整体氛围;平台必须单独拆出来,因为后面需要不断移动、缩放和复制;球也需要独立出来,用于播放弹跳效果。
在 Cocos Creator 中建立下面的节点结构:
各节点的职责如下:
-
background:背景节点,也是伪 3D 脚本的挂载节点。 -
platform:平台模板,运行时会复制出多个平台。 -
ball:玩家看到的发光球。 -
p:四个透视定位点的父节点。 -
p1、p2:跑道近端的左右边界。 -
p3、p4:跑道远端的左右边界。
伪 3D 的关键:透视
我们先不写代码,在背景上确定跑道的透视范围。
将 p1、p2 放在画面下方,分别对齐跑道近端的左右两侧;将 p3、p4 放在地平线附近,对齐跑道远端的左右两侧。
四个点会组成一个梯形:
p3 ------ p4
远端
│
│
│
近端
p1 ---------------- p2
平台会从 p3、p4 的中点出发,沿着跑道中线移动到 p1、p2 的中点。
这四个点,就是整个伪 3D 世界的轨道骨架。
“Z 轴”
2D 节点只有 x 和 y,没有真正的空间深度。
没有条件,我们就创造条件。
为每个平台额外保存一个 depth:
type PlatformItem = {
node: Node;
depth: number;
};
这里的 depth 不属于 Cocos 的坐标系统,只是我们自己定义的一个数字。
约定如下:
| depth | 平台状态 |
|---|---|
| 小于 0 | 还没有进入画面 |
| 等于 0 | 位于跑道远端 |
| 0 到 1 | 正在从远处向近处移动 |
| 等于 1 | 到达跑道近端 |
| 大于 1 | 继续向镜头外移动 |
每一帧,让这个数字不断增加:
item.depth += depthSpeed * dt;
depthSpeed 控制平台前进的速度,dt 是当前帧经过的时间。
平台节点仍然只在二维画布上运动,但有了 depth,我们就拥有了一条自己编出来的假 Z 轴。
根据 depth 计算平台位置
有了深度,接下来要解决的问题是:平台在每个深度应该放在哪里?
先取得近端和远端的中点,再通过 math.lerp 计算平台当前的位置:
lerp 是线性插值,可以把它理解为:
从 A 走到 B,现在已经走了百分之多少?
当 depth = 0 时,返回远端位置;当 depth = 0.5 时,平台走到远端与近端的中间;当 depth = 1 时,平台到达近端。
然后把结果设置给平台:
item.node.setPosition(x, y);
这一步完成以后,平台已经能够从地平线移动到画面下方,但它看起来更像一张大小不变的贴纸在屏幕上滑动,距离 3D 还差一点意思。
平台还需要近大远小
人眼判断物体远近,一个非常重要的依据就是大小。
同一个物体在远处看起来小,在近处看起来大。
因此,平台的位置由 depth 控制,缩放也必须由同一个 depth 控制:

例如:
-
farScale = 0.12:平台刚进入远端时只有正常尺寸的 12%。 -
nearScale = 1:平台到达近端时恢复到正常大小。
直接使用线性缩放已经能看到纵深效果,不过变化会稍微有些机械。
我们可以先对深度做一次平滑处理:
const t = math.clamp01(depth);
const smooth = t * t * (3 - 2 * t);
const scale = math.lerp(farScale, nearScale, smooth);
这条曲线会让平台在远处的变化相对平缓,靠近镜头后放大更加明显,视觉上比完全匀速缩放更自然。
这里需要注意一个原则:
平台的位置和大小必须由同一个 depth 控制。
如果平台的位置还在远处,大小却已经接近屏幕宽度,玩家会立刻发现这不是透视,只是一张图片在乱跑。
效果如下:
让球的弹跳与平台同步
平台已经能正常移动,接下来轮到球了。
最直接的做法,是给球播放一个无限循环的上下移动动画。
但这样做有一个问题:球的动画和平台的移动互不相关。
只要运行时间稍长,或者修改了平台速度,球就会逐渐和平台错开,最后非常认真地踩在空气上。
因此,球的弹跳不能拥有自己的节奏。
平台什么时候到达落点,决定了球什么时候落下。
我们先定义一个 ballContactDepth,表示球踩中平台时,平台所处的深度。
然后找出下一块即将经过这个落点的平台:

再根据平台深度计算当前弹跳进度:
phase 会从 0 逐渐增加到 1。
接着使用正弦函数计算跳跃高度:
![]()
它刚好能形成一条拱形曲线:
| phase | jump | 球的状态 |
|---|---|---|
| 0 | 0 | 从平台起跳 |
| 0.5 | 1 | 到达最高点 |
| 1 | 0 | 落到下一块平台 |
最后把跳跃高度加到球的基础位置上:
ball.setPosition(x, baseY + jump * jumpHeight);
因为 phase 来自平台的深度,所以修改 depthSpeed 后,球的弹跳节奏也会自动跟着改变。
修改 depthGap 后,平台间距和球的弹跳周期也会一起改变。
加入平台踩踏反馈
现在球已经能按节奏落到平台上,但如果平台只是继续从球下面穿过去,玩家仍然很难感受到“踩中”。
因此,平台到达接触深度以后,再加入一个很短的踩踏动画:
现在看起来就不再是平台从球脚下路过,而是球真正踩中并击穿了平台。
再进一步
本期只演示了最基础的中线平台和自动弹跳。
基于现有结构,还可以继续扩展:
- 增加左、中、右三条轨道。
- 使用点击或滑动控制球换轨。
- 根据音乐 BPM 控制平台生成节奏。
- 增加
Perfect、Good、Miss判定。 - 增加分数和连击系统。
- 制作普通平台、加速平台和特殊平台。
- 增加球的拖尾、落地粒子和镜头震动。
- 根据歌曲配置动态生成平台序列。
尤其是音乐节奏部分,下一步不应该继续使用固定的 depthGap,而是根据歌曲的节拍时间计算每个平台的初始深度。
这样平台到达落点的时刻,才能真正与音乐鼓点对齐。
结语
以上就是在 2D 项目中实现伪 3D 游戏效果的开发经验,不知道小伙伴们有没有更好的思路?评论区告诉大家。
我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助,也希望通过您能帮助到大家。
实不相瞒,想要个赞和爱心!请把该文章分享给你觉得有需要的其他小伙伴。谢谢!
推荐文章:









