简介:这份资源面向使用 Cocos Creator 2.4.10 进行游戏开发的初学者与进阶开发者,聚焦 2D 人物与 3D 背景混合场景的搭建,解决人物移动控制与透视摄像机视角设置等常见问题。压缩包共 33 个文件,约 313KB,包含 4 个 ts 脚本用于人物移动与输入监听逻辑,4 个 png 贴图与 1 个 atlas 图集承载 2D 人物素材,6 个 json 与 1 个 fire 场景文件记录项目配置与场景布局,另有 meta、creator.d.ts 等工程声明文件,目录结构完整可直接导入编辑器运行。目前已有 464 人学习下载。通过该工程,读者可参考 2D 精灵与 3D 模型共存的节点组织方式,理解透视摄像机参数对空间纵深感的影响,并借鉴脚本中位置更新与键盘、触摸输入的绑定思路,快速搭建可移动角色的混合维度游戏原型。
1. 2D人物踩在3D地面上:这个Cocos Creator项目到底在做什么
如果你做过2D横版或者俯视角游戏,大概都遇到过同一个尴尬:人物是纸片,背景也是纸片,整个画面平得像一张贴纸。想加点纵深感,要么硬画透视背景,要么干脆转3D引擎重做,成本都不低。这个基于Cocos Creator 2.4.10的项目走了一条中间路线——用2D精灵做人物,用3D模型搭场景,再让摄像机带一点透视角度,人物在3D地面上移动时会产生近大远小的空间感。它适合两类人:一类是想在2D项目里低成本引入3D背景的独立开发者,另一类是正在学Cocos Creator、想搞明白2D与3D混合渲染到底怎么落地的新手。项目本身不复杂,但把「2D人物 + 3D背景 + 透视视角 + 移动控制」这条链路完整跑通了,拿来当混合渲染的起点正合适。
2. 场景搭建与节点结构:2D和3D怎么塞进同一个场景
2.1 先搞清楚Cocos Creator 2.4.10的渲染分层
Cocos Creator 2.4.x 的渲染体系里,2D和3D并不是两套完全隔离的世界,它们共享同一个场景树,但走不同的渲染管线。2D节点(cc.Node 挂 cc.Sprite)默认由2D渲染层处理,3D节点(挂 cc.MeshRenderer 之类)走3D管线。关键在于摄像机的配置——2.4.x 里摄像机有 visibility 和 clearFlags 两个属性,决定了它能看到哪些层、清屏时清什么。
这个项目的核心思路是:主摄像机保持透视投影(Perspective),让它同时能看到3D地面和2D人物。人物虽然是2D精灵,但因为摄像机是透视的,只要把人物节点放在3D空间里合适的位置,它就会跟着透视关系产生缩放效果。听起来简单,实际配置时最容易翻车的就是层级和深度问题。
2.2 节点树长什么样
一个能跑起来的典型节点结构大致是这样:
Scene ├── Main Camera (透视摄像机,visibility含2D和3D层) ├── 3D Environment │ ├── Ground (3D平面/地形模型) │ ├── Props (场景装饰物,3D模型) │ └── Directional Light(平行光,照亮3D场景) ├── 2D Character │ ├── Body (cc.Sprite,人物主体) │ └── Shadow (cc.Sprite,脚下阴影) └── UI Canvas (2D UI层,独立摄像机或同摄像机)这里有个细节值得说:人物节点虽然挂的是2D精灵,但它的 position 是在3D空间里计算的。也就是说,人物的 x、y、z 三个坐标都参与运算,z 值决定了它在3D场景中的前后位置。地面用3D平面,人物贴着地面走,摄像机从斜上方往下看,透视感就出来了。
2.3 摄像机参数怎么调
摄像机是这个项目的灵魂,参数不对,要么人物被3D模型挡住,要么透视感全无。常见做法是把摄像机放在场景斜上方,朝向地面中心:
// Main Camera 的关键配置(在编辑器里设置,也可代码动态改) // 投影模式:透视 camera.projection = cc.Camera.ProjectionType.PERSPECTIVE; // 视场角,45~60度之间透视感比较自然 camera.fov = 50; // 摄像机位置:斜上方俯视 camera.node.setPosition(0, 8, 10); // 看向场景中心 camera.node.lookAt(cc.v3(0, 0, 0)); // 可见层:同时包含2D和3D层 camera.visibility = cc.Camera.VisibilityFlag.DEFAULT | cc.Camera.VisibilityFlag.UI_2D;fov 这个参数直接决定透视强度。fov 越大,近处物体越大、远处越小,空间感越强,但太大画面会变形;fov 太小就接近正交投影,透视感消失。我一般从50开始试,根据场景大小微调。摄像机位置也要注意,y 太高会变成俯视地图,y 太低又看不到地面纵深,8到12之间比较稳妥。
注意:2.4.x 里如果摄像机的 clearFlags 设成 SOLID_COLOR,3D场景和2D人物会一起被清屏重绘,顺序不对就会出现人物闪烁。建议把3D和2D放在同一个摄像机下渲染,或者用 depth 控制渲染顺序。
3. 人物移动与透视跟随:从输入到坐标更新的完整链路
3.1 输入监听与移动方向计算
人物移动的输入部分很常规,监听键盘或者触摸,算出移动方向向量。但因为这个项目是透视视角,移动方向不能简单用屏幕坐标,得转换到3D世界空间里。
// CharacterController.js cc.Class({ extends: cc.Component, properties: { moveSpeed: 5, // 移动速度,单位/秒 groundNode: cc.Node, // 3D地面节点引用 }, onLoad() { // 注册键盘输入 cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); this.moveDir = cc.v3(0, 0, 0); }, onKeyDown(event) { switch(event.keyCode) { case cc.macro.KEY.w: this.moveDir.z = -1; break; // 向前 case cc.macro.KEY.s: this.moveDir.z = 1; break; // 向后 case cc.macro.KEY.a: this.moveDir.x = -1; break; // 向左 case cc.macro.KEY.d: this.moveDir.x = 1; break; // 向右 } }, onKeyUp(event) { // 松开按键时清零对应方向 switch(event.keyCode) { case cc.macro.KEY.w: case cc.macro.KEY.s: this.moveDir.z = 0; break; case cc.macro.KEY.a: case cc.macro.KEY.d: this.moveDir.x = 0; break; } }, });这里用 w/s 控制 z 轴、a/d 控制 x 轴,是因为摄像机是斜俯视的,z 轴方向在屏幕上表现为「上下」,x 轴表现为「左右」。如果摄像机角度变了,这套映射也要跟着调。moveDir 是个三维向量,归一化之后乘以速度和时间就是每帧的位移量。
3.2 每帧更新位置并限制在地面范围内
移动逻辑放在 update 里,核心是「方向归一化 → 乘速度和时间 → 累加到当前位置」。同时要限制人物不能走出地面边界,否则会掉进虚空。
update(dt) { if (this.moveDir.mag() < 0.01) return; // 没有输入就不动 // 归一化,保证斜向移动不会更快 let dir = this.moveDir.normalize(); // 计算本帧位移 let delta = cc.v3(dir.x * this.moveSpeed * dt, 0, dir.z * this.moveSpeed * dt); // 更新人物位置 let pos = this.node.position.add(delta); // 边界限制:假设地面是20x20的平面,中心在原点 let halfSize = 10; pos.x = cc.misc.clampf(pos.x, -halfSize, halfSize); pos.z = cc.misc.clampf(pos.z, -halfSize, halfSize); // y 保持贴地高度,人物脚底对齐地面 pos.y = 0.5; this.node.setPosition(pos); }cc.misc.clampf是Cocos自带的钳制函数,把值限制在区间内。边界值 halfSize 要根据实际地面尺寸改,地面多大就设多大。人物 y 值设成0.5是因为2D精灵的锚点默认在中心,要让脚底贴地,得把节点抬高半个精灵高度。这个数值每个人物不一样,得根据精灵尺寸调。
3.3 透视视角下的人物朝向与缩放
透视摄像机下,人物离摄像机近就大、远就小。如果项目需要人物始终面向移动方向,可以用简单的旋转:
// 根据移动方向旋转人物(假设人物默认朝前) if (dir.mag() > 0.01) { // atan2 算出水平面内的角度 let angle = Math.atan2(dir.x, dir.z) * 180 / Math.PI; // 平滑插值,避免瞬间转向 let current = this.node.eulerAngles.y; let target = angle; this.node.eulerAngles = cc.v3(0, cc.misc.lerp(current, target, 0.2), 0); }Math.atan2(dir.x, dir.z)返回的是弧度,转成角度后赋给节点的 y 轴旋转。cc.misc.lerp做线性插值,让转向有个过渡,不会太生硬。如果人物是2D精灵,旋转时要注意精灵本身是否支持多方向贴图,否则转过去还是同一张图,看起来会很怪。常见做法是准备四方向或八方向的序列帧,根据角度切换。
提示:透视视角下人物缩放是自动的,不需要手动改 scale。但如果你发现人物在远处变得太小看不清,可以给摄像机加一个最小缩放限制,或者把人物节点单独放到一个不受透视影响的层里。
4. 避坑与常见问题:混合渲染最容易翻车的几个点
4.1 人物被3D模型遮挡,明明站在前面却看不见
现象:人物走到某些3D道具后面,直接被挡住消失,但逻辑上人物应该在前方。
原因:2D精灵和3D模型的渲染顺序由摄像机的 depth 和节点的 z 值共同决定。2D节点默认可能被排到3D之后渲染,或者 z 值计算方式和3D不一致。
解决:把人物节点的 z 值设得比3D道具更靠近摄像机,或者在人物上挂一个透明的3D包围盒来参与深度测试。更稳妥的做法是把2D人物和3D场景放在同一个渲染队列里,用节点的 worldPosition.z 来控制前后关系。
4.2 移动时人物抖动或跳帧
现象:人物移动不流畅,偶尔会瞬移一下。
原因:update 里直接改 position 没有考虑帧率波动,或者输入方向没有归一化导致斜向移动速度异常。
解决:位移计算必须乘 dt(delta time),保证不同帧率下速度一致。方向向量一定要 normalize,否则斜向移动会变成1.414倍速度。另外检查是否有其他地方也在改人物位置,多个脚本同时操作同一个节点会打架。
4.3 透视摄像机下UI错位或变形
现象:血条、按钮这些UI元素跟着3D透视一起变形了。
原因:UI Canvas 和3D场景共用了同一个透视摄像机。
解决:给UI单独建一个正交投影的摄像机,或者把UI Canvas的 renderMode 设成独立模式。2.4.x 里推荐用两个摄像机:一个透视的管3D和2D人物,一个正交的管UI。两个摄像机的 visibility 分开设置,互不干扰。
4.4 地面边界判断不准,人物走到边缘还在走
现象:人物已经走出地面模型了,但还能继续移动,看起来像悬空。
原因:边界限制用的是硬编码的数值,和实际地面尺寸不匹配。
解决:把地面尺寸做成可配置的属性,或者直接从地面节点的 boundingBox 读取实际大小。如果地面是不规则形状,就得用碰撞检测或者寻路网格来判断可行走区域,不能简单用矩形钳制。
4.5 2D精灵在3D空间里光照不对
现象:3D场景有明暗变化,但2D人物始终一个亮度,显得很突兀。
原因:2D精灵默认不受3D光照影响,走的是自己的渲染管线。
解决:如果想让2D人物也受光照影响,可以改用3D材质渲染精灵,或者手动根据人物位置计算一个亮度值,动态调整精灵的 color 属性。简单做法是给人物加一个跟随的2D光源效果,用叠加一层半透明黑色精灵来模拟阴影。
5. 二次开发与改造:把这个Demo变成你自己的项目
5.1 换人物和场景资源
项目里的人物是2D精灵,场景是3D模型,替换起来很直接。人物部分,把 cc.Sprite 的 spriteFrame 换成你自己的序列帧就行,注意锚点要设在脚底,不然贴地会飘。场景部分,把3D模型拖进场景,调整位置和缩放,确保地面在 y=0 附近。如果换了地面尺寸,记得同步改移动脚本里的边界值。
// 把边界值改成从地面节点动态获取 properties: { groundNode: cc.Node, // 拖入3D地面节点 }, onLoad() { // 读取地面模型的包围盒 let box = this.groundNode.getComponent(cc.MeshRenderer).model.worldBounds; this.halfX = box.size.x / 2; this.halfZ = box.size.z / 2; }, update(dt) { // 用动态边界替代硬编码 pos.x = cc.misc.clampf(pos.x, -this.halfX, this.halfX); pos.z = cc.misc.clampf(pos.z, -this.halfZ, this.halfZ); }这样换场景就不用改代码了,地面多大人物就能走多远。
5.2 加碰撞和交互
纯移动太单调,实际项目里人物要能撞墙、捡东西、触发事件。2D人物和3D场景做碰撞,常见做法是在3D场景里放隐形的碰撞体,人物移动时做射线检测或者简单的距离判断。
// 简单的圆形碰撞检测 checkCollision(newPos) { for (let obstacle of this.obstacles) { let dist = cc.v2(newPos.x - obstacle.x, newPos.z - obstacle.z).mag(); if (dist < obstacle.radius + this.radius) { return true; // 碰撞了 } } return false; }obstacles 是一个数组,存了每个障碍物的位置和半径。移动前先算一下新位置会不会撞,会撞就不让走。这种方法适合障碍物不多、形状简单的场景。复杂场景还是得上物理引擎或者寻路网格。
5.3 调整透视强度和摄像机跟随
如果觉得透视太强或太弱,改摄像机的 fov 就行。想让摄像机跟着人物走,把摄像机节点设成人物节点的子节点,或者每帧把摄像机位置设成人物位置加一个偏移量。
// 摄像机跟随,保持固定偏移 lateUpdate(dt) { let targetPos = this.character.position.add(this.offset); // 平滑跟随,避免抖动 this.cameraNode.position = this.cameraNode.position.lerp(targetPos, 0.1); }offset 是摄像机相对人物的偏移向量,比如 (0, 8, 10)。用 lerp 做平滑,跟随会有点延迟感,看起来更舒服。lateUpdate 在 update 之后执行,保证人物位置已经更新完了再动摄像机。
5.4 性能优化要注意的地方
2D和3D混合渲染比纯2D更吃性能,尤其是3D模型面数多的时候。几个优化方向:3D场景尽量用低模,远处物体用LOD或者直接剔除;2D人物的序列帧图集要合并,减少draw call;摄像机的 visibility 只开必要的层,别把UI层也塞进透视摄像机。如果目标平台是移动端,fov 别设太大,透视计算量会随视锥体增大而增加。
提示:2.4.10 的3D渲染在移动端上性能一般,如果场景复杂,建议把3D背景烘焙成静态贴图,只在需要透视感的地方保留少量3D模型。
5.5 一个我踩过的坑
刚开始做这个项目时,我把人物节点直接放在3D地面节点的子节点下,想着这样人物就能跟着地面一起动。结果人物移动时坐标全乱了,因为子节点的 position 是相对父节点的,地面一旋转,人物的移动方向也跟着转。后来把人物和地面拆成平级节点,人物位置用世界坐标算,才正常。从那以后我每次做混合场景,都强制把2D和3D节点放在平级,坐标全部用世界坐标算,不再嵌套。希望这个经验能帮你少走点弯路。
本文还有配套的精品资源,点击获取