简介:一套基于 Cocos Creator 制作的「天天酷跑」风格 2D 跑酷游戏源码包,面向已掌握引擎基础操作、希望深入学习动画与动作系统的初中级开发者。资源源自精选实战教程(2)的配套代码,聚焦角色动画切换、帧动画配置、行为树与动作控制及性能优化等关键环节,可直接在 Cocos Creator 中打开工程对照学习,也可作为二次开发起点。压缩包共包含 46 个文件,体积约 6.98MB,以 anim 动画剪辑、png 贴图、js/ts 逻辑脚本、fire 场景文件及 plist 配置为主,结构与工程说明清晰,便于定位动画、脚本和资源模块。已有 132 人学习下载。压缩包内含完整 Cocos Creator 工程目录、配置文件、README 与许可说明,适合需要参考帧动画/动作系统设计、项目结构管理及源码打包流程的开发者,对照着动手改造或移植到自己的 2D 跑酷项目,能更快理解动画驱动与角色行为相关的工程实现。
1. 天天酷跑源码包里的动画与动作:这份 Cocos Creator 实战教程到底能让你练到什么
拿到《天天酷跑 Cocos Creator 实战教程(2)》源码包,多数人第一反应是先把构建跑通、装到手机上试玩一把。真正打开工程才发现,做跑酷玩法,动画和动作相关代码占了至少一半工作量:跑动循环、起跳与落地帧、滑铲压扁拉伸、跳跃拖尾,每一处都绕不开动作系统的组织方式。这份源码的价值,是用竖屏跑酷的骨架把 Animation 组件、动作状态机、手感参数和源码打包串成一条完整链路,适合已经会拖节点、想正经写游戏逻辑的人,也适合想复盘跑酷动作组织思路的学习者。
2. 动作系统的组织:跑酷玩法为什么要把"跑、跳、滑铲"拆成独立状态
2.1 跑酷玩法对动画的三个硬要求:循环、打断、互斥
天天酷跑这类竖屏跑酷的核心玩法一句话就能讲完:角色自动往前跑,玩家点一下屏幕起跳,遇到横杆下滑铲。玩法越简单,动作表现的容错空间就越小,只要跳跃响应慢了一帧,玩家就会撞障碍,然后把账算到"手感差"头上。所以动作系统的组织,本质上是在满足三个硬要求:循环、打断、互斥。
循环指的是跑动动画必须无缝衔接,循环点不能出现跳帧或 pose 突变;打断指的是跳跃、滑铲这类操作必须能随时中断当前动画,并在下一帧就生效,切换延迟趋近于零;互斥指的是"空中不能滑铲、滑铲中不能再跳"这类规则必须被强制约束,而不是靠"恰好没人触发"来维持。三个要求叠在一起,结论非常明确:角色的动作不能散落成一堆 if-else 临时播放动画,而应该收敛成一个状态机。
这也是这类教程源码里动作部分最值得读的地方。拿到源码包,先别急着看地图生成和碰撞逻辑,把主角节点挂了哪些组件、动画剪辑叫什么名字、哪个脚本在统一发"切换动作"的调用找出来,整份代码的骨架就清楚了。常见做法是把这些逻辑收敛到一个 CharacterAction 之类的脚本里,所有要切动作的地方都调它的接口,而不是各自去 anim.play()。
2.2 动画资源的分层:AnimationClip、帧序列图与 Tween 各自管什么
在 Cocos Creator 里能驱动画面"动起来"的手段有三种,很多新手把三者混着用,导致同一个属性的写入方不固定,最后动画互相覆盖。先分清它们各自管什么,再决定主角用哪种。
| 驱动方式 | 适用场景 | 控制入口 | 资源形态 |
|---|---|---|---|
| cc.Animation + AnimationClip | 循环跑动、跳跃曲线、滑铲姿态 | anim.play('run') | .anim 剪辑 + 贴图 |
| 帧序列图动画 | 爆炸、受击闪光、落地尘土 | sprite.spriteFrame = frames[i] | 图集 / 序列帧文件夹 |
| Tween 缓动 | 压扁拉伸、镜头震动、数字跳动 | tween(node).to(...) | 代码内执行 |
主角的动作应该走第一行:由 Animation 组件播放剪辑,因为跑、跳、滑铲需要循环、可打断、可精确控制时间和事件。帧序列图更适合一次性特效,比如落地溅起的尘土,用代码按帧换 spriteFrame,播完即弃。Tween 则是锦上添花用的,跳跃前的蹲蓄力、落地瞬间的压扁回弹、受击镜头抖动,都是很短促的数值插值,不该占用动画剪辑的时间轴。
另外注意一个容易翻车的点:如果你拿到的是 Spine 或 DragonBones 导出的 JSON 动画,播放入口是 Skeleton 组件,不是 cc.Animation。这套状态机的思路照样适用,只是把 anim.play(next) 换成 skeleton.setAnimation(0, name, loop)。拿 cc.Animation 去播 JSON 动画,资源类型对不上,编辑器里可能看不出问题,一打包运行就报错找不到动画。
2.3 动作状态机的最小实现:枚举加一个切换接口
状态机的核心其实很朴素:一个枚举、一个当前状态、一个切换接口。关键在于切换接口要统一收口,禁止到处直接调用 anim.play()。下面这个实现可以直接套进 Cocos Creator 2.4.x 的 TypeScript 工程。
// ActionState.ts —— 动作状态枚举,值直接等于动画剪辑名 export enum ActionState { Idle = 'idle', Run = 'run', Jump = 'jump', Slide = 'slide', Die = 'die' }// CharacterAction.ts —— 挂在角色根节点上的动作状态机 import { ActionState } from './ActionState'; const { ccclass, property } = cc._decorator; @ccclass('CharacterAction') export class CharacterAction extends cc.Component { @property(cc.Animation) anim: cc.Animation = null; private _state: ActionState = ActionState.Idle; public get state(): ActionState { return this._state; } /** 统一动作入口:所有要切动作的代码都走这里 */ public play(next: ActionState) { if (this._state === next) return; // 同状态不重复播放 if (!this.canSwitchTo(next)) { cc.warn(`状态切换被拒:${this._state} -> ${next}`); return; } this._state = next; this.anim.stop(); // 先停旧状态,避免抢轨道 this.anim.play(next); // clip 名与枚举值一致 this.node.emit('action-changed', this._state); } /** 互斥规则集中在这里,以后加二段跳就在此扩展 */ private canSwitchTo(next: ActionState): boolean { if (this._state === ActionState.Idle) return true; if (this._state === ActionState.Run) { // 跑动中可被跳、滑铲、死亡打断 return true; } if (this._state === ActionState.Jump) { // 跳跃中只允许滑铲改变姿态或死亡,不允许直接回跑 return next === ActionState.Slide || next === ActionState.Die; } if (this._state === ActionState.Slide) { return next === ActionState.Run || next === ActionState.Jump || next === ActionState.Die; } return false; } }代码里有两个刻意为之的细节。一是枚举值直接等于动画剪辑名,anim.play(next) 时不用再做一层映射,少一个可能出错的地方;二是切换接口对外只暴露 play(),内部状态字段用 getter 只读,所有切换都必须经过 canSwitchTo 的互斥校验,规则集中在一处。
第三个细节是用 node.emit('action-changed') 广播状态变化,而不是在状态机里直接操作碰撞体、拖尾、粒子。这样跳跃时谁要放大碰撞体、滑铲时谁要压低碰撞体,都各自监听事件去处理,状态机和表现代码解耦。以后加二段跳,只需要在枚举里加一个 JumpUp 状态,在 canSwitchTo 里补一条规则,其他地方不用动。
提示:Cocos Creator 3.x 里同样思路可用,只是把 cc._decorator 换成 'cc' 模块的 decorator 导入方式,cc.Animation 换为 Animation,逻辑不变。
3. 把主角的跑、跳、滑铲在场景里跑通:核心组件、脚本与参数
3.1 搭建主角预制体:Animation 组件、剪辑循环与 Play On Load 设置
动手写脚本之前,先把主角预制体的结构定好。我一般会建一个 Character 根节点,下面挂 Sprite 子节点用于显示,再挂 BoxCollider 用于碰撞判定,根节点上挂 Animation 组件和两个脚本:CharacterAction 管状态,JumpController 管跳跃物理。这样做的目的是把"显示"和"判定"分离,跳跃压扁时只缩放 Sprite,不缩放碰撞体。
搭建步骤按顺序走:
- 新建 Character 节点,添加 cc.Sprite 组件并指定默认跑动贴图。
- 添加 cc.Animation 组件,把 run、jump、slide 三个剪辑拖进 Clips 列表。
- 双击 run 剪辑进入动画编辑器,在剪辑属性里打开循环开关,让它无缝循环播放。
- jump 和 slide 剪辑设为不循环,播完停在最后一帧。滑铲结束时由代码切回 run,而不是靠剪辑自动回到第一帧。
- 关闭 Animation 组件的 Play On Load,初始动作交给状态机在 onEnable 里统一处理。
第 5 步很多人会忽略。Play On Load 默认播放的是 Clips 列表里第一个剪辑,如果它的循环方式和状态机不一致,就会出现"场景一加载就乱动"的问题。正确的做法是让状态机成为唯一播放入口,动画组件只负责"被播放"。
3.2 跳跃与落地的物理参数:重力、起跳速度、判定时机怎么调
跑酷的手感全靠两个数撑着:重力加速度和起跳速度。这里不建议用物理引擎的 RigidBody 去推角色跳,因为 RigidBody 的摩擦、阻尼会让每次跳跃轨迹有微小差异,跑酷这种必须精确复现手感的玩法经不起这种随机性。常见做法是自己写一个基于 update 的运动控制器,用一个垂直速度变量模拟重力。
// JumpController.ts —— 自定义重力的跳跃控制 const { ccclass, property } = cc._decorator; @ccclass('JumpController') export class JumpController extends cc.Component { @property gravity = 2600; // 像素/秒^2,手感主旋钮 @property jumpSpeed = 1150; // 起跳瞬间垂直速度,像素/秒 @property groundY = 0; // 地面高度(角色脚底坐标) private vy = 0; // 当前垂直速度 private isJumping = false; private action: CharacterAction = null; onLoad() { // 依赖同节点上的状态机,启动时做一次绑定 this.action = this.getComponent(CharacterAction); } /** 玩家点击时调用 */ tryJump() { // 空中不能二段跳,用高度阈值而不是标志位判断更稳 if (this.node.y > this.groundY + 1) return; this.vy = this.jumpSpeed; this.isJumping = true; this.action.play(ActionState.Jump); } update(dt: number) { if (!this.isJumping) return; // 帧率波动大时钳制 dt,避免逻辑跳变 dt = Math.min(dt, 1 / 30); this.vy -= this.gravity * dt; this.node.y += this.vy * dt; // 落地:回到地面并切回跑动 if (this.node.y <= this.groundY) { this.node.y = this.groundY; this.isJumping = false; this.action.play(ActionState.Run); } } }参数怎么设,直接关系到手感。以 720×1280 设计分辨率为例,gravity 取 2400~2800,jumpSpeed 取 1000~1200,跳跃最高点可以用公式 h = v² / 2g 估算,v=1150、g=2600 时最高点大约 254 像素,正好翻过一到两个角色身高的障碍。调参的口诀是:跳起来"飘"就加 gravity,"沉"就加 jumpSpeed;最高点不够就加 jumpSpeed,但加得太多会让上升弧线失真。
落地判定不要用 node.y 精确等于 groundY,浮点误差会导致有一次判定漏掉。用 <= 阈值并强制吸附回地面,代码里已经体现。另外 dt 必须钳制,否则掉帧瞬间叠加一大段时间步长,角色会直接穿过障碍。
3.3 落地反馈与拖尾:MotionStreak 和粒子的事件驱动启用
跳跃要有力度感,光靠位移不够,还要有视觉残留。角色上升时加一条拖尾,落地时溅一点尘土,观感立刻不一样。这两个效果都别在 update 里用开关控制,应该挂在动作切换事件上。
// 关卡脚本监听角色动作切换,统一驱动特效开关 player.on('action-changed', (state: ActionState) => { const trail = playerNode.getComponent(cc.MotionStreak); // 只在上升段开拖尾,到顶后关闭,避免拖尾一直画在屏幕上 trail.enabled = (state === ActionState.Jump); if (state === ActionState.Run) { playLandDust(); // 落地尘土粒子,播放一次 } });MotionStreak 组件的关键参数是 fadeTime、stroke 和纹理。fadeTime 控制拖尾消散时间,跑酷里取 0.12~0.2 秒比较合适,太长了像鬼影,太短了看不出轨迹;stroke 是拖尾宽度,不要超过角色身宽的一半。MotionStreak 要挂在角色 Sprite 后面的子节点上,渲染层级靠后,不然拖尾会盖住角色。
落地尘土这类粒子特效,我习惯做成一个独立的一个粒子预制体,在落地动画的事件帧上触发,而不是在代码里 new 一个粒子节点。理由很简单:粒子参数在编辑器里调,美术自己能改;用代码临时创建节点,参数全藏在脚本里,改一次要翻一次代码。
4. 动画与动作高频踩坑:跑酷项目里最容易翻车的 5 个点
做动画驱动的项目,编辑器里预览永远正常,出问题基本集中在"状态切换"和"打包"两个环节。下面 5 条是我在跑酷项目里反复遇到的,按现象、原因、解决的顺序写。
4.1 动画事件设了却不触发,角色动画显示不全
现象:在 AnimationClip 的事件轨道上加了函数,编辑器预览正常,进游戏后播到那一帧没反应,角色该做的动作、该发的粒子全没出现。
原因:动画事件是调用"挂载 Animation 组件节点"上的组件方法。如果函数写在子节点脚本里,或者脚本还没加载完,事件就找不到目标;另一个常见原因是打包时脚本压缩把函数名改写了,动画事件按函数名去找,名字对不上就静默失败。
解决:事件函数统一挂在根节点的组件上,并且声明为公开方法。打包测试时先关掉脚本压缩跑一遍,确认是压缩导致之后,在构建配置里把事件函数名加入保留名单,或者改用节点事件方式,在代码里监听事件而不是依赖函数名匹配。
4.2 落地瞬间角色闪回第一帧再继续跑
现象:跳跃落地后,角色先闪回跑动动画的第一帧或某个奇怪 pose,下一帧才开始正常跑。
原因:jump 剪辑的最后一帧和 run 剪辑的第一帧不是同一个 SpriteFrame,状态切换又是 stop + play,新剪辑从第 0 帧开始播,两个 pose 相接时就出现了瞬间跳变。除此之外,Animation 组件如果开了 Play On Load,加载时也会抢一次播放,视觉上就是闪一下。
解决:从美术侧约定,所有动作剪辑的第一帧和最后一帧都对齐到 idle 的标准站姿。代码侧,落地切换时不要依赖剪辑自动回到首帧,可以在切换前把 anim.currentClip.time 读到接近结尾的位置再切,但更推荐的做法是把 run 剪辑的循环尾帧和 jump 剪辑的落地帧对齐,从源头上消除跳变。
4.3 同一节点上多个剪辑抢轨道,动画互相覆盖
现象:run 和 slide 来回切换时,角色 SpriteFrame 忽跑忽滑,或者切了几次之后动画彻底停住不再动。
原因:多个 AnimationClip 的属性轨道都在写 spriteFrame 属性,旧剪辑的播放没有彻底停止,新剪辑已经写入同一轨道,同一帧两个求值结果互相覆盖,表现就是抖动或停住。代码里如果同时用 Animator 换图、又用 Animation 播剪辑,双方写同一个属性,也会出现这个问题。
解决:规范所有切换都走状态机接口,切换前先 anim.stop() 停掉旧剪辑,再 play 新剪辑。检查两个剪辑的属性轨道,确保没有同名属性轨道在写同一个目标。代码动态换 spriteFrame 的路径要和 Animation 剪辑的写入路径二选一,同一属性不允许两个写入方。
4.4 打包成 APK 后动画资源丢失或贴图发白
现象:编辑器里一切正常,打包安装后角色不显示,日志报找不到 spriteFrame,或者贴图整片白色、半透明重影。
原因:动画剪辑引用的贴图不在首包引用链上,构建时被资源裁剪排除了;另一个常见原因是开启了纹理压缩,部分 Android 机型 GPU 不支持对应纹理格式,解码失败就会白图。
解决:打包前在构建面板的资源统计里过一遍,确认 AnimationClip 引用的所有 SpriteFrame 都在 main bundle 的引用链上。如果素材放在 resources 目录并用 cc.resources.load 动态加载,注意打包后路径大小写必须和代码一致,Android 对大小写敏感。纹理压缩选项在测试包阶段先关掉,确认功能正常再按需开启,并且要在真机上验证,不要只看模拟器。
4.5 场景切换回来动作状态与碰撞体尺寸不一致
现象:角色死亡或结算后回到主场景,角色还在播跳跃或滑铲动画,碰撞体也停留在滑铲那种低矮尺寸,一进游戏就撞障碍。
原因:场景重新加载时,onLoad 只执行一次,而 Animation 组件的 Play On Load 可能先于脚本复位,或者状态机的 _state 被重置成 Idle,但碰撞体尺寸、拖尾开关没有跟着重置。状态机的数据和表现数据脱节了。
解决:在 CharacterAction 里增加一个 reset() 公开方法,把状态、碰撞体尺寸、拖尾、粒子一次性复位。复位动作写在 onEnable 里,而不是依赖组件的 Play On Load。这条在多人协作、场景频繁切换的项目里最容易犯,因为每个人只改自己负责的部分,没人做全局复位。
5. 从源码工程到 APK:Cocos Creator 打包链路与失败排查
5.1 构建前必查的工程配置:包名、方向、场景列表与资源选项
源码包拿到手,先在构建发布面板里过一遍这些配置,再点构建按钮。配置错了,APK 出来了也要返工。
| 配置项 | 建议值 | 作用 |
|---|---|---|
| 包名 | com.xxx.ttkp 这类三段式 | 决定安装唯一标识,后续接入 SDK 回调也要用它 |
| 屏幕方向 | 竖屏 Portrait | 天天酷跑是竖屏玩法,横屏会直接破坏操作 |
| 启动场景 | 主菜单场景 | 必须在场景列表第一位,放错会白屏或加载异常 |
| MD5 Cache | 正式发包开启 | 资源文件名带 md5,避免真机缓存旧资源 |
| 压缩纹理 | 测试包先关 | 部分旧机型 GPU 无对应格式会贴图白屏 |
场景列表是重灾区。构建面板里如果把所有场景都勾上了,启动场景又不是第一位,装到手机上会先加载一个不相干的场景再跳转,表现就是闪一下黑屏再加载半天。如果项目里有 loading 场景和动画进度条,确保 loading 场景是第一位的,主玩法场景放在其后按需加载。
5.2 命令行构建与 Gradle 出包:一条可复现的打包命令
编辑器点构建固然方便,但反复做版本包时,命令行更可靠,还能接进 CI。Cocos Creator 2.4.x 的命令行打包方式如下。
# 1. 用命令行触发 Creator 构建(在 Creator 安装目录下执行) /Applications/CocosCreator/Creator/2.4.9/CocosCreator.app/Contents/MacOS/CocosCreator \ --project ~/workspace/tian-tian-ku-pao \ --build "platform=android;debug=true;buildPath=./build" # 2. 构建产出的原生工程一般在 build/android/proj,进入后直接出 debug 包 cd ~/workspace/tian-tian-ku-pao/build/android/proj ./gradlew assembleDebug --stacktrace # 3. APK 产物路径 # build/android/proj/app/build/outputs/apk/debug/app-debug.apk--project 指向工程目录,--build 后面的引号里是构建参数。platform=android 指定平台,debug=true 代表脚本调试包,正式发包要改成 debug=false。buildPath 是产物输出目录,保持默认也行。整个过程分为两步:Creator 负责把场景、脚本、资源编译成原生工程;Gradle 负责把原生工程编译成 APK 并签名。
Cocos Creator 3.x 的命令行稍有不同,但思路一致:
# 3.x 使用 cocos 命令行,cocos 命令由 Dashboard 安装 cocos build -p android --debug --project ~/workspace/tian-tian-ku-pao-3x # 产物目录同样是 build/android/proj cd ~/workspace/tian-tian-ku-pao-3x/build/android/proj ./gradlew assembleDebug首次打包一定要有耐心,Gradle 和 Android SDK 依赖是现下的,时长完全取决于网络环境。后面再打,依赖有缓存就会快很多。
5.3 三个高频打包失败:从报错信息定位问题
Gradle 卡在下载依赖或超时。报错通常是下载 distributionUrl 超时,这是 Gradle wrapper 在拉取 Gradle 发行包。解决方法是修改 build/android/proj 下的 gradle-wrapper.properties,把 distributionUrl 换成可访问的镜像地址,或者手动下载对应的 Gradle 发行包,放到本机 gradle 缓存目录里,再重新执行打包命令。
报 "Unable to locate a Java Runtime" 或 "SDK location not found"。这是环境变量没配对。Cocos Creator 2.4 需要 ANDROID_SDK_ROOT 指向 Android SDK 目录,JAVA_HOME 指向 JDK。注意 Cocos 2.4 对 NDK 版本有兼容性要求,不要盲目升级高版本 NDK,优先用构建面板里引导安装的版本,人为升级 NDK 反而会导致链接阶段报错。
报签名错误 "Keystore was tampered with, or password was incorrect"。assembleDebug 用的是自动生成的 debug.keystore,一般不会出错;凡是涉及自己配置的 release keystore 才容易翻。原因基本是密码输错或 keystore 文件路径不对。把路径写成绝对路径,密码从配置文件读,避免命令行转义问题。
注意:打包问题最忌不看日志直接重试。Gradle 的 --stacktrace 参数在排查时一定要带上,它会指出是资源问题、SDK 问题还是签名问题,省掉大量盲试的时间。
6. 把教程源码改成自己的玩法:动画工作流的三个进阶习惯
6.1 状态表先行:动手前先把动作清单写成一张表
拿到任何跑酷源码包,先别急着改素材。我习惯先建一个文档,把动作名称、循环方式、能否被打断、事件帧整理成一张表,状态机代码和这张表一一对应。排查问题时按表说话,不用满项目找 anim.play 的调用点。这张表还是加新动作的依据,加一个滑翔就只在表里加一行,再落到状态机和互斥规则。
6.2 判定和特效交给动画事件,不交给定时器
落地尘土、踩怪扣血、攻击判定帧,这些都应该放在动画剪辑的事件轨道上。事件跟着动画走,动画变速、暂停、被打断时判定天然同步,不会出现"人已经落地 0.2 秒了,定时器才触发"的怪手感。事件回调里只做触发,不做耗时计算,粒子生成这种操作丢给对象池去拿。
6.3 手感参数集中到一个配置文件
// GameplayConfig.ts —— 手感相关参数集中管理 export const GameplayConfig = { gravity: 2600, // 像素/秒^2,调手感的主旋钮 jumpSpeed: 1150, // 起跳垂直速度,像素/秒 jumpHeightGuard: 254, // 理论最高点,调参时校验用 streakFadeTime: 0.15, // 跳跃拖尾消散时间 landingBufferMs: 80, // 落地预输入缓冲,单位毫秒 };调手感时只改这个文件,不用动逻辑代码,真机跑两三把就能收敛。这里想特别提一下落地预输入缓冲:玩家在落地前 80 毫秒内按下跳跃,落地瞬间要立刻起跳,否则会感觉"按了没反应"。这个缓冲值是跑酷手感的关键细节,教程源码里不一定写了,但你自己做玩法时一定要补上。wayne
我带跑酷小项目时吃过不少亏,总结下来最大的一条是:动画素材永远画得比预期好,出问题的全是状态切换和事件对接。先把状态表、事件清单、参数配置这三样立住,后面加玩法就像拼插片,改一个地方不会牵动全局。希望帮到你。
本文还有配套的精品资源,点击获取