
简介一套基于 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 取 24002800jumpSpeed 取 10001200跳跃最高点可以用公式 h v² / 2g 估算v1150、g2600 时最高点大约 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.120.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. 从源码工程到 APKCocos 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 platformandroid;debugtrue;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 后面的引号里是构建参数。platformandroid 指定平台debugtrue 代表脚本调试包正式发包要改成 debugfalse。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我带跑酷小项目时吃过不少亏总结下来最大的一条是动画素材永远画得比预期好出问题的全是状态切换和事件对接。先把状态表、事件清单、参数配置这三样立住后面加玩法就像拼插片改一个地方不会牵动全局。希望帮到你。本文还有配套的精品资源点击获取