其实这篇文章类似版本早在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);//当然这是一个需要自调用的函数,所以请保证你不会让它死循环
当然,setTimeout和setInterval都是可用的,只是相比后两者, requestAnimationFrame不会像它的前辈那样导致内存泄漏还有时间片误差也就是漏帧的出现。
综上所述,我们只要让它自调用就能达到我们的目的了。
有过其他语言经历特别是从事过客户端开发的童鞋应该对以上过程比较熟悉,我们想通过代码实现动画往往是一个蛮费力不讨好的事情,就像上图中的“序列帧”,如果使用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:
另外,再绘制之前,缩放你的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
也许现在的大前端的浪潮也正是这样,但是最后这位伟大的皇帝(无冕之皇)死在了自己元老会的面前。