【技术分享】2D MMO中角色动画的优化总结

概述

我们的项目是传统的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大小,算是一个额外好处。

优化之二:去掉Plist文件

本以为经过上面的优化,手机上的卡顿应该解决了,但是很不幸,从表现上看只解决了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的纹理信息,然后用:

new cc.SpriteFrame.create([Texture2D ]  [rect ]  [rotated ]  [offset ]  [originalSize ] )

动态创建帧,把这个帧传入动画剪裁。后面的播放就和原来的一样了。

经过这一次的优化,基本上把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的脚本会合并,这样项目就可以根据自己的情况对脚本进行分离处理了。
75赞

收藏了,感谢分享。:hamburger:

1赞

感谢分享。请问你们有做对canvas模式下的优化吗 还有里面没提到音效音乐 有做特殊处理吗

感谢反馈,你最后的期望将来我们都会实现。

1.4.2 可以内嵌所有 SpriteFrame 的 json,这样 IO 就不会有频繁请求了。

:joy:不过我们有了自己的一个工作流程,而且对自己项目的可控性强一些。就不再用引擎提供的了。

后面那几个功能倒是热切期待啊,希望项目上线之时能用上,那就完美了。

不过话话是哪个功能,也许UI可以用上,是自动图集吗?

感谢分享~~~

1赞

因为我们主要是面向原生的手游的,基本上Canvas就不再考虑了。

1.4.2 构建时可以勾选内联所有 SpriteFrame

这个只有web包里才有这个选项 ios android没有找到

好吧……

看到论坛有人说:
尽量不要在resources下放东西,resources下的东西不会合并json

就是说在resources之外的会合并Json吗?还是说只有Web版的会这样?

web 平台,会在 resources 之外尽量合并 json,resources 之内也会稍微合并。

1赞

楼主的分享很好。谢谢

纹理压缩的流程,这块可以具体说说嘛?只是换图片格式就可以了?

加上压缩的同时用GZip对图片再进行压缩,这个gzip是怎么用的。json也可以用gzip再压缩的吧?

还有内存管理这块呢,怎么处理的?

图片加载,不是说 web上是异步的,app上是同步的吗?

图片压缩这一块主要是针对手机版的,他们内部的引擎就是Cocos2D-X,本来就是支持PVR这些压缩纹理的。
GZip就是一种文件压缩格式,Cocos2D也是可以自动识别的,你只要压缩好提供给引擎。自动就会解析成图片的。

图片加载都是异步的,Json原生版是同步的,但是因为Json比较小,速度比较快。

想到2点

1.所有的各种配置文件,都换成protobuf,包体更小,加载更快

2.室友预制模型(网页游戏的做法),人多的的时候先用默认模型显示,加载好资源,再显示真正的资源。

另外想问下
1.客户端和服务器同步大概怎么做的
2.资源的释放管理和内存 怎么管理的?

有个疑问想请教一下,
因为ccc加载都是异步的,如果在播放某个动画时再创建,加载图片会延迟,这样的话,播放时会不会有些卡顿的情况?这个情况是怎么处理的?

楼主分享的太棒了,我们是做Web端的,1,2都挺管用,我们不用AnimationClip,而是单独整了一个序列帧播放模块…

图片是创建角色时就加载好的,不要等到播放动画再加载。我说的是动画剪辑的创建,这个是同步的。且性能消耗很低。

1赞

换成pb可以减小文件大小,但js没有内罢支持的库,如果用js脚本实现的,效率可能会更慢。
以前我试过用bson,解析效率是json的几十倍慢。

2赞