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