Laya 动画系列二 Animation 和IDE的帧动画、动效模板

一、AnimationPlayerBase extends Sprite

动画播放基类,提供了基础的动画播放控制方法和帧标签事件相关功能。可以继承此类,但不要直接实例化此类,因为有些方法需要由子类实现。

/**
 * 

开始播放动画。play(...)方法被设计为在创建实例后的任何时候都可以被调用, * 当相应的资源加载完毕、调用动画帧填充方法(set frames)或者将实例显示在舞台上时, * 会判断是否正在播放中,如果是,则进行播放。

*

配合wrapMode属性,可设置动画播放顺序类型。

* @param start (可选)指定动画播放开始的索引(int)或帧标签(String)。 * 帧标签可以通过addLabel(...)和removeLabel(...)进行添加和删除。 * @param loop (可选)是否循环播放。 * @param name (可选)动画名称。 * @param showWarn(可选)是否动画不存在时打印警告 */ public function play(start:* = 0, loop:Boolean = true, name:String = "",showWarn:Boolean=true):void { this._isPlaying = true; this.index = (start is String) ? _getFrameByLabel(start) : start; this.loop = loop; this._actionName = name; _isReverse = wrapMode == WRAP_REVERSE; if (this.interval > 0) { timerLoop(this.interval, this, _frameLoop, null, true, true); } }

可以看到interval控制了帧率,再看看_frameLoop方法,首先判断_isReverse和wrapMode,也就是播放模式,最后执行这一句this.index = this._index;

public function set index(value:int):void {
    _index = value;
    _displayToIndex(value);
    if (_labels && _labels[value]) {
        var tArr:Array = _labels[value];
        for (var i:int = 0, len:int = tArr.length; i < len; i++) {
            event(Event.LABEL, tArr[i]);
        }
    }
}

/**
 * @private
 * 显示到某帧
 * @param value 帧索引
 */
protected function _displayToIndex(value:int):void {
}

处理了Event.LABEL帧事件,并且把具体如何显示帧,留给了子类去实现_displayToIndex方法。至于帧事件,可以看一下这个方法:

/**
 * 增加一个帧标签到指定索引的帧上。当动画播放到此索引的帧时
 * 会派发Event.LABEL事件,派发事件是在完成当前帧画面更新之后。
 * @param   label   帧标签名称
 * @param   index   帧索引
 */
public function addLabel(label:String, index:int):void {
    if (!_labels) _labels = {};
    if (!_labels[index]) _labels[index] = [];
    _labels[index].push(label);
}

_labels是一个Object,它的key是一个数字,也就是帧索引(注意帧索引,是从0开始的)。它的value是一个数组,会把帧标签名称都存到这个数组中。从上面的set index方法中也能看到,进入某一帧时,会判断_labels相应的帧标签数组,派发里面所有名称的事件。

二、Animation extends AnimationPlayerBase

1.首先去看如何显示帧_displayToIndex

/**@private */
override protected function _displayToIndex(value:int):void {
    if (this._frames) this.graphics = this._frames[value];
}

官方对_frames的解释是,_frames是当前动画的帧图像数组。本类中,每个帧图像是一个Graphics对象,而动画播放就是定时切换Graphics对象的过程。可以看到_displayToIndex方法确实是在切换graphics对象。

2.动画缓存池framesMap
public static var framesMap:Object = {};
动画模版缓存池是以一定的内存开销来节省CPU开销,当相同的动画模版被多次使用时,相比于每次都创建新的动画模版,使用动画模版缓存池,只需创建一次,缓存之后多次复用,从而节省了动画模版创建的开销。
动画模版缓存池,以key-value键值对存储,key可以自定义,也可以从指定的配置文件中读取,value为对应的动画模版,是一个Graphics对象数组.使用loadImages(...)、loadAtlas(...)、loadAnimation(...)、set source方法可以创建动画模版。

3.有三个类似的方法:loadImages,loadAtlas,loadAnimation,以及它们三个的综合体set source

public function loadImages(urls:Array, cacheName:String = ""):Animation {
    this._url = "";
    if (!_setFramesFromCache(cacheName)) {
        this.frames = framesMap[cacheName] ? 
        framesMap[cacheName] : createFrames(urls, cacheName);
    }
    return this;
}

public function loadAtlas(url:String,
    loaded:Handler = null, cacheName:String = ""):Animation {
    this._url = "";
    var _this_:Animation = this;
    function onLoaded(loadUrl:String):void {
        if (url === loadUrl) {
            _this_.frames = framesMap[cacheName] ?
            framesMap[cacheName] : createFrames(url, cacheName);
            if (loaded) loaded.run();
        }
    }
    if (!_this_._setFramesFromCache(cacheName)) {
        if (Loader.getAtlas(url)) onLoaded(url);
        else Laya.loader.load(url, Handler.create(
        null, onLoaded, [url]), null, Loader.ATLAS);
    }
    return this;
}

public function loadAnimation(url:String,
    loaded:Handler = null, atlas:String = null):Animation {
    this._url = url;
    var _this_:Animation = this;
    if (!_actionName) _actionName = "";
    if (!_this_._setFramesFromCache("")) {
        if (!atlas || Loader.getAtlas(atlas)) {
            _loadAnimationData(url, loaded, atlas);
        } else {
            Laya.loader.load(atlas, Handler.create(this, _loadAnimationData,
            [url, loaded, atlas]), null, Loader.ATLAS)
        }
    } else {
        _this_._setFramesFromCache(_actionName, true);
        index = 0;
        if (loaded) loaded.run();
    }
    return this;
}

根据参数来看:
loadImages加载的是图片路径集合,比如[url1,url2,url3,...]。
loadAtlas加载的是图集路径,比如animation.loadAtlas("resource/ani/fighter.json");
loadAnimation加载的是由IDE创建的ani文件,比如ani.loadAnimation("tinyGame/coin0.ani");

这里注意_url属性,这个属性只有在loadAnimation这个方式时,记录传入的ani路径,其它两个方式,都是空字符串。它的用处是在_setFramesFromCache方法中,强行把传入的name参数变成_url + "#" + name;

/**@private */
protected function _setFramesFromCache(name:String, 
    showWarn:Boolean = false):Boolean {
    if (_url) name = _url + "#" + name;
    if (name && framesMap[name]) {
        var tAniO:*;
        tAniO = framesMap[name];
        if (tAniO is Array) {
            this._frames = framesMap[name];
            this._count = _frames.length;
        } else {
            if (tAniO.nodeRoot) {
                //如果动画数据未解析过,则先进行解析
                framesMap[name] = _parseGraphicAnimationByData(tAniO);
                tAniO = framesMap[name];
            }
            this._frames = tAniO.frames;
            this._count = _frames.length;
            //如果读取的是动画配置信息,帧率按照动画设置的帧率播放
            if (!_frameRateChanged) _interval = tAniO.interval;
            _labels = _copyLabels(tAniO.labels);
        }
        return true;
    } else {
        if (showWarn) trace("ani not found:", name);
    }
    return false;
}

也就是说,IDE创建的ani,会被缓存成key是"url#动画名称" 对应相应动画名称的动画模板。而另外两种方式,是否缓存,以及缓存key叫什么,是可以自己控制的,对应的参数是cacheName。cacheName默认为空,也就是不进行缓存。如果不为空,则表示使用此值做key进行动画模板缓存。

至于set source,算是对上面三个方法的一个整体封装吧,传入参数如:图集:"xx/a1.atlas";图片集合:"a1.png,a2.png,a3.png";LayaAir IDE动画"xx/a1.ani"

public function set source(value:String):void {
    if (value.indexOf(".ani") > -1) loadAnimation(value);
    else if (value.indexOf(".json") > -1 ||
    value.indexOf("als") > -1 ||
    value.indexOf("atlas") > -1) loadAtlas(value);
    else loadImages(value.split(","));
}

4.createFrames
这个方法在loadImages和loadAtlas都在使用,区别就是一个传入的是urls(图片路径集合),一个是url(图集路径)。

public static function createFrames(url:*, name:String):Array {
    var arr:Array,i:int,n:int,g:Graphics;
    if (url is String) {
        var atlas:Array = Loader.getAtlas(url);
        if (atlas && atlas.length) {
            arr = [];
            for (i = 0, n = atlas.length; i < n; i++) {
                g = new RunDriver.createGraphics();
                g.drawTexture(Loader.getRes(atlas[i]), 0, 0);
                arr.push(g);
            }
        }
    } else if (url is Array) {
        arr = [];
        for (i = 0, n = url.length; i < n; i++) {
            g = new RunDriver.createGraphics();
            g.loadImage(url[i], 0, 0);
            arr.push(g);
        }
    }
    if (name) framesMap[name] = arr;
    return arr;
}

从这里也能看出如何去创建一个Graphics对象。注意最后一句if (name) framesMap[name] = arr;,印证了上面所说,cacheName为空时,不会缓存。

由于这是个静态方法,也可以在不创建Animation实例前,先创建动画缓存,比如:

//创建动画模板dizziness
Laya.Animation.createFrames(this.aniUrls("die", 4), "die");
Laya.Animation.createFrames(this.aniUrls("fire", 7), "fire");
Laya.Animation.createFrames(this.aniUrls("atk", 3), "atk");
Laya.Animation.createFrames(this.aniUrls("move", 4), "move");
        
/**
 * 创建一组动画的url数组(美术资源地址数组)
 * aniName  动作的名称,用于生成url
 * length   动画最后一帧的索引值,
 */
private aniUrls(aniName: string, length: number): any {
    var urls: any = [];
    for (var i: number = 1; i < length; i++) {
        //动画资源路径要和动画图集打包前的资源命名对应起来
        urls.push("imgs/role/wp116/" + aniName + i + ".png");
    }
    return urls;
}

5.clearCache
根据上面所说,IDE创建的ani,会以"url#aniName"作为key来缓存,清理时也要注意这一点。

public static function clearCache(key:String):void {
    var cache:Object = framesMap;
    var val:String;
    var key2:String = key + "#";
    for (val in cache) {
        if (val === key || val.indexOf(key2) == 0) {
            delete framesMap[val];
        }
    }
}

6.重写的play方法

override public function play(start:* = 0,
    loop:Boolean = true, name:String = "",showWarn:Boolean=true):void {
    if (name) _setFramesFromCache(name, showWarn);
    this._isPlaying = true;
    this.index = (start is String) ? _getFrameByLabel(start) : start;
    this.loop = loop;
    this._actionName = name;
    _isReverse = wrapMode == WRAP_REVERSE;
    if (this._frames && this.interval > 0) {
        timerLoop(this.interval, this, _frameLoop, null, true, true);
    }
}

start参数支持帧标签来控制播放。
loop参数默认是true,即循环播放
关于name参数,注释是这样写的:动画模板在动画模版缓存池中的key,也可认为是动画名称。如果name为空,则播放当前动画序列帧;如果不为空,则在动画模版缓存池中寻找key值为name的动画模版,如果存在则用此动画模版初始化当前序列帧并播放,如果不存在,则仍然播放当前动画序列帧;如果没有当前动画的帧数据,则不播放,但该实例仍然处于播放状态。

//创建一个Animation实例
var tl:Animation = new Animation();
//加载动画文件
tl.loadAnimation("TimeLine.ani");
//添加到舞台
Laya.stage.addChild(tl);
//播放Animation动画
tl.play();

//创建一个新的Animation实例
var tl2:Animation = new Animation();
//加载动画文件
tl2.loadAnimation("TimeLine.ani");
//添加到舞台
Laya.stage.addChild(tl2);
//播放Animation动画的pivot动画
tl2.play(0, true, "pivot");
//动画的显示位置
tl2.pos(300,0);

上面的代码,就是IDE制作的ani中,有两个动画,可以通过name参数去选择播放,缺省播放第一个动画。


image.png

7.在loadAnimation方式中,牵涉到_loadAnimationData方法,进而使用了_parseGraphicAnimation方法。这就牵涉到GraphicAnimation类了。

三、帧动画的数据解析

下面这三个类,针对IDE制作的动画及动效模板进行解析。比较复杂,暂时不做阅读。

1.FrameAnimation extends AnimationPlayerBase
关键帧动画播放类

2.EffectAnimation extends FrameAnimation
动效模板。用于为指定目标对象添加动画效果。每个动效有唯一的目标对象,而同一个对象可以添加多个动效。 当一个动效开始播放时,其他动效会自动停止播放

3.GraphicAnimation extend FrameAnimation
就是IDE创建的时间轴动画


image.png
四、IDE动画编辑器使用

参考时间轴动画编辑器详解,介绍很详细,以下记录自己的使用经验
1.选中左侧属性,可以设置相应的缓动函数类型和标签

image.png

image.png

image.png

当设置标签后,设置标签的帧会出现红色圆点。可以在项目中,通过标签名用代码对该帧进行操作。

2.多个节点,也就是分层动画,比如上图中的GraphicNode2,Graphic3
注意官方示例中所说,拖拽一个新的组件到场景。即会自动新增一个节点层,刚开始我很困惑怎么增加新节点层。
另外,注意,时间轴动画的负坐标区域内,无法触发点击事件,如果需要用到点击事件交互,则动画的X与Y必须位于正坐标区域,也就是十字红线交叉的右下区域。

3.设置关键帧坐标时TIPS
如果要改坐标,先改后面的。如果从前往后改,后面的坐标动了,前一个会自动变成中间值 。

4.我想做一个光斑绕扇形移动的动画,如下图,圆弧段可以逐帧移动,而两个直线区域则可以使用补间动画,补间动画起始点和结束点,相隔的帧数,可以大概估算一下,保证每帧的移动速度和圆弧段每帧移动速度差不多就行。


image.png

这里有个方便的技巧就是,先建一个测试动画,把光斑都放到同一帧,否则无法直观看到全部的运动轨迹。当然,把背景图片也放进去,位置更精确。然后再建正式动画,每一帧的位置就可以参考测试动画里的坐标了。

5.Animation中是可以使用遮罩的


image.png

如图,这个动画显示的不是光斑,而是光条。


image.png

image.png

方法就是,在Animation里拖一个Sprite,设置renderType为mask。然后在里面用Lines画出轨迹,同时设置线宽。当然越宽,光条露出来的就越粗了。
image.png
五、动效模板

参考创建动效模板(EffectAnimation)

image.png

EffectAnimation extends FrameAnimation
动效模板。用于为指定目标对象添加动画效果。每个动效有唯一的目标对象,而同一个对象可以添加多个动效。 当一个动效开始播放时,其他动效会自动停止播放

动效模板是基于时间轴的动画效果,通过预设动画效果,然后把效果附加给某个组件。使得组件无需编码,却轻松实现与编码相同的动画效果。动效模板不能独立显示,仅可作为动效模板让UI页面中的组件获得动画效果。

1.按照上述示例,发现绑定一个组件后,我的组件坐标会改变为动效模板的坐标
参考关于动效模板使用后改变了原来组件的坐标问题

删除x,y

2.希望某个动效完成后,做一些事情
参考监听动效动画的COMPLETE(播放完成)事件

在var里取名,即可进行代码侦听

事件目前只有这几个,暂时不支持自定义事件。

3.参考动画模板中,要求到拖入一张图,然后制作一个动画,这张图是随便都可以的吗
资源目录中png和jpg格式的都可以

你可能感兴趣的:(Laya 动画系列二 Animation 和IDE的帧动画、动效模板)