这次跟大家分享的是《基于Cocos2d-x的回合MMORPG开发经验》,我们主要是以LUA和C++来开发项目,使用两者的原因之一是LUA和C++语法简单,并且LUA开发稳定性好,不容易造成崩溃,而且它支持在线更新的机制。用LUA开发的话,逻辑代码和资源文件会比较好,这个优点在多平台运营的时候很明显。因为你更新了一个软件之后,需要先下载平台,这个时候如果用其他软件编辑会导致所有的数据不一样。这个模式的第二个优点就是出现逻辑错误以后我们可以将错误的信息从玩家处收回来,进行错误信息的搜集。第三个优点是内存垃圾回收,以往C++内存垃圾回收是很麻烦的,用LUA的话,你只要能够按时去清除变量,基本上不需要去担心内存的使用情况。因此LUA在脚本里面的使用算是比较好的。
不过这种模式也会有一些问题,比如说游戏启动时需要加载LUA文件,像现阶段需要加载500多个LUA文件,这是个很头痛的事情,但是不得不加载。加载LUA文件是需要一定时间的,现阶段大概需要10秒钟,整体的效率要比C++低,毕竟是脚本语言,还是有一些可以优化的方案。我们现在采用的方式是把复杂数学计算的逻辑都放在Cocos2d-x引擎层,用C以及C++来实现。对于需要灵活可变、利于开发维护的逻辑宜放在脚本层里面处理,这样就增加了灵活性。需要注意一点就是LUA和C之间的穿越对性能有一定的损失。如果你将它的方法全部都进行包装的话,就会造成一个云计算的负担,所以建议大家直接把它包装成LUA的开发包,数据存到LUA的对象,这个对象就是LUA的Teb表。
我们现在使用的框架是开发者使用比较多的MVC框架,这种开发框架大家应该都比较熟悉,特别是经常在安卓下面开发游戏的程序员。有一点不同的是我们添加了一个消息监听的机制,我们的UI也是重新包装了一次,并且写了一套UI管理的方案,原因是我们使用的Cocos2d-x版本比较旧,没有太丰富的空间,之后我们对它进行了包装,重新管理它,显示了它的优先级别。这个方案的实现比较简单,不像一些比较复杂的UI管理器一样有配置、有坐标、能拖动的界面可以操作,目前是手动操作的。我们建立这套框架就是为了减少代码之间的偶合性,所以目前基本上能够满足这种需求。
前端开发遇到的难点
接下来跟大家说一下我们在前端开发遇到的难点,第一个难点就是游戏大小包。一个包的2D至少要1G,3D的话大概在1G以上,而手机上1G的话,可能没有人去玩,所以不得不对游戏包的大小进行控制。我们目前使用的是打包工具是TP。游戏包的大小会对用户转换率造成较大的影响,这是一个比较重要的内容。有很多人喜欢选用PNG的格式,这种格式有很多优点,它的兼容性很好,体积也比较小,但是如果进行压缩以后,会对图像的质量有非常大的影响。
另外还有一种格式体积很小、兼容性非常好、画面质量也非常好,是我们目前主要的图像模式,在大部分的机型上没有出现过问题。以前还使用过JPG的格式,但是有一点不好,它对于手机芯片一定要GPU的CPU,现在这种CPU大部分都不太支持,只有诺基亚和三星少部分机型支持,所以不可能使用这个作为主要的打包格式。
为了方便打包整个游戏的资源,我们编写了一个打包脚本以完成整个打包游戏的工作。我们需要做的是配置这些脚本的参数,点一下之后就可以完成所有的打包工作,并且同一个包可以配置多种打包的格式,方便我们配置压缩的比例和方案。目前第一个游戏刚做出来的时候有80多兆,经过压缩之后,再经过我们选择不同的打包的格式和打包脚本,最终将大小压缩到了48兆左右,这个是在没有删减任何游戏内容的前提下做的事情。还有需要注意的一点,大家对游戏进行打包时,如果有透明空间的话,它一样会占用你的内存,所以打包的时候要注意不要有太多的透明空间。
难点:启动速度
我们遇到另外一个难点就是启动速度。启动的时候加载LUA文件很慢,所以每次加载要等十几秒才能够进入游戏,我们第一次启动加载了一分多钟才进入,当时是完全没有办法接受的。后来在代码上做了一些优化的工作,将一些文件放到后面去加载,不过我们目前没有这样做,因为这样可能会造成其他的问题。现在想到的办法就是把配置文件里面的函数尽量压缩,优化表格结构,让列数尽量减少。比如说只导出和保管客户端要用到的数据,这样可以节省很多的空间。我们能将1M的压缩到1K,这样一来还是比较有用的。目前我们的启动时间是14秒左右。其实我们也有想过其他的一些加速方式,比如说将它编译成C代码,就可以极大加快它的速度,但是编译成C代码之后,就没有优化代码的更新了,就会失去这个机制,我觉得很划不来,所以放弃了。另外我们也考虑过阶梯的优化方案,但是优化阶梯更适合服务器的开发,比如有大量的逻辑运算和操作方面对于性能提升比较高,对于客户端来说,客户端的运行结构比较复杂,它对于性能的提升是比较有限的。
难点:闪退问题
还有一个难点就是经常会遇到闪退的问题。其中比较严重的问题就是资源加载内存峰值造成闪退。为了避免内存暴涨,我们需要测试所有图片,对256色度的PNG8格式图片进行了测试。它本身会用到的库加起来总共不到1M,这个是库内部申请的内存,之后再进行分配,大概会消耗16MB缓存。PNG8格式的图片是一个更真实的测试环境,它将测试的方式复制纹理到GPU中,然后会再释放png库和zlib库所用到的零散内存,最终的峰值达到48M,之后内存会稳定在16M。此外,我们对于pvr.ccz+Rgba4444的文件进行测试,测试的情况就好很多,它加载文件只消耗了1点几兆,缓存需要8兆,然后进行数据解包操作,再到文件的缓存,加载到CUP,最终这个峰值达到16M,所以它的内存会达到8M,峰值是16M。
针对JPG质量的测试结果,我们分析JPG库里面有一个内存池的管理,并不是想象中使用那么高的内存。它首先会进行一个内存分配工作,这个使用大概有0.5M,基本上可以忽略。然后数据缓存,再到对齐的缓存,会使用12M的内部空间,复制纹理到GPU需要12M的空间,同时又会再分配12M的内存。也就是说这个时候达到峰值32M,最后它的峰值会慢慢降回到12M,然后退掉12M,之后会回到复制数据使用的缓存。所以JPG的内存使用最高达到36M,没有网上说得那么高,我们自己测试的数据峰值并没有比PNG高,目前PNG是使用内存峰值最高的。JPG的格式在有一些机型上会有兼容性的问题,在后面的机型适配部分我会跟大家介绍这个情况。
闪退的一个有效解决方法是降低进程占用的基本内存。使用Cocos2d-x本身的纹理缓存机制,一是清理纹理缓存,二是清理动画对象缓存,三是清理帧对象缓存。大家尽量不要使用大图片,如果将大图片切割成小图片的话会好一点。另外还有一个不错的方法就是改变纹理加载的机制,可以在加载纹理的过程中间隔一点的时间,对于峰值起到很大的缓解作用。我们目前使用的方法就是隔一帧进行加载,这样可以有效防止峰值加载的问题。当连续加载很多帧的时候,你的真实内存涨幅会很快,如果你每隔一帧加载的话,涨幅会比较少。目前由于每个手机系统的内存管理的机制不同,导致手机使用操作系统的内存机制都不太一样,但是有一点可以确定,就是使用了换页的概念。因为在一些测试的机型上,可以检测到的纹理内存比实际的纹理内存要小几十兆。
另外,还有其他的办法降低进程平均使用的总量,Cocos2d-x本身提供了纹理缓冲的机制,功能实用,加快了运行的速度,我们也的确在使用中或者在内存出现问题的时候去考虑清理缓存,这是比较快速解决问题的办法。但是我还是建议大家不要频繁清理缓存,因为每次清理完缓存的代价就是下次要重新下载所有的资源,那样会非常慢。我们现在的解决方式就是在一些比较合适的时机进行缓存的清理,而且Cocos2d-x对于内存缓存有警报的作用,清理的工作也都是针对于具体的操作需要而做的,但是不太建议在内存警报的时候将刚刚加载的缓存全部清掉。我们测试安卓系统上内存警报的泛值是12%到15%,像动画对象缓存和清理帧对象的缓存占用空间不大,没有什么清理的价值。另外一个比较容易忽略的方法就是LUA有一个JC的机制,有时候会清理掉20M的内存,这个也是比较可观的。
测试的积存的占用情况
第一个是PC上的峰值,PC上的峰值是212M,这里统计出来的数据是根据PSS。可以看到后面的三星I9001占用的内存大概是240M。其实Cocos2d-x开发这个大型的MMORPG占用内存是比较严重的问题,一般来说都会遇到内存占用的问题。我们可以看到比较有意思的地方是三星I9300这款机器,它的峰值只有90多兆,但是它的纹理内存达到110到120,说明这台机器纹理内存的管理比其他的系统要好。接下来是海尔360,差不多是200M,OPPO以及小米、HTCG11、摩托罗拉这些在图上都有具体的峰值。
另外一个闪退的原因就是C++指针使用不当。自定义的C++对象导出给LUA使用的时候需要引起注意。如果这个时候你的C++对象已经清除的话,在LUA上是不可能知道的,这是使用Cocos2d-x过程中最容易遇到的一个问题,特别是使用C++和LUA的开放模式。另外,如果你交给LUA管理的话,就算你使用的时候没有问题,但是它的时间会很难掌握,可能会经常有对象被LUA控制,会造成在一定时间内,有很大一部分内存被占用掉。我们目前选择的方案有两块数据,分别是LUA和C++,现在的模式是各管各的。
难点:适配机型
这个适配机型也是遇到很多问题的,在安卓平台适配的可以有安卓2.3.7-4.2.2,不过大部分的机器都可以很流畅运行,包括一些主流的机器都没有问题,比如三星、联想、OPPO、索尼、HTC、LG、小米、MX。但是有一些比较麻烦的机器,比如华为的低端机,很容易造成闪退的问题。还有一个就是小米手机,小米用户是我们的主要用户群,但是用小米机器编辑图片的时候会经常造成白屏,目前我们不太清楚什么原因造成的。
后端开发的经验
现在我介绍一下后端开发的经验。C++和LUA开发模式,你要说它有优势也可以,有劣势也可以。首先,这种格式是单服单进程多线程架构模式,这种架构模式很方便、简单,因为只有一个内存需要维护。另外它可以支持热更新,我们在实际过程中会遇到网络环境差的问题,手机网络环境比PC网络环境差很多,2G的网络要比WIFI网络环境好,WIFI的网络环境要比3G的网络情况好,这是我们自己测试的情况,但也不是绝对的测试情况。在这种情况下经常会遇到收不到包、发不出去包,或者是收到包了,但是数据收不全等问题。
现在我们有两个个解决方案:
一个是建立发包收包的重发机制,从逻辑层面进行弥补。一旦出现发不出去包或者收不到包的情况,我们会不停的重试这个过程,重试一段时间都没有办法完成的话,就要再进行重发。
另外一个解决方案就是减少对于网络的依赖,数据包不要太大。因为很多自动回合的游戏,也许能达到30回合,这个时候包的数据是比较大的,可能会发生一次发不完的情况,需要多发几次,但是实际上增加了网络上传输的风险,所以如果能做成小包的话,就尽量做成小包。
以下是张成与现场观众的互动问答:
提问:我过C++开发,你们用什么工具进行调试?
回答:我们写了一个调试的功能,我们只要在Cocos2d-x里面就可以堆栈的信息,你要看到运行状态的值,其实我们大部分情况下会输出,这样调试会比较快一点,但是如果查不出来的话,我们也会用Cocos2d-x工具来调试。
提问:专门进行内存调试的时候,在虚拟机上面有几十兆,但是在真机上面只有几兆,以哪个为准呢?
回答:以真机为准。比如说安卓的调试,用安卓的虚拟器,它比真机的数据高很多,虚拟的那个数据不是真实的。
提问:后台架构的话,用LUA、以及用C、用C++对比的优点跟缺点是什么呢?
回答:LUA本身的优势就是它的本身语言层面就可以解决编程的问题,稍微会比C以及C++更快一点,性能上没有区别。