其實這篇文章類似版本早在12年就在網上各處出現了,也隨着HTML5的興起,HTML的新特性也是倍受開發者們追捧,自然相關HTML5的高性能動畫與游戲的相關文章也是層出不窮的,筆者也是在12年接觸的相關技術,不過俗話說“隔行如隔山”,隨着大前端時代的到來,前端的工作范圍和知識疆界也在不斷的擴展,需要的知識結構和知識體系也在不斷的豐富,最近也基於所在團隊不斷需要有新人有這方面的知識儲備,於是就有了此文。當然本文並不會提供任何一段完整可用的代碼,伸手黨也請耐下心來看吧,理解了原理實現其實是一件很簡單的事情。
什么是動畫?什么是游戲?
如上所述,既然是面對新人的,所以有必要從根源開始講起。首先需要回到“是什么”的問題?
這里援引某度的定義:
“動畫的概念不同於一般意義上的動畫片,動畫是一種綜合藝術,它是集合了繪畫、漫畫、電影、數字媒體、攝影、音樂、文學等眾多藝術門類於一身的藝術表現形式。最早發源於19世紀上半葉的英國,興盛於美國,中國動畫起源於20世紀20年代。動畫是一門年青的藝術,它是唯一有確定誕生日期的一門藝術,1892年10月28日埃米爾·雷諾首次在巴黎著名的葛萊凡蠟像館向觀眾放映光學影戲,標志着動畫的正式誕生,同時埃米爾·雷諾也被譽為“動畫之父”。動畫藝術經過了100多年的發展,已經有了較為完善的理論體系和產業體系,並以其獨特的藝術魅力深受人們的喜愛。
動畫的英文有很多表述,如animation、cartoon、animated cartoon、cameracature。其中較正式的 "Animation" 一詞源自於拉丁文字根anima,意思為“靈魂”,動詞animate是“賦予生命”的意思,引申為使某物活起來的意思。所以動畫可以定義為使用繪畫的手法,創造生命運動的藝術。
動畫技術較規范的定義是采用逐幀拍攝對象並連續播放而形成運動的影像技術。不論拍攝對象是什么,只要它的拍攝方式是采用的逐格方式,觀看時連續播放形成了活動影像,它就是動畫。”
當然筆者還聽說過另外種說法動畫起源於我國的連環畫,當然這都是后話了,回到正題,撇開歷史因素和文化因素不談,單就技術實現來說動畫其實就是“采用逐幀拍攝對象並連續播放而形成的影像技術”。如下圖:
在用戶的視界內,從左到右連續定寬展示這種圖片就能給人看上去一種這個“球球”動起來了一樣的感覺。
說白了,其實是利用人類大腦的“腦洞”和眼睛的“視覺暫留”,將原本“連續”的世界抽象成一個“離散”的世界,再使用了一種類似“積分”的方式把每一個畫面累積起來,形成了動畫。
這里還有值得一提的就是“幀(frame)”,相信看電影、玩游戲的童鞋都對這個幀有過一定的了解。通俗的來說我們更多提到的是“每秒多少幀(FPS)”,比如24幀以上我們就不會感受到“卡”,這是因為我們的眼睛能將我們看到的畫面暫時保存在腦中約1/24秒,所以只要保證每秒展示24副連續圖片,那么正常人其實就能感受到一個連續的影像了。但是反過來,幀數的提高其實並不能改變人眼對圖像的識別機制,所以動畫《進擊的巨人》部分劇集使用了60幀/秒的技術,還有去年剛上映的《比利林恩的中場故事》甚至還有120幀/秒的場次,其實大多數人的眼睛或說腦子其實是處理不過來的,那么問題來了,他們這么做到底有什么用呢?有興趣的童鞋可以研究下,由於離題太遠本文就不展開了。
接下來是游戲,因為游戲的涵蓋范圍更廣,這里就僅僅只對本文中涉及到的游戲做一個膚淺的解釋:
“凡是能夠進行交互的,它們通常還會涉及到動畫。”
當然,這里剝離了它的最大的價值“帶來快樂”。其實生活在這個互聯網時代,游戲基本成了生活的一個必備要素之一,或多或少,或深或淺,游戲都現實的充當着生活的調劑品或者更多的作用,本文便不再贅述了。
如何實現動畫與游戲?
接下來我們來談下“怎么辦”的問題。動畫的實現方式上文中已經提及了,結合筆者前端的技術背景,在這里羅列一些大概的動畫實現方式:
非JS的:
CSS3提供給了我們兩個非常實用的特性,讓我們能夠借助非JS的方法實現動畫,而且非常的簡單。那么,首先是
transition,嚴格的來說它並不能算是動畫,至少字面翻譯它應該被稱為過渡,但是我們在日常工作中,經常使用這個屬性來快速實現簡易的動畫效果,這里附上相關
鏈接,如果不清楚的童鞋可以查閱下;另一種就是
animation,同樣是CSS3的新特性,這個應該是各種意義上真正被用作動畫的屬性了,結合keyframe的使用,你可以實現很多你想要的動畫效果
鏈接。當然,基於css3的動畫其實也是HTML5的動畫的一個分支,要提高性能的話還需要借助3D加速,如preserve-3d,will-change,translate3d等hack技術提高體驗,不過本文就不在這里展開了。
着重說下另一種,也是本文着重想講的一點,JS的動畫實現:
首先我們需要一個繪圖容器(雖然div也是一種選擇,不過它太陳舊了,而且和html5並沒有什么關系,本文就只闡述canvas了),給它取個名字,然后通過js找到它
var canvas = document.querySelector('YourCanvasId'); //當然是用getElementById也是可以的,畢竟它是沒有兼容問題的
然后,既然是“畫”,那么我們需要一支畫筆:
var ctx = canvas.getContext('2d');//畫筆為什么叫“ctx”呢?這其實是context的縮寫,是canvas api提供的繪圖對象的引用
拿到筆,通過什么方法畫呢:
ctx.drawImage(YourParams);//這個重載了n次(?)的方法剛接觸的時候可謂復雜之極(笑)
這樣,就能畫出你需要繪制的圖像的一幀了,至於
圖片需要加載完成之后才能繪制之類的基礎知識,這里就不展開了。
接下來,我們需要讓它動起來,該怎么辦呢?我們需要每個一個固定的時間就調用drawImage方法,不斷更新繪圖容器里的圖案,讓我們的眼睛看到運動的影響:
requestAnimationFrame(YourTicker);//當然這是一個需要自調用的函數,所以請保證你不會讓它死循環
綜上所述,我們只要讓它自調用就能達到我們的目的了。
有過其他語言經歷特別是從事過客戶端開發的童鞋應該對以上過程比較熟悉,我們想通過代碼實現動畫往往是一個蠻費力不討好的事情,就像上圖中的“序列幀”,如果使用css3實現動畫的話,只需要使用keyframe驅動background-position或者transform變化就可以了,但是想通過代碼實現,首先我們至少需要一個INTERVAL句柄,來幫助我們在至少1/24秒調用一次繪制的方法,手動讓圖片展示我們希望他展示的部分,這其實是一件蠻廢力的事情,而如果使用CSS3則不需要你那么麻煩,寫一個幀動畫也許就完事了。當然這也是css3為什么會有animation屬性的原因所在,但隨着業務的發展(或者說。。人們“腦洞”越來越大?)我們需要玩的越來越多的“新花樣”,css3的animation解決的大抵是網頁維度的動畫問題,而canvas的api才能解決webapp的動畫和游戲的問題。舉個栗子:
我們需要讓剛才的動畫“動起來”,或者說產生和用戶的復雜交互,這個時候該怎么辦?也許CSS3玩得很溜的童鞋會說,css3依然可以實現這些!
是的,誠然,筆者也這樣實現過。但是當你打開的chrome devtools,特別是看到你的頁面在執行你復雜css3動畫是產生的paint flashing和FPS meter里過山車一樣的幀率變化時,你會發現,特別的,當你費勁心血寫的那些動畫在手機(特別是前幾年的Android機器上)的時候,s**t,為什么這么卡?電腦上明明好好的!
雖然技術日新月異,但是依托於瀏覽器的webview的性能依然是有限的,它並不是一個能夠讓我們隨意發揮創造力的一個舞台,我們在實現心中所想的時候,還需要考慮到性能問題,當reflow repaint等瀏覽器渲染機制一次又一次折磨着你的時候,canvas無疑是一劑“包治百病”的良葯(當然,歷史總是驚人的相似)。
扯了那么多,回到實現動畫與游戲的問題上來,接着動畫往后走,我們繼續向我們場景中注入事件監聽(當然,理想情況下這應該事件事件捕獲)
stage.addEventListener('click', YourClickHandler, true); //這里我們需要向canvas的上級,也就是整個舞台添加事件監聽
這一個操作構建了用戶與動畫的交互,使之成為了一個最初級的游戲。
一切就這樣順應而生。
記得第一次寫游戲的時候,能想起一句話
“上帝說,要有光,於是,便有了光”
不知道有多少人會和筆者有一樣的感受(笑)
這是一個快餐的時代,如果更快的實踐?
簡單的講完了原理之后,很多童鞋可能會筆者當初剛知道原理的時候一樣,有同樣的問題,時代發展得那么快,我們哪有功夫慢慢去堆一套這么個東西出來呢?這個問題同樣困擾着筆者,在筆者剛過去的2016年就碰到了這樣一個場景:
業務方需要在春節上線5個HTML5的游戲,時間基本只有兩周,整個項目處於劇本暫無、人員未定、資源沒有,但是檔期已定的嚴苛條件下
拿2016年的年度詞匯來說,這是一個典型的“黑天鵝”事件,但是既然它已經發生了,那么我們能做的,就只有“面對不確定性”。這個時候筆者也想起來早在2012年學習HTML5的時候就嘗試着寫過一個html5的游戲引擎,也經過團隊的平衡考量,最終選擇一個蠻有淵源的H5引擎Hilo,用於快速開發。雖然至今對內部事件機制仍抱有一些質疑,但是整體api的可用性,易用性,還有針對web端這樣的一個特性,筆者最終選擇了這個引擎,順利完成了那個“甲方虐我千百遍,我待甲方如初戀”的需求。
回到這個問題上,這次也許我們面對的是一個游戲的開發需求,下次也許是一個其他的什么的開發需求,在這個迅速變化、發展的時代,作為一個前端工程師(請記住,我們是工程師),我們應該如何自處呢?我想起了《黑天鵝》一書作者納西姆·塔勒布的另外一部書《反脆弱》他在扉頁里這樣寫道
“從不確定性中獲益”
希望你我能都從中獲益吧。
總結一下
在html5時代,如何高效的的實現動畫和游戲呢?
首先,我們可以依賴CSS3提供的強大支持來實現部分靜態、固定的動畫效果(當然別忘了那些硬件加速的小姿勢,能夠迅速的幫你提高性能),而對於那些復雜的動畫,我們則需要依賴canvas的強大能力,當然,再進一步,我們還能使用webgl(但是不得不說,雖然canvas在部分android的表現已經不算太好,但是webgl。。還有是否支持的前置問題)。
然后,對於基於canvas的動畫或者游戲,高效的抽離和管理interval和事件監聽,如使用requestAnimationFrame替換setInterval和setTimeout,所有tick在一個統一的ticker中管理,以及將避免過多的事件監聽,都能有效的提高整個webapp的性能。
最后,前文可能並沒有提及的,內存的管理,資源的回收,這也是大部分性能問題的症結所在,基於過往的習慣,我們大多數時候都只關心如何構建和使用資源,而在游戲的場景中,卻多了一個回收資源(比如說restart)的場景存在,我們不能總寄希望於依賴頁面刷新去處理,能夠對自己創建申請的內存資源進行管理和回收,這也是工程化思想很重要的一部分。如何更好的使用單例模式構造一經創建而不需要頻繁回收的對象,使用工廠模式批量的構造我們需要的對象,使用裝飾者模式在不破壞游戲結構的前提下完成打點,這都是我們需要關心的工作。
一些也許有用的小知識
講道理文章到上面就應該結束了,但是說了一堆大家都懂的話未免也太無聊了,以下羅列搜集一點自己的實踐中的干活,希望再各位童鞋踩坑的時候能夠幫到大家:
1、首先是關於canvas的,同樣也是我們老僧常談的一倍屏VS多倍屏的問題,我們都知道手機現在至少都是個2倍屏,那么聰明的你肯定會想到,既然圖片都需要針對2倍屏做適配,那么canvas應該也會有類似的問題才對吧?沒錯,canvas也會因為手機的2倍屏而導致成像模糊,最簡單的處理方式是:
你的dom中給一個2倍寬高的canvas:
<canvas width="YourScreenWidth * 2" height="YourScreenHeight * 2"></canvas>
另外,再繪制之前,縮放你的ctx為原始的1/2:
ctx.scale(.5, .5)
想知道具體原因的童鞋,不妨思考一下為什么。
2、想必很多同學都有過使用rem開發響應式頁面的經歷,那樣的頁面確實能夠很好的填充整個屏幕,但是如果換做是canvas呢?rem的那套實現還能夠好用么?其實這個問題大家只要想想,比如你玩的如LOL、DOTA、或者某某手游,它會因為你屏幕變化而隨着等比例變化么?就很好回答了。
3、和CSS3的的渲染一樣,開啟硬件加速往往是解決性能瓶頸的少數不多的方法,當然canvas平台上還有一個webgl的解決方案,能夠優化你的html5 app的性能。
4、也許細心的童鞋在上文中也發現了,為什么要使用事件委托來添加監聽呢?因為整個canvas容器就一個eventHandler,基於性能考量,這是性能最好的一個方案(至於實現嘛,事件隊列也是蠻成熟的思想了,這里就不贅述了)。而同樣的,所有需要延時調用的,我們也會都是用那唯一的已經啟動ticker來處理。
5、關於裝飾者模式的數據采集。其實大多數公司都會有數據監控的需求,因為他們需要知道他們做的這些webapp的實際效果如何,那么數據監控則是勢在必行之舉,但它本身是和游戲是沒有關系的,所以基於aop的思想,也依賴babel強大的能力,我們能夠使用裝飾者模式,以最少的成本換取項目盡可能少的破壞。
目前能想到的就這些了,也許未來碰到了新的坑也會更新上來。
然后最后按慣例需要升華下主題。生而有幸,在前端最迅猛發展的幾年選對了行,入了行,並且一步步走到了今天,隨着前端的大潮席卷着全球:
動畫 游戲 node 數據庫 hybridapp
這又讓我想起了著名的羅馬大帝蓋烏斯·尤利烏斯·愷撒說過的一句話:
I came, I saw, I conquered
也許現在的大前端的浪潮也正是這樣,但是最后這位偉大的皇帝(無冕之皇)死在了自己元老會的面前。