基于Unity3D的内存优化的研究

2016-12-19 05:24刘克李建方
关键词:调用开发者内存

刘克,李建方

(中国传媒大学 计算机学院,北京 100024)



基于Unity3D的内存优化的研究

刘克,李建方

(中国传媒大学 计算机学院,北京 100024)

近年来,Unity3D游戏引擎已经成为手机游戏行业最为流行的制作平台之一,但是很多游戏都存在卡顿,耗电速度快,占用内存过多等现象,影响了玩家体验。Unity的内存分为程序代码段,托管堆和本机堆。本篇论文重点讨论了在这三种内存上优化的方案,以及一些经验和技巧。通过内存优化,可以有效的提升内存的利用率,减少内存碎片和占用的空间。这样的方法有效的减少了像智能手机这样内存空间十分有限的移动设备负担,提高了开发效率,增强了玩家游戏体验。

内存优化;Unity3D;托管堆;本机堆

1 引言

Unity Technologies在2005年发布了Unity1.0版本,截至2014年底,Unity全球注册用户已经达到300万,Unity编辑器每月活跃开发者数量达到100万,Unity的PC插件安装量达到2亿。在十年的时间里,Unity游戏引擎已经演变成一个跨平台的、高度集合的、方便易用的集成开发环境,成为手机游戏市场的主流的开发引擎之一[1]。然而,Unity在内存占用上一直被开发者所诟病,这一点在移动设备上表现得尤其突出,动辄内存就会飙升至上百兆,轻则导致游戏卡顿,影响游戏体验,重则加快手机耗电速度,导致游戏被系统强制退出。

虽然Unity官方文档给出了一些内存管理的方法,并声称Unity内存全部都是由系统托管,但在实际项目中面临的问题复杂多变,欠缺经验的开发者通常不能正确使用内存,有的使游戏场景产生很多不必要的内存占用,有的产生的内存碎片使内存千疮百孔,这些都是造成内存占用过多的原因。因此只有正确的使用内存,才能确保Unity的回收机制正确运行。

本篇论文在编程方式,资源管理,以及动态内存加载机制研究了内存的优化,并在实际项目中产生了良好的反馈,使游戏在不损失效果的情况下将内存的消耗降低。

2 Unity3D游戏引擎概述

Unity3D是基于组件式的游戏引擎,在引擎中,任何一个物体的存在都可以被看作为一个游戏对象,它们都继承自Monobehaviour类,在不同的游戏对象上附着上多种组件,会使游戏对象表现出丰富的行为,这是各种各样游戏物体的本质。譬如:在一个空的游戏对象上附着一个摄像机组件,此时游戏物体会表现成为一个摄像机;在一个空的游戏对象上附着一个人物蒙皮网格、动画控制机、刚体以及一个角色控制脚本组件,则会使游戏物体表现为一个人物角色。(图1)所有的组件都是继承于Component类,开发者可以将组件自由的拖拽到游戏物体上,这使得游戏物体变得非常容易扩展和管理。

Unity3D还采用了所见即所得的开发方式,开发者可以在游戏场景中通过拖拽和摆放游戏对象来构建自己的游戏世界。在菜单中点击游戏播放按钮即可进入游戏,对参数做出的调整能够看到即时的变化效果。

图1 组件组合图

Unity提供了扩展接口供开发者使用,开发者可以根据自己的实际情况开发出插件来扩展引擎,提高开发效率。提供的诸多方便成为这款引擎流行的主要原因,但同时也使得引擎的管理成为一个麻烦,使之易于上手,难于精通。

3 Unity3D中内存的种类

Unity3D使用的内存类型共有三种:程序代码段、托管堆(Managed Heap)以及本机堆(Native Heap)[2]。

3.1 程序代码段

程序代码包括了Unity引擎使用的库,以及开发者所写的所有的游戏代码。在编译后,得到的运行文件将会被加载到设备中执行,并占用一定内存。这部分内存实际上是没有办法去进行优化的,它们将在程序运行开始一直占用内存,直到程序的结束才将内存释放。一个空的Unity默认场景,没有任何游戏物体和代码,在IOS设备上占用内存为17MB左右,而加上一些自己的代码很容易就上升到20MB左右。

3.2 托管堆

托管堆是被Mono使用的一部分内存。Mono项目是一个开源的.net框架的一种实现,在Unity项目开发中,它充当了基本类库的角色。托管堆用来存放类的实例,比如通过new生成的列表,实例中的各种声明的变量等。托管堆能自动地改变堆的大小以满足程序所需要的内存,并且在定时进行垃圾回收来释放不再使用的内存。若程序员忘记清除对已经不需要再使用的内存的引用,会导致Mono认为这块内存一直被程序使用,从而无法进行垃圾回收。

3.3 本机堆

本机堆是Unity引擎进行申请和操作资源的地方,比如网格模型,贴图,关卡数据,音效等。Unity使用了自己的一套内存管理机制来使这块内存具有和托管堆类似的功能。基本理念是,如果在某个关卡里需要某个资源,那么在需要时进行加载,之后在没有任何引用时进行卸载。虽然看上去和托管堆一样,但是由于Unity自身的一套自动加载和卸载资源的机制,让两者差异仍然很大。自动加载资源可以为开发者提供了方便,但是同时也意味着开发者失去了手动管理所有加载资源的权利,这是导致大量内存占有的罪魁祸首。

4 Unity3D的内存优化

4.1 程序代码段优化

在程序代码段上可做的优化并不多,主要是通过减少打包时引用库的数量。

当使用Unity开发时,默认的Mono包含库可以说大部分用不上,在Player Setting(Edit->Project Setting->Player)面板里,将最下方的Optimization栏目中“API Compatibility Level”选为.NET 2.0 Subset,表示你只会使用到部分的.NET 2.0 Subset,不需要Unity将全部.NET的API包含进去

这部分优化的力度需要根据代码所用到的.NET的功能来进行调整,有可能不能使用Subset或者最大的剥离力度。如果超出了限度,很可能会在需要该功能时因为找不到相应的库而导致程序崩溃。比较好地解决方案是仍然用最强的剥离,并辅以较小的第三方的类库来完成所需功能。

在此部分的优化收效甚微,还增加了程序崩溃的概率,应该是最后考虑的内存优化方案。

4.2 托管堆优化

托管堆存储的是程序在代码中申请的内存,在Unity中,托管堆为其新申请的内存分配空间,如果空间不足,则向系统申请更多空间。

当使用完一个实例之后,通常来说在脚本中就不会再有对该对象的引用了。这包括将变量设置为NULL或其他引用,超出了变量的作用域,或者对游戏对象发送Destroy()。在每隔一段时间,Mono的垃圾回收机制将检测内存,将没有再被引用的内存释放回收。总的来说,程序员要做的就是在尽可能早的时间将不需要的引用去除掉,这样回收机制才能正确地把不需要的内存清理出来。但是内存清理过于频繁可能会造成游戏的瞬时卡顿,影响了玩家的游戏体验。因此,最好选择合适的时机,对内存进行批量清理,比如当玩家暂停游戏,或者在场景切换的时候。

在游戏中,会遇到反复多次创建并销毁实例的情况,比如在打飞机游戏中,按理来说,飞机每次发射子弹时就要用Instantiate接口来实例化一个子弹,子弹离开屏幕后又要对其调用Destroy()进行垃圾回收,如此频繁的开辟内存导致的后果是:内存碎片增多,内存的再分配会变得越来越吃力;造成游戏卡顿。一种解决方案是:将经常反复调用的对象在不需要的时候将其隐藏并放到一个重用数组中,之后需要时,再从重用数组中取出并恢复显示。虽然消耗了部分内存,但是将极大的改善游戏的性能[3]。

一般建立可重用对象的机制叫做可重用对象池,其组织结构见表1。

重用对象池的使用方法为:创建一个类并应用单例模式,因为在逻辑上只能保持存在一个对象池。将写好的脚本挂载到场景中。在Get方法中,程序首先会在objPool中查询是否存有需要得到的游戏物体,若没有则通过Instantiate接口实例化一个新的游戏物体,若存有则在列表中直接取出游戏物体并将其激活。在Save方法中,程序首先判断objPool是否存在此类游戏物体的键值对,若没有则创立此类物体键值对,并将游戏物体存入列表中,若有则将游戏物体直接存入列表中,并将游戏物体隐藏。在优化前调用Instantiate接口的地方全都改成Get方法,调用Destroy接口的地方全都改为Save方法[4]。

表1 对象池

4.3 本机堆的优化

当Unity加载完成一个场景的时候,场景中所有的资源(模型,材质,贴图,脚本,声音等)都会被自动加载到内存中。这样做的好处是:由于场景中的资源都被预先载入到内存中,在一个场景中用户会拥有良好的游戏体验。但这样的代价是将内存的占用增多。由于Unity最初的设计目的是面向台式机,对于台式机来说,这样的占用丝毫没有任何问题,但是这样的内存管理策略迁移到移动平台后会出现大量的弊端,因为移动设备的内存空间非常有限,若不加优化很可能造成游戏的崩溃。

一般内存占用的峰值会发生在从一个游戏场景载入到另一个游戏场景的时候。由于当前场景的一些数据(玩家生命值,玩家等级,玩家物品栏等)需要保存到下一个场景继续使用,为了对这些数据进行保护,Unity通常会先加载新的场景,然后将旧场景中需要保留的数据复制到新场景后,再将旧场景所占用的内存释放,这样在某一时刻会导致两个场景同时占用内存,是内存占用成倍增加,这对于移动设备是非常危险的,移动设备很可能会因内存不足导致游戏卡死或者崩溃。为了避免这样的问题,可以设置一个过度场景,过度场景只包含很少的资源(进度条,背景图片),在切换场景时,可以先加载到过度场景,然后由过度场景加载到所需切换的场景,可以有效避免内存暴涨。

在进行场景切换时,当前这个场景中被标记为DontDestroyOnLoad的资源不会被卸载掉。一般这个借口用来保存玩家的一些状态比如分数,级别等偏向文本的信息。此时这个资源所携带的组件也不会被释放掉,若这个资源包含了很多占用内存的组件(声音、网格、贴图等),它们将一直占用内存,应尽量避免这种情况[5]。

另外一种值得注意的情况是脚本中对资源的引用。大部分脚本将在场景转换时随之失效并被回收,但是,某些游戏物体由于调用了DontDestroyOnLoad接口不会被销毁,这包括附着在游戏物体上的脚本。而这些脚本很可能含有对其它物体或者物体上的组件以及资源的引用,这样相关的资源就都得不到释放。另外,被声明static的脚本或者采用单例模式的脚本在场景切换时也不会被销毁,如果这个脚本含有大量对其它资源的引用,也会出现很多奇怪的问题,有些是资源得不到释放,有些是弹出空指针异常。因此,对代码进行解耦和减少对其它脚本的依赖是十分有必要的。如果确实无法避免这种情况,那应当显式的对这些不再使用的引用对象调用Destroy接口或者将其设置为NULL。这样在垃圾回收的时候,这些内存将被认为已经无用而被回收。

Unity3D提供给开发者两个动态加载的接口,一个是Resources.Load,另一个是AssetBundle。两者的实质区别并不是很大,Resources.Load只能加载来自Resources文件夹目录下的资源,一般用来加载本地资源。在Resources文件夹下存放的资源,无论是否会被加载到场景中,都会被打包到本地,增加游戏文件的大小,对游戏的下载量起到一定的副作用。因此,应控制Resources文件夹下资源的数量和所占空间。在Unity中,几乎可以将任何物体封装成为AssetBundle文件,将其存储在服务器端,通过客户端下载即可使用。把游戏资源从网络读取到本地内存时,此时文件只是一个内存镜像数据块,当调用AssetBundle.Load接口时,才将内存镜像数据块中的数据资源复制并反序列化成游戏可用资源,内存分布如图2所示。

图2 内存分布

如图可知,在游戏场景调用Instantiate接口时,实际上是将存储在区域2中的内容复制一份到区域3中,因此调用Destroy接口回收内存时,只是释放掉的区域3的内存,若游戏中不再使用此类对象,那么区域1和区域2的内存成为没有用的游离数据块,为了释放区域1和区域2的内存,应当调用AssetBundle.Unload接口,当调用AssetBundle(false),释放的是区域2的内存,当调用AssetBundle(true)时,释放的是区域1和区域2 的内存,若再次想使用此类资源,只能在服务器或本地重新加载。

使用动态加载机制可以减少内存占用,但是也增加了处理的时间,可以根据自身的特点需要在处理时间和空间上寻找一个平衡点。在小内存环境中,内存的初始化占用十分重要,因为它决定了游戏关卡能否被正常加载,使用动态加载机制能够有效减轻这种压力。

5 Unity3D资源优化

如果只重视程序优化而不注重资源优化,有时会使优化事倍功半。曾经有一款手机游戏,在代码上几乎没有做任何改动,在不损失游戏效果的前提下,通过资源重制使占用的空间缩小为原来的十分之一。以下将介绍优化资源的主要方法。

5.1 制作并整理图集

一张图片除了包含本身的色彩纹理信息外还包括了自身某些额外信息,若单独一张图片上有很多空余的地方,它们都会增加内存的占用,如果将很多图片合并在一张较大的图片上,则会减少很多不必要的浪费。同时图片的数量越少,能够减少Unity中DrawCall(相当耗费性能的操作)次数,减少CPU性能消耗。

在制作图集中应减少图集中的空白地方。图集中完全透明的像素和不透明的像素所占的内存空间其实是一样的。因此在素材量不变的情况下,要尽量减少图集中的空白。有时一张1024×1024的图集中,素材所占的面积还没超过一半,这时可以考虑将这张图集切成两张512x512的图集。但是应该尽量避免使用1024×512的图集,因为这样会降低兼容性(某些平台要求贴图必须为方形)。

需要注意的是,假设界面A上的有一个图标,在界面B也需要一个一模一样的图标,不要因为在制作时贪图方便,在界面B中直接引用界面A的图标,因为这样会将这个界面B也载入内存,造成不必要的浪费。图3给出了一个制作标准图集的图集提供参考。

图3 图集

5.2 模型优化

在移动设备上,每个网格模型控制在300-1500个多边形会达到比较好的效果。对于桌面平台,理论范围在1500-4000个面。如果游戏中任意时刻屏幕出现大量角色,那么应该降低每个角色的面数。

尽可能的复制物体。譬如:一个400面的物体,在场景中复制50次,所消耗的性能和一个物体消耗的性能几乎相等。

6 总结

如何在Unity3D中有效的优化内存是本文讨论的重点,本篇论文主要介绍了在程序代码段,托管堆以及本机堆优化内存的经验和方法。通过对象池技术,可以减少申请内存和回收内存带来的性能开销。动态申请内存机制可以避免游戏场景初始化时对设备带来压力。内存优化的方式并不是一成不变的,无论使用什么内存优化方式,都要预期游戏运行的环境,然后定制与运行环境相符合的内存优化策略,提高内存优化效率。

[1]刘钢,孙文涛.Unity 官方案例精讲[M].北京:中国铁道出版社,2015.

[2]王巍.Unity3D中的内存管理[EB/OL].http://onevcat.com/2012/11/memory-in-unity3d/,2012.

[3]金玺曾.Unity3D手机游戏开发[M].北京:清华大学出版社,2013

[4]Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides.设计模式[M].北京:机械工业出版社,2000.

[5]陈泉宏.Unity API 解析[M].北京:人民邮电出版社,2014

[6]Ken CF.Advanced 3D Game Programming All in One [M].Course Technology PTR,2005.

(责任编辑:马玉凤)

Research of Memory Optimization Based on Unity3D

LIU Ke,LI Jian-fang

(School of Computer Science,Communication University of China,Beijing 100024,China)

In recent years,Unity3D game engine has become one of the most popular platforms in mobile game industry,but lots of games exist the problem that including bad feedback,power consumption too fast,too much memory occupation,which impulse a bad affect on user experience.The memory of Unity is divided into program code,the managed heap and native heap.This paper mainly discussed the strategy,experience and skills in memory optimization.Which can effectively improve memory utilization,have less memory fragmentation and occupation.This method effectively reduces the memory burden in the device like smart phone which have a very limited memory space,improved the development efficiency,and enhanced the gaming experience.

memory optimization;unity3D;managed heap;native heap

2016-03-04

刘克(1990-),男(汉族),山东德州人,中国传媒大学硕士研究生.E-mail:kobeliuke@163.com

TP311.5

文章编号:1673-4793(2016)05-0056-06

猜你喜欢
调用开发者内存
核电项目物项调用管理的应用研究
笔记本内存已经在涨价了,但幅度不大,升级扩容无须等待
“春夏秋冬”的内存
系统虚拟化环境下客户机系统调用信息捕获与分析①
“85后”高学历男性成为APP开发新生主力军
16%游戏开发者看好VR
内存搭配DDR4、DDR3L还是DDR3?
利用RFC技术实现SAP系统接口通信
上网本为什么只有1GB?
C++语言中函数参数传递方式剖析