本次同享总结,起源于企鹅桌球项目,但是不仅仅限于项目本身。虽然基于Unity3D,很多东西同样适用于Cocos。本文从以下10大点进行阐述:
1.架构设计
2.原生插件/平台交互
3.版本和补丁
4.用脚本,还是不用?这是壹个问题
5.资源管理
6.性能优化
7.异常和Crash
8.适配和兼容
9.调试及开发工具
10.项目运营
1、架构设计
好的架构利用大规模项目的多人团队开发与代码管理,也利用查找错误与后期维护。
-
框架的挑选:需要根据团队、项目来进行挑选,没有最好的框架,只有最合适的框架。
-
框架的运用:统一的框架能规范我们的行为,互相之间可以相对平滑切换,可维护性大大提高。除此之外,还能代码解耦。例如StrangeIOC是壹个超轻量级与高度可扩展的控制反转(IoC)框架,专门为C#与Unity编写。已知企业内部运用StrangeIOC框架的游戏有:企鹅桌球、欢乐麻将、植物大战僵尸Online。
依赖注入(Dependency Injection,简称DI),是壹个重要的面给对象编程的法则来削减计算机程序的耦合问题。依赖注入还有壹个名字叫做控制反转(Inversion of Control,英文缩写为IoC)。依赖注入是这样壹个过程:由于某客户类只依赖于服务类的壹个接口,而不依赖于具体服务类,所以客户类只定义壹个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。即对象在被创建的时候,由壹个运行上下文环境或专门组件将其所依赖的服务类对象的引用传递向它。也可以说,依赖被注入到对象中。所以,控制反转是,关于壹个对象怎么获得他所依赖的对象的引用,这个责任的反转。
StrangeIOC采用MV反恐精英(数据模型?Model,展示视图?View,逻辑控制?Controller,服务Service)结构,通过消息/信号进行交互与通信。整个MV反恐精英框架跟flash的robotlegs基本一致。
-
数据模型 Model:主要负责数据的存储与基本数据处理
-
展示视图 View:主要负责UI界面展示与动画表现的处理
-
逻辑控制 Controller:主要负责业务逻辑处理,
-
服务Service:主要负责独立的网络收发请求等的一些功能。
-
消息/信号:通过消息/信号去解耦Model、View、Controller、Service这四种模块,他们之间通过消息/信号进行交互。
-
绑定器Binder:负责绑定消息处理、接口和实例对象、View和Mediator的对应关系。
-
MV反恐精英 Context:可以理解为MVC各个模块存在的上下文,负责MVC绑定与实例的创建工作。
企鹅桌球客户端项目框架
-
代码目录的组织:一般客户端用得相对多的MVC框架,如何划分目录?
-
先按业务功能划分,再按照?MVC?来划分。”蛋糕心语”就是运用的这种方法。
-
先按MVC划分,再按照业务功能划分。”D9″、”宝宝斗场”、”魔法花园”、”企鹅桌球”、”欢乐麻将”运用的这种方法。
根据运用习惯,可以自行挑选。单人主推”先按业务功能划分,再按照 MVC 来划分”,使得模块更聚焦(高内聚),第二种方法用多了发现随着项目的运营模块增多,没有第一种那么好维护。
Unity项目目录的组织:结合Unity规定的一些特殊的用途的文件夹,大家提议Unity项目文件夹组织方法如下。
其中,Plugins支持Plugins/{Platform}这样的命名规范:
-
Plugins/x86
-
Plugins/x86_64
-
Plugins/Android
-
Plugins/iOS
如果存在Plugins/{Platform},则加载Plugins/{Platform}目录下的文件,否则加载Plugins目录下的,也就是说,如果存在{Platform}目录,Plugins根目录下的DLL是不会加载的。
另外,资源组织采用分文件夹存储”成品资源”及”原料资源”的方法处理:防止无关资源参加打包,RawResource即原始资源,Resource即成品资源。当然并不限于RawResource这种形式,其他Unity规定的特殊文件夹都可以这样,例如Raw Standard Assets。
企业组件
-
msdk(sns、付款midas、推送灯塔、监控Bugly)
-
apollo
-
apollo voice
-
xlua
目前大家的企鹅桌球、四国军棋都接入了apollo,但是如果服务器不采用apollo框架,不提议客户端接apollo,而是直接接msdk减少二次封装信息的丢失与带来的错误,方便以后更新维护,并且减少导入无用的代码。
第三方插件选型
-
NGUI
-
DoTween
-
GIF
-
GAF
-
VectrosityScripts
-
PoolManager
-
Mad Level Manger
2、原生插件/平台交互
虽然大多时候运用Unity3D进行游戏开发时,只需要运用C#进行逻辑编写。但有时候不可避免的需要运用与编写原生插件,例如一些第三方插件只提供C/C++原生插件、复用已有的C/C++模块等。有一些功能是Unity3D实现不了,必须要调用Android/iOS原生接口,比如获得手机的硬件信息(UnityEngine.SystemInfo没有提供的部分)、调用系统的原生弹窗、手机震动等等
2.1C/C++插件
编写与运用原生插件的几个决定因素点:
创建C/C++原生插件
-
导出接口必须是C ABI-compatible函数
-
函数调用约定
在C#中标识C/C++的函数并调用
-
标识 DLL 中的函数。至少指定函数的名称与包含该函数的 DLL 的名称。
-
创建用于容纳 DLL 函数的类。可以运用现有类,为每一非托管函数创建单独的类,或者创建包含一组相关的非托管函数的壹个类。
-
在托管代码中创建原型。运用?DllImportAttribute?标识 DLL 与函数。?用?static?与?extern?修饰符标记方式。
-
调用 DLL 函数。像处理其他任何托管方式一样调用托管类上的方式。
在C#中创建回调函数,C/C++调用C#回调函数
-
创建托管回调函数。
-
创建壹个委托,并将其作为参数传递向?C/C++函数。平台调用会自动将委托转换为常见的回调格式。
-
确保在回调函数完成其工作之前,垃圾回收器不会回收委托。
那么C#和原生插件之间是怎么实现互相调用的呢?
1.将源码编译为托管模块;
2.将托管模块搭配为程序集;
3.加载公共语言运行时CLR;
4.执行程序集代码。
注:CLR(公共语言运行时,Common Language Runtime)与Java虚幻机一样也是壹个运行时环境,它负责资源管理(内存分配与垃圾收集),并保证应用与底层操作系统之间必备的分离。
为了提升平台的可靠性,以及为了达到面给事务的电子商务应用所标准的稳定性级别,CLR还要负责其他一些任务,比如监视程序的运行。按照.NET的说法,在CLR监视之下运行的程序属于”托管”(managed)代码,而不在CLR之下、直接在裸机上运行的应用或者组件属于”非托管”(unmanaged)的代码。
这几个过程我总结为下图:
图 .NET上的程序运行
回调函数是托管代码C#中的定义的函数,对回调函数的调用,实现从非托管C/C++代码中调用托管C#代码。那么C/C++是怎么调用C#的呢?大致分为2步,可以用下图表示:
将回调函数指针注册到非托管C/C++代码中(C#中回调函数指委托delegate)
调用注册过的托管C#函数指针
相相对托管调用非托管,回调函数方法稍微复杂一些。回调函数特别适合重复执行的任务、异步调用等情况下运用。
由上面的说明可以了解CLR提供了C#程序运行的环境,和非托管代码的C/C++交互调用也由它来完成。CLR提供两种用于和非托管C/C++代码进行交互的机制:
-
平台调用(Platform Invoke,简称PInvoke或者P/Invoke),它使托管代码能够调用从非托管DLL中导出的函数。
-
COM 互操作,它使托管代码能够通过接口和组件对象模型 (COM) 对象交互。思考跨平台性,Unity3D不运用这种方法。
平台调用依赖于元数据在运行时查找导出的函数并封送(Marshal)其参数。下图显示了这一过程。
注意:1.除涉及回调函数时以外,平台调用方式调用从托管代码流给非托管代码,而绝不会以相反方给流动。虽然平台调用的调用只能从托管代码流给非托管代码,但是数据仍然可以作为输入参数或输出参数在两个方给流动。2.图中DLL表示动态库,Windows平台指.dll文件、Linux/Android指.so文件、Mac OS X指.dylib/framework文件、iOS中只能运用.a。后文都运用DLL代指,并且DLL运用C/C++编写。
当”平台调用”调用非托管函数时,它将依次执行以下操作:
-
查找包含该函数的DLL。
-
将该DLL加载到内存中。
-
查找函数在内存中的地址并将其参数推到堆栈上,以封送所需的数据(参数)。
-
将控制权转移向非托管函数。
注意:只在首次调用函数时,才会查找与加载 DLL 并查找函数在内存中的地址。iOS中运用的是.a已经静态打包到最终执行文件中。
2.2Android插件
Java同样提供了这样壹个扩展机制JNI(Java Native Interface),能够和C/C++互相通信。
注:
-
JNI wiki这里不深入说明JNI,有兴趣的可以自行去研究。如果你还不了解JNI也不用怕,就像Unity3D运用C/C++库一样,用起来还是相对简单的,只需要了解这个东西即可。并且Unity3D对C/C++桥接器这块做了封装,提供AndroidJNI/AndroidJNIHelper/AndroidJavaObject/AndroidJavaClass/AndroidJavaProxy方便运用等,具体运用后面在说明。JNI提供了若干的API实现了Java与其他语言的通信(主要是C&C++)。从Java1.1最初,JNI要求成为java平台的一部分,它允许Java代码与其他语言写的代码进行交互,保证本地代码能工作在任何Java?虚幻机环境下。”
-
作为姿势扩展,提一下Android Java虚幻机。Android的Java虚幻机有2个,最最初是Dalvik,后面Google在Android 4.4系统新增一种应用运行玩法ART。ART和Dalvik 之间的主要不同差异是其具有提前 (AOT) 编译玩法。 根据 AOT 概念,设备安装应用时,DEX 字节代码转换仅进行一次。 相比于 Dalvik,这样可实现真实的优势 ,因为 Dalvik 的即时 (JIT) 编译方式需要在每次运行应用时都进行代码转换。下文中用Java虚幻机代指Dalvik/ART。
C#/Java都可以与C/C++通信,那么通过编写壹个C/C++模块作为桥接,就使得C#和Java通信成为了也许,如下图所示:
注:C/C++桥接器本身跟Unity3D没有直接关系,不属于Android与Unity3D,图中放在Unity3D中是为了代指libunity.so中实现的桥接器以表示真正的情况。
通过JNI既可以用于Java代码调用C/C++代码,也可用于C/C++代码和Java(Dalvik/ART虚幻机)的交互。JNI定义了2个决定因素概念/结构:JavaVM、JNIENV。JavaVM提供虚幻机创建、销毁等操作,Java中壹个进程可以创建多个虚幻机,但是Android壹个进程只能有壹个虚幻机。JNIENV是线程相关的,对应的是JavaVM中的当前线程的JNI环境,只有附加(attach)到JavaVM的线程才有JNIENV指针,通过JNIE暗夜猎手指针可以获得JNI功能,否则不能够调用JNI函数。
C/C++要访问的Java代码,必须要能访问到Java虚幻机,获得虚幻机有2中方式:
-
在加载动态链接库的时候,JVM会调用JNI_OnLoad(JavaVM* jvm, void* reserved),第壹个参数会传入JavaVM指针。
-
在C/C++中调用JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args)创建JavaVM指针
所以,大家只需要在编写C/C++桥接器so的时候定义JNI_OnLoad(JavaVM* jvm, void* reserved)方式即可,然后把JavaVM指针保存起来作为上下文运用。
获得到JavaVM之后,还不能直接拿到JNI函数去获得Java代码,必须通过线程关联的JNIENV指针去获得。所以,作为壹个好的开发习惯在每次获得壹个线程的JNI相关功能时,先调用AttachCurrentThread;又或者每次通过JavaVM指针获得当前的JNIENV:java_vm->GetEnv((void**)&jni_env,?version),一定是已经附加到JavaVM的线程。通过JNIENV可以获得到Java的代码,例如你想在本地代码中访问壹个对象的字段(field),你可以像下面这样做:
1.对于类,运用jni_env->FindClass获取类对象的引用
2.对于字段,运用jni_env->GetFieldId获取字段ID
3.运用对应的方式(例如jni_env->GetIntField)获得字段的值
类似地,要调用壹个方式,你step1.得获取壹个类对象的引用obj,step2.是方式methodID。这些ID通常是指给运行时内部数据结构。查找到它们需要些字符串相对,但一旦你实际去执行它们获取字段或者做方式调用是特别快的。step3.调用jni_env->CallVoidMethodV(obj,methodID,args)。
从上面的示例代码,大家可以看出运用原始的JNI方法去和Android(Java)插件交互是多的繁琐,要自己做太多的事情,并且为了性能需要自己思考缓存查询到的方式ID,字段ID等等。幸运的是,Unity3D已经为大家封装好了这些,并且思考了性能优化。Unity3D主要提供了一下2个级别的封装来帮助高效编写代码:
注:Unity3D中对应的C/C++桥接器包含在libunity.so中。
-
Level 1:AndroidJNI、AndroidJNIHelper,原始的封装等于于大家上面自己编写的C# Wrapper。AndroidJNIHelper与AndroidJNI自动完成了很多任务(指找到类定义,构造方式等),并且运用缓存使调用java速度最快。AndroidJavaObject与AndroidJavaClass基于AndroidJNIHelper与AndroidJNI创建,但在处理自动完成部分也有很多自己的逻辑,这些类也有静态的版本,用来访问java类的静态成员。
-
Level 2:AndroidJavaObject、AndroidJavaClass、AndroidJavaProxy,这个3个类是基于Level1的封装提供了更高层级的封装运用起来更简单,这个在第三部分详细说明。
2.3iOS插件
iOS编写插件比Android要简单很多,因为Objective-C也是 C-compatible的,完全兼容要求C语言。这些就可以特别简单的包一层 extern “c”{},用C语言封装调用iOS功能,暴露向Unity3D调用。并且可以跟原生C/C++库一样编成.a插件。C#和iOS(Objective-C)通信的原理跟C/C++完全一样:
除此之外,Unity iOS支持插件自动集成方法。全部位于Asset/Plugings/iOS文件夹中后缀名为.m , .mm , .c , .cpp的文件都将自动并入到已生成的Xcode项目中。然而,最终编进执行文件中。后缀为.h的文件不能被包含在Xcode的项目树中,但他们将出今年目标文件系统中,从而使.m/.mm/.c/.cpp文件编译。这样编写iOS插件,除了需要对iOS Objective-C有一定知道之外,和C/C++插件没有差别,反而更简单。
3、版本和补丁
任何游戏(端游、手机游戏)都应该提供游戏内升级的路径。一般游戏分为全量升级/整包升级、增量升级、资源升级。
全量
android游戏内完整安装包下载(ios跳转到AppStore下载)
增量:主要指android省流量升级
-
可以运用bsdiff生成patch包
-
应用宝也提供增量升级sdk可供接入
资源
Unity3D通过运用AssetBundle即可实现动态升级资源的功能。
手机游戏在实现这块时需要注意的几点:
1.游戏发布出一定要提供游戏内升级的路径。即使是删掉测试,保不准这期间需要进行资源或者BUG修复升级。很多玩家并不了解怎么升级,而且Android手机应用分发平台多样,分发平台本身也不会跟官方同步升级(非常是小的分发平台)。
2.升级功能要提供强制升级、非强制升级设置化选项,并指定哪些版本可以不强更,哪些版本必须强更。
3.当游戏提供非强制升级功能之后,现网一定会存在多个版本。如果需要针对不同版本做不同的升级,例如设置文件A针对1.0.0.1修改了一项,针对1.0.0.2修改了另一项,2个版本需要分别升级对应的修改,需要自己实现升级攻略IIPS不提供这个功能。当需要复杂的升级攻略,主推自己编写升级服务器与客户端逻辑,不运用iips组件(其实自己实现也很简单)。
没有运营经验的人会挑选二进制,认为二进制安全、更小,这对端游/手机游戏外网只存在壹个版本的游戏适合,对一般不强升版本的手机游戏并不适合,反而会对升级与维护带来很大的麻烦。
4.设置运用XML或者JSON等文本格式,更利于多版本的兼容与升级。最最初企鹅桌球客户端运用的二进制格式(由excel转换而来),但是随着运营设置格式需要增加字段,这样老版本程序就分析不了新的二进制数据,向兼容与升级带来了很大的麻烦。这样就标准上面提到的针对不同步做不同升级,又或者设置一最初就预留好足够的扩展项,其实不管如何预留扩展也很难跟上需求的变化,而且一最初会把设置表复杂化但是其实只有一张或者几张才会变更结构。
5.iOS版本的送审版本需要连接特定的包含新内容的服务器,现网服务器还不包含新内容。送审通过之后,上架游戏现网服务器会进行升级,iOS版本需要连接现网服务器而非送审服务器,但是这期间又不能修改客户度,这个切换需要通过服务器下发开关进行控制。例如通过指定送审的iOS游戏版本号,客户端判断本地版本号是否为送审版本,如果是连接送审服务器,否则连接现网服务器。
4、用脚本,还是不用?这是壹个问题
方便升级,减少Crash(非常是运用C++的cocos引擎)
通过上面一节【版本和补丁】了解要实现代码升级是特别困难的,正式这个原因客户端开发的压力是相对大的,如果出现了相对严重的BUG必须发强制升级版本,运用脚本可以化解这个问题。
由于Unity3D手机游戏升级成本相对大,而且目前企鹅桌球标准不能强制升级,这导致新版本的活动主题覆盖率提高相对慢、出现问题之后难以修复。针对这个情况,思考引入lua进行活动主题开发,后续发布活动主题及修复bug只需要发布lua资源,进行资源升级即可,大大降低了发布与修复问题的成本。
可选方案还有运用Html5进行活动主题开发,目前游戏中已经预埋了Html5活动主题入口,并且已经用来发过”玩家调查”、”企鹅TAB宣传”等。但是和lua对比,不能做到和Unity3D的深度融合,体验不如运用lua,例如不能操作游戏中的ui、不能完成复杂界面的制作、不能复用已有的功能、玩家付费充值跟已有的也会有差别
游戏脚本之王——Lua
在企业内部魔方相对喜爱用lua,火隐忍者(手机游戏)unity+ulua,全民水浒cocos2d-x+lua等等都有运用lua进行开发。大家可以运用企业内部的xlua组件,也可以运用ulua、UniLua等等。
5、资源管理
5.1资源管理器
-
业务不容直接运用引擎或者系统原生接口,而是封装壹个资源管理器负责:资源加载、卸载
-
兼容Resource.Load和AssetBundle资源互相变更需求,开发期间运用Resource.Load方法而不必打AB包效率更高
-
加载资源时,不管是同步加载还是异步加载,最好是运用异步编码方法(回调函数或者消息通知机制)。如果哪一天资源由本地加载改为从服务器按需加载,而游戏中的逻辑都是同步方法编码的,改起来将特别痛苦。其实异步编码方法很简单,不比同步方法复杂。
5.2资源类型
图片/纹理(对性能、包体影响最大因素)
音频
-
背景音乐,企鹅桌球运用.ogg/.mp3
-
音效,企鹅桌球运用.wav
数据
动画/特效
5.3图片-文件格式和纹理格式
常用的图像文件格式有BMP,TGA,JPG,GIF,PNG等;
常用的纹理格式有R5G6B5,A4R4G4B4,A1R5G5B5,R8G8B8, A8R8G8B8等。
文件格式是图像为了存储信息而运用的对信息的特殊编码方法,它存储在磁盘中,或者内存中,但是并不能被GPU所识别,因为以给量计算见长的GPU对于这些复杂的计算无能为力。这些文件格式当被游戏读入后,还是需要经过CPU解压成R5G6B5,A4R4G4B4,A1R5G5B5,R8G8B8, A8R8G8B8等像素格式,再传送到GPU端进行运用。
纹理格式是能被GPU所识别的像素格式,能被快速寻址并采样。举个例子,DDS文件是游戏开发中常用的文件格式,它内部可以包含A4R4G4B4的纹理格式,也可以包含A8R8G8B8的纹理格式,甚至可以包含DXT1的纹理格式。在这里DDS文件有点容器的意味。OpenGL ES 2.0支持以上提到的R5G6B5,A4R4G4B4,A1R5G5B5,R8G8B8,A8R8G8B8等纹理格式,其中 R5G6B5,A4R4G4B4,A1R5G5B5每个像素占用2个字节(BYTE),R8G8B8每个像素占用3个字节,A8R8G8B8每个像素占用 4个字节。
基于OpenGL ES的压缩纹理有常见的如下几种实现:
1)ETC1(Ericsson texture compression),ETC1格式是OpenGL ES图形要求的一部分,并且被全部的Android设备所支持。
2)PVRTC (PowerVR texture compression),支持的GPU为Imagination Technologies的PowerVR SGX系列。
3)ATITC (ATI texture compression),支持的GPU为Qualcomm的Adreno系列。
4)S3TC (S3 texture compression),也被称为DXTC,在电脑上广泛被运用,但是在移动设备上还是属于新鲜事物。支持的GPU为NVIDIA Tegra系列。
5.4资源工具
有了规范就可以做工具检查,从源头到打包
6、性能优化
掉帧主要针对GPU与CPU做解析;内存占用大主要针对美术资源,音效,设置表,缓存等解析;卡顿也需要对GPU与CPU峰值解析,另外IO或者GC也易导致。
6.1工欲善其事,必先利其器
-
Unity Profiler
-
XCode?instruments
-
Qualcomm?Adreno Profiler
-
NVIDIA PerfHUD ES Tegra
6.2CPU:完美守则减少计算
复用,UIScrollView Item复用,避免频繁创建销毁对象
缓存,例如Transform
运算裁剪,例如碰撞检测裁剪
-
粗略碰撞检测(划分空间——二分/四叉树/八叉树/网格等,降低碰撞检测的数量)
-
精确碰撞检测(检查候选碰撞结果,进而确定对象是否真正发生碰撞)
-
休眠机制:避免SIM静止的球
-
逻辑帧和渲染帧分离
-
分帧处理
-
异步/多线程处理
6.3GPU:完美守则减少渲染
-
纹理压缩
-
批处理减少DrawCall(unity-Static Batching与Dynamic Batching,cocos SpriteBatchNode)
-
减少无效/不必备绘制:屏幕外的裁剪,Flash脏矩阵算法,
-
LOD/特效分档
-
NGUI动静分离(UIPanel.LateUpdate的消耗)
-
控制人物骨骼数、模型面数/顶点数
-
降帧,并非全部场景都需要60帧(企鹅桌球游戏场景60帧,其他场景30帧;天天酷跑,在最初游戏前,FPS被限制为30,游戏最初之后FPS才为60。天天飞车的FPS为30,但是当用户一段时间不点击界面后,FPS自动降)
6.4内存:完美守则减少内存分配/碎片、及时释放
-
纹理压缩-Android ETC1、iOS PVRTC 4bpp、windows DXT5
-
对象池-PoolManager
-
合并空闲图集
-
UI九宫格
-
删除不用的脚本(也会占用内存)
6.5IO:完美守则减少/异步io
-
资源异步/多线程加载
-
预加载
-
文件压缩
-
合理规划资源合并打包,并非texturepacker打包成大图集一定好,会增加文件io时间
6.6网络:其实也是IO的一种
运用单线程——共用UI线程,通过事件/UI循环驱动;还是多线程——单独的网络线程?
单线程:由游戏循环(事件)驱动,单线程玩法比运用多线程玩法开发、维护简单很多,但是性能比多线程要差一些,所以在网络IO的时候,需要注意别阻塞到游戏循环。介绍,如果网络IO不复杂的情况下,主推运用该玩法。
-
在UI线程中,别调用也许阻塞的网络函数,优先思考非阻塞IO
-
这是网络开发者经常犯的错误之一。比如:做壹个简单如 gethostbyname 的调用,这个操作在小范围中不会存在任何问题,但是在有些情况中现实世界的玩家却会因此阻塞数分钟之久!如果你在 GUI 线程中调用这样壹个函数,对于用户来说,在函数阻塞时,GUI 一直都处于 frozen 或者 hanged 状态,这从用户体验的角度是绝对不允许的。
多线程:单独的网络线程,运用独立的网络线程有壹个特别明显的好处,主线程可以将脏活、累活交向网络线程做使得UI更流畅,例如消息的编解码、加解密工作,这些都是特别耗时的。但是运用多线程,向开发与维护带来一定成本,并且如果没有一定的经验写出来的网络库不那么稳定,容易出错,甚至导致游戏崩溃。下面是几点注意事项:
千万千万别在网络线程中,回调主线程(UI线程)的回调函数。而是网络线程将数据预备好,让主线程主动去取,亦或者说网络线程将网络数据作为壹个事件驱动主线程去取。当年我在用Cocos2d-x + Lua做魔法花园的手机demo时,就采用的多线程玩法,开始在网络线程直接调用主线程回调函数,经常会导致莫名其妙的Crash。因为网络线程中没有渲染所必须的opengl上下文,会导致渲染出问题而Crash。
6.7包大小
-
运用压缩格式的纹理/音频
-
尽量不容运用System.Xml而运用较小的Mono.Xml
-
启用Stripping来减小库的大小
-
Unity strip level(strip by byte code)
-
Unity3D输出APK,关掉X86架构
-
iOS?Xcode?strip开启
6.8耗电
下面影响耗电的几个因素与影响度摘自企业内部的一篇文章。
7、异常和Crash
7.1防御式编程
非法的输入中保护你的程序
断言
防不胜防,不管怎么防御总错过手的时候,这就需要异常捕获与上报。
7.2异常捕获
异常捕获已经有很多第三组件可供接入,这里不说明组件的而接入,而是简单谈一下异常捕获的原理。
由于很多错误并不是发生在开发工作者调试阶段,而是在用户或测试工作者运用阶段;这就需要相关代码维护工作者对于程序异常捕获收集现场信息。异常和Crash的监控与上报,这里不说明Bugly的运用,按照apollo或者msdk的文档接入即可,没有太多可以说的。这里主要透过Bugly说明手机游戏的几类异常的捕获与解析:
Unity3D C#层异常捕获:相对简单运用Application.RegisterLogCallback/Application.RegisterLogCallbackThreaded(在壹个新的线程中调用委托)注册回调函数。非常注意:保证项目中只有壹个Application.RegisterLogCallback注册回调,否则后面注册的会覆盖前面注册的回调!回调函数中stackTrace参数包异常调用栈。
public void HandleLog(string logString, string stackTrace, LogType type)
{
if (logString == null || logString.StartsWith(cLogPrefix))
{
return;
}
ELogLevel level = ELogLevel.Verbose;
switch (type)
{
case LogType.Exception:
level = ELogLevel.Error;
break;
default:
return;
}
if (stackTrace != null)
{
Print(level, ELogTag.UnityLog, logString + “n” + stackTrace);
}
Android Java层异常捕获
try…catch显式的捕获异常一般是不引起游戏Crash的,它又称为编译时异常,即在编译阶段被处理的异常。编译器会强制程序处理全部的Checked异常,因为Java认为这类异常都是可以被处理(修复)的。如果没有try…catch这个异常,则编译出错,错误提示类似于”Unhandled exception type?xxxxx”。
UnChecked异常又称为运行时异常,由于没有相应的try…catch处理该异常对象,所以Java运行环境将会终止,程序将退出,也就是大家所说的Crash。那为啥子不会加在try…catch呢?
-
无法将全部的代码都加上try…catch
-
UnChecked异常通常都是较为严重的异常,或者说已经破坏了运行环境的。比如内存地址,即使大家try…catch住了,也不能明确了解怎么处理该异常,才能保证程序接下来的运行是正确的。
Uncaught异常会导致应用程序崩溃。那么当崩溃了,大家是否可以做些啥子呢,就像Application.RegisterLogCallback注册回调打印日志、上报服务器、弹窗提示用户?Java提供了壹个接口向大家,可以完成这些,这就是UncaughtExceptionHandler,该接口含有壹个纯虚函数:
public abstract void uncaughtException (Thread thread, Throwableex)
Uncaught异常发生时会终止线程,此时,系统便会通知UncaughtExceptionHandler,告知它被终止的线程以及对应的异常,然后便会调用uncaughtException函数。如果该handler没有被显式配置,则会调用对应线程组的默认handler。如果大家要捕获该异常,必须实现大家自己的handler,并通过以下函数进行配置:
public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)
非常注意:多次调用setDefaultUncaughtExceptionHandler配置handler,后面注册的会覆盖前面注册的,以最后一次为准。实现自定义的handler,只需要继承UncaughtExceptionHandler该接口,并实现uncaughtException方式即可。
static class MyCrashHandler implements UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread thread, final Throwable throwable) {
// Deal this exception
}
}
在任何线程中,都可以通过setDefaultUncaughtExceptionHandler来配置handler,但在Android应用程序中,全局的Application与Activity、Service都同属于UI主线程,线程名称默认为”main”。所以,在Application中应该为UI主线程添加UncaughtExceptionHandler,这样整个程序中的Activity、Service中出现的UncaughtException事件都可以被处理。
捕获Exception之后,大家还需要了解崩溃堆栈的信息,这样有助于大家解析崩溃的原因,查找代码的Bug。异常对象的printStackTrace方式用于打印异常的堆栈信息,根据printStackTrace方式的输出结果,大家可以找到异常的源头,并跟踪到异常一路触发的过程。
public static String getStackTraceInfo(final Throwable throwable) {
String trace = “”; try {
Writer writer = new StringWriter;
PrintWriter pw = new PrintWriter(writer);
throwable.printStackTrace(pw);
trace = writer.toString;
pw.close;
} catch (Exception e) { return “”;
} return trace;
}
Android Native Crash:前面大家了解可以编写与运用C/C++原生插件,除非C++运用try…catch捕获异常,否则一般会直接crash,通过捕获信号进行处理。
iOS 异常捕获:
跟Android、Unity类似,iOS也提供NSSetUncaughtExceptionHandler?来做异常处理。
#import “CatchCrash.h”
@implementation CatchCrash
void uncaughtExceptionHandler(NSException *exception)
{
// 异常的堆栈信息
NSArray *stackArray = [exception callStackSymbols];
// 出现异常的原因
NSString *reason = [exception reason];
// 异常名称
NSString *name = [exception name];
NSString *exceptiOnInfo= [NSString stringWithFormat:@”Exception reason:%@nException name:%@nException stack:%@”,name, reason, stackArray];
NSLog(@”%@”, exceptionInfo);
NSMutableArray *tmpArr = [NSMutableArray arrayWithArray:stackArray];
[tmpArr insertObject:reason atIndex:0];
[exceptionInfo writeToFile:[NSString stringWithFormat:@”%@/Documents/error.log”,NSHomeDirectory()] atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
@end
但是内存访问错误、重复释放等错误引起崩溃就无能为力了,因为这种错误它抛出的是信号,所以还必须要专门做信号处理。
windows crash:同样windows提供SetUnhandledExceptionFilter函数,配置顶尖一级的异常处理函数,当程序出现任何未处理的异常,都会触发你配置的函数里,然后在异常处理函数中获得程序异常时的调用堆栈、内存信息、线程信息等。
8、适配和兼容
8.1UI适配
-
锚点(UIAnchor、UIWidgetAnchor属性)
-
NGUI UIRoot统一配置缩放比例
-
UIStretch
8.2兼容
-
shader兼容:例如if语句有的机型支持不好,Google nexus 6在shader中运用了if就会crash
-
字体兼容:android复杂的环境,有的手机厂商与rom会对字体进行优化,去掉android默认字体,如果不打包字体会不现实中文字
9、调试及开发工具
9.1日志及跟踪
事实证明,打印日志(printf调试法)是特别有效的方式。壹个好用的日志调试,必要以下几个功能:
-
日志面板/控制台,格式化输出
-
冗长级别(verbosity level):ERROR、WARN、INFO、DEBUG
-
频道(channel):按功能等进行模块划分,如网络频道只接收/显示网络模块的消息,频道提议运用枚举进行命名。
-
日志同时会输出到日志文件
-
日志上报
9.2调试用绘图工具
调试绘图用工具指开发及调试期间为了可视化的绘图用工具,如企鹅桌球开发调试时会运用VectrosityScripts可视化球桌的物理模型(实际碰撞线)帮助调试。这类工具可以节省大量时间及快速定位问题。通常调试用绘图工具包含:
-
支持绘制基本图形,如直线、球体、点、坐标轴、包围盒等
-
支持自定义设置,如颜色、粒度(线的粗细/球体半径/点的大小)等
9.3游戏内置菜单/作弊工具
在开发调试期间提供游戏进行中的一些设置选项及作弊工具,以方便调试与提升效率。例如企鹅桌球游戏中提供:
-
游戏内物理引擎参数调整菜单;
-
修改球杆瞄准线长度/反射线数量、修改签到奖励领取天数等作弊工具
注意游戏内的全部开发调试用的工具,都需要通过编译宏开关,保证发布版本不会把工具代码包含进去。
9.4Unity扩展
Untiy引擎提供了特别强大的编辑器扩展功能,基于Unity Editor可以实现特别多的功能。企业内部、外部都有特别的开源扩展可用
企业外部,如GitHub上的:
UnityEditor-MiniExtension
Unity-Resource-Checker
UnityEditorHelper
MissingReferencesUnity
Unity3D-ExtendedEditor
…
企业内部:
10、项目运营
自动构建
版本号——主版本号.特性版本号.修正版本号.构建版本号
[构建版本号]应用分发平台更新判断基准
自动构建
企业内部接入SODA即可,提议搭建自己的构建机,开发期间每天N Build排队会死人的,另外也可以搭建自己的搭建构建平台
统计上报
Tlog上报
-
玩家转化决定因素流程统计(重要)
-
Ping统计上报
-
游戏业务的统计上报(例如桌球球局相关的统计上报)
-
灯塔自定义上报
运营模板
上线前的checklist
项目 |
要点 |
介绍 |
指标 |
灯塔上报 |
1. 灯塔自带统计信息2. 自定义信息上报 |
灯塔里面包含很多统计数据,需要检查是否ok |
1. 版本/渠道分布 2. 运用频率统计 3. 留存统计(1天留存、3天留存、7天留存、14天留存) 4. 用户结构统计(有效用户、沉默用户、流失用户、回流用户、更新用户、新增用户) 5. 硬件统计(机型+版本、分辨率、操作系统、内存、cpu、gpu) 6. Crash统计(Crash版本、Crash硬件、Crash次数等) 等等 |
信鸽推送 |
能够针对单个玩家,全部玩家推送消息 |
||
米大师付款 |
正常付款 |
||
安全组件 |
1. TSS组件接入 2. 隐藏内部符号表:C++开发的代码运用strip编绎选项,抹除程序的符号 3. 决定因素数据加密,如影子变量+异或加密算法 |
根据安全中心提供的文档完成全部项 |
接入安全组件,并通过安全中心的验收 |
稳定性 |
crash率 |
用户crash率:发生CRASH的用户数/运用用户数启动crash率:启动5S内发生crash用户数/运用用户数 |
低于3% |
弱网络 |
断线重连思考,缓存消息,重发机制等等 |
客户端的核心场景必须有断线重连机制,并在有网络抖动、延时、丢包的网络场景下,客户端需达到以下标准: 一. 不能出现以下现象: 1、游戏中不能出现收支不等、客户端卡死/崩溃等异常情况; 2、游戏核心功能(如登录、单局、付款等)不能有导致游戏无法正常进行的UI、交互问题; 3、不能有损害玩家利益或可被玩家额外获利的问题; 4、需要有合理的重连机制,避免每次重连都返回到登录界面。 二. 需要对延时的情况有相应的提示 |
|
兼容性 |
通过适配测试 |
||
游戏升级 |
1. 整包升级2. 增量升级 |
非常介绍:iOS送审版本支持连特定环境,和正式环境不同差异开,需要通过服务器开关控制 |
|
性能 |
内存、CPU、帧率、流量、安装包大小 |
【内存占用标准】 Android平台:在对应档次客户端超低设置以上,均需满足以下内存消耗指标(PSS): 1档机型指标:顶尖PSS<=300MB (PSS高于这个要求会影响28%用户的体验,约1800万) 2档机型指标:顶尖PSS<=200MB(PSS高于这个要求会影响45%用户的体验,约3000万) 3档机型指标:顶尖PSS<=150MB(PSS高于这个要求会影响27%用户的体验,约1800万) iOS平台:在对应档次客户端超低设置以上,均需满足以下内存消耗指标(PSS): 1档机型指标:消耗内存(real mem)不大于250MB(高于这个要求会影响53%用户的体验,约1900万) 2档机型指标:消耗内存(real mem)不大于200MB(高于这个要求会影响47%用户的体验,约1700万) 【CPU占用标准】 Android平台:CPU占用(90%)小于60% iOS平台:CPU占用(90%)小于80% 【帧率标准】 1档机型(CPU为四核1.4GHZ,RAM为2G)或以上机型:游戏核心模式中,最小FPS应不小于25帧/秒 2档机型(CPU为两核1.1GHZ,RAM为768M)或以上机型:游戏核心模式中,最小FPS应不小于25帧/秒 3档机型(CPU为1GHZ,RAM为768M)或以上机型:游戏核心模式中,最小FPS应不小于18帧/秒 【流量消耗标准】 游戏核心模式流量消耗情况(非一次性消耗)应满足以下条件: 1.对于分局的游戏场景,单局消耗流量不超过200KB 2.对于不分局游戏场景或流量和局时有关的场景,10分钟消耗流量不超过500KB |