☰
Cocos Creator跑酷动画实战:状态机与动作切换源码解析
2026/9/28 2:03:54 网站建设 项目流程

简介:本资源是Cocos Creator实战教程第二篇的配套源码包,面向正在学习2D游戏开发、希望掌握跑酷类游戏动画与动作系统的初中级开发者。内容围绕角色动画创建、动画控制器状态切换、关键帧事件触发,以及行为树驱动的寻路、追逐、躲避等动态行为展开,并涉及精灵图集管理、对象池、性能分析工具等优化思路。压缩包共46个文件,约6.98MB,包含4个anim动画文件、4个js脚本、4个json配置、2个fire场景、4个png与2个jpg贴图,以及meta、plist、ts声明等工程辅助文件,结构完整可直接导入编辑器运行调试。目前已有132人学习。通过这份源码,读者能对照实现角色跑步、跳跃、攻击等动画切换与行为逻辑,理解动画控制器与行为树的协作方式,并借鉴资源打包与性能优化做法,为同类跑酷项目提供可复用的工程参考。

1. 天天酷跑式跑酷动画在 Cocos Creator 里到底难在哪

跑酷游戏看着简单,真动手做才发现坑全在动画和动作衔接上。天天酷跑这类横版跑酷,玩家 90% 的时间盯着角色,角色只要有一帧滑步、一次落地穿地、一次二段跳动作卡住,手感立刻崩。Cocos Creator 实战教程里讲动画、动作相关的部分,核心就三件事:帧动画怎么切、状态机怎么管、物理位移怎么和动画对齐。这套源码打包的价值不在于代码多长,而在于它把「跑、跳、二段跳、滑铲、受击、死亡」这几个动作的切换逻辑和参数配置摊开给你看。适合已经会用 Cocos Creator 拖控件、但对 Animation 组件和状态切换没系统认知的开发者,也适合想拿一套能跑的跑酷骨架改自己项目的熟手。下面按「先立住原理、再动手复现、最后避坑」的顺序拆。

2. 跑酷动画的底层:帧动画、骨骼动画与状态机怎么选

2.1 帧动画和骨骼动画在跑酷里的分工

天天酷跑这类 2D 横版跑酷,角色动作主流是帧动画(SpriteFrame 序列),不是骨骼动画。原因很直接:帧动画每一帧是美术画好的,动作表现力可控,切换成本低,Cocos Creator 里用 Animation 组件挂 SpriteFrame 轨道就能播。骨骼动画(Spine / DragonBones)适合动作多、需要换装的场景,但跑酷角色动作数量固定,帧动画更省事。

不过帧动画有个硬伤:动作之间的过渡帧要美术补,否则跑切跳会「闪」。常见做法是给关键动作加 2 到 3 帧过渡,或者在代码里用交叉淡化掩盖。我一般会在 Animation 组件上给每个动作单独建 Clip,跑是run、跳是jump、二段跳是jump2、滑铲是slide,然后用状态机控制播放哪个 Clip。

选型判断标准:

维度帧动画骨骼动画
动作数量少而固定多且需组合
换装需求无有
内存占用帧数×单帧大小骨骼+贴图
过渡控制需补帧或淡化骨骼插值天然平滑
Cocos 接入Animation + SpriteFrameSpine/DragonBones 组件

跑酷选帧动画,是因为动作集合封闭,且美术能直接控制每一帧的形变,落地反馈更「实」。

2.2 状态机:跑酷动作切换的中枢

动作切换不能靠 if-else 堆。跑酷角色状态至少包括:Idle、Run、Jump、DoubleJump、Slide、Hurt、Dead。每个状态有进入条件、退出条件、可播放的 Clip、是否可被打断。用状态机管,逻辑才不炸。

Cocos Creator 里可以手写一个轻量状态机,也可以用cc.StateMachine这类第三方。我倾向手写,因为跑酷状态少,手写可控。核心结构是:当前状态 + 转移表。转移表用对象描述,比如Run -> Jump条件是「按下跳跃且在地面」。

// 轻量状态机:状态定义与转移 const State = { IDLE: 'idle', RUN: 'run', JUMP: 'jump', DOUBLE_JUMP: 'doubleJump', SLIDE: 'slide', HURT: 'hurt', DEAD: 'dead' }; // 转移表:from -> [{ to, condition }] const transitions = { [State.IDLE]: [{ to: State.RUN, condition: (ctx) => ctx.isMoving }], [State.RUN]: [ { to: State.JUMP, condition: (ctx) => ctx.jumpPressed && ctx.onGround }, { to: State.SLIDE, condition: (ctx) => ctx.slidePressed && ctx.onGround }, { to: State.HURT, condition: (ctx) => ctx.hurt } ], [State.JUMP]: [ { to: State.DOUBLE_JUMP, condition: (ctx) => ctx.jumpPressed && !ctx.onGround && ctx.canDoubleJump }, { to: State.RUN, condition: (ctx) => ctx.onGround } ], [State.DOUBLE_JUMP]: [ { to: State.RUN, condition: (ctx) => ctx.onGround } ], [State.SLIDE]: [ { to: State.RUN, condition: (ctx) => !ctx.slidePressed || ctx.slideTimeUp } ], [State.HURT]: [ { to: State.RUN, condition: (ctx) => ctx.hurtTimeUp }, { to: State.DEAD, condition: (ctx) => ctx.hp <= 0 } ] };

这段代码的关键在condition是函数,运行时拿上下文判断。ctx里放onGround、jumpPressed、canDoubleJump这些标志,由角色控制器每帧更新。参数说明:canDoubleJump必须在落地时重置为 true,否则二段跳只能用一次;slideTimeUp是滑铲计时器,防止按住不放一直滑。转移表的好处是加状态只改数据,不改逻辑分支。

2.3 动画事件与物理位移的对齐

跑酷最容易被忽略的是「动画和位移不同步」。角色跑动时,动画在播,但位移是代码加的,两者速度不匹配就滑步。解决办法有两种:一是让位移速度等于动画每帧位移量×帧率;二是用动画事件在特定帧触发位移变化。

Cocos Creator 的 Animation 组件支持添加事件帧(Event Frame)。比如跳跃动画在起跳帧触发onJumpStart,在落地帧触发onLand。代码里监听这些事件,在onJumpStart里施加向上的速度,在onLand里重置canDoubleJump和onGround。

// 监听动画事件,对齐物理与动作 const anim = this.node.getComponent(cc.Animation); anim.on('jump_start', () => { // 起跳帧:施加向上速度 this.body.linearVelocity = cc.v2(this.body.linearVelocity.x, this.jumpSpeed); this.onGround = false; }, this); anim.on('land', () => { // 落地帧:重置二段跳与地面标志 this.canDoubleJump = true; this.onGround = true; this.body.linearVelocity = cc.v2(this.body.linearVelocity.x, 0); }, this);

参数说明:jumpSpeed要和跳跃动画的时长匹配,跳太高动画播完人还在空中会显得飘,跳太低动画没播完就落地会穿地。我一般用「跳跃高度 = 初速度² / (2×重力)」反推初速度,再让动画时长约等于滞空时间。重力在物理系统里设,Cocos Creator 2.x 默认是cc.director.getPhysicsManager().gravity,3.x 在物理系统配置里改。

3. 用 Cocos Creator 把跑酷动作系统跑起来的最小步骤

3.1 角色节点与 Animation 组件的搭建

先在场景里建角色节点,挂 Sprite 和 Animation 组件。Sprite 的 SpriteFrame 先随便给一张,Animation 组件负责切 Clip。然后在资源管理器里建 Animation Clip 文件,每个动作一个:run.anim、jump.anim、jump2.anim、slide.anim、hurt.anim、dead.anim。

每个 Clip 里加一条 SpriteFrame 轨道,把美术给的序列帧拖进去。注意帧率:跑酷跑动一般 12 到 15 帧每秒,跳跃 10 到 12 帧,滑铲 12 帧左右。帧率太高动作会「快进」,太低会卡顿。Cocos Creator 的 Animation Clip 默认采样率是 60,要手动改成你想要的帧率,或者在 Clip 的sample属性里设。

// 播放指定动作 Clip,并处理循环与速度 playAction(state) { const clipName = this.stateToClip[state]; // 状态到 Clip 名的映射 const clip = this.anim.getClips().find(c => c.name === clipName); if (!clip) return; // 跑动循环,跳跃/滑铲不循环 clip.wrapMode = (state === State.RUN || state === State.IDLE) ? cc.WrapMode.Loop : cc.WrapMode.Normal; this.anim.play(clipName); }

逻辑说明:stateToClip是状态到 Clip 名的映射表,比如Run -> 'run'。wrapMode控制循环,跑和待机循环,跳跃和滑铲播一次就停。参数说明:cc.WrapMode.Loop是循环,cc.WrapMode.Normal是播完停在最后一帧。如果跳跃动画播完还停在空中,说明状态没切回 Run,检查onGround是否在落地事件里正确置位。

3.2 输入处理与状态切换的接线

输入用cc.systemEvent监听键盘,或者用触摸事件。跑酷一般两个操作:跳跃和滑铲。跳跃按下触发jumpPressed,滑铲按下触发slidePressed。这些标志每帧传给状态机。

// 输入监听:跳跃与滑铲 onLoad() { cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this); } onKeyDown(event) { switch (event.keyCode) { case cc.macro.KEY.space: this.ctx.jumpPressed = true; break; case cc.macro.KEY.down: this.ctx.slidePressed = true; break; } } onKeyUp(event) { switch (event.keyCode) { case cc.macro.KEY.space: this.ctx.jumpPressed = false; break; case cc.macro.KEY.down: this.ctx.slidePressed = false; break; } }

逻辑说明:jumpPressed在按下时置 true,松开置 false。状态机里判断jumpPressed && onGround才切 Jump,避免空中按跳跃直接切。参数说明:cc.macro.KEY.space是空格键,cc.macro.KEY.down是下方向键。如果要支持触摸,把 KEY_DOWN 换成 TOUCH_START,判断触摸区域是上半屏还是下半屏。

3.3 二段跳与滑铲的边界处理

二段跳的边界是「只能跳一次」。实现上,canDoubleJump初始 true,第一次跳后置 false,落地事件里重置 true。滑铲的边界是「滑铲时不能跳」,状态机里 Slide 状态不接受 Jump 转移即可。

// 每帧更新上下文,驱动状态机 update(dt) { // 更新地面检测 this.ctx.onGround = this.checkGround(); // 更新滑铲计时 if (this.currentState === State.SLIDE) { this.slideTimer += dt; this.ctx.slideTimeUp = this.slideTimer >= this.slideDuration; } else { this.slideTimer = 0; this.ctx.slideTimeUp = false; } // 更新受伤计时 if (this.currentState === State.HURT) { this.hurtTimer += dt; this.ctx.hurtTimeUp = this.hurtTimer >= this.hurtDuration; } // 跑状态机 this.stateMachine.tick(this.ctx); }

逻辑说明:checkGround()用射线或碰撞检测判断是否在地面。slideDuration是滑铲持续时间,一般 0.5 到 0.8 秒。hurtDuration是受击硬直,一般 0.3 到 0.5 秒。参数说明:slideTimer和hurtTimer在状态切换时要清零,否则下次进状态计时会接着上次的走。这个坑我踩过,滑铲第二次直接秒结束,查了半天才发现计时器没重置。

4. 源码打包里那些让你翻车的动画与动作坑

4.1 动作切换闪帧:现象是跑切跳时角色闪一下白

现象:角色从跑切到跳,画面闪一帧白或者闪一帧其他动作的帧。原因是 Animation 组件在切换 Clip 时,默认会先停当前 Clip 再播新 Clip,中间有一帧空档,SpriteFrame 被清空或显示默认图。解决:在切换前先anim.stop()再anim.play(),或者给 Sprite 设一个默认 SpriteFrame 兜底。更稳的做法是用anim.crossFade()做交叉淡化,但跑酷动作切换快,淡化反而糊,我一般直接 stop + play 并确保 SpriteFrame 不为空。

4.2 二段跳失效:现象是空中按第二下没反应

现象:第一次跳正常,空中再按跳跃没反应。原因通常是canDoubleJump没在落地时重置,或者状态机里 DoubleJump 的转移条件写成了onGround。检查两点:落地事件里有没有this.canDoubleJump = true;转移表里Jump -> DoubleJump的条件是不是jumpPressed && !onGround && canDoubleJump。还有一个隐蔽原因:jumpPressed在按下时置 true,但状态机每帧 tick,如果同一帧内先切了 Jump 又判断 DoubleJump,会误触发。解决是在状态切换后把jumpPressed消费掉,置 false。

4.3 滑铲穿地:现象是滑铲时角色陷进地面

现象:滑铲动作播放时,角色碰撞体下移,陷进地面。原因是滑铲动画里角色蹲下,Sprite 的锚点或碰撞体没跟着调。解决:给滑铲单独设一个碰撞体,或者用动画事件在滑铲开始时缩小碰撞体、结束时恢复。Cocos Creator 里可以在 Animation Clip 里加一条碰撞体尺寸的轨道,但更简单的是代码里在进入 Slide 状态时改cc.BoxCollider的 size。

4.4 动画事件不触发:现象是监听的回调没执行

现象:Animation 组件上加了事件帧,代码里anim.on('jump_start', ...)没反应。原因有三个:事件帧的名字和监听的名字不一致;事件帧加在了错误的 Clip 上;Animation 组件没在onLoad里注册监听。排查顺序:先看 Clip 里事件帧的 Function 名,再看代码里on的第一个参数是否完全一致(大小写敏感),最后确认监听注册在play之前。我习惯在onLoad里统一注册所有动画事件,避免播到一半才注册漏掉。

4.5 帧动画内存爆:现象是动作多了之后加载慢、内存高

现象:动作 Clip 一多,游戏启动加载慢,运行时内存高。原因是每个 SpriteFrame 都是独立纹理,帧数多纹理就多。解决:把同一角色的序列帧打成图集(Atlas),Cocos Creator 里用自动图集或 TexturePacker。图集能把多张帧合并成一张纹理,减少 DrawCall 和内存碎片。参数上,图集尺寸别超过 2048×2048,否则低端机可能不支持。另外,不常用的动作(死亡、受击)可以延迟加载,别一开始全塞进内存。

5. 让跑酷动作更跟手的三个进阶技巧

5.1 用输入缓冲让跳跃不丢帧

跑酷玩家经常在落地前几帧按跳跃,如果状态机严格判断onGround,这一下就丢了,手感很「粘」。解决办法是输入缓冲:按下跳跃时记录时间戳,如果在落地后 100 到 150 毫秒内,仍然执行跳跃。实现上在ctx里加jumpBufferTime,落地时检查当前时间 - jumpBufferTime < bufferWindow。

// 输入缓冲:落地前按跳,落地后仍生效 onKeyDown(event) { if (event.keyCode === cc.macro.KEY.space) { this.ctx.jumpPressed = true; this.ctx.jumpBufferTime = performance.now(); // 记录按下时刻 } } // 落地事件里检查缓冲 anim.on('land', () => { this.canDoubleJump = true; this.onGround = true; const now = performance.now(); if (now - this.ctx.jumpBufferTime < 150) { // 缓冲窗口内,立即起跳 this.ctx.jumpPressed = true; } });

参数说明:150是缓冲窗口毫秒数,太小没效果,太大角色会「自动跳」。我一般设 120 到 150,实测这个区间最跟手。这个技巧在动作游戏里叫「跳跃缓冲」,是手感调优的后悔药。

5.2 用 Coyote Time 让离地后还能跳

和输入缓冲对称的是 Coyote Time:角色刚离开平台边缘的几帧内,仍然允许跳跃。跑酷里角色跑过平台边缘,玩家按跳,如果严格判断onGround就跳不了,体验很差。实现上在onGround变 false 时记录时间,之后 80 到 120 毫秒内仍视为可跳。

// Coyote Time:离地后短暂仍可跳 update(dt) { const wasOnGround = this.ctx.onGround; this.ctx.onGround = this.checkGround(); if (wasOnGround && !this.ctx.onGround) { this.coyoteTimer = 0; // 刚离地,开始计时 } if (!this.ctx.onGround) { this.coyoteTimer += dt; } this.ctx.canJump = this.ctx.onGround || this.coyoteTimer < 0.1; // 100ms 内可跳 }

参数说明:0.1是 Coyote 窗口秒数,对应 100 毫秒。这个值和输入缓冲配合,跑酷手感会明显「顺」。注意 Coyote Time 只对第一次跳生效,二段跳不适用,否则空中能连跳。

5.3 用动画速度缩放匹配位移速度

跑动动画的播放速度和角色实际位移速度要匹配。如果角色加速了,动画还按原速播,就滑步。解决办法是动态调 Animation 的speed。Cocos Creator 的 AnimationState 有speed属性,可以按当前速度 / 基准速度缩放。

// 根据位移速度缩放跑动动画速度 updateRunAnimation() { const baseSpeed = 300; // 基准速度,对应动画原速 const currentSpeed = Math.abs(this.body.linearVelocity.x); const scale = currentSpeed / baseSpeed; const state = this.anim.getAnimationState('run'); if (state) { state.speed = cc.misc.clampf(scale, 0.5, 2.0); // 限制缩放范围 } }

参数说明:baseSpeed是动画设计时的基准速度,问美术要。clampf把缩放限制在 0.5 到 2.0,防止速度极端时动画鬼畜。这个技巧在加速道具、减速陷阱场景里特别有用。我一般会在角色控制器里每帧调一次,开销很小。

这三个技巧——输入缓冲、Coyote Time、动画速度缩放——是跑酷手感调优里性价比最高的。源码打包里如果没带,自己加上去,手感提升立竿见影。我自己的习惯是先把状态机和动画事件跑通,再逐个加这三个,每加一个实测一次,别一次全上,否则出问题不知道是谁的锅。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询