今天在项目中发现了一个UI的性能消耗比较异常的问题。在Unity的Profiler里面我看到一项Canvas.SendWillRenderCanvasesCPU消耗持续比较高,但是查看游戏的UI界面好像动态变化的UI并没有。在逐个关闭UI才定位到有一张图片一直在做渐变效果,所以才导致UI一直在重建。这里写个简单的例子模拟下,在场景中创建一张Image,然后挂载一个测试脚本,在这个脚本里面一直更新这个Image的alpha通道就可以看到这种现象。
1 |
|
今天在项目中发现了一个UI的性能消耗比较异常的问题。在Unity的Profiler里面我看到一项Canvas.SendWillRenderCanvasesCPU消耗持续比较高,但是查看游戏的UI界面好像动态变化的UI并没有。在逐个关闭UI才定位到有一张图片一直在做渐变效果,所以才导致UI一直在重建。这里写个简单的例子模拟下,在场景中创建一张Image,然后挂载一个测试脚本,在这个脚本里面一直更新这个Image的alpha通道就可以看到这种现象。
1 |
|
今天检查项目中代码的Boxing问题的时候。有一个点当时让我困惑了不少时间。如下:
1 |
|
这里在实际代码运行过程中Func1会产生Boxing而Func0没有产生Boxing,按照自己浏览的C#的文档对Boxing的理解这里应该都是会产生Boxing的才对。下面是C#的Boxing文档说明的:
Boxing is the process of converting a value type to the type object or to any interface type implemented by this value type. When the CLR boxes a value type, it wraps the value inside a System.Object and stores it on the managed heap.
查看了IEnumerator
1 |
|
是interface类型,那么上面的函数中yield return 0.1f中的0.1f这个float类型的变量转换为IEnumerator
1 |
|
那么问题就很清楚了,Func0其实每次迭代的时候接收值的变量就是float类型(对应于IL的float32类型)的所以根本不需要转换类型。但是对于Func1函数,编译成IL代码存储当前值的变量是object类型的,所以当我们Func1中返回值为0的int类型时,这个int型的变量会被转换成object类型而导致Boxing。
其实正确的操作是先看C#关于泛型的文档才对。不过现在看下也不迟。其中第一段就有如下描述:
by using a generic type parameter T you can write a single class that other client code can use without incurring the cost or risk of runtime casts or boxing operations
到这儿疑问就比较明确的解决了。
原文Optimizing graphics rendering in Unity games。这里对这篇文章的粗略翻译,当作自己的笔记。
在这篇文章中我们将会学习Unity在渲染一帧的时候其内部的发生的事情,在渲染的时候会发生什么类型的性能问题同时我们怎样去处理这些性能问题。
在阅读这篇文章之前,必须明白没有一个同样的尺度来提高渲染的性能问题。在我们游戏中影响性能问题的原因有很多同时非常依赖游戏运行的硬件和操作系统。通过调查,实验和细心的分析实验结果来解决性能问题是我们最重要的方式。
这篇文章包含的信息主要是大部分常见的性能问题的修改建议和一些更深层次的阅读链接。我们的游戏中可能也存在这其中的一个问题或者一些问题或者都没有包含。但是这篇文章仍然将帮助我们理解我们自己的问题同时给予我们去搜索我们问题的知识和线索。
在我们开始介绍之前,让我们快速简单看下Unity渲染一帧时其内部发生了什么。理解事件流和正确的术语将会帮助我们 理解,调查和解决我们的性能问题。
注:在这篇文章中,我们将会使用”对象(object)”术语来表示一个我们游戏的渲染的对象。任何渲染组件的GameObject将会被统称为对象。
在最基本的层面上,渲染可以描述为一下几步:
现在让我们来更近一步看看究竟发生了什么。这篇文章后面将会详细的介绍上面的每一个步骤。但是现在让我们先来熟悉下文中使用的词语和理解CPU和GPU在渲染中扮演的不同角色。
描述渲染的词语通常用渲染管线(rendering pipeline),记住这是一个非常有用的情景。高效的渲染都是关于保持更高信息的传送能力。
对于每一个渲染的帧,CPU会做如下的工作:
对于每个draw call包含的batchs,CPU现在必须要入如下的工作:
同时,GPU会做如下的工作:
现在我们理解了当Unity渲染一帧时其内部发生了什么,接下来让我们思考下当渲染发生的时候会随之发生什么问题。
原文Optimizing garbage collection in Unity games。这里对这篇文章的粗略翻译,当作自己的笔记。
Garbage Collector在这里被翻译成名词GC。对于Garbage Collection这里翻译成GC内存回收。
内存垃圾: 代码中销毁了(disposed)但是GC还没有清理的内存。
为了理解GC(本文GC指代Garbage Collector)在内存分配和回收时是怎样工作的,我们必须首先了解Unity的引擎代码和我们自己编写的脚本是怎么利用内存来工作的。
Unity引擎代码运行时管理内存的方式叫做手动管理内存(manual memory management)。也就是说引擎代码必须显示的声明内存是怎么使用的。手动管理内存没有用到GC,这部分内容本文不做介绍。
当运行我们自己编写的脚本时Unity管理这部分内存的方式叫做自动内存管理(automatic memory management).这意味这我们自己编写的代码不需要很详细的去告诉Unity怎样去管理这部分内存。Unity都帮我们做好了。
简而言之,Unity的自动管理内存工作方式如下:
现在我们了解了内存使用的流程,让我们来更深入的来了解栈和堆上的内存的分配和释放。
下面的内容是阅读3D数学基础后结合自己的理解的总结
我们接触到的地方就是Unity的Transform组件。Transform组件维护了四元数实现方位角位的变换。但是我们也看到Transform组件提供了eulerAgnles属性,按理说里面也实现了欧拉角的,但事实并不是这样。接下来详细分析下
首先看看类似与这种操作,我们对一个GameObject实现旋转的时候,可以直接对其transform组件的eulerAngles属性赋值需要旋转的角度即可。
1 |
|
这个是transform的eulerAngles的实现,可以看到其实里面直接把欧拉角转换成了四元数来处理,所以欧拉角其实只是作为一个方法的表达形式,并没有作为Transform的成员变量之类的属性来维护 (实际上也不需要,因为有了四元数之后,欧拉角和四元数就可以实现相互的转换)
1 |
|
我们想对一个GameObject实现旋转还可以对其的transform组件的rotation属性以四元素的方式赋值,来实现旋转。
1 |
|
对于rotation属性,其具体实现是这样的
1 |
|
可以看到标红色的那行,传入的四元数被赋值参数被赋值给了m_LocalRotation成员变量。
上面介绍了在Unity里面封装的Quaternion的方位与角位的情况。那么下面Quaternion里面常用到的一个叫做Lerp的方法。相应的这个方法有一个 对应的Slerp方法。那么这两个方法到底有什么区别呢?下面来看看。
Lerp是用来求两个目标之前的差值,其表达式为Lerp(a, b, t)。a和b分别为起始和终点两个点,t为差值参数变量,范围在0到1之间。Lerp表示为标准的线性差值公式的话,其公式为:
1 |
|
代码实现着这样的:
1 |
|
Slerp的计算方式(里面数学公式编辑比较麻烦,干脆直接写下来上图片了):
得出了推导公式,那么代码实现就比较简单了:
1 |
|
两种方式比较: Lerp更少的计算量,Slerp更加平滑。实际测试下使用Slerp和Lerp的运动效果
1 |
|
通过一个个的小球,我把每次Slerp和Lerp的插值画出来,形成了一个轨迹(其中红色是Slerp的插值轨迹,白色的是Lerp的插值轨迹)
通过这个轨迹图片可以看得到(由于各个小球的Z周有一点差异,导致画面有一点透视效果),代码Slerp的红色小球的轨迹相邻的之间距离比较均匀,但是代表Lerp的白色的小球两球之前的距离由最开始逐渐变小然后到达中间之后又开始变大。分析之后可以归结到下面两张图片中(左图描述Lerp,右图描述Slerp)。Lerp求得的是四元数在圆上的弦上的等分,而Slerp求得的是四元数载圆上的圆弧的等分(论据的图是参考的这里)。
项目中的使用问题:
1 |
|