闲鱼Flutter互动引擎系列——骨骼动画篇

avatar
@阿里巴巴集团
原文链接: mp.weixin.qq.com

骨骼动画是一种通过控制骨骼参数来实现多帧动画的方式,区别于GIF的不连贯和序列帧的体积大,骨骼动画有较好的灵活性和流畅性。目前骨骼动画已经被大规模地在游戏和动画中所使用,大有一种取代帧动画的趋势,Candy互动引擎对骨骼动画的支持自然是必不可少的一环。

从工具入手

动画是互动中很重要的一环,通过恰到好处的动画形式往往可以给用户更加新鲜的体验。由于动画制作是一项需要和UED高度合作的工作,面对UED无尽的参数变更,选择一个好用的工具就变得至关重要,毕竟谁也不希望自己在开开心心的敲代码的时候UED过来找你调动画参数。参考行业目前的工具链体系,并从中选择出一套适合我们的工具是比较好的选择。

先看曾被Flutter官方推荐过的骨骼动画制作工具Flare,其优势在于对于Flutter的支持较为完善。但是问题在于这套工具对于设计师来说比较陌生,而且.flr格式是一个全新的格式不够通用,所以最终我们放弃了这个方案。

为了配合集团现有的互动工具链,我们把目光放到了前端领域,白鹭引擎(Egret)是一个在前端领域小有名气的游戏引擎,其配套的工具体系包括DragonBone(骨骼动画)、Feather(粒子动画)等。这套工具在互动领域已经被使用了多年,对于设计师来说比较好上手。使用这套体系最大的问题是在Flutter上没有相应的实现,但是其产物的文档非常完善,所以我们最终选择在Flutter上解析并实现相应的Runtime。

基础知识

要进行骨骼动画的制作必然要先对骨骼动画本身要有一个基础了解,骨骼动画的详细介绍可以参考DragonBone官方教程,对于骨骼的几个关键概念我们还是必须先进行了解。

  • 骨架(Armature):骨架是骨骼的集合,骨架中至少包含一个骨骼。

  • 骨骼(Bone):骨骼是骨骼动画的基本组成部分,骨骼之间存在父子关系,父亲的变换会影响到孩子。一般通过骨骼的旋转、缩放、平移等变换即可形成动画。

  • 插槽(Slot):插槽是图片的容器,是骨骼和图片的桥梁。一根骨骼可以挂载多个插槽,可以视作骨骼是插槽的父节点,骨骼的变换会影响插槽。

  • 显示对象(DisplayData):显示对象通常为图片。一个插槽中可以有多个显示对象,但同时只会有一个被显示,通过修改当前显示的对象可以形成帧动画。

顾名思义”骨骼“就是骨骼动画的核心部件,正是因为这种模仿生物的骨骼的设计,使得设计师可以通过调整骨骼的参数,让角色做出丰富且自然的动作。我们要进行骨骼动画的渲染肯定不能脱离Candy游戏系统去完成,那么随之我们的第一个问题就诞生了,骨骼动画的核心部件“骨骼”在Candy中到底应该扮演一个什么样的角色呢?

骨架渲染

问题1:每一根骨骼在Candy中的角色是什么?

上一篇文章中也有提到Candy游戏系统是由四大元素构成的:

  • Game:游戏类,负责整个游戏的管理,Scene的加载管理以及各子系统管理与调度。

  • Scene:游戏场景类,负责游戏场景中各游戏对象的管理。

  • GameObject:游戏对象类,游戏世界中游戏对象的最小单位,游戏世界中的任何物体都是GameObject。

  • Component:游戏组件类,表示游戏对象的能力属性,比如SpriteComponent表示精灵组件,表示绘制精灵的能力。

由于骨骼是包含了父子关系的树形结构,而GameObject也是一个树形结构,我们很自然地会想到每一根骨骼就是一个GameObject每一个插槽就是对应的Component。因为在绘制时,后绘制的对象一定是覆盖在最上层的,所以以树状结构进行绘制最大的问题就是——父子间的绘制的顺序是一定的。如下图的绘制顺序是身体 -> 衣服 -> 披风,或者是衣服 -> 披风 -> 身体,无论是哪一种显然都是错误。

我们的解决方法是将这颗树进行拍平为列表,我们把每一个插槽(Slot)都作为了一个GameObject,并根据Zorder进行排序,那么我们最终会得到一个排好序的插槽列表,在渲染的时候根据插槽列表依次进行渲染即可。

这样的做法会带来一个新的问题,插槽的位置信息数据都是相对数据,在使用树状的结构进行渲染的时候并不是问题,但是现在拍平之后,渲染的位置该如何确定呢?

问题2:骨骼中的位置信息和最终渲染的位置信息如何对应?

因为骨骼中的参数都是相对值,这样做的好处在于在改变父骨骼位置时,子骨骼天然就会受到父骨骼的影响变换位置。所以其实这个问题就是如何把相对值变为绝对值,我们可以通过一些数学计算来完成这件事,具体的原理就不在此展开讲解。在Flutter中,通过自定义了一个Transform类并封装了相应的变换函数来即可实现坐标的转换,这样做的好处在于可以重载相应的运算符以便做动画的时候进行使用。

解决了上述两个问题,我们其实就已经知道了该如何渲染一个骨架。下面这张Candy实现骨骼动画的架构图,其中分为三个部分。

Parser层:考虑到骨骼动画的编辑器有很多,为了兼容市面上不同的编辑器,我们增加了一层解析层将不同编辑器生成的产物,转化为我们预定好的相对通用的骨骼结构数据。

Data层:Data层是一个相对通用的骨架数据,其内部包括了骨骼数据、插槽数据、展示对象数据、动画数据等,通过骨架数据我们可以知道最终应该渲染什么内容。由于我们第一个兼容的编辑器是Dragonbone,所以这些数据中属性的定义大多参照了Dragonbone中的定义,这里就不将每一个属性都展开来说了。

Render层:一个骨架就是一个独立的GameObject,骨架中的每一个插槽都会对应一个子GameObject。骨架中的骨骼起到的是辅助计算渲染坐标的作用,我们通过插槽所属的骨骼计算出渲染时要用的绝对坐标并填到相应的TransformComponent中。 最后,显示对象中的图片使用SpriteComponent进行渲染到正确的位置上。

动画实现

骨骼动画其实是由每一根骨骼的多个属性动画复合而成的,简单骨骼动画针对每一根骨骼及插槽其实可以拆分为以下几个动画:

  • 骨骼(插槽)的位移动画

  • 骨骼(插槽)的旋转动画

  • 骨骼(插槽)的缩放动画

  • 插槽的透明度动画

这些简单动画都可以归纳为补间动画,我们只需要在游戏每一次Update的时候将对应的属性值改变,自然就形成了动画的效果。那么每一个时刻的值应该是多少呢?这就需要一个插值器来告诉我们,Flutter的Animation对于插值器提供了很好的支持,回忆一下使用Animation的时候,是不是通过每一次触发刷新了之后从Animation中取出value值来赋值到相应的地方,同理使用在这也是一样的。

因为骨骼动画会有很多的关键帧,所以这里使用了Flutter中的一种特殊的Animation——TweenSequence。TweenSequence 可以传入一个 List<TweenSequenceItem<T>>items 每一个TweenSequenceItem都可以设置一个补间动画和相应的权重。在保证每一个骨骼的动画总帧数相同的情况下,可以直接使用每两个关键帧之间包含的帧数作为权重,相应的前一关键帧帧的值则为起始值,后一帧关键帧的作为终止值。举个例子:

                                                                    

    ///Transform2 为自己定义的一个数据结构,只要重载了相应的运算符,一样可以被Animation所使用

    TweenSequenceItem <Transform2 > _parseTransformAnimation(

    TransformFrame cur,

    TransformFrame next, {

    int duration,

    }) {

    if (cur != null && next != null && cur.duration != null ) {

    final Animatable< Transform2> tween = Tween< Transform2>(

    begin : cur.transForm,

    end : next.transForm,

    );

    if ((cur.duration != null && cur.duration > 0 ) ||

    (duration != null&& duration > 0)) {

    return TweenSequenceItem< Transform2>(

    tween: tween,

    weight:

    duration == null? cur.duration?.toDouble() : duration.toDouble(),

    );

    }

    }

    returnnull ;

    }

动画效果

这里以闲鱼币中的捞鱼小人为例子(可以通过 “闲鱼首页 -> 右上角签到图标” 进入闲鱼币池塘进行体验)。

性能表现

我们使用干净的Demo工程渲染多个上图中的小人进行测试,测试机型:iPhoneXs。

骨骼数量能一定程度上也能衡量动画的复杂度(还与变换次数相关),可以发现大于1000根骨骼时性能开始出现衰减,并随着骨骼数量的增加逐渐明显,在3000根骨骼以上时出现明显卡顿。这一性能已经完全可以满足App中内嵌中小型游戏的需求(其中内存增加问题会在后续的性能篇中进行阐述)。

现状和展望

目前Candy已经实现了对基础骨骼动画、粒子动画、属性动画的支持,并且已经在闲鱼币业务中落地使用,后续会应用在更多的场景之中。随着场景的增加,我们面临的挑战也就越来越多。

动画赋能app

一个App必定不会有很多的游戏内容,我们实现的动画如果仅在游戏场景下使用那其实落地的场景就会很有限,所以我们将Candy引擎中的动画部分封装为了Widget,使得Flutter App可以天然无缝地使用动画。

更多动画能力的支持

Lottie是一种深受设计师以及开发同学喜爱的方式,对于Lottie的支持我们也已经开始进行开发,等待完成后再与大家分享。

从动画升级为互动

互动是由一个个动画组合而成的,与传统的动画最大的差距在于互动需要有可交互性,在用户发生不同交互时要进行不同动画的切换,这也就意味着我们需要有编排各个动画之间的关系的能力。这是一套很完整的体系,需要有相应的逻辑编排工具以及端侧对于动态逻辑编排的实现,我们目前正在与集团中前端互动小组的同学合作复用前端现有的工具链,但是前端比起Flutter在动态逻辑方面有天生的优势,所以我们希望结合闲鱼团队的Fass以及Flutter-dx去构建这套体系。 在完成之后也会有相应的文章与大家分享我们的做法和心路历程,同时也欢迎大家和我们一起进行探讨,碰撞出更多的火花。

闲鱼团队是Flutter+Dart FaaS前后端一体化新技术的行业领军者,就是现在! 客户端/服务端java/架构/前端/质量工程师 面向校园+社会招聘,base杭州阿里巴巴西溪园区,一起做有创想空间的社区产品、做深度顶级的开源项目,一起拓展技术边界成就极致!

*投喂简历给小闲鱼→guicai.gxy@alibaba-inc.com

开源项目、峰会直击、关键洞察、深度解读 请认准 闲鱼技术