经典游戏服务器端架构概述 (一)(转)

架构的分析模型
一. 讨论的背景
现代电子游戏,基本上都会使用一定的网络功能。从验证正版,到多人交互等等,都需要架设一些专用的服务器,以及编写在服务器上的程序。因此,游戏服务器端软件的架构,本质上也是游戏服务器这个特定领域的软件架构。
软件架构的分析,可以通过不同的层面入手。比较经典的软件架构描述,包含了以下几种架构:
运行时架构——这种架构关心如何解决运行效率问题,通常以程序进程图、数据流图为表达方式。在大多数开发团队的架构设计文档中,都会包含运行时架构,说明这是一种非常重要的设计方面。这种架构也会显著的影响软件代码的开发效率和部署效率。本文主要讨论的是这种架构。

逻辑架构——这种架构关心软件代码之间的关系,主要目的是为了提高软件应对需求变更的便利性。人们往往会以类图、模块图来表达这种架构。这种架构设计在需要长期运营和重用性高的项目中,有至关重要的作用。因为软件的可扩展性和可重用度基本是由这个方面的设计决定的。特别是在游戏领域,需求变更的频繁程度,在多个互联网产业领域里可以说是最高的。本文会涉及一部分这种架构的内容,但不是本文的讨论重点。

物理架构——关心软件如何部署,以机房、服务器、网络设备为主要描述对象。

数据架构——关心软件涉及的数据结构的设计,对于数据分析挖掘,多系统协作有较大的意义。

开发架构——关心软件开发库之间的关系,以及版本管理、开发工具、编译构建的设计,主要为了提高多人协作开发,以及复杂软件库引用的开发效率。现在流行的集成构建系统就是一种开发架构的理论。

二. 游戏服务器架构的要素
服务器端软件的本质,是一个会长期运行的程序,并且它还要服务于多个不定时,不定地点的网络请求。所以这类软件的特点是要非常关注稳定性和性能。这类程序如果需要多个协作来提高承载能力,则还要关注部署和扩容的便利性;同时,还需要考虑如何实现某种程度容灾需求。由于多进程协同工作,也带来了开发的复杂度,这也是需要关注的问题。
功能约束,是架构设计决定性因素。一个万能的架构,必定是无能的架构。一个优秀的架构,则是正好把握了对应业务领域的核心功能产生的。游戏领域的功能特征,于服务器端系统来说,非常明显的表现为几个功能的需求:
对于游戏数据和玩家数据的存储

对玩家客户端进行数据广播

把一部分游戏逻辑在服务器上运算,便于游戏更新内容,以及防止外挂。

针对以上的需求特征,在服务器端软件开发上,我们往往会关注软件对电脑内存和CPU的使用,以求在特定业务代码下,能尽量满足承载量和响应延迟的需求。最基本的做法就是“时空转换”,用各种缓存的方式来开发程序,以求在CPU时间和内存空间上取得合适的平衡。在CPU和内存之上,是另外一个约束因素:网卡。网络带宽直接限制了服务器的处理能力,所以游戏服务器架构也必定要考虑这个因素。
对于游戏服务器架构设计来说,最重要的是利用游戏产品的需求约束,从而优化出对此特定功能最合适的“时-空”架构。并且最小化对网络带宽的占用。

[图-游戏服务器的分析模型]

三. 核心的三个架构
基于上述的分析模型,对于游戏服务端架构,最重要的三个部分就是,如何使用CPU、内存、网卡的设计:
内存架构:主要决定服务器如何使用内存,以保证尽量少的内存泄漏的可能,以及最大化利用服务器端内存来提高承载量,降低服务延迟。

调度架构:设计如何使用进程、线程、协程这些对于CPU调度的方案。选择同步、异步等不同的编程模型,以提高服务器的稳定性和承载量。同时也要考虑对于开发带来的复杂度问题。现在出现的虚拟化技术,如虚拟机、Docker、云服务器等,都为调度架构提供了更多的选择。

通信模式:决定使用何种方式通讯。网络通讯包含有传输层的选择,如TCP/UDP;据表达层的选择,如定义协议;以及应用层的接口设计,如消息队列、事件分发、远程调用等。

本文的讨论,也主要是集中于对以上三个架构的分析。
四. 游戏服务器模型的进化历程
最早的游戏服务器是比较简单的,如UO《网络创世纪》的服务端一张3.5寸软盘就能存下。基本上只是一个广播和存储文件的服务器程序。后来由于国内的外挂、盗版流行,各游戏厂商开始以MUD为模型,建立主要运行逻辑在服务器端的架构。这种架构在MMORPG类产品的不断更新中发扬光大,从而出现了以地图、视野等分布要素设计的分布式游戏服务器。而在另外一个领域,休闲游戏,天然的需要集中超高的在线用户,所以全区型架构开始出现。现代的游戏服务器架构,基本上都希望能结合承载量和扩展性的有点来设计,从而形成了更加丰富多样的形态。本文的讨论主要是选取这些比较典型的游戏服务器模型,分析其底层各种选择的优点和缺点,希望能探讨出更具广泛性,更高开发效率的服务器模型。
分服模型

一. 模型描述

分服模型是游戏服务器中最典型,也是历久最悠久的模型。其特征是游戏服务器是一个个单独的世界。每个服务器的帐号是独立的,而且只用同一服务器的帐号才能产生线上交互。在早期服务器的承载量达到上限的时候,游戏开发者就通过架设更多的服务器来解决。这样提供了很多个游戏的“平行世界”,让游戏中的人人之间的比较,产生了更多的空间。所以后来以服务器的开放、合并形成了一套成熟的运营手段。一个技术上的选择最后导致了游戏运营方式的模式,是一个非常有趣的现象。

[图-分服模型]
二. 调度架构
单进程游戏服务器最简单的游戏服务器只有一个进程,是一个单点。这个进程如果退出,则整个游戏世界消失。在此进程中,由于需要处理并发的客户端的数据包,因此产生了多种选择方法:

[图-单进程调度模型]
同步-动态多线程:每接收一个用户会话,就建立一个线程。这个用户会话往往就是由客户端的TCP连接来代表,这样每次从socket中调用读取或写出数据包的时候,都可以使用阻塞模式,编码直观而简单。有多少个游戏客户端的连接,就有多少个线程。但是这个方案也有很明显的缺点,就是服务器容易产生大量的线程,这对于内存占用不好控制,同时线程切换也会造成CPU的性能损失。更重要的多线程下对同一块数据的读写,需要处理锁的问题,这可能让代码变的非常复杂,造成各种死锁的BUG,影响服务器的稳定性。同步-多线程池:为了节约线程的建立和释放,建立了一个线程池。每个用户会话建立的时候,向线程池申请处理线程的使用。在用户会话结束的时候,线程不退出,而是向线程池“释放”对此线程的使用。线程池能很好的控制线程数量,可以防止用户暴涨下对服务器造成的连接冲击,形成一种排队进入的机制。但是线程池本身的实现比较复杂,而“申请”、“施放”线程的调用规则需要严格遵守,否则会出现线程泄露,耗尽线程池。

异步-单线程/协程:在游戏行业中,采用Linux的epoll作为网络API,以期得到高性能,是一个常见的选择。游戏服务器进程中最常见的阻塞调用就是网路IO,因此在采用epoll之后,整个服务器进程就可能变得完全没有阻塞调用,这样只需要一个线程即可。这彻底解决了多线程的锁问题,而且也简化了对于并发编程的难度。但是,“所有调用都不得阻塞”的约束,并不是那么容易遵守的,比如有些数据库的API就是阻塞的;另外单进程单线程只能使用一个CPU,在现在多核多CPU的服务器情况下,不能充分利用CPU资源。异步编程由于是基于“回调”的方式,会导致要定义很多回调函数,并且把一个流程里面的逻辑,分别写在多个不同的回调函数里面,对于代码阅读非常不理。——针对这种编码问题,协程(Coroutine)能较好的帮忙,所以现在比较流行使用异步+协程的组合。不管怎样,异步-单线程模型由于性能好,无需并发思维,依然是现在很多团队的首选。

异步-固定多线程:这是基于异步-单线程模型进化出来的一种模型。这种模型一般有三类线程:主线程、IO线程、逻辑线程。这些线程都在内部以全异步的方式运行,而他们之间通过无锁消息队列通信。

多进程游戏服务器多进程的游戏服务器系统,最早起源于对于性能问题需求。由于单进程架构下,总会存在承载量的极限,越是复杂的游戏,其单进程承载量就越低,因此开发者们一定要突破进程的限制,才能支撑更复杂的游戏。一旦走上多进程之路,开发者们还发现了多进程系统的其他一些好处:能够利用上多核CPU能力;利用操作系统的工具能更仔细的监控到运行状态、更容易进行容灾处理。多进程系统比较经典的模型是“三层架构”:在多进程架构下,开发者一般倾向于把每个模块的功能,都单独开发成一个进程,然后以使用进程间通信来协调处理完整的逻辑。这种思想是典型的“管道与过滤器”架构模式思想——把每个进程看成是一个过滤器,用户发来的数据包,流经多个过滤器衔接而成的管道,最后被完整的处理完。由于使用了多进程,所以首选使用单进程单线程来构造其中的每个进程。这样对于程序开发来说,结构清晰简单很多,也能获得更高的性能。

[图-经典的三层模型]
尽管有很多好处,但是多进程系统还有一个需要特别注意的问题——数据存储。由于要保证数据的一致性,所以存储进程一般都难以切分成多个进程。就算对关系型数据做分库分表处理,也是非常复杂的,对业务类型有依赖的。而且如果单个逻辑处理进程承载不了,由于其内存中的数据难以分割和同步,开发者很难去平行的扩展某个特定业务逻辑。他们可能会选择把业务逻辑进程做成无状态的,但是这更加加重了存储进程的性能压力,因为每次业务处理都要去存储进程处拉取或写入数据。除了数据的问题,多进程也架构也带来了一系列运维和开发上的问题:首先就是整个系统的部署更为复杂了,因为需要对多个不同类型进程进行连接配置,造成大量的配置文件需要管理;其次是由于进程间通讯很多,所以需要定义的协议也数量庞大,在单进程下一个函数调用解决的问题,在多进程下就要定义一套请求、应答的协议,这造成整个源代码规模的数量级的增大;最后是整个系统被肢解为很多个功能短小的代码片段,如果不了解整体结构,是很难理解一个完整的业务流程是如何被处理的,这让代码的阅读和交接成本巨高无比,特别是在游戏领域,由于业务流程变化非常快,几经修改后的系统,几乎没有人能完全掌握其内容。
三. 内存架构
由于服务器进程需要长期自动化运行,所以内存使用的稳定是首要大事。在服务器进程中,就算一个触发几率很小的内存泄露,都会积累起来变成严重的运营事故。需要注意的是,不管你的线程和进程结构如何,内存架构都是需要的,除非是Erlang这种不使用堆的函数式语言。
动态内存在需要的时候申请内存来处理问题,是每个程序员入门的时候必然要学会的技能。但是,如何控制内存释放却是一个大问题。在C/C++语言中,对于堆的控制至关重要。有一些开发者会以树状来规划内存使用,就是一般只new/delete一个主要的类型的对象,其他对象都是此对象的成员(或者指针成员),只要这棵树上所有的对象都管理好自己的成员,就不会出现内存漏洞,整个结构也比较清晰简单。

[图-对象树架构]
在Objective C语言中,有所谓autorealse的特性,这种特性实际上是一种引用计数的技术。由于能配合在某个调度模型下,所以使用起来会比较简单。同样的思想,有些开发者会使用一些智能指针,配合自己写的框架,在完整的业务逻辑调用后一次性清理相关内存。

[图-根据业务处理调度管理内存池]
在带虚拟机的语言中,最常见的是Java,这个问题一般会简单一些,因为有自动垃圾回收机制。但是,JAVA中的容器类型、以及static变量依然是可能造成内存泄露的原因。加上无规划的使用线程,也有可能造成内存的泄露——有些线程不会退出,而且在不断增加,最后耗尽内存。所以这些问题都要求开发者专门针对static变量以及线程结构做统一设计、严格规范。

预分配内存动态分配内存在小心谨慎的程序员手上,是能发挥很好的效果的。但是游戏业务往往需要用到的数据结构非常多,变化非常大,这导致了内存管理的风险很高。为了比较彻底的解决内存漏洞的问题,很多团队采用了预先分配内存的结构。在服务器启动的时候分配所有的变量,在运行过程中不调用任何new关键字的代码。
这样做的好处除了可以有效减少内存漏洞的出现概率,也能降低动态分配内存所消耗的性能。同时由于启动时分配内存,如果硬件资源不够的话,进程就会在启动时失败,而不是像动态分配内存的程序一样,可能在任何一个分配内存的时候崩溃。然而,要获得这些好处,在编码上首先还是要遵循“动态分配架构”中对象树的原则,把一类对象构造为“根”对象,然后用一个内存池来管理这些根对象。而这个内存池能存放的根对象的数目,就是此服务进程的最大承载能力。一切都是在启动的时候决定,非常的稳妥可靠。

[图-预分配内存池]
不过这样做,同样有一些缺点:首先是不太好部署,比如你想在某个资源较小的虚拟机上部署一套用来测试,可能一位内没改内存池的大小,导致启动不成功。每次更换环境都需要修改这个配置。其次,是所有的用到的类对象,都要在根节点对象那里有个指针或者引用,否则就可能泄漏内存。由于对于非基本类型的对象,我们一般不喜欢用拷贝的方式来作为函数的参数和返回值,而指针和应用所指向的内存,如果不能new的话,只能是现成的某个对象的成员属性。这回导致程序越复杂,这类的成员属性就越多,这些属性在代码维护是一个不小的负担。
要解决以上的缺点,可以修改内存池的实现,为动态增长,但是具备上限的模型,每次从内存池中“获取”对象的时候才new。这样就能避免在小内存机器上启动不了的问题。对于对象属性复杂的问题,一般上需要好好的按面向对象的原则规划代码,做到尽量少用仅仅表示函数参数和返回值的属性,而是主要是记录对象的“业务状态”属性为主,多花点功夫在构建游戏的数据模型上。

四. 进程间通讯手段
在多进程的系统中,进程间如何通讯是一个至关重要的问题,其性能和使用便利性,直接决定了多进程系统的技术效能。
Socket通讯TCP/IP协议是一种通用的、跨语言、跨操作系统、跨机器的通讯方案。这也是开发者首先想到的一种手段。在使用上,有使用TCP和UDP两个选择。一般我们倾向在游戏系统中使用TCP,因为游戏数据的逻辑相关性比较强,UDP由于可能存在的丢包和重发处理,在游戏逻辑上的处理一般比较复杂。由于多进程系统的进程间网络一般情况较好,UDP的性能优势不会特别明显。
要使用TCP做跨进程通讯,首先就是要写一个TCP Server,做端口监听和连接管理;其次需要对可能用到的通信内容做协议定制;最后是要编写编解码和业务逻辑转发的逻辑。这些都完成了之后,才能真正的开始用来作为进程间通信手段。

使用Socket编程的好处是通用性广,你可以用来实现任何的功能,和任何的进程进行协作。但是其缺点也异常明显,就是开发量很大。虽然现在有一些开源组件,可以帮你简化Socket Server的编写工作,简化连接管理和消息分发的处理,但是选择目标建立连接、定制协议编解码这两个工作往往还是要自己去做。游戏的特点是业务逻辑变化很多,导致协议修改的工作量非常大。因此我们除了直接使用TCP/IP socket以外,还有很多其他的方案可以尝试。

[图-TCP通讯]消息队列在多进程系统中,如果进程的种类比较多,而且变化比较快,大量编写和配置进程之间的连接是一件非常繁琐的工作,所以开发者就发明了一种简易的通讯方法——消息队列。这种方法的底层还是Socket通讯实现,但是使用者只需要好像投递信件一样,把消息包投递到某个“信箱”,也就是队列里,目标进程则自动不断去“收取”属于自己的“信件”,然后触发业务处理。
这种模型的好处是非常简单易懂,使用者只需要处理“投递”和“收取”两个操作即可,对于消息也只需要处理“编码”和“解码”两个部分。在J2EE规范中,就有定义一套消息队列的规范,叫JMS,Apache ActiveMQ就是一个应用广泛的实现者。在linux环境下,我们还可以利用共享内存,来承担消息队列的存储器,这样不但性能很高,而且还不怕进程崩溃导致未处理消息丢失。

[图-消息队列]
需要注意的是,有些开发者缺乏经验,使用了数据库,如MySQL,或者是NFS这类运行效率比较低的媒介作为队列的存储者。这在功能上虽然可以行得通,但是操作一频繁,就难以发挥作用了。如以前有一些手机短信应用系统,就用mysql来存储“待发送”的短信。
消息队列虽然非常好用,但是我们还是要自己对消息进行编解码,并且分发给所需要的处理程序。在消息到处理程序之间,存在着一个转换和对应的工作。由于游戏逻辑的繁多,这种对应工作完全靠手工编码,是比较容易出错的。所以这里还有进一步的改进空间。

远程调用有一些开发者会希望,在编码的时候完全屏蔽是否跨进程在进行调用,完全可以好像调用本地函数或者本地对象的方法一样。于是诞生了很多远程调用的方案,最经典的有Corba方案,它试图实现能在不同语言的代码直接,实现远程调用。JAVA虚拟机自带了RMI方案的支持,在JAVA进程之间远程调用是比较方便的。在互联网的环境下,还有各种Web Service方案,以HTTP协议作为承载,WSDL作为接口描述。
使用远程调用的方案,最大好处是开发的便捷,你只需要写一个函数,就能在任何一个其他进程上对此函数进行调用。这对游戏开发来说,就解决了多进程方案最大的一个开发效率问题。但是这种便捷是有成本的:一般来说,远程调用的性能会稍微差一点,因为需要用一套统一的编解码方案。如果你使用的是C/C++这类静态语言,还需要使用一种IDL语言来先描述这种远程函数的接口。但是这些困难带来的好处,在游戏开发领域还是非常值得的。

[图-远程调用]

五. 容灾和扩容手段
在多进程模型中,由于可以采用多台物理服务器来部署服务进程,所以为容灾和扩容提供了基础条件。
在单进程模型下,容灾常常使用的热备服务器,依然可以在多进程模型中使用,但是开着一台什么都不做的服务器完全是为了做容灾,多少有点浪费。所以在多进程环境下,我们会启动多个相同功能的服务器进程,在请求的时候,根据某种规则来确定对哪个服务进程发起请求。如果这种规则能规避访问那些“失效”了的服务进程,就自动实现了容灾,如果这个规则还包括了“更新新增服务进程”的逻辑,就可以做到很方便的扩容了。而这两个规则,统一起来就是一条:对服务进程状态的集中保存和更新。
为了实现上面的方案,常常会架设一个“目录”服务器进程。这个进程专门负责搜集服务器进程的状态,并且提供查询。ZooKeeper就是实现这种目录服务器的一个优秀工具。

[图-服务器状态管理]
尽管用简单的目录服务器可以实现大部分容灾和扩容的需求,但是如果被访问进程的内存中有数据存在,那么问题就比较复杂了。对于容灾来说,新的进程必须要有办法重建那个“失效”了的进程内存中的数据,才可能完成容灾功能;对于扩容功能来说,新加入的进程,也必须能把需要的数据载入到自己的内存中才行,而这些数据,可能已经存在于其他平行的进程中,如何把这部分数据转移过来,是一个比较耗费性能和需要编写相当多代码的工作。——所以一般我们喜欢对“无状态”的进程来做扩容和容灾。

作者:JumboWu
链接:https://www.jianshu.com/p/6dc22d5de649

FairyGUI中常用的方法,对比UGUI[转]

素材管理
可以直接将图片(Gif)、声音(mp3)、动画、文字等素材从资源浏览器拖动到库中。
资源Url

UIPackage.getItemURL(“包名“,“资源名”)
AudioClip clip = (AudioClip)UIPackage.GetItemAsset(“包名称”,”声音名称”);

菜单”编辑”->“创建位图字体”,(输入字符,显示对应图片)
多信息文本(GRichTextField)支持链接和图文混排。
用例: aTextField.text = “请去找王大锤”;

列表 (GList)
AddChild : 增加一个项目。
AddChildAt :在指定的位置增加一个项目。
RemoveChild : 删除一个项目。
RemoveChildAt : 删除一个指定位置的项目。
RemoveChildren : 删除一个范围内的项目,或者全部删除。
GList内建了对象池。
使用对象池后的方法:
AddItemFromPool : 从池里取出(如果有)或者新建一个对象,添加到列表中
GetFromPool : 从池里取出(如果有)或者新建一个对象
ReturnToPool : 将对象返回池里
RemoveChildToPool : 删除一个列表项目,并将对象返回池里
RemoveChildToPoolAt : 删除一个指定位置的项目,并将对象返回池里
RemoveChildrenToPool:删除一个范围内的项目,或者全部删除,并将删除的对象都返回池里
添加对象时不使用池,对象池将不断增大。
正确的做法:从池中创建对象。即使用AddItemFromPool或GetFromPool。

虚拟列表 GList.SetVirtual
使用GList.numItems设定列表项目的数量。注意与GList.numChildren区分,后者是当前列表容器的显示对象数量。
但要注意显示对象和列表项目的数量在数量上和顺序上都是不一致的,也就是GetChildAt(0)获得的显示对象并不等于列表的第一条项目。
列表滚动到目标位置(第500个),调用GList.AddSelection(500)
虚拟列表只能通过numItems改变列表项目的数量,不允许使用AddChild或RemoveChild增删对象。例如如果要清空列表,必须要通过设置numItems=0,而不是RemoveChildren。
循环列表 GList.SetVirtualAndLoop()。

GList.ensureBoundsCorrect()通知GList立刻重排。(手动触发重新排列)

装载器 GLoader
运行时设置装载器内容的方式是:
aLoader.url = “ui://xxxxx”;
aLoader.url = “demo/aimage”; //这里加载的是路径为Assets/Resources/demo/aimage的一个贴图
例如,你希望从AssetBundle中获取资源,那么你可以扩展GLoader。首先编写你的Loader类,例如:

这是一个egret中使用到的自定义loader,使用getResByUrl这个API加载外部资源。
然后我们就可以用fairygui.UIObjectFactory.setLoaderExtension(MyGLoader);注册我们要使用的Loader类。注册完成后,游戏中所有装载器都是由MyGLoader实例化产生。
组件
点击穿透 默认情况下,组件的矩形区域将拦截点击,勾选后,点击事件可以穿透组件中没有内容的空白区域。
控制器 Controller
Controller c1 = aComponent.GetController(“c1”)
c1.selectedIndex = 1;//1是页面索引

c1.selectedPage = ‘pageName’;//pageName就是页面名称

说明:
对所有参与的页面,元件将分别保存不同的坐标值;对所有没有参与的页面,元件将保存同一个坐标值。
缓动功能 (位移的插值?)
按钮控制器 Button
单选组(RadioGroup)
在程序中,要获得或设置哪个按钮被选中也非常简单,使用控制器的selectedIndex或者selectedPage就可以了。

设置关联 ( 类似锚点,相对位置的设置 )

这里有一个垂直中线的关联,下面会说明,暂且不表。置可以看到,在文本内容发生变化时,文本的中线位置没有发生变化,位所以问号图标的置不变;但文本的右侧位发生了变化,所以叹号图标随之发生了移动,保持了与文本右侧的距离。
在代码中设置关联
(比如浏览器窗口被玩家拖大拖小),组件依然保持在右侧位置,那么可以这样调用:
aComoponet.addRelation(GRoot.inst, RelationType.Right_Right);

又例如希望一个动态添加到舞台的组件始终保持满屏大小,可以这样调用
aComoponet.setSize(GRoot.inst.width, GRoot.inst.height);
aComoponet.addRelation(GRoot.inst, RelationType.Size);

RelationType.Size相当于RelationType.Width_Width和RelationType.Height_Height的组合。这里强调一下,使组件变为满屏大小这个操作必须由你完成,也就是上面代码中的setSize调用。关联并不能完整这项任务,因为关联是不管元件当前的大小的,它只会在目标变化时保持两者大小的差别。

A:和容器组件”垂直中线“关联
B:和容器组件”右->右”关联
C:和容器组件“右->右”,”底->底“关联
D:和容器组件”底->底”,“垂直中线%”关联
E:和容器组件“底->底”关联
然后运行时把这个界面设置为满屏就可以了。
aComponent.SetSize(GRoot.inst.width, GRoot.inst.height);
aComoponet.addRelation(GRoot.inst, RelationType.Size);//当屏幕改变时仍然保持全屏

FairyGUI教程:(九)组件的扩展
FairyGUI提供的控件有:
图片(GImage)、动画(GMovieClip)、图形(GGraph)、文本(GTextField)、多信息文本(GRichTextFIeld)、列表(GList)、装载器(GLoader)、组件(GComponent)。这些都是非常基础的控件,他们是UI制作中的最小粒度。但仅靠这些基础控件是不足以制作出各种复杂的UI界面的。我们通过”扩展“功能,从组件(GComponent)中扩展出来以下这些复合组件:按钮(GButton),标签(GLabel),下拉框(GComboBox),进度条(GProgressBar),滑动条(GSlider),滚动条(GScrollbar)。

可以看到有六种“扩展”选择。组件可以随意在这些“扩展”中切换。选择哪种“扩展”,组件就有了哪个扩展的属性和行为特性。
指定统一点击音效
如果希望游戏中所有按钮的点击都有声音反馈,你并不需要每个按钮都设置一次声音属性,因为在游戏中可以使用代码UIConfig.buttonSound=’xxx’为所有按钮设置一个点击声音。
滚动条
运行时使用的滚动条需要通过以下代码设置:
UIConfig.horizontalScrollBar = ‘xxx’;
UIConfig.verticalScrollBar = ‘yyy’;
这里xxx和yyy就是滚动条资源的url。
UIConfig.defaultScrollBarDisplay = ScrollBarDisplayType.Auto; // 设置为,滚动时显示滚动条

9、自定义扩展
当基础组件、扩展组件都不能满足你的需求时,你可以编写自定义的扩展。使用API UIObjectFactory.setPackageItemExtension完成定义。例如:
UIObjectFactory.setPackageItemExtension(UIPackage.getItemURL(“包名“,”组件A”), MyComponent );
public class MyComponent extends GComponent
{
override protected function constructFromXML(xml:XML):void
{
super.constructFromXML(xml);
//在这里继续你的初始化
}
}
这样就为组件A指定了一个实现类MyComponent 。以后所有组件A创建出来的对象(包括在编辑器里使用的组件A)都为MyComponent 类型。例如:
var obj:MyComponent = UIPackage.createObject(“包名“, ”组件A”) as MyComponent ;
注意:如果组件A只是一个普通的组件,没有定义“扩展”,那么基类是GComponent,如上例所示;如果组件A的扩展是按钮,那么MyComponent的基类应该为GButton,如果扩展是进度条,那么基类应该为GProgressBar,等等。这个不要弄错。

控制器Tween效果
动效的播放在代码中启动,例如:
someComponent.GetTransition(“peng”).Play();
Play有多种原型,例如可以重复播放一定次数,可以在播放结束时回调等。要中途停止动效的播放,可以调用:
someComponent.GetTransition(“peng”).Stop();
Stop方法也可以带参数,原型是
public void Stop(bool setToComplete, bool processCallback);
setToComplete表示是否将组件的状态设置到播放完成的状态,如果否,组件的状态就会停留在当前时间。processCallback是否调用Play设定的播放完成回调函数。

注意:UI动效播放完毕后,组件的状态将停留在最后一帧,而不是回到第一帧,如果你希望动效播放完后组件的状态复原到播放前,你可以最后添加一帧重新设置组件的状态。

高级用法:
public void SetValue(string label, params object[] aParams)
改变指定帧的数值,例如某帧Label为aa,这帧是设置某个元件的XY值的,那么调用setValue(aa, 100,200)就可以将原来此帧设置XY的数值改为100,200。
public void SetHook(string label, TransitionHook callback)
可以设定运行到某帧时发生一个调用。

多国语言支持
2、运行时动态加载语言文件。这种方法相对比较灵活。
Unity代码片段:
string fileContent; //自行载入语言文件,这里假设已载入到此变量
FairyGUI.Utils.XML xml = new FairyGUI.Utils.XML(fileContent);
UIPackage.SetStringsSource(xml);

(十二)UI适配策略 (分辨率 设置)
GRoot.inst.setContentScaleFactor(设计分辨率宽度,设计分辨率高度,适配策略);
someComponent.setSize(GRoot.inst.width, GRoot.inst.height);// 满屏

GRoot是FairyGUI的根组件,它的大小与屏幕分辨率的关系为
GRoot.inst.width = 屏幕宽度/GRoot.contentScaleFactor;
GRoot.inst.height = 屏幕高度/GRoot.contentScaleFactor;

(十三)显示架构API
FairyGUI在原生的渲染引擎上封装了一层显示对象结构。
基础类显示对象:
GObject:显示对象的基类。
GGraph:图形对象。对应于编辑器中显示的一个图形。
GImage:图片对象。对应于编辑器中显示的一个图片。
GMovieClip:动画对象。对应于编辑器中显示的一个动画。
GLoader:装载器对象。对应于编辑器中显示的一个装载器。
GTextField:文本对象。对应于编辑器中显示的一个文本。
GRichTextField:多媒体文本。对应于编辑器中显示的一个多媒体文本。
GTextInput:输入文本对象。对应于编辑器中显示的一个文本,且文本类型被设置为“输入”。
容器类显示对象:
GComponent:组件对象。对应于编辑器中创建的一个组件。
GList:列表对象。对应于编辑器中显示的一个列表。
容器的扩展:
GLabel:标签对象。当一个组件的扩展为“标签”时,即为此类型。
GButton:按钮对象。当一个组件的扩展为“按钮”时,即为此类型。
GComboBox:组合框对象。当一个组件的扩展为“组合框”时,即为此类型。
GScrollBar:滚动条对象。当一个组件的扩展为“滚动条”时,即为此类型。
GProgressBar:进度条对象。当一个组件的扩展为“进度条”时,即为此类型。
GSlider:滑动条对象。当一个组件的扩展为“滑动条”时,即为此类型。
FairyGUI和Flash/Cocos类似,采用树状的结构组织显示对象。容器可以包含一个或多个基础显示对象,也可以包含容器。这个树状结构称为显示列表。如下图:

处于最前面的且元件范围包含点击位置的元件将捕获鼠标或触摸事件,并且开始冒泡传递(请参考事件系统 Flash/Starling/Egret/Unity)
对于组件(GComponent),宽度x高度的范围内点击检测都是有效的,无论这个范围内是否有子元件。举个例子说明。组件A和组件B分别为:

将B先添加进舞台,然后再添加A到舞台,也就是说,A显示B的前面,效果如下图:

可以看到,虽然A在B的上面,但红色方块是可见的,因为A在此区域并没有内容。当点击图中绿色点的位置时,点击事件将在A上面触发,而B是点击不了的。这是因为在A的范围内,点击是不能穿透的。
如果希望A能被穿透应该怎样?可以通过GComponent.opaque属性设定。当设定为false时,这个组件将能被穿透。例如A.opaque=false,这时,只有点击4个白块时,A才接收到点击事件,如果点击绿色点位置,B将接收到点击事件。这个特性在设计一些全屏界面时尤其要注意。也可以在编辑器里直接设定:

滚动支持
你可以在编辑器里很方便的对一个组件添加滚动特性。如果一个组件在编辑器里设置为滚动,我们可以通过GComponent.scrollPane获得滚动控制对象。
ScrollPane提供了多个API访问和控制滚动:
ScrollPane.percX/ScrollPane.percY:获得或设置当前滚动的位置,以百分比来计算,取值范围是0-1。如果希望滚动条从当前值到设置值有一个动态变化的过程,可以使用ScrollPane.setPercX和ScrollPane.setPercY,这两个API提供了一个是否使用缓动的参数。
ScrollPane.posX/ScrollPane.posY:获得或设置当前滚动的位置,以像素来计算。取值范围决定于当前显示的内容大小与视口大小的差别。例如,一个垂直滚动的组件,如果视口大小为100像素,实际内容大小为300像素,那么posY的取值范围是0~200像素,当posY=200时,滚动条滚动到最下方。posX/posY与percX/percY不同在于,除非开发者自己设置或者用户拖动滚动条,percX/percY是不变的,举个例子,当前某滚动容器的percX=0.1, posX=100,如果往容器里添加一定内容后,内容的宽度增大,percX的值仍将保持不变,依然是0.1,但posX的值会发生相应的改变。所以如果希望容器内增删内容后滚动条不发生滚动,可以先记录posX/posY的位置,添加完之后再设置posX/posY为此前记录的位置。
ScrollPane.scrollToView:调整滚动位置,使指定的元件出现在视口内。
窗口系统
窗口使用前首先要设置窗口中需要显示的内容,这通常是在编辑器里制作好的,可以直接使用Window.contentPane进行设置。建议把设置contentPane等初始化操作放置到Window.onInit方法中。
另外,FairyGUI还提供了一套机制用于窗口动态创建。动态创建是指初始时仅指定窗口需要使用的资源,等窗口需要显示时才实际开始构建窗口的内容。首先需要在窗口的构造函数中调用Window.addUISource。这个方法需要一个IUISource类型的参数,而IUISource是一个接口,用户需要自行实现载入相关UI包的逻辑。当窗口第一次显示之前,IUISource的加载方法将会被调用,并等待载入完成后才返回执行Window.OnInit,然后窗口才会显示。
调用Window.show显示窗口的流程:

如果你需要窗口显示时播放动画效果,那么覆盖doShowAnimation编写你的动画代码,并且在动画结束后调用onShown。
覆盖onShown编写其他需要在窗口显示时处理的业务逻辑。
调用Window.hide隐藏窗口的流程:

如果你需要窗口隐藏时播放动画效果,那么覆盖doHideAnimation编写你的动画代码,并且在动画结束时调用Window.hideImmediately(注意不是直接调用onHide!)。
覆盖onHide编写其他需要在窗口隐藏时处理的业务逻辑。
通常窗口会包括一个可用于拖动的标题栏,关闭按钮等。我们在编辑器创建窗口组件时,可以为它创建一个名称为frame的组件,frame组件的扩展应该选择为“标签”,这样外部组件能够为其设置图标和标题属性。frame组件中约定的内容还包括:
closeButton:一个名称为closeButton的按钮将自动作为窗口的关闭按钮。
dragArea:一个名称为dragArea的图形(类型设置为空白)将自动作为窗口的检测拖动区域,当用户在此区域内按住并拖动时,窗口随之被拖动。
contentArea:一个名称为contentArea的图形(类型设置为空白)将作为窗口的主要内容区域,这个区域只用于Window.showModalWait。当调用Window.showModalWait时,窗口会被锁定,如果设定了contentArea,则只锁定contentArea指定的区域,否则锁定整个窗口。如果你希望窗口在modalWait状态下依然能够拖动和关闭,那么就不要让contentArea覆盖标题栏区域。
注意以上的约定均为可选,是否含有组件frame,或者组件frame里是否含有约定的功能组件,并不会影响窗口的正常显示和关闭。
Popup管理
在UI系统中我们经常需要一些弹出一些组件,这些组件在用户点击空白地方的情况下就会自动消失,又或者由开发者控制消失。GRoot提供了几个API管理这些Popup组件。
GRoot.showPopup:弹出一个组件,如果指定了目标,则会调整弹出的位置到目标的下方,形成一个下拉的效果。同时提供了参数可以用来指定是向上弹出或者向下弹出。
GRoot.hidePopup: 默认情况下,用户点击空白地方就会自动关闭弹出的组件。也可以调用此API手工关闭。不指定参数时,所有当前的弹出都关闭。
FairyGUI会根据组件的大小自动计算弹出位置,以确保组件显示不会超出屏幕。

(十五)事件系统:Unity
发表回复
Unity平台参考了Flash的事件机制,设计了自己独特的事件系统。EventDispatcher是事件分发的中心,GObject就是一个EventDispatcher。每个事件类型都对应一个EventListener,接收事件并调用处理函数。例如需要编写某个元件单击的处理逻辑:
aObject.onClick.Add(aCallback);
void aCallback()
{
//some logic
}
事件回调函数
每个事件可以注册一个或多个回调函数。函数原型为:
public delegate void EventCallback0();
public delegate void EventCallback1(EventContext context);
函数可以不带参数或带一个参数。
EventContext
EventContext是回调函数的参数类型。
EventContext.sender:获得事件的分发者。
EventContext.initiator:获得事件的发起者。一般来说,事件的分发者和发起者是相同的,但如果事件已发生冒泡,则可能不相同。参考下面冒泡的描述。
EventContext.type:事件类型。
EventContext.inputEvent:如果事件是键盘/触摸/鼠标事件,通过访问inputEvent对象可以获得这类事件的相关数据。
EventContext.data:事件的数据。根据事件不同,可以有不同的含义。
事件冒泡和事件捕获
一些特殊的事件,比如鼠标/触摸事件,具备向父组件传递的特性,这个传递过程叫做冒泡。例如当手指接触A元件时,A元件触发TouchBegin事件,然后A元件的父组件B触发TouchBegin事件,然后B的父组件C也触发TouchBegin事件,以此类推,直到舞台根部。这个设计保证了所有关联的显示对象都有机会处理触摸事件,而不只是最顶端的显示对象。
冒泡过程可以被打断,通过调用EventContext.StopPropagation()可以使冒泡停止向父组件推进。
从上面的冒泡过程可以看出,事件处理的顺序应该是:A’s listeners->B’s listeners->C’s listeners,这里还有一种机制可以让链路上任意一个对象可以提前处理事件,这就是事件捕获。事件捕获是反向的,例如在上面的例子中,就是C先捕获事件,然后是B,再到A。所以所有事件处理的完整顺序应该是:
C’s capture listeners->B’s capture listeners->A’s capture listeners->A’s listeners->B’s listeners->C’s listeners
捕获传递链是不能中止的,冒泡传递链可以通过StopPropagation中止。
事件捕获的设计可以使父元件优于子元件和孙子元件检查事件。
并非所有事件都有冒泡设计,请参考下面关于各个事件的说明。并非只有冒泡事件才有捕获设计。在非冒泡事件中,capture的回调优于普通回调,仅此而已,可以作为一个优先级特性来使用。
InputEvent
对键盘事件和鼠标/触摸事件,可以通过EventContext.inputEvent获得此类事件的相关数据。InputEvent.x/InputEvent.y:鼠标或手指的位置;这是舞台坐标,因为UI可能因为自适配发生了缩放,所以如果要转成UI元件中的坐标,要使用GObject.GlobalToLocal转换。
InputEvent.keyCode:按键代码;
InputEvent.modifiers:参考UnityEngine.EventModifiers。
InputEvent.mouseWheelDelta:鼠标滚轮滚动值。
InputEvent.touchId:拖动使用手指;在PC平台,该值为0,没有意义。
InputEvent.isDoubleClick:是否双击。
EventListener
EventListener.Add/EventListener.Remove:添加或删除一个回调,回调函数可以带一个参数或者不带参数。参数的类型是object,它的实际含义随不同事件不同而不同;
EventListener.AddCapture/EventListener.RemoveCapture:添加或删除一个捕获期回调。
事件类型
GObject.onClick:单击。冒泡事件。事件数据为InputEvent对象。
GObject.onRightClick:右键单击。冒泡事件。事件数据为InputEvent对象。
GObject.onTouchBegin:鼠标或手指按下。冒泡事件。事件数据为InputEvent对象。
GObject.onTouchEnd:鼠标或手指释放。冒泡事件。事件数据为InputEvent对象。
GObject.onRollOver:鼠标移入元件。事件数据为InputEvent对象。
GObject.onRollOut:鼠标移出元件。事件数据为InputEvent对象。
GObject.onAddedToStage:元件被添加到舞台。无事件数据。
GObject.onRemovedFromStage:元件从舞台移出。无事件数据。
GObject.onKeyDown:元件接收到按键事件。只有获得焦点的情况下才能接收按键事件。冒泡事件。事件数据为InputEvent对象。
GObject.onClickLink:文本中的链接被点击。事件数据为href值,字符串类型。
GObject.onPositionChanged:元件的位置改变。无事件数据。
GObject.onSizeChanged:元件的大小改变。无事件数据。
GObject.onDragStart/GObject.onDragEnd:拖动是指玩家按住元件拖动然后释放的过程。注意只有设置了GObject.draggable属性的元件才会触发拖动事件。拖动过程中可以获得两个通知:开始和结束。当onDragStart中,调用EventContext.PreventDefault()可以立刻取消拖动。无事件数据。
GTextField.onFocusIn/GTextField.onFocusOut:只有输入类型的文本才会触发这个事件。当输入文本获得焦点时,在移动设备上会弹出小键盘。无事件数据。
GTextField.onChanged:只有输入类型的文本才会触发这个事件。无事件数据。
GMovieClip.onPlayEnd:动画设定的播放次数已经播放完毕。无事件数据。
GList.onClickItem:当GList容器内的元件被点击时触发的事件类型;事件数据为当前点击的GObject对象。
GComponent.onScroll:如果容器是滚动类型容器,则滚动发生时产生该事件。
GComponent.onDrop:注意要和普通拖动区别,一个元件被拖动并释放后并不会触发Drop事件。Drop事件需配合DragDropManager使用。当DragDropManager拖动的图标在某个元件上释放时,这个元件就会触发onDrop。事件数据为DragDropManager.StartDrag中传递的source值。
GButton.onChanged:当单选或者多选按钮选中状态改变时会触发该事件(只有在用户点击后状态改变才会触发,如果是程序改变不会触发)。无事件数据。
GComboBox.onChanged:当用户从下拉列表中选择一项时触发该事件。无事件数据。
GSlider.onChanged:当用户拖动滑块改变Slider的值时触发该事件。无事件数据。
Controller.onChanged:当控制器页面改变会触发该事件(改变selectedIndex会触发该事件,setSelectedIndex则不会)。无事件数据。

(十八)在Unity项目中使用FairyGUI
1. 使用UIPanel
只需3步就可以使用将编辑器中制作好的界面放入到Unity的场景中。第一步,从GameObject菜单中选择FairyGUI->UIPanel:

第二步,在Inspector里点击PackageName或者ComponentName,将弹出选择组件的窗口:

第三步:这个窗口里列出了所有工程里能找到的UI包,选择一个包和组件,然后点击OK。

可以看到,UI组件的内容显示出来了。(注意:Unity4版本目前不支持显示内容,只能显示线框)
如果UI包修改了,或者其他一些情况导致UIPanel显示不正常,可以使用下面的菜单刷新:

当运行后,获得UIPanel的UI的方式是:
UIPanel panel = gameObject.GetComponent();
GComponent view = panel.ui;
与其他UIPackage.CreateObject创建出来的界面不同,UIPanel在GameObjec销毁时(手动销毁或者过场景等)时会一并销毁。
UIPane只保存了UI包的名称和组件的名称,它不对纹理或其他资源产生任何引用,也就是UI使用的资源不会包含在场景数据中。
在编辑状态下,无论UI组件引用了哪些UI包的资源,包括放置在Resources目录下的和不放置在Resources下的,都能够正确显示。但当运行后,UIPanel只能自动载入放置在Resources目录或其子目录下的UI包,也只会载入自身所在的UI包,其他情况的UI包(例如引用到的UI包或打包到AssetBundle的UI包)不能自动载入。你需要在UIPanel创建前使用UIPackage.AddPackage准备好这类UI包。UIPanel在Start事件或者第一次访问UIPanel.ui属性时创建UI界面,你仍然有机会在Awake里完成这些操作。
下面是UIPanel的一些属性说明:
Render Mode: 有三种:
Screen Space Overlay (默认值),表示这个UI在屏幕空间显示,这时Transform的Scale将被锁定,而且不建议修改Transform中的其他内容(让他们保持为0)。如果要修改面板在屏幕上的位置,使用UI Transform(参考下面关于UI Transform的说明)。
Screen Space Camera 表示这个UI在屏幕空间显示,但不使用FairyGUI默认的正交相机,而是使用指定的正交相机。
World Space 表示这个UI在世界空间显示,由透视相机渲染。默认的使用场景的主相机,如果不是,那么设置Render Camera。当使用这种模式时,使用Transfrom修改UI在世界空间中的位置、缩放、旋转。但你仍然可以使用UI Transform。
注意:Render Mode只定义了FairyGUI对待这个UI的方式,通常是坐标相关的操作(如点击检测等),但和渲染无关。UI由哪个相机渲染是由GameObject的layer决定的。所以如果发现UI没有显示出来,可以检查一下GameObject的layer是否正确。例如如果是Screen Space,GameObject应该在UI层,如果是WorldSpace,则是在Default层或者其他自定义的层次。
Render Camera:当Render Mode是Screen Space Camera或者World Space时可以设置。如果不设置,默认为场景的主相机。
Sorting Order:调整UIPanel的显示顺序。越大的显示在越前面。
Fairy Batching:是否启用Fairy Batching。关于Fairy Batching请参考下面的说明。切换这个值,可以在编辑模式下实时看到DrawCall的变化(切换后点击一下Game,Stat里显示的内容才会更新),这可以使你更加容易决定是否启用这项技术。
Touch Disabled:勾选后,将关闭点击检测。当这个UI没有可交互的内容时可以勾选以提高点击检测时的性能。例如头顶血条这些类型的UI,就可以勾选。
UI Transform:当Render Mode是Screen Space时可以使用这里的设置调整UI在屏幕上的位置。你仍然可以调整Transform改变UI的位置,但我不建议你这样做,因为Transform中的坐标位置是没有经过不同分辨率自适应的调整的。当Render Mode是World Space时,建议使用Transform设置UI的位置,你仍然可以调整UI Transform改变UI的位置,但调整的效果可能不那么直观。同时你可以使用Scene视图中下图所示的原点调整UI Transform的位置属性:

Fit Screen:这里可以设置UIPanel适配屏幕。
Fit Size:UI将铺满屏幕。
Fit Width And Set Middle:UI将横向铺满屏幕,然后上下居中。
Fit Height And Set Center:UI将纵向铺满屏幕,然后左右居中。
这里提供的选项不多,因为FairyGUI推荐的是在FairyGUI编辑器中整体设计,而不是在Unity里摆放小元件。例如如果需要不同的UI在屏幕上的各个位置布局,你应该在FairyGUI编辑器中创建一个全屏大小的组件,然后在里面放置各个子组件,再用关联控制布局;最后将这个全屏组件放置到Unity,将Fit Screen设置为Fit Size即可。错误的做法是把各个子组件放置到Unity里再布局。
HitTest Mode:这里可以设置UIPanel处理鼠标或触摸事件的方式。
Default: 这是默认的方式。FairyGUI会用内置的机制检测鼠标或触摸动作,不使用射线,UIPanel也不需要创建碰撞体,效能比较高。
Raycast: 在这种方式下,UIPanel将自动创建碰撞体,并且使用射线方式进行点击检测。这种方式适合于UIPanel还需要和其他3D对象互动的情况。
2. 动态载入UI界面
在很多情况下,你并不需要将UI界面放到场景中。使用代码载入编辑器制作好的界面也非常简单:
GComponent view = UIPackage.CreateObject(“包名”, “组件名”) as GComponent
动态载入的界面不会自动销毁,例如一个背包窗口,你并不需要在每次过场景都销毁。如果要销毁界面,调用Dispose方法即可,例如
view.Dispose();
也可以动态创建UIPanel为任意游戏对象挂接UI界面,方法为:
UIPanel panel = yourGameObject.AddComponent();
panel.packageName = “包名”;
panel.componentName = “组件名”;
panel.CreateUI();
UIPanel的生命周期将和yourGameObject保持一致。再次提醒,注意yourGameObject的layer。
3. 载入UI包
Unity项目载入UI包有以下几种方式,开发者可以根据项目需要选择其中一种或者多种混搭使用:
1)将打包后的文件直接发布到Unity的Resources目录或者其子目录下,

然后在代码中调用UIPackage.AddPackage(“demo”), demo就是发布时填写的文件名。如果在子目录下,调用UIPackage.AddPacakge(“路径/demo”)即可。
2)将发布后的文件打包为两个AssetBundle,即定义文件和资源各打包为一个bundle(desc_bundle+res_bundle)。这样做的好处是一般UI的更新都是修改元件位置什么的,不涉及图片资源的更新,那么只需要重新打包和推送desc_bundle就行了,不需要让玩家更新通常体积比较大的res_bouble,节省流量。打包程序由开发者按照自己熟悉的方式自行实现。以demo为例,请遵循以下规则打包:
a)demo.bytes单独打包为desc_bundle;
b)其他资源(demo@atlas0.png等),打包到res_bundle(在此例中就是atlas0和sprites)
然后在代码中调用UIPackage.AddPackage(desc_bundle, res_bundle)。bundle的载入由开发者自行实现。
3)将发布后的文件打包为一个AssetBundle。打包程序由开发者按照自己熟悉的方式自行实现。以demo为例,将demo.bytes和其他资源(demo@atlas0.png等),都放入bundle。然后在代码中调用UIPackage.AddPackage(bundle, bundle)。bundle的载入由开发者自行实现。
在使用AssetBundle的载入方案中,将由FairyGUI接管bundle并负责bundle资源的释放。
4. UI适配
可以通过两种方式设置UI自适应,第一种方式是在游戏的启动创建里任意对象挂一个FairyGUI/UIContentScaler组件:

这里选项的含义可以参考教程里关于UI适配的说明。
另外一种方式是通过代码,可以参考教程里关于UI适配的说明。
5.图集的处理
FairyGUI编辑器发布到Unity的资源通常包含一个或多个UI图集,以demo工程为例,这里demo@atlas0.png就是一个图集

图集是由UI编辑器自动生成的,但在Unity里可以改变图集的属性,常用的设置有:
1)Generate Mip Maps
不勾选。UI不使用Mip Maps功能。
2)Filter Mode
使用Bilinear,这样图像在缩放时能产生比较平滑的过渡效果,副作用是会产生一定的模糊。而且单色的图像缩放会产生不必要的渐变边缘。而使用Point,则图像在缩放时会块状化。

一般UI图集使用Bilinear即可。你也可以在UI编辑器里将图片安排到不同图集,然后每个图集设置不同的Filter Mode以满足特殊需求。
3)Max Size
一般设置到2048。
4)Format
UI图集一般都是PNG格式,并带有透明通道。同时,UI对画质的要求比较高,所以建议选择AutoMatic TrueColor。但TrueColor有一个最大的问题是文件大,而且占用内存较高,例如1024×1024的图集,将占用4MB的内存,2048×2048则达到16MB。
如果对内存使用比较敏感,也可以选用压缩格式,即AutoMatic Compressed。在桌面平台上即相同于DXT5,在Android平台上相当于ETC1,在IPhone平台则为PVRTC。这些格式能够大大降低内存占用,也因为它们是显卡直接支持的格式,所以Unity在载入时省去了解码位图的步骤,能够加快载入速度。但它们都是属于有损压缩,在显示质量上肯定不如TrueColor,特别是图集颜色十分丰富,或者有颜色渐变时,失真会比较厉害。特别重要的是,ETC1是不支持透明通道的,PVRTC对透明通道的支持也比较弱,所以并不适合带透明通道的图集。FairyGUI提供了ETC1/PVRTC+A的解决方案。首先,在发布时勾选“为Alpha通道创建单独的贴图”,如下图:

这样,就产生了两个不含透明通道的贴图,一张去除了原图透明通道的贴图,和一张将原贴图透明通道数值转换为等价灰度的贴图。这两张贴图都可以设置为Automatic Compressed。(一定要注意,不能再将主贴图设置为True Color)

FairyGUI提供了特制的着色器处理两张贴图的合并。开发者并不需要编写额外的代码就可以使用这项技术。
TIPS
你可以将本身就不含透明通道的位图(例如一些大型的背景图)安排到一张图集上。如果一张图集内的所有图片都不包含透明通道,那么最终输出的图集也不包含透明通道。不含透明通道的图集可以选用Automatic 16bits格式。
6. 字体的处理
FairyGUI使用Unity的动态字体技术渲染文字。只需以下几个简单的步骤就可以完成设置:
1)拷贝一个字体文件到项目的Resources目录或Resources/Fonts目录。字体文件可以随意使用一个,例如arial.ttf,或simhei.ttf等,这个ttf是什么字体并不影响最终显示字体的选择。
2)在Unity中点击字体文件,在inspector中修改字体属性

注意在Font Names中填写的是字体名称,标准的字体名称可以从这里找到: 中文字体的英文名称对照表。例如Droid Sans Fallback是Android平台支持中文的内置默认字体之一。多个字体用逗号隔开,Unity会使用第一个在系统中能找到的字体。
3)设置UIConfig.defaultFont=”字体文件名称”即可,注意这里使用的是文件名称,也就是说,如果放置在Resources目录的是arial.ttf,则UIConfig.defaultFont=”arial”,不需要带.ttf后缀。
因为我们没有勾选Include Font Data,所以无论这个字体文件有多大,最终并不会包含在我们的发布包中,也就是说不会增大发行包的体积。Unity会在实际运行的系统中查找与Font Names匹配的第一个字体,并使用此字体进行动态渲染。
如果你是在Unity5.x平台开发,除了上述方式外,得益于Unity5.x引入的新的字体管理API,也可以不放置字体文件到Resources目录,你只需要直接设置UIConfig.defaultFont=”字体名称“即可,同样,多个字体名称用逗号隔开。例如:UIConfig.defaultFont=”Droid Sans Fallback, LTHYSZK, Helvetica-Bold, Microsoft YaHei, SimHei”;
4)如果你的界面使用了多种字体,比如对单独的文字设置了字体:

这里用到了“黑体”这个名字的字体,这是与UIConfig.defaultFont里设置的不同的字体,我们也需要注册这种字体。方法是,首先做好上面1)和2),假如字体文件名称是HeiTi.ttf,然后
FontManager.RegisterFont(“黑体”, FontManager.GetFont(“HeiTi”));
RegisterFont的第一个参数对应编辑器里使用的字体名称;第二个参数,是Unity中放入的字体文件资源。如果文件带路径,这也需要把路径填上。
5)当你使用部分字体的粗体效果时,你会发现粗体的效果在Unity中的显示不正确,这是因为有些字体不带粗体效果的,这时候Unity就会用拉宽来实现,就像变扁了。fairygui可以用额外的mesh来解决粗体的显示。方法是:
FontManager.GetFont(“字体路径”).customFold = true;
这里字体路径与UIConfig.default里设置的内容应该完全一样。
6)某些字体,unity渲染有粗体效果,但当设置成斜体时,粗体效果又丢失(例如雅黑)。fairygui在这种情况取消unity默认渲染粗体的效果,改为增加额外的面渲染粗体。激活这个功能的方法是
FontManager.GetFont(“字体路径”).customBoldAndItalic = true;
如果已经设置了customBold,不需要再设置customBoldAndItalic。
备注1
动态字体要求玩家的运行系统环境中有你设定的字体,如果没有,实际使用的字体可能并不是你想要的效果。因此,你可以选择嵌入整个字体文件。方法是把你要用的字体文件放到Resources目录,并把Include Font Data勾选,这样整个字体文件就会包含到最终的发布包中,坏处就是会大大增加发行包的体积。
备注2
在实际游戏制作过程中发现在桌面平台下Unity对中文字体的渲染稍显模糊和暗淡,因此FairyGUI使用了特制的着色器解决了这个问题。以下是Unity默认的字体渲染效果和FairyGUI的字体渲染效果的比较:

可以看见经过FairyGUI的特殊处理,中文文字更清晰更亮。
只有在桌面平台下FairyGUI才会开启这种技术,移动平台永远不会开启,因为在高DPI情况下,字体默认的渲染效果已经非常漂亮。
另外,如果你不喜欢这种显示效果,或者你使用的是全英文的文字,也可以手动把这种技术关闭:UIConfig.renderingTextBrighterOnDesktop = false;
7 在UI中穿插其他3D对象
FairyGUI底层使用Renderer.sortingOrder来决定对象的渲染顺序,利用这个特性,我们可以很容易的将UI对象与其他3D对象(例如例子)安排在一起。FairyGUI提供了封装类GoWrapper,可以直接包装其他GameObject插入到UI层次中。如下图,一个粒子特效被安排到了UI之间。

又例如,一个3D模型穿插在UI中间

详细实现方法可以参考FairyGUI-unity包中的Assets/FairyGUI/Examples/Particles和Assets/FairyGUI/Examples/Model。
8. 与RenderTexture配合
在UI上展现3D内容的另一种方式是使用RenderTexture,特别是需要进行剪裁的情况下(如果不需要剪裁,推荐直接放置3D内容,免除RenderTexture性能消耗。参考6. 在UI中穿插其他3D对象)。在FairyGUI中,可以将Image.texture设置为一个RenderTexture,然后就可以像使用普通图片一样使用这个RenderTexture了,可以出现在任何地方,包括滚动容器。特别地,FairyGUI还能支持将RenderTexture所在位置的背景图片影射到RenderTexture渲染相机的背景上,这样就不用担心透贴的问题了。如下图,NPC使用RenderTexture渲染到UI窗口中。

详细实现方法可以参考FairyGUI-unity包中的Assets/FairyGUI/Examples/RenderTexture。
9. Drawcall优化
在Unity中,每次引擎准备数据并通知GPU的过程称为一次Draw Call(DC)。Draw Call次数是一项非常重要的性能指标。UI系统一般包含数量众多的物体,有效控制DC是衡量一个UI系统是否实用的关键因素,特别是在移动设备上。
我们先来看看NGUI是怎么做的,NGUI把UIPanel中的Widget按depth排序,然后将相同材质的Widget进行Mesh合并,例如使用相同图集的图片,或者文字。Mesh合并的优点是合并后这些Widget就只产生一个DC。但这个合并过程需要计算所有Widget坐标相对于Panel的变换,而且如果Widget行为改变,例如平移,缩放等,都会触发Mesh重新合并,这会带来一定的CPU消耗,这就需要开发者谨慎组织UI元素到各个UIPanel,并且对深度需要细致安排,否则达不到减少DC效果的同时更可能带来比较大的CPU消耗。
FairyGUI没有采取合并Mesh的策略,原因有两个:
● FairyGUI使用的是树状显示对象结构,各个元件之间的层次关系非常复杂;
● FairyGUI编辑器给予用户最大的设计自由度,加上动效的引入,各个元件的状态都可能非常动态;
FairyGUI基于Unity的Dynamic Batching技术,提供了深度调整技术进行 drawcall优化 。FairyGUI能在不改变最终显示效果的前提下,尽可能的把相同材质的物体调整到连续的renderingOrder值上,以促使他们能够被Unity Dynamic Batching优化。Dynamic Batching是Unity提供的Draw Call Batching技术之一。如果动态物体共用着相同的材质,那么Unity会自动对这些物体进行批处理。但Dynamic Batching的一个重要的前提是这些动态物体是连续渲染的。先来看看FairyGUI中物体的渲染顺序,例如:

这里有4个按钮,每个按钮都是一个组件,每个组件里包含一个图片和一个文字对象。FairyGUI是树状的显示对象结构,那么他们按深度排序应该是:

因为文字和图片的材质并不相同,所以每次从文字到图片都产生上下文切换,所以产生了6个DC。
FairyGUI的深度调整技术可以优化这种情况。观察一下,其实四个按钮之间并不相交,所以FairyGUI智能地将渲染顺序调整为:

因为FairyGUI使用了图集,而且动态文字也使用了相同的贴图,这样,DC就降低到了2个,达到了优化的目的。实际情况会比这个复杂很多,但FairyGUI能在不改变最终显示效果的前提下,尽可能的把相同材质的物体调整到连续的renderingOrder值上,以促使他们能够被Unity Dynamic Batching优化。而对开发者来说,这些底层上的调整是透明的,也就是不会影响原来的显示对象层次。从效率上考虑,这种技术仅比较物体之间的显示矩形区域(一个Rect)是否相交,所以速度是非常快的,不会带来过多的CPU负荷。
FairyGUI提供了一个开关控制组件是否启用深度调整,API是:
GComponent someComponent;
someComponent.fairyBatching = true;
如果某个组件设置了fairyBatching,那么无需在子组件和孙子组件再启用fairyBatching。一般只在顶层组件打开这个功能,例如主界面,加载界面等。注意,Window这个类已经自动打开了fairyBatching,这符合我们的使用习惯,因为一般我们都是以窗口为单位安排功能的。如果界面不复杂,Draw Call本来就不高的情况下,开发者也可以忽略这个功能,从10个DC优化到8个DC并没有什么意义。在实际使用过程中,
对于打开了fairyBatching的组件,当开发者动态改变子元件或者孙子元件的位置或大小时,并不会自动触发深度调整,例如一个图片原来显示在一个窗口里的顶层,你用Tween将它从原来的位置移到另外一个位置,这个图片就有可能被窗口里的其他元素遮挡。这时开发者可以手动触发深度调整,API是
someObject.InvalidateBatchingState();
这个API并不需要由开启了fairyBatching的组件调用,它可以用任何一个内含的元件发起。对于UI动效(Transitions),FairyGUI已经自动调用了这个API,所以开发者不必处理。
下载并运行Demo,可以观察fairyBatching的实际效果。例如这个Demo的首页:

设置了fairyBatching后由42个DC减少到了6个DC,另外,可以看到Saved by batching: 27的字样。

与ULUA配合
一、安装
1、将以下语句添加到Assets\uLua\Editor\WrapFile.cs适当的位置,然后调用Lua的菜单Gen Lua Wrap Files,重新生成绑定文件。
_GT(typeof(EventContext)),
_GT(typeof(EventDispatcher)),
_GT(typeof(EventListener)),
_GT(typeof(InputEvent)),
_GT(typeof(DisplayObject)),
_GT(typeof(Container)),
_GT(typeof(Stage)),
_GT(typeof(Controller)),
_GT(typeof(GObject)),
_GT(typeof(GGraph)),
_GT(typeof(GGroup)),
_GT(typeof(GImage)),
_GT(typeof(GLoader)),
_GT(typeof(PlayState)),
_GT(typeof(GMovieClip)),
_GT(typeof(TextFormat)),
_GT(typeof(GTextField)),
_GT(typeof(GRichTextField)),
_GT(typeof(GTextInput)),
_GT(typeof(GComponent)),
_GT(typeof(GList)),
_GT(typeof(GRoot)),
_GT(typeof(GLabel)),
_GT(typeof(GButton)),
_GT(typeof(GComboBox)),
_GT(typeof(GProgressBar)),
_GT(typeof(GSlider)),
_GT(typeof(PopupMenu)),
_GT(typeof(ScrollPane)),
_GT(typeof(Transition)),
_GT(typeof(UIPackage)),
_GT(typeof(Window)),
_GT(typeof(GObjectPool)),
_GT(typeof(Relations)),
_GT(typeof(RelationType)),
2、拷贝FairyGUI.lua到你的lua文件存放目录。
二、使用
1、普通方法的侦听和删除侦听
require ‘FairyGUI’
function OnClick() –也可以带上事件参数,OnClick(context)
print(‘you click’)
end
UIPackage.AddPackage(‘Demo’)
local view = UIPackage.CreateObject(‘Demo’, ‘DemoMain’)
GRoot.inst:AddChild(view)
view.onClick:Add(OnClick)
–view.onClick:Remove(OnClick)
2、类方法的侦听和删除侦听
require ‘FairyGUI’
TestClass = class(‘TestClass’, {})
function TestClass:ctor()
UIPackage.AddPackage(‘Demo’)
self.view = UIPackage.CreateObject(‘Demo’, ‘DemoMain’)
GRoot.inst:AddChild(self.view)
self.view.onClick:Add(TestClass.OnClick, self)
–self.view.onClick:Remove(TestClass.OnClick, self)
end
function TestClass:OnClick() –也可以带上事件参数,TestClass:OnClick(context)
print(‘you click’)
end
TestClass.New()
3、如果要使用Tween,你可以直接使用GObject.TweenXXXX系列函数,免除了Wrap DOTween的麻烦。

Unity3D占用内存太大的解决方法[转]

Unity3D 里有两种动态加载机制:一个是Resources.Load,另外一个通过AssetBundle,其实两者区别不大。 Resources.Load就是从一个缺省打进程序包里的AssetBundle里加载资源,而一般AssetBundle文件需要你自己创建,运行时 动态加载,可以指定路径和来源的。

其实场景里所有静态的对象也有这么一个加载过程,只是Unity3D后台替你自动完成了。

详细说一下细节概念:
AssetBundle运行时加载:
来自文件就用CreateFromFile(注意这种方法只能用于standalone程序)这是最快的加载方法
也可以来自Memory,用CreateFromMemory(byte[]),这个byte[]可以来自文件读取的缓冲,www的下载或者其他可能的方式。
其实WWW的assetBundle就是内部数据读取完后自动创建了一个assetBundle而已
Create完以后,等于把硬盘或者网络的一个文件读到内存一个区域,这时候只是个AssetBundle内存镜像数据块,还没有Assets的概念。
Assets加载:
用AssetBundle.Load(同Resources.Load) 这才会从AssetBundle的内存镜像里读取并创建一个Asset对象,创建Asset对象同时也会分配相应内存用于存放(反序列化)
异步读取用AssetBundle.LoadAsync
也可以一次读取多个用AssetBundle.LoadAll
AssetBundle的释放:
AssetBundle.Unload(flase)是释放AssetBundle文件的内存镜像,不包含Load创建的Asset内存对象。
AssetBundle.Unload(true)是释放那个AssetBundle文件内存镜像和并销毁所有用Load创建的Asset内存对象。

一个Prefab从assetBundle里Load出来 里面可能包括:Gameobject transform mesh texture material shader script和各种其他Assets。
你 Instaniate一个Prefab,是一个对Assets进行Clone(复制)+引用结合的过程,GameObject transform 是Clone是新生成的。其他mesh / texture / material / shader 等,这其中些是纯引用的关系的,包括:Texture和TerrainData,还有引用和复制同时存在的,包括:Mesh/material /PhysicMaterial。引用的Asset对象不会被复制,只是一个简单的指针指向已经Load的Asset对象。这种含糊的引用加克隆的混合, 大概是搞糊涂大多数人的主要原因。
专门要提一下的是一个特殊的东西:Script Asset,看起来很奇怪,Unity里每个Script都是一个封闭的Class定义而已,并没有写调用代码,光Class的定义脚本是不会工作的。其 实Unity引擎就是那个调用代码,Clone一个script asset等于new一个class实例,实例才会完成工作。把他挂到Unity主线程的调用链里去,Class实例里的OnUpdate OnStart等才会被执行。多个物体挂同一个脚本,其实就是在多个物体上挂了那个脚本类的多个实例而已,这样就好理解了。在new class这个过程中,数据区是复制的,代码区是共享的,算是一种特殊的复制+引用关系。
你可以再Instaniate一个同样的Prefab,还是这套mesh/texture/material/shader…,这时候会有新的GameObject等,但是不会创建新的引用对象比如Texture.
所以你Load出来的Assets其实就是个数据源,用于生成新对象或者被引用,生成的过程可能是复制(clone)也可能是引用(指针)
当你Destroy一个实例时,只是释放那些Clone对象,并不会释放引用对象和Clone的数据源对象,Destroy并不知道是否还有别的object在引用那些对象。
等到没有任何 游戏场景物体在用这些Assets以后,这些assets就成了没有引用的游离数据块了,是UnusedAssets了,这时候就可以通过 Resources.UnloadUnusedAssets来释放,Destroy不能完成这个任 务,AssetBundle.Unload(false)也不行,AssetBundle.Unload(true)可以但不安全,除非你很清楚没有任何 对象在用这些Assets了。
配个图加深理解:

Unity3D占用内存太大怎么解决呢?

虽然都叫Asset,但复制的和引用的是不一样的,这点被Unity的暗黑技术细节掩盖了,需要自己去理解。

关于内存管理
按照传统的编程思维,最好的方法是:自己维护所有对象,用一个Queue来保存所有object,不用时该Destory的,该Unload的自己处理。
但这样在C# .net框架底下有点没必要,而且很麻烦。
稳妥起见你可以这样管理

创建时:
先建立一个AssetBundle,无论是从www还是文件还是memory
用AssetBundle.load加载需要的asset
加载完后立即AssetBundle.Unload(false),释放AssetBundle文件本身的内存镜像,但不销毁加载的Asset对象。(这样你不用保存AssetBundle的引用并且可以立即释放一部分内存)
释放时:
如果有Instantiate的对象,用Destroy进行销毁
在合适的地方调用Resources.UnloadUnusedAssets,释放已经没有引用的Asset.
如果需要立即释放内存加上GC.Collect(),否则内存未必会立即被释放,有时候可能导致内存占用过多而引发异常。
这样可以保证内存始终被及时释放,占用量最少。也不需要对每个加载的对象进行引用。

当然这并不是唯一的方法,只要遵循加载和释放的原理,任何做法都是可以的。

系统在加载新场景时,所有的内存对象都会被自动销毁,包括你用AssetBundle.Load加载的对象和Instaniate克隆的。但是不包括AssetBundle文件自身的内存镜像,那个必须要用Unload来释放,用.net的术语,这种数据缓存是非托管的。

总结一下各种加载和初始化的用法:
AssetBundle.CreateFrom…..:创建一个AssetBundle内存镜像,注意同一个assetBundle文件在没有Unload之前不能再次被使用
WWW.AssetBundle:同上,当然要先new一个再 yield return 然后才能使用
AssetBundle.Load(name): 从AssetBundle读取一个指定名称的Asset并生成Asset内存对象,如果多次Load同名对象,除第一次外都只会返回已经生成的Asset 对象,也就是说多次Load一个Asset并不会生成多个副本(singleton)。
Resources.Load(path&name):同上,只是从默认的位置加载。
Instantiate(object):Clone 一个object的完整结构,包括其所有Component和子物体(详见官方文档),浅Copy,并不复制所有引用类型。有个特别用法,虽然很少这样 用,其实可以用Instantiate来完整的拷贝一个引用类型的Asset,比如Texture等,要拷贝的Texture必须类型设置为 Read/Write able。

总结一下各种释放
Destroy: 主要用于销毁克隆对象,也可以用于场景内的静态物体,不会自动释放该对象的所有引用。虽然也可以用于Asset,但是概念不一样要小心,如果用于销毁从文 件加载的Asset对象会销毁相应的资源文件!但是如果销毁的Asset是Copy的或者用脚本动态生成的,只会销毁内存对象。
AssetBundle.Unload(false):释放AssetBundle文件内存镜像
AssetBundle.Unload(true):释放AssetBundle文件内存镜像同时销毁所有已经Load的Assets内存对象
Reources.UnloadAsset(Object):显式的释放已加载的Asset对象,只能卸载磁盘文件加载的Asset对象
Resources.UnloadUnusedAssets:用于释放所有没有引用的Asset对象
GC.Collect()强制垃圾收集器立即释放内存 Unity的GC功能不算好,没把握的时候就强制调用一下

在3.5.2之前好像Unity不能显式的释放Asset

举两个例子帮助理解
例子1:
一个常见的错误:你从某个AssetBundle里Load了一个prefab并克隆之:obj = Instaniate(AssetBundle1.Load(‘MyPrefab”);
这个prefab比如是个npc
然后你不需要他的时候你用了:Destroy(obj);你以为就释放干净了
其实这时候只是释放了Clone对象,通过Load加载的所有引用、非引用Assets对象全都静静静的躺在内存里。
这种情况应该在Destroy以后用:AssetBundle1.Unload(true),彻底释放干净。
如果这个AssetBundle1是要反复读取的 不方便Unload,那可以在Destroy以后用:Resources.UnloadUnusedAssets()把所有和这个npc有关的Asset都销毁。
当然如果这个NPC也是要频繁创建 销毁的 那就应该让那些Assets呆在内存里以加速游戏体验。
由此可以解释另一个之前有人提过的话题:为什么第一次Instaniate 一个Prefab的时候都会卡一下,因为在你第一次Instaniate之前,相应的Asset对象还没有被创建,要加载系统内置的 AssetBundle并创建Assets,第一次以后你虽然Destroy了,但Prefab的Assets对象都还在内存里,所以就很快了。

顺便提一下几种加载方式的区别:
其实存在3种加载方式:
一是静态引用,建一个public的变量,在Inspector里把prefab拉上去,用的时候instantiate
二是Resource.Load,Load以后instantiate
三是AssetBundle.Load,Load以后instantiate
三种方式有细 节差异,前两种方式,引用对象texture是在instantiate时加载,而assetBundle.Load会把perfab的全部assets 都加载,instantiate时只是生成Clone。所以前两种方式,除非你提前加载相关引用对象,否则第一次instantiate时会包含加载引用 assets的操作,导致第一次加载的lag。

例子2:
从磁盘读取一个1.unity3d文件到内存并建立一个AssetBundle1对象
AssetBundle AssetBundle1 = AssetBundle.CreateFromFile(“1.unity3d”);
从AssetBundle1里读取并创建一个Texture Asset,把obj1的主贴图指向它
obj1.renderer.material.mainTexture = AssetBundle1.Load(“wall”) as Texture;
把obj2的主贴图也指向同一个Texture Asset
obj2.renderer.material.mainTexture =obj1.renderer.material.mainTexture;
Texture是引用对象,永远不会有自动复制的情况出现(除非你真需要,用代码自己实现copy),只会是创建和添加引用
如果继续:
AssetBundle1.Unload(true) 那obj1和obj2都变成黑的了,因为指向的Texture Asset没了
如果:
AssetBundle1.Unload(false) 那obj1和obj2不变,只是AssetBundle1的内存镜像释放了
继续:
Destroy(obj1),//obj1被释放,但并不会释放刚才Load的Texture
如果这时候:
Resources.UnloadUnusedAssets();
不会有任何内存释放 因为Texture asset还被obj2用着
如果
Destroy(obj2)
obj2被释放,但也不会释放刚才Load的Texture
继续
Resources.UnloadUnusedAssets();
这时候刚才load的Texture Asset释放了,因为没有任何引用了
最后CG.Collect();
强制立即释放内存
由此可以引申出论坛里另一个被提了几次的问题,如何加载一堆大图片轮流显示又不爆掉
不考虑AssetBundle,直接用www读图片文件的话等于是直接创建了一个Texture Asset
假设文件保存在一个List里
TLlist<string> fileList;
int n=0;
IEnumerator OnClick()
{
WWW image = new www(fileList[n++]);
yield return image;
obj.mainTexture = image.texture;

n = (n>=fileList.Length-1)?0:n;
Resources.UnloadUnusedAssets();
}
这样可以保证内存里始终只有一个巨型Texture Asset资源,也不用代码追踪上一个加载的Texture Asset,但是速度比较慢
或者:
IEnumerator OnClick()
{
WWW image = new www(fileList[n++]);
yield return image;
Texture tex = obj.mainTexture;
obj.mainTexture = image.texture;

n = (n>=fileList.Length-1)?0:n;
Resources.UnloadAsset(tex);
}
这样卸载比较快

 

 

Hog的评论引用:

感觉这是Unity内存管理暗黑和混乱的地方,特别是牵扯到Texture
我最近也一直在测试这些用AssetBundle加载的asset一样可以用Resources.UnloadUnusedAssets卸载,但必须先AssetBundle.Unload,才会被识别为无用的asset。比较保险的做法是
创建时:
先建立一个AssetBundle,无论是从www还是文件还是memory
用AssetBundle.load加载需要的asset
用完后立即AssetBundle.Unload(false),关闭AssetBundle但不摧毁创建的对象和引用
销毁时:
对Instantiate的对象进行Destroy
在合适的地方调用Resources.UnloadUnusedAssets,释放已经没有引用的Asset.
如果需要立即释放加上GC.Collect()
这样可以保证内存始终被及时释放
只要你Unload过的AssetBundle,那些创建的对象和引用都会在LoadLevel时被自动释放。

 

全面理解Unity加载和内存管理机制之二:进一步深入和细节
Unity几种动态加载Prefab方式的差异:
其实存在3种加载prefab的方式:
一是静态引用,建一个public的变量,在Inspector里把prefab拉上去,用的时候instantiate
二是Resource.Load,Load以后instantiate
三是AssetBundle.Load,Load以后instantiate
三种方式有细节差异,前两种方式,引用对象texture是在instantiate时加载,而assetBundle.Load会把perfab的全部 assets都加载,instantiate时只是生成Clone。所以前两种方式,除非你提前加载相关引用对象,否则第一次instantiate时会 包含加载引用类assets的操作,导致第一次加载的lag。官方论坛有人说Resources.Load和静态引用是会把所有资源都预先加载的,反复测试的结果,静态引用和Resources.Load也是OnDemand的,用到时才会加载。

几种AssetBundle创建方式的差异:
CreateFromFile:这种方式不会把整个硬盘AssetBundle文件都加载到 内存来,而是类似建立一个文件操作句柄和缓冲区,需要时才实时Load,所以这种加载方式是最节省资源的,基本上AssetBundle本身不占什么内 存,只需要Asset对象的内存。可惜只能在PC/Mac Standalone程序中使用。
CreateFromMemory和www.assetBundle:这两种方式AssetBundle文件会整个镜像于内存中,理论上文件多大就需要多大的内存,之后Load时还要占用额外内存去生成Asset对象。

什么时候才是UnusedAssets?
看一个例子:
Object obj = Resources.Load(“MyPrefab”);
GameObject instance = Instantiate(obj) as GameObject;
………
Destroy(instance);
创建随后销毁了一个Prefab实例,这时候 MyPrefab已经没有被实际的物体引用了,但如果这时:
Resources.UnloadUnusedAssets();
内存并没有被释放,原因:MyPrefab还被这个变量obj所引用
这时候:
obj  = null;
Resources.UnloadUnusedAssets();
这样才能真正释放Assets对象
所以:UnusedAssets不但要没有被实际物体引用,也要没有被生命周期内的变量所引用,才可以理解为 Unused(引用计数为0)
所以所以:如果你用个全局变量保存你Load的Assets,又没有显式的设为null,那 在这个变量失效前你无论如何UnloadUnusedAssets也释放不了那些Assets的。如果你这些Assets又不是从磁盘加载的,那除了 UnloadUnusedAssets或者加载新场景以外没有其他方式可以卸载之。

一个复杂的例子,代码很丑陋实际也不可能这样做,只是为了加深理解

IEnumerator OnClick()

{

Resources.UnloadUnusedAssets();//清干净以免影响测试效果

yield return new WaitForSeconds(3);

float wait = 0.5f;

//用www读取一个assetBundle,里面是一个Unity基本球体和带一张大贴图的材质,是一个Prefab

WWW aa = new WWW(@”file://SpherePrefab.unity3d”);

yield return aa;

AssetBundle asset = aa.assetBundle;

yield return new WaitForSeconds(wait);//每步都等待0.5s以便于分析结果

Texture tt = asset.Load(“BallTexture”) as  Texture;//加载贴图

yield return new WaitForSeconds(wait);

GameObject ba = asset.Load(“SpherePrefab”) as  GameObject;//加载Prefab

yield return new WaitForSeconds(wait);

GameObject obj1 = Instantiate(ba) as GameObject;//生成实例

yield return new WaitForSeconds(wait);

Destroy(obj1);//销毁实例

yield return new WaitForSeconds(wait);

asset.Unload(false);//卸载Assetbundle

yield return new WaitForSeconds(wait);

Resources.UnloadUnusedAssets();//卸载无用资源

yield return new WaitForSeconds(wait);

ba = null;//将prefab引用置为空以后卸无用载资源

Resources.UnloadUnusedAssets();

yield return new WaitForSeconds(wait);

tt = null;//将texture引用置为空以后卸载无用资源

Resources.UnloadUnusedAssets();

}

这是测试结果的内存Profile曲线图

Unity3D占用内存太大怎么解决呢?

图片:p12.jpg

很经典的对称造型,用多少释放多少。

这是各阶段的内存和其他数据变化

说明:
1        初始状态
2        载入AssetBundle文件后,内存多了文件镜像,用量上升,Total Object和Assets增加1(AssetBundle也是object)
3        载入Texture后,内存继续上升,因为多了Texture Asset,Total Objects和Assets增加1
4        载入Prefab后,内存无明显变化,因为最占内存的Texture已经加载,Materials上升是因为多了Prefab的材质,Total Objects和Assets增加6,因为 Perfab 包含很多 Components
5        实例化Prefab以后,显存的Texture Memory、GameObjectTotal、Objects in Scene上升,都是因为实例化了一个可视的对象
6        销毁实例后,上一步的变化还原,很好理解
7        卸载AssetBundle文件后,AssetBundle文件镜像占用的内存被释放,相应的Assets和Total Objects Count也减1
8        直接Resources.UnloadUnusedAssets,没有任何变化,因为所有Assets引用并没有清空
9        把Prefab引用变量设为null以后,整个Prefab除了Texture外都没有任何引用了,所以被UnloadUnusedAssets销毁,Assets和Total Objects Count减6
10        再把Texture的引用变量设为null,之后也被UnloadUnusedAssets销毁,内存被释放,assets和Total Objects Count减1,基本还原到初始状态

从中也可以看出:
Texture加载以后是到内存,显示的时候才进入显存的Texture Memory。
所有的东西基础都是Object
Load的是Asset,Instantiate的是GameObject和Object in Scene
Load的Asset要Unload,new的或者Instantiate的object可以Destroy

原文地址

 

Unity 3D中的内存管理[转]

Unity3D在内存占用上一直被人诟病,特别是对于面向移动设备的游戏开发,动辄内存占用飙上一两百兆,导致内存资源耗尽,从而被系统强退造成极 差的体验。类似这种情况并不少见,但是绝大部分都是可以避免的。虽然理论上Unity的内存管理系统应当为开发者分忧解难,让大家投身到更有意义的事情中 去,但是对于Unity对内存的管理方式,官方文档中并没有太多的说明,基本需要依靠自己摸索。最近在接手的项目中存在严重的内存问题,在参照文档和 Unity Answer众多猜测和证实之后,稍微总结了下Unity中的内存的分配和管理的基本方式,在此共享。

虽然Unity标榜自己的内存使用全都是“Managed Memory”,但是事实上你必须正确地使用内存,以保证回收机制正确运行。如果没有做应当做的事情,那么场景和代码很有可能造成很多非必要内存的占用, 这也是很多Unity开发者抱怨内存占用太大的原因。接下来我会介绍Unity使用内存的种类,以及相应每个种类的优化和使用的技巧。遵循使用原则,可以 让非必要资源尽快得到释放,从而降低内存占用。

 

Unity中的内存种类
实际上Unity游戏使用的内存一共有三种:程序代码、托管堆(Managed Heap)以及本机堆(Native Heap)。

程序代码包括了所有的Unity引擎,使用的库,以及你所写的所有的游戏代码。在编译后,得到的运行文件将会被加载到设备中执行,并占用一定内存。

这部分内存实际上是没有办法去“管理”的,它们将在内存中从一开始到最后一直存在。一个空的Unity默认场景,什么代码都不放,在iOS设备上占 用内存应该在17MB左右,而加上一些自己的代码很容易就飙到20MB左右。想要减少这部分内存的使用,能做的就是减少使用的库,稍后再说。

托管堆是被Mono使用的一部分内存。Mono项目一个开源的.net框架的一种实现,对于Unity开发,其实充当了基本类库的角色。

托管堆用来存放类的实例(比如用new生成的列表,实例中的各种声明的变量等)。“托管”的意思是Mono“应该”自动地改变堆的大小来适应你所需要的内存,

并且定时地使用垃圾回收(Garbage Collect)来释放已经不需要的内存。关键在于,有时候你会忘记清除对已经不需要再使用的内存的引用,

从而导致Mono认为这块内存一直有用,而无法回收。

最后,本机堆是Unity引擎进行申请和操作的地方,比如贴图,音效,关卡数据等。Unity使用了自己的一套内存管理机制来使这块内存具有和托管堆类似的功能。

基本理念是,如果在这个关卡里需要某个资源,那么在需要时就加载,之后在没有任何引用时进行卸载。听起来很美好也和托管堆一样,

但是由于Unity有一套自动加载和卸载资源的机制,让两者变得差别很大。自动加载资源可以为开发者省不少事儿,

但是同时也意味着开发者失去了手动管理所有加载资源的权力,这非常容易导致大量的内存占用(贴图什么的你懂的),

也是Unity给人留下“吃内存”印象的罪魁祸首。

 

优化程序代码的内存占用
这部分的优化相对简单,因为能做的事情并不多:主要就是减少打包时的引用库,改一改build设置即可。

对于一个新项目来说不会有太大问题,但是如果是已经存在的项目,可能改变会导致原来所需要的库的缺失(虽说一般来说这种可能性不大),

因此有可能无法做到最优。

 

当使用Unity开发时,默认的Mono包含库可以说大部分用不上,在Player Setting(Edit->Project Setting->Player或者Shift+Ctrl(Command)+B里的Player Setting按钮)

面板里,将最下方的Optimization栏目中“Api Compatibility Level”选为.NET 2.0 Subset,表示你只会使用到部分的.NET 2.0 Subset,不需要Unity将全部.NET的Api包含进去。接下来的“Stripping Level”表示从build的库中剥离的力度,每一个剥离选项都将从打包好的库中去掉一部分内容。你需要保证你的代码没有用到这部分被剥离的功能,

选为“Use micro mscorlib”的话将使用最小的库(一般来说也没啥问题,不行的话可以试试之前的两个)。库剥离可以极大地降低打包后的程序的尺寸以及程序代码的内存占用,唯一的缺点是这个功能只支持Pro版的Unity。

这部分优化的力度需要根据代码所用到的.NET的功能来进行调整,有可能不能使用Subset或者最大的剥离力度。

如果超出了限度,很可能会在需要该功能时因为找不到相应的库而crash掉(iOS的话很可能在Xcode编译时就报错了)。

比较好地解决方案是仍然用最强的剥离,并辅以较小的第三方的类库来完成所需功能。

一个最常见问题是最大剥离时Sysytem.Xml是不被Subset和micro支持的,如果只是为了xml,完全可以导入一个轻量级的xml库来解决依赖(Unity官方推荐这个)。

关于每个设定对应支持的库的详细列表,可以在这里找到。关于每个剥离级别到底做了什么,Unity的文档也有说明。

实际上,在游戏开发中绝大多数被剥离的功能使用不上的,因此不管如何,库剥离的优化方法都值得一试。

 

托管堆优化
Unity有一篇不错的关于托管堆代码如何写比较好的说明,在此基础上我个人有一些补充。

首先需要明确,托管堆中存储的是你在你的代码中申请的内存(不论是用js,C#还是Boo写的)。

一般来说,无非是new或者Instantiate两种生成object的方法(事实上Instantiate中也是调用了new)。

在接收到alloc请求后,托管堆在其上为要新生成的对象实例以及其实例变量分配内存,如果可用空间不足,则向系统申请更多空间。

当你使用完一个实例对象之后,通常来说在脚本中就不会再有对该对象的引用了(这包括将变量设置为null或其他引用,超出了变量的作用域,

或者对Unity对象发送Destory())。在每隔一段时间,Mono的垃圾回收机制将检测内存,将没有再被引用的内存释放回收。总的来说,

你要做的就是在尽可能早的时间将不需要的引用去除掉,这样回收机制才能正确地把不需要的内存清理出来。但是需要注意在内存清理时有可能造成游戏的短时间卡顿,

这将会很影响游戏体验,因此如果有大量的内存回收工作要进行的话,需要尽量选择合适的时间。

如果在你的游戏里,有特别多的类似实例,并需要对它们经常发送Destroy()的话,游戏性能上会相当难看。比如小熊推金币中的金币实例,按理说每枚金币落下台子后

都需要对其Destory(),然后新的金币进入台子时又需要Instantiate,这对性能是极大的浪费。一种通常的做法是在不需要时,不摧毁这个GameObject,而只是隐藏它,

并将其放入一个重用数组中。之后需要时,再从重用数组中找到可用的实例并显示。这将极大地改善游戏的性能,相应的代价是消耗部分内存,一般来说这是可以接受的。

关于对象重用,可以参考Unity关于内存方面的文档中Reusable Object Pools部分,或者Prime31有一个是用Linq来建立重用池的视频教程(Youtube,需要FQ,上,下)。

如果不是必要,应该在游戏进行的过程中尽量减少对GameObject的Instantiate()和Destroy()调用,因为对计算资源会有很大消耗。在便携设备上短时间大量生成和摧毁物体的

话,很容易造成瞬时卡顿。如果内存没有问题的话,尽量选择先将他们收集起来,然后在合适的时候(比如按暂停键或者是关卡切换),将它们批量地销毁并 且回收内存。Mono的内存回收会在后台自动进行,系统会选择合适的时间进行垃圾回收。在合适的时候,也可以手动地调用 System.GC.Collect()来建议系统进行一次垃圾回收。

要注意的是这里的调用真的仅仅只是建议,可能系统会在一段时间后在进行回收,也可能完全不理会这条请求,不过在大部分时间里,这个调用还是靠谱的。

 

本机堆的优化
当你加载完成一个Unity的scene的时候,scene中的所有用到的asset(包括Hierarchy中所有GameObject上以及脚本中赋值了的的材质,贴图,动画,声音等素材),

都会被自动加载(这正是Unity的智能之处)。也就是说,当关卡呈现在用户面前的时候,所有Unity编辑器能认识的本关卡的资源都已经被预先加 入内存了,这样在本关卡中,用户将有良好的体验,不论是更换贴图,声音,还是播放动画时,都不会有额外的加载,这样的代价是内存占用将变多。Unity最 初的设计目的还是面向台式机,

几乎无限的内存和虚拟内存使得这样的占用似乎不是问题,但是这样的内存策略在之后移动平台的兴起和大量移动设备游戏的制作中出现了弊端,因为移动设 备能使用的资源始终非常有限。因此在面向移动设备游戏的制作时,尽量减少在Hierarchy对资源的直接引用,而是使用Resource.Load的方 法,在需要的时候从硬盘中读取资源,

在使用后用Resource.UnloadAsset()和Resources.UnloadUnusedAssets()尽快将其卸载掉。总之,这里是一个处理时间和占用内存空间的trade off,

如何达到最好的效果没有标准答案,需要自己权衡。

在关卡结束的时候,这个关卡中所使用的所有资源将会被卸载掉(除非被标记了DontDestroyOnLoad)的资源。注意不仅是DontDestroyOnLoad的资源本身,

其相关的所有资源在关卡切换时都不会被卸载。DontDestroyOnLoad一般被用来在关卡之间保存一些玩家的状态,比如分数,级别等偏向文 本的信息。如果DontDestroyOnLoad了一个包含很多资源(比如大量贴图或者声音等大内存占用的东西)的话,这部分资源在场景切换时无法卸 载,将一直占用内存,

这种情况应该尽量避免。

另外一种需要注意的情况是脚本中对资源的引用。大部分脚本将在场景转换时随之失效并被回收,但是,在场景之间被保持的脚本不在此列(通常情况是被附 着在DontDestroyOnLoad的GameObject上了)。而这些脚本很可能含有对其他物体的Component或者资源的引用,这样相关的 资源就都得不到释放,

这绝对是不想要的情况。另外,static的单例(singleton)在场景切换时也不会被摧毁,同样地,如果这种单例含有大量的对资源的引用,也会成为大问题。

因此,尽量减少代码的耦合和对其他脚本的依赖是十分有必要的。如果确实无法避免这种情况,那应当手动地对这些不再使用的引用对象调用Destroy()

或者将其设置为null。这样在垃圾回收的时候,这些内存将被认为已经无用而被回收。

需要注意的是,Unity在一个场景开始时,根据场景构成和引用关系所自动读取的资源,只有在读取一个新的场景或者reset当前场景时,才会得到清理。

因此这部分内存占用是不可避免的。在小内存环境中,这部分初始内存的占用十分重要,因为它决定了你的关卡是否能够被正常加载。因此在计算资源充足

或是关卡开始之后还有机会进行加载时,尽量减少Hierarchy中的引用,变为手动用Resource.Load,将大大减少内存占用。在 Resource.UnloadAsset()和Resources.UnloadUnusedAssets()时,只有那些真正没有任何引用指向的资源 会被回收,因此请确保在资源不再使用时,将所有对该资源的引用设置为null或者Destroy。

同样需要注意,这两个Unload方法仅仅对Resource.Load拿到的资源有效,而不能回收任何场景开始时自动加载的资源。与此类似的还有 AssetBundle的Load和Unload方法,灵活使用这些手动自愿加载和卸载的方法,是优化Unity内存占用的不二法则~

总之这些就是关于Unity3d优化细节,具体还是查看Unity3D的技术手册,以便实现最大的优化.

原帖:http://www.onevcat.com/2012/11/memory-in-unity3d/