ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

天天酷跑类游戏开发:Cocos Creator动画状态机与打击感实战

2026/10/7 6:15:11 拓冰建站 浏览量
天天酷跑类游戏开发:Cocos Creator动画状态机与打击感实战 简介《Cocos Creator 实战教程2·天天酷跑》配套源码包是一份可直接运行的2D游戏工程面向想系统学习动画与动作系统的初中级开发者以跑酷玩法为示例展示角色动画、行为树和性能优化等关键环节。压缩包共46个文件大小6.98MB包含4个anim动画文件、4个js脚本、4个json配置以及png、fire、plist等素材覆盖动画剪辑、状态切换、逻辑配置和界面场景等模块。已有132人学习。解压后可在Cocos Creator中直接运行项目对照源码查看角色跑步、跳跃动画的帧序列和事件触发写法理解动画控制器与行为树的组合方式并参考对象池、精灵图集等优化实战。项目目录结构清晰按资源、脚本、配置模块分区便于边看教程边调试适合希望快速上手完整项目、提升动画与动作系统实现能力的开发者。1. 这个「天天酷跑」实战教程到底在教什么用 Cocos Creator 照着天天酷跑搭一个 Demo十个人里八个会卡在同一关角色跑步是“平移”的跳跃只有 y 轴位移落地没有压扁拉伸全程像剪贴画在滑。真正让跑酷游戏“活”起来的是动画与动作相关的那一层——状态机怎么切、帧事件钉在哪、打包之后动画会不会丢。这个系列教程的第二篇就是把源码里看得到却抄不明白的这部分拆开讲。它解决的问题很具体动画选型、角色状态切换、打击感反馈以及最后源码打包成 APK 时动画资源的坑。适合已经用 Creator 做过小 Demo、想往完整跑酷游戏靠的人也适合那些被“编辑器里正常、真机上翻车”折磨过的人。下面的内容全部按 Cocos Creator 3.x 的组件和 API 来写2.x 的老项目思路一致函数名略有出入照着迁移不费劲。2. 跑酷动画的选型骨骼动画和帧动画怎么选、状态机怎么拆2.1 骨骼动画还是帧动画天天酷跑这类跑酷的主角该用什么跑酷游戏的主角动画有两条路序列帧动画把一帧帧 PNG 按顺序播和骨骼动画spine、DragonBones 这类。我见过不少刚上手的朋友一上来就全用序列帧理由是“教程里就是这么教的”结果角色跑步动画 24 帧、每帧一张 256 纹理主角一个人吃掉 6MB 图集还没算金币和特效。天天酷跑这种动辄几百米的无尽跑酷场景和怪物的实例数量非常大动画资源的内存占用直接决定低端机能不能跑。我一般把这两类拆开用主角、Boss、需要换装的角色用 spine 骨骼动画动作自然、占内存小换一个皮肤只是换贴图金币旋转、拖尾特效、UI 弹窗、数字滚动这类简单循环用序列帧。理由很直白——骨骼动画的“身体”是网格和骨骼数据贴图只有一张换动作不换资源序列帧每帧都是一整张贴图动作一多资源量成倍涨但它播起来最稳任何设备都不会出现骨骼权重算错的问题。选型时还要看团队的动画工作流。美术只会出 Spine 就用 spine只会出帧动画就老老实实序列帧不要为了“技术先进”逼美术学新工具那不是工程问题是效率问题。动画资源在 Creator 里落地时spine 文件导入后生成的是.spine资源加贴图挂spine.Skeleton组件播放序列帧是拿一个 Sprite 节点用Animation组件在动画剪辑里逐帧替换spriteFrame属性。两种方式都能够跑通下面这套状态机差别只在播放 API 和内存占用不改变设计思路。2.2 把跑、跳、滑、死拆成动画状态机状态表与切换条件跑酷角色的动画不能“想播哪个播哪个”否则会出现空中播跑步、死了还在滑铲这种鬼畜。正确做法是先定一张状态表把所有可能出现的动作列全再写清每个状态从哪来、能到哪去。天天酷跑主角的常驻状态我拆成六个待机、奔跑、上升跳跃、下落、滑铲、死亡。状态触发条件动画循环离开条件idle 待机进入游戏、暂停恢复循环收到开始指令run 奔跑开始游戏、落地后循环按跳跃键、按下滑键、死亡jump 上升起跳瞬间单次垂直速度变负fall 下落上升结束、跳崖单次落地slide 滑铲按下滑键循环离开滑铲区域、松开die 死亡碰撞障碍单次播完自动结束这里有两个容易犯的错。第一是把 jump 和 fall 合成一个“跳跃”动画。跑酷判定落点、踩怪、二段跳都要在精确的动画帧上做拆开后你可以让角色升到最高点再切 fall判定窗口更准。第二是忽略 idle。无尽跑酷虽然开局就 run但每局结束、暂停恢复都要回 idle没有它会多写一堆“强制播放 run”的特判代码。状态切换还有个优先级问题。我习惯按“死亡 滑铲 跳跃/下落 奔跑 待机”来挡。比如角色正在 run 时收到下滑指令要立刻切 slide但 slide 没结束不能切 jump角色在 fall 时被障碍撞到要允许 die 打断一切。这个顺序写死在switchState里比在update里写一堆if判断好维护得多。2.3 动画资源导入参数spine 的 premultiply 与循环设置资源导入这一步最容易被跳过导致后面打包一上真机就黑块。选中 spine 资源.spine文件属性检查器里有两个必须确认的参数Premultiplied Alpha和动画缓存模式。Premultiplied Alpha必须和美术导出 Spine 时的设置保持一致美术在 Spine 里勾了“预乘”Creator 这边也必须勾反了就是描边黑边、半透明区域发暗。拿不准时把一张半透明的 PNG 放场景里做透明度渐变对比黑边明显就切换这个选项。动画缓存模式有三个值REALTIME、SHARED_CACHE、PRIVATE_CACHE。REALTIME每帧实时计算骨骼动画最平滑但 CPU 开销最大SHARED_CACHE多个同骨骼资源共用一个动画状态省内存但没法独立控制播放速度PRIVATE_CACHE每个实例独立缓存。跑酷主角这种全屏焦点角色用REALTIME或PRIVATE_CACHE小怪物和 NPC 用SHARED_CACHE。帧动画那边重点检查图集是否勾了packable没有勾会导致每帧 SpriteFrame 各自占一张独立贴图包体和内存直接翻几倍。3. 用 Animation 组件跑通角色状态切换控制器代码与 tween 补帧3.1 搭 Animation 组件与五个动画剪辑从资源面板到属性检查器这一步在编辑器和代码之间来回切。给角色根节点挂一个Animation组件然后把五个动画剪辑拖进组件的Clips列表。注意剪辑名字必须是英文小写后续play(run)的入参就是这个名字中文名在序列化时容易出编码问题真机上偶发找不到剪辑。动画剪辑的循环设置不在这里做在动画编辑器里做。选中某个剪辑打开动画编辑器右下角属性里可以勾Loop。run、idle、slide 必须勾循环jump、fall、die 不勾。这里有个容易看漏的地方wrapMode里除了 Loop还有一个 PingPong 选项做待机呼吸可以用做跑步千万别用——跑步动画一旦 PingPong 会变成正着跑一步、倒着跑一步看着像喝多了。搭好后在场景里点播放确认每个剪辑都能正常预览。老项目里经常出现预览正常、实例上看不到动画的情况原因大多是Animation组件的PlayOnLoad被勾上导致场景加载时先播了一个剪辑再被代码play覆盖时出现短暂黑屏或闪烁。所以我的建议是所有跑酷角色的PlayOnLoad一律关掉统一交给状态机控制。3.2 写 PlayerController输入映射、状态互斥与动画播放动画组件的壳搭好之后核心逻辑就在控制器脚本里。下面这个是跑酷主角的完整状态切换骨架输入用键盘真机上改成虚拟按键调用同样的接口import { _decorator, Component, Animation, input, Input, EventKeyboard, KeyCode, tween, Vec3 } from cc; const { ccclass, property } _decorator; enum PlayerState { Idle idle, Run run, Jump jump, Fall fall, Slide slide, Die die, } ccclass(PlayerController) export class PlayerController extends Component { property(Animation) anim: Animation | null null; private _state: PlayerState PlayerState.Idle; private _isGrounded true; private _canDoubleJump false; onLoad() { // 键盘输入只做演示移动端换成触屏按钮回调 input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this); } start() { this.switchState(PlayerState.Run); } onKeyDown(event: EventKeyboard) { if (event.keyCode KeyCode.SPACE) { this.tryJump(); } else if (event.keyCode KeyCode.ARROW_DOWN) { this.trySlide(); } } tryJump() { if (this._state PlayerState.Die) return; if (this._isGrounded) { this.switchState(PlayerState.Jump); this._isGrounded false; this._canDoubleJump true; // 允许二段跳 } else if (this._canDoubleJump (this._state PlayerState.Jump || this._state PlayerState.Fall)) { this.switchState(PlayerState.Jump); this._canDoubleJump false; } } trySlide() { if (this._state PlayerState.Run || this._state PlayerState.Fall) { this.switchState(PlayerState.Slide); } } switchState(next: PlayerState) { // 状态互斥跳和落之间允许切换滑铲不可中途起跳 if (next this._state) return; if (this._state PlayerState.Slide next PlayerState.Jump) return; if (this._state PlayerState.Die) return; this.anim?.play(next); this._state next; } onLand() { // 由碰撞系统或 Y 轴位移判底后调用 this._isGrounded true; this._canDoubleJump false; if (this._state ! PlayerState.Die) { this.switchState(PlayerState.Run); } } }代码的核心是switchState里的互斥判断。没有这层判断连续按跳跃键会在 jump 和 fall 之间来回抽角色在半空不停变动作这就是典型的“状态机抖动”。跳和落之间我允许互切是为了二段跳时能把手感做顺滑铲和死亡则不参与跳转。注意onLand要由碰撞系统调用不要放在动画播完的回调里因为落地时机由物理计算决定动画只是视觉表现。播放调用很简单anim.play(run)传入的就是剪辑名。有个细节播放重复调用同一个剪辑不会有问题但频繁切不同的剪辑在低端机上有微小卡顿。优化手段是给优先用的 run、slide 剪辑设置AnimationClip的isShared为 true让多个实例共享动画数据减少序列化压力。3.3 用 tween 补动作细节跳跃压扁、落地拉伸与镜头跟随动画只解决肢体动作角色的空间位移和“重量感”要靠 tween 补。跑酷游戏跳跃时如果只有 y 轴移动看起来就是一个纸片人在跳。我给起跳和落地各加了一组 squash stretch压扁拉伸用 tween 控制节点缩放squashAndStretch() { const scale this.node.scale; // 压扁落地瞬间横向变宽、纵向变矮 tween(this.node) .to(0.06, { scale: new Vec3(scale.x * 1.15, scale.y * 0.85, 1) }) .to(0.12, { scale: new Vec3(scale.x, scale.y, 1) }) .start(); } stretchOnJump() { const scale this.node.scale; // 拉伸起跳瞬间纵向变长 tween(this.node) .to(0.08, { scale: new Vec3(scale.x * 0.9, scale.y * 1.1, 1) }) .to(0.1, { scale: new Vec3(scale.x, scale.y, 1) }) .start(); }这两个方法分别在tryJump里调stretchOnJump、在onLand里调squashAndStretch。时间参数有讲究0.06 秒压缩、0.12 秒回弹这个比例符合真实物体的弹性衰减。数值改大就变成卡通橡胶感改小几乎看不出建议以 0.08 为分界线自己试手感。镜头跟随也是动作体验的一部分。跑酷镜头一般固定在角色后方但跳跃时可以加一点 y 轴偏移让玩家提前看到上方障碍。写法是给相机节点挂跟随脚本在跳跃时用 tween 把相机的本地偏移从(0, 0, 0)缓动到(0, 2, 0)落地再缓动回来。注意镜头缓动时间要比角色跳跃总时长短一截不然落地时镜头还没归位玩家会觉得画面在“晃”。4. 把「动作」做出打击感帧事件、碰撞判定与屏幕震动4.1 在动画时间轴上钉事件帧踩地音效、粒子与判定开关动画不只是“播放”它还是一个时间轴——某些瞬间必须触发特定逻辑。比如滑铲时角色压低的那一帧要触发尘土粒子、踩到怪物头顶那帧要弹跳并播放得分音效。Creator 的动画编辑器支持在时间轴上添加事件帧做法是选中动画剪辑在时间轴对应帧位置右键添加事件填入方法名。这个方法要挂在角色节点上也就是和Animation组件同一个节点的组件上。比如我在 run 剪辑第 8 帧添加了名为onFootstep的事件在 slide 剪辑第 3 帧添加onSlideDust在 jump 剪辑第 2 帧添加onJumpRise。代码侧对应onFootstep() { // 踩地音效不管跑步还是跳跃落地都走这个回调 AudioManager.instance.playSFX(footstep_grass); // 可以在这里做极轻的屏幕震动幅度 0.02 就够 } onSlideDust() { // 滑铲扬尘粒子的挂点 this.dustParticle.node.active true; this.dustParticle.play(); } onJumpRise() { // 起跳瞬间的音效反馈延迟不宜超过动画帧的 1 帧 AudioManager.instance.playSFX(jump_whoosh); }帧事件最怕的是时机错位音效比动作早 2 帧或晚 3 帧打击感全毁。音频对帧这件事有点玄学我的经验是——视觉和听觉的同步误差超过 100ms玩家就会觉得“打不到点”。因此事件帧不要钉在动画的第 0 帧留出 1 到 3 帧的预备动作比如起跳音效放在第 3 帧而不是起跳瞬间反而更有发力感。4.2 判定框跟随动画帧开关判断这是「有效攻击」还是「擦肩而过」跑酷的碰撞判定可以粗暴地用一个大 Box 包住角色但玩家很快会察觉“明明跳过了尖刺还是死”。角色的视觉模型和判定模型必须分开。我给角色挂了两个碰撞体一个是身体主判定Box全程开启用于撞障碍另一个是脚部特殊判定也即“踩怪判定”只在特定的几帧开启用于踩怪弹跳。脚部判定在动画事件里动态开关enableFootHitbox() { this.footCollider.enabled true; } disableFootHitbox() { this.footCollider.enabled false; } onCollisionEnter(other: Collider2D) { if (other.tag 100) { // 怪物 tag 固定为 100 if (this.footCollider.enabled) { this.bounceOnEnemy(); // 踩到怪物弹跳 } else { this.die(); // 身体撞到怪物 } } }踩怪弹跳的判定窗口我放在角色下落的第 1 到第 4 帧这时脚部碰撞体才打开。为什么不做物理材质触发的自动弹跳因为跑酷的“踩怪”本质是玩家主动落点控制如果怪物顶部任何位置都能踩玩家会养成无脑跳的坏习惯游戏深度就没了。把这个判定框缩到脚底一小条还顺带解决了“擦肩而过却判定命中”的差评点。实现时记得footCollider.enabled false要在onDisable里重置否则角色死亡重开后碰撞体还是关的会出现穿模。4.3 反馈的最后一公里屏幕震动与得分飘字动作判定的结果必须“看得见”。屏幕震动是跑酷反馈性价比最高的一招我用 tween 做最简版本shakeCamera(duration: number 0.15, magnitude: number 0.06) { const camera this.mainCamera.node; const origin camera.position.clone(); tween(camera) .by(duration * 0.4, { position: new Vec3(magnitude, magnitude, 0) }) .by(duration * 0.3, { position: new Vec3(-magnitude, -magnitude, 0) }) .by(duration * 0.3, { position: new Vec3(magnitude * 0.6, -magnitude * 0.6, 0) }) .call(() camera.setPosition(origin)) .start(); }震动幅度magnitude我用 0.06 到 0.08过低没感觉过高玩家会头晕。踩怪弹跳用 0.05死亡用 0.12。震完必须.call复位否则相机会永久偏离原点。真机上更细腻的做法是在update里用随机偏移模拟但 tween 版已经覆盖 90% 的使用场景性能开销也低。得分反馈要做“数值增长”而不是瞬间加分数。玩家对数字变化天生敏感100 分跳 3 次加到 1000 分和直接显示 1000 分前者的爽感完全不同rollScore(label: Label, target: number, duration: number 0.4) { const start Number(label.string) || 0; tween({ value: start }) .to(duration, { value: target }, { onUpdate: (current: any) { label.string Math.floor(current.value).toString(); }, easing: quadOut, }) .start(); }easing: quadOut让前期涨得快、后期慢比匀速更容易制造“破纪录”的成就感。这里有个性能细节不要每帧都改 Label 的string花销大在onUpdate里做整数判断值没变就不赋值。5. 避坑动画在编辑器里正常、真机与打包后翻车的五个典型5.1 播放层翻车动画显示不全、状态机抖动、回调不触发动画显示不全。现象真机上角色贴图花屏、缺一块或者透明编辑器里完全正常。原因动态合图Dynamic Atlas在动画播放时还没把贴图合进去或者合图后 UV 计算在个别 GPU 驱动上有偏差。解决把角色的 SpriteFrame 全部拖入场景中某个不显示的 Sprite 节点的spriteFrame属性强制它们进合图或者在项目设置里关掉动态合图改用预打包图集。金币、特效这类小图最容易触发我习惯把所有 UI 和特效小图都放进一张手动图集。状态机抖动。现象连续快速按跳跃键角色在半空反复切换 jump/fall动作抽搐严重时还会卡进 slide 里出不来。原因update里每帧判断输入输入的判定帧重叠导致状态互斥失效。解决switchState里做状态拦截并且加一个“冷却帧”机制——同一状态至少持续 2 帧才允许切走。二段跳也必须在 jump/fall 播到第 5 帧之后才响应避免起跳瞬间连按变成两段跳手感稀碎。动画回调不触发。现象动画明明播完了FINISHED回调就是不走但编辑器里单测却正常。原因动画播放过程中节点被setActive(false)或者节点的Animation组件被pause()回调在inactive的节点上不会派发。解决把动画结束的回调放在状态机里轮询检测而不是只依赖Animation.Event.FINISHED。我在update里加一个帧计数器超过当前剪辑总帧数就强制判定结束这样可以绕开所有暂停和失活问题。5.2 资源与构建层翻车黑块、loading 不播、包体异常骨骼动画打包后黑块。现象构建 APK 装上后spine 角色的贴图区域变成黑色或紫色帧动画正常。原因有二一是贴图的Premultiplied Alpha设置和资源实际数据不符二是纹理压缩格式不支持半透明强制压缩成 ETC 后透明区域变成黑块。解决spine 贴图导入时把类型设置为sprite-frame纹理压缩格式选 RGBA8888 或 ETC2 的带 Alpha 版本。打包时看到压缩纹理选项先想清楚这个贴图有没有透明通道有就一律不走 ETC1。loading 动画不播。现象进游戏先黑屏然后直接跳到主界面中间那个转圈或插画过渡动画一次都没出现过。原因loading 场景引用的动画资源没有进对应 Bundle或者场景常驻节点提前把主场景的资源预热了过渡动画被跳过。解决把 loading 动画的全部资源放在和 loading 场景同一个 Bundle 里把启动场景设置为 loading同时关掉场景常驻节点的persist属性避免闪屏后资源预加载把过渡阶段冲掉。这个坑几乎只在真机上复现编辑器预览通常发现不了。包体异常膨胀。现象代码没加多少APK 从 20MB 涨到 60MB打开一看全是动画资源重复打包。原因同一个动画剪辑被多个场景引用时Cocos 的构建系统会按 Bundle 去重但如果你把所有资源都塞进resources目录它会强制全部打进首包重复资源也会被资源数据库标记为 “已使用”。解决动画资源按场景分包首场景只放首场景必要的动画金币和怪物相关的动画挪到对应关卡 Bundle。构建时看一眼“构建日志”里的各 Bundle 体积异常大的那个就是嫌疑犯。6. 打包 APK 前的动画资源收尾构建参数与包体瘦身技巧走完上面几章项目已经能跑、能玩、能痛最后一步是把源码打包成 APK。构建面板里和动画资源相关的选项就几个我按自己的项目给一份快查表构建选项作用推荐设置代码压缩Uglify压缩脚本体积打开开启后版本号务必 1压缩纹理纹理转 GPU 格式只对有把握的图集开spine 贴图保持 RGBA8888MD5 Cache文件指纹增量更新打开包体拆分按 Bundle 分包动画资源按场景拆首包只留启动场景和加载动画API Level目标安卓版本按发行渠道填过低会导致纹理格式兼容问题构建前我习惯先做一次资源检查清理无引用的动画剪辑把不用的动画帧删除确认所有 spine 贴图都没有遗留的未使用贴图集。动画资源比代码更容易撑爆包体我第一次跑酷 Demo 用全套序列帧APK 构建出来 80MB换成 spine 主角加序列帧特效其他不动包体直接掉到 20MB 以内。从那以后我养成了习惯——每次构建前看一眼build目录下assets文件夹里的资源分布哪个 Bundle 体积异常就去查资源。最后分享一个我自己的排错顺序先看编辑器预览再看浏览器模拟最后才上真机。动画相关的问题 80% 在编辑器里就能暴露真机上多出来的那 20% 大多是纹理压缩、内存和合图问题。别一上来就追求真机调试那个环境变量多排查成本高。把编辑器和浏览器两关过了再打包基本上就是一次过。以上都是血泪经验换来的希望对要照着这个教程做跑酷游戏的你有所帮助。本文还有配套的精品资源点击获取