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

一、AnimationPlayerBase extends Sprite

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

/**
 * <p>开始播放动画。play(...)方法被设计为在创建实例后的任何时候都可以被调用,
 * 当相应的资源加载完毕、调用动画帧填充方法(set frames)或者将实例显示在舞台上时,
 * 会判断是否正在播放中,如果是,则进行播放。</p>
 * <p>配合wrapMode属性,可设置动画播放顺序类型。</p>
 * @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格式的都可以

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容