unity3D优化之内存管理

在U3D游戏中内存管理一直都是让人比较头疼事情,现在手机游戏是越做越大,和端游一样,每次卡顿和每次的内存增长对玩家来说都是一个比较差的体验。听到过一句话,说游戏开发做久了就会变成“GC怪”,因为在游戏开发过程中,需求变化多,功能不停的迭代,内存问题也一直存在,需要不停的去优化它。

在unity2018中集成来正版的.NET4.X和C#7.3,引入了ref return 和ref locals,让值类型的操作更加便捷,在U3D2019中更是加入来增量式GC,减少来GC带来的卡顿问题。比以前的版本灵活来许多。

讲了这么多,可能很多人还不知道什么是GC,GC的全称是Garbage Collection,也就是垃圾回收的意思,是一种自动管理堆内存的机制,管理堆内存上对象的分配和释放。

一、内存管理方式

我们常用的内存管理方式有三种:

1.手动管理,像C/C++一样使用malloc/free或者new/delete来为对象分配释放内存。这张方法的优点是速度快,没有任何额外开销,缺点是要去人工了解每个对象的使用情况,这样很容易发生各种问题,比如内存泄漏,野指针和空悬指针等。

2.使用引用计数,它的思想是对象创建出来以后,维护一个针对该对象的计数,使用该对象的地方对该计数加1,使用完后减1,当计数为0时,销魂该对象。这种方法类似半自动内存管理方式,优点是可以把分配和释放的开销分布在实际使用过程当中,速度比较快,不过会存在一个循环引用的问题。引用计数是一种比较常用的内存管理方法,比如U3D中物理引擎PhysX就是使用引用计数来管理各种对象的。

3.追踪式GC器,unity使用的GC器是一种叫标记/清除的算法,它的思路是当程序需要进行垃圾回收时,从根出发标记所有可以到达的对象,然后回收没有标记的对象,这是一种全自动的内存管理方法,程序员完全不用追踪对象的使用情况,也不存在循环引用无法回收的问题。在unity中使用的是一种叫boehm-Demers-Weiser的GC器,它的特点是:

(1)stop the world ,即当GC发生时,程序的所有线程都必须停止工作,在回收时也要停掉所有线程。

(2)不分代,.NET和java会把托管堆分成多个代,新生代的内存空间非常小,而且一般来说,GC主要集中在新生代上,让每一次GC的速度很快,但是在U3D中GC是完全不分代的,只要发生GC,就会对整个托管堆进行GC。

(3)不压缩,不会对堆内存进行碎片整理,类似我们的磁盘一样,使用久了就会有很多的碎片,造成磁盘上有很多小空隙。同样在U3D中GC会造成托管堆出现很多这样的间隙,这些间隙不会合并,当申请一个新对象时,如果没有任何一个间隙大于这个新对象的大小,堆内存就会增加。

二、影响GC性能的因素

主要因素有2个:

1、可达对象的数量

2、托管堆的大小

可达对象是不会被GC回收的对象,减少这类对象的方法是减少对象的数量,如下:

将会产生10个Actor对象。

如果使用以下方法:

只产生一个对象,方法很简单,但是却很有用,如果在需要每帧去处理的对象上去使用这类优化,会得到意想不到的效果。

而优化托管堆的大小主要通过以下几个方面:

1、减少临时内存分配,因为临时内存会使内存短暂的增长,而且会产生碎片

2、防止内存泄漏,也就是存在互相引用无法回收对象的情况,或者没用到的对象,但是对它还有引用,导致释放不掉。

3D游戏世界里的万向节锁

我们通常认为当前世界是一个三维的世界,用坐标系来表示就是XYZ,在3D游戏世界里也是用这样的三维坐标系来表示的。

 

X代表水平坐标,Y代表垂直坐标,Z代表的是由屏幕往里的一个深度坐标,也就是垂直于屏幕的坐标轴。

如下图,在一个3D世界里,绿色代表Y坐标,红色代表X坐标,而垂直于屏幕的蓝色是Z坐标。

 

这样在一个3D世界里面就可以方便的表示一个物体的位置信息和旋转信息。

那么什么是万向节锁?

其实就是一个物体在一个3D世界里面随着旋转顺序和旋转角度的改变,导致物体只能在一个固定的平面旋转,无法旋转到你预想的角度。

由于物体的旋转,物体的坐标轴方向也发生了改变,导致其中2条坐标轴发生了重合

当你围绕着重合的坐标轴旋转时,物体的旋转方向始终在一个平面上旋转。

比如在Unity3D 中我们把一个物体按照先沿X轴旋转90度。

 

3D世界里面的人物沿X轴旋转了90度,人物的头看向了下方也就是Y轴的反方向。

此时物体的坐标轴也发生了改变,那么是哪个轴发生了改变呢?

可以自行想象一下。

其实只有Y轴发生了改变,由于物体围绕X旋转了90度,那么这个时候的Y轴也围绕X旋转了90度,也是和原来的Z轴重合了,原来的Y轴是由人物的脚指向头这个方向的,当人物围绕X轴旋转90度后。

Y轴还是按照由人物的脚的方向指向了头,也就刚好是垂直于屏幕的。

这样就导致了,当前的Y轴和原来的Z轴重合了。

这个时候你可能会想既然Y轴改变了,Z轴也会改变啊,原来的Z轴是从人物的身体由屏幕外指向屏幕里的,那么当物体围绕X轴旋转后,那么此时Z轴还是应该由人物的身体穿过人物的背面,也就是当前的绿色坐标轴。

按照我们的社会经验物体围绕X轴旋转90度后,Z轴和Y轴只是交换了位置而已啊。

但是其实在3D游戏世界里面不是这样去定义的。

而是按照陀螺仪的原理去解释的。

我们看下图,一个陀螺仪装置:

 

陀螺仪的装置非常简单,中间有一根竖轴,穿过一个圆盘。

圆盘称为转子,竖轴称为旋转轴。转子用金属制成,增加质量,从而增大惯性。竖轴外侧是三层嵌套的圆环,它们互相交叉,带来了三个方向自由度的旋转。

简单分解后就是如下图:

 

假如把绿色圆盘的转轴称作为Y轴,红色圆盘的转轴称作为X轴,蓝色圆盘的转轴称作为Z轴。

那么当绿色圆盘围绕它的转轴旋转时,并不会改变红色和蓝色旋转轴方向的变化。

 

当红色圆盘围绕着它的转轴旋转时,绿色圆盘的转轴方向发生了改变。

 

当蓝色的圆盘围绕着它的转轴旋转时,绿色和蓝色的转轴方向都发生了改变。

 

当红色圆盘围绕转轴旋转90度后

 

绿色圆盘的转轴和蓝色圆盘的转轴重合了,而红色圆盘的转轴方向并没有改变,这个时候就发生了万向节死锁现象。

绿色圆盘的旋转和蓝色圆盘的旋转都在同一个平面上旋转了,只要红色圆盘不发生改变,不管如何旋转绿色和蓝色圆盘都无法解除这种状态。

如在Unity3D中当把物体旋转围绕X轴旋转90度后。

把人物围绕Y轴再旋转40度后:

 

 

把人物围绕Z轴再旋转40度后:

 

 

我们发现物体不管是围绕Y轴还是Z轴旋转,都只是在一个平面旋转,而不是不同的平面了。

如果开始只把物体围绕X轴旋转89度的话,结果又会不同了。

UGUI屏幕点击关闭Tips小技巧

在做MMORPG 游戏中在UI上会有很多的Tips弹窗的UI,策划的需求是点击Tips外面要关闭这个Tips那么我们首先想到的做法是,获取点击的区域或者对象判断是否点击是 tipsUI如果是则不关闭,

如果不是则关闭Tips,那么这样的做法有很多,其中一个是设置一个透明image背景,然后在这个Image上添加Button组件,当点击这个背景时关闭这个tips弹窗,当然这个背景要扩展到整个屏幕,

然后要设置成透明的这样才能不挡住其他UI。

        但是我感觉这样的方法好像多了一个几个步骤而且有点麻烦,既然我们只要一个透明带raycasts的背景而已,那么用一个空的Text组件也是一样可以实现。把字体和文字都设置为空,这样既可以不用设置透明度也可以达到一个点击屏幕的效果。


RectMask2D和Mask

最近项目中做UI,用的比较多的Scroll Rect这个组件,发现他是通过一个遮罩来显示Scroll Rect里的内容的,在UGUI中Scroll View自动创建的遮罩用的是Mask组件,但是我通过搜索发现还有

RectMask2D这个组件,然后我用上去发现效果是一样的。查了一下U3D的文档:

the limitations of RectMask2D control are:

  • It only works in 2D space    它只能工作在2D空间中

  • It will not properly mask elements that are not coplanar    它不能遮罩不在一个平面上的元素

The advantages of RectMask2D are:

  • It does not use the stencil buffer   它不会使用模板缓存

  • No extra draw calls       不会增加额外的 draw calls

  • No material changes    不会有材质的变化

  • Fast performance    速度更快的显示

是这样解释的,这简直是神器啊,比原来的Mask好用多了,但是前提是你用不到3D UI这个功能,那么如果你是做普通的2D UI 使用这个组件做遮罩是完全没有问题的而且还可以减少draw calls数量于是我测试了一下。


使用了Mask组件的Scroll View,draw calls是5


QQ截图20170910113531.png


使用了RectMask2D组件的Scroll View,draw calls是4


QQ截图20170910113654.png


使用了RectMask2D组件比Mask组件draw calls少了1个,这在移动设备中是非常重要的优化啊。



KSFramework

新公司,新项目,新的框架,这又将是一个挑战。

现在我们项目用的是 KSFramework+Xlua  ,KSFramework是一个Unity 5 Asset Bundle开发框架和工具集,专注于运行时热重载,本身是使用了SLua作为脚本引擎。但是我们项目中给它改成了XLua作为脚本引擎,因为xlua效率方面比slua似乎更高一点,而且在KSframework中把slua换成xlua也是非常方便的,即使换成ulua也是可以的。

        但是最近发现的是 在xlua里面只要出错 在手机端里游戏就会出现闪退,打出的PC包也是直接崩溃掉,之前用tolua的时候一般lua代码报错是不会导致游戏直接崩溃的。

这一点确实很蛋疼。

protobuf协议字段优化后的处理

特色

我们的游戏项目中使用了protobuf协议,但是由于协议优化了的缘故,当服务器发送字段值等于0 的时候,客户端协议解析出来后看不到这个值为0零的字段,但是如果直接用“.“去访问是可以访问的。于是问题就来了当我们数据需要更新的时候,如果在lua中 直接 for in pairs 去获得解析到的数据的键值,如果这个键的值是0 的话那么那么将会获取不到,也就是说这个时候无法判断这个字段是否是 0 ,还是根本服务器就没有发送过来。

于是我们就使用看 按位与的方式去检测和判断 服务器是否发了这个字段,对此,如果是需要更新的协议服务器都要多发一个mask 字段 这是一个 二进制转换后的十进制数,协议字段按照顺序,如果发了,,则这个位置为1,否则为0,如协议:

//申请入帮
message C2GSApplyJoinOrg {
    optional uint32 orgid = 1;
    optional uint32 flag = 2;
}

如果服务器发送了 orgid = 10 , flag = 0 过来,那么客户端只能看到 orgid = 10 这个字段,而看不到 flag 这个。

所以在客户端中我们就做了如下处理:

--dOri 是服务发送过来经过客户端解析后的数据
function CNetCtrl.DecodeMaskData(self, dOri)
	local d = {}
	local lKey = {"orgid","flag"}
	if lKey then
		local iMask = dOri.mask --服务发送时带一个二进制的mask字段
		if iMask then
			local right = 1
			for i=1, #lKey do
				right = right * 2
				if MathBit.andOp(iMask, right) ~= 0 then
					local key = lKey[i]
					d[key] = dOri[key]
				end
			end
			-- table.print(d, "CNetCtrl解析mask: "..sType)
			return d
		end
	end
	return dOri
end


function MathBit.__base(left, right, op)  
    if left < right then  
        left, right = right, left  
    end  
    local res = 0  
    local shift = 1  
    while left ~= 0 do  
        local ra = left % 2  
        local rb = right % 2  
        res = shift * op(ra,rb) + res
        shift = shift * 2
        left = math.modf( left / 2) --相当于右移一位
        right = math.modf( right / 2)
    end  
    return res
end
  
function MathBit.andOp(left, right)  
    return MathBit.__base(left, right, MathBit.__andBit)
end  

function MathBit.__andBit(left,right)  --按位相与
    return (left == 1 and right == 1) and 1 or 0      
end

这样处理完成之后的数据 客户端就是可以打印出来查看了。并且可以直接使用for in pairs 去打印他们为0的值。

2D场景坐骑飞行效果

        最近在公司做一个坐骑系统里面有一个飞行的功能,一开始觉得应该是提高他的Y轴,后来发现效果并不明显,于是觉得如果要在2D场景中实现纵深的效果,还是要有大小比例的变化才能看上去 有高度。

于是把飞行的模型放大到 1.2 倍,发现效果还不是很明显,于是又把今天拉伸了 一点 效果才明显一点了,发现其他2D游戏的飞行中还带有云层效果看上去 效果就好多了。而且这个跟你坐的坐骑也有很大的关系

如果本身不是飞行的东西 你坐上去感觉就很怪异了。

       所以总结得出要在2D场景中实现坐骑的飞行效果 1.放大模型  2.拉伸镜头  3.加入云彩特效或其他  

NGUI动态锚点设置

最近项目上需要动态去设置控件的锚点,之前很少会碰到这样的问题,一般都是直接在预制体上设置好的,于是用VS查看了一下API 发现了UIWidget 上都是自带锚点设置的,但是我试用了 

        widget.bottomAnchor 

        widget.leftAnchor 

        widget.rightAnchor 

        widget.topAnchor 

这几个属性发现都不行 设置完成后位置还是会乱掉,然后我又试用了widget.SetAnchor()这个函数这个函数有3个重载函数,是用来设置锚点的目标对象的,于是我用widget.SetAnchor(obj,left,bottom,right,top)

试了一下,设置锚点对象和 偏移位置,发现即使是这样设置也和面板上的设置不同,面板上得设置是可以设置锚点的相对偏移位置的,而这个函数只是设置了偏移位置,没有对锚点的相对位置做设置也就是说没有如下图设置的 相对偏移目标

QQ截图20170314231151.png

继续在VS上查找NGUI的锚点API发现 ,4个方向下的锚点属性还有一个绝对位置和 相对位置可以设置

 widget.rightAnchor.absolute,widget.rightAnchor.relative

我猜想这个可能就是设置相对位置的属性但是怎么设置呢?

这是一个问题,我把这个API贴到百度上果然很多人也遇到了这样的问题仔细研究下发现还挺麻烦的,相对的距离的设置 如果为1表示是 面板设置的 bottom 或者left ,为0.5表示center,为0表示top或者right,而绝对距离则是面板上填写的数值于是,用代码设置锚点就很简单了.

  1. 首先用widget.SetAnchor()这个函数设置好锚点目标和绝对距离

  2. 然后用 widget.rightAnchor.absolute,widget.rightAnchor.relative设置好相对距离和绝对距离(这里需要再设置一遍和SetAnchor函数设置的一样)

由于我们的项目逻辑层全是lua于是我只好把这些用到的函数封装了一遍。

如下图

QQ截图20170314232702.png

希望各位同行少入点坑。。。

UGUI Text 逐字出现

   在做剧情系统的时候,有个剧情模式叫立绘对话,也就是那种你说一句我说一句的那种,策划的要求是希望对话是逐字出现的,而且文字都是在表格里面配置的还需要支持字体颜色的配置.显示一句完成后自动显示下一句.

   读取表格显示字体是很简单的.但是要逐字出现,由于之前没做过,首先想到的是,字体应该都是已经全部放上去了,然后在Text的上面放上一层遮罩.没隔一段时间把上面这层遮罩缩短一个字.这样就可以实现逐字出现的样式了.但是后面发现不行,因为每个字的大小和宽度是不一样的,而且还会有标点符号的区分.

  于是只能是使用那种每次往Text组件上加上一个字,但是这样做的问题又出现了,因为是每次加上一个字的,也就是说事先定义的颜色标记是没有用的他会被逐字打印在Text上了,比如 <color>标记他会首先打印 "<" 这个符合然后陆续把<color>整个显示出来 而不会像直接显示文字那样把<color>标记里面的文字按照定义的颜色显示出来.

  为了实现这个功能我只好用个正则表达式,把<color></color>整个内容先截取出来然后再赋值颜色值整体显示出来,也就是说,只要遇到有颜色标记的字就截取出来然后整体显示出来,这样就可以实现所需要的功能了.

部分代码如下:

//截出带颜色部分字体
   Regex reg = new Regex("(?<=<color).*?(?=</color>)", RegexOptions.None);
   MatchCollection matchs = reg.Matches(dialogDeploy_.text);
   //Debug.Log(matchs[0].Index-6+"-----"+ matchs[0].Length+6+8);
   int k = 0;
   ch = dialogDeploy_.text.ToCharArray();//转换成数组
   for (int j = 0; j < ch.Length; ++j)
    {

     yield return new WaitForSeconds(0.1f);
     if (isNext)
     {
      break;
     }
      if (ch[j] == '<' && matchs[k].Length + 6 + 8 
      < dialogDeploy_.text.Length)
     {
       //匹配出带颜色的字体
       showText_.text += 
       dialogDeploy_.text.Substring(j, matchs[k].Length + 6 + 8);
       j += matchs[k++].Length + 6 + 8;
     }
     else
     {
       showText_.text += ch[j];//显示每个字
     }
               
            }
            yield return new WaitForSeconds(0.5f);
            showText_.text = "\n";
            ch = null;
            ++i;
 }

U3D动态建模

做项目时美术居然要把英雄的属性值设置成雷达图的样式,看起来直观一点,然而他们只给了我一张图片……….

我当时懵逼了,一张图让我怎么做动态改变? 难道不应该是做动画吗? 然而我想了一下似乎做动画也不是很好,然后我开始想什么东西可以让UI动态的改变呢? 根据输入的值显示不同的形状呢?

这是值得研究的问题.在网上找了一些资料发现 可以用一个面片来做,把面片放在UI上面然后动态改变面片的顶点,实现不同的形状.

雷达图如下:

QQ截图20161107163618.jpg

由于我们的UI部分是需要进行热更新的所以这个功能我是用LUA写的但是可以很容易的翻译成C#的:

 local attributeMesh = self.transform:Find("模型物体"):GetComponent(UnityEngine.MeshFilter)
 local verticesInit=attributeMesh.mesh.vertices
 this.SetHeroAttribute(attributeMesh,0,0,0.1)
 function this.SetHeroAttribute(attributeMesh,x,y,z)
    local  mesh = attributeMesh.mesh
    local vertices = mesh.vertices
    vertices[1] =verticesInit[1]-UnityEngine.Vector3(x,y,z)//把原来的顶点值减去设置的值再赋给模型的顶点
    vertices[2] =verticesInit[2]-UnityEngine.Vector3(x,y,z)//这里只改变了 第2和第3个顶点的值,这样原来的5角型就会变的不规则了.
    mesh.vertices = vertices
end

这个面片实际上有5个顶点,但是为了演示我在代码里只改变了它2个点.