-
隐藏的方式
1.1 设置Active为false方式。这种方式UIPanel的所有脚本逻辑、Rendering、Canvas.BuildBatch都会停止。构建的渲染的Mesh也会被清理。
1.2 设置不被渲染的Camera的Layer层上。这种方式相当于只是停掉了Rendering,其他的逻辑照常执行。
-
优缺点比较
2.1 方式1隐藏Panel的时候构建的Mesh会被清理掉,会节省这部分内存。而且在动态Panel的时候(Panel上包含移动变化的GameObject),Canvas的BuildBatch不会再调用,这会节省CPU(节省的程度根据Panel的复杂度决定的)。但是这种方式在隐藏和重新显示的时候由于需要重新构建渲染的Mesh,会消耗一定的CPU和产生GC.Allocate。
2.2 方式2的好处在于Panel隐藏和重新显示的时候不需要重新构建渲染的Mesh,不会产生额外的CPU消耗和GC.Allocate。但是这种方式在隐藏的时候Panel的所有逻辑任然在执行(Update等),在动态Panel的话Canvas的BuildBatch也会照常执行,这会一直消耗CPU资源。
Unity模型渲染层级顺序问题
问题描述
项目中遇到一个问题,在Untiy中如果关闭ZTest的话,模型的子部件之间的层级可能会出现错乱的现象,模型是3ds max导出的模型。
原因思考
既然是层级错误,根据Unity渲染管线的规则决定层级的有两个地方。一个是传入Unity的顶点数据的顺序(如果用了顶点数据索引,那么就是索引的顺序),另外一个是Unity里面的DepthTest功能决定。那么两种情况的规则是怎样的呢?
Unity中的shader有一个RenderQueue设置,使用设置了同一个RendererQueue的值的shader模型在同一批被渲染,比如RenderQueue设置了一般不透明物体设置为Geometry(对应的数值为2000),透明物体物体设置为Transparent(对应的数值为3000)。Geometry的对象会先渲染,然后才会渲染Transparent的物体。本文我遇到的问题的模型都是用的同一个shader,所以RendererQueue不是问题的原因。
首先看DepthTest的作用,在Unity中利用了深度缓冲来保证了渲染的层级问题,深度缓冲是根据RenderQueue和深度值(顶点的Z值)来确定的。渲染管线在光栅化之后会对顶点数据做一次DepthTest 。
对于顶点数据的顺序的影响也是经过这个问题之后我才发现的。在DepthTest打开的情况下(Shader里面ZWrite&ZTest开启),这个顺序也没有什么作用的,但是当我们关闭DepthTest的时候(比如渲染半透明的物体需要进行混合操作),Unity的渲染就直接根据这个数据的顺序去渲染了。
基于上面两种情况的分析,通过测试发现我们的模型在打开DepthTest的时候是没有层级的问题的,关闭DepthTest之后才出现层级混乱的问题。这样我们就把问题定位到原因了。那么接下来验证一下原因
问题解决
首先我直接在Unity中通过代码生成一个Mesh来渲染,目的是为了可以方便的更改顶点数据的顺序。(创建Mesh的代码参考了ProceduralMesh,根据自己测试需求做了一些改动)
1 |
|
在使用正确的triangles的数据并且关闭ZTest的时候,显示的效果如下:
这种效果是正确的。在使用乱序的triangles的索引并且关闭ZTest的时候,显示效果如下:
这看起来很怪异,再看看3D空间视图里面的顶部视图:
通过这个图我们可以看到,其实乱序的triangles数据渲染出来的效果是因为蓝色的那个三角形被渲染在前面了,然而他本身的位置是在红色和绿色的后面的。说明他比红色和绿色的面片要后渲染出来。
上面的两个测试证明了在关闭ZTest的时候,Unity的渲染顺序其实就是根据triangles数据的顺序来渲染的。如果我们开启ZTest,那么上面两个triangles数据渲染出来的模型我们会发现都是没有问题的。现在证明了是triangles顺序的问题但是我们模型的triangles的数据来自于3dsMax,说明这应该是在3dsMax导出模型的一些设置问题,或者说是模型本身有问题。
3DS Max导出模型的问题
现在问题定位到模型导出的问题,由于没用过3dsMax。为了测试需要我直接摸索创建了一个包含3个Cube的模型,然后合并三个模型的Mesh。导出FBX之后再在Unity观察三个Cube的层级关系,结果重现了问题模型的问题。那么到底是哪一步出错了呢?经过无数次的测试,最终发现了triangles顺序是由合并Mesh的顺序决定的。如果想得到triangles数据的顺序正好是对应顶点的层级由低到高的顺序,那么在3dsMax中在合并Mesh(Attach子模型)的时候需要从层级最低的模型逐个Attach到层级最高的模型。
最后测试的Shader(其实就是官方给的Unlit-Alpha改了一点点)
1 |
|
Unity脚本自动构建IPA包
Unity项目在iOS平台打包是一件比较费时间的事情,所以写了脚本来实现自动话打包生成ipa文件,避免每次去到处xcode工程然后配置证书之类的设置。
测试环境 Unity5.5.1f3, macOS 10.12.4, Xcode 8.3.2
工具使用介绍
打包的配置用到了很重要的XUPorter插件,打包的代码UnityIPABuilder。下面简单讲解一下里面的具体实现
Xcode打包主要有两个步骤,第一步是导出Xcode工程,第二步是构建Xcode工程并且导出ipa包。这里两种方式启动打包步骤。
第一种在Editor模式下打包。制作了一个Editor界面如下:
里面的Export按钮的功能是直接导出一个配置号参数的Xcode工程,Build按钮的功能是首先会进行Export操作,然后对Export出的工程进行Build直接生成IPA包。整个界面比较简单,如果不满足自己的需求的话可以通过在BuildWindow里面添加。
第二种不启动Editor,直接用脚本打包。这种模式下我们直接启动项目打包脚本cmd_start_unity_ipa_builder加上对应的参数即可。例如sample工程的打包直接运行一下命令(先配置好xcode_basic.cfg)即可得到最终的ipa文件:
1 |
|
这个脚本会静默启动Unity来执行ProjectBuilder的打包方法。并且会把打包的日志存放在{$ProjectPath}/iOSBuild/build/log目录下。
导出Xcode工程部分
这一步可以通过Unity的内置API完成。在这一步中我们还需要做的是设置好导出的Xcode的工程的各种配置(证书,版本号,Framework等)。导出Xcode工程直接只用Unity的API
1 |
|
这个API可以通过BuildOptions配置在Editor中的Player Setttings里面的各种配置。我的工具里面只添加了ScriptingImpletion、Architecture、渠道、Version和Build五个参数。这是根据我自己的需求来添加的,其他的设置在Palyer Setting设置好了基本上就不需要动态改变了,这几个参数是每次打包改动比较频繁的选项。
先看下我们的整体构建步骤:
1 |
|
导出Xcode工程之后我们要做的就是配置Xcode工程。这里我用的是XUPorter插件来进行修改的,这个插件可以很方便的帮助我们自动把依赖的Framework、静态库、文件等添加到Xcode工程,这样可以解决我们在使用第三方插件的时候依赖的库每次打包需要手动添加的问题(5.5.1f1版本支持添加iOS系统自带的Framwork,但是不支持添加第三方的库,比如libsqlite3.dylib文件)。另外为了配置为了配置证书和Info.plist文件,稍微改动了一下里面的代码。
1 |
|
OnPosProcessBuild函数在XUPorter插件里面XCodePostProcess文件里面的一个函数,这个函数本来是加了[PostProcessBuild(999)]属性,加了这个属性的函数是Unity在BuildPlayer调用之后会主动调用的函数。但是这里我们去掉这个属性,我们不需要Unity来主动调用这个函数。因为我们的构建的步骤是: BuildPlayer导出Xcode工程,XUPorter配置XCode工程,shell脚本构建XCode工程。XCodePostProcess属于第二步XUPorter配置XCode工程这一步,如果让Unity来主动调用XCodePostProcess的话在不修改XUPorter的情况下我们无法得知XUPorter执行完毕的时间。所以我们自己在调用完BuildPlayer之后接着调用XCodePostProcess然后再调用shell构建,这样顺序来执行。
对于配置XUPorter的参数,大家可以参考工程的XUPorter插件下面的Bugly.projmods配置文件。详细的解释可以参考作者的原文介绍。至于ProjectBasicConfig配置,这个直接共用的shell脚本使用的配置文件,构建部分详解下。
至此我们可以得到一个配置号所有参数的Xcode工程。
构建Xcode工程部分
构建Xcode部分我设计的是不依赖Export部分,也就是说我们不需要在Export部分配置好证书或者版本号等这些参数(xcode_basic.cfg文件包含的参数),对于任何Xcode工程我们都可以通过这个脚本进行Build生成IPA包。构建脚本只有auto_build_xcode_project一个文件,使用方法我们可以通过执行命令查阅:
1 |
|
我们可以先对shell脚本添加可执行权限,之后运行以来就不需要sh命令了
1 |
|
当然,我们在使用这个脚本之前请先配置这个脚本对应的配置文件xcode_basic.cfg,这个配置文件里面包含的参数是我们在使用这个脚本自动打包可以动态改变的所有参数。如果还需要修改额外的参数,那就需要自己另外添加了。整个配置文件我分了三个渠道,分别是Debug,QA和Release。每个渠道的配置的参数是一样的:
1 |
|
打包脚本会自动读取相应的渠道的配置,这个配置的参数是有序的,不要改变他们之间的顺序。这里和大家说下怎么获取得到描述文件和证书的指。
描述文件在Xcode里面的Build Settings里面Signing里面先选择对应的描述文件,然后在选项中选择other就会选择我们描述文件的值:
证书直接在系统应用钥匙串访问里面查看,双击点开一个钥匙串可以看得到:
直接复制里面的常用名称对应的值即可。
其实Xcode的自动构建比较简单,主要是通过XcodeBuild来构建,构建好了之后会生成一个app包,然后再通过xcrun命令把这个app包压缩\转换成ipa包即可。
1 |
|
至此我们就得到了ipa包了。如果需要自动上传到发布平台(比如fir.im),可以直接在生成ipa包之后调用发布平台提供的上传命令直接上传即可,这样整个过程就完全自动化了。
Unity Coroutine的实现原理
在Unity中我经常用到Coroutine的功能,但是对于Coroutine一直有一些疑问没有得到答案,下面先上一个在项目里面经常使用Coroutine的场景的sample:
1 |
|
-
Test中yield语句调用之后为什么可以立即停止,等到 WaitForSeconds 的时候接着执行,这是怎么做到的?
-
Test的返回值是IEnumerator,但是函数内部的return用了yield来修饰,yield到底做了什么工作。
针对第二个问题我找到了一个比较详细的解释, 里面有这样一段话:
The yield keyword actually does quite a lot here. The function returns an object that implements the IEnumerable interface.
相当于yield关键字会自动生成一个集成IEnumerable的对象,那么这个对象到底是什么样子呢? 首先使用ILSpy工具查看下这段代码代码的C#版本:
1 |
|
可以看到通过ILSpy解析之后Test方法已经完全变了,但是也验证了上面的说法。yield关键字会自动转换成一个IEnumerable的对象,这里自动生成了一个名字为TestCoroutine.{Test}c_Iterator0 的类,并且创建了这个类型的 {Test}c_Iterator 对象。
1 |
|
那这个对象内部是怎样实现的呢?现在我们得看下TestCoroutine.{Test}c_Iterator0的实现了(这里方法自动加了DebuggerHidden属性,所以只能看IL版本的代码)
1 |
|
这里只截取了MoveNext方法,因为这个方法里面包含了我们Sample中的Test函数的所有逻辑操作。这段代码我加了注释,通过看代码基本上已经解决了我的第一个疑惑了,下面再做一下分析。Sample中的方法Test在编译之后会转换成大概这个样子(只是模拟):
1 |
|
到这里已经知道了yield关键字所做的操作了,那么现在来看看这个{Test}c_Iterator0对象是怎样被调用的。
首先我们看到我们的Sample中的代码:
1 |
|
这说明创建的{Test}c_Iterator0对象传入的StartCoroutine方法中了,浏览了StartCoroutine的实现,这个方法直接绑定的C++层MonoBehaviour的StartCoroutine方法,这里简述下里面的实现(不贴Unity代码了):
- 调用StartCoroutine方法,该方法会以传入的IEnumerator参数(这里是{Test}c_Iterator0对象)创建一个C++的Coroutine对象
- 这个对象保存会保存参数IEnumerator对象,并且会先获取出IEnuerator的MoveNext和Current方法。这两个方法也是IEunerator最关键的方法
- 创建好之后这个Coroutine对象会保存在MonoBehaviour一个成员变量List中,这样使得MonoBehaviour具备StopCoroutine功能,StopCoroutine能够找到对应Coroutine并停止
- Coroutine对象会调用成员方法run,启动这个Coroutine
这个步骤大概是这样的(代码只表现大概逻辑,不能正确执行):
1 |
|
那么现在生成了C++层的Coroutine对象了,再分析Coroutine现在作为执行者它是怎么实现的。Coroutine里面针对yield对象(Sample里面是WaitForSeconds)类型做了处理:
- WaitForSeconds
- WaitForFixedUpdate
- WaitForEndOfFrame
- Coroutine (C#层)
- WWW
- AsyncOperation
这些类型处理的方式是定义了一个类似定时调用的管理类DelayedCallManager。比如我的条件是WaitForSceonds(5), 那么Coroutine里面会创建一个CallDelayed,把时间设置魏5秒,然后DelayedCallManager的Update里面会直接算时间,到时间了就会回调Coroutine。WWW类型比较特殊它本身做了类似的处理,它提供了一个方法CallWhenDone,当它完成的时候直接回调Coroutine。
这个步骤大概是这样的(代码只表现大概逻辑,不能正确执行):
1 |
|
其他的的类型会直接在下一帧调用,比如yield return 0
整个过程粗略的看大概就是这个样子。上面的分析没有深入到每个条件判断之类的,但是已经够了解Coroutine的全貌了。有错误的地方欢迎指出来,非常感谢。
C语言的变量初始化问题
问题
今天在写opengl shader的时候遇到了一个问题(使用C语言编写的),出问题的代码如下(这里只列出出问题的部分代码):
1 |
|
这段代码的原本的功能是计算点光源。可是代码跑起来之后使用这个shader的模型颜色明显异常。效果如下:
最终查出来的错误的原因是main函数里面result未初始化的原因导致的。一开始我以为是写的光照函数CalcPointLight里面实现有错误,定位了很久才发现出问题的点。
正确的效果应该是这样的:
验证了一下result如果没有初始化直接使用的话,它会对应一个stack区的随机值。这样的话在累加光照值之后计算的color值肯定是错误的。(其实我想如果C语言会报错提示使用为初始化的局部变量会更加人性化)
原理
顺着这个思路想了一下,为什么C语言的全局变量(global)就算不赋值会被自动初始化位默认值,但是局部变量(local)不会呢? 学习了一下C语言的内存布局结构,然后自己验证了一下然后明白了这个原因。
首先我们得知道C语言的内存布局结构,这篇文章 Memory layout of c program 讲的非常详细。这里列出我要用到的部分,首先看结构(图片来自这篇文章):
从这篇文章里面我明白了两点:
- 全局变量是存放在data段内存的。data段分为uninitialized data(bss)段和initalized data,未初始化的变量是放在bss段的,这部分内存存放的变量是会被自动初始化的(这是C语言的特性)
- 局部变量是存放在stack段的。这部分内存是被runtime时期动态分配的。(其实局部变量在代码编译之后就是一个地址,接下来会演示出来)
明白了这两点,那我通过一个demo来验证一下是否在内存中真的是这个样子。
测试环境: Ubuntu 16.02.2 i386
1 |
|
利用demo里面声明的一个全局变量(global_var)和局部变量(local_var)来验证现在的问题。(对local_var加常量3的操作仅仅是为了能够快速定位local_var在反编译代码中的位置)
首先可以通过linux的size命令查看总的内存分布
编译test_c_variable_init.c文件
gcc -o test_c_variable_init test_c_variable_init.c
得到可执行文件test_c_variable_init,现在可以查看到这个可执行文件的内存分布:
然后我们在test_c_variable_init.c中增加一个全局变量:
int global_var2;
按照之前的步骤我们再次查看重新编译的文件的内存分布:
可以看到bss段增加了4kb,这个大小就是我们增加的global_var2变量的大小。按照同样的方式增加local_var2我们会发现data和bss段都不会增加。这样可依初步确定上面列出的两点结论了。
然后我们通过查看反编译代码直接来查看变量在编译之后的位置,这样就明确之前的两点结论。
objdump -D test_c_variable_init > test_c_variable_init.d
我们把反编译之后的代码重定向到test_c_variable_init.d文件,然后我们查看下这个文件,其中有两个地方可以让我们得到想要的答案。
这是反编译之后的main函数:
可以看到347行的 $0xc ,之前我列出demo的时候为local_var特地添加了一个加常量3的操作,通过这个操作我可以很快定位到了$0xc 就是对应的local_var变量了, $0x3 就是常量3(addl就是汇编里面的加运算)。这里证明了local_var是分配在stack区的。此外我们注意到 0x804a020 这个地址其实就是global_var变量了,跳转到这个地址
可以很清楚的看到 0x804a020 就是global_var并且存放在bss段的。
这样我们就验证了上面的两点结论了,也知道了为什么未初始化的全局变量会被自动初始化位默认值,局部变量不会被初始化了。