在做這個項目之前,我也和很多人的想法一樣覺得:H5做動畫性能不行,只能完成簡單動畫,可是事實並非如此。所以借此篇分享振奮下想在H5下做酷炫游戲的人心。
體驗游戲請長按二維碼識別:
好吧,知道你懶。不想掃碼的可以看下面的視頻:
初識入門
什么是骨骼動畫,本篇先簡單做下科普,其他大家自行百度哦。
比幀動畫:它比幀動畫大幅節省了資源空間,也比幀動畫對手機性能有更高的要求,WebGL下能達到最佳的展示效果。
編輯器選擇:業界比較主流的骨骼動畫編輯器有SPINE和DragonBones(龍骨,egret白鷺公司在維護)。我們聯系到CP使用的SPINE編輯器比較多,而又需要同樣一個資源文件,三端公用(iOS,安卓,HTML5)。SPINE的運行庫選擇更多,所以我們選用了SPINE編輯器,雖然都是骨骼動畫,但是他們的動畫原理是略有差別的。
運行庫選擇:SPINE官網上有各種語言運行庫的推薦,單js有10余種運行庫,到底選哪個游戲引擎,工欲善其事必先利其器,選擇很重要。選擇恐懼症的話還是在科學的量化的方式選擇更適合我們項目的游戲引擎。主要有性能,是否支持webgl,庫體積,論壇活躍度,API健全度等5個維度綜合分析后,選擇了PIXI引擎。附件會附上對比表格。PIXI的使用上比較便利,官網上有豐富的例子讓你熟悉它的使用,這里不做詳述。
深入了解骨骼動畫原理
在新接手這個項目之前,公司km上還鮮有骨骼動畫H5的實踐,聽了IEG T4專家david分享的游戲開發經驗,雖然沒有提到H5上的經驗,但是他建議的熟讀游戲引擎源碼很是受用,熟讀代碼既能讓你實現功能的時候更得心應手(一下就找到最優方案,而不是不停返工),也能在性能優化時有的放矢,所以就這樣,一手api,一手源碼開始了空間寵物的開發。
先讓我們解開骨骼動畫神秘的面紗吧~純手畫給大家奉上我對於骨骼動畫的理解,這里僅僅是SPINE編輯器下的原理,不同的編輯器有略微的不同,這里不多余說明。如果當時有人給我這張圖,能節省我1天工作量,嗚嗚~
圖解:
1、骨骼動畫中的人物是由有骨骼,插槽,附件部分組成,這三部分都是1對N的關系:
-
骨骼是一個樹狀結構,有個明顯的好處就是,如果動畫時要設置位移,只要設置根節點的位移,整個任務都能一起位移。
-
附件其實人物外表的展示,主要有三種類型:圖片,蒙皮,權重蒙皮。圖片好理解,就好像是一張貼花貼着,很僵硬,就像下面圖一的那個長槍,圖二就是蒙皮,可以定義形變,讓整個長槍動得更加自然流暢
圖一
圖二
為什么蒙皮能自由形變呢?因為它有頂點,邊緣,三角區域這三個概念,能對圖片某個區域變形,這中特性在webgl是原生支持的,但canvas2d下是引擎自己寫的,這個就是說骨骼動畫對性能要求更高就是因為這個。為什么這三個概念能自由變形呢?看下面的圖解。
只要移動了那個頂點就能拉長鼻子
2、骨骼動畫中的動畫,是基於時間軸的,定義某個時間點顯示那個附件,骨骼的位移和旋轉等等,如我手畫的下圖所示:
3、引擎播放骨骼動畫流程。
-
引入SPINE編輯器導出的json文件
-
引擎自動引入同名的atlas和png文件
-
解析json和atlas文件,生成spine對象
-
加入到容器里面
-
定時器渲染,播放動作
4、其他,主要的就上面3點了,其他的自行了解咯。
空間寵物關鍵技術實現
1、實時換裝
換裝功能跟附件息息相關,圖片類型的附件引擎自帶換裝方式,但是蒙皮類型的附件卻沒有!
最簡單的方式,hack引擎從atlas讀取出來的附件信息,修改它的texture指向換裝之后texture(webgl渲染用的紋理)。再new 一遍 Spine對象,這樣雖然能實現需求,但是畫面會有閃動,體驗不好
所以在熟讀了一遍PIXI代碼之后,找到了更優雅的方式,pixi有一個Texture.fromCanvas的接口,可以把一個canvas作為一個紋理繪制,所以:把canvas代替png繪制,如果有換裝,就用canvas的clearRect擦除和drawImage覆蓋以前位置的圖片(需要注意的是旋轉這個參數)。bingo,換裝功能完成,換裝時就不再閃動了,而且圖片類型的附件還是使用引擎自帶換裝方式,更快!流程大致如下:
經驗教訓:實現代碼之前務必熟讀引擎源碼,理解原理流程,打通任督二脈,找到最佳實現方法。
2、組合動作編輯 用戶對於寵物不同部分的動作自由組合,形成特色動作
實現原理:
animation.state['setAnimationByName'|'addAnimationByName'](track, act_name, loop, delay);
可以基於軌道track(int)來做動作的疊加
亮點:
-
實現播放動作有限次
-
維持一個播放隊列
3、快照分享
問題1:WebGL截圖空白
答:
-
WebGL獲取上下文時,有一個關鍵參數:preserveDrawingBuffer,默認為false,表示在繪圖完成后不保留繪圖緩沖區。如果設置成true,會影響性能
-
在定時器里面,同步截圖,DONE!
問題2:毛玻璃效果怎么實現?需要重新引入庫來解決嗎?
答:否,pixi引擎自帶支持哦,Container對象底下有個filters參數可實現毛玻璃效果
4、分享gif,因為我們本來是骨骼動畫,如果要分享出去必須截圖再合成gif。
方案1:單次播放法,滿幀截取的情況下,ios能達到要求,但是安卓上不同機型,不同機器現狀都可能截出不同效果的gif,差的時候只能2,3張,效果差而且不穩定
方案2:多次播放截圖法,我們通常用的定時器計數的方式比較多,但是瀏覽器每輪播放狀態無法做到一致,所以計數截圖法不可取,所以就要用到計時的方式,所幸requestAnimationFrame里面回調會傳過來一個time,引擎會轉化當前動作已經播放了多長時間t,我們可以通過t來截取。
返回:由Math.floor(t*fps)生成的index截圖組成的數組;最終的截圖數組長度 >= fps * 動畫總時長就停止截圖 最終的效果也是很贊的。
這種截圖方式損失的時間,那會不會截圖的時間過長呢,我針對截圖時間做了下統計:
動畫的平均長度為1.8s,在兩個移動平台上的截圖耗時:
iOS: 2025ms
Android: 4535ms
這個分享頁面需要用戶填寫心情之類的,4s以內的截圖耗時可以接受。
引擎優化
第一次用PIXI,第一次用PIXI-SPINE。作為一個要上線承載上億用戶的產品,開發過程,遇到了一些引擎水土不服的地方,主要有:
1、播放動畫的時候展示錯亂,臉部五官漂移
定位:是因為同一個插槽下面有多種類型的附件。
解決:看源碼時,發現引擎在定時器更新遍歷插槽時間軸的時候,在region切換到mesh類型的時候或者mesh切到region的時候,引擎沒有隱藏之前的附件。所以就會產生漂移。之后如果還要展示之前的附件只需要重新設置可見性即可。修改代碼:
if (type === spine.AttachmentType.region) { if (slot.currentMesh) { slot.currentMesh.visible = false; slot.currentMesh = undefined; slot.currentMeshName = undefined; } .... }
...
if (type === spine.AttachmentType.skinnedmesh || type === spine.AttachmentType.mesh || type === spine.AttachmentType.linkedmesh) { if (slot.currentSprite) { slot.currentSprite.visible = false; slot.currentSprite = undefined; slot.currentSpriteName = undefined; }
.... }
2、畫面閃動,無法正常顯示動畫
定位:把webgl渲染強行切成canvas2d的,顯示就正常了,說明還是webgl下某種特性兼容性的問題。
解決:把PIXI升級到v4.0.2解決。至於具體原因這里不做具體說明。
3、增加圖片超時邏輯
解決:PIXI加載圖片的邏輯就是先聲明一個img,img.onload之后再觸發loaded事件,業務再去處理依賴於這個圖片的邏輯,沒有超時邏輯,會一直等待。頁面就是不可點擊的狀態,對於一個成熟的產品必須有很高的可用性,所以必須有完善的錯誤兼容的邏輯!就在源碼里加上了超時邏輯,業務能正常執行。
性能調優
功能開發好不容易完了,兼容性也OK了,但是性能卻挺糟糕,crash,發熱,進入游戲慢,性能bug單狂轟濫炸,靜下心來各個擊破,最終項目各個性能指標達標,在外網穩定運行。
工欲善其事必先利其器,定位性能問題,要通過工具去分析哪里是性能瓶頸,才能有的放矢,雖然通過chrome的性能分析工具已經能發現大部分問題,但是ios和安卓上因為實現的差異是不是存在其他的問題,也需要測試一下才放心。這里搜羅了一下常用的性能分析工具,供大家參考。
1、Crash
原因:頁面內存占用過高是主要因素,什么資源最占用內存呢,通過Chrome Profiles面板分析便知。
54%的內存都消耗在動作的Timeline數據上了。用排除法分析了一下一個動作的json占用的內存數。
每個動作原始數據+解析成數組總共占用390kB。每個用戶每次用的動作有限,並不需要把完整的動作數據加載出來,所以解決Crash可以通過一下方法
a. 首次只加載模型json以及必須的2個動作數據,其余的按需加載,解析完塞到寵物的動作數據里面(這里需要改寫源碼暴露讀取動作數據的接口)
效果:內存減少:49M減小到18M,減少了80%(Chrome上測試)。
b. iOS上啟用wkWebView,大幅提高Webview的穩定性。
2、發熱
原因:發熱跟CPU和GPU占用息息相關,WebView作為一個比較高層的應用,對CPU和GPU的占用是要比原生App的占用高很多,這應該也是HTML 5游戲發展的瓶頸所在。減少資源大小,將Canvas里面固定的圖片獨立出來等等措施都收效甚微。只要WebGL在requestAnimationFrame里面不停渲染就會發熱嚴重。
解決:降級策略看起來這里唯一行得通的解決方案了,沒有渲染就沒有發熱。所以就要把渲染用在刀口上。損失非關鍵體驗,安卓停掉默認動作停止渲染,需要時再打開渲染,改成隔3秒播放一次動作讓用戶感覺也是在一直動。
效果:有效解決手機發熱的問題。
3、幀耗時-FPS
PIXI是支持自動識別瀏覽器是否支持WebGL來選擇是Canvas2D,還是WebGL來渲染動畫,先科普下WebGL的市場占用率吧。
iOS:iOS 7.1以上都是支持WebGL的。
Android:安裝了TBS的機器支持WebGL。
注:TBS是騰訊QQ瀏覽器研發的Android平台WebView內核,目前廣泛用於手機QQ、微信、QQ空間中。
因為 TBS是熱更新的,新安裝的APP沒有TBS,就不支持WebGL,而又是我們推廣的關鍵時期。所以Canvas2D下的渲染也需要做好它的優化。
解決:Canvas2D下的幀耗時的優化可以通過將Canvas2D中不變的背景獨立到DOM上,不放在Canvas里面渲染
效果:優化了40%左右。
4、游戲啟動速度優化
解決:
a. 不采用一般游戲進入先loading資源,采用背景和寵物初步加載的方式。
效果:背景可見時間減少4s 8.4->4.2s
b. 動作JSON按需加載
效果:寵物可交互的時間減少了2.8s 8.4->5.6s
5、WebGL內存泄漏
表現:安卓上黑屏,iOS上因為用了wkWebView會重新loading。
定位:在排行榜頻繁切換好友的時候會必現,排行榜的設計模式是每個好友都是獨立的,每次都銷毀上一個寵物,再添加下一個寵物,這是面向對象編程基本的思路。
可是這樣切換幾次后就會異常,直覺告訴我是跟WebGL或者內存有關。這里先科普下瀏覽器的內存占用分幾塊:
js heap和Dom內存的占用通過chrome的profiles和timelines面板可以看出來,但是其他的內存占用可以在哪里看呢?通過chrome的更多工具 -> 資源管理器即可!
有了這個強大的工具,問題定位將不是問題,其實主要是兩個問題:
-
js內存上漲,切換12個好友js內存上漲了18M,定位下來增長的內存是附件mesh等等信息。
-
GPU內存只升不降,GPU內存就是WebGL占用的內存,如果有獨立顯卡的話他的內存占用是和瀏覽器內存獨立的,但是手機端沒有獨顯,可能會跟瀏覽器有共享內存,這個按機型而異。但是GPU內存增長太多絕不是好事,它可能會影響瀏覽器申請更多的GPU,造成頁面黑屏,也會導致瀏覽器占用內存過多,被原生app殺掉或者重啟。所以這里就是頻繁申請釋放GPU內存造成的內存泄漏。
解決:既然已經發了問題,解決方法就應運而出啦
a. 同一種寵物模型對象復用,切換下一個好友的寵物就是換裝,解決js內存過度增長!
效果:切換12個好友內存增長了5M而已
b. 換裝紋理復用,不銷毀。因為換裝紋理是一個canvas,完全可以擦除之后再回收再利用。解決了GPU的內存泄漏。
效果:
以前的方案,5次切換好友之后,GPU內存直接飆升了90MB,從166MB到255MB。
復用紋理后,5次切換好友,GPU只上漲了30MB,第二次切換后並沒有上漲。從166M到199M。
c. 從排行榜切換回來之后,銷毀好友的寵物模型。這個是PIXI官方提供的銷毀接口。
pet.stage.destroy(true)
銷毀后,GPU內存恢復到最初狀態170MB左右。
經過上述三次改造后,內存就維護在一個比較穩定的狀態,黑屏和reload的問題就修復好了。
總結
-
做游戲,需要熟悉引擎源碼以及WebGL。越熟悉越好。
-
項目需要的內存越多,能夠運行它的終端就越少,所以一定要想方設法的定位內存占用大頭,逐個擊破。
-
可以使用降級策略,降級策略要能因機制宜,切忌大刀闊斧。根據機器的剩余內存和GPU核數或者當前的FPS值來做降級策略。