news 2026/5/6 9:00:12

Day 05 · 让你的游戏会“动“:动画系统从 Clip 到状态机全解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Day 05 · 让你的游戏会“动“:动画系统从 Clip 到状态机全解

Day 05 · 让你的游戏会"动":动画系统从 Clip 到状态机全解

学习目标:掌握 Cocos 动画剪辑、动画组件、AnimationGraph 状态机和 Tween 缓动

预计时间:3 小时

难度:⭐⭐⭐☆☆


Cocos 动画系统全景

动画系统 ├── Animation Clip(动画剪辑)—— 单段动画数据 ├── Animation 组件 —— 播放管理器(简单场景) ├── AnimationGraph(动画图)—— 状态机(复杂角色动画) ├── Skeletal Animation —— 骨骼动画(DragonBones/Spine) └── Tween(缓动)—— 代码驱动的补间动画

1. Animation Clip(动画剪辑)

动画剪辑是动画的数据文件,记录了一段时间内节点属性(位置、旋转、透明度等)的变化曲线。

1.1 在编辑器中创建动画剪辑

  1. 选中要添加动画的节点
  2. 在 Inspector 中点击添加组件Animation
  3. 在 Animation 组件的 Clips 列表中点击+
  4. 创建新的 Animation Clip 文件

1.2 动画编辑器

打开动画编辑器(窗口 → 动画编辑器):

操作流程

  1. 点击"录制"按钮(红点)
  2. 移动时间轴到 0 秒,设置节点属性(位置/透明度等)→ 自动添加关键帧
  3. 移动时间轴到 1 秒,修改节点属性 → 自动添加关键帧
  4. 点击播放预览效果
  5. 点击"录制"按钮关闭录制模式

可以制作动画的属性

  • 节点的positionrotationscale
  • Sprite 的color(淡入淡出)
  • Sprite 的spriteFrame(帧动画)
  • UIOpacity 的opacity
  • 其他任意数值属性

2. Animation 组件(程序控制)

import{_decorator,Component,Animation,AnimationState}from'cc';const{ccclass,property}=_decorator;@ccclass('AnimationController')exportclassAnimationControllerextendsComponent{private_anim:Animation=null!;onLoad(){this._anim=this.getComponent(Animation)!;}start(){// 播放默认动画(defaultClip)this._anim.play();// 播放指定名称的动画this._anim.play('run');// 暂停this._anim.pause();// 继续this._anim.resume();// 停止this._anim.stop();// 获取动画状态(可以控制速度、时间等)conststate:AnimationState=this._anim.getState('jump')!;state.speed=2;// 2倍速播放state.wrapMode=2;// WrapMode.Loop = 2 循环播放this._anim.play('jump');}// 监听动画事件onLoad2(){this._anim=this.getComponent(Animation)!;// 动画结束事件this._anim.on(Animation.EventType.FINISHED,this.onAnimFinished,this);// 最后一帧事件(循环动画的每次循环结束)this._anim.on(Animation.EventType.LASTFRAME,this.onLastFrame,this);// 停止事件this._anim.on(Animation.EventType.STOP,this.onAnimStop,this);}onAnimFinished(type:Animation.EventType,state:AnimationState){console.log('动画结束:',state.name);// 动画播完后切换到下一个状态if(state.name==='die'){this.node.destroy();}}onLastFrame(type:Animation.EventType,state:AnimationState){console.log('最后一帧:',state.name);}onAnimStop(type:Animation.EventType,state:AnimationState){console.log('动画停止:',state.name);}onDestroy(){this._anim.off(Animation.EventType.FINISHED,this.onAnimFinished,this);this._anim.off(Animation.EventType.LASTFRAME,this.onLastFrame,this);this._anim.off(Animation.EventType.STOP,this.onAnimStop,this);}}

3. 帧动画(Sprite 序列帧)

制作人物跑步、爆炸等帧动画:

import{_decorator,Component,Sprite,SpriteFrame,Animation}from'cc';const{ccclass,property}=_decorator;@ccclass('FrameAnimation')exportclassFrameAnimationextendsComponent{// 在 Inspector 中拖入所有帧图片@property([SpriteFrame])walkFrames:SpriteFrame[]=[];@property([SpriteFrame])runFrames:SpriteFrame[]=[];@property({displayName:'帧率',min:1,max:60})fps:number=12;private_sprite:Sprite=null!;private_currentFrames:SpriteFrame[]=[];private_frameIndex:number=0;private_timer:number=0;onLoad(){this._sprite=this.getComponent(Sprite)!;}playWalk(){this._currentFrames=this.walkFrames;this._frameIndex=0;}playRun(){this._currentFrames=this.runFrames;this._frameIndex=0;}update(deltaTime:number){if(this._currentFrames.length===0)return;this._timer+=deltaTime;constframeDuration=1/this.fps;if(this._timer>=frameDuration){this._timer-=frameDuration;this._frameIndex=(this._frameIndex+1)%this._currentFrames.length;this._sprite.spriteFrame=this._currentFrames[this._frameIndex];}}}

4. AnimationGraph(动画状态机)

对于复杂角色(有 idle/walk/run/jump/die 多种状态),应使用AnimationGraph

4.1 创建 AnimationGraph

  1. 资源管理器右键 →新建AnimationGraph
  2. 双击打开 AnimationGraph 编辑器

4.2 状态机设计

Entry → [Idle] ←→ [Walk] ←→ [Run] ↓ [Jump] → [Fall] → [Land] → [Idle] ↓ [Die]

4.3 AnimationController 脚本

import{_decorator,Component,AnimationController}from'cc';const{ccclass,property}=_decorator;@ccclass('CharacterAnimator')exportclassCharacterAnimatorextendsComponent{private_animCtrl:AnimationController=null!;// AnimationGraph 中定义的变量名privatereadonlyPARAM_SPEED='speed';// Float:移动速度privatereadonlyPARAM_IS_JUMP='isJump';// Boolean:是否在空中privatereadonlyPARAM_IS_DEAD='isDead';// Boolean:是否死亡privatereadonlyTRIGGER_ATTACK='attack';// Trigger:攻击触发器onLoad(){this._animCtrl=this.getComponent(AnimationController)!;}// 更新移动速度(用于 Idle <-> Walk <-> Run 切换)setMoveSpeed(speed:number){this._animCtrl.setValue(this.PARAM_SPEED,speed);}// 跳跃状态setJumping(isJump:boolean){this._animCtrl.setValue(this.PARAM_IS_JUMP,isJump);}// 死亡die(){this._animCtrl.setValue(this.PARAM_IS_DEAD,true);}// 触发攻击动画triggerAttack(){this._animCtrl.setValue(this.TRIGGER_ATTACK,true);}// 在 AnimationGraph 转换条件中配置:// Idle → Walk:speed > 10// Walk → Run:speed > 150// Run → Walk:speed < 150// Walk → Idle:speed < 10// Any → Jump:isJump == true// Jump → Idle:isJump == false// Any → Die:isDead == true}

4.4 在编辑器中配置 AnimationGraph

  1. 选中角色节点 →添加组件AnimationController
  2. 将 AnimationGraph 文件拖入 Graph 槽


5. Tween(代码驱动缓动动画)

Tween 是纯代码控制的补间动画,无需动画文件,非常适合 UI 动画:

import{_decorator,Component,Node,tween,Vec3,UIOpacity,Easing}from'cc';const{ccclass}=_decorator;@ccclass('TweenDemo')exportclassTweenDemoextendsComponent{start(){this.playEntrance();}// UI 入场动画:从屏幕外飞入playEntrance(){conststartPos=newVec3(0,-800,0);constendPos=newVec3(0,0,0);this.node.setPosition(startPos);tween(this.node).to(0.5,{position:endPos},{easing:'backOut',// 弹出效果onUpdate:(target,ratio)=>{// ratio: 0~1,当前进度}}).call(()=>{console.log('入场动画完成');}).start();}// 弹跳缩放动画(强调效果)playPunchScale(){tween(this.node).to(0.1,{scale:newVec3(1.3,1.3,1)}).to(0.1,{scale:newVec3(0.9,0.9,1)}).to(0.05,{scale:newVec3(1.1,1.1,1)}).to(0.05,{scale:newVec3(1,1,1)}).start();}// 淡出销毁fadeOutAndDestroy(){constopacity=this.node.getComponent(UIOpacity)!;tween(opacity).to(0.3,{opacity:0}).call(()=>{this.node.destroy();}).start();}// 循环动画:悬浮效果playHoverLoop(){constoriginalY=this.node.position.y;tween(this.node).to(1.0,{position:newVec3(0,originalY+20,0)},{easing:'sineInOut'}).to(1.0,{position:newVec3(0,originalY-20,0)},{easing:'sineInOut'}).union()// 将上面两个 tween 合并为一个.repeatForever()// 无限循环.start();}// 序列动画(多个动画依次执行)playSequence(){tween(this.node).delay(0.5)// 延迟 0.5 秒.to(0.3,{scale:newVec3(1.2,1.2,1)}).to(0.3,{scale:newVec3(1,1,1)}).delay(1).call(()=>console.log('序列完成')).start();}// 停止所有 TweenstopAllTweens(){this.node.stopAllActions();// 停止该节点的所有 tween// 或:tween(this.node).stop();}}

5.1 常用缓动曲线


图片源自 http://hosted.zeh.com.br/tweener/docs/en-us/

缓动名效果适用场景
linear匀速匀速移动
sineInOut平滑开始和结束悬浮、摇摆
backOut超出后回弹UI 弹出
bounceOut弹跳结束落地、弹跳
elasticOut弹簧效果强调弹出
expoIn极慢后极快冲刺加速
expoOut极快后极慢刹车减速

6. 实战:制作得分弹出动画

你接下来在编辑器里做 3 步就行:

给 enemy 节点挂组件:把 Enemy(assets/Enemy.ts)挂到你的敌人节点上
给按钮挂组件:把 GameOverClick 挂到按钮节点(或任意节点)上,并在属性里把 enemy 槽位拖拽绑定到那个敌人节点
绑定按钮点击事件:Button 组件的 ClickEvents 里选 GameOverClick 所在节点,方法选 justClick

import{_decorator,Component,Node}from'cc';import{ScorePopup}from'./ScorePopup';const{ccclass,property}=_decorator;@ccclass('Enemy')exportclassEnemyextendsComponent{@property({displayName:'击败得分',min:0})score:number=100;@property({displayName:'飘字父节点(UI/Canvas等)'})popupParent:Node|null=null;/** * 在“敌人被击败”的那一刻调用它。 * 你可以在碰撞/受伤/HP归零处调用 defeat()。 */defeat(){constparent=this.popupParent??this.node.parent;if(parent){constp=this.node.worldPosition;ScorePopup.show(parent,this.score,p.x,p.y);}this.node.destroy();}}
import{_decorator,Component,Node}from'cc';import{Enemy}from'./Enemy';const{ccclass,property}=_decorator;@ccclass('GameOverClick')exportclassGameOverClickextendsComponent{@property({type:Node,displayName:'要模拟击败的敌人'})enemy:Node=null!;start(){}update(deltaTime:number){}justClick(){constenemyNode=this.enemy;if(!enemyNode)return;constenemy=enemyNode.getComponent(Enemy);enemy?.defeat();}}
import{_decorator,Component,Label,Node,tween,UIOpacity,UITransform,Vec3}from'cc';const{ccclass,property}=_decorator;@ccclass('ScorePopup')exportclassScorePopupextendsComponent{// 调用此方法在指定位置弹出得分文字staticshow(parent:Node,score:number,worldX:number,worldY:number){// 动态创建 Label 节点constpopupNode=newNode('ScorePopup');parent.addChild(popupNode);constworldPos=newVec3(worldX,worldY,0);constparentUITransform=parent.getComponent(UITransform);if(parentUITransform){// UI 节点下更推荐用本地坐标,避免与 position tween 混用坐标系constlocalPos=parentUITransform.convertToNodeSpaceAR(worldPos);popupNode.setPosition(localPos);}else{popupNode.setWorldPosition(worldPos);}constlabel=popupNode.addComponent(Label);label.string=`+${score}`;label.fontSize=36;label.color.fromHEX('#FFD700');// 金色constopacity=popupNode.addComponent(UIOpacity);// 动画:向上飘+淡出tween(popupNode).by(0.8,{position:newVec3(0,80,0)},{easing:'expoOut'}).start();tween(opacity).delay(0.3).to(0.5,{opacity:0}).call(()=>popupNode.destroy()).start();}// 使用方式:// ScorePopup.show(this.node.parent!, 100, enemy.worldPosition.x, enemy.worldPosition.y);}




7. 今日总结

  • ✅ 理解动画系统全景:Clip / Animation 组件 / AnimationGraph / Tween
  • ✅ 掌握动画编辑器:录制关键帧、编辑曲线
  • ✅ 掌握帧动画(序列帧)的代码实现
  • ✅ 学会 AnimationGraph 状态机设计与脚本控制
  • ✅ 掌握 Tween API 和常用缓动效果
  • ✅ 实战:得分弹出动画

⚠️ 动画常见坑

问题解决方案
Tween 动画中途停不下来节点销毁前调用node.stopAllActions()
AnimationGraph 状态不切换检查转换条件的参数名是否和代码中 setValue 一致
帧动画闪烁fps设置太高,或 SpriteFrame 未打包图集(Atlas)
动画结束事件没触发wrapMode 设置为 Loop 时不会触发 FINISHED,用 LASTFRAME

← Day 04 | 系列目录 | Day 06 →

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/6 8:59:45

Tensorflow离线安装全攻略:从whl下载到ARM架构适配(附资源链接)

TensorFlow离线安装全攻略&#xff1a;从whl下载到ARM架构适配 在边缘计算和嵌入式开发领域&#xff0c;离线环境下的TensorFlow部署一直是工程师们的痛点。想象一下&#xff0c;当你带着开发板深入工厂现场调试&#xff0c;或是需要在保密网络中进行AI模型部署时&#xff0c;…

作者头像 李华
网站建设 2026/4/12 3:37:00

照片变3D模型就这么简单!Face3D.ai Pro保姆级教程,从安装到导出

照片变3D模型就这么简单&#xff01;Face3D.ai Pro保姆级教程&#xff0c;从安装到导出 1. 环境准备与快速部署 1.1 系统要求检查 在开始之前&#xff0c;请确认你的设备满足以下最低配置要求&#xff1a; 操作系统&#xff1a;Linux&#xff08;推荐Ubuntu 18.04及以上&am…

作者头像 李华
网站建设 2026/4/11 17:20:07

超厉害的AI教材写作工具,低查重快速产出高质量教材!

在整理教材的过程中&#xff0c;我们常常遇到棘手的难题&#xff0c;这项工作简直像是一种“精细活”。其中最大的挑战就是如何找到平衡与衔接的点&#xff01;一方面&#xff0c;我们总是担心会遗漏重要的核心知识点&#xff1b;另一方面&#xff0c;如何控制好难度的递进关系…

作者头像 李华
网站建设 2026/4/11 22:24:07

相亲软件靠谱,还是知名品牌靠谱?我给你讲明白

各位单身的朋友&#xff0c;今天咱们不绕弯子&#xff0c;直接聊聊当下市面上那些五花八门的相亲平台——有工具型的&#xff0c;有连锁型的&#xff0c;也有主打创新模式的。我会把它们的优缺点掰开揉碎了说&#xff0c;最后给大家一个最实在的推荐。先说说两款工具型平台&…

作者头像 李华
网站建设 2026/4/11 18:31:35

react native如何发送蓝牙命令

使用react-native-ble-plx插件&#xff1a; import { createContext, useState, useEffect, useContext, useRef } from react; import { BleManager } from react-native-ble-plx; import * as Location from expo-location; import { Platform, PermissionsAndroid, ToastAn…

作者头像 李华