骨骼动画实现秘密!闲鱼 Flutter 互动引擎告诉你
簡(jiǎn)介: 代表骨骼動(dòng)畫(huà)是一種通過(guò)控制骨骼參數(shù)來(lái)實(shí)現(xiàn)多幀動(dòng)畫(huà)的方式,區(qū)別于 GIF 的不連貫和序列幀的體積大,骨骼動(dòng)畫(huà)有較好的靈活性和流暢性。目前骨骼動(dòng)畫(huà)已經(jīng)被大規(guī)模地在游戲和動(dòng)畫(huà)中所使用,大有一種取代幀動(dòng)畫(huà)的趨勢(shì),Candy 互動(dòng)引擎對(duì)骨骼動(dòng)畫(huà)的支持自然是必不可少的一環(huán)。
作者|馬驍(塵蕭)
出品|阿里巴巴新零售淘系技術(shù)部
前言
代表骨骼動(dòng)畫(huà)是一種通過(guò)控制骨骼參數(shù)來(lái)實(shí)現(xiàn)多幀動(dòng)畫(huà)的方式,區(qū)別于 GIF 的不連貫和序列幀的體積大,骨骼動(dòng)畫(huà)有較好的靈活性和流暢性。目前骨骼動(dòng)畫(huà)已經(jīng)被大規(guī)模地在游戲和動(dòng)畫(huà)中所使用,大有一種取代幀動(dòng)畫(huà)的趨勢(shì),Candy 互動(dòng)引擎對(duì)骨骼動(dòng)畫(huà)的支持自然是必不可少的一環(huán)。
從工具入手
動(dòng)畫(huà)是互動(dòng)中很重要的一環(huán),通過(guò)恰到好處的動(dòng)畫(huà)形式往往可以給用戶更加新鮮的體驗(yàn)。由于動(dòng)畫(huà)制作是一項(xiàng)需要和 UED 高度合作的工作,面對(duì) UED 無(wú)盡的參數(shù)變更,選擇一個(gè)好用的工具就變得至關(guān)重要,畢竟誰(shuí)也不希望自己在開(kāi)開(kāi)心心的敲代碼的時(shí)候 UED 過(guò)來(lái)找你調(diào)動(dòng)畫(huà)參數(shù)。參考行業(yè)目前的工具鏈體系,并從中選擇出一套適合我們的工具是比較好的選擇。
先看曾被 Flutter 官方推薦過(guò)的骨骼動(dòng)畫(huà)制作工具 Flare,其優(yōu)勢(shì)在于對(duì)于 Flutter 的支持較為完善。但是問(wèn)題在于這套工具對(duì)于設(shè)計(jì)師來(lái)說(shuō)比較陌生,而且.flr格式是一個(gè)全新的格式不夠通用,所以最終我們放棄了這個(gè)方案。
為了配合集團(tuán)現(xiàn)有的互動(dòng)工具鏈,我們把目光放到了前端領(lǐng)域,白鷺引擎(Egret)是一個(gè)在前端領(lǐng)域小有名氣的游戲引擎,其配套的工具體系包括DragonBone(骨骼動(dòng)畫(huà))、Feather(粒子動(dòng)畫(huà))等。
這套工具在互動(dòng)領(lǐng)域已經(jīng)被使用了多年,對(duì)于設(shè)計(jì)師來(lái)說(shuō)比較好上手。使用這套體系最大的問(wèn)題是在 Flutter 上沒(méi)有相應(yīng)的實(shí)現(xiàn),但是其產(chǎn)物的文檔非常完善,所以我們最終選擇在 Flutter 上解析并實(shí)現(xiàn)相應(yīng)的 Runtime。
基礎(chǔ)知識(shí)
要進(jìn)行骨骼動(dòng)畫(huà)的制作必然要先對(duì)骨骼動(dòng)畫(huà)本身要有一個(gè)基礎(chǔ)了解,骨骼動(dòng)畫(huà)的詳細(xì)介紹可以參考 :
- DragonBone 官方教程,對(duì)于骨骼的幾個(gè)關(guān)鍵概念我們還是必須先進(jìn)行了解。
- 骨架(Armature):骨架是骨骼的集合,骨架中至少包含一個(gè)骨骼。
- 骨骼(Bone):骨骼是骨骼動(dòng)畫(huà)的基本組成部分,骨骼之間存在父子關(guān)系,父親的變換會(huì)影響到孩子。一般通過(guò)骨骼的旋轉(zhuǎn)、縮放、平移等變換即可形成動(dòng)畫(huà)。
- 插槽(Slot):插槽是圖片的容器,是骨骼和圖片的橋梁。一根骨骼可以掛載多個(gè)插槽,可以視作骨骼是插槽的父節(jié)點(diǎn),骨骼的變換會(huì)影響插槽。
- 顯示對(duì)象(DisplayData):顯示對(duì)象通常為圖片。一個(gè)插槽中可以有多個(gè)顯示對(duì)象,但同時(shí)只會(huì)有一個(gè)被顯示,通過(guò)修改當(dāng)前顯示的對(duì)象可以形成幀動(dòng)畫(huà)。
- 顧名思義”骨骼“就是骨骼動(dòng)畫(huà)的核心部件,正是因?yàn)檫@種模仿生物的骨骼的設(shè)計(jì),使得設(shè)計(jì)師可以通過(guò)調(diào)整骨骼的參數(shù),讓角色做出豐富且自然的動(dòng)作。
我們要進(jìn)行骨骼動(dòng)畫(huà)的渲染肯定不能脫離 Candy 游戲系統(tǒng)去完成,那么隨之我們的第一個(gè)問(wèn)題就誕生了,骨骼動(dòng)畫(huà)的核心部件“骨骼”在 Candy 中到底應(yīng)該扮演一個(gè)什么樣的角色呢?
骨架渲染
問(wèn)題1:每一根骨骼在 Candy 中的角色是什么?
上一篇文章中也有提到 Candy 游戲系統(tǒng)是由四大元素構(gòu)成的:
- Game:游戲類,負(fù)責(zé)整個(gè)游戲的管理,Scene的加載管理以及各子系統(tǒng)管理與調(diào)度。
- Scene:游戲場(chǎng)景類,負(fù)責(zé)游戲場(chǎng)景中各游戲?qū)ο蟮墓芾怼?/li>
- GameObject:游戲?qū)ο箢?#xff0c;游戲世界中游戲?qū)ο蟮淖钚挝?#xff0c;游戲世界中的任何物體都是GameObject。
- Component:游戲組件類,表示游戲?qū)ο蟮哪芰傩?#xff0c;比如SpriteComponent表示精靈組件,表示繪制精靈的能力。
- 由于骨骼是包含了父子關(guān)系的樹(shù)形結(jié)構(gòu),而 GameObject 也是一個(gè)樹(shù)形結(jié)構(gòu),我們很自然地會(huì)想到每一根骨骼就是一個(gè) GameObject 每一個(gè)插槽就是對(duì)應(yīng)的 Component 。
因?yàn)樵诶L制時(shí),后繪制的對(duì)象一定是覆蓋在最上層的,所以以樹(shù)狀結(jié)構(gòu)進(jìn)行繪制最大的問(wèn)題就是——父子間的繪制的順序是一定的。如下圖的繪制順序是身體 -> 衣服 -> 披風(fēng),或者是衣服 -> 披風(fēng) -> 身體,無(wú)論是哪一種顯然都是錯(cuò)誤。
我們的解決方法是將這顆樹(shù)進(jìn)行拍平為列表,我們把每一個(gè)插槽(Slot)都作為了一個(gè) GameObject ,并根據(jù) Zorder 進(jìn)行排序,那么我們最終會(huì)得到一個(gè)排好序的插槽列表,在渲染的時(shí)候根據(jù)插槽列表依次進(jìn)行渲染即可。
這樣的做法會(huì)帶來(lái)一個(gè)新的問(wèn)題,插槽的位置信息數(shù)據(jù)都是相對(duì)數(shù)據(jù),在使用樹(shù)狀的結(jié)構(gòu)進(jìn)行渲染的時(shí)候并不是問(wèn)題,但是現(xiàn)在拍平之后,渲染的位置該如何確定呢?
問(wèn)題2:骨骼中的位置信息和最終渲染的位置信息如何對(duì)應(yīng)?
因?yàn)楣趋乐械膮?shù)都是相對(duì)值,這樣做的好處在于在改變父骨骼位置時(shí),子骨骼天然就會(huì)受到父骨骼的影響變換位置。
所以其實(shí)這個(gè)問(wèn)題就是如何把相對(duì)值變?yōu)榻^對(duì)值,我們可以通過(guò)一些數(shù)學(xué)計(jì)算來(lái)完成這件事,具體的原理就不在此展開(kāi)講解。
在 Flutter 中,通過(guò)自定義了一個(gè) Transform 類并封裝了相應(yīng)的變換函數(shù)來(lái)即可實(shí)現(xiàn)坐標(biāo)的轉(zhuǎn)換,這樣做的好處在于可以重載相應(yīng)的運(yùn)算符以便做動(dòng)畫(huà)的時(shí)候進(jìn)行使用。
解決了上述兩個(gè)問(wèn)題,我們其實(shí)就已經(jīng)知道了該如何渲染一個(gè)骨架。下面這張 Candy 實(shí)現(xiàn)骨骼動(dòng)畫(huà)的架構(gòu)圖,其中分為三個(gè)部分。
- Parser層:考慮到骨骼動(dòng)畫(huà)的編輯器有很多,為了兼容市面上不同的編輯器,我們?cè)黾恿艘粚咏馕鰧訉⒉煌庉嬈魃傻漠a(chǎn)物,轉(zhuǎn)化為我們預(yù)定好的相對(duì)通用的骨骼結(jié)構(gòu)數(shù)據(jù)。
- Data層:Data層是一個(gè)相對(duì)通用的骨架數(shù)據(jù),其內(nèi)部包括了骨骼數(shù)據(jù)、插槽數(shù)據(jù)、展示對(duì)象數(shù)據(jù)、動(dòng)畫(huà)數(shù)據(jù)等,通過(guò)骨架數(shù)據(jù)我們可以知道最終應(yīng)該渲染什么內(nèi)容。由于我們第一個(gè)兼容的編輯器是Dragonbone,所以這些數(shù)據(jù)中屬性的定義大多參照了Dragonbone中的定義,這里就不將每一個(gè)屬性都展開(kāi)來(lái)說(shuō)了。
- Render層:一個(gè)骨架就是一個(gè)獨(dú)立的GameObject,骨架中的每一個(gè)插槽都會(huì)對(duì)應(yīng)一個(gè)子GameObject。骨架中的骨骼起到的是輔助計(jì)算渲染坐標(biāo)的作用,我們通過(guò)插槽所屬的骨骼計(jì)算出渲染時(shí)要用的絕對(duì)坐標(biāo)并填到相應(yīng)的TransformComponent中。最后,顯示對(duì)象中的圖片使用SpriteComponent進(jìn)行渲染到正確的位置上。
動(dòng)畫(huà)實(shí)現(xiàn)
骨骼動(dòng)畫(huà)其實(shí)是由每一根骨骼的多個(gè)屬性動(dòng)畫(huà)復(fù)合而成的,簡(jiǎn)單骨骼動(dòng)畫(huà)針對(duì)每一根骨骼及插槽其實(shí)可以拆分為以下幾個(gè)動(dòng)畫(huà):
- 骨骼(插槽)的位移動(dòng)畫(huà)
- 骨骼(插槽)的旋轉(zhuǎn)動(dòng)畫(huà)
- 骨骼(插槽)的縮放動(dòng)畫(huà)
- 插槽的透明度動(dòng)畫(huà)
這些簡(jiǎn)單動(dòng)畫(huà)都可以歸納為補(bǔ)間動(dòng)畫(huà),我們只需要在游戲每一次Update的時(shí)候?qū)?duì)應(yīng)的屬性值改變,自然就形成了動(dòng)畫(huà)的效果。
那么每一個(gè)時(shí)刻的值應(yīng)該是多少呢?這就需要一個(gè)插值器來(lái)告訴我們,Flutter的Animation對(duì)于插值器提供了很好的支持,回憶一下使用Animation的時(shí)候,是不是通過(guò)每一次觸發(fā)刷新了之后從Animation中取出value值來(lái)賦值到相應(yīng)的地方,同理使用在這也是一樣的。
因?yàn)楣趋绖?dòng)畫(huà)會(huì)有很多的關(guān)鍵幀,所以這里使用了Flutter中的一種特殊的Animation——TweenSequence。TweenSequence 可以傳入一個(gè) List>items 每一個(gè)TweenSequenceItem都可以設(shè)置一個(gè)補(bǔ)間動(dòng)畫(huà)和相應(yīng)的權(quán)重。
在保證每一個(gè)骨骼的動(dòng)畫(huà)總幀數(shù)相同的情況下,可以直接使用每?jī)蓚€(gè)關(guān)鍵幀之間包含的幀數(shù)作為權(quán)重,相應(yīng)的前一關(guān)鍵幀幀的值則為起始值,后一幀關(guān)鍵幀的作為終止值。舉個(gè)例子:
///Transform2 為自己定義的一個(gè)數(shù)據(jù)結(jié)構(gòu),只要重載了相應(yīng)的運(yùn)算符,一樣可以被Animation所使用 TweenSequenceItem<Transform2> _parseTransformAnimation( TransformFrame cur, TransformFramenext, { int duration, }) { if(cur != null&& next!= null&& cur.duration != null) { finalAnimatable<Transform2> tween = Tween<Transform2>( begin: cur.transForm, end: next.transForm, ); if((cur.duration != null&& cur.duration > 0) || (duration != null&& duration > 0)) { returnTweenSequenceItem<Transform2>(tween: tween,weight:duration == null? cur.duration?.toDouble() : duration.toDouble(), ); } } returnnull; }動(dòng)畫(huà)效果
這里以閑魚(yú)幣中的撈魚(yú)小人為例子(可以通過(guò) “閑魚(yú)首頁(yè) -> 右上角簽到圖標(biāo)” 進(jìn)入閑魚(yú)幣池塘進(jìn)行體驗(yàn))。
性能表現(xiàn)
我們使用干凈的 Demo 工程渲染多個(gè)上圖中的小人進(jìn)行測(cè)試,測(cè)試機(jī)型:iPhoneXs。
骨骼數(shù)量能一定程度上也能衡量動(dòng)畫(huà)的復(fù)雜度(還與變換次數(shù)相關(guān)),可以發(fā)現(xiàn)大于1000根骨骼時(shí)性能開(kāi)始出現(xiàn)衰減,并隨著骨骼數(shù)量的增加逐漸明顯,在3000根骨骼以上時(shí)出現(xiàn)明顯卡頓。這一性能已經(jīng)完全可以滿足App中內(nèi)嵌中小型游戲的需求(其中內(nèi)存增加問(wèn)題會(huì)在后續(xù)的性能篇中進(jìn)行闡述)。
現(xiàn)狀和展望
目前Candy已經(jīng)實(shí)現(xiàn)了對(duì)基礎(chǔ)骨骼動(dòng)畫(huà)、粒子動(dòng)畫(huà)、屬性動(dòng)畫(huà)的支持,并且已經(jīng)在閑魚(yú)幣業(yè)務(wù)中落地使用,后續(xù)會(huì)應(yīng)用在更多的場(chǎng)景之中。隨著場(chǎng)景的增加,我們面臨的挑戰(zhàn)也就越來(lái)越多。
? 動(dòng)畫(huà)賦能 app
一個(gè) App 必定不會(huì)有很多的游戲內(nèi)容,我們實(shí)現(xiàn)的動(dòng)畫(huà)如果僅在游戲場(chǎng)景下使用那其實(shí)落地的場(chǎng)景就會(huì)很有限,所以我們將 Candy 引擎中的動(dòng)畫(huà)部分封裝為了Widget,使得Flutter App可以天然無(wú)縫地使用動(dòng)畫(huà)。
? 更多動(dòng)畫(huà)能力的支持
Lottie 是一種深受設(shè)計(jì)師以及開(kāi)發(fā)同學(xué)喜愛(ài)的方式,對(duì)于 Lottie 的支持我們也已經(jīng)開(kāi)始進(jìn)行開(kāi)發(fā),等待完成后再與大家分享。
? 從動(dòng)畫(huà)升級(jí)為互動(dòng)
互動(dòng)是由一個(gè)個(gè)動(dòng)畫(huà)組合而成的,與傳統(tǒng)的動(dòng)畫(huà)最大的差距在于互動(dòng)需要有可交互性,在用戶發(fā)生不同交互時(shí)要進(jìn)行不同動(dòng)畫(huà)的切換,這也就意味著我們需要有編排各個(gè)動(dòng)畫(huà)之間的關(guān)系的能力。
這是一套很完整的體系,需要有相應(yīng)的邏輯編排工具以及端側(cè)對(duì)于動(dòng)態(tài)邏輯編排的實(shí)現(xiàn),我們目前正在與集團(tuán)中前端互動(dòng)小組的同學(xué)合作復(fù)用前端現(xiàn)有的工具鏈,但是前端比起 Flutter 在動(dòng)態(tài)邏輯方面有天生的優(yōu)勢(shì),所以我們希望結(jié)合閑魚(yú)團(tuán)隊(duì)的 Fass 以及 Flutter-dx 去構(gòu)建這套體系。
在完成之后也會(huì)有相應(yīng)的文章與大家分享我們的做法和心路歷程,同時(shí)也歡迎大家和我們一起進(jìn)行探討,碰撞出更多的火花。
總結(jié)
以上是生活随笔為你收集整理的骨骼动画实现秘密!闲鱼 Flutter 互动引擎告诉你的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 60秒完成病毒基因对比 阿里云向社会免费
- 下一篇: 一个优秀的Push平台,需要经历怎样的前