初级unity开发的重难点知识总结
Posted 事出毙有因
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初级unity开发的重难点知识总结相关的知识,希望对你有一定的参考价值。
生命周期函数
10个
Awake –>OnEnable–> Start –> FixedUpdate–> Update–> LateUpdate –>OnGUI –> Reset –> OnDisable –> OnDestroy
Awake最先执行。Awake函数仅执行一次;如果游戏对象(即gameObject)的初始状态为关闭状态,那么运行程序,Awake函数不会执行;如果游戏对象的初始状态为开启状态,那么Awake函数会执行;对象初始化之后第一调用的函数就是Awake;而Start是在对象初始化后,第一次Update之前调用的,在 Start中进行初始化不是很安全,因为它可能被其他自定义的函数抢先。
Update是在每次渲染新的一帧的时候才会调用,FixedUpdate,是在固定的时间间隔执行,不受游戏帧率(fps)的影响 ,FixedUpdate的时间间隔可以在项目设置中更改,Edit->Project Setting->time 找到Fixed timestep。就可以修改了.
FixedUpdate,每固定帧绘制时执行一次,和 update 不同的是 FixedUpdate 是渲染帧执行,如果你的渲染效率低下的时候FixedUpdate 调用次数就会跟着下降。FixedUpdate比较适用于物理引擎的计算,因为是跟每帧渲染有关。Update 就比较适合做控制。
LateUpdate,,是在所有 update 结束后才调,比较适合用于命令脚本的执行。官网上例子是摄像机的跟随,都是在所有 update 操作完才跟进摄像机,不然就有可能出现摄像机已经推进了,但是视角里还未有角色的空帧出现。
List的底层实现
List底层是用数组实现的,每天添加元素时会确保数组的空间和容量,每次扩容为2的指数,移除时会调用RemoveAt 来移除,RemoveAt里面用的是 Array.Copy 来拷贝数组,从index处之后的元素往前移一位。时间复杂度为O(N)线性时间。清零时容量也会设置为0,排序采用快速排序。
List 源码用数组实现的,常用接口的时间复杂度为线性时间,多次元素增加,扩容方式为2的指数,如果元素数量有65个,则扩容(64*2)128,造成大量的内存空间的浪费。
没有对多线程做任何的加锁安全处理,无法处理并发情况下_size++ 的执行顺序,因此在多线程使用的时候 要进行加锁等安全处理操作。List 兼容性强,但效率并不高。
Dictionary的底层实现
Dictionary的key和value是通过hash函数进行映射的,根据key获取hash值,把一个对象转换成唯一且确定值的函数就叫做哈希函数,每一个hash值都对应一个桶用于存放一个实体对象,但是不同的hashcode有可能对应同一个hash桶,这样会产生冲突,所以用拉链法来解决冲突问题,一个hash桶会类似一个链表,将冲突的实体存储在里面。
初始化Hash桶数量的时候,若未自定义数量,首次分配3个。如果我们自定义了数量,自定义的值会再根据primes数组进行计算,得出到底使用多大的桶数量。
MVC的理解
MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。
Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
View(视图) - 视图代表模型包含的数据的可视化。
Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。
为什么要使用MVC
-
多个视图能共享一个模型。同一个模型可以被不同的视图重用,大大提高了代码的可重用性。
-
由于MVC的三个模块相互独立,改变其中一个不会影响其他两个,所以依据这种设计思想能构造良好的松耦合的构件。
-
控制器提高了应用程序的灵活性和可配置性。控制器可以用来联接不同的模型和视图去完成用户的需求,这样控制器可以为构造应用程序提供强有力的手段
M代表模型,V代表试图,C代表控制器,这是一种用于应用程序分层开发的模式,视图提供数据的可视化,而控制器可以作用于这两者,可以控制数据的变化和更新视图,这样可以让视图和模型分离,这样改变其中一个不会影响其他独立的部分。并且一个模型可以被多个不同的视图使用,提高了代码的复用性。同时控制器也提高了程序的灵活性和复用性。
UGUI相关
UGUI图集有什么用
UGUI图集是一种将多个纹理合并为一个组合纹理的资源。Unity 可以调用此单个纹理来发出单个绘制调用而不是发出多个绘制调用,能够以较小的性能开销一次性访问压缩的纹理。
在UGUI中即使你什么都不干,在项目打包时,Unity也会自动将你的一些小图片资源合在一张大图里面,也就是合成一张图集,但是这种默认的方式,会产生大量的Draw Call,对于性能消耗比较大而NGUI则是开发者必须要将图片资源打包成图集,然后才能使用这些图片资源因此无论是UGUI还是NGUI,掌握图集的打包都是非常重要的。
不同平台,UI贴图的资源格式是怎么选择的
AssetBundle相关
AssetBundle有什么用
AssetBundle就像一个ZIP压缩文件,里面存储着不同平台的特殊资源。使用AssetBundle管理资源,资源是和安装包分离,单独打包的。
一个游戏的资源一般会按照类型或是逻辑分成很多小包,游戏运行时需要用到什么资源就从对应的包中读取相应的资源。
两种压缩方式:
LZMA
LZ4
打包AssetBundle的接口叫什么
unity资源打包的接口,就是BuildPipeline.BuildAssetBundles函数
其对应具体的三个参数,第一个是bundle的输出路径,下面来详细分析一下剩下的2个参数:
-
BuildAssetBundleOptions
具体的参数设置,可以参看API当中的详细信息,下面主要集中说三个参数,分别对应三种压缩格式的选择。
-
BuildAssetBundleOptions.None :
采用LZMA的压缩格式, 这种压缩格式要求资源在使用之前需要全部被解压,这就会带来在使用一个极小的文件的时候会额外带来较长的解压时间消耗。比较蛋疼的是,一旦这个bundle被解压之后,在磁盘上又会以LZ4的格式重新压缩,LZ4的压缩格式,在使用资源的时候不需要全部解压。
这种压缩格式主要用于一个bundle中资源都需要被加载的时候,例如打包角色或者场景资源的时候,这种压缩格式在初始化下载的时候被推荐(更小的包体),这些资源在被解压后,又会以LZ4的格式被缓存。 -
BuildAssetBundleOptions.UncompressedAssetBundle:
无压缩的打包,加载的文件更大,但是时间更快(省去解压的时间)
-
BuildAssetBundleOptions.ChunkBasedCompression:
采用LZ4的压缩格式,相比于LZMA而言文件体积更大,但是不要求在使用之前整个bundle都被解压。LZ4使用chunk based 算法,这就运行文件以chunk或者piece的方式加载,只解压一个chunk文件,而无需解压bundle中其余不相关的chunk。
-
-
BuildTarget
也就是当前资源需要被使用的平台的分类,在打完资源后,会发现文件夹中有更多的文件,一般是2*(n+1), 对于各个不同的资源,都会有一个manifest文件,一般是 bundlename+“.manifest”,除此之外,还会有一个额外的manifest文件,对应不同的平台会有不同的额外的manifest(这是一个总体的manifest文件)。
如何卸载AssetBundle ?
AssetBundle.Unload(bool)
如何卸载AssetBundle Load出来的Asset ?
GameObject.Destroy()
GameObject.DestroyImmediate()
Resources.UnloadAsset()
Resources.UnloadUnusedAssets()
Unity中,有哪些资源加载的方式
Unity对资源进行加载和卸载主要有两种方式:Resources和AssetBundle
- 直接在脚本中public一个对象,然后在监视器面板进行赋值
- 直接在程序中进行find查找
- Resource.load
Resources.Load 加载的资源必须放在 Resources 文件夹下。游戏打包时,Resources文件夹下文件会被全部压缩打包并进行加密,并且 Resources文件夹的目标将不存在,只能通过 Resources.Load 进行加载。 - 把资源打成Assetbudle,然后用的时候load进来
assetbundle就是对资源的打包处理,同时这种资源格式便于从互联网上下载
使用 WWW 或 UnityWebRequest 或者 AssetBundl.LoadFromFile 或者 Resources.Load 加载到 AssetBundle 后。还需要通过 AssetDataBundle 对象调用 LoadAsset 方法,将 包 加载为具体的 资源类型对象。如果是 GameObject 对象,可以通过 Instantiate 在场景创建具体的 GameObject。
什么是drawcall ?如何优化drawcall?
它是CPU调用底层图形接口的操作。DrawCall就是一个让CPU调用图形接口在屏幕上绘制对应的东西的指令。
UI上的DrawCall通常打个图集。
根据情况选择合适的资源合批方式:
- Static Batching(静态批次),场景中的物体可以进行静态合批,对于没有移动且公用材质的物件,能有效减少drawcall。一般来说,静态批次比动态批次更有效,但它会占用更多的内存,也会消耗更少的CPU。其原理是把静态批处理的模型合并到一个新的网格中。(勾选File-BuildSettins-PlayerSettings-OtherSettings-StaticBatching)
- Dynamic Batching(动态批次),当物件使用相同的材质,并且满足一定的条件时,Unity会自动合并物件来减少draw call。Dynamic batching 是自动做的,不需要教你做其他的事情。其原理是每一帧把进行批处理的模型网络进行合并,再把合并后模型数据传递给GPU,条件是有模型顶点数量限制。(勾选File-BuildSettins-PlayerSettings-OtherSettings-DynamicBatching)
- GPU Instancing合批,可以为不同的实例提供不同的旋转比例颜色等特性,不能将Skinned Mesh Renderer应用到实例化,并不是所有平台和API都支持实例化。
- 直接用代码合并,获取物体的MeshRenderer,重新生成一个material然后赋值给新物体,将旧物体隐藏。
什么是overdraw ?如何优化overdraw ?
Overdraw是指屏幕上的某个像素在同一帧的时间内被绘制了多次。
UWA分析报告中,以总填充总数来表达一帧内渲染的像素数量,过多Overdraw可能会引起GPU过载,影响动画的播放和界面响应速度。
可以通过在Scene窗口里设置OverDraw模式来查看,场景中越亮的地方表示overdraw越高。
降低overdraw,可以做如下优化:
1:禁用不可见的面板,比如当打开一个系统时如果完全挡住了另外一个系统,则可以将被遮挡住的系统面板禁用。
2:不要使用空的Image做按键响应。在Unity中RayCast使用Graphi作为基本元素来检测touch。在很多项目中,很多同学使用空的image并将alpha设置为0来接收touch事件,这样会产生不必要的overdraw。可以实现一个只在逻辑上响应Raycast但是不参与绘制的组件即可。
什么是GC?如何优化GC?
GC是为了避免内存溢出而产生的垃圾回收机制
避免:
1)减少 new 产生对象的次数
2)使用公用的对象(静态成员)
3)将 String 换为 StringBuilder
在xLua/toLua中,Lua是如何与C#交互的?如何优化Lua与C#交互的效率?
Lua Call C#
Wrap 方式:
首先生成 C# 源文件所对应的Wrap文件,由 Lua 文件调用 Wrap 文件,再由 Wrap 文件调用 C# 文件
反射方式:
当索引系统 API、dll 库或者第三方库时,如果无法将代码的具体实现进行代码生成,可采用此方式实现交互。
缺点:执行效率低。
过程
lua->wrap->C#
先生成 Wrap 文件(中间文件/适配文件)或者编写 C# 源文件所对应的 c 模块, wrap 文件把字段方法,然后将源文件内容通过 Wrap 文件或者 C 模块把字段方法注册到 lua 虚拟机中(解释器 luajit ),然后 lua 通过 wrap 去调用这个模块的函数。
C# Call Lua
虚拟栈操作方式
C# 把请求或数据放在栈顶,然后 lua 从栈顶取出该数据,在 lua 中做出相应处理(查询,改变),然后把处理结果放回栈顶,最后 C# 再从栈顶取出 lua 处理完的数据,完成交互。
或者在 config 文件中添加相应类型也可以
过程
C#->Bridge->dll->Lua OR C#->dll->Lua
C# 生成 Bridge 文件, Bridge 调 dll 文件( dll 是用 C 写的库),先调用 lua 中 dll 文件,由 dll 文件执行lua代码
交互优化
- 尽量不要在 lua 中传递 Unity 中的类,尽量只传递int,float,double 类型
解决方法:在 C# 中封装方法 Unity 类型的赋值,使用 id(int) 代表对应 object 的传递 - 调用的 C# 方法参数数量尽量少与4个
- C# 方法尽量为静态方法(减少 lua gc )
- 在 lua 中调用 C# 方法获取数据时,其方法参数尽量使用 out 关键字(将表查找转换为对栈访问)
没有生成静态代码,反射调用
- a.把C#对象映射到lua的userdata,userdata只保留一个信息,就是这个对象在C#侧的objects_pool的索引
- b.根据obj获取其ClassType,根据type与调用函数(位置栈里边index2),通过反射获取实际执行的MethodInfo
- c.根据MethodInfo构造一个满足LuaCSFunction的delegate并压栈
- d.delegate构造,根据MethodInfo,取得args信息,根据args从栈中取出赋值,反射调用函数后把结果压栈;GetAsType是把栈上Lua对象转换成指定类型的C#对象,PushCsObject是把一个C#对象按映射规则压到Lua栈上
- e.将统一的反射调用方法objectIndex设置为所有C#对象的metatable的__index
反射带来的问题:
- a.拆装箱开销
- b.stripping引用的反射失效
- c.泛型方法,触发JIT,引起IOS异常
- d.失去静态检查好处
非反射做法
用反射的方式,用工具生成要用在lua侧调用的类的,类似反射调用代码(见lua_call_Csharp示例),只是去除了里边的反射内容,因为针对每个类是确定的,不需要用反射
静态代码的问题:
- a.生成时,有宏定义的方法,如Editor会有问题
- b.生成的代码,增大编译后的代码大小,尤其il2cpp,相应地增加包体大小
*进程、线程和协程
进程是程序执行时的一个实例,即它是程序已经执行到何种程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。
线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
进程——资源分配的最小单位,线程——程序执行的最小单位
进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。
一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。
协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。
在任一指定时刻只有一个协程在运行
例子:假设有一个操作系统,是单核的,系统上没有其他的程序需要运行,有两个线程 A 和 B ,A 和 B 在单独运行时都需要 10 秒来完成自己的任务,而且任务都是运算操作,A B 之间也没有竞争和共享数据的问题。现在 A B 两个线程并行,操作系统会不停的在 A B 两个线程之间切换,达到一种伪并行的效果,假设切换的频率是每秒一次,切换的成本是 0.1 秒(主要是栈切换),总共需要 20 + 19 * 0.1 = 21.9 秒。如果使用协程的方式,可以先运行协程 A ,A 结束的时候让位给协程 B ,只发生一次切换,总时间是 20 + 1 * 0.1 = 20.1 秒。如果系统是双核的,而且线程是标准线程,那么 A B 两个线程就可以真并行,总时间只需要 10 秒,而协程的方案仍然需要 20.1 秒。
子线程是不能直接访问unity对象的,可以通过委托来访问。
仅能从主线程中访问 Unity3D 的组件,对象和 Unity3D 系统调用
支持:如果同时你要处理很多事情或者与 Unity 的对象互动小可以用 thread,否则使用coroutine。
注意:C#中有 lock 这个关键字,以确保只有一个线程可以在特定时间内访问特定的对象
协程的原理:
协程函数的返回值时IEnumerator,它是一个迭代器,可以把它当成执行一个序列的某个节点的指针。
它提供了两个重要的接口,分别是Current(返回当前指向的元素)和MoveNext()(将指针向后移动一个单位,如果移动成功,则返回true)。
yield关键词用来声明序列中的下一个值或者是一个无意义的值。
如果使用yield return x(x是指一个具体的对象或者数值)的话,
那么MoveNext返回为true并且Current被赋值为x,如果使用yield break使得MoveNext()返回为false。
如果MoveNext函数返回为true意味着协程的执行条件被满足,则能够从当前的位置继续往下执行。否则不能从当前位置继续往下执行。
每次yield返回后会把后面的语句块挂起,此时会返回一个bool值判断当前协程是否结束,如果没有结束会待特定时间后继续进行协程。
使用协程的时机距离:异步加载资源的时候
yield return null; // 下一帧再执行后续代码
yield return 0; //下一帧再执行后续代码
yield return 6;//(任意数字) 下一帧再执行后续代码
yield break; //直接结束该协程的后续操作
yield return WWW();//等待WWW操作完成后再执行后续代码
渲染管线
Vertex Shader:对顶点进行处理,顶点动画在这个部分进行。主要是把顶点变换到裁切空间上,
Triangel Processing:绘制三角形
Rasterization:把三角形的像素绘制出来,也就是光栅化。根据顶点位置插值化处理每个像素。
Pixel Shader:对像素进行处理,例如根据灯光做出明暗变化,根据贴图坐标去采样贴图让模型更有纹理细节。
Frame Buffer:后处理阶段,比如抗锯齿、校色、添加bloom(辉光)等
以上是关于初级unity开发的重难点知识总结的主要内容,如果未能解决你的问题,请参考以下文章