Bootstrap

Cocos Creator引擎开发:动作效果与角色动画_动画事件与回调

动画事件与回调

在Cocos Creator中,动画事件与回调是实现复杂动画逻辑和交互的重要手段。通过动画事件,我们可以在动画的关键帧上触发特定的回调函数,从而实现动画与游戏逻辑的紧密结合。本节将详细介绍如何在Cocos Creator中使用动画事件与回调,包括事件的创建、绑定和触发过程。

动画事件的创建

在Cocos Creator中,动画事件可以通过以下两种方式创建:

  1. 在动画编辑器中创建

    • 打开动画编辑器,选择需要添加事件的动画片段。

    • 在时间轴上找到需要插入事件的关键帧。

    • 右键点击关键帧,选择“添加事件”选项。

    • 在弹出的对话框中输入事件名称和参数。

  2. 通过代码创建

    • 使用cc.AnimationClip对象的addEvent方法在关键帧上添加事件。

示例:在动画编辑器中创建动画事件

假设我们有一个角色跳跃动画,我们希望在动画的最高点触发一个事件,该事件可以用来播放音效或触发其他逻辑。

  1. 打开动画编辑器,选择角色的跳跃动画片段。

  2. 在时间轴上找到跳跃动画的最高点,假设在第10帧。

  3. 右键点击第10帧,选择“添加事件”选项。

  4. 在弹出的对话框中输入事件名称,例如jump_peak,并可以添加一些参数,例如{"sound": "jump_sound"}

示例:通过代码创建动画事件


import { _decorator, Component, Node, Animation, AnimationClip, AnimationEvent } from 'cc';

const { ccclass, property } = _decorator;



@ccclass('AnimationEvents')

export class AnimationEvents extends Component {

    @property({ type: Node })

    private animatorNode: Node = null;



    start() {

        // 获取动画组件

        const animation = this.animatorNode.getComponent(Animation);



        // 创建一个新的动画片段

        const clip = new AnimationClip();

        clip.name = 'jump_clip';

        clip.duration = 1.0; // 动画持续时间为1秒



        // 创建一个动画事件

        const event = new AnimationEvent();

        event.frame = 0.5; // 事件发生在动画的0.5秒处

        event.type = 'event';

        event.tag = 'jump_peak';

        event.callback = this.onJumpPeak.bind(this); // 绑定回调函数



        // 将事件添加到动画片段

        clip.addEvent(event);



        // 将动画片段添加到动画组件

        animation.addClip(clip);



        // 播放动画

        animation.play('jump_clip');

    }



    // 回调函数

    private onJumpPeak(params: any) {

        console.log('Jump peak reached!', params);

        // 播放音效

        this.playSound(params.sound);

    }



    // 播放音效的函数

    private playSound(soundName: string) {

        // 假设有一个音效管理器

        const audioManager = this.node.getComponent(AudioManager);

        if (audioManager) {

            audioManager.playSound(soundName);

        }

    }

}

动画事件的绑定

动画事件的绑定是指将动画事件与特定的回调函数关联起来。在Cocos Creator中,可以通过以下方式绑定动画事件:

  1. 在动画编辑器中绑定

    • 在动画编辑器中创建的事件会自动绑定到动画片段。

    • 你可以在事件的回调属性中选择一个脚本方法,或者直接输入方法名。

  2. 通过代码绑定

    • 使用cc.AnimationClip对象的addEvent方法时,可以指定一个回调函数。

示例:通过代码绑定动画事件


import { _decorator, Component, Node, Animation, AnimationClip, AnimationEvent } from 'cc';

const { ccclass, property } = _decorator;



@ccclass('AnimationEvents')

export class AnimationEvents extends Component {

    @property({ type: Node })

    private animatorNode: Node = null;



    start() {

        // 获取动画组件

        const animation = this.animatorNode.getComponent(Animation);



        // 创建一个新的动画片段

        const clip = new AnimationEvent();

        clip.name = 'jump_clip';

        clip.duration = 1.0; // 动画持续时间为1秒



        // 创建一个动画事件

        const event = new AnimationEvent();

        event.frame = 0.5; // 事件发生在动画的0.5秒处

        event.type = 'event';

        event.tag = 'jump_peak';

        event.callback = this.onJumpPeak.bind(this); // 绑定回调函数



        // 将事件添加到动画片段

        clip.addEvent(event);



        // 将动画片段添加到动画组件

        animation.addClip(clip);



        // 播放动画

        animation.play('jump_clip');

    }



    // 回调函数

    private onJumpPeak(params: any) {

        console.log('Jump peak reached!', params);

        // 播放音效

        this.playSound(params.sound);

    }



    // 播放音效的函数

    private playSound(soundName: string) {

        // 假设有一个音效管理器

        const audioManager = this.node.getComponent(AudioManager);

        if (audioManager) {

            audioManager.playSound(soundName);

        }

    }

}

动画事件的触发

当动画播放到绑定事件的关键帧时,会自动触发回调函数。触发过程是基于时间轴的关键帧位置,因此需要确保事件的关键帧位置与动画逻辑相匹配。

示例:触发动画事件

假设我们有一个角色跳跃动画,我们在动画的最高点添加了一个事件jump_peak,并在该事件的回调函数中播放音效。

  1. 动画编辑器中的设置

    • 在时间轴的第10帧添加事件jump_peak

    • 设置事件的回调函数为onJumpPeak

  2. 代码中的设置


import { _decorator, Component, Node, Animation, AnimationClip, AnimationEvent } from 'cc';

const { ccclass, property } = _decorator;



@ccclass('AnimationEvents')

export class AnimationEvents extends Component {

    @property({ type: Node })

    private animatorNode: Node = null;



    start() {

        // 获取动画组件

        const animation = this.animatorNode.getComponent(Animation);



        // 加载动画片段

        const clip = animation.getClip('jump_clip');



        // 检查动画片段是否存在

        if (clip) {

            // 检查动画事件是否存在

            const event = clip.findEventByTag('jump_peak');

            if (event) {

                event.callback = this.onJumpPeak.bind(this); // 绑定回调函数

            } else {

                console.error('Event "jump_peak" not found in animation clip "jump_clip"');

            }



            // 播放动画

            animation.play('jump_clip');

        } else {

            console.error('Animation clip "jump_clip" not found');

        }

    }



    // 回调函数

    private onJumpPeak(params: any) {

        console.log('Jump peak reached!', params);

        // 播放音效

        this.playSound(params.sound);

    }



    // 播放音效的函数

    private playSound(soundName: string) {

        // 假设有一个音效管理器

        const audioManager = this.node.getComponent(AudioManager);

        if (audioManager) {

            audioManager.playSound(soundName);

        }

    }

}

动画事件的应用场景

动画事件与回调在动作游戏中有广泛的应用,以下是一些常见的应用场景:

  1. 音效播放

    • 在动画的关键帧上触发音效播放,例如角色跳跃的最高点播放跳跃音效。
  2. 技能释放

    • 在动画的特定帧上触发技能释放逻辑,例如角色释放大招的动画过程中触发大招效果。
  3. 状态切换

    • 在动画结束时切换角色的状态,例如角色从跳跃状态切换到落地状态。
  4. 触发特效

    • 在动画的特定帧上触发特效,例如角色受到攻击时播放受伤特效。

示例:技能释放的动画事件

假设我们有一个角色释放技能的动画,我们在动画的关键帧上添加事件skill_release,并在该事件的回调函数中触发技能效果。

  1. 动画编辑器中的设置

    • 在时间轴的第5帧添加事件skill_release

    • 设置事件的回调函数为onSkillRelease

  2. 代码中的设置


import { _decorator, Component, Node, Animation, AnimationClip, AnimationEvent, ParticleSystem } from 'cc';

const { ccclass, property } = _decorator;



@ccclass('SkillAnimation')

export class SkillAnimation extends Component {

    @property({ type: Node })

    private animatorNode: Node = null;



    @property({ type: ParticleSystem })

    private skillEffect: ParticleSystem = null;



    start() {

        // 获取动画组件

        const animation = this.animatorNode.getComponent(Animation);



        // 加载动画片段

        const clip = animation.getClip('skill_clip');



        // 检查动画片段是否存在

        if (clip) {

            // 检查动画事件是否存在

            const event = clip.findEventByTag('skill_release');

            if (event) {

                event.callback = this.onSkillRelease.bind(this); // 绑定回调函数

            } else {

                console.error('Event "skill_release" not found in animation clip "skill_clip"');

            }



            // 播放动画

            animation.play('skill_clip');

        } else {

            console.error('Animation clip "skill_clip" not found');

        }

    }



    // 回调函数

    private onSkillRelease(params: any) {

        console.log('Skill released!', params);

        // 播放技能特效

        this.playSkillEffect();

        // 触发技能逻辑

        this.triggerSkillLogic(params.target);

    }



    // 播放技能特效的函数

    private playSkillEffect() {

        if (this.skillEffect) {

            this.skillEffect.play();

        }

    }



    // 触发技能逻辑的函数

    private triggerSkillLogic(target: Node) {

        // 假设有一个技能管理器

        const skillManager = this.node.getComponent(SkillManager);

        if (skillManager) {

            skillManager.castSkill(target);

        }

    }

}

多个动画事件的处理

在某些情况下,一个动画片段可能需要绑定多个事件。Cocos Creator允许在一个动画片段中添加多个事件,并且每个事件可以绑定不同的回调函数。

示例:多个动画事件

假设我们有一个角色攻击动画,我们希望在动画的开始、中间和结束时分别触发不同的事件。

  1. 动画编辑器中的设置

    • 在时间轴的第0帧添加事件attack_start

    • 在时间轴的第5帧添加事件attack_middle

    • 在时间轴的第10帧添加事件attack_end

  2. 代码中的设置


import { _decorator, Component, Node, Animation, AnimationClip, AnimationEvent } from 'cc';

const { ccclass, property } = _decorator;



@ccclass('AttackAnimation')

export class AttackAnimation extends Component {

    @property({ type: Node })

    private animatorNode: Node = null;



    start() {

        // 获取动画组件

        const animation = this.animatorNode.getComponent(Animation);



        // 加载动画片段

        const clip = animation.getClip('attack_clip');



        // 检查动画片段是否存在

        if (clip) {

            // 绑定开始事件

            const startEvent = clip.findEventByTag('attack_start');

            if (startEvent) {

                startEvent.callback = this.onAttackStart.bind(this);

            } else {

                console.error('Event "attack_start" not found in animation clip "attack_clip"');

            }



            // 绑定中间事件

            const middleEvent = clip.findEventByTag('attack_middle');

            if (middleEvent) {

                middleEvent.callback = this.onAttackMiddle.bind(this);

            } else {

                console.error('Event "attack_middle" not found in animation clip "attack_clip"');

            }



            // 绑定结束事件

            const endEvent = clip.findEventByTag('attack_end');

            if (endEvent) {

                endEvent.callback = this.onAttackEnd.bind(this);

            } else {

                console.error('Event "attack_end" not found in animation clip "attack_clip"');

            }



            // 播放动画

            animation.play('attack_clip');

        } else {

            console.error('Animation clip "attack_clip" not found');

        }

    }



    // 开始事件的回调函数

    private onAttackStart(params: any) {

        console.log('Attack started!', params);

        // 播放攻击音效

        this.playSound(params.sound);

    }



    // 中间事件的回调函数

    private onAttackMiddle(params: any) {

        console.log('Attack middle!', params);

        // 触发攻击逻辑

        this.attackTarget(params.target);

    }



    // 结束事件的回调函数

    private onAttackEnd(params: any) {

        console.log('Attack ended!', params);

        // 播放攻击结束音效

        this.playSound(params.endSound);

    }



    // 播放音效的函数

    private playSound(soundName: string) {

        // 假设有一个音效管理器

        const audioManager = this.node.getComponent(AudioManager);

        if (audioManager) {

            audioManager.playSound(soundName);

        }

    }



    // 触发攻击逻辑的函数

    private attackTarget(target: Node) {

        // 假设有一个攻击管理器

        const attackManager = this.node.getComponent(AttackManager);

        if (attackManager) {

            attackManager.attack(target);

        }

    }

}

动画事件的参数传递

在创建动画事件时,可以传递参数给回调函数。这些参数可以在回调函数中使用,以实现更复杂的逻辑。

示例:传递参数给动画事件

假设我们有一个角色攻击动画,我们希望在动画的中间帧传递攻击目标的信息给回调函数。

  1. 动画编辑器中的设置

    • 在时间轴的第5帧添加事件attack_middle,并传递参数{"target": "enemy_node"}
  2. 代码中的设置


import { _decorator, Component, Node, Animation, AnimationClip, AnimationEvent } from 'cc';

const { ccclass, property } = _decorator;



@ccclass('AttackAnimation')

export class AttackAnimation extends Component {

    @property({ type: Node })

    private animatorNode: Node = null;



    start() {

        // 获取动画组件

        const animation = this.animatorNode.getComponent(Animation);



        // 加载动画片段

        const clip = animation.getClip('attack_clip');



        // 检查动画片段是否存在

        if (clip) {

            // 绑定中间事件

            const middleEvent = clip.findEventByTag('attack_middle');

            if (middleEvent) {

                middleEvent.callback = this.onAttackMiddle.bind(this);

            } else {

                console.error('Event "attack_middle" not found in animation clip "attack_clip"');

            }



            // 播放动画

            animation.play('attack_clip');

        } else {

            console.error('Animation clip "attack_clip" not found');

        }

    }



    // 中间事件的回调函数

    private onAttackMiddle(params: any) {

        console.log('Attack middle!', params);

        // 获取攻击目标

        const targetNode = this.node.getChildByName(params.target);

        if (targetNode) {

            // 触发攻击逻辑

            this.attackTarget(targetNode);

        }

    }



    // 触发攻击逻辑的函数

    private attackTarget(target: Node) {

        // 假设有一个攻击管理器

        const attackManager = this.node.getComponent(AttackManager);

        if (attackManager) {

            attackManager.attack(target);

        }

    }

}

动画事件的调试

在开发过程中,调试动画事件是非常重要的。Cocos Creator提供了多种调试工具和方法,可以帮助我们确保动画事件按预期触发。

示例:调试动画事件

  1. 使用控制台输出

    • 在回调函数中使用console.log输出事件信息,以便在控制台中查看事件的触发情况。
  2. 使用编辑器的调试工具

    • 在动画编辑器中启用“事件标记”选项,可以在时间轴上显示事件标记,方便查看事件的位置。

import { _decorator, Component, Node, Animation, AnimationClip, AnimationEvent } from 'cc';

const { ccclass, property } = _decorator;



@ccclass('DebugAnimationEvents')

export class DebugAnimationEvents extends Component {

    @property({ type: Node })

    private animatorNode: Node = null;



    start() {

        // 获取动画组件

        const animation = this.animatorNode.getComponent(Animation);



        // 加载动画片段

        const clip = animation.getClip('jump_clip');



        // 检查动画片段是否存在

        if (clip) {

            // 绑定事件

            const event = clip.findEventByTag('jump_peak');

            if (event) {

                event.callback = this.onJumpPeak.bind(this);

            } else {

                console.error('Event "jump_peak" not found in animation clip "jump_clip"');

            }



            // 播放动画

            animation.play('jump_clip');

        } else {

            console.error('Animation clip "jump_clip" not found');

        }

    }



    // 回调函数

    private onJumpPeak(params: any) {

        console.log('Jump peak reached!', params);

        // 播放音效

        this.playSound(params.sound);

    }



    // 播放音效的函数

    private playSound(soundName: string) {

        // 假设有一个音效管理器

        const audioManager = this.node.getComponent(AudioManager);

        if (audioManager) {

            audioManager.playSound(soundName);

        }

    }

}

动画事件的性能优化

在处理大量动画事件时,性能优化是一个需要考虑的重要因素。以下是一些优化建议:

  1. 减少不必要的事件绑定

    • 只在必要的动画片段上绑定事件,避免在所有动画片段上都绑定事件。这可以通过在动画编辑器中精心设计事件位置,或者在代码中根据具体需求动态绑定事件来实现。
  2. 使用缓存

    • 将常用的动画片段和事件缓存起来,避免每次播放动画时都重新创建和绑定事件。这样可以减少内存分配和垃圾回收的开销,提高性能。

示例:使用缓存优化动画事件

假设我们有一个角色,需要频繁播放跳跃动画和攻击动画,我们可以将这些动画片段和事件缓存起来,以减少每次播放动画时的开销。


import { _decorator, Component, Node, Animation, AnimationClip, AnimationEvent, resources } from 'cc';

const { ccclass, property } = _decorator;



@ccclass('OptimizedAnimationEvents')

export class OptimizedAnimationEvents extends Component {

    @property({ type: Node })

    private animatorNode: Node = null;



    private jumpClip: AnimationClip = null;

    private attackClip: AnimationClip = null;



    onLoad() {

        // 加载动画片段并缓存

        resources.load('animations/jump_clip', AnimationClip, (err, clip) => {

            if (err) {

                console.error('Failed to load jump_clip', err);

            } else {

                this.jumpClip = clip;

                this.bindJumpEvents();

            }

        });



        resources.load('animations/attack_clip', AnimationClip, (err, clip) => {

            if (err) {

                console.error('Failed to load attack_clip', err);

            } else {

                this.attackClip = clip;

                this.bindAttackEvents();

            }

        });

    }



    start() {

        // 获取动画组件

        const animation = this.animatorNode.getComponent(Animation);



        // 检查动画片段是否存在

        if (this.jumpClip) {

            animation.addClip(this.jumpClip);

        } else {

            console.error('Jump clip not loaded');

        }



        if (this.attackClip) {

            animation.addClip(this.attackClip);

        } else {

            console.error('Attack clip not loaded');

        }

    }



    // 绑定跳跃动画的事件

    private bindJumpEvents() {

        if (this.jumpClip) {

            const event = new AnimationEvent();

            event.frame = 0.5; // 事件发生在动画的0.5秒处

            event.type = 'event';

            event.tag = 'jump_peak';

            event.callback = this.onJumpPeak.bind(this);



            // 将事件添加到动画片段

            this.jumpClip.addEvent(event);

        }

    }



    // 绑定攻击动画的事件

    private bindAttackEvents() {

        if (this.attackClip) {

            const startEvent = new AnimationEvent();

            startEvent.frame = 0.0; // 事件发生在动画的0.0秒处

            startEvent.type = 'event';

            startEvent.tag = 'attack_start';

            startEvent.callback = this.onAttackStart.bind(this);



            const middleEvent = new AnimationEvent();

            middleEvent.frame = 0.5; // 事件发生在动画的0.5秒处

            middleEvent.type = 'event';

            middleEvent.tag = 'attack_middle';

            middleEvent.callback = this.onAttackMiddle.bind(this);



            const endEvent = new AnimationEvent();

            endEvent.frame = 1.0; // 事件发生在动画的1.0秒处

            endEvent.type = 'event';

            endEvent.tag = 'attack_end';

            endEvent.callback = this.onAttackEnd.bind(this);



            // 将事件添加到动画片段

            this.attackClip.addEvent(startEvent);

            this.attackClip.addEvent(middleEvent);

            this.attackClip.addEvent(endEvent);

        }

    }



    // 跳跃事件的回调函数

    private onJumpPeak(params: any) {

        console.log('Jump peak reached!', params);

        // 播放音效

        this.playSound(params.sound);

    }



    // 攻击开始事件的回调函数

    private onAttackStart(params: any) {

        console.log('Attack started!', params);

        // 播放攻击音效

        this.playSound(params.sound);

    }



    // 攻击中间事件的回调函数

    private onAttackMiddle(params: any) {

        console.log('Attack middle!', params);

        // 获取攻击目标

        const targetNode = this.node.getChildByName(params.target);

        if (targetNode) {

            // 触发攻击逻辑

            this.attackTarget(targetNode);

        }

    }



    // 攻击结束事件的回调函数

    private onAttackEnd(params: any) {

        console.log('Attack ended!', params);

        // 播放攻击结束音效

        this.playSound(params.endSound);

    }



    // 播放音效的函数

    private playSound(soundName: string) {

        // 假设有一个音效管理器

        const audioManager = this.node.getComponent(AudioManager);

        if (audioManager) {

            audioManager.playSound(soundName);

        }

    }



    // 触发攻击逻辑的函数

    private attackTarget(target: Node) {

        // 假设有一个攻击管理器

        const attackManager = this.node.getComponent(AttackManager);

        if (attackManager) {

            attackManager.attack(target);

        }

    }



    // 播放跳跃动画

    public playJump() {

        const animation = this.animatorNode.getComponent(Animation);

        if (this.jumpClip) {

            animation.play('jump_clip');

        } else {

            console.error('Jump clip not loaded');

        }

    }



    // 播放攻击动画

    public playAttack() {

        const animation = this.animatorNode.getComponent(Animation);

        if (this.attackClip) {

            animation.play('attack_clip');

        } else {

            console.error('Attack clip not loaded');

        }

    }

}

解释

  1. 加载动画片段并缓存

    • onLoad方法中,我们使用resources.load方法加载动画片段,并将其缓存到jumpClipattackClip属性中。
  2. 绑定事件

    • bindJumpEventsbindAttackEvents方法中,我们分别为跳跃动画和攻击动画添加事件,并绑定回调函数。
  3. 播放动画

    • playJumpplayAttack方法中,我们检查动画片段是否已经加载并缓存,然后播放相应的动画。

通过这种方式,我们避免了每次播放动画时重新创建和绑定事件,从而提高了性能。

总结

动画事件与回调是Cocos Creator中实现复杂动画逻辑和交互的重要工具。通过在动画编辑器中创建事件、通过代码绑定事件、传递参数给事件以及调试事件,我们可以灵活地控制动画与游戏逻辑的结合。在处理大量动画事件时,合理的性能优化策略可以显著提高游戏的运行效率。

希望本节内容能帮助你更好地理解和使用Cocos Creator中的动画事件与回调功能。如果你有任何疑问或需要进一步的帮助,请参考Cocos Creator的官方文档或社区资源。
在这里插入图片描述

;