DragonBones骨骼动画事件系统详解

db.jpg 在几个版本之前的DragonBones一直使用自己的事件系统,而在Egret版本中,我则无法忍受this问题所带来的各种麻烦。鉴于不完善的东西就自己动手完善起来,我则将DragonBones TypeScript版本的事件系统迁移到了Egret中。至此,你可以像使用Egret事件机制一样来使用DragonBones的事件。我向DragonBones提交了合并申请,在几个版本之前,DragonBones接受了这个合并请求。现在,是时候向大家正式讲解一下DragonBones所有事件机制的使用方法了(其实,我应该更早一点的将这部分技术文档公布,而不是被Lancelot催促撰写这部分内容)。 首先你要清楚的了解,在Egret中,我们的事件是如何派发的,以及它的响应方式。在即将推出的Egret 2.5的版本中会稍稍有一些变化,但可惜,这些变化不在我们此次的讨论范围当中。我们依然基于Egret 2.0.5来阐述此问题。 当你的程序修改了某种状态时,我们希望能够得到系统的通知,并告知我们,那些状态被修改了,同时得到一些相关信息。在Egret 中,负责消息派发的根类被命名为 egret.EventDispatcher 。每一次传递的消息,我们也可以称之为系统给我们派发了一个 Message,而这个消息的内容,我们将其封装为 egret.Event。这个机制很美好,因为当没有任何东西被改变的时候,我们也不希望做一些无用的操作。

上面的机制在Egret中被完整的实现了,而在DragonBones中,这个机制依然可以正常运行,但稍稍有些不同。 我建议你一遍阅读这边文章,一遍翻看DragonBones的源码,在DragonBones中,存在这一个名为 dragonBones.Event 的类,如果你查阅它的继承关系,你会发现,它直接继承自 egret.Event,同时没有做任何扩展操作。我们可以直接让 dragonBones.Event 从这个世界上消失,但这样会导致很多游戏中的骨骼动画无法正常运行,为了兼容,我们妥协了。 DragonBones中也有能够派发事件的类,dragonBones.EventDispatcher这个类直接继承自 egret.EventDispatcher。所以,你现在能够明白,为什么DragonBones能够拥有和Egret统一的事件机制。他们的关系如下图: 屏幕快照 2015-09-22 下午8.17.07.png OK,现在我们可以开心的使用DraonBones的事件系统了,接下来要搞明白的是,DragonBones到底会派发什么样的事件给我们,我们能得到哪些变化的通知。 在DragonBones源码目录中,你可以查看到一个名为 events 的文件夹,如图: 屏幕快照 2015-09-22 下午8.31.55.png 我们所有能用的事件都存放在这个文件夹中。 通过命名,你可以看出这些事件对应的是哪些类型的事件,例如 AnimationEvent.ts ,定义了骨骼动画中相关状态的事件类型。ArmatureEvent.ts 中定义了骨架相关事件类型。你可以通过Egret API手册来获取这些事件的细节信息。

DragonBones中系统事件的使用方法

使用DragonBones Pro默认的项目,导出一个动画数据。 屏幕快照 2015-09-22 下午9.18.13.png 创建空白项目,并编写代码: class Main extends egret.DisplayObjectContainer { public constructor() { super(); this.addEventListener(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this); } private onAddToStage(event:egret.Event) { //初始化Resource资源加载库 //initiate Resource loading library RES.addEventListener(RES.ResourceEvent.CONFIG_COMPLETE, this.onConfigComplete, this); RES.loadConfig("resource/resource.json", "resource/"); } /** * 配置文件加载完成,开始预加载preload资源组。 * configuration file loading is completed, start to pre-load the preload resource group */ private onConfigComplete(event:RES.ResourceEvent):void { RES.removeEventListener(RES.ResourceEvent.CONFIG_COMPLETE, this.onConfigComplete, this); RES.addEventListener(RES.ResourceEvent.GROUP_COMPLETE, this.onResourceLoadComplete, this); RES.addEventListener(RES.ResourceEvent.GROUP_LOAD_ERROR, this.onResourceLoadError, this); RES.addEventListener(RES.ResourceEvent.GROUP_PROGRESS, this.onResourceProgress, this); RES.loadGroup("preload"); } /** * preload资源组加载完成 * Preload resource group is loaded */ private onResourceLoadComplete(event:RES.ResourceEvent):void { if (event.groupName == "preload") { RES.removeEventListener(RES.ResourceEvent.GROUP_COMPLETE, this.onResourceLoadComplete, this); RES.removeEventListener(RES.ResourceEvent.GROUP_LOAD_ERROR, this.onResourceLoadError, this); RES.removeEventListener(RES.ResourceEvent.GROUP_PROGRESS, this.onResourceProgress, this); this.createGameScene(); } } /** * 资源组加载出错 * The resource group loading failed */ private onResourceLoadError(event:RES.ResourceEvent):void { //TODO console.warn("Group:" + event.groupName + " has failed to load"); //忽略加载失败的项目 //Ignore the loading failed projects this.onResourceLoadComplete(event); } /** * preload资源组加载进度 * Loading process of preload resource group */ private onResourceProgress(event:RES.ResourceEvent):void { } private textfield:egret.TextField; /** * 创建游戏场景 * Create a game scene */ private createGameScene():void { var dragonbonesData = RES.getRes( "Dragon_json" ); var textureData = RES.getRes( "texture_json" ); var texture = RES.getRes( "texture_png" ); var dragonbonesFactory:dragonBones.EgretFactory = new dragonBones.EgretFactory(); dragonbonesFactory.addDragonBonesData(dragonBones.DataParser.parseDragonBonesData(dragonbonesData)); dragonbonesFactory.addTextureAtlas(new dragonBones.EgretTextureAtlas(texture,textureData)); var armature: dragonBones.Armature = dragonbonesFactory.buildArmature("Dragon"); this.addChild(armature.display); armature.display.x = 200; armature.display.y = 300; armature.display.scaleX = 0.5; armature.display.scaleY = 0.5; dragonBones.WorldClock.clock.add( armature ); egret.Ticker.getInstance().register( function(frameTime:number){dragonBones.WorldClock.clock.advanceTime(0.02)}, this ); armature.addEventListener( dragonBones.AnimationEvent.START, this.startPlay,this); armature.addEventListener( dragonBones.AnimationEvent.LOOP_COMPLETE, this.loop_com,this); armature.animation.gotoAndPlay("walk"); } private startPlay(evt:dragonBones.ArmatureEvent) { console.log( "armature 开始播放动画!"); } private loop_com(evt:dragonBones.ArmatureEvent) { console.log( "armature 动画播放完一轮完成!"); } } 编译并运行,你应该在控制台看到如下输出: 屏幕快照 2015-09-22 下午9.56.42.png 需要特殊说明的是,在 dragonBones.AnimationEvent 这个事件类型中包含 COMPLETELOOP_COMPLETE 两种完成动画播放事件类型,两种事件类型使用最为常用,但稍有区别。 - COMPLETE:当前动作播放到最后一帧,播放完成后则停止动画,或者跳转到其他动画时,派发此事件。 - LOOP_COMPLETE:当前动画进行循环播放,当播放到最后一帧时,派发此事件。 在DragonBones中,你能看到的事件,除了dragonBones.SoundEvent以外,其他事件都是由 dragonBones.Armature 对象派发的。

高大上的用户自定义事件

这个需求来源于我们对动画使用的场景,当一个人物动画播放“射击”动画时,我们希望在某一个关键帧能够派发一个自定义事件,来通知游戏业务逻辑执行射击操作。而在DragonBones中我们已经为你准备好了这一切。 在下面的截图中,我们寻找到时间轴最后一层的事件层,然后在第10帧添加关键帧,打开属性面板,在”事件“一栏的”值中设置自定义字符串,其值为ttt(请允许我的命名不严谨,因为者仅仅是个演示)。 屏幕快照 2015-09-22 下午10.15.29.png 重新导出数据,并修改代码如下: class Main extends egret.DisplayObjectContainer { public constructor() { super(); this.addEventListener(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this); } private onAddToStage(event:egret.Event) { //初始化Resource资源加载库 //initiate Resource loading library RES.addEventListener(RES.ResourceEvent.CONFIG_COMPLETE, this.onConfigComplete, this); RES.loadConfig("resource/resource.json", "resource/"); } /** * 配置文件加载完成,开始预加载preload资源组。 * configuration file loading is completed, start to pre-load the preload resource group */ private onConfigComplete(event:RES.ResourceEvent):void { RES.removeEventListener(RES.ResourceEvent.CONFIG_COMPLETE, this.onConfigComplete, this); RES.addEventListener(RES.ResourceEvent.GROUP_COMPLETE, this.onResourceLoadComplete, this); RES.addEventListener(RES.ResourceEvent.GROUP_LOAD_ERROR, this.onResourceLoadError, this); RES.addEventListener(RES.ResourceEvent.GROUP_PROGRESS, this.onResourceProgress, this); RES.loadGroup("preload"); } /** * preload资源组加载完成 * Preload resource group is loaded */ private onResourceLoadComplete(event:RES.ResourceEvent):void { if (event.groupName == "preload") { RES.removeEventListener(RES.ResourceEvent.GROUP_COMPLETE, this.onResourceLoadComplete, this); RES.removeEventListener(RES.ResourceEvent.GROUP_LOAD_ERROR, this.onResourceLoadError, this); RES.removeEventListener(RES.ResourceEvent.GROUP_PROGRESS, this.onResourceProgress, this); this.createGameScene(); } } /** * 资源组加载出错 * The resource group loading failed */ private onResourceLoadError(event:RES.ResourceEvent):void { //TODO console.warn("Group:" + event.groupName + " has failed to load"); //忽略加载失败的项目 //Ignore the loading failed projects this.onResourceLoadComplete(event); } /** * preload资源组加载进度 * Loading process of preload resource group */ private onResourceProgress(event:RES.ResourceEvent):void { } private textfield:egret.TextField; /** * 创建游戏场景 * Create a game scene */ private createGameScene():void { var dragonbonesData = RES.getRes( "Dragon_json" ); var textureData = RES.getRes( "texture_json" ); var texture = RES.getRes( "texture_png" ); var dragonbonesFactory:dragonBones.EgretFactory = new dragonBones.EgretFactory(); dragonbonesFactory.addDragonBonesData(dragonBones.DataParser.parseDragonBonesData(dragonbonesData)); dragonbonesFactory.addTextureAtlas(new dragonBones.EgretTextureAtlas(texture,textureData)); var armature: dragonBones.Armature = dragonbonesFactory.buildArmature("Dragon"); this.addChild(armature.display); armature.display.x = 200; armature.display.y = 300; armature.display.scaleX = 0.5; armature.display.scaleY = 0.5; dragonBones.WorldClock.clock.add( armature ); egret.Ticker.getInstance().register( function(frameTime:number){dragonBones.WorldClock.clock.advanceTime(0.02)}, this ); armature.addEventListener( dragonBones.AnimationEvent.START, this.startPlay,this); armature.addEventListener( dragonBones.AnimationEvent.LOOP_COMPLETE, this.loop_com,this); armature.addEventListener( dragonBones.FrameEvent.ANIMATION_FRAME_EVENT, this.frame_event,this); armature.animation.gotoAndPlay("walk"); } private startPlay(evt:dragonBones.ArmatureEvent) { console.log( "armature 开始播放动画!"); } private loop_com(evt:dragonBones.ArmatureEvent) { console.log( "armature 动画播放完一轮完成!"); } private frame_event(evt:dragonBones.FrameEvent) { console.log( "armature 播放到了一个关键帧! 帧标签为:",evt.frameLabel); } } 编译并运行,截图如下: 屏幕快照 2015-09-22 下午10.18.05.png 我们可以看到,当动画播放到第10帧时,会派发 dragonBones.FrameEvent.ANIMATION_FRAME_EVENT事件,在事件的响应函数中,可以得到frameLabel属性,该属性就是我们在 DragonBones Pro中填写的“值”。 好了,你可能注意到,事件关键帧的属性面板中不仅仅包含“事件”,还包含“跳转”和“声音”。如果你使用了跳转,那么值的内容应该为另外一个动画的名称。如果你使用了“声音”,那么我们要好好聊一聊了。 现在我们在第4帧添加一个新的事件关键帧,并在“声音”中设置值为abc,如图: 屏幕快照 2015-09-22 下午10.21.55.png 导出新数据,并修改代码如下: class Main extends egret.DisplayObjectContainer { public constructor() { super(); this.addEventListener(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this); } private onAddToStage(event:egret.Event) { //初始化Resource资源加载库 //initiate Resource loading library RES.addEventListener(RES.ResourceEvent.CONFIG_COMPLETE, this.onConfigComplete, this); RES.loadConfig("resource/resource.json", "resource/"); } /** * 配置文件加载完成,开始预加载preload资源组。 * configuration file loading is completed, start to pre-load the preload resource group */ private onConfigComplete(event:RES.ResourceEvent):void { RES.removeEventListener(RES.ResourceEvent.CONFIG_COMPLETE, this.onConfigComplete, this); RES.addEventListener(RES.ResourceEvent.GROUP_COMPLETE, this.onResourceLoadComplete, this); RES.addEventListener(RES.ResourceEvent.GROUP_LOAD_ERROR, this.onResourceLoadError, this); RES.addEventListener(RES.ResourceEvent.GROUP_PROGRESS, this.onResourceProgress, this); RES.loadGroup("preload"); } /** * preload资源组加载完成 * Preload resource group is loaded */ private onResourceLoadComplete(event:RES.ResourceEvent):void { if (event.groupName == "preload") { RES.removeEventListener(RES.ResourceEvent.GROUP_COMPLETE, this.onResourceLoadComplete, this); RES.removeEventListener(RES.ResourceEvent.GROUP_LOAD_ERROR, this.onResourceLoadError, this); RES.removeEventListener(RES.ResourceEvent.GROUP_PROGRESS, this.onResourceProgress, this); this.createGameScene(); } } /** * 资源组加载出错 * The resource group loading failed */ private onResourceLoadError(event:RES.ResourceEvent):void { //TODO console.warn("Group:" + event.groupName + " has failed to load"); //忽略加载失败的项目 //Ignore the loading failed projects this.onResourceLoadComplete(event); } /** * preload资源组加载进度 * Loading process of preload resource group */ private onResourceProgress(event:RES.ResourceEvent):void { } private textfield:egret.TextField; /** * 创建游戏场景 * Create a game scene */ private createGameScene():void { var dragonbonesData = RES.getRes( "Dragon_json" ); var textureData = RES.getRes( "texture_json" ); var texture = RES.getRes( "texture_png" ); var dragonbonesFactory:dragonBones.EgretFactory = new dragonBones.EgretFactory(); dragonbonesFactory.addDragonBonesData(dragonBones.DataParser.parseDragonBonesData(dragonbonesData)); dragonbonesFactory.addTextureAtlas(new dragonBones.EgretTextureAtlas(texture,textureData)); var armature: dragonBones.Armature = dragonbonesFactory.buildArmature("Dragon"); this.addChild(armature.display); armature.display.x = 200; armature.display.y = 300; armature.display.scaleX = 0.5; armature.display.scaleY = 0.5; dragonBones.WorldClock.clock.add( armature ); egret.Ticker.getInstance().register( function(frameTime:number){dragonBones.WorldClock.clock.advanceTime(0.02)}, this ); armature.addEventListener( dragonBones.AnimationEvent.START, this.startPlay,this); armature.addEventListener( dragonBones.AnimationEvent.LOOP_COMPLETE, this.loop_com,this); armature.addEventListener( dragonBones.FrameEvent.ANIMATION_FRAME_EVENT, this.frame_event,this); dragonBones.SoundEventManager.getInstance().addEventListener( dragonBones.SoundEvent.SOUND, this.sound_event,this); armature.animation.gotoAndPlay("walk"); } private startPlay(evt:dragonBones.ArmatureEvent) { console.log( "armature 开始播放动画!"); } private loop_com(evt:dragonBones.ArmatureEvent) { console.log( "armature 动画播放完一轮完成!"); } private frame_event(evt:dragonBones.FrameEvent) { console.log( "armature 播放到了一个关键帧! 帧标签为:",evt.frameLabel); } private sound_event(evt:dragonBones.SoundEvent) { console.log( "armature 要播放声音啦!声音的值为:",evt.sound); } } 编译并运行,你应该看到如下图效果: 屏幕快照 2015-09-22 下午10.26.21.png 值得注意的一点(其实前面也说过了),dragonBones.SoundEvent.SOUND事件由 dragonBones.SoundEventManager.getInstance() 这个单例对象来派发,并非 dragonBones.Armature 对象。你需要特别注意这一点!!!! 我想你在读完这篇文章后,能够明白DragonBones为你准备的系统事件和自定义事件是如何使用的,现在开心的做你的游戏吧!!