简介这份源码包面向Cocos Creator初学者与进阶开发者聚焦2D跑酷类游戏中的动画与动作系统帮助读者通过完整项目理解角色动画创建、状态切换与动态行为控制。包内共46个文件以meta、png、json、anim、js等类型为主涵盖动画资源、脚本逻辑、场景配置与项目设置压缩包约6.98MB结构清晰便于对照学习。已有132人学习下载。读者可从中获取天天酷跑式跑酷游戏的完整实现思路包括动画控制器管理跑步、跳跃等状态切换行为树组织寻路、追逐等复杂逻辑以及精灵图集、对象池等性能优化手段适合作为实战参考与二次开发基础。1. 天天酷跑式跑酷动画Cocos Creator 里最容易被低估的三件事跑酷游戏看起来简单——角色一直往前跑玩家点两下跳一下滑一下但真正动手在 Cocos Creator 里复刻天天酷跑那套动画体系时你会发现翻车点根本不在玩法逻辑而在动画状态机、帧动画与骨骼动画的混用、以及动作切换时的衔接处理。这套实战教程的第二篇核心就是解决「角色跑起来像滑步、跳起来像飘、落地像卡帧」这三类高频问题。它适合已经能跑通 Cocos Creator 基础场景、写过简单脚本但一碰到多状态动画切换就头大的开发者。源码打包里通常包含角色动画控制器、动作状态枚举、帧事件回调、以及一套可直接替换美术资源的预制体结构。下面按「先理解为什么这样设计再动手复现最后看坑在哪」的顺序拆开讲。2. 跑酷动画的状态机设计为什么不能用 if-else 硬切2.1 跑酷角色的动作集合与状态迁移关系天天酷跑这类横版跑酷角色动作看似只有跑、跳、滑铲、倒地四种但实际状态远不止这些。跑本身分加速跑、匀速跑、减速跑跳分起跳、空中上升、空中下落、落地缓冲滑铲分进入滑铲、滑铲持续、滑铲起身。如果把这些全部塞进一个if-else链里判断当前该播哪个动画代码会在第三天就变成不可维护的黑匣子。常见做法是定义一个有限状态机FSM把每个动作视为一个状态节点状态之间的迁移由输入事件和物理条件共同触发。Cocos Creator 里可以用cc.Class写一个轻量状态机组件挂在角色节点上由它统一管理Animation组件的播放、停止和交叉淡入淡出。状态迁移的核心规则只有三条第一同一时刻只能有一个主状态处于激活第二迁移必须满足前置条件比如只有在地面状态才能触发跳跃第三迁移过程中要处理动画混合避免硬切造成视觉跳变。这三条听起来像废话但实际写的时候很多人会在第二条上翻车——空中二段跳的条件判断如果写在跳跃状态内部而不是由状态机统一裁决就会出现「空中还能触发滑铲」的玄学 bug。2.2 用 TypeScript 实现一个可复用的动作状态机下面这段代码是一个最小可用的跑酷角色状态机挂在角色根节点上依赖同节点上的cc.Animation组件。它不处理物理只负责根据外部传入的输入信号和物理状态决定播哪个动画。// RunnerStateMachine.ts const { ccclass, property } cc._decorator; // 定义角色所有可能的状态 enum RunnerState { Idle 0, Run 1, JumpUp 2, JumpDown 3, Slide 4, Fall 5, } ccclass export class RunnerStateMachine extends cc.Component { property(cc.Animation) anim: cc.Animation null; // 当前状态外部只读 private _current: RunnerState RunnerState.Idle; // 是否在地面由物理脚本每帧写入 public isOnGround: boolean true; // 是否正在滑铲由输入脚本写入 public isSliding: boolean false; // 状态到动画名的映射动画名与编辑器里 clip 名称一致 private stateToClip: RecordRunnerState, string { [RunnerState.Idle]: idle, [RunnerState.Run]: run, [RunnerState.JumpUp]: jump_up, [RunnerState.JumpDown]: jump_down, [RunnerState.Slide]: slide, [RunnerState.Fall]: fall, }; // 允许的迁移表key 是当前状态value 是可达状态数组 private transitionTable: RecordRunnerState, RunnerState[] { [RunnerState.Idle]: [RunnerState.Run, RunnerState.JumpUp], [RunnerState.Run]: [RunnerState.Idle, RunnerState.JumpUp, RunnerState.Slide], [RunnerState.JumpUp]: [RunnerState.JumpDown, RunnerState.Fall], [RunnerState.JumpDown]: [RunnerState.Run, RunnerState.Fall], [RunnerState.Slide]: [RunnerState.Run, RunnerState.JumpUp], [RunnerState.Fall]: [RunnerState.Run], }; // 尝试迁移到目标状态返回是否成功 public tryTransition(target: RunnerState): boolean { if (target this._current) return false; const allowed this.transitionTable[this._current]; if (!allowed || allowed.indexOf(target) -1) { return false; // 非法迁移直接拒绝 } this.playClip(target); this._current target; return true; } private playClip(state: RunnerState) { const clipName this.stateToClip[state]; if (!clipName) return; // 交叉淡入 0.08 秒避免硬切 this.anim.crossFade(clipName, 0.08); } // 每帧由外部驱动根据物理和输入决定下一个状态 public tick(speed: number) { if (!this.isOnGround) { // 空中根据垂直速度判断上升还是下落这里用简化逻辑 const next this._current RunnerState.JumpUp ? RunnerState.JumpDown : RunnerState.Fall; this.tryTransition(next); return; } if (this.isSliding) { this.tryTransition(RunnerState.Slide); return; } if (speed 0.1) { this.tryTransition(RunnerState.Run); } else { this.tryTransition(RunnerState.Idle); } } }逻辑说明transitionTable是这套状态机的后悔药它把「什么状态能跳到什么状态」显式写死任何不在表里的迁移都会被tryTransition拒绝。这样即使输入脚本因为帧率波动连续发了两次跳跃信号第二次也会因为当前状态已经是JumpUp而被挡掉。crossFade的第二个参数是混合时长跑酷游戏建议设在 0.05 到 0.12 秒之间太短会看到明显跳帧太长会让动作显得拖沓。tick方法不自己驱动而是由角色控制器在update里调用这样状态机本身不依赖引擎生命周期方便单测。参数方面isOnGround和isSliding是外部写入的布尔量不要在这个组件里做物理检测否则状态机会和物理系统耦合后期换碰撞方案时改起来很痛苦。动画名映射表里的 clip 名称必须和 Cocos Creator 动画编辑器里创建的 Animation Clip 资源名完全一致大小写敏感这是新手最常踩的坑之一。2.3 状态机与 Animation 组件的职责边界很多人会把状态机和cc.Animation混在一起用比如在动画结束回调里直接改状态或者在状态机里手动play某个 clip 而不走crossFade。这两种做法都会让状态和视觉表现不同步。正确的边界是状态机只决定「该播哪个逻辑状态」Animation组件只负责「怎么播这个 clip」。动画播放完毕的事件比如倒地动画播完应该通过Animation的finished事件回调通知状态机由状态机决定下一步迁移而不是在回调里直接改节点上的其他组件。另一个边界问题是帧事件。跑酷游戏里经常需要在跑步动画的某一帧触发脚步声、在滑铲动画的某一帧触发碰撞体缩小。这些帧事件应该挂在 Animation Clip 上由美术或动画师在编辑器里添加而不是在状态机代码里用计时器模拟。用计时器模拟帧事件一旦动画速度被调整事件触发时机就会错位这是血泪经验。3. 帧动画与骨骼动画的混合使用跑酷角色的两种动画方案怎么选3.1 帧动画与骨骼动画在跑酷场景下的性能与表现对比天天酷跑原版角色是典型的帧动画每一帧都是预渲染好的位图播放时按序列切换 SpriteFrame。这种方案在 Cocos Creator 里实现简单性能开销主要在纹理内存和 DrawCall 上。骨骼动画比如 DragonBones 或 Spine 导出的资源则是运行时计算骨骼变换内存占用小但 CPU 开销随骨骼数量线性增长。跑酷游戏的角色通常只占屏幕很小一块帧动画的纹理图集如果控制在 1024x1024 以内内存完全可接受。但帧动画的致命伤是动作过渡不自然——从跑到跳如果中间没有过渡帧切换时会看到明显的姿势跳变。骨骼动画可以通过混合两个动画的骨骼姿态实现平滑过渡这是它的优势。实际项目里我一般这样选主角用骨骼动画因为主角动作多、过渡要求高小怪和特效道具用帧动画因为数量多、单个动作简单帧动画的 DrawCall 合并更容易控制。如果团队没有骨骼动画师全用帧动画也不是不行但要在状态机里把过渡帧单独做一组 clip比如run_to_jump这种过渡动画播完再进jump_up。3.2 在 Cocos Creator 中配置帧动画的完整步骤帧动画的配置在 Cocos Creator 里分三步导入图集、创建 Animation Clip、挂到 Animation 组件上。下面用命令行方式说明资源目录结构假设你把角色帧图放在assets/textures/runner/下。# 资源目录结构示例 assets/ textures/ runner/ run_01.png run_02.png run_03.png ... jump_up_01.png jump_up_02.png animations/ runner.anim # 动画剪辑文件 prefabs/ Runner.prefab # 角色预制体在 Cocos Creator 编辑器里选中run_01.png到run_08.png右键创建 Animation Clip命名为run。然后在 Animation 编辑器中把帧序列拖入时间轴设置采样率为 12 到 15 帧每秒。跑酷游戏的跑步动画不需要太高帧率12 帧足够太高反而增加纹理内存。关键参数是wrapMode跑步和滑铲设为Loop跳跃和倒地设为Normal。speed属性可以在运行时通过代码调整用来实现加速跑时动画加快的效果。但注意调整speed会同时影响帧事件的触发时机如果动画里有帧事件要么不用speed要么把帧事件改成基于归一化时间的回调。3.3 骨骼动画的导入与动作混合参数设置如果用的是 DragonBones 导出的骨骼动画Cocos Creator 里需要安装对应的插件然后把导出的_ske.json和_tex.json以及纹理图集一起拖入项目。骨骼动画的播放通过dragonBones.ArmatureDisplay组件控制动作混合用fadeIn方法。// 骨骼动画动作切换示例 const armature this.node.getComponent(dragonBones.ArmatureDisplay); // 播放跑步动作混合时间 0.1 秒 armature.playAnimation(run, 0.1); // 切换到跳跃混合时间 0.08 秒 armature.playAnimation(jump_up, 0.08);playAnimation的第二个参数是混合时长骨骼动画的混合比帧动画更敏感建议设在 0.05 到 0.15 秒之间。超过 0.2 秒会看到明显的动作重叠角色像在同时做两个动作。另外骨骼动画的timeScale可以独立于帧动画调整加速跑时把timeScale设到 1.2 到 1.5 之间视觉上更跟手。注意骨骼动画的纹理图集如果包含透明区域在 Cocos Creator 里要设置premultiplyAlpha为 true否则边缘会出现黑边。这个选项在纹理资源的属性面板里导入后默认是关闭的。4. 动作切换的衔接处理为什么你的角色跳起来像飘4.1 起跳与落地的动画衔接时间窗口跑酷游戏里最影响手感的就是起跳和落地。起跳时如果从跑步动画直接硬切到跳跃动画角色会瞬间从水平姿势变成垂直姿势视觉上像被弹射出去。落地时如果从下落动画直接切回跑步角色会像砸在地上又瞬间弹起。解决方法是引入过渡动画和衔接时间窗口。起跳时先播一个 2 到 3 帧的run_to_jump过渡动画同时给角色一个向上的初速度过渡动画播完时角色已经离地再切到jump_up。落地时先播jump_down的最后一帧然后播一个 2 帧的land_to_run过渡再切回run。这个时间窗口的具体数值取决于动画帧率和角色移动速度。以 12 帧每秒的帧动画为例2 帧过渡大约是 0.167 秒。如果角色水平速度是每秒 300 像素0.167 秒内角色会移动 50 像素过渡动画的位移要匹配这个距离否则会出现滑步。匹配位移的常见做法是在过渡动画的每一帧上手动调整角色根骨骼或节点的位置偏移让动画位移和物理位移对齐。4.2 用代码控制过渡动画的播放与中断过渡动画的播放不能简单用crossFade因为crossFade是同时播两个动画并混合而过渡动画需要独占播放。下面这段代码展示了一个过渡动画队列的实现思路。// TransitionPlayer.ts ccclass export class TransitionPlayer extends cc.Component { property(cc.Animation) anim: cc.Animation null; private _transitioning: boolean false; private _pendingClip: string ; // 播放过渡动画结束后自动切到目标动画 public playTransition(transitionClip: string, targetClip: string, onDone?: () void) { if (this._transitioning) { // 如果正在过渡记录待播目标等当前过渡结束再处理 this._pendingClip targetClip; return; } this._transitioning true; const state this.anim.play(transitionClip); state.wrapMode cc.WrapMode.Normal; state.speed 1.0; // 监听播放结束 state.on(finished, () { this._transitioning false; if (this._pendingClip) { const next this._pendingClip; this._pendingClip ; this.playTransition(transitionClip, next, onDone); return; } this.anim.crossFade(targetClip, 0.08); if (onDone) onDone(); }, this); } }逻辑说明_transitioning标志位防止过渡动画被重复触发_pendingClip用来处理过渡期间收到的新目标请求。state.on(finished)是 Cocos Creator 动画状态的事件监听只在wrapMode为Normal时触发。过渡动画的speed固定为 1.0不要在这里调速度否则过渡时长会变位移匹配就乱了。参数方面crossFade的混合时长在过渡动画结束后用 0.08 秒这个值比直接切换的 0.05 秒稍长因为过渡动画的最后一帧和目标动画的第一帧姿势差异已经很小稍长的混合能让衔接更顺。如果过渡动画和目标动画的姿势差异仍然很大说明过渡动画本身设计有问题需要回去改动画而不是加大混合时长。4.3 空中动作的物理与动画同步跳跃过程中角色的垂直位置由物理系统控制但动画的播放进度是独立的。如果物理上升时间和jump_up动画时长不匹配就会出现角色已经在下落了但动画还在播上升或者角色还在上升但动画已经播完。同步的方法是让动画时长等于物理上升时间。假设跳跃初速度是v0重力加速度是g上升时间t v0 / g。如果jump_up动画有 6 帧帧率 12时长 0.5 秒那么v0 / g应该等于 0.5。在 Cocos Creator 的物理系统里g通常设为 960 到 1200 像素每二次方秒反推v0就是 480 到 600 像素每秒。这个数值需要和动画师对齐不能各做各的。提示如果物理参数和动画时长实在对不上可以在update里根据角色垂直速度动态调整动画的speed。上升快时speed调大上升慢时speed调小让动画进度始终匹配物理位置。但这是权宜之计最好还是从参数源头对齐。5. 源码打包里的资源组织与预制体结构拿到源码后先改哪里5.1 源码包目录结构与关键文件说明一份典型的 Cocos Creator 跑酷动画源码包目录结构通常如下表所示。拿到源码后不要急着运行先按表里的说明确认每个目录的用途再决定改哪里。目录/文件用途修改优先级assets/scripts/state/状态机与动作控制脚本高逻辑核心assets/scripts/input/输入处理与手势识别中按需替换assets/animations/所有 Animation Clip 资源高替换美术资源时必改assets/textures/角色与场景纹理图集高替换美术资源时必改assets/prefabs/Runner.prefab角色预制体挂载所有组件高组件引用在这里assets/scenes/Main.scene主场景包含相机和 UI低通常不用动settings/项目设置与物理参数中调手感时改修改优先级最高的是assets/scripts/state/和assets/animations/。状态机脚本里的stateToClip映射表必须和assets/animations/里的 clip 名称一一对应替换美术资源后如果 clip 名称变了这里不改就会报空动画。Runner.prefab里的Animation组件引用了具体的 clip 资源替换图集后需要重新拖拽引用这是编辑器操作没有代码能替代。5.2 替换美术资源时的四个必改引用点替换角色美术资源是拿到源码后最常见的操作但很多人只换了图集忘了改引用运行起来角色变成白块或者动画不播。必改的四个引用点如下。第一assets/textures/下的图集文件替换后要在 Cocos Creator 里重新打图集如果用了 Auto Atlas并确认每个 SpriteFrame 的packable属性为 true。第二assets/animations/下的 Animation Clip 里每一帧引用的 SpriteFrame 要重新指定如果帧数变了时间轴也要重排。第三Runner.prefab上Animation组件的defaultClip和clips数组要重新拖拽。第四状态机脚本里的stateToClip映射表要按新 clip 名称改。这四个点里第二个最容易漏。Animation Clip 里的帧引用是 UUID 绑定的替换图集后 UUID 变了但编辑器有时不会自动更新需要手动在 Animation 编辑器里重新拖一次。如果替换后动画播出来是空白先检查这里。5.3 用脚本批量校验动画引用完整性手动检查每个 clip 的引用太累可以写一个编辑器脚本批量校验。下面这段代码放在assets/editor/目录下在 Cocos Creator 编辑器里通过菜单触发。// AnimationChecker.ts 放在 assets/editor/ 下 const { ccclass } cc._decorator; // 编辑器扩展校验所有 Animation Clip 的帧引用是否有效 export function checkAllAnimationClips() { const clipUuids Editor.assetdb.queryAssets(db://assets/animations/**/*.anim, animation-clip); let errorCount 0; clipUuids.forEach((uuid: string) { const clip Editor.assetdb.assetInfoByUuid(uuid); if (!clip) return; // 读取 clip 的序列化数据检查每帧引用的 SpriteFrame const json JSON.parse(clip.source); const curves json.curves || []; curves.forEach((curve: any) { if (curve.type cc.SpriteFrame curve._values) { curve._values.forEach((val: any) { if (val val.__uuid__) { const frameInfo Editor.assetdb.assetInfoByUuid(val.__uuid__); if (!frameInfo) { console.error([AnimationChecker] 无效引用: clip${clip.name}, uuid${val.__uuid__}); errorCount; } } }); } }); }); console.log([AnimationChecker] 校验完成无效引用数: ${errorCount}); }逻辑说明这段脚本通过Editor.assetdb查询所有.anim资源解析其序列化 JSON遍历curves数组里类型为cc.SpriteFrame的曲线检查每个引用的 UUID 是否能在资源数据库里找到。找不到就输出错误日志。errorCount汇总无效引用总数方便判断是否需要批量修复。参数方面db://assets/animations/**/*.anim是资源路径通配符如果你的动画不在这个目录下改成实际路径。curve.type除了cc.SpriteFrame还可能是cc.Node或cc.Color这里只检查 SpriteFrame 引用因为替换图集时只有这个会失效。编辑器脚本的触发方式是在 Cocos Creator 菜单栏添加一个自定义菜单项调用checkAllAnimationClips具体菜单注册代码参考 Cocos Creator 编辑器扩展文档。6. 跑酷动画的进阶调优从能跑到手感跟手6.1 用动画曲线控制跑步速度的视觉反馈跑步动画的播放速度如果恒定角色加速时看起来会像在滑步。进阶做法是把角色水平速度映射到动画的speed属性上速度越快动画播得越快。但直接线性映射会导致低速时动画慢得像幻灯片高速时快得看不清。常见做法是用一条分段曲线低速段speed变化平缓中速段线性高速段再次平缓。在 Cocos Creator 里可以用cc.AnimationState的speed属性配合一个AnimationCurve资源来实现。但更简单的做法是在状态机的tick里根据速度算一个系数直接设state.speed。系数公式可以是speed 0.8 0.4 * (currentSpeed / maxSpeed)这样速度为零时动画以 0.8 倍速播满速时 1.2 倍速。这个范围不会让动画失真太严重。6.2 滑铲动作的碰撞体同步与动画事件滑铲是跑酷游戏里唯一需要改变碰撞体形状的动作。跑步时碰撞体是竖长的胶囊滑铲时要变成横长的胶囊。如果碰撞体切换和动画切换不同步就会出现角色已经站起来了但碰撞体还是滑铲状态导致撞到本该躲过的障碍。同步的方法是使用动画帧事件。在滑铲动画的第一帧添加事件onSlideStart在最后一帧添加事件onSlideEnd状态机监听这两个事件来切换碰撞体。不要用计时器因为滑铲动画的时长可能被speed调整计时器会错位。// 在状态机里监听动画帧事件 this.anim.on(onSlideStart, () { // 切换碰撞体为滑铲形状 const collider this.node.getComponent(cc.PhysicsBoxCollider); collider.size cc.size(80, 40); collider.apply(); }, this); this.anim.on(onSlideEnd, () { const collider this.node.getComponent(cc.PhysicsBoxCollider); collider.size cc.size(40, 80); collider.apply(); }, this);apply()方法在修改碰撞体尺寸后必须调用否则物理系统不会立即更新。这个坑我踩过改完尺寸不调apply碰撞体还是旧的调试了半天才发现。6.3 用录制回放验证动画状态迁移的正确性状态机的 bug 往往在特定输入序列下才出现手动测试很难覆盖全。一个实用的技巧是加一个录制回放模块把玩家的输入序列和对应的时间戳录下来出问题时回放这段输入观察状态迁移日志。录制模块只需要记录三个东西时间戳、输入类型跳跃/滑铲、输入时的角色状态。回放时按时间戳重放输入同时在每次状态迁移时打日志。日志格式建议是[时间] 状态A - 状态B触发条件XXX。这样一旦发现异常迁移能直接定位到是哪条输入触发的。这个模块不需要很复杂一个数组加两个按钮就能实现。但它是后期调手感的后悔药没有它每次改状态机都像在赌运气。我现在的习惯是任何有状态机的项目第一天就把录制回放搭好后面省下的调试时间远超搭建成本。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?