简介:面向Cocos Creator 2.4.10开发者的2D人物与3D背景混合场景示例,围绕人物移动控制与透视视角设置两大核心需求展开,适合刚接触2.5D玩法的初级游戏开发者快速上手。压缩包共33个文件,以TypeScript脚本、json配置、png图像资源和3dTest.fire场景文件为主;其中4个ts脚本承载角色位移与视角逻辑,json保存工程与场景配置,png提供素材贴图,fire记录场景层级,atlas负责图集打包,整体目录结构完整,可直接导入编辑器运行对照。资源重点演示了摄像机透视参数调整、节点位置更新与输入监听的关键写法,同时展示了2D精灵动画与3D场景物件在同一项目中的组织方式,便于理解混合渲染下的层级管理与坐标系切换;跟随工程内脚本注释与目录命名,可快速迁移到自己的2D+3D混合项目,省去重复搭建基础框架的时间。压缩包仅313KB,内容精炼无冗余,已有464人浏览学习,适合作为入门阶段的随开随用范例。
1. Cocos Creator 2.4.10 里“2D 人物 + 3D 背景”:先确定渲染边界
Cocos Creator 2.4.10 要想做“2D 人物 + 3D 背景”,最常翻车的地方不是美术资源,而是把 2D 精灵直接丢进 3D 场景。我的实际做法是把这个组合拆成两件事:人物的 2D 素材变成 3D 空间里的一张贴片,再让它每帧朝向摄像机。这样人物移动、透视视角、前后遮挡就都能走 3D 逻辑,后续要加物理碰撞和相机跟随也顺理成章。
这套方案适合角色是像素风、序列帧或动态立绘,但背景又需要有纵深和近大远小效果的项目。你不需要重新建模人物,还能让角色从房子后面走出来、被树木挡住、离镜头越远越小。后面章节会从渲染管线的差异开始,依次讲透视摄像机怎么设置、人物贴片怎么搭、移动和相机跟随怎么写,最后给你一组最容易踩翻的坑。
2. 为什么不能把 2D 精灵直接当 3D 模型:渲染管线与选型
2.1 2D 与 3D 渲染管线:Cocos Creator 2.4.10 的两套队列
Cocos Creator 2.4.10 虽然能同时放 2D 和 3D 节点,但内部渲染路径是两套。3D 节点走模型队列,有深度缓冲,MeshRenderer 的材质直接参与透视和遮挡;2D 的 Sprite、Label、Graphics 则挂在 Canvas 下,走 UI 渲染队列,由 UI 相机在最后阶段绘制。UI 队列默认是“后绘制者优先”,所以 2D Sprite 会盖住 3D 场景,而且不会因为距离摄像机远而变小。
如果你在场景根节点下直接新建一个带 Sprite 组件的节点,它确实能显示,但它的深度信息在 3D 相机里是不参与比较的。具体表现就是:人物永远浮在背景最上层,角色站到一面墙后面,墙反而挡不住人;摄像机拉远,人物尺寸不变,背景却缩成一小块。这就是为什么这个组合必须先确认角色到底放在哪个坐标系里。
2.2 三个常见方案对比
| 方案 | 角色所在空间 | 遮挡关系 | 透视缩放 | 实现成本 | 适用场景 |
|---|---|---|---|---|---|
| UI 叠加映射 | Canvas 2D | 手动处理 | 手动换算 | 低 | 剧情演出、对话框、站立 NPC |
| 3D Plane 贴片 + Billboard | 3D 节点 | 自动加深 | 自动 | 中 | 可移动角色、2.5D RPG、跑酷 |
| 3D 模型替身 | 3D 节点 | 自动 | 自动 | 高 | 纯 3D 项目,美术量充足 |
第一类方案常见做法是:把人物的 3D 逻辑位置单独用一个空节点记录,每帧把空节点的世界坐标转成屏幕坐标,再赋给 Canvas 下的 Sprite。优点是人物永远是一张清晰的 2D 图,适合对话系统;坏处是角色一旦要走到建筑后面,你就得自己写遮挡判断,甚至在遮挡时手动调透明度,越写越接近玄学。
第二类方案是把人物做成 3D 场景中的一张“立牌”,用 Plane 作为载体,把人物贴图贴上去。Plane 是 3D 节点,所以它能被树、墙正常遮挡,也能随着透视镜头变小放大。人物图像总得有一面正对摄像机,所以需要每帧做一次朝向修正,这就是 Billboard。对这个需求,我一般直接选第二类。
2.3 选型:Plane 贴片 + Billboard,人物留在 3D 空间
选择 Plane 贴片,核心理由是它把人物彻底拉进 3D 空间。Plane 自带 MeshRenderer,可以给它指定材质、贴图,也可以被 3D 摄像机渲染。人物移动就变成修改这个 3D 节点的 position,人物的朝向则交给 Billboard。透视视角下,人物贴片离镜头越远,画面里越小,这是引擎自动完成的,不需要你手算屏幕坐标。
这里有个容易忽略的点:人物图片自带透明通道,所以材质必须开启透明混合,否则图片周围会有一块实心方形底。另外,Plane 默认是平的,你要把这张“立牌”立起来,而不是平铺在地上。我在编辑器里的习惯是:先建一个空节点作为 Player 根节点,再在下面挂一个子 Plane 作为视觉层。Billboard 脚本挂到根节点,子 Plane 调整初始旋转。这样后续如果要加阴影、脚印、受击闪白,都挂在根节点下,不会干扰人物朝向。
3. 搭建场景:透视摄像机、地面与人物贴片
3.1 透视摄像机参数:让背景有近大远小
在 2.4.10 里,新建场景自带的 Main Camera 默认投影方式可能不是透视,所以进编辑器第一件事就是处理摄像机。选中 Main Camera,在 Inspector 里找到 Projection 属性,改成 Perspective。这一步没做的话,后面所有“越远越小”的效果都不会出现,3D 背景看起来就像一张被拉平的画。
FOV 我一般设置在 45 到 50 之间。FOV 越小,望远镜感越强,背景纵深会显得平;FOV 越大,近处物体会被拉伸变形,人物走到画面边缘时尤其明显。Near 和 Far 分别设成 0.1 和 100 左右,Near 不要随手填 0.001,深度精度不够时远处容易闪烁。摄像机初始位置可以放在(0, 4, 6),站在这个位置向原点看,正好是一个偏仰视的第三人称角度。
透视视角的验证很直接:把摄像机放在固定位置,把一个立方体从摄像机近处慢慢移向远处,画面里立方体必须持续变小。如果没变化,就说明 Projection 还是正交模式,或者脚本里有代码又改回了 Ortho。
3.2 创建地面和背景示意模块
为了验证移动和遮挡,我会先搭一套最简陋的环境。层级管理器里右键创建 3D 对象,选 Plane,命名 Ground。把 Scale 改成(10, 1, 10),位置放在(0, 0, 0),这就是一片 10×10 的地面。再创建几个 Cuboid 或圆柱,摆在靠近镜头的位置,模拟背景里的墙或树。不需要美术资源,先让几何体把空间撑起来,后面再替换成正式模型。
地面默认是水平摆放的,Player 站在它上面,所以地面 Y 坐标是 0,人物根节点脚下也要对齐到 0。背景模型注意不要和地面重叠太多,否则后面排查遮挡时会分不清是材质问题还是模型穿模。搭建阶段尽量保持一个 3D 节点一个目的,不要让背景模型把摄像机和人物之间的视线全挡住。
3.3 人物贴片:Plane、透明材质和初始朝向
人物贴片这一步最容易错,我按顺序做。先把人物 PNG 导入资源管理器,检查图片属性里的 Alpha 是否开启。然后在资源管理器里右键创建材质,命名 PlayerMat,选中后在 Inspector 里把 Effect 选成 builtin-standard,把 Main Texture 设成人物图。Rendering Mode 要改成 Transparent,有些版本里显示为 Blend,意思是颜色会和背景做混合,透明区域才能真正透明。
接着右键创建 3D 对象里的 Plane,挂在 Player 空节点下面,把 PlayerMat 拖到 MeshRenderer 的 Materials 数组里。这里的关键是初始朝向:如果编辑器里看到 Plane 是水平躺在地上的,把子节点绕 X 轴旋转 -90 度立起来。立起来之后,人物正面的朝向不一定对准摄像机,先大概转一下,后面有 Billboard 脚本会自动修正。
材质设置完成后,建议先跑一次场景看效果。如果人物周围出现黑色方块,是 Rendering Mode 没有改成透明;如果整个人物变成灰白色,是图片导入设置里没有开启 Alpha 或图片本身是 JPG。这些问题越早发现越好,等贴上复杂的序列帧再排查会浪费时间。
3.4 Billboard 脚本:让 2D 人物始终面朝摄像机
Billboard 脚本是这套方案里最短但最关键的代码。它的任务只有一个:每帧把人物节点的朝向对准摄像机。在 2.4.10 里可以直接用节点的 lookAt 方法。我一般写成这样:
const { ccclass, property } = cc._decorator; @ccclass export default class Billboard extends cc.Component { @property({ type: cc.Node }) cameraNode: cc.Node = null; lateUpdate() { if (!this.cameraNode) { return; } // lookAt 会让节点的某个轴指向目标位置 this.node.lookAt(this.cameraNode.position); } }这段代码挂在 Player 根节点上,cameraNode 从编辑器拖入 Main Camera。lookAt 每帧执行一次,所以摄像机旋转也好、人物移动也好,下一帧人物都会重新面对镜头。代码里的逻辑很简单:没有拖相机就跳过,否则每帧更新朝向。因为 2.4.10 的 3D 节点位置用 Vec3,cameraNode.position拿到的已经是世界坐标,只要人物和相机都在根场景下,这个值可以直接用来 lookAt。
如果挂上去之后人物背对镜头,不要在代码里硬转 180 度,而是把 Player 根节点的初始 Y 轴旋转在编辑器里调一下。常见做法是新建一个子节点放 Plane,通过子节点修正视觉朝向,根节点只负责 lookAt。这样代码保持统一,不同角色即使初始角度不同,也只需要调整子节点。
4. 人物移动控制:键盘输入、速度与相机跟随
4.1 输入监听与移动脚本
人物移动要改的是 Player 根节点的 3D 位置,不是 Sprite 的坐标。Cocos Creator 2.4.10 的键盘监听可以直接挂在组件里,用cc.systemEvent监听按下和抬起。下面这段是我常用的 PlayerController:
const { ccclass, property } = cc._decorator; const KEY_W = 87; const KEY_A = 65; const KEY_S = 83; const KEY_D = 68; @ccclass export default class PlayerController extends cc.Component { @property({ type: cc.Float }) moveSpeed = 6; @property({ type: cc.Float }) limitX = 4; @property({ type: cc.Float }) limitZ = 4; private _keys = new Set<number>(); onLoad() { cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this._onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this._onKeyUp, this); } onDestroy() { cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this._onKeyDown, this); cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this._onKeyUp, this); } private _onKeyDown(e: any) { this._keys.add(e.keyCode); } private _onKeyUp(e: any) { this._keys.delete(e.keyCode); } update(dt: number) { let dx = 0; let dz = 0; if (this._keys.has(KEY_W)) dz -= 1; if (this._keys.has(KEY_S)) dz += 1; if (this._keys.has(KEY_A)) dx -= 1; if (this._keys.has(KEY_D)) dx += 1; if (dx === 0 && dz === 0) { return; } // 归一化方向,避免斜向移动速度变快 const dir = cc.v3(dx, 0, dz).normalize(); const offset = cc.v3( dir.x * this.moveSpeed * dt, 0, dir.z * this.moveSpeed * dt ); const pos = this.node.position.clone().add(offset); pos.x = Math.max(-this.limitX, Math.min(this.limitX, pos.x)); pos.z = Math.max(-this.limitZ, Math.min(this.limitZ, pos.z)); this.node.setPosition(pos); } }这段代码的逻辑分三块。第一块是按下和抬起收集按键码,放进一个 Set,避免重复监听导致方向错乱。第二块是在 update 里从 Set 读出当前方向,dx、dz 分别代表横向和纵向。第三块是速度乘以 dt,得到这一帧应该移动的距离。需要注意 y 轴保持 0,因为角色站在地面上,不能把 y 方向加进来。
关于 keyCode,这里直接用数字是因为 2.4.10 里不同小版本对 cc.macro.KEY 的字段稍有差异,数字是浏览器标准键值,W 是 87、A 是 65、S 是 83、D 是 68。如果你要在移动端跑,这段键盘逻辑可以留作 PC 调试,真正发布时换成下一节的虚拟摇杆。
4.2 速度和边界参数怎么调
moveSpeed 我默认给 6,单位是米每秒,在这个 3D 坐标系里大约是一般角色的小跑速度。如果做追逐场面,给到 8 到 10;做讲究手感的对战,就给 3 到 4,并且可以在速度上叠加阻尼。limitX 和 limitZ 要根据地面尺寸设。地面 Plane 的 Scale 是 10,半长是 5,所以边界限制在 4 比较合理,留出半米防止人物站在平面外边沿。
这里还要注意人物的根节点 position.y 始终是 0。Plane 子节点负责视觉层,它的 y 可以根据人物脚底位置微调。如果你直接把贴片节点的 position 写到地面以下半截,人物看起来就会“穿地”。常见做法是人物根节点落地,子节点偏移,视觉层和逻辑层分开。
4.3 相机跟随:透视视角下的第三人称跟随
人物移动后,摄像机如果要一直看着角色,需要写一个跟随脚本。我这里用 lateUpdate,而不是 update,是为了让人物先走完这一帧,相机再跟着移动,减少一帧的滞后感。脚本如下:
const { ccclass, property } = cc._decorator; @ccclass export default class CameraFollow extends cc.Component { @property({ type: cc.Node }) target: cc.Node = null; @property({ type: cc.Float }) distance = 6; @property({ type: cc.Float }) height = 4; @property({ type: cc.Float }) lookTargetY = 1; lateUpdate() { if (!this.target) { return; } const p = this.target.position; // 相机放在人物后方斜上方 this.node.setPosition(p.x, this.height, p.z + this.distance); // 朝向人物躯干位置,不是朝向脚底 this.node.lookAt(cc.v3(p.x, this.lookTargetY, p.z)); } }distance 是摄像机在 Z 方向上离人物的距离,越大画面越平,透视越弱;height 控制俯视程度,height 越大越像俯视图。lookTargetY 推荐设成 1,因为 2D 人物的视觉中心大概在腰部到胸部之间,如果直接 lookAt 到(p.x, 0, p.z),人物会偏向屏幕上方,画面重心不对。
在 2.4.10 里,把这两个脚本都挂好后,可以在编辑器的预览窗口中直接跑。人物用 W/S/A/D 移动,镜头跟在斜后方,人物始终正面朝向镜头,背景则保持透视视角。这个状态适合作为进一步开发的基线版本。
5. 透视视角下的 5 个常见坑与排查思路
5.1 人物和地面一起闪烁、人物身上出现断层
现象是预览时人物或地面某些表面频繁闪烁,像有两层纸在互相“抢深度”。
原因通常是两个平面几乎完全重合,或者摄像机的 Near 设得太小,深度缓冲精度不足。人物贴片虽然是竖着的,但当你为了修遮挡把它降到接近地面时,它和地面之间的距离小于深度精度,就会闪。
解决方法是把人物根节点的 position.y 抬高到 0.01 或 0.05。它虽然和地面有一点点缝隙,但视觉上几乎看不出来。同时把摄像机的 Near 从 0.001 改回 0.1,给深度缓冲留出足够的精度区间。
5.2 人物周围出现黑色方块或白色底
现象是人物贴图能看见,但周围一圈是黑边或者整块白底,完全没有透明效果。
原因是材质没有开启透明混合。builtin-standard 材质默认是不透明模式,人物 PNG 的透明通道只有在 Rendering Mode 切到 Transparent 或 Blend 后才会参与混色。另外图片导入设置里如果 Alpha 被关闭,引擎会丢掉透明通道,材质再怎么改都白搭。
解决方法是先确认图片属性里 Alpha 开启,再把材质的 Rendering Mode 改成 Transparent。改完如果黑边变成彩色边,说明 PNG 本身带了预乘 alpha,需要在图片导入设置里把 Premultiply Alpha 选项也打开或关掉,来回切一次观察边缘变化。
5.3 Billboard 把人物转反、人物始终背对镜头
现象是 lookAt 生效了,人物没有躺平,但显示的是背面,或者人物侧着身子走。
原因是 Cocos Creator 2.4.10 的 lookAt 默认会让节点的某个轴向对准相机,这个轴不一定等于你的 Plane 正面朝向。每个角色的初始旋转不同,不能指望一个 lookAt 适配所有资源。
解决方法是不要直接让挂着贴片的 Plane 执行 lookAt。我给每个角色建一个根节点,Billboard 脚本挂在根节点上,Plane 作为子节点。子节点的初始旋转负责“正面朝哪个方向”,根节点的 lookAt 负责整体朝向。这样不管素材正面在哪,都只需要调整一次子节点。
5.4 人物移动时脚下飘浮,像在滑冰
现象是人物走路时脚和地面的接触点不固定,一会儿扎进地面,一会儿悬在半空。
原因是人物节点的位置代表的是锚点,也就是贴片中心,不是脚底。2D 人物素材的地面接触点通常在图片下方大约 80% 到 90% 的位置。如果你直接让节点 position.y 等于 0,就等于把人物中心钉在地面上,脚自然要往土里沉。
解决方法是把 Plane 子节点的 y 设置成负的半个到四分之一贴片高度,让人物脚底视觉上踩在根节点的 y=0 上。具体数值在编辑器里开着预览拖动,直到走起来不穿帮为止。这个偏移只属于视觉层,逻辑运动和碰撞都用根节点坐标。
5.5 人物走到屏幕边缘时明显拉伸变形
现象是背景没问题,但人物作为一张立牌移到画面边缘时,左右会变宽,看起来像被压扁的面饼。
原因是透视摄像机的 FOV 在宽屏幕上让边缘区域拉伸,人物立牌的尺寸越大越明显。2D 素材是平面的,拉伸后不会像 3D 模型那样有体积感,所以特别扎眼。
解决方法一是把 FOV 从 50 收到 40 左右,损失一点视野换边缘形变下降;二是限制人物的移动范围,不要让角色总贴屏幕边缘。也可以在代码里根据屏幕宽高比动态调 FOV,但那属于后期优化,项目前期先用固定 FOV 和边界限制最省事。
6. 进阶验证:把移动换成摇杆,再用“关背景、慢速度”验证整套链路
前面跑通后,可以顺手做两件事。第一件是把键盘移动抽象成一个公共接口,让虚拟摇杆也能复用。我在 PlayerController 里增加一个 moveByDir 方法,把键盘输入和摇杆输入统一成“给一个平面方向向量”。虚拟摇杆的常见实现是监听 TOUCH_MOVE,取摇杆拖动的偏移量,除以最大半径得到 0 到 1 的强度,再把偏移方向映射到 3D 空间的 x 和 z。
public moveByDir(moveX: number, moveZ: number) { const dir = cc.v3(moveX, 0, moveZ).normalize(); const offset = cc.v3( dir.x * this.moveSpeed * this._dt, 0, dir.z * this.moveSpeed * this._dt ); // 后续移动逻辑复用同一个 clamp 流程 }我这里用 _dt 表示 update 传入的帧间隔。实际项目中,摇杆回调拿到的偏移量要先乘一个强度系数,否则手指轻轻一碰角色就飞出边界。
验证方面,我的习惯是先把场景里所有背景模型隐藏,只留下地面和人物贴片,关闭摄像机跟随,用一个固定斜角度观察。这样能快速判断人物是不是每帧都正对镜头、移动方向是否和 WASD 一致。确认没问题后,把速度改成 1,继续走一遍,观察人物脚底是否和地面保持固定关系。最后再把背景恢复,依次让摄像机从人物正前、正侧、背后环绕一圈,看遮挡和透明材质是否正常。这套验证做完,基本就能确定整套方案能否进入正式开发了。
我最早做类似需求时一直把人物放在 Canvas 里,用屏幕坐标映射去模拟 3D 场景,后来一加树和墙的遮挡就变成手写遮挡判断,越写越乱。现在习惯是让 2D 人物作为 3D 空间里的贴片存在,把深度、透视、遮挡都交给引擎,反而省心很多。希望帮到你。
本文还有配套的精品资源,点击获取