简介:这是一份面向安卓移动开发课程设计的完整实战资源,基于 Cocos Creator v2.0 打造 2D 闯关小游戏,并配套完整源码、设计方案、PPT、演示视频与说明文档。资源既能作为计算机相关专业学生的课设/毕设参考,也适合想快速上手 Cocos Creator 的初学者跟做练习。压缩包共 312 个文件,大小仅 8.91MB,内含 JS 游戏逻辑脚本、场景与预制体配置、动画/音效资源、PNG 美术素材,以及 MD/Word 文档、MP4 演示视频和 PPT 汇报材料,结构清晰,便于按模块查阅;已有 73 人学习下载。代码经过运行验证,基本功能可正常使用,资源中还保留了角色跳跃、奔跑、死亡等关键动画文件与系统开发说明文档,能帮助读者理解 2D 闯关游戏的状态机设计与项目组织方式。结合 README 与文档说明,可快速跑通项目并据此扩展其他玩法。
1. 用 Cocos Creator v2.0 做 2D 闯关安卓小游戏,先想清楚这三件事
课程设计季最常见的场景:题目是“基于 Cocos Creator v2.0 的 2D 闯关安卓小游戏”,要求交的却是一整套:源码、APK、设计方案、PPT、演示视频。很多组把时间全砸在调碰撞和画角色上,最后打包和文档草草交差,答辩时被问两句就断片。
选 Cocos Creator v2.0 很实际:JS 脚本上手快,编辑器里拖场景所见即所得,构建面板能直接出安卓原生工程,没有安卓原生开发经验也能把 APK 打出来。v2.0 架构虽然旧,但课程设计相关的教程和案例多半以它为例,踩坑基本都能搜到现成的解法。
下面按「玩法实现 → 安卓打包 → 交付物 → 自检加分」的顺序,讲一条能落地的路线,每步给代码、给参数、给避坑点。
2. 用 Cocos Creator v2.0 实现 2D 闯关:关卡、角色、碰撞怎么串起来
闯关游戏最容易做歪的地方,不是角色动作多华丽,而是每关手搭一个场景、在编辑器里摆几十个节点。我一般会把关卡抽象成纯数据,用一个 Manager 按配置生成节点。这样做的好处有三条:加关卡只改配置不分叉场景;评估关卡难度只要看数值;答辩现场改一个数字就能“临时生成新关”,效果比念 PPT 强得多。
2.1 关卡数据驱动设计:用一份配置表生成整关
先定义第一关的数据,把平台、敌人、星星全部写进对象。
// assets/scripts/data/LevelConfig.js // 每关一份配置,后续加关卡只需要在末尾追加对象 module.exports = { level1: { needItem: 3, // 集齐 3 颗星星才允许过关 spawn: { x: -260, y: 80 }, // 玩家出生点 platforms: [ { type: 'ground', x: 0, y: -280, w: 1280, h: 80 }, { type: 'box', x: 420, y: -100, w: 200, h: 40 }, { type: 'box', x: 680, y: -40, w: 200, h: 40 } ], enemies: [ { x: 900, y: -280, moveRange: 160, speed: 100 } ], items: [ { x: 450, y: -50 }, { x: 700, y: 10 }, { x: 1000, y: -260 } ] } };再写 LevelManager,按配置实例化预制体:
// assets/scripts/manager/LevelManager.js const LevelConfig = require('../data/LevelConfig'); cc.Class({ extends: cc.Component, properties: { groundPrefab: cc.Prefab, // 编辑器里逐个从资源面板拖入 boxPrefab: cc.Prefab, enemyPrefab: cc.Prefab, itemPrefab: cc.Prefab }, loadLevel(id) { const cfg = LevelConfig['level' + id]; const map = this.node.getChildByName('Map'); map.removeAllChildren(); // 平台:按 w/h 改尺寸后再摆位,预制体保持一份即可 cfg.platforms.forEach(p => { const node = cc.instantiate(this[p.type + 'Prefab']); node.width = p.w; node.height = p.h; node.setPosition(p.x, p.y); map.addChild(node); }); // 敌人:实例化后把巡逻参数传进 EnemyCtrl,同一个预制体复用 cfg.enemies.forEach(e => { const node = cc.instantiate(this.enemyPrefab); node.setPosition(e.x, e.y); node.getComponent('EnemyCtrl').setup(e); map.addChild(node); }); // 星星:没有额外参数,直接批量生成 cfg.items.forEach(i => { const node = cc.instantiate(this.itemPrefab); node.setPosition(i.x, i.y); map.addChild(node); }); } });这段代码的逻辑是先找到 Canvas 下的空白节点 Map,把上一次的关卡节点全部清掉,再按配置生成新节点。type写成'ground'、'box',通过this[p.type + 'Prefab']动态取属性,就能让配置和预制体一一对应,不需要写分支判断。注意星星和敌人的预制体不能互相替换,setup(data)只在敌人身上调用,星星预制体上没有 EnemyCtrl 组件。
提示:编辑器里 Blueprint 的用处就在这里——同一个预制体,改配置就等于换一关。真正要放进 build 包的场景数量越少,构建时间和出错概率都越低。
2.2 玩家角色:刚体、碰撞体与跳跃手感调参
玩家节点上挂 Sprite、Animation、RigidBody(动态刚体)和一个 BoxCollider。碰撞体宽度不要铺满整个贴图,取角色身体宽度的 70%~80%,否则角色侧身擦过平台边缘时会被当成撞墙,这是最常见的“贴图没碰、人却卡住”的原因。
物理系统默认不开启,在玩家脚本的 onLoad 里打开并设重力:
// assets/scripts/player/PlayerCtrl.js cc.Class({ extends: cc.Component, properties: { moveSpeed: 320, // 水平移动速度 px/s jumpImpulse: 480, // 一次跳跃冲量,配合重力一起调 groundGroup: 'ground' }, onLoad() { const pm = cc.director.getPhysicsManager(); pm.enabled = true; pm.gravity = cc.v2(0, -1200); // 负值向下,绝对值越大滞空越短 this.body = this.getComponent(cc.RigidBody); this.body.enabledContactListener = true; // 不打开就收不到碰撞回调 this.groundContacts = 0; // 键盘给预览和编辑器调试用 cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKey, this); // 触摸给安卓真机用 cc.systemEvent.on(cc.SystemEvent.EventType.TOUCH_START, this.onTouch, this); }, onKey(e) { if (e.keyCode === cc.macro.KEY.space) this.jump(); }, onTouch() { this.jump(); }, update() { // 只在水平方向改速度,垂直方向交给物理引擎 let vx = 0; if (this.leftBtn && this.leftBtn.pressed) vx -= this.moveSpeed; if (this.rightBtn && this.rightBtn.pressed) vx += this.moveSpeed; this.body.linearVelocity = cc.v2(vx, this.body.linearVelocity.y); if (vx !== 0) this.node.scaleX = vx > 0 ? 1 : -1; // 左右翻转朝向 }, jump() { if (this.groundContacts <= 0) return; // 不在地面不允许二段跳 // 先清空垂直速度,再打冲量,否则连续起跳会越跳越高 const v = this.body.linearVelocity; this.body.linearVelocity = cc.v2(v.x, 0); this.body.applyLinearImpulse(cc.v2(0, this.jumpImpulse), this.body.getWorldCenter(), true); }, onBeginContact(contact, self, other) { if (other.node.group === this.groundGroup) this.groundContacts++; }, onEndContact(contact, self, other) { if (other.node.group === this.groundGroup) this.groundContacts--; } });这里有个容易被忽略的细节:groundContacts是累计值而不是布尔值。角色同时站在两块平台上的时候,一块平台脱离接触,计数减一还有另一块兜底;如果用布尔值,会掉下去但状态没变,表现就是“按了跳跃没反应”。既然物理在模拟,就不要手动改 node 的 position,改位置会跟刚体解算打架,轻则抖动,重则穿模。
真机没有键盘,UI 上放左右两个虚拟按钮,各自挂一个 VirtualButton:
// assets/scripts/ui/VirtualButton.js cc.Class({ extends: cc.Component, properties: { dir: -1 // 左按钮填 -1,右按钮填 1 }, onLoad() { this.pressed = false; this.node.on(cc.Node.EventType.TOUCH_START, () => { this.pressed = true; }, this); this.node.on(cc.Node.EventType.TOUCH_END, () => { this.pressed = false; }, this); this.node.on(cc.Node.EventType.TOUCH_CANCEL, () => { this.pressed = false; }, this); } });然后把这两个节点拖到 PlayerCtrl 的 leftBtn、rightBtn 属性里。需要说明的是,TOUCH_CANCEL 必须处理,手指滑出按钮区域时系统会发 CANCEL,不处理的话状态会卡在按下去。
手感参数以如下为起点,调一轮大约需要 10 分钟:
| 参数 | 起点值 | 调整方向 |
|---|---|---|
| gravity.y | -1200 | 绝对值变大,跳跃干脆、滞空短;变小则飘 |
| jumpImpulse | 480 | 决定跳跃高度,配合平台纵向间距换算 |
| moveSpeed | 320 | 决定水平距离,平台横向间距按它来排 |
| BoxCollider 宽度 | 角色宽 70%~80% | 太大蹭墙,太小视觉上“没碰到也算碰” |
注意:跳跃距离 ≈ 水平速度 × 滞空时间。改 moveSpeed 时一定要回头复核平台间距,这是闯关手感出问题的第一来源。
2.3 障碍、收集与关卡切换:碰撞分组和状态流转
物理碰撞矩阵在「项目设置 → 分组管理」里配置。先建四个分组:ground、player、enemy、item,然后把玩家节点的 Group 设为 player,平台设为 ground,敌怪设为 enemy,星星设为 item。矩阵按下面这张表勾:
| 分组 | 需要碰撞的对象 | 用途 |
|---|---|---|
| ground | player、enemy | 玩家落地判定,敌人在地面巡逻 |
| enemy | player、ground | 敌人保持在地面,也能对玩家造成伤害 |
| item | player | 只跟玩家碰撞,收集后消失 |
| player | ground、enemy、item | 所有交互都在玩家身上收口 |
敌人巡逻用 EnemyCtrl 实现:
// assets/scripts/enemy/EnemyCtrl.js cc.Class({ extends: cc.Component, properties: { speed: 100, moveRange: 160 }, setup(data) { if (data) { this.speed = data.speed; this.moveRange = data.moveRange; } this.originX = this.node.x; }, update() { // 超过巡逻边界就反向;原点 +- range 作为边界 if (Math.abs(this.node.x - this.originX) >= this.moveRange) { this.speed *= -1; } const b = this.getComponent(cc.RigidBody); b.linearVelocity = cc.v2(this.speed, b.linearVelocity.y); } });敌人必须也挂 RigidBody 并选动态类型,才能跟地面产生物理碰撞,否则会直接飘着穿过平台。这个脚本的写法有一点粗糙:速度在边界反复反转时可能会在临界点抖动,问题不大,但答辩前建议把它替换成用originX + cos(t)的往返公式,视觉上更顺滑。
收集星星和玩家死亡都放在一个全局单例 GameMgr 里处理:
// 挂在 UI 根节点上的 GameMgr.js window.Game = window.Game || { score: 0, currentLevel: 1 }; cc.Class({ extends: cc.Component, restart() { if (window.Game.score > 0) { window.Game.score = 0; } cc.director.loadScene('Game'); // 重新加载主场景,LevelManager 按 currentLevel 重建 }, onItemGot() { window.Game.score++; const cfg = require('../data/LevelConfig'); const need = cfg['level' + window.Game.currentLevel].needItem; if (window.Game.score >= need) { window.Game.currentLevel++; cc.director.loadScene('Game'); } } });为什么不把分数和关卡号挂在组件属性上?因为 loadScene 会销毁当前场景的节点,属性值跟着没了。挂到 window 上的对象跨场景存活,这是 Creator 里最简单直接的存档方式。玩家撞到敌人时,在 PlayerCtrl 的 onBeginContact 里判断other.node.group === 'enemy',调到 GameMgr.restart() 即可。
3. 用 Cocos Creator v2.0 打包安卓 APK:构建配置与真机适配
新人在这一步最容易卡住,而且往往是环境问题而不是代码问题。先明确 Cocos Creator v2.0 的构建机制:构建面板做的是“生成安卓原生工程 + 拷贝资源 + 编译脚本”,最终 APK 由 Android Studio 完成。所以后面要用两个工具,别指望 Creator 一个按钮直接出包。
3.1 构建发布面板的几个必改项
打开「项目 → 构建发布」,平台选 Android,构建一次,输出到项目下的 build/android。构建面板里值得逐项确认的配置如下:
| 配置项 | 推荐设置 | 理由 |
|---|---|---|
| 包名 | com.jx.classdesign | 反域名格式,不能以数字开头,不能含中文 |
| 构建路径 | build/android | 默认即可,交给答辩的源码包要排除该目录 |
| API Level | minSdk 21,target 28 附近 | 覆盖考场绝大多数安卓 9 / 安卓 10 真机 |
| ABI | arm64-v8a、armeabi-v7a | 只保留 ARM 架构,去掉 x86 能明显减小包体 |
| 调试模式 | 正式演示前关掉 | 开启后每帧跑 debug 逻辑,帧率更低 |
v2.0 时代对应的原生工具链是 JDK 8 + Gradle 4.x/5.x,NDK 推荐 r16b 或 r18。电脑上如果装的是 JDK 17,构建时会直接报 Gradle 不兼容,这不是你代码的问题,换 JDK 8 就好。首次构建会下载大量依赖,建议提前连网构建一次,别到答辩前一天才第一次开项目。
3.2 用 Android Studio 出 release APK 的落地步骤
构建完成后,产物目录下就是安卓工程(不同 2.x 小版本结构略有差别,但一定有 build.gradle 和 gradlew)。用 Android Studio 打开这个目录,等 Gradle 同步完成。之后有两种打包方式:菜单Build → Generate Signed Bundle / APK按向导操作,或者在工程的 build.gradle 里直接配置签名:
// 放在 android 节点里 android { signingConfigs { release { storeFile file('release.keystore') // 用 keytool 生成,或 AS 向导生成后放这里 storePassword 'demo123456' keyAlias 'release' keyPassword 'demo123456' } } buildTypes { release { minifyEnabled false signingConfig signingConfigs.release } } }配置好之后,在工程根目录执行:
./gradlew assembleRelease生成的 APK 在app/build/outputs/apk/release/下。需要说明两个常见坑:第一,签名文件要提交到源码包但保管好密码,老师重新打包时会用到;第二,用 debug 签名打的包也能装,但换一台机器重打后包名相同签名不同,会提示卸载重装,答辩前用同一台电脑、同一个签名文件出包最稳妥。
有些同学喜欢直接把 build/android 整个目录打进源码包,这是不对的,解压到新电脑后会带着你本机的绝对路径。源码包里只保留 assets、project.json、package.json 等 Creator 工程文件,build 目录和 temp 目录全部排除,文档里写明“打开 Creator 后重新构建一次”即可。
3.3 竖屏锁向、屏幕适配与刘海屏安全区
2D 闯关按竖屏做,比例建议 720×1280。Canvas 节点上把设计分辨率改成 720×1280,勾选 Fit Width,取消 Fit Height。对应的脚本等价写法是:
// 放在 GameMgr 的 onLoad 里,优先级高于 Canvas 面板设置 cc.view.setDesignResolutionSize(720, 1280, cc.ResolutionPolicy.FIXED_WIDTH);FIXED_WIDTH 的含义是:宽度始终铺满 720 逻辑像素,高度按真机屏幕比例伸缩。竖屏游戏用这个策略,左右永远不会被裁切,上下多出来的区域用来做背景延伸。副作用是不同屏幕看到的纵向范围不同,所以 UI 按钮不要贴边太近。
锁竖屏要到安卓工程里改,找到游戏主 Activity:
<!-- AndroidManifest.xml 中游戏主 Activity 增加属性 --> <activity android:name="org.cocos2dx.javascript.AppActivity" android:screenOrientation="portrait" android:configChanges="orientation|screenSize|keyboardHidden" />刘海屏适配不要自己算,Creator 2.x 的组件库里自带 SafeArea,拖到 UI 根节点或底部按钮节点的子节点上,运行时会自动读取安全区并限制内容不进入挖孔区域。如果跑在旧手机上没刘海,这个组件不会产生任何额外偏移,放心挂。
4. 把设计方案、PPT、演示视频做成答辩加分的组合拳
很多组最后只交了 APK 和源码,PPT 是保姆级复制别人的模板,演示视频是手机随手一拍。这四样东西其实是同一套逻辑:让一个没打开你项目的人,在 10 分钟内看懂你做了什么、怎么做、有没有用心。
4.1 设计方案文档按“评审视角”组织
文档不要从“项目背景”这种万金油章节开场,评审只会看三个问题:需求是什么、你用什么方案实现、为什么用这个方案。推荐结构:
| 章节 | 内容要点 |
|---|---|
| 需求分析 | 玩法一句话、功能清单、目标机型与系统版本 |
| 总体设计 | 场景划分、节点树截图、物理分组表、状态流转 |
| 详细实现 | PlayerCtrl、LevelManager、EnemyCtrl 的核心代码片段配注释 |
| 测试结果 | 真机列表、帧率数据、碰到并解决的问题 |
| 总结 | 个人分工、难点攻克、可改进方向 |
详细实现一章,截图比代码更重要。编辑器里把「层级管理器」和「属性检查器」截图,标注出刚体、碰撞体、分组三个关键属性,评审一眼就能看出你理解组件体系。代码段只放关键 20 行前后,全文贴源码会暴露你抄作业的事实。
4.2 演示视频:用 scrcpy 录真机画面再压一遍
手机拍屏幕的问题是对焦飘、反光、旁边有杂音。我一般用 scrcpy 把安卓真机画面投到电脑,同时录原始视频:
adb devices # 先确认设备连接 scrcpy --max-size 1080 --max-fps 30 --record demo_raw.mp4录完再交给 ffmpeg 做压缩和转码,为的是让视频体积控制在 50MB 以内,老师手机能直接看:
ffmpeg -i demo_raw.mp4 -c:v libx264 -b:v 3M -pix_fmt yuv420p -c:a aac -movflags +faststart demo_final.mp4参数说明:-b:v 3M限制视频码率,-pix_fmt yuv420p保证任何播放器都能解码,-movflags +faststart让视频边下边播,不用等全部加载。如果不想装 scrcpy,用 Android Studio 自带的 Logcat 提供的设备录屏也行,但分辨率固定,不如 scrcpy 灵活。
视频时长控制在 90~150 秒:前 10 秒一句话说明玩法,接着 15 秒展示编辑器场景结构和物理分组,中间 60 秒是完整的真机通关演示,包括故意死一次再重试,最后 15 秒切到代码编辑器讲其中一段关键实现,结尾放一张关卡数据配置截图。演示视频里没有真人声音也没关系,背景音乐 + 字幕就是及格水准。
4.3 PPT 结构、讲稿节奏与常见扣分点
PPT 控制在 10~14 页,每页只留一个结论。功能清单页用表格列功能优先级,物理分组页贴碰撞矩阵的截图,代码页不要粘贴源码而是用伪代码表达流程。页与页之间删掉“谢谢观看”之类的过渡页,答辩时间很紧凑。
常见的扣分点有三个:一是 PPT 和实际项目版本对不上,PPT 里写“支持暂停”,演示的时候没这功能,这是立场性问题;二是视频里用的关卡和现场演示的不是同一关,评审会直接问“这关是哪来的”;三是把源码包发给老师前没有打开验证过,解压报错印象分直接清零。我的做法是提交前把 zip 解压到另一台电脑,按 README 重新构建一遍再交。
5. 闯关可玩性加分项与答辩前的快速自检
前面做到了,项目已经及格。想在答辩现场被多问几句、多拿几分,加两个手感部件:土狼时间和跳跃缓冲。这是平台跳跃类游戏标准的手感优化,代码量很小,但效果极其明显。
5.1 手感加分:土狼时间与跳跃缓冲
土狼时间的意思是,玩家走出平台边缘后的 0.1~0.15 秒内仍然允许起跳,消除“明明站在边缘却跳不起来”的挫败感。跳跃缓冲是反过来:玩家在落地前 0.1 秒按下跳跃按钮,落地瞬间自动弹出跳跃,消除“按了没反应”的延迟感。
// PlayerCtrl.js 中追加两个计时器属性 properties: { coyoteTime: 0.12, // 掉出平台后仍可起跳的宽容时间 jumpBufferTime: 0.1 // 落地前按跳,保留 0.1 秒的跳跃意图 }, update(dt) { // 土狼时间:在地面清零,离开地面开始累计 this.coyoteTimer = this.groundContacts > 0 ? 0 : this.coyoteTimer + dt; // 跳跃缓冲:记录“想跳”的意图,持续递减 this.bufferTimer = Math.max(0, this.bufferTimer - dt); }, onTouch() { this.bufferTimer = this.jumpBufferTime; // 先记意图,不一定马上跳 this.jump(); }, jump() { // 条件:土狼时间未超限,且跳跃意图还保留着 if (this.coyoteTimer < this.coyoteTime && this.bufferTimer > 0) { this.bufferTimer = 0; this.coyoteTimer = this.coyoteTime; // 防止一次落地触发连跳 const v = this.body.linearVelocity; this.body.linearVelocity = cc.v2(v.x, 0); this.body.applyLinearImpulse(cc.v2(0, this.jumpImpulse), this.body.getWorldCenter(), true); } }实现原理是两套独立计时器:coyoteTimer 判断“还能不能跳”,bufferTimer 判断“有没有跳的意图”,两者同时成立才真正执行冲量。加完之后把 coyoteTime 调到 0.12、jumpBufferTime 调到 0.1,让三个没玩过的人试玩,观察他们是不是“明明感觉能跳却跳不出来”,如果是,把 coyoteTime 加到 0.15。
5.2 物理调试、FPS 与提交前的快速自检
物理调试对新手做碰撞特别有用,把调试绘制打开,屏幕上会画出所有碰撞体和关节:
// GameMgr 里临时打开,发布前必须注释掉 const pm = cc.director.getPhysicsManager(); pm.debugDrawFlags = pm.DrawBits.e_aabbBit | pm.DrawBits.e_shapeBit;再用几行代码挂一个 FPS 标签,真机上手一测就知道自己的优化水平:
// FpsLabel.js,挂在 Canvas 下的文本节点上 cc.Class({ extends: cc.Component, update() { this.getComponent(cc.Label).string = Math.round(1 / cc.director.getDeltaTime()) + ' FPS'; } });提交前按下面这组问题在真机上过一遍,比任何自查表都实在:
- 连续两次快速起跳,穿过薄平台后会不会复位?角色卡进平台内超过 0.5 秒视为碰撞体参数不合格。
- 死亡重试后,分数、位置、敌人巡逻原点是否全部归位?
- 安卓返回键处理没有?菜单层按返回应该弹确认框,游戏中按返回应该暂停或回菜单,直接闪退是硬伤。
- 锁屏 30 秒再恢复,游戏计时和音效状态是否正常?
- 同一关在 720×1280 和 1080×1920 两台真机上各跑一遍,按钮是否被遮挡、画面是否被裁切?
最后把调好的手感参数备份到 LevelConfig 里,换机器重打包也不至于丢了那套试了两个小时的数值。
本文还有配套的精品资源,点击获取