为什么手游都用unity3d

2019-08-09 15:32:51 qq_37310110 阅读数 1285

欢迎加入Unity业内qq交流群:956187480

qq扫描二维码加群


本文从以下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采用MVCS(数据模型?Model,展示视图?View,逻辑控制?Controller,服务Service)结构,通过消息/信号进行交互和通信。整个MVCS框架跟flash的robotlegs基本一致,(忽略语言不一样)详细的参考<http://www.cnblogs.com/skynet/archive/2012/03/21/2410042.html>

  • 数据模型 Model:主要负责数据的存储和基本数据处理
  • 展示视图 View:主要负责UI界面展示和动画表现的处理
  • 逻辑控制 Controller:主要负责业务逻辑处理,
  • 服务Service:主要负责独立的网络收发请求等的一些功能。
  • 消息/信号:通过消息/信号去解耦Model、View、Controller、Service这四种模块,他们之间通过消息/信号进行交互。
  • 绑定器Binder:负责绑定消息处理、接口与实例对象、View与Mediator的对应关系。
  • MVCS 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#与原生插件之间是如何实现互相调用的呢?在弄清楚这个问题之前,我们先看下C#代码(.NET上的程序)的执行的过程:(更详细一点的介绍可以参见我之前写的博客:http://www.cnblogs.com/skynet/archive/2010/05/17/1737028.html
1.将源码编译为托管模块;
2.将托管模块组合为程序集;
3.加载公共语言运行时CLR;
4.执行程序集代码。
注:CLR(公共语言运行时,Common Language Runtime)和Java虚拟机一样也是一个运行时环境,它负责资源管理(内存分配和垃圾收集),并保证应用和底层操作系统之间必要的分离。
为了提高平台的可靠性,以及为了达到面向事务的电子商务应用所要求的稳定性级别,CLR还要负责其他一些任务,比如监视程序的运行。按照.NET的说法,在CLR监视之下运行的程序属于"托管"(managed)代码,而不在CLR之下、直接在裸机上运行的应用或者组件属于"非托管"(unmanaged)的代码。这几个过程我总结为下图:

回调函数是托管代码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指针,通过JNIEVN指针可以获取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活动入口,并且已经用来发过"玩家调查"、"腾讯棋牌宣传"等。但是与lua对比,不能做到与Unity3D的深度融合,体验不如使用lua,例如不能操作游戏中的ui、不能完成复杂界面的制作、不能复用已有的功能、玩家付费充值跟已有的也会有差异

游戏脚本之王——Lua

在公司内部魔方比较喜欢用lua,火隐忍者(手游)unity+ulua,全民水浒cocos2d-x+lua等等都有使用lua进行开发。我们可以使用公司内部的xlua组件,也可以使用ulua、UniLua等等。


5、资源管理


a.资源管理器

  • 业务不要直接使用引擎或者系统原生接口,而是封装一个资源管理器负责:资源加载、卸载
  • 兼容Resource.Load与AssetBundle资源互相变更需求,开发期间使用Resource.Load方式而不必打AB包效率更高
  • 加载资源时,不管是同步加载还是异步加载,最好是使用异步编码方式(回调函数或者消息通知机制)。如果哪一天资源由本地加载改为从服务器按需加载,而游戏中的逻辑都是同步方式编码的,改起来将非常痛苦。其实异步编码方式很简单,不比同步方式复杂。

b.资源类型

图片/纹理(对性能、包体影响最大因素)
音频

  • 背景音乐,腾讯桌球使用.ogg/.mp3
  • 音效,腾讯桌球使用.wav

数据

  • 文本
  • 二进制

动画/特效
c.图片-文件格式与纹理格式

常用的图像文件格式有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,在PC上广泛被使用,但是在移动设备上还是属于新鲜事物。支持的GPU为NVIDIA Tegra系列。

d.资源工具
有了规范就可以做工具检查,从源头到打包

  • 资源导入检查
  • 资源打包检查

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
运算裁剪,例如碰撞检测裁剪

  • 粗略碰撞检测(划分空间——二分/四叉树/八叉树/网格等,降低碰撞检测的数量)
  • 精确碰撞检测(检查候选碰撞结果,进而确定对象是否真实发生碰撞)
  • 休眠机制:避免模拟静止的球
  • 逻辑帧与渲染帧分离
  • 分帧处理
  • 异步/多线程处理

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
公司内部:
TUT、BeautyUnity、UnityDependencyBy


10、项目运营


自动构建

版本号——主版本号.特性版本号.修正版本号.构建版本号

[构建版本号]应用分发平台升级判断基准

自动构建

  • Android
  • iOS —?XUPorter

公司内部接入SODA即可,建议搭建自己的构建机,开发期间每日N Build排队会死人的,另外也可以搭建自己的搭建构建平台

统计上报
Tlog上报

  • 玩家转化关键步骤统计(重要)
  • Ping统计上报
  • 游戏业务的统计上报(例如桌球球局相关的统计上报)
  • 灯塔自定义上报

运营模板

  • 配置化
  • 服务器动态下发
  • CDN拉取图片并缓存

上线前的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


欢迎加入Unity业内qq交流群:956187480

qq扫描二维码加群

2019-03-18 17:40:22 zhangmiaoping23 阅读数 2135

重点知识:

1. \assets\bin\Data\Managed\Assembly-CSharp.dll扔进Reflector+reflexil环境

2.libs\libmono.so的mono_image_open_from_data_with_name函数


MonoImage *mono_image_open_from_data_with_name (char *data, guint32 data_len, gboolean need_copy, MonoImageOpenStatus *status, gboolean refonly, const char *name);

转:https://bbs.pediy.com/thread-226135.htm

最近找乐子破解了点手游玩玩,顺便分享一下,目标APK--发条XX。

1、大致分析

拿到APK先看看libs和assets,发现有libmono.so以及\assets\bin\Data\Managed\Assembly-CSharp.dll,可以确定是unity3d手游,可以尝试修改 Assembly-CSharp.dll来破解游戏,不过发条英雄的这个dll加密了,所以需要先解密。

2、解密dll

把 libmono.so扔进ida,找到mono_image_open_from_data_with_name函数,F5查看发现了解密代码:

在这个函数先判断加载的dll是否包含ssem字符串,接着判断前两个字节是否等于82(0x52),77(0x4d),因此可以确定下面的for循环是解密代码,比较简单,抄下来自己写一个解密程序(附件有一份),然后对这个 Assembly-CSharp.dll进行解密。

3、修改dll

把 Assembly-CSharp.dll扔进Reflector+reflexil环境进行修改(不会的同学百度学习一下),找到关键函数get_DamageValue,直接改成返回100W,然后攻击就变成了100W。

不过这个时候怪物攻击也是100W,因此再找到Player的applyDamage函数,修改这个函数直接ret,然后玩家就无敌了。

最后保存为一个新的dll并替换掉之前APK里面的 Assembly-CSharp.dll,解密后的dll头部不等于82(0x52),77(0x4d),因此so可以直接加载。

4、运行

重新签名,打包成新的APK,安装运行,一刀100W。

5、结束语

服务器信赖客户端的计算结果就导致这种情况,不过服务器加强数据校验也能检测出来。有兴趣的同学可以去改改dll实现竞技场百战百胜。

6、附件

decrypt.c

#include <stdio.h>
#include <elf.h>
#include <unistd.h>
#include <fcntl.h>
#include <error.h>
#include <errno.h>
#include <stdlib.h>

typedef char _BYTE;

int main(int argc, const char **argv) {
	
    if(argc != 3) {
    printf("usage:%s <in> <out>\n", argv[0]);
    return -1;
    }  


    FILE *fpDll;  
    fpDll = fopen(argv[1], "r");  
    if(fpDll == NULL)  
    {  
        printf("open  fail\n");  
    }  
    fseek(fpDll, 0, SEEK_END);  
    int len = ftell(fpDll);  
    fseek(fpDll, 0, SEEK_SET);  
    char *data = (char *)malloc(len);  
    fread(data, 1, len, fpDll);  
    fclose(fpDll);  

    signed int v11; // [sp+20h] [bp-18h]@3
  signed int v12; // [sp+24h] [bp-14h]@3
int v10;
size_t i;

v12 = 55;
      v11 = 89;
char *src = data;
int n = len;
 if ( *(_BYTE *)src == 82 && *((_BYTE *)src + 1) == 77 )
      {
        for ( i = 4; i < n; ++i )
        {
          v10 = v12 + v11;
          v12 = v11;
          v11 = v10;
          if ( v10 > 0x10000000 )
          {
            v12 = 1;
            v11 = 1;
          }
          *((_BYTE *)src + i) ^= v10;
        }
        *(_BYTE *)src = 77;
        *((_BYTE *)src + 1) = 90;
        *((_BYTE *)src + 2) = -112;
        *((_BYTE *)src + 3) = 0;
      }

FILE* pFile = fopen(argv[2],"wb");  
    fwrite(data,len,1,pFile);  
    fclose(pFile);     

}

 

 

Reflector+Reflexil 相结合实现对DLL文件修改

reflexil

https://github.com/sailro/Reflexil/releases

第一步:

下载Reflector 10和Reflexil 2.2,装好后将Reflexil的插件DLL文件加载到Reflector中

具体操作:Tools->Add-Ins->+->选择Reflexil的DLL文件Reflexil.Reflector.AIO.dll,点击确定加载到Reflector中

第二步:

2.1 Reflector加载需要编译的DLL或exe文件,找到对应的方法。

2.2 打开Tools->Reflexil  你将会看到对应方法的IL代码

选择Reflace all with code 对对应代码进行修改后,点击左下角的 Complie(编译),然后点击“确定”;

第三步:

确定操作完毕后,在左侧DLL或exe文件上右击

Save as 程序集编译成功!

 

Reflector反编译插件Reflexil 图文使用教程

Reflexil 它是Reflector的一个插件,结合Reflector,可以进行DLL代码注入等工作,实践证明完全可用,方便开发人员对.NET程序进行修改;可以作为一个Reflector插件修改程序集的IL并保存到磁盘文件,也可以在自己的.NET程序中调用进行更为灵活的.NET程序集修改。

下载好Reflexil后,在Add-ins 界面,点"+",选择Reflexil.Reflector.AIO.dll,然后Close

现在在Tool中会多一个Reflexil选项

它的界面

用法:

把我刚才写的Hello world拖进Reflector,我们选中程序集,命名空间,类名,方法, Reflexil的界面都是不一样的.

这里可以知道,我们能注入类,接口,枚举,方法,等,也可以重命名,删除.

现在我们来添加一个方法试试,选择Inject method 。 Item Name:Demo(此时只能声明无参,无返回值的方法,后面会讲什么添加这些)

点ok会给出警告

 意思就是,做大改动(注入,删除,重命名)时你是看不见结果,让你保存一次在导入新的dll文件,那就保存一次

保存好后,把新dll拖到Reflector中,就会发现在 Programe中多了一个Dmeo方法.

现在来给Demo 写实现

选择Demo 后右边的界面为:

选择Main:

instructions 中是IL指令, 高级玩家可以直接修改添加指令,但不是高级玩家咋办?

我们选择Replace all with code... 替换所以代码.

不过在这之前 先在Parameters添加一个参数string name,并在Attributes 将Return type设为stirng

我再次点击Replace all with code...  代码已经更新为

修改完成后,点compile 如果没有错误,就会生成IL指令,点ok就行

现在在来修改一下Main方法,调用Demo

代码已经改完,现在保存看看执行效果:

原程序:

修改后程序:

 

https://www.52pojie.cn/thread-271347-1-1.html

Reflector 之reflexil使用

潜水潜了那么久,上来换口气..
昨天和小伙伴一起玩一个CrackMe(C#),打算一起来写KeyGen,他用的是爆破的方法,我修复程序后发现里面常量各种溢出...无奈,只能用reflexil注入方法,然后写出KeyGen.
分享下使用reflexil常用的几个方法..

Reflector 之reflexil使用

先写个简单的控制台程序

工具,添加插件.

选中reflexil 1.6 (1.7从来没附加成功过.不知道为啥..)

一 直接修改操作数

可以直接编辑IL

Update 后

在程序集中右键

另存程序.

执行刚保存的程序

还可以直接添加IL 接着让后面继续输出

二 直接注入IL

右键,新建(new Create),填写对应的操作码,选择类型,

值得注意的是右边的按钮,append(添加),接着是插入在选择之前,然后是插入在选择之后;别选错了.

注意    注入的时候操作数的类型别选错了.

然后继续参照上面的,将程序另存一次.

三 替换代码

如果不想折腾IL,直接选择 替换所有代码.

不过这意味着你要重写所有代码(大多数时,我都会用在重写某个方法上)…然后提交一下,他会自动编译, 然后继续参照上面的,将程序另存一次.

提交完成后会自动编译,并且再右侧区域会生成对应的IL.

然后继续参照上面的,将程序另存一次.

执行下…

四  注入方法

别选错地方了.是你要将方法注入到某个类中,不是注入class所以,一定是在类上右键

暂时只能注入返回值为void的方法

对了,点击OK后会有个该死的提示,这提示的大致意思是:

当你在执行,增加,删除,重命名,等动作时,你不会直接看见相应的操作,他们是不同步的.

你必须要重新加载程序集才可以看见.

当然,你还需要将程序另存一次.!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

然后你需要关闭程序集,接着重新打开刚才另存的 

现在方法已经注入进去了.

然后我需要给它添加点内容..

让方法输出个InjectedMethod字符串

在方法中添加IL

接着修改Main方法,并且调用刚才注入的方法.

另存下..

执行结果.

Ps:   如果是替换代码的话,先更改injectmethod时不用实现main方法,只需要修改injectmethod()方法,然后再去修改Main方法.调用时因为injectmethod方法不是static所以需要实例化Program

接下来说说添加有参数,有返回值的方法:

先在参数(parameter)标签,中添加一个参数.

然后在属性标签中修改返回值为string.

接着使用替换代码的功能

可以看见方法已经带有参数并且有返回值.

修改下这个方法

打印下传进来的名字,并且打印当前时间

修改主函数调用

编译,然后不要忘记另存….

Good Job .

Have Fun .

By McevilRock

9-7/2014

2017-06-21 17:37:43 avi9111 阅读数 38968

其实我倒不很讨厌现在的游戏,页游有讨厌过,也没玩过,而且过去了,也不是太在意。现在的手游,很火的如阴阳师,王者荣耀这些,和以前的游戏不同那是正常的,毕竟社会是一直变化中,玩家也和以前的玩家不同了,就算同是以前的玩家,现在随着年龄的增加,心态也大不同了,如果和以前一样的游戏,还有谁会玩呢

(是的,我就是在黑,某情怀仙)

其实只有一点我是搞不通的,做手游做成小游戏也可以啊,非得搞个又长又臭的新手引导(其实工作量不少的,哥也被坑过不少),通常这个新手引导直接让我删游戏

但我自己做了一次策划之后,我好像想通了


玩家开了2枪

“不错哦,那些是属于你的奖励,去拿吧”

玩家跑去捡起宝箱,

“现在再尝试更远的目标吧”

玩家开了2抢

“太远了,手枪的射程不够,尝试走近点吧”

玩家向前到射击点,开2抢

跌落1把ak47

“尝试下新武器吧”

。。。

。。。

。。。


这个是我完全离开手机,电脑,用笔和纸写的,那个酸爽

如果要写下去,还可以很长,而且越写越爽,写书的人是很爽的,所以他就是写了一些又长又臭的新手引导

基本上都是无脑的策划写的,然后他又没有创造力把这个东西视觉化,也没有执行力,只是简单把这段文字按工作排班,所以就有了对话式的新手引导

(想象一下你把罗姨写的哈利波特全文直接搬到电影院去,是什么结果,应该就是进电影院看2个小时的结尾字幕)

我们就是不想看书才玩游戏啊,,,咳咳,,,我们就是看书太用功了,为了祖国4个现代化一直努力着,偶尔才想玩游戏放松一下啊

手游,次时代,页游,就是有很多不同的东西,界限还是很多的,只有自以为是的人才以为是一样的, ”不就都是一个游戏嘛!?”

想知道自己是不是自以为是的人?那你觉得真理是不是描述事物一般规则的唯一不变的理论?

这样的出品离我们理想的游戏差好多,所以这样的工作模式也和我们的理想差好多,但这样的游戏也不失为一个经典历史产物,后人自有评价的了

2013-04-15 09:47:28 miaoweiye 阅读数 1441

Unity3D教程:手游如何使用Unity3D自带的重力感应?重力感应在手机游戏的开发中非常常见。Unity3D本身集合了重力感应的相关内容。简单的JS脚本示范一下重力感应的使用:

//物体的贴图
var round : Texture2D;

//物体在屏幕中显示的X Y坐标
var x = 0;
var y = 0;

//物体屏幕显示的最大 X Y 范围
var cross_x = 0;
var cross_y = 0;

function Start(){
//初始化赋值
cross_x = Screen.width -  round.width;
cross_y = Screen.height -  round.height;
}

function OnGUI () {

//整体显示 x y z 重力感应的重力分量
GUI.Label(Rect(0,0,480,100),"position is " + Input.acceleration);

//绘制物体
GUI.DrawTexture(Rect(x,y,256,256),round);
}

function Update(){

//根据重力分量修改物体的位置这里乘以30的意思是让物体移动的快一些
x += Input.acceleration.x * 30;
y += -Input.acceleration.y * 30;

//避免物体超出屏幕
if(x < 0){
x = 0;
}else if(x > cross_x){
x = cross_x;
}

if(y < 0){
y = 0;
}else if(y > cross_y){
y = cross_y;
}
}

这里的Input是指Unity中的输入,acceleration便是其重力了,x和y分别代表其重力分量。创建完毕之后只需添加纹理图片即可:

Unity3D教程:手游如何使用Unity3D自带的重力感应

Unity3D教程:手游如何使用Unity3D自带的重力感应

 

2018-04-29 10:58:53 larry_zeng1 阅读数 10627

本次分享总结,起源于腾讯桌球项目,但是不仅仅限于项目本身。虽然基于Unity3D,很多东西同样适用于Cocos。本文从以下10大点进行阐述:架构设计、原生插件/平台交互、版本与补丁、用脚本,还是不用?这是一个问题、资源管理、性能优化、异常与Crash、适配与兼容、调试及开发工具、项目运营。


1.架构设计

好的架构利用大规模项目的多人团队开发和代码管理,也利用查找错误和后期维护。

  • 框架的选择:需要根据团队、项目来进行选择,没有最好的框架,只有最合适的框架。
  • 框架的使用:统一的框架能规范大家的行为,互相之间可以比较平滑切换,可维护性大大提升。除此之外,还能代码解耦。例如StrangeIOC是一个超轻量级和高度可扩展的控制反转(IoC)框架,专门为C#和Unity编写。已知公司内部使用StrangeIOC框架的游戏有:腾讯桌球欢乐麻将植物大战僵尸Online[https://github.com/strangeioc/strangeioc](https://github.com/strangeioc/strangeioc)

依赖注入(Dependency Injection,简称DI),是一个重要的面向对象编程的法则来削减计算机程序的耦合问题。依赖注入还有一个名字叫做控制反转(Inversion of Control,英文缩写为IoC)。依赖注入是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。即对象在被创建的时候,由一个运行上下文环境或专门组件将其所依赖的服务类对象的引用传递给它。也可以说,依赖被注入到对象中。所以,控制反转是,关于一个对象如何获取他所依赖的对象的引用,这个责任的反转


StrangeIOC采用MVCS(数据模型 Model,展示视图 View,逻辑控制 Controller,服务Service)结构,通过消息/信号进行交互和通信。整个MVCS框架跟flash的robotlegs基本一致,(忽略语言不一样)详细的参考[http://www.cnblogs.com/skynet/archive/2012/03/21/2410042.html](http://www.cnblogs.com/skynet/archive/2012/03/21/2410042.html)

  • 数据模型 Model:主要负责数据的存储和基本数据处理
  • 展示视图 View:主要负责UI界面展示和动画表现的处理
  • 逻辑控制 Controller:主要负责业务逻辑处理,
  • 服务Service:主要负责独立的网络收发请求等的一些功能。
  • 消息/信号:通过消息/信号去解耦Model、View、Controller、Service这四种模块,他们之间通过消息/信号进行交互。
  • 绑定器Binder:负责绑定消息处理、接口与实例对象、View与Mediator的对应关系。
  • MVCS Context:可以理解为MVC各个模块存在的上下文,负责MVC绑定和实例的创建工作。

腾讯桌球客户端项目框架

1.2代码目录的组织:一般客户端用得比较多的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。

1.2公司组件

  • msdk(sns、支付midas、推送灯塔、监控Bugly)
  • apollo
  • apollo voice
  • xlua

目前我们的腾讯桌球、四国军棋都接入了apollo,但是如果服务器不采用apollo框架,不建议客户端接apollo,而是直接接msdk减少二次封装信息的丢失和带来的错误,方便以后升级维护,并且减少导入无用的代码。

1.3第三方插件选型

  • 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 和函数。 用staticextern修饰符标记方法。
      • 调用 DLL 函数。像处理其他任何托管方法一样调用托管类上的方法。
  • 在C#中创建回调函数,C/C++调用C#回调函数
    • 创建托管回调函数。
    • 创建一个委托,并将其作为参数传递给 C/C++函数。平台调用会自动将委托转换为常见的回调格式。
    • 确保在回调函数完成其工作之前,垃圾回收器不会回收委托。

那么C#与原生插件之间是如何实现互相调用的呢?在弄清楚这个问题之前,我们先看下C#代码(.NET上的程序)的执行的过程:(更详细一点的介绍可以参见我之前写的博客:http://www.cnblogs.com/skynet/archive/2010/05/17/1737028.html

  • 将源码编译为托管模块;
  • 将托管模块组合为程序集;
  • 加载公共语言运行时CLR;
  • 执行程序集代码。
    注: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-https://en.wikipedia.org/wiki/Java_Native_Interface,这里不深入介绍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个关键概念/结构:JavaVMJNIENVJavaVM提供虚拟机创建、销毁等操作,Java中一个进程可以创建多个虚拟机,但是Android一个进程只能有一个虚拟机JNIENV是线程相关的,对应的是JavaVM中的当前线程的JNI环境,只有附加(attach)到JavaVM的线程才有JNIENV指针,通过JNIEVN指针可以获取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, voidreserved)方法即可,然后把JavaVM指针保存起来作为上下文使用
获取到JavaVM之后,还不能直接拿到JNI函数去获取Java代码,必须通过线程关联的JNIENV指针去获取。所以,作为一个好的开发习惯在每次获取一个线程的JNI相关功能时,先调用AttachCurrentThread();又或者
每次通过JavaVM指针获取当前的JNIENV:java_vm->GetEnv((void)&jni_env, version),一定是已经附加到JavaVM的线程。通过JNIENV可以获取到Java的代码,例如你想在本地代码中访问一个对象的字段(field),你可以像下面这样做:

  • 对于类,使用jni_env->FindClass获得类对象的引用
  • 对于字段,使用jni_env->GetFieldId获得字段ID
  • 使用对应的方法(例如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类的静态成员。更详细接口参考帮助文档:http://docs.unity3d.com/ScriptReference/AndroidJNI.htmlhttp://docs.unity3d.com/ScriptReference/AndroidJNIHelper.html
  • 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即可实现动态更新资源的功能。

手游在实现这块时需要注意的几点:

  • 游戏发布出一定要提供游戏内更新的途径。即使是删掉测试,保不准这期间需要进行资源或者BUG修复更新。很多玩家并不知道如何更新,而且Android手机应用分发平台多样,分发平台本身也不会跟官方同步更新(特别是小的分发平台)。
  • 更新功能要提供强制更新、非强制更新配置化选项,并指定哪些版本可以不强更,哪些版本必须强更。
  • 当游戏提供非强制更新功能之后,现网一定会存在多个版本。如果需要针对不同版本做不同的更新,例如配置文件A针对1.0.0.1修改了一项,针对1.0.0.2修改了另一项,2个版本需要分别更新对应的修改,需要自己实现更新策略IIPS不提供这个功能。当需要复杂的更新策略,推荐自己编写更新服务器和客户端逻辑,不使用iips组件(其实自己实现也很简单)。

没有运营经验的人会选择二进制,认为二进制安全、更小,这对端游/手游外网只存在一个版本的游戏适合,对一般不强升版本的手游并不适合,反而会对更新和维护带来很大的麻烦。

  • 配置使用XML或者JSON等文本格式,更利于多版本的兼容和更新。最开始腾讯桌球客户端使用的二进制格式(由excel转换而来),但是随着运营配置格式需要增加字段,这样老版本程序就解析不了新的二进制数据,给兼容和更新带来了很大的麻烦。这样就要求上面提到的针对不同步做不同更新,又或者配置一开始就预留好足够的扩展项,其实不管怎么预留扩展也很难跟上需求的变化,而且一开始会把配置表复杂化但是其实只有一张或者几张才会变更结构。
  • iOS版本的送审版本需要连接特定的包含新内容的服务器,现网服务器还不包含新内容。送审通过之后,上架游戏现网服务器会进行更新,iOS版本需要连接现网服务器而非送审服务器,但是这期间又不能修改客户度,这个切换需要通过服务器下发开关进行控制。例如通过指定送审的iOS游戏版本号,客户端判断本地版本号是否为送审版本,如果是连接送审服务器,否则连接现网服务器。

4.用脚本,还是不用?这是一个问题

方便更新,减少Crash(特别是使用C++的cocos引擎)

通过上面一节【版本与补丁】知道要实现代码更新是非常困难的,正式这个原因客户端开发的压力是比较大的,如果出现了比较严重的BUG必须发强制更新版本,使用脚本可以解决这个问题。
由于Unity3D手游更新成本比较大,而且目前腾讯桌球要求不能强制更新,这导致新版本的活动覆盖率提升比较慢、出现问题之后难以修复。针对这个情况,考虑引入lua进行活动开发,后续发布活动及修复bug只需要发布lua资源,进行资源更新即可,大大降低了发布和修复问题的成本。
可选方案还有使用Html5进行活动开发,目前游戏中已经预埋了Html5活动入口,并且已经用来发过"玩家调查"、"腾讯棋牌宣传"等。但是与lua对比,不能做到与Unity3D的深度融合,体验不如使用lua,例如不能操作游戏中的ui、不能完成复杂界面的制作、不能复用已有的功能、玩家付费充值跟已有的也会有差异

游戏脚本之王——Lua

在公司内部魔方比较喜欢用lua,火隐忍者(手游)unity+ulua,全民水浒cocos2d-x+lua等等都有使用lua进行开发。我们可以使用公司内部的xlua组件,也可以使用ulua[http://ulua.org/]、UniLua[https://github.com/xebecnan/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,在PC上广泛被使用,但是在移动设备上还是属于新鲜事物。支持的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
  • 运算裁剪,例如碰撞检测裁剪
    • 粗略碰撞检测(划分空间——二分/四叉树/八叉树/网格等,降低碰撞检测的数量)
    • 精确碰撞检测(检查候选碰撞结果,进而确定对象是否真实发生碰撞)
    • 休眠机制:避免模拟静止的球
  • 逻辑帧与渲染帧分离
  • 分帧处理
  • 异步/多线程处理

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.6包大小

  • 使用压缩格式的纹理/音频
  • 尽量不要使用System.Xml而使用较小的Mono.Xml
  • 启用Stripping来减小库的大小
  • Unity strip level(strip by byte code)
  • Unity3D输出APK,取消X86架构
  • iOS Xcode strip开启

6.7耗电

下面影响耗电的几个因素和影响度摘自公司内部的一篇文章。


7.异常与Crash

7.1防御式编程

  • 非法的输入中保护你的程序
    • 检查每一输入参数
    • 检查来自外部的数据/资源
  • 断言
  • 错误处理
  • 隔栏
    防不胜防,不管如何防御总有失手的时候,这就需要异常捕获和上报。

7.2异常捕获


由于很多错误并不是发生在开发工作者调试阶段,而是在用户或测试工作者使用阶段;这就需要相关代码维护工作者对于程序异常捕获收集现场信息。异常与Crash的监控和上报,这里不介绍Bugly的使用,按照apollo或者msdk的文档接入即可,没有太多可以说的。这里主要透过Bugly介绍手游的几类异常的捕获和分析。

Unity3D C#层异常捕获

相比比较简单使用:

code>Application.RegisterLogCallback/Application.RegisterLogCallbackThreaded

注册回调函数/在一个新的线程中调用委托。特别注意:保证项目中只有一个Application.RegisterLogCallback注册回调,否则后面注册的会覆盖前面注册的回调!回调函数中stackTrace参数包异常调用栈。

publicvoidHandleLog(stringlogString, stringstackTrace, LogType type){if(logString == null|| logString.StartsWith(cLogPrefix)){return;}ELogLevel level = ELogLevel.Verbose;switch(type){caseLogType.Exception:level = ELogLevel.Error;break;default:return;}if(stackTrace != null){Print(level, ELogTag.UnityLog, logString + "\n"+ stackTrace);}else{Print(level, ELogTag.UnityLog, logString);}}

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, finalThrowable 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(finalThrowable throwable) {  

    String trace = "";
    try{  
        Writer writer = newStringWriter();
        PrintWriter pw = newPrintWriter(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
voiduncaughtExceptionHandler(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):按功能等进行模块划分,如网络频道只接收/显示网络模块的消息,频道建议使用枚举进行命名。
  • 日志同时会输出到日志文件
  • 日志上报(iOS屏蔽文档目录,出了问题也拿不到日志)

9.2调试用绘图工具

调试绘图用工具指开发及调试期间为了可视化的绘图用工具,如腾讯桌球开发调试时会使用VectrosityScripts可视化球桌的物理模型(实际碰撞线)帮助调试。这类工具可以节省大量时间及快速定位问题。通常调试用绘图工具包含:

  • 支持绘制基本图形,如直线、球体、点、坐标轴、包围盒等
  • 支持自定义配置,如颜色、粒度(线的粗细/球体半径/点的大小)等

9.3游戏内置菜单/作弊工具

在开发调试期间提供游戏进行中的一些配置选项及作弊工具,以方便调试和提高效率。例如腾讯桌球游戏中提供:

  • 游戏内物理引擎参数调整菜单;
  • 修改签到奖励领取天数等作弊工具

注意游戏内的所有开发调试用的工具,都需要通过编译宏开关,保证发布版本不会把工具代码包含进去

9.4Unity扩展

Untiy引擎提供了非常强大的编辑器扩展功能,基于Unity Editor可以实现非常多的功能。公司内部、外部都有非常的开源扩展可用
公司外部,如GitHub上的:
UnityEditor-MiniExtension
Unity-Resource-Checker
UnityEditorHelper
MissingReferencesUnity
Unity3D-ExtendedEditor

公司内部:
TUTBeautyUnityUnityDependencyBy

10.项目运营

自动构建

  • 版本号——主版本号.特性版本号.修正版本号.构建版本号
    • [构建版本号]应用分发平台升级判断基准
  • 自动构建
    • Android
    • iOS — XUPorter

公司内部接入SODA即可,建议搭建自己的构建机,开发期间每日N Build排队会死人的,另外也可以搭建自己的搭建构建平台

统计上报

  • Tlog上报
  • 玩家转化关键步骤统计(重要)
  • Ping统计上报
  • 游戏业务的统计上报(例如桌球球局相关的统计上报)
  • 灯塔自定义上报

运营模板

  • 配置化
  • 服务器动态下发
  • CDN拉取图片并缓存

上线前的checklist

项目要点说明指标
灯塔上报1. 灯塔自带统计信息 2. 自定义信息上报灯塔里面包含很多统计数据,需要检查是否ok1. 版本/渠道分布 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.1GH,RAM为768M)或以上机型:游戏核心玩法中,最小FPS应不小于25帧/秒 
3. 档机型(CPU为1GHZ,RAM为768M)或以上机型:游戏核心玩法中,最小FPS应不小于18帧/秒
【流量消耗要求】游戏核心玩法流量消耗情况(非一次性消耗)应满足以下条件:
1. 对于分局的游戏场景,单局消耗流量不超过200KB 
2. 对于不分局游戏场景或流量与局时有关的场景,10分钟消耗流量不超过500KB