2D MMO中角色动画的优化总结
在深圳Cocos沙龙上,有幸结识了社区中大名顶顶的Colin,Shawn在社区论坛上第一次看到Colin的团队用CocosCreator制作的《热血暗黑》时就被深深地震撼到了!更为重要的是,Colin将他的技术心得和宝贵开发经验写成文字,每一篇分享都是满满的干货。而且幸运的是Shawn得到Colin的授权许可,将他的文章散播到奎特尔星球,我们一起欣赏一起成长!
我们的项目是传统的2D MMO,即人物动画是以图片帧的方式表现的,一个角色大约有8个动作,1个动作有8个方向,1个方向约有10到20帧的图片。这样算起来一个角色由近千张图片帧组成。这样的量对内存和性能都有很高的要求。从立项到目前经历了很多次优化,终于在流畅度和内存占用上达到比较理想的状态。
不知道群里有多少人也在用Creator做类似的MMO手游,我在这里把优化的经历大概写出来供大家参考,也许你们有更好的优化方案,都欢迎一起讨论。
最初为了快速实现效果,采有很直接的方式:
一个动作一张图集(Plist),8个动作则有8个图集。
每个动作只包含5个方向的图片帧,另外3个方向通过翻转实现。
动画则用Creator提供的动画剪辑(AnimationClip)来实现。
由于一个AnimationClip只能播放一个方向的动画,那么一个角色就需要8*8=64个AnimationClip,如果每个动画剪辑都要在编辑器中编辑,估计美工人员会先晕倒了。
幸好.anim文件是json格式,很容易理解它的含义,于是我们用Python写了一个导出脚本,美术只需要提供角色的所有散图,脚本调用TexturePacker先合成图集,再动态生成anim文件,最后生成一个prefab。
这个prefab就代表一个角色外形。程序的使用就很简单,首次使用先加载Prefab并实例化出结点。从结点取出Animation组件并调用播放接口即可。当角色释放时,把结点收回存到NodePool去,以备下次使用。
从上面的实现看,其实不用Profile也很容易知道会有性能问题,虽然里面使用了NodePool缓存创建过的角色外形。但首次加载这些资源会很卡:一个角色的图集和动画剪辑加起来接近100个文件,想想那个IO压力都觉得蛋疼。
在PC的Web端,这个性能问题没有暴露出来;在安卓甚至苹果机上,一旦旁边有角色进场景,马上就能感受到卡顿,有时甚至能卡上1到2秒。因为我们做的MMO,场景中的玩家进进出出是很平常的,这样的卡顿是不能接受的。
首先能想的是去掉anim文件,角色动画其实很简单,只是一些序列帧的播放,并没有用到移动缩放这些动画类型。用anim文件来描述动画有点浪费了,完全可以用另一个简单的Json文件记录动作的信息,比如这个角色有几个动作,每个动作有几个方向,每个方向有几个帧,只要这些信息就够了,类似这样:
"run": [5, 20, 0.033],
"idle": [5, 4, 0.1],
}
而图集中图片帧的名字,需要有一点规则,即可以通过动作名
,方向值
,帧索引
这些合成。然后就可以找到对应的图片帧了。
在程序中,仍然是使用AnimationClip
来实现动画,只不过它是动态创建的,使用引擎提供的一个API:cc.AnimationClip.createWithSpriteFrames
,具体可以参考文档。AnimationClip
不用一次性创建出来,可以在播放某个动画时再创建,这样创建的消耗就平摊出来了。我们实现了一个AnimControler
组件,通过它来播放动画,而延迟创建动画剪辑这些就顺理成章就封装到组件里面了,外部逻辑不用关心。
这一轮优化下来,一个角色只有动作图集和一个Json文件,去掉了大量的anim文件。包体也因此少了30M大小,算是一个额外好处。
本以为经过上面的优化,手机上的卡顿应该解决了,但是很不幸,从表现上看只解决了1/3,仍然有0.5到1秒多的卡顿。接下来就各种脑洞和调试了,甚至怀疑加载图片是不是用同步的方式,后来问了@panda,以及自己查看引擎代码,终于确认用的是异步方式。这样的话不应该呀,难道解析Plist文件用了这么长的时间?
后来在调试原生版本时,发现Plist文件原来会按图片帧打散,一个帧就一个json文件,里面描述了帧的位置偏移等信息。想想我们一个角色有近千个帧图片,当加载Plist时,引擎要把所有的Json文件加载进来。这比加载anim文件可蛋疼多了。
既然发现了瓶颈所在,解决方案就自然而然了:去掉Plist文件,只留图片。那么怎么知道每一帧的信息呢,答案还是从Plist中找。我又用万能的Python写了一个工具,把Plist的帧信息提取到上面提到的json文件中去,然后把Plist文件删除。
在程序中,我不再加载cc.SpriteAtlas
,而是直接加载cc.Texture2D
,然后当创建cc.AnimationClip
时,我需要从配置中找到cc.SpriteFrame
的纹理信息,然后用:
动态创建帧,把这个帧传入动画剪裁。后面的播放就和原来的一样了。
经过这一次的优化,基本上把IO的负担都消除了,我们在IOS上测试了一下,果然如丝般顺滑,进打怪地图会刷出大量的怪物,也感觉不到卡顿。在安卓上也基本可以接受,在大量角色进来时会有很微小的卡,时间不会超过100毫秒,而因为有了缓存,后面也是顺滑的。至于安卓的这个微卡,我归结为两个:
图片加载进来后解析成纹理的过程。png文件涉及到解压,反编码成raw形式。
json文件的加载和解析。json的加载其实是同步的,而解析成JS对象也是需要时间的。
上面这两个其实也和安卓的性能相关。
我们的角色图片用的是png,如果把散图合成一张大图,大概接近于2048*2048,即一个角色加载到内存,会消耗10M以上的内存。再加上角色是可以换装的,一个场景同时出现10个以上的外形是很平常的。光角色这一块就会消耗掉100多M的内存。还是挺让人着急的。
考虑到我们的游戏类型是偏写实的,画面的彩色相对会复杂一些,就决定用PVR,ETC这些格式来看看效果。后面在构建过程中,加入了纹理压缩的流程,IOS用PVR4,安卓用ETC+Alpha,最后的效果完全可以接受,在手机的小屏幕上看不出太大的区别。
PVR4使内存减少了8倍,ETC+Alpha则减少了4倍,这个优化量太可观了,而且附加的好处也很多:
加上压缩的同时用GZip对图片再进行压缩,图片大小比原来的PNG减了3倍,使得程序包又减少了几十M。
由于压缩纹理直接传给GPU,加载纹理只有一个反解压GZip的过程,加载过程也必然大大加快。
都说内存,速度,和发热是手游优化的三座大山。但引擎是不大可能完全帮你解决这些问题的,最重要的还是要根据引擎的特点,自己研究出合适项目的优化方案。引擎只需要在底层提供好机制,具体策略由项目去考虑。
对于Creator后面的进化,很希望这几个方向得到加强:
JS引擎的效率,Web端的V8已经足够优秀,但原生的SpiderMonkey显然差了很多,官方似乎也在提升原生引擎的版本,期望后续的消息。
类似Unity的
AssetBundle
,有了这个机制,就可以对资源进行分包,这对大游戏的分包发布很有好处。工程脚本的分割,游戏里面有大量的策划配置文件,有些动辄上万行,整个工程的脚本合并起来会达到好几M,这对热更新非常不友好的。如果能把脚本分割,每个脚本可以打一个
Tag
,相同Tag的脚本会合并,这样项目就可以根据自己的情况对脚本进行分离处理了。
最后给大家欣赏一下Colin与他的团队正在开发的游戏《热血暗黑》中的游戏图截:
《热血传奇》与《暗黑破坏神》的集大成者
霸气非凡的人物装备
欢迎关注「奎特尔星球」微信公众号,有代码、有教程、有视频、有故事,一起来玩吧!