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

张开发
2026/4/10 13:29:13 15 分钟阅读

分享文章

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 在编辑器中创建动画剪辑选中要添加动画的节点在 Inspector 中点击添加组件→Animation在 Animation 组件的 Clips 列表中点击创建新的 Animation Clip 文件1.2 动画编辑器打开动画编辑器窗口 → 动画编辑器操作流程点击录制按钮红点移动时间轴到 0 秒设置节点属性位置/透明度等→ 自动添加关键帧移动时间轴到 1 秒修改节点属性 → 自动添加关键帧点击播放预览效果点击录制按钮关闭录制模式可以制作动画的属性节点的position、rotation、scaleSprite 的color淡入淡出Sprite 的spriteFrame帧动画UIOpacity 的opacity其他任意数值属性2. Animation 组件程序控制import{_decorator,Component,Animation,AnimationState}fromcc;const{ccclass,property}_decorator;ccclass(AnimationController)exportclassAnimationControllerextendsComponent{private_anim:Animationnull!;onLoad(){this._animthis.getComponent(Animation)!;}start(){// 播放默认动画defaultClipthis._anim.play();// 播放指定名称的动画this._anim.play(run);// 暂停this._anim.pause();// 继续this._anim.resume();// 停止this._anim.stop();// 获取动画状态可以控制速度、时间等conststate:AnimationStatethis._anim.getState(jump)!;state.speed2;// 2倍速播放state.wrapMode2;// WrapMode.Loop 2 循环播放this._anim.play(jump);}// 监听动画事件onLoad2(){this._animthis.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.namedie){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}fromcc;const{ccclass,property}_decorator;ccclass(FrameAnimation)exportclassFrameAnimationextendsComponent{// 在 Inspector 中拖入所有帧图片property([SpriteFrame])walkFrames:SpriteFrame[][];property([SpriteFrame])runFrames:SpriteFrame[][];property({displayName:帧率,min:1,max:60})fps:number12;private_sprite:Spritenull!;private_currentFrames:SpriteFrame[][];private_frameIndex:number0;private_timer:number0;onLoad(){this._spritethis.getComponent(Sprite)!;}playWalk(){this._currentFramesthis.walkFrames;this._frameIndex0;}playRun(){this._currentFramesthis.runFrames;this._frameIndex0;}update(deltaTime:number){if(this._currentFrames.length0)return;this._timerdeltaTime;constframeDuration1/this.fps;if(this._timerframeDuration){this._timer-frameDuration;this._frameIndex(this._frameIndex1)%this._currentFrames.length;this._sprite.spriteFramethis._currentFrames[this._frameIndex];}}}4. AnimationGraph动画状态机对于复杂角色有 idle/walk/run/jump/die 多种状态应使用AnimationGraph。4.1 创建 AnimationGraph资源管理器右键 →新建→AnimationGraph双击打开 AnimationGraph 编辑器4.2 状态机设计Entry → [Idle] ←→ [Walk] ←→ [Run] ↓ [Jump] → [Fall] → [Land] → [Idle] ↓ [Die]4.3 AnimationController 脚本import{_decorator,Component,AnimationController}fromcc;const{ccclass,property}_decorator;ccclass(CharacterAnimator)exportclassCharacterAnimatorextendsComponent{private_animCtrl:AnimationControllernull!;// AnimationGraph 中定义的变量名privatereadonlyPARAM_SPEEDspeed;// Float移动速度privatereadonlyPARAM_IS_JUMPisJump;// Boolean是否在空中privatereadonlyPARAM_IS_DEADisDead;// Boolean是否死亡privatereadonlyTRIGGER_ATTACKattack;// Trigger攻击触发器onLoad(){this._animCtrlthis.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 → Walkspeed 10// Walk → Runspeed 150// Run → Walkspeed 150// Walk → Idlespeed 10// Any → JumpisJump true// Jump → IdleisJump false// Any → DieisDead true}4.4 在编辑器中配置 AnimationGraph选中角色节点 →添加组件→AnimationController将 AnimationGraph 文件拖入 Graph 槽5. Tween代码驱动缓动动画Tween 是纯代码控制的补间动画无需动画文件非常适合 UI 动画import{_decorator,Component,Node,tween,Vec3,UIOpacity,Easing}fromcc;const{ccclass}_decorator;ccclass(TweenDemo)exportclassTweenDemoextendsComponent{start(){this.playEntrance();}// UI 入场动画从屏幕外飞入playEntrance(){conststartPosnewVec3(0,-800,0);constendPosnewVec3(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(){constopacitythis.node.getComponent(UIOpacity)!;tween(opacity).to(0.3,{opacity:0}).call((){this.node.destroy();}).start();}// 循环动画悬浮效果playHoverLoop(){constoriginalYthis.node.position.y;tween(this.node).to(1.0,{position:newVec3(0,originalY20,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 节点挂组件把 Enemyassets/Enemy.ts挂到你的敌人节点上给按钮挂组件把 GameOverClick 挂到按钮节点或任意节点上并在属性里把 enemy 槽位拖拽绑定到那个敌人节点绑定按钮点击事件Button 组件的 ClickEvents 里选 GameOverClick 所在节点方法选 justClickimport{_decorator,Component,Node}fromcc;import{ScorePopup}from./ScorePopup;const{ccclass,property}_decorator;ccclass(Enemy)exportclassEnemyextendsComponent{property({displayName:击败得分,min:0})score:number100;property({displayName:飘字父节点(UI/Canvas等)})popupParent:Node|nullnull;/** * 在“敌人被击败”的那一刻调用它。 * 你可以在碰撞/受伤/HP归零处调用 defeat()。 */defeat(){constparentthis.popupParent??this.node.parent;if(parent){constpthis.node.worldPosition;ScorePopup.show(parent,this.score,p.x,p.y);}this.node.destroy();}}import{_decorator,Component,Node}fromcc;import{Enemy}from./Enemy;const{ccclass,property}_decorator;ccclass(GameOverClick)exportclassGameOverClickextendsComponent{property({type:Node,displayName:要模拟击败的敌人})enemy:Nodenull!;start(){}update(deltaTime:number){}justClick(){constenemyNodethis.enemy;if(!enemyNode)return;constenemyenemyNode.getComponent(Enemy);enemy?.defeat();}}import{_decorator,Component,Label,Node,tween,UIOpacity,UITransform,Vec3}fromcc;const{ccclass,property}_decorator;ccclass(ScorePopup)exportclassScorePopupextendsComponent{// 调用此方法在指定位置弹出得分文字staticshow(parent:Node,score:number,worldX:number,worldY:number){// 动态创建 Label 节点constpopupNodenewNode(ScorePopup);parent.addChild(popupNode);constworldPosnewVec3(worldX,worldY,0);constparentUITransformparent.getComponent(UITransform);if(parentUITransform){// UI 节点下更推荐用本地坐标避免与 position tween 混用坐标系constlocalPosparentUITransform.convertToNodeSpaceAR(worldPos);popupNode.setPosition(localPos);}else{popupNode.setWorldPosition(worldPos);}constlabelpopupNode.addComponent(Label);label.string${score};label.fontSize36;label.color.fromHEX(#FFD700);// 金色constopacitypopupNode.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 →

更多文章