2018-11-24 19:31:45 qq_34691688 阅读数 4656
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

自己平时挺喜欢玩游戏的,游戏玩的多了,就会有自己的想法,对于每一款玩过的游戏都会有自己的看法,要是有一天能玩上自己开发的游戏就更开心了,也一直在这条道路上探索着,成长着。

在平时做项目的时候,遇到许许多多的问题,随手就把百度、Google的答案,还有一些前辈的经验记录在word,以前比较没有经验,感觉无所谓记录在word,后面想要查找的时候发现,我去,我word文档不见了又或者是换了一台电脑,哎呀,学习文档没有拷过来,失策失策,以后记录知识问题就在这里记录吧,里面记录的知识也会把它再一次总结,放到博客里面,要是里面的知识点跟您的很相似,莫怪莫怪,主要是觉得您写的非常好,如果没有带上你的文章链接,请您告诉我,我会加进去,谢谢大佬。

 学习的道路是有点枯燥无味,但是每当学习到新的技能之后,那喜悦的感觉就像获得了新大陆,慢慢加油吧!如果大神们有空可以指点一波!等我有空的时候,慢慢的把这些知识按照模块搬到博客上来,然后写上自己的一些使用心得吧!

2018-08-28 18:05:03 q764424567 阅读数 387
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

一、前言

做游戏经验比较丰富的人都知道,优化的好坏一直是一个游戏的评判标准之一,它直接影响着玩家们的游戏体验,优化一直是项目中开发周期比较长的一个点,也是开发者头疼的一个问题,要求掌握的知识点比较全面,经验也要求比较丰富。
这篇文章参考很多文章的知识点,加以总结与学习,从最基础的概念讲起,配合讲解各种优化技巧,希望大家可以在我的文章中学到一些东西。

二、参考文章

【Unity优化总结】https://blog.csdn.net/ynnmnm/article/details/36759789
【Unity教程之再谈Unity中的优化技术】https://www.cnblogs.com/gabo/p/4592194.html
【Unity优化之GC——合理优化Unity的GC】https://www.cnblogs.com/zblade/p/6445578.html
【Unity优化Unity优化技巧进阶(持续更新中…)】https://blog.csdn.net/andrewfan/article/details/59057811
【Unity性能优化】https://blog.csdn.net/wwlcsdn000/article/details/78777639
【Unity优化技巧】http://www.360doc.com/content/17/1215/10/30388632_713252002.shtml#
【Unity教程之再谈Unity中的优化技术】http://www.unity.5helpyou.com/2786.html

三、正文

目录

一、CPU优化

     DrawCall优化

     算法优化

     物理算法优化

     物理效果优化

     GC优化

     脚本优化

     文件优化

二、GPU优化

     顶点计算优化

     片元计算优化

三、带宽(资源)优化

     纹理贴图优化

     帧优化

     资源优化

四、Unity3d自带的优化技术

     优化几何体

     LOD

     遮挡剔除

     批处理

五、官方建议








一、CPU优化

DrawCall优化

DrawCall一直都是老生常谈的问题了,为什么总是这个东西在消耗资源呢,这是个什么东西呢

1.什么是DrawCall

Drawcall是CPU向GPU发送绘制命令的接口调用。理论上每一个不同材质的物件需要渲染在屏幕上时,CPU都会调用图形API ( openGL or Diract3D ) 的Draw接口触发显卡进行绘制。

2.为什么优化Drawcall?

Drawcall对硬件和驱动而言,要求大量设置状态(使用哪些顶点、哪些shader等)和状态转换。而Drawcall最大的消耗在于:如果每次drawcall只提交少量的数据将导致CPU瓶颈,CPU无法将GPU填满。Drawcall对GPU的耗费在于硬件一直等待CPU提交数据,而无法得到有效利用。GPU大量的时间耗费在不断切换状态和正确性检测上。 GPU在Draw Call之间,为了防止前后Draw的依赖关系造成绘制错误或者资源竞用,一般会在Draw Call后Flush整个流水线,小粒度的Draw Call对GPU流水线来说是个很大的浪费。(这个问题在D3D老版本存在,在新版D3D11中得到改善。)实际上unity官方指出,Drawcall数量的降低并非重点,重点是减少批次的数量,Drawcall优化实际上是对批次数量的优化。

延伸阅读:
  · why are draw calls expensive? — Stack Overflow
  · Direct3D Draw函数 异步调用原理解析

3.如何优化Drawcall?

在Unity中对Drawcall的优化有以下几个策略:Drawcall batching,合并打包图集,减少光照和阴影以及遮挡剔除和视锥剔除等。以下分别谈一下各个策略的优缺点。

3.1Drawcall Batching

  • 静态批次 Drawcall static batching
      场景中的多个物件如果是不移动的(包括位置、缩放、旋转等),并且共享同一材质,比如地形、建筑、花盆等,那么可以选择采用静态批次。静态批次只需要在Inspector勾选static选项即可。静态批次需要注意的是,unity会将进行批次的多个对象合并成一个大的对象,也会导致内存损耗,有时候要避免太多对象静态批次造成的内存过高。这也表明,优化并非绝对做好某一方面,而是平衡各个硬件的瓶颈和效率,选择相对适中的方案。
  • 动态批次 Drawcall dynamic batching
      动态批次是运动的物件在unity中也可以进行批次渲染,动态批次不需要手动设置,是unity自动进行的,但是这里有诸多陷阱和约束,开发者需要遵守一定的限制条件才能享受动态批次的好处。
      
      1) . 动态批次是逐顶点处理的,因此仅对少于900个顶点的mesh有效。如果shader使用了顶点位置,法线和UV那么仅支持低于300顶点的mesh,而如果shader使用了顶点位置,法线、UV0、UV1和切向量,则之多仅支持180顶点。

      2) . 缩放问题

      缩放对于批次是有影响的,这里涉及到一个统一缩放和非统一缩放的概念。统一缩放即为三轴同比例缩放,比如(1,1,1),(2,2,2)(5,5,5)… 非统一缩放即为三轴不同比例缩放,如(1,2,1)(2,1,1)(1,2,3)等等。

      Unity对统一缩放的对象是不进行动态批次的,而对非同一缩放的对象是可以进行动态批次的。这里有点诡异,查阅了一些资料,解释如下:

      对于非同一缩放的物件,unity将其mesh进行了复制,因此即便是从相同物件进行的非同一缩放的两个对象是两份mesh;对统一缩放的对象来说,unity不对mesh进行复制,而是使用同一mesh进行缩放,此时复制mesh来进行批次渲染是不值得的,但是对于非统一缩放的对象,既然已经复制了mesh(不是为了批次,而是其他原因决定复制mesh),那么进行批次是顺带实现的。

      很多时候分辨率也是造成性能下降的原因,尤其是现在很多国内山寨机,除了分辨率高其他硬件简直一塌糊涂,而这恰恰中了游戏性能的两个瓶颈:过大的屏幕分辨率+糟糕的GPU。因此,我们可能需要对于特定机器进行分辨率的放缩。当然,这样会造成游戏效果的下降,但性能和画面之间永远是个需要权衡的话题。
    在Unity中设置屏幕分辨率可以直接调用Screen.SetResolution。

    (参考 Dynamic Batching and Scale ——unity3d answers

      3) . 使用了不同的材质,即便实质上是相同的(比如两个一模一样的材质),也不会进行批次。

      4) . 拥有lightmap的物件含有额外的材质属性,比如lightmap偏移和缩放系数等,所以拥有lightmap的物件不能批次。

      5) . 多通道的shader会妨碍批处理操作,接受实时阴影的物件无法批次。
      
      6). unity渲染是有顺序的,渲染排序有可能打断动态批次。
      

3.2渲染顺序跟什么有关呢?

首先根据物件到摄像机的距离,进行远处物件先渲染近处物件后渲染。相同材质的物件尽量在一层,不要让不同材质的物件进入这一层。如果无法保证这一点,那么还有一种方法:修改shader中渲染队列值。即打开shader 将subshader中的tag{}中queue 修改为小于2500的值。

  渲染队列小于等于2500时,unity认为其是不透明的,对于不同材质但z值相同对象,unity不对其进行排序,这样能保证相同材质的多个对象能是一个批次,不同材质的对象如果进入两个相同材质的对象之间,不会打破批次;

   渲染队列大于2500时,unity会对不同材质的对象进行排序,此时如果不同材质的对象进入到两个相同材质的对象之间的话,会使相同材质的对象批次被打破。

  批次先写到这,其实很多网上都有,不过有些没深入讲解,也有些没给出解决办法,我就使用每个方案时遇到的困难给出了自己的解决方案。其实批次还有不少研究的地方,之后想到了会继续更新。

3.3如何控制渲染顺序

需要控制绘制顺序,主要原因是为了最大限度的避免overdraws,也就是同一个位置的像素可以需要被绘制多变。在PC上,资源无限,为了得到最准确的渲染结果,绘制顺序可能是从后往前绘制不透明物体,然后再绘制透明物体进行混合。但在移动平台上,这种会造成大量overdraw的方式显然是不适合的,我们应该尽量从前往后绘制。从前往后绘制之所以可以减少overdraw,都是因为深度检验的功劳。
在Unity中,那些Shader中被设置为“Geometry” 队列的对象总是从前往后绘制的,而其他固定队列(如“Transparent”“Overla”等)的物体,则都是从后往前绘制的。这意味这,我们可以尽量把物体的队列设置为“Geometry” 。
而且,我们还可以充分利用Unity的队列来控制绘制顺序。例如,对于天空盒子来说,它几乎覆盖了所有的像素,而且我们知道它永远会在所有物体的后面,因此它的队列可以设置为“Geometry+1”。这样,就可以保证不会因为它而造成overdraws。

  延伸阅读:   

  · Unity -Draw Call Batching

  · Unity Drawcall 优化手记

4.合并图集

  其实合并图集也是利用了Unity的Drawcall batching。将多个纹理进行打包成图集是为了减少材质,这样多个对象共享一个材质,并进而使用同一个纹理和shader,触发unity的动态批次。图集打包工具有很多,Asset store中也可以搜到不少,比如Texture Packer Free 、 DrawCall Optimizer(收费) Mesh Baker Free 等等都可以将贴图打包合并。

  但是合并图集也有缺点,合并贴图时应该注意选择同时出现在屏幕的对象贴图进行合并。如果不能做到这一点,那么合并图集可能起到反作用,即渲染一个对象需要加载过多无用贴图,造成内存占用率升高。我的项目这个方案也是采用之后又弃用的,因为归类同时出现在屏幕的贴图并非易事!
  

5.光照和阴影

实时光照和阴影可能增加Drawcall,带有光源计算的shader材质会因为光照产生多个Drawcall。使用灯光会打断Drawcall batching,尽量使用烘焙灯光贴图等技巧来实现灯光效果。

6.时刻警惕透明物体

而对于透明对象,由于它本身的特性(可以看之前关于Alpha Test和Alpha Blending的一篇文章)决定如果要得到正确的渲染效果,就必须从后往前渲染(这里不讨论使用深度的方法),而且抛弃了深度检验。这意味着,透明物体几乎一定会造成overdraws。如果我们不注意这一点,在一些机器上可能会造成严重的性能下面。例如,对于GUI对象来说,它们大多被设置成了半透明,如果屏幕中GUI占据的比例太多,而主摄像机又没有进行调整而是投影整个屏幕,那么GUI就会造成屏幕的大量overdraws。
因此,如果场景中大面积的透明对象,或者有很多层覆盖的多层透明对象(即便它们每个的面积可以都不大),或者是透明的粒子效果,在移动设备上也会造成大量的overdraws。这是应该尽量避免的。
对于上述GUI的这种情况,我们可以尽量减少窗口中GUI所占的面积。如果实在无能为力,我们可以把GUI绘制和三维场景的绘制交给不同的摄像机,而其中负责三维场景的摄像机的视角范围尽量不要和GUI重叠。对于其他情况,只能说,尽可能少用。当然这样会对游戏的美观度产生一定影响,因此我们可以在代码中对机器的性能进行判断,例如首先关闭所有的耗费性能的功能,如果发现这个机器表现非常良好,再尝试开启一些特效功能。

  延伸阅读:

    · Forward Rendering Path Details

    · Light Troubleshooting and Performance

算法优化

  1. 降低算法的复杂度,了解常见的数据结构和算法
  2. 使用协程来避免多余的更新计算。
  3. 缓存耗时计算的中间值
  4. 缓存获取过的Componenet
  5. IMGUI只用与测试,发布时应该屏蔽

物理算法优化

  • 尽可能少用刚体,不重要的物体可以自己写简易的模拟物理
  • 尽可能减少直接的模型网格碰撞,应使用包围盒简易碰撞体替代
  • 如果可以的话,增加fixedUpdate的运行间隔
  • 使用高效的射线检测算法,并避免产生GC

物理效果优化

  • 镜头
    Clipping Planes
    Occlusion Culling 默认勾上了。但是没有任何效果
    打开Window-Occlusion Culling 需要bake 一下
    需要bake的东西,必须是Static
    Smallers Occluder 挡住后面的东西,优化做不好,就可能是负优化
    在Scene场景,选中摄像机,可以设置Occlusion Culling是Edit或者Visualize。这个时候随着镜头的移动,镜头中的物体就会动态的显示了。
    从屏幕上看到的点,都不会剔除掉的。
    Unity 3专业版内置了一个强大的 Occlusion Culling 插件 Umbra免费的
  • 光照
    Bake and Probes
    我们的目标就是降低:SetPass Calls
    Light 选则Bake光,使用静态光。出现色差
    设置:Scenes In Build Player Setting
    找到Color Space 有 Linear(不支持移动端) Gamma
    缺点:移动的物体不会受到光照的影响。这个时候就需要创建光照探针了。Light-Light Probe Group.记录静态光照的效果。
  • 碰撞
    Collider尽可能简单
    控制rigidbody数量
    Rigidbody检查方式,检测间隔,Collision Detection 持续的。离散型的。
  • CheckList
    Simple checklist to make your game faster
    对于PC建筑(取决于目标GPU)时,请记住下面的200K和3M顶点数每帧。
    如果你使用内置着色器,从挑选的那些移动或熄灭类别。他们在非移动平台以及工作,但更复杂的着色器的简化和近似版本。
    保持每个场景低的不同材料的数量,并共享不同的对象尽可能之间尽可能多的材料。
    将Static非运动物体的属性,以允许像内部优化静态批次。
    只有一个(最好是定向)pixel light影响几何体,而不是整数倍。
    烘烤照明,而不是使用动态照明。
    尽可能使用压缩纹理格式,以及超过32位纹理使用16位纹理。
    避免使用雾在可能的情况。
    使用遮挡剔除,以减少可见的几何图形的量和抽取呼叫中的有很多闭塞复杂静态场景的情况。闭塞记扑杀设计你的水平。
    使用包厢到“假”遥远的几何体。
    使用像素着色器或纹理组合搭配,而不是多遍方法有几个纹理。
    使用half精度变量在可能的情况。
    尽量减少使用复杂的数学运算,如的pow,sin并cos在像素着色器。
    使用每个片段较少纹理。

GC优化

  在游戏运行的时候,数据主要存储在内存中,当游戏的数据在不需要的时候,存储当前数据的内存就可以被回收以再次使用。内存垃圾是指当前废弃数据所占用的内存,垃圾回收(GC)是指将废弃的内存重新回收再次使用的过程。

  Unity中将垃圾回收当作内存管理的一部分,如果游戏中废弃数据占用内存较大,则游戏的性能会受到极大影响,此时垃圾回收会成为游戏性能的一大障碍点。

  下面我们主要学习垃圾回收的机制,垃圾回收如何被触发以及如何提GC收效率来提高游戏的性能。

1. Unity内存管理机制简介

  要想了解垃圾回收如何工作以及何时被触发,我们首先需要了解unity的内存管理机制。Unity主要采用自动内存管理的机制,开发时在代码中不需要详细地告诉unity如何进行内存管理,unity内部自身会进行内存管理。这和使用C++开发需要随时管理内存相比,有一定的优势,当然带来的劣势就是需要随时关注内存的增长,不要让游戏在手机上跑“飞”了。

  unity的自动内存管理可以理解为以下几个部分:

  1)unity内部有两个内存管理池:堆内存和堆栈内存。堆栈内存(stack)主要用来存储较小的和短暂的数据,堆内存(heap)主要用来存储较大的和存储时间较长的数据。

  2)unity中的变量只会在堆栈或者堆内存上进行内存分配,变量要么存储在堆栈内存上,要么处于堆内存上。

  3)只要变量处于激活状态,则其占用的内存会被标记为使用状态,则该部分的内存处于被分配的状态。

  4)一旦变量不再激活,则其所占用的内存不再需要,该部分内存可以被回收到内存池中被再次使用,这样的操作就是内存回收。处于堆栈上的内存回收及其快速,处于堆上的内存并不是及时回收的,此时其对应的内存依然会被标记为使用状态。

  5) 垃圾回收主要是指堆上的内存分配和回收,unity中会定时对堆内存进行GC操作。

  在了解了GC的过程后,下面详细了解堆内存和堆栈内存的分配和回收机制的差别。

2.堆栈内存分配和回收机制

  堆栈上的内存分配和回收十分快捷简单,因为堆栈上只会存储短暂的或者较小的变量。内存分配和回收都会以一种顺序和大小可控制的形式进行。

  堆栈的运行方式就像stack: 其本质只是一个数据的集合,数据的进出都以一种固定的方式运行。正是这种简洁性和固定性使得堆栈的操作十分快捷。当数据被存储在堆栈上的时候,只需要简单地在其后进行扩展。当数据失效的时候,只需要将其从堆栈上移除。

3. 堆内存分配和回收机制

  堆内存上的内存分配和存储相对而言更加复杂,主要是堆内存上可以存储短期较小的数据,也可以存储各种类型和大小的数据。其上的内存分配和回收顺序并不可控,可能会要求分配不同大小的内存单元来存储数据。

  堆上的变量在存储的时候,主要分为以下几步:

  1)首先,unity检测是否有足够的闲置内存单元用来存储数据,如果有,则分配对应大小的内存单元;

  2)如果没有足够的存储单元,unity会触发垃圾回收来释放不再被使用的堆内存。这步操作是一步缓慢的操作,如果垃圾回收后有足够大小的内存单元,则进行内存分配。

  3)如果垃圾回收后并没有足够的内存单元,则unity会扩展堆内存的大小,这步操作会很缓慢,然后分配对应大小的内存单元给变量。

  堆内存的分配有可能会变得十分缓慢,特别是在需要垃圾回收和堆内存需要扩展的情况下,通常需要减少这样的操作次数。

4.垃圾回收时的操作

  当堆内存上一个变量不再处于激活状态的时候,其所占用的内存并不会立刻被回收,不再使用的内存只会在GC的时候才会被回收。

  每次运行GC的时候,主要进行下面的操作:

  1)GC会检查堆内存上的每个存储变量;

  2)对每个变量会检测其引用是否处于激活状态;

  3)如果变量的引用不再处于激活状态,则会被标记为可回收;

  4)被标记的变量会被移除,其所占有的内存会被回收到堆内存上。

  GC操作是一个极其耗费的操作,堆内存上的变量或者引用越多则其运行的操作会更多,耗费的时间越长。

5. 何时会触发垃圾回收

  主要有三个操作会触发垃圾回收:

   1) 在堆内存上进行内存分配操作而内存不够的时候都会触发垃圾回收来利用闲置的内存;

   2) GC会自动的触发,不同平台运行频率不一样;

   3) GC可以被强制执行。

  特别是在堆内存上进行内存分配时内存单元不足够的时候,GC会被频繁触发,这就意味着频繁在堆内存上进行内存分配和回收会触发频繁的GC操作。

6. GC操作带来的问题

  在了解GC在unity内存管理中的作用后,我们需要考虑其带来的问题。最明显的问题是GC操作会需要大量的时间来运行,如果堆内存上有大量的变量或者引用需要检查,则检查的操作会十分缓慢,这就会使得游戏运行缓慢。其次GC可能会在关键时候运行,例如在CPU处于游戏的性能运行关键时刻,此时任何一个额外的操作都可能会带来极大的影响,使得游戏帧率下降。

  另外一个GC带来的问题是堆内存的碎片划。当一个内存单元从堆内存上分配出来,其大小取决于其存储的变量的大小。当该内存被回收到堆内存上的时候,有可能使得堆内存被分割成碎片化的单元。也就是说堆内存总体可以使用的内存单元较大,但是单独的内存单元较小,在下次内存分配的时候不能找到合适大小的存储单元,这也会触发GC操作或者堆内存扩展操作。

  堆内存碎片会造成两个结果,一个是游戏占用的内存会越来越大,一个是GC会更加频繁地被触发。

7. 分析GC带来的问题

  GC操作带来的问题主要表现为帧率运行低,性能间歇中断或者降低。如果游戏有这样的表现,则首先需要打开unity中的profiler window来确定是否是GC造成。

  了解如何运用profiler window,可以参考此处,如果游戏确实是由GC造成的,可以继续阅读下面的内容。

8. 分析堆内存的分配

  如果GC造成游戏的性能问题,我们需要知道游戏中的哪部分代码会造成GC,内存垃圾在变量不再激活的时候产生,所以首先我们需要知道堆内存上分配的是什么变量。

  堆内存和堆栈内存分配的变量类型

  在Unity中,值类型变量都在堆栈上进行内存分配,其他类型的变量都在堆内存上分配。如果你不知道值类型和引用类型的差别,可以查看此处。

  下面的代码可以用来理解值类型的分配和释放,其对应的变量在函数调用完后会立即回收:
  

void ExampleFunciton()
{

  int localInt = 5;  
}

对应的引用类型的参考代码如下,其对应的变量在GC的时候才回收:

void ExampleFunction()
{
  List localList = new List();      
}

9. 利用profiler window 来检测堆内存分配

  我们可以在profier window中检查堆内存的分配操作:在CPU usage分析窗口中,我们可以检测任何一帧cpu的内存分配情况。其中一个选项是GC Alloc,通过分析其来定位是什么函数造成大量的堆内存分配操作。一旦定位该函数,我们就可以分析解决其造成问题的原因从而减少内存垃圾的产生。现在Unity5.5的版本,还提供了deep profiler的方式深度分析GC垃圾的产生。

10. 降低GC的影响的方法

 大体上来说,我们可以通过三种方法来降低GC的影响:

  1)减少GC的运行次数;

  2)减少单次GC的运行时间;

  3)将GC的运行时间延迟,避免在关键时候触发,比如可以在场景加载的时候调用GC

似乎看起来很简单,基于此,我们可以采用三种策略:

  1)对游戏进行重构,减少堆内存的分配和引用的分配。更少的变量和引用会减少GC操作中的检测个数从而提高GC的运行效率。

  2)降低堆内存分配和回收的频率,尤其是在关键时刻。也就是说更少的事件触发GC操作,同时也降低堆内存的碎片化。

  3)我们可以试着测量GC和堆内存扩展的时间,使其按照可预测的顺序执行。当然这样操作的难度极大,但是这会大大降低GC的影响。
  

11. 减少内存垃圾的数量

减少内存垃圾主要可以通过一些方法来减少:

11.1缓存

  如果在代码中反复调用某些造成堆内存分配的函数但是其返回结果并没有使用,这就会造成不必要的内存垃圾,我们可以缓存这些变量来重复利用,这就是缓存。

   例如下面的代码每次调用的时候就会造成堆内存分配,主要是每次都会分配一个新的数组:

void OnTriggerEnter(Collider other)
{
     Renderer[] allRenderers = FindObjectsOfType<Renderer>();
     ExampleFunction(allRenderers);      
}

对比下面的代码,只会生产一个数组用来缓存数据,实现反复利用而不需要造成更多的内存垃圾:

private Renderer[] allRenderers;

void Start()
{
   allRenderers = FindObjectsOfType<Renderer>();
}

void OnTriggerEnter(Collider other)
{
    ExampleFunction(allRenderers);
}

11.2 不要在频繁调用的函数中反复进行堆内存分配

  在MonoBehaviour中,如果我们需要进行堆内存分配,最坏的情况就是在其反复调用的函数中进行堆内存分配,例如Update()和LateUpdate()函数这种每帧都调用的函数,这会造成大量的内存垃圾。我们可以考虑在Start()或者Awake()函数中进行内存分配,这样可以减少内存垃圾。

  下面的例子中,update函数会多次触发内存垃圾的产生:

void Update()
{
    ExampleGarbageGenerationFunction(transform.position.x);
}

通过一个简单的改变,我们可以确保每次在x改变的时候才触发函数调用,这样避免每帧都进行堆内存分配:

private float previousTransformPositionX;

void Update()
{
    float transformPositionX = transform.position.x;
    if(transfromPositionX != previousTransformPositionX)
    {
        ExampleGarbageGenerationFunction(transformPositionX);    
        previousTransformPositionX = trasnformPositionX;
    }
}

另外的一种方法是在update中采用计时器,特别是在运行有规律但是不需要每帧都运行的代码中,例如:

void Update()
{
    ExampleGarbageGeneratiingFunction()
}

通过添加一个计时器,我们可以确保每隔1s才触发该函数一次:

private float timeSinceLastCalled;
private float delay = 1f;
void Update()
{
    timSinceLastCalled += Time.deltaTime;
    if(timeSinceLastCalled > delay)
    {
         ExampleGarbageGenerationFunction();
         timeSinceLastCalled = 0f;
    }
}

  通过这样细小的改变,我们可以使得代码运行的更快同时减少内存垃圾的产生。

  附: 不要忽略这一个方法,在最近的项目性能优化中,我经常采用这样的方法来优化游戏的性能,很多对于固定时间的事件回调函数中,如果每次都分配新的缓存,但是在操作完后并不释放,这样就会造成大量的内存垃圾,对于这样的缓存,最好的办法就是当前周期回调后执行清除或者标志为废弃。

11.3 清除链表

  在堆内存上进行链表的分配的时候,如果该链表需要多次反复的分配,我们可以采用链表的clear函数来清空链表从而替代反复多次的创建分配链表。

void Update()
{
    List myList = new List();
    PopulateList(myList);       
}

通过改进,我们可以将该链表只在第一次创建或者该链表必须重新设置的时候才进行堆内存分配,从而大大减少内存垃圾的产生:

private List myList = new List();
void Update()
{
    myList.Clear();
    PopulateList(myList);
}

11.4 对象池

  即便我们在代码中尽可能地减少堆内存的分配行为,但是如果游戏有大量的对象需要产生和销毁依然会造成GC。对象池技术可以通过重复使用对象来降低堆内存的分配和回收频率。对象池在游戏中广泛的使用,特别是在游戏中需要频繁的创建和销毁相同的游戏对象的时候,例如枪的子弹这种会频繁生成和销毁的对象。

  要详细的讲解对象池已经超出本文的范围,但是该技术值得我们深入的研究This tutorial on object pooling on the Unity Learn site对于对象池有详细深入的讲解。

  附:对象池技术属于游戏中比较通用的技术,如果有闲余时间,大家可以学习一下这方面的知识。

12 造成不必要的堆内存分配的因素

我们已经知道值类型变量在堆栈上分配,其他的变量在堆内存上分配,但是任然有一些情况下的堆内存分配会让我们感到吃惊。下面让我们分析一些常见的不必要的堆内存分配行为并对其进行优化。

12.1字符串

  在c#中,字符串是引用类型变量而不是值类型变量,即使看起来它是存储字符串的值的。这就意味着字符串会造成一定的内存垃圾,由于代码中经常使用字符串,所以我们需要对其格外小心。

  c#中的字符串是不可变更的,也就是说其内部的值在创建后是不可被变更的。每次在对字符串进行操作的时候(例如运用字符串的“加”操作),unity会新建一个字符串用来存储新的字符串,使得旧的字符串被废弃,这样就会造成内存垃圾。

  我们可以采用以下的一些方法来最小化字符串的影响:

  1)减少不必要的字符串的创建,如果一个字符串被多次利用,我们可以创建并缓存该字符串。

  2)减少不必要的字符串操作,例如如果在Text组件中,有一部分字符串需要经常改变,但是其他部分不会,则我们可以将其分为两个部分的组件,对于不变的部分就设置为类似常量字符串即可,见下面的例子。

  3)如果我们需要实时的创建字符串,我们可以采用StringBuilderClass来代替,StringBuilder专为不需要进行内存分配而设计,从而减少字符串产生的内存垃圾。

  4)移除游戏中的Debug.Log()函数的代码,尽管该函数可能输出为空,对该函数的调用依然会执行,该函数会创建至少一个字符(空字符)的字符串。如果游戏中有大量的该函数的调用,这会造成内存垃圾的增加。

  在下面的代码中,在Update函数中会进行一个string的操作,这样的操作就会造成不必要的内存垃圾:

public Text timerText;
private float timer;
void Update()
{
    timer += Time.deltaTime;
    timerText.text = "Time:"+ timer.ToString();
}

通过将字符串进行分隔,我们可以剔除字符串的加操作,从而减少不必要的内存垃圾:

public Text timerHeaderText;
public Text timerValueText;
private float timer;
void Start()
{
    timerHeaderText.text = "TIME:";
}

void Update()
{
   timerValueText.text = timer.ToString();
}

12.2 Unity3d函数的调用

  在代码编程中,当我们调用不是我们自己编写的代码,无论是Unity自带的还是插件中的,我们都可能会产生内存垃圾。Unity的某些函数调用会产生内存垃圾,我们在使用的时候需要注意它的使用。

  这儿没有明确的列表指出哪些函数需要注意,每个函数在不同的情况下有不同的使用,所以最好仔细地分析游戏,定位内存垃圾的产生原因以及如何解决问题。有时候缓存是一种有效的办法,有时候尽量降低函数的调用频率是一种办法,有时候用其他函数来重构代码是一种办法。现在来分析unity中常见的造成堆内存分配的函数调用。

  在Unity中如果函数需要返回一个数组,则一个新的数组会被分配出来用作结果返回,这不容易被注意到,特别是如果该函数含有迭代器,下面的代码中对于每个迭代器都会产生一个新的数组:

void ExampleFunction()
{
    for(int i=0; i < myMesh.normals.Length;i++)
    {
        Vector3 normal = myMesh.normals[i];
    }
}

对于这样的问题,我们可以缓存一个数组的引用,这样只需要分配一个数组就可以实现相同的功能,从而减少内存垃圾的产生:

void ExampleFunction()
{
    Vector3[] meshNormals = myMesh.normals;
    for(int i=0; i < meshNormals.Length;i++)
    {
        Vector3 normal = meshNormals[i];
    }
}

  此外另外的一个函数调用GameObject.name 或者 GameObject.tag也会造成预想不到的堆内存分配,这两个函数都会将结果存为新的字符串返回,这就会造成不必要的内存垃圾,对结果进行缓存是一种有效的办法,但是在Unity中都对应的有相关的函数来替代。对于比较gameObject的tag,可以采用GameObject.CompareTag()来替代。

  在下面的代码中,调用gameobject.tag就会产生内存垃圾:

private string playerTag="Player";
void OnTriggerEnter(Collider other)
{
    bool isPlayer = other.gameObject.tag == playerTag;
}

  采用GameObject.CompareTag()可以避免内存垃圾的产生:
private string playerTag = “Player”;
void OnTriggerEnter(Collider other)
{
bool isPlayer = other.gameObject.CompareTag(playerTag);
}
不只是GameObject.CompareTag,unity中许多其他的函数也可以避免内存垃圾的生成。比如我们可以用Input.GetTouch()和Input.touchCount()来代替Input.touches,或者用Physics.SphereCastNonAlloc()来代替Physics.SphereCastAll()。

12.3 装箱操作

  装箱操作是指一个值类型变量被用作引用类型变量时候的内部变换过程,如果我们向带有对象类型参数的函数传入值类型,这就会触发装箱操作。比如String.Format()函数需要传入字符串和对象类型参数,如果传入字符串和int类型数据,就会触发装箱操作。如下面代码所示:
  

void ExampleFunction()
{
    int cost = 5;
    string displayString = String.Format("Price:{0} gold",cost);
}

  在Unity的装箱操作中,对于值类型会在堆内存上分配一个System.Object类型的引用来封装该值类型变量,其对应的缓存就会产生内存垃圾。装箱操作是非常普遍的一种产生内存垃圾的行为,即使代码中没有直接的对变量进行装箱操作,在插件或者其他的函数中也有可能会产生。最好的解决办法是尽可能的避免或者移除造成装箱操作的代码。

12.4 协程

  调用 StartCoroutine()会产生少量的内存垃圾,因为unity会生成实体来管理协程。所以在游戏的关键时刻应该限制该函数的调用。基于此,任何在游戏关键时刻调用的协程都需要特别的注意,特别是包含延迟回调的协程。

  yield在协程中不会产生堆内存分配,但是如果yield带有参数返回,则会造成不必要的内存垃圾,例如:

yield return 0;

  由于需要返回0,引发了装箱操作,所以会产生内存垃圾。这种情况下,为了避免内存垃圾,我们可以这样返回:


yield return null;

  另外一种对协程的错误使用是每次返回的时候都new同一个变量,例如:

while(!isComplete)
{
    yield return new WaitForSeconds(1f);
}

  我们可以采用缓存来避免这样的内存垃圾产生:

WaitForSeconds delay = new WaiForSeconds(1f);
while(!isComplete)
{
    yield return delay;
}

  如果游戏中的协程产生了内存垃圾,我们可以考虑用其他的方式来替代协程。重构代码对于游戏而言十分复杂,但是对于协程而言我们也可以注意一些常见的操作,比如如果用协程来管理时间,最好在update函数中保持对时间的记录。如果用协程来控制游戏中事件的发生顺序,最好对于不同事件之间有一定的信息通信的方式。对于协程而言没有适合各种情况的方法,只有根据具体的代码来选择最好的解决办法。

12.4 foreach 循环

  在unity5.5以前的版本中,在foreach的迭代中都会生成内存垃圾,主要来自于其后的装箱操作。每次在foreach迭代的时候,都会在堆内存上生产一个System.Object用来实现迭代循环操作。在unity5.5中解决了这个问题,比如,在unity5.5以前的版本中,用foreach实现循环:
  

void ExampleFunction(List listOfInts)
{
    foreach(int currentInt in listOfInts)
    {
        DoSomething(currentInt);
    }
}

如果游戏工程不能升级到5.5以上,则可以用for或者while循环来解决这个问题,所以可以改为:

void ExampleFunction(List listOfInts)
{
    for(int i=0; i < listOfInts.Count; i++)
    {
        int currentInt = listOfInts[i];
        DoSomething(currentInt);
    }
}

12.4.1 foreach怎么产生垃圾的

  研究过这个问题的人都应该知道,就是它会引起频繁的GC Alloc。也就是说,使用它之后,尤其在Update方法中频繁调用时,会快速产生小块垃圾内存,造成垃圾回收操作的提前到来,造成游戏间歇性的卡顿。
  问题大家都知道,也都给出了建议,就是尽可能不要用。在start方法里倒无所谓,因为毕竟它只执行一次。Update方法一秒钟执行大概50-60次,这里就不要使用了。这个观点整体上是正确的,因为这样做毕竟避开了问题。
  不过有一点点不是很方便的就是,foreach确实带来了很多便捷性的编码。尤其是结合了var之后,那么我们究竟还能不能使用它,能使用的话,应该注意哪些问题?带着这些问题,我做了以下的测试。

12.4.2 重现GC Alloc问题

  首先,我写了一个简单的脚本来重现这个问题。
这个类中包括一个int数组,一个泛型参数为int的List。
  代码如下:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ForeachTest : MonoBehaviour {

    int[] m_intArray;
    List<int> m_intList;
    ArrayList m_arryList;
    public void Start () 
    {
        m_intArray = new int[2];
        m_intList = new List<int>();
        m_arryList = new ArrayList();
        for (int i = 0; i < m_intArray.Length; i++)
        {
            m_intArray[i] = i;
            m_intList.Add(i);
            m_arryList.Add(i);
        }
    }

    void Update () 
    {
         testIntListForeach();
    }

    void testIntListForeach()
    {
        for (int i = 0; i < 1000; i++)
        {
            foreach (var iNum in m_intList)
            {
            }
        }
    }
}

12.4.3 应用于IntList的foreach

  首先我们看应用于泛型List的情况,如下图:
这里写图片描述
  这里确实是在产生GC Alloc,每帧产生39.1KB的新内存。我使用的Unity版本是64位的5.4.3f1,可能不同的版本产生的内存大小有些差别,但是产生新内存是不可避免的。

12.4.4 应用于IntList的GetEnumerator

  接下来,我又做了另外一种尝试,就是用对等的方式写出同样的代码。将测试代码部分改成如下:

for (int i = 0; i < 1000; i++)
        {
            var iNum = m_intList.GetEnumerator();
            while (iNum.MoveNext())
            {
            }
        }

  原本以为,这个结果与上面的方式应该相同。不过结果出乎意料。
这里写图片描述
  它并没产生任何的新内存。于是,我准备使用IL反编译器来了解它的GCAlloc是如何产生的。
我们知道,List是动态数组,是可以随时增长、删减的,而int[]这种形式,在C#里面被编译成Array的子类去执行。为了有更多的对比,我将foreach和GetEmulator也写一份同样的代码,应用于Int数组和ArrayList,先查看运行的结果,然后一起查看他们的IL代码。

12.4.5 应用于IntArray的foreach

 for (int i = 0; i < 1000; i++)
        {
            foreach (var iNum in m_intArray)
            {
            }
        }

这里写图片描述
结果是没有产生GC Alloc。

12.4.6 应用于IntArray的GetEnumerator

  for (int i = 0; i < 1000; i++)
        {
            var iNum = m_intArray.GetEnumerator();
            while (iNum.MoveNext())
            {
            }
        }

这里写图片描述
结果是这里也在产生GC Alloc,每帧产生31.3KB的新内存。

12.4.7 应用于ArrayList的foreach

for (int i = 0; i < 1000; i++)
        {
            foreach (var iNum in m_intArray)
            {
            }
        }

这里写图片描述
结果是这里也在产生GC Alloc,每帧产生23.4KB的新内存(在32位版Unity5.3.4f1测试)。

12.4.8 应用于ArrayList的GetEnumerator

 for (int i = 0; i < 1000; i++)
        {
            var iNum = m_intArray.GetEnumerator();
            while (iNum.MoveNext())
            {
            }
        }

这里写图片描述

12.4.9 GC Alloc产生情况小结

小结 int[] (Array) List< int > ArrayList
foreach 不产生 产生 产生
GetEnumerator 产生 不产生 产生

延伸阅读:
【Unity优化】Unity中究竟能不能使用foreach?

12.5 函数引用

  函数的引用,无论是指向匿名函数还是显式函数,在unity中都是引用类型变量,这都会在堆内存上进行分配。匿名函数的调用完成后都会增加内存的使用和堆内存的分配。具体函数的引用和终止都取决于操作平台和编译器设置,但是如果想减少GC最好减少函数的引用。

12.6 LINQ和常量表达式

  由于LINQ和常量表达式以装箱的方式实现,所以在使用的时候最好进行性能测试。

13. 重构代码来减小GC的影响

  即使我们减小了代码在堆内存上的分配操作,代码也会增加GC的工作量。最常见的增加GC工作量的方式是让其检查它不必检查的对象。struct是值类型的变量,但是如果struct中包含有引用类型的变量,那么GC就必须检测整个struct。如果这样的操作很多,那么GC的工作量就大大增加。在下面的例子中struct包含一个string,那么整个struct都必须在GC中被检查:

public struct ItemData
{
    public string name;
    public int cost;
    public Vector3 position;
}
private ItemData[] itemData;

  我们可以将该struct拆分为多个数组的形式,从而减小GC的工作量:

private string[] itemNames;
private int[] itemCosts;
private Vector3[] itemPositions;

  另外一种在代码中增加GC工作量的方式是保存不必要的Object引用,在进行GC操作的时候会对堆内存上的object引用进行检查,越少的引用就意味着越少的检查工作量。在下面的例子中,当前的对话框中包含一个对下一个对话框引用,这就使得GC的时候会去检查下一个对象框:

public class DialogData
{
     private DialogData nextDialog;
     public DialogData GetNextDialog()
     {
           return nextDialog;

     }
}

  通过重构代码,我们可以返回下一个对话框实体的标记,而不是对话框实体本身,这样就没有多余的object引用,从而减少GC的工作量:

public class DialogData
{
    private int nextDialogID;
    public int GetNextDialogID()
    {
       return nextDialogID;
    }
}

  当然这个例子本身并不重要,但是如果我们的游戏中包含大量的含有对其他Object引用的object,我们可以考虑通过重构代码来减少GC的工作量。

14. 定时执行GC操作

主动调用GC操作

  如果我们知道堆内存在被分配后并没有被使用,我们希望可以主动地调用GC操作,或者在GC操作并不影响游戏体验的时候(例如场景切换的时候),我们可以主动的调用GC操作:

System.GC.Collect()

通过主动的调用,我们可以主动驱使GC操作来回收堆内存。

脚本优化

  • 常规循环 尽量少放在Update

  • 变量的隐性调用 go.transform不推荐 (这是一种遍历查找组件的方式) 推荐GetComponent(Transform)(Markdown不支持应该是尖角括号)

  • Gmaeobject.Find 推荐使用Tag查找 保存变量

  • 多线程
    协程处理,大量循环使用分携程执行,前边添加yield return设置执行顺序

IEnumerator Work()
    {
        //线程不安全
        //StartCoroutine(MyIoWork1());
        //StartCoroutine(MyIoWork2());
        yield return StartCoroutine(MyIoWork1());
        yield return StartCoroutine(MyIoWork2());
    }

    IEnumerator MyIoWork1()
    {
        for (int i = 0; i < 1000; i++)
        {
            System.IO.File.Delete("c:\a.zip");
            yield return null;
        }

    }

    IEnumerator MyIoWork2()
    {
        for (int i = 0; i < 1000; i++)
        {
            System.IO.File.Delete("c:\a.zip");
            yield return null;
        }

    }
  • 数学 合理降低计算的精度
    例子:
    计算距离:Mathf.Sqrt
    计算方向:Vector3.Angle
    Minimize use of complex mathematical operations such as pow, sin and cos in pixel shaders.
using UnityEngine;
using System.Collections;

public class TestMagnitude : MonoBehaviour {

    public Transform player1;
    public Transform player2;

    void Start()
    {
        Calc();
    }

    void Calc()
    {
        float distance1 = (player1.position - transform.position).magnitude;
        float distance2 = (player2.position - transform.position).magnitude;
        Debug.Log("Player1 is closer than Player2:" + (distance1 < distance2).ToString());

        float distance11 = (player1.position - transform.position).sqrMagnitude;
        float distance22 = (player2.position - transform.position).sqrMagnitude;
        Debug.Log("Player1 is closer than Player2:" + (distance1 < distance2).ToString());
    }
}
using UnityEngine;
using System.Collections;

public class Compare : MonoBehaviour {

    public Transform player1;
    public Transform player2;

    // Use this for initialization
    void Start () {

        float angle = Vector3.Angle(player2.forward, player1.forward);
        float dot = Vector3.Dot(player2.forward,player1.forward);

        Debug.Log("Dot=" + dot +" Angle=" + angle);
    }

    // Update is called once per frame
    void Update () {

    }
}
  • Object Pool 适用于频繁操作的对象
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class TestPool : MonoBehaviour
{
    public Transform root;
    public GameObject prefab;

    public float loopCount;
    public float prefabCount;
    List<GameObject> objects;

    // Use this for initialization
    void Start()
    {

        objects = new List<GameObject>();
        System.DateTime startTime = System.DateTime.Now;
        TestCaseWaitOutPool();
        System.DateTime endTime = System.DateTime.Now;
        string totalMs = (endTime - startTime).TotalMilliseconds.ToString();

        Debug.Log("Test case Without Pool take " + totalMs + "ms.");
    }

    // Update is called once per frame
    void Update()
    {

    }

    void TestCaseWaitOutPool()
    {
        for (int j = 0; j < loopCount; j++)
        {
            for (int i = 0; i < prefabCount; i++)
            {
                //create prefab
                GameObject go = Instantiate(prefab);
                //set parent
                go.transform.parent = root;
                //add to list
                objects.Add(go);
            }
            for (int i = 0; i < prefabCount; i++)
            {
                GameObject.Destroy(objects[i]);
            }

            //destory prefab
            objects.Clear();
        }

    }
}

使用pool之后

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class TestPool : MonoBehaviour
{
    public SimplePool pool;
    public Transform root;
    public GameObject prefab;

    public float loopCount;
    public float prefabCount;
    List<GameObject> objects;

    // Use this for initialization
    void Start()
    {

        objects = new List<GameObject>();
        // with out pool
        System.DateTime startTime = System.DateTime.Now;
        TestCaseWaitOutPool();
        System.DateTime endTime = System.DateTime.Now;
        string totalMs = (endTime - startTime).TotalMilliseconds.ToString();

        Debug.Log("Test case Without Pool take " + totalMs + "ms.");

        // with pool
        System.DateTime poolstartTime = System.DateTime.Now;
        TestCaseWithPool();
        System.DateTime poolendTime = System.DateTime.Now;
        string pooltotalMs = (poolendTime - poolstartTime).TotalMilliseconds.ToString();

        Debug.Log("Test case With Pool take " + pooltotalMs + "ms.");
    }

    void TestCaseWaitOutPool()
    {
        for (int j = 0; j < loopCount; j++)
        {
            for (int i = 0; i < prefabCount; i++)
            {
                //create prefab
                GameObject go = Instantiate(prefab);
                //set parent
                go.transform.parent = root;
                //add to list
                objects.Add(go);
            }
            for (int i = 0; i < prefabCount; i++)
            {
                GameObject.Destroy(objects[i]);
            }

            //destory prefab
            objects.Clear();
        }

    }

    void TestCaseWithPool()
    {
        for (int i = 0; i < loopCount; i++)
        {
            List<GameObject> objectsList = pool.GetObjects((int)prefabCount);
            pool.DestroyObjects(objectsList);
        }

    }
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class SimplePool : MonoBehaviour
{
    public Transform root;
    public GameObject prefab;
    public int size;

    List<GameObject> pooled;

    void Start()
    {
        pooled = new List<GameObject>();
        Prewarm();
    }
    void Prewarm()
    {
        PoolObjects(size);
    }

    List<GameObject> PoolObjects(int _amount)
    {
        List<GameObject> newPooled = new List<GameObject>();
        for (int i = 0; i < _amount; i++)
        {
            GameObject go = Instantiate(prefab);
            go.transform.parent = root;
            go.SetActive(false);
            newPooled.Add(go);
        }
        pooled.AddRange(newPooled);
        return newPooled;
    }

    public List<GameObject> GetObjects(int _amount)
    {
        List<GameObject> pooledObjects = pooled.FindAll(_go => !_go.activeSelf);
        if (pooledObjects.Count < _amount)
        {
            List<GameObject> newObjects = PoolObjects(_amount - pooledObjects.Count);
            pooledObjects.AddRange(newObjects);
            foreach (var go in pooledObjects)
            {
                go.SetActive(true);
            }
            return pooledObjects;
        }
        else
        {
            foreach (var go in pooledObjects)
            {
                go.SetActive(true);
            }
            return pooledObjects;
        }
    }

    public void DestroyObjects(List<GameObject> objects)
    {
        for (int i = 0; i < objects.Count; i++)
        {
            objects[i].SetActive(false);
        }
    }
}

Test case Without Pool take 226.1487ms.
UnityEngine.Debug:Log(Object)
TestPool:Start() (at Assets/TestPool.cs:26)
Test case With Pool take 58.0407ms.
UnityEngine.Debug:Log(Object)
TestPool:Start() (at Assets/TestPool.cs:34)

时间从 226.1487ms 缩短到58.0407ms。这就是效率。

  • 删除脚本中为空或不需要的默认方法,尽量少在Update中做事情,脚本不用时把它deactive。

  • 如何找到需要优化的代码

  • Total 与 Self
    在 Unity-Window-Profiler
    Overview 里面的 Total(总的占用包括调用其他人的部分),Self(仅自身占用)

using UnityEngiusing System.Collections;

public class TestTime : MonoBehaviour {

    public GameObject prefab;

    void Start()
    {
        //self
        System.Threading.Thread.Sleep(2000);

        Create(); // others
    }

    void Create()
    {
        for (int i = 0; i < 10000; i++)
        {
            GameObject go = GameObject.Instantiate(prefab);
            GameObject.Destroy(go);
        }
    }
}

文件优化

1.AssetBundle 创建 读取
设置Prefab。
AssetBundle New:env/go 路径自行设置

using UnityEngine;
using System.Collections;
using System;

public class TestAssetBundle : MonoBehaviour {

    public string path;
    public string file;

    // Use this for initialization
    void Start () {

        StartCoroutine( Load());
    }

    IEnumerator Load()
    {
        string _path = "file:///" + Application.dataPath + path;
        WWW www = WWW.LoadFromCacheOrDownload(_path,1);
        yield return www;

        AssetBundle bundle = www.assetBundle;
        AssetBundleRequest request = bundle.LoadAssetAsync(file);
        yield return request;

        GameObject prefab = request.asset as GameObject;
        Instantiate(prefab);

        //Clean
        bundle.Unload(false);
        www.Dispose();
    }

}

2.移动端打包优化
缩减包体积
设置 Andriod player setting Optimlzation .NET2.0(完整版) .NET2.0 Subset(简化版)
Stripping Leve Disabled .Strip Assembies. Strip Byte Code(一般用这个就可以了) .Use Micro mscorlib

1.momo version 
    full 
    subset 
2.stripping level 
    disabled 
    strip bute code 
3.媒体文件 
    图片 psd/png/jpg 
    音频 ogg/mp3/wav 
    fbx 公用animationclip

如何找到打包出的资源占比
先进行打包,打包完毕后查看控制台输出
这里写图片描述
打开文本日志,查看各资源的占比,确定优化的方向(此处是空场景测试)
这里写图片描述
3.跨平台开发效率优化
- 节省时间 utomate 自动化打包插件
- 显示面板改进 Odin - Inspector and Serializer
- DebugConsole Editor Console Pro

这里写图片描述

二、GPU优化

顶点计算优化

  • 尽可能在制作时控制顶点数目
  • 移除那些不需要的硬边缘和UV缝接
  • 使用Unity的LODGroup
  • 调整视椎体裁剪Frustum culling
  • 使用遮蔽剔除Occlusion culling

片元计算优化

  • 尽可能减少复杂的片元计算,如实时光照和实时阴影
  • 使用光照贴图实现全局光照,light probes实现简易的动态阴影
  • 使用更加高效的Shader(Unity上的Mobile版本)
  • 减少半透明物体,并控制它们的渲染顺序,尽可能减少重绘(OverDraw)

三、带宽(资源)优化

纹理贴图优化

  • 图集/材质/Mesh合并
    优化图集
  • 纹理压缩格式
    在ios设备上,请使用PVR格式。wp8和win8设备上,DXT格式。android设备,不透明贴图选择通用支持的ETC1格式;而透明贴图,4大GPU厂商各自有自己的压缩格式,可以把贴图分成一张RGB图和一张alpha通道图,都用ETC1格式,游戏里再合成;也可以简单选择RGBA4444格式。
  • 合并纹理(Atlas)
    虽然批处理是个很好的方式,但很容易就打破它的规定。例如,场景中的物体都使用Diffuse材质,但它们可能会使用不同的纹理。因此,尽可能把多张小纹理合并到一张大纹理(Atlas)中是一个好主意。
  • 利用网格的顶点数据
    但有时,除了纹理不同外,还有对于不同的物体,它们在材质上还有一些微小的参数变化,例如颜色不同、某些浮点参数不同。但铁定律是,不管是动态批处理还是静态批处理,它们的前提都是要使用同一个材质。是同一个,而不是同一种,也就是说它们指向的材质必须是同一个实体。这意味着,只要我们调整了参数,就会影响到所有使用这个材质的对象。那么想要微小的调整怎么办呢?由于Unity中的规定非常死,那么我们只好想些“歪门邪道”,其中一种就是使用网格的顶点数据(最常见的就是顶点颜色数据)。
    前面说过,经过批处理后的物体会被处理成一个VBO发送给GPU,VBO中的数据可以作为输入传递给Vertex Shader,因此我们可以巧妙地对VBO中的数据进行控制,从而达到不同效果的目的。一个例子是,还是之前的森林,所有的树使用了同一种材质,我们希望它们可以通过动态批处理来实现,但不同树的颜色可能不同。这时我么可以利用网格的顶点数据来调整。具体方法,可以参见后面会写的一篇文章。
    但这种方法的缺点就是会需要更多的内存来存储这些用于调整参数用的顶点数据。没办法,永远没有绝对完美的方法。

  • 减少纹理大小
    之前提到过,使用Texture Atlas可以帮助减少Draw Calls,而这些纹理的大小同样是一个需要考虑的问题。在这之前要提到一个问题就是,所有纹理的长宽比最好是正方形,而且长度值最好是2的整数幂。这是因为有很多优化策略只有在这种时候才可以发挥最大效用。
    Unity中查看纹理参数可以通过纹理的面板:
    这里写图片描述
    而调整参数可以通过纹理的Advance面板:
    这里写图片描述
    上面各种参数的说明可以参见文档。其中和优化相关的主要有“Generate Mip Maps”、“Max Size”和“Format”几个选项。
    “Generate Mip Maps”会为同一张纹理创建出很多不同大小的小纹理,构成一个纹理金字塔。而在游戏中可以根据距离物体的远近,来动态选择使用哪一个纹理。这是因为,在距离物体很远的时候,就算我们使用了非常精细的纹理,但肉眼也是分辨不出来的,这种时候完全可以使用更小、更模糊的纹理来代替,而这大量可以节省访问的像素的数目。但它的缺点是,由于需要为每一个纹理建立一个图像金字塔,因此它会需要占用更多的内存。例如上面的例子,在勾选“Generate Mip Maps”前,内存占用是0.5M,而勾选了“Generate Mip Maps”后,就变成了0.7M。除了内存的占用以外,一些时候我们也不希望使用Mipmaps,例如GUI纹理等。我们还可以在面板中查看生成的Mip Maps:
    这里写图片描述
    Unity中还提供了查看场景中物体的Mip Maps的使用情况。更确切的说是,展示了物体理想的纹理大小。其中红色表示这个物体可以使用更小的纹理,蓝色表示应该使用更大的纹理。
    这里写图片描述
    “Max Size”决定了纹理的长宽值,如果我们使用的纹理本身超过了这个最大值,Unity会对其进行缩小来满足这个条件。这里再重复一点,所有纹理的长宽比最好是正方形,而且长度值最好是2的整数幂。这是因为有很多优化策略只有在这种时候才可以发挥最大效用。
    “Format”负责纹理使用的压缩模式。通常选择这种自动模式就可以了,Unity会负责根据不同的平台来选择合适的压缩模式。而对于GUI类型的纹理,我们可以根据对画质的要求来选择是否进行压缩,具体可以参见之前关于画质的文章。
    我们还可以根据不同的机器来选择使用不同分辨率的纹理,以便让游戏在某些老机器上也可以运行。

帧优化

  • 限帧
    在移动设备上,Unity默认是60帧/秒,建议关掉垂直同步,把FPS限为30,进入后台时为1。限帧可以显著的减少发热和耗电。

资源优化

  • 资源预加载
    将需要的资源预先加载完成,做一个过渡的页面,效果会好很多
  • 资源卸载
    每30秒自动进行一次资源卸载(UnloadUnusedAssets),有时还会触发垃圾收集(GC.Collect)。改为进、出战场时卸载未被引用的资源,而战斗中不再定时卸载,解决了偶尔卡顿的问题。

四、Unity3d自带的优化技术

优化几何体

这一步主要是为了针对性能瓶颈中的”顶点处理“一项。这里的几何体就是指组成场景中对象的网格结构。
3D游戏制作都由模型制作开始。而在建模时,有一条我们需要记住:尽可能减少模型中三角形的数目,一些对于模型没有影响、或是肉眼非常难察觉到区别的顶点都要尽可能去掉。例如在下面左图中,正方体内部很多顶点都是不需要的,而把这个模型导入到Unity里就会是右面的情景:
这里写图片描述这里写图片描述
在Game视图下,我们可以查看场景中的三角形数目和顶点数目:
这里写图片描述
可以看到一个简单的正方形就产生了这么多顶点,这是我们不希望看到的。
同时,尽可能重用顶点。在很多三维建模软件中,都有相应的优化选项,可以自动优化网格结构。最后优化后,一个正方体可能只剩下8个顶点:

这里写图片描述这里写图片描述
它对应的顶点数和三角形数目如下:
这里写图片描述
等等!这里,你可能要问了,为什么顶点数是24,而不是8呢?美术朋友们经常会遇到这样的问题,就是建模软件里显示的模型顶点数和Unity中的不一样,通常Unity会多很多。谁才是对的呢?其实,它们是站在不同的角度上计算的,都有各自的道理,但我们真正应该关心的是Unity里的数目。
我们这里简单解释一下。三维软件里更多地是站在我们人类的角度理解顶点的,即我们看见的一个点就是一个。而Unity是站在GPU的角度上,去计算顶点数目的。而在GPU看来,看起来是一个的很有可能它要分开处理,从而就产生了额外的顶点。这种将顶点一分为多的原因,主要有两个:一个是UV splits,一个是Smoothing splits。而它们的本质其实都是因为对于GPU来说,顶点的每一个属性和顶点之间必须是一对一的关系。UV splits的产生,是因为建模时,一个顶点的UV坐标有多个。例如之前的立方体的例子,由于每个面都有共同的顶点,因此在不同面上,同一个顶点的UV坐标可能发生改变。这对于GPU来说,这是不可理解的,因此它必须把这个顶点拆分成两个具有不同UV坐标的定顶点,它才甘心。而Smoothing splits的产生也是类似的,不同的时,这次一个顶点可能会对应多个法线信息或切线信息。这通常是因为我们要决定一个边是一条Hard Edge还是Smooth Edge。Hard Edge通常是下面这样的效果(注意中间的折痕部分):
这里写图片描述这里写图片描述
而如果观察它的顶点法线,就会发现,折痕处每个顶点其实包含了两个不同的法线。因此,对于GPU来说,它同样无法理解这样的事情,因此会把顶点一分为二。而相反,Smooth Edge则是下面的情况:
这里写图片描述这里写图片描述
对于GPU来说,它本质上只关心有多少个顶点。因此,尽可能减少顶点的数目其实才是我们真正对需要关心的事情。因此,最后一条优化建议就是:移除不必要的Hard Edge以及纹理衔接,即避免Smoothing splits和UV splits。

LOD

LOD技术有点类似于Mipmap技术,不同的是,LOD是对模型建立了一个模型金字塔,根据摄像机距离对象的远近,选择使用不同精度的模型。它的好处是可以在适当的时候大量减少需要绘制的顶点数目。它的缺点同样是需要占用更多的内存,而且如果没有调整好距离的话,可能会造成模拟的突变。
在Unity中,可以通过LOD Group来实现LOD技术:
这里写图片描述这里写图片描述
通过上面的LOD Group面板,我们可以选择需要控制的模型以及距离设置。下面展示了油桶从一个完整网格到简化网格,最后完全被剔除的例子:
这里写图片描述这里写图片描述这里写图片描述这里写图片描述

使用Lightmaps

Lightmaps的很常见的一种优化策略。它主要用于场景中整体的光照效果。这种技术主要是提前把场景中的光照信息存储在一张光照纹理中,然后在运行时刻只需要根据纹理采样得到光照信息即可。
当然与之配合的还有Light Probes技术。风宇冲有一个系列文章讲过,但是时间比较久远,但教程我相信网上有很多。

使用God Rays

场景中很多小型光源效果都是靠这种方法模拟的。它们一般并不是真的光源产生的,很多情况是通过透明纹理进行模拟。

减少实时光照

实时光照对于移动平台是个非常昂贵的操作。如果只有一个平行光还好,但如果场景中包含了太多光源并且使用了很多多Passes的shader,那么很有可能会造成性能下降。而且在有些机器上,还要面临shader失效的风险。例如,一个场景里如果包含了三个逐像素的点光源,而且使用了逐像素的shader,那么很有可能将Draw Calls提高了三倍,同时也会增加overdraws。这是因为,对于逐像素的光源来说,被这些光源照亮的物体要被再渲染一次。更糟糕的是,无论是动态批处理还是动态批处理(其实文档中只提到了对动态批处理的影响,但不知道为什么实验结果对静态批处理也没有用),对于这种逐像素的pass都无法进行批处理,也就是说,它们会中断批处理。
例如,下面的场景中,四个物体都被标识成了“Static”,它们使用的shader都是自带的Bumped Diffuse。而所有的点光源都被标识成了“Important”,即是逐像素光。可以看到,运行后的Draw Calls是23,而非3。这是因为,只有“Forward Base”的Pass时发生了静态批处理(这里的动态批处理由于多Pass已经完全失效了),节省了一个Draw Calls,而后面的“Forward Add” Pass,每一次渲染都是一个单独的Draw Call(而且可以看到Tris和Verts数目也增加了):

这点正如文档中说的:The draw calls for “additional per-pixel lights” will not be batched。原因我不是很清楚,这里有一个讨论,但里面的意思说是对静态批处理没有影响,和我这里的结果不一样,知道原因的麻烦给我留言,非常感谢。我也在Unity论坛里提问里。
我们看到很多成功的移动游戏,它们的画面效果看起来好像包含了很多光源,但其实这都是骗人的。

遮挡剔除

遮挡剔除是用来消除躲在其他物件后面看不到的物件,这代表资源不会浪费在计算那些看不到的顶点上,进而提升性能。关于遮挡剔除,Unity Taiwan有一个系列文章大家可以看看(需翻墙):
Unity 4.3 关于Occlusion Culling : 基本篇
Unity 4.3 关于Occlusion Culling : 最佳做法
Unity 4.3 关于Occlusion Culling : 错误诊断
具体的内容大家可以自行查找。
现在我们来谈像素优化。

像素优化

像素优化的重点在于减少overdraw。之前提过,overdraw指的就是一个像素被绘制了多次。关键在于控制绘制顺序。
Unity还提供了查看overdraw的视图,在Scene视图的Render Mode->Overdraw。当然这里的视图只是提供了查看物体遮挡的层数关系,并不是真正的最终屏幕绘制的overdraw。也就是说,可以理解为它显示的是如果没有使用任何深度检验时的overdraw。这种视图是通过把所有对象都渲染成一个透明的轮廓,通过查看透明颜色的累计程度,来判断物体的遮挡。
这里写图片描述
上图图,红色越是浓重的地方表示overdraw越严重,而且这里涉及的都是透明物体,这意味着性能将会受到很大影响。

批处理

这方面的优化教程想必是最多的了。最常见的就是通过批处理(Batching)了。从名字上来理解,就是一块处理多个物体的意思。那么什么样的物体可以一起处理呢?答案就是使用同一个材质的物体。这是因此,对于使用同一个材质的物体,它们之间的不同仅仅在于顶点数据的差别,即使用的网格不同而已。我们可以把这些顶点数据合并在一起,再一起发送给GPU,就可以完成一次批处理。
Unity中有两种批处理方式:一种是动态批处理,一种是静态批处理。对于动态批处理来说,好消息是一切处理都是自动的,不需要我们自己做任何操作,而且物体是可以移动的,但坏消息是,限制很多,可能一不小心我们就会破坏了这种机制,导致Unity无法批处理一些使用了相同材质的物体。对于静态批处理来说,好消息是自由度很高,限制很少,坏消息是可能会占用更多的内存,而且经过静态批处理后的所有物体都不可以再移动了。
首先来说动态批处理。Unity进行动态批处理的条件是,物体使用同一个材质并且满足一些特定条件。Unity总是在不知不觉中就为我们做了动态批处理。例如下面的场景:
这里写图片描述
这个场景共包含了4个物体,其中两个箱子使用了同一个材质。可以看到,它的Draw Calls现在是3,并且显示Save by batching是1,也就是说,Unity靠Batching为我们节省了1个Draw Call。下面,我们来把其中一个箱子的大小随便改动一下,看看会发生什么:
这里写图片描述
可以发现,Draw Calls变成了4,Save by batching的数目也变成了0。这是为什么呢?它们明明还是只使用了一个材质啊。原因就是前面提到的那些需要满足的其他条件。动态批处理虽然自动得令人感动,但它对模型的要求很多:

  • 顶点属性的最大限制为900,而且未来有可能会变。不要依赖这个数据。
  • 一般来说,那么所有对象都必须需要使用同一个缩放尺度(可以是(1, 1, 1)、(1, 2, 3)、(1.5, 1.4, 1.3)等等,但必须都一样)。但如果是非统一缩放(即每个维度的缩放尺度不一样,例如(1, 2, 1)),那么如果所有的物体都使用不同的非统一缩放也是可以批处理的。这个要求很怪异,为什么批处理会和缩放有关呢?这和Unity背后的技术有关系,有兴趣的可以自行谷歌,比如这里。
  • 使用lightmap的物体不会批处理。多passes的shader会中断批处理。接受实时阴影的物体也不会批处理。
    上述除了最常见的由于缩放导致破坏批处理的情况,还有就是顶点属性的限制。例如,在上面的场景中我们添加之前未优化后的箱子模型:
    这里写图片描述
    可以看到Draw Calls一下子变成了5。这是因为新添加的箱子模型中,包含了474个顶点,而它使用的顶点属性有位置、UV坐标、法线等信息,使用的总和超过了900。
    动态批处理的条件这么多,一不小心它就不干了,因此Unity提供了另一个方法,静态批处理。接着上面的例子,我们保持修改后的缩放,但把四个物体的“Static Flag”勾选上:
    这里写图片描述
    点击Static后面的三角下拉框,我们会看到其实这一步设置了很多东西,这里我们想要的只是“Batching static”一项。这时我们再看Draw Calls,恩,还是没有变化。但是不要急,我们点击运行,变化出现了:
    这里写图片描述
    Draw Calls又回到了3,并且显示Save by batching是1。这就是得利于静态批处理。而且,如果我们在运行时刻查看模型的网格,会发现它们都变成了一个名为Combined Mesh (roo: scene)的东西。这个网格是Unity合并了所有标识为“Static”的物体的结果,在我们的例子里,就是四个物体:
    这里写图片描述
    你可以要问了,这四个对象明明不是都使用了一个材质,为什么可以合并成一个呢?如果你仔细观察上图的话,会发现里面标明了“4 submeshes”,也就是说,这个合并后的网格其实包含了4个子网格,也就是我们的四个对象。对于合并后后的网格,Unity会判断其中使用同一个材质的子网格,然后对它们进行批处理。
    但是,我们再细心点可以发现,我们的箱子使用的其实是同一个网格,但合并后却变成了两个。而且,我们观察运行前后Stats窗口中的“VBO total”,它的大小由241.6KB变成了286.2KB,变大了!还记得静态批处理的缺点吗?就是可能会占用更多的内存。文档中是这样写的:
    “Using static batching will require additional memory for storing the combined geometry. If several objects shared the same geometry before static batching, then a copy of geometry will be created for each object, either in the Editor or at runtime. This might not always be a good idea – sometimes you will have to sacrifice rendering performance by avoiding static batching for some objects to keep a smaller memory footprint. For example, marking trees as static in a dense forest level can have serious memory impact.”
    也就是说,如果在静态批处理前有一些物体共享了相同的网格(例如这里的两个箱子),那么每一个物体都会有一个该网格的复制品,即一个网格会变成多个网格被发送给GPU。在上面的例子看来,就是VBO的大小明显增大了。如果这类使用同一网格的对象很多,那么这就是一个问题了,这种时候我们可能需要避免使用静态批处理,这意味着牺牲一定的渲染性能。例如,如果在一个使用了1000个重复树模型的森林中使用静态批处理,那么结果就会产生1000倍的内存,这会造成严重的内存影响。这种时候,解决方法要么我们可以忍受这种牺牲内存换取性能的方法,要么不要使用静态批处理,而使用动态批处理(前提是大家使用相同的缩放大小,或者大家都使用不同的非统一缩放大小),或者自己编写批处理的方法。当然,我认为最好的还是使用动态批处理来解决。
    有一些小提示可以使用:
  • 尽可能选择静态批处理,但得时刻小心对内存的消耗。
  • 如果无法进行静态批处理,而要使用动态批处理的话,那么请小心上面提到的各种注意事项。例如:
  • 尽可能让这样的物体少并且尽可能让这些物体包含少量的顶点属性。
  • 不要使用统一缩放,或者都使用不同的非统一缩放。
  • 对于游戏中的小道具,例如可以捡拾的金币等,可以使用动态批处理。
  • 对于包含动画的这类物体,我们无法全部使用静态批处理,但其中如果有不动的部分,可以把这部分标识成“Static”。

五、官方建议

 1.PC平台的话保持场景中显示的顶点数少于200K~3M,移动设备的话少于10W,一切取决于你的目标GPU与CPU。
 2.如果你用U3D自带的SHADER,在表现不差的情况下选择Mobile或Unlit目录下的。它们更高效。
 3.尽可能共用材质。
 4.将不需要移动的物体设为Static,让引擎可以进行其批处理。
 5.尽可能不用灯光。
 6.动态灯光更加不要了。
 7.尝试用压缩贴图格式,或用16位代替32位。
 8.如果不需要别用雾效(fog)
 9.尝试用OcclusionCulling,在房间过道多遮挡物体多的场景非常有用。若不当反而会增加负担。
 10.用天空盒去“褪去”远处的物体。
 11.shader中用贴图混合的方式去代替多重通道计算。
 12.shader中注意float/half/fixed的使用。
 13.shader中不要用复杂的计算pow,sin,cos,tan,log等。
 14.shader中越少Fragment越好。
 15.注意是否有多余的动画脚本,模型自动导入到U3D会有动画脚本,大量的话会严重影响消耗CPU计算。
 16.注意碰撞体的碰撞层,不必要的碰撞检测请舍去。

  延伸阅读:
    · Optimizing Graphics Performance
    · [Unity3D]图形渲染优化、渲染管线优化、图形性能优化
    · 深入浅出聊优化:从Draw Calls到GC

2014-05-23 11:48:03 xujixiaoshen 阅读数 635
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

Unity3D开发总结一
分类: Unity3D开发 2013-02-27 15:20 1461人阅读 评论(0) 收藏 举报
1.太空射击游戏开发
a. 场景操作与Maya一样,F居中,场景操作与Maya一致
b.  unity3d的Project可以理解为C#的项目
c. FirstPerson contraller需要在自定义键盘中定制
 
2. C#开发
d.  Unpdata 每帧计算一次,  start -->脚本绑定的object初始化的时候执行一次
e. 每个gameobject上可以绑定多个script
f. script 的类名应与所绑定的object的对象名称一致,eg:
public class objectA: Monobehavior....
g. 也可以直接将脚本拖曳到object上
f. 类中的public属性在 inspector中就成为script组件的属性了
 
3.
a.  2D 游戏-->正交投影camera,
b. project相当于资源-->不一定会出现在Heriachy中,Hierarchy中的object就是游戏场景中的物体和节点
c. Time类.deltaTime -->只读(秒),就是本帧的持续时间
d.
Updata :一定显示,也解算(相当于realtime)
lateUpdata :
FixUpdata: 在刚体解算的时候使用该方法,而不是Updata-->相当于everyFrame,
e. 运行的时序是(从前到后): FixUpdata--->Updata-->lateUpdata
4.
f. Vector.up  Vector.down  Vector.left ..是归一化的矢量
g.   tranform.position--->是当前类所绑定的对象的position ,transform是当前绑定对象的根节点(下面可以有很多组件的子节点),
h.  Destroy(this.GameObject) --->回收当前实例中的GameObject属性对象
i.  prefab-->克隆对象(包括相关的所有component),整体打包
j.  script组件也是gameobject
h.  AudioSource可以Add到任何object上,生存期和被绑定的object相同,主背景音乐一般加到一个空gameobject上或者Maincamera上
i.  OnGui() --->创建界面的方法,每帧都重构,eg:
   if(Gui.Button(new Rect(0,60,100,50),“pause”)  Audio.pause();  //检测Button是否按下
j. Add组件的原则是-->和什么ojbect/实例相关就Add到什么ojbect/实例上
k.  void start()
{  //本实例创建时的相关设置
}
 
l.  x=Random.Rang(-8.0f,8.0f);
   y=6.5f;
    z=0.0f;
   transform.position=new vector(x,y,z);  //transform.position是世界坐标
m.
float amiToMove =currentSpeed*Time.deltaTime;
transform.Translate(vector.down*amiToMove);   //在当前位置的基础上偏移(相对位移)

n.   particle中不勾选simulate in worldspace则particle就可以随object(发射体)移动,反之不行

o.  score(得分),生命值等--->应该设置为全局静态变量

p. onGUI()
{ //界面元素每帧都重构}
q.  void onTrggerEnter(collider  otherobject)//在有穿插碰撞的时候检测,collider  otherobject是被碰撞物体
r.  script必须拖曳,且和某个object绑定后才能生效
s.  场景Build 后才能实现  loadLevel(场景序号) //scene切换
t. project就是资源工厂(进行各种资源的打包,重组)--->只为场景提供资源,与Hierachy没有直接关系
u.  Transform.Translate--->使用local坐标,如果使用world坐标应该如下:
    transform.translate(vector3.down*aimToMove,space.world)
v.  Random.Rang(0,1)..
w.  transform.Rotate 、transform.translate 都是相对偏移量
x.  while ()
{   yield  reture  0; // 可以在每一个遍历或者循环中都返回一次到调用处,类似于一个迭代子,好处是不用全部完成后再返回所有的数据,节约时间 }
y.  tranform.translate( ) //是个动作,相对位移
    local.position  //是object的当前世界坐标
z. prefab //好处是:instance化,节约计算量; 可以组合打包为新的对象进行使用
 
4.
a. 创建Prefab物体时候与Maya创建instance一样,最好将其Freeze,而且unity3d中许多物体初始创建时,最好Freeze,不要使用默认的创建位置
b. this.gameobject才是当前script类绑定的ojbect
 
5.碰撞检测的注意事项
c.  要进行碰撞,两个物体必须都有Collsion组件,要发生碰撞检测事件的话,其中之一还必须为rigidbody
d. 两个物体如果只想实现表面碰撞(不穿透)那么不要勾选OnTrigger,此时要使用 OnCollsionEnter()方法来检测碰撞事件;
e. 如果想两个物体既要发生碰撞,而且可以穿透(运动不被对方所阻挡),那么要勾选OnTrigger,此时要使用OnTriggerEnter()方法来检测碰撞事件;
 
6
f.得到tranform组件的方法:
(enemy)otherCollider.GetComponent("enemy") //"enemy"是Gameobject 的name,此方法限于本gameobject中所绑定的组件
7
g.  yield -->一般只和 startCoroutine() 配合使用,因为 yield语句返回IEnumerator
h.
  while()
{ yield return 0; //0 只是停止一帧;  waitForSecond(秒)指定停止多少秒  }
 
i.  只要涉及到线程的等待就要涉及多线程startCoroutine()方法-->这样不会阻塞主调用线程的进程
j. 但是js中不需要多线程程startCoroutine()方法实现等待功能,JS中直接让主线程调用一个方法(该方法中有yield,或者waitForSecond(秒)方法就可)
 
 
********************坦克克星****************
1.
a. 显示2D贴图的spriteMesh -->是个非常有用的东西,在制作2D Game中平面化的 object的必杀器;
spriteMesh可以和cube 结合使用,也可以直接创建 spriteMesh,然后再添加collsion组件
b. 将texture-->直接拖曳到object上-->就产生响应的材质
c.  Material中也能加入script 对响应的材质进行控件// d.
e.
var ani=GetComponent("anisprite")  //用于获得组件名称为script脚本组件实例 ,ani 即为该实例的应用,通过ani 可以对ani中属性进行设置
    ani.anisprite(3,10,0,0);//直接调用ani实例中的方法,可以理解为游戏运行时所有与gameobject绑定的组件都实例化了,包括script组件;
                                    //anisprite是个功能类,
f.  Unity3D,script组件本质:
       GameObject(同时有A,B两个script组件)
  scriptA        scriptB
当场景运行时,scriptA类和scriptB类都进行实例化,-->充当工具类来使用,所以就可以通过e中的方式来型号调用方法
 
g.  scriptMode-->来显示图片序列动画
h. transform.lcocalEulerAngles.z> 310(角度) //lcocalEulerAngles是object的自身坐标的角度值(标量)
i.  var  fwd=transform.transformDirection(vector3.left)  //将当前object的local 坐标转换为世界坐标系下的坐标
    physics.Raycast(transform.position,fwd,10)) //Raycast检测从某个方向到某个距离之内是否有物体被ray射中!
                                                          //transform.position 是从什么位置发射;
                                                         //fwd 朝什么方向(都为世界坐标)
j.   function start()
{    AudioSource.PlayclipAtPoint(explosion,vector(0,0,-5)) //在某点播放声音explosion,   }
 
2.
k. 废弃的ojbect要及时destroy掉, destroy(gameobject,0.5)//0.5秒后gameobject被销毁
l.   同时按住两个以上的键的检测方法:
     if(input.GetButton("Jump"))  //是否按下空格键
        {   if(Input.GetAxis("Horizontal")>0  //按下空格键的同时是否按下水平键
        }
 
m.
rigid  body; //定义一个rigid物体
body.velocity.x=3*speed;  // 该rigid物体的.velocity.x设置为3*speed
n.
如果如图所示要跑到沿着炮管发射,可以如下:
 myprojecttile=instantiate(projecttile,transform.position+fwd,transform.rotation);//  transform.rotation是让当前炮弹的倾角和炮管的倾角transform.rotation一致
 myprojecttitle.velocity=fwd*20;  //myprojecttitle炮弹是rigid所以可以使用此方式,使其获得一个初速度
 
2.
a.tag标签的功能类型与组的使用,unity中可以使用name或者tag来标示一个object
b. Gameobject.FindwithTag("tanko");  //在整个场景中寻找名为tanko的物体,返回值是一个数组(因为可能会找到多个物体)
c.  Application.Quit() //退出场景
 
 
 
 
****************************Unity3D中的动画系统*******************************
1.
a.  unity中可以导入fbx模型
b.  gameObject.animation.playQueued("Aim") //执行Aim动画片段
c.
gameObject.animation.playQueued("Aim");
gameObject.animation.playQueued("Idel") ;   //顺序执行 Aim动画片段和Idel片段
d.  play-->自定义键// Negative Button 是松开键; Postitive Button是按下键
e.  Animation编辑窗口 //shift+滚轮: 纵向放大视图   ctl+滚轮:横向放大视图
f.  在动画编辑栏上部双击-->可以添加某个触发事件的方法
g.  动画clip
  clip.setcurve("",typeof(Transform),localPosition.x,curve)  //setcurve方法可以对curve自定义动画曲线实现加载,并可以绑定到指定的属性上去;
h. 动画clip和music一样只需要在start中执行一次就可以一直往后播放,不用累计
 
 
*****************************投篮游戏*****************************i.
1.
a. 动画组件上sprite Animation-->分割连续的动画为多个clip
b. 让gameobject旋转的方法:
   transform.Rotate(new vector3(0,speed*time.deltatime*input.GetAxis("Horizontal"),0)) //transform.Rotate与transform.translate一样是让当前object进行位置或者角度偏移一个相对量,
c.  gameobject.transform.RotateAround(new vector3(沿着什么点),new vector3(0,1,0沿什么轴),speed*Time.deltaTime*input.GetAxis("Horizontal")偏移旋转多少角度);
d. object.position   object.rotation  //都是世界坐标的位置、角度量
e.  主动刚体才要设置rigig ,被动刚体不要设置rigid
f.只有具有collision组件的ojbect才能添加 physicMaterial
g.  OnGUI-->可以放在任何script中,这样只要显示与某个object相关的GUI
h.  一般在被碰撞物体中做碰撞检测,
i.  只要单次播放的声音,可以使用AudioSource.playAudioAtPoint //比如在爆炸事件中单次播放一次
 
 
 
**************************PlatForm**************************
 
1.
a. unity中世界坐标的Z轴正方向向屏幕里面 X
b. render.material.mainTextureScale=vector2(0.1,1) //  mainTextureScale是贴图UV平铺的坐标
    render.material.mainTextureOffset=vector2(0.1,0)// mainTextureOffset是贴图的offset量
c.  transform.renderer.material=runMaterial; // transform好像是当前脚本绑定物体的根节点,切换当前object的材质为runMaterial材质
   aniPlay.ansprite(10,10,true);//  通过GetComponent("aniPlay")得到的aniplay实例(工具类)
d. input .GetAxis("Horizontal") >0 //自定义的方向键向右 Negeative
   input .GetAxis("Horizontal") >0 // 自定义的方向键向左 Positive
 
2.
e.  character contraller -->主角色的物理特征控制器
f. character控制器属性 //contraller.IsGround 撞地检测;jumpEnable=false -->使用左右键控制移动
g.   character运行三部曲:首先判读是否接地-->根据input.GetAxis获得速度方向-->根据速度方向,确定人物动画方向-->将速度方向*均匀的速度(水平或者垂直)
--->最后将速度变化  v-=time.delta*g
h.  由于空中不能Jump-->所以Jump放在OnGround中获得--->且只有一次,而空中不获得velocity
i. 单次播放sound的经典模式:
  void playsound(soundName)
{  AudioSource.playclipAtPoint(soundName,vector3(0,0,-10));
    yield waitForSecond(soundName.length);  //将sound播放soundName.length的时长
}
j. time.delta-->其实对每一个帧无太大意义;
  updata()中每帧中所有的方法只调用计算一次,其实与时间无关,time.deltatime只是改帧持续的时间;
 
k. updata() 每帧执行与time.deltatime关系的原理图:
<----------------------------------每帧的时间长度(time.deltatime)-------------------------------------------->
每帧开始时:
计算所有  <----------->期间完成于线程和yield、waitForSecond相关的方法<------------->该帧结束
updata的方法            
 
例如:
  void updata()
{  transform.translate(vector3.left*speed); // 帧开始计算一次
    aniplay.anisprite(0,0,false); //因为anisprite方法与线程有关,所有一直要持续到帧结束
                                          //
}
 
 
 
 
*********************机器人之战*********************
 
2.
a. //ayercontraller.cs
  updata()
{
if(jumpEnable)
{  aniplay.anisprite(10,3,0,30,1,false) ;       }//调用aniplay.cs中的anisprite方法,因为anisprite是在updata中每帧更新的,所以相当于
                                                    //playcontraller调用多线程aniplay中的updata方法来执行动画
}
b.  Charater contraller碰撞物体的核心代码:
velocity.y -=gravity*Time.deltaTime;// 每帧速度变化
controller.Move(velocity*Time.deltaTime); //每帧位移的变化
c.   if(input.GetAxis("Horizontal")&&!input.Getkey("Z");  //按下水平键,且没有按下z键
d.  Transform.position// 当前object的位置
     laserPosRight.transform.position  //某个gameobject的位置(且出现在当前script的引用中)
e. Mathf.cos(15.0/180*3.14) //Mathf.cos要求用弧度
 
3.动画播放可以分为:
a.  动态装载播放,例如:
player左右上下跳,-->不同条件装载相应的Material,然后调用线程anisprite来播放动画
b. 静态绑定动画,例如:
子弹instance物体-->直接在gameobject上绑定anisprite组件并且指定好固定参数
 
3.
h. 碰撞是否为isTrigger都会发生碰撞
i.  var  objects= GameOject.FindGameobjectwithTag("rock")  //返回一个数组
j.  transform.translateDirection(direction) //将自身坐标转换为世界坐标
 
4.对于敌人script编写的原理
a. 设置敌人的几种状态,例如:行走、攻击、被杀、
b. 根据各种事件来设置状态
c. 设计达到各种状态所要播放的动画和行为
 
5.自身旋转的两种方式:
a.  transform.localEulerAngle3.Z+=amtToRotate ;
b. transform.localRotate(vector3...);
 
6.
a. 空EmptyObject-->只有且只有是Transform类型,才可以用于定位,例如可以作为发射口(炮弹)的定位器
b.  深度camera-->要使用layer,Masker
c.  如果只将scene的某个部分导入到另一个scene-->可以只将scene中的物体(例如:player) -->生成prefab,然后到处与player prefab相关的资源(不包括scene)为
Assert,-->然后在另一个场景中导入即可
 
7.
d. 如果IsTrigger被勾选,那么只能使用OnTriggerEnter() 来检测
e. 一个子弹对应一个其上面绑定的脚本实例(一个脚本随着instance的物体被实例化成多份)
f.  OnTriggerEnter(other: collider)
{  if( other.tag=="player“&&name=="trigger")  //“name=="trigger": 被碰撞的物体(本脚本绑定的主物体)  ;   other.tag=="player“: 主动碰撞物体
{...}
}
g. 类似静态方法/属性-->不用到绑定对象,就可以供全局使用
 
8.
a. myTop=GameObject.Find("/player/top");    //    /(Hirechy根)player/top(指定Gameobject下的组件)
    myTop.active=false;  // 不可见
 
 
 
*******************GUI设计*******************
1.
b.   每个UI(对话框都是一个实例)
c.  if(input.GetKey(Keycode.m)) //检测M键是否被按下
d.  GUI.DragWindow() //可以拖动窗口实例
e.  GUI.skin  与C#中的skin很相似
f.   窗口绘制的主方法
DoMyWindow(int  windowID)
{  intToolBar=GUI.ToolBar(tabButton,inToolBar,StoolBars,GUI.skin.Getstyle("TabButton"));  //tabButton 是button 的类型,Getstyle()是使用指定的皮肤
}
 
 
******************愤怒的小鸟******************
1.
a. GUITexture-->可以直接作为界面的logo使用
b. GUITexture可以在一个scene中添加多个,通过Z轴(深度)的前后进行叠加
c. GUITexture与其他GameObject一样
d. if(input.GetMoveButton(0))  //检测是否点击鼠标左键
 
2.
e.    drawTexture 的坐标是:  X轴正方向朝屏幕右边;Y轴正方向朝屏幕的下方 //与C#的UI坐标一致
      screen坐标是:  X轴正方向朝屏幕右边;Y轴正方向朝屏幕的上方方
f.  GUI.depth=-1; //可以设置GUI的深度值(前后)
g.  Sprite可以直接使用(如果不需要碰撞)  //可以不用与Cube结合使用
h.  invokeRepeating("Move",2, time)  //invokeRepeating方法 -->在2秒以后,每time秒后调用Move方法,类似于线程
i.  camera.main.screenPointToRay() //将屏幕上的一点转换为射线
j.   object 层次:
   transform.gameobject.collider.isTrigger=true;  //transform.是当前脚本绑定节点的根,.collider是gameobject下的collision组件
k. 事件控制场景机制下/物体的状态设置很重要,也很方便
 
3.
a. 画连续曲线时,不可能一次性设置太多vertex(eg: lineRender.setVertexCount(100)) ..一次性设置100个点太多,绘制时会出现问题);
而是要如下绘制:
lineRender.setVertexCount(index+1) //每次设置第index+1的点
lineRender.setPosition(index,position);  //设置第index点的位置
b.  object有collision后只能检测碰撞,不会产生碰撞的互动效果(例如:被碰飞),要有互动效果的话,必须加rigid组件
 
4.
c.  myDigit=instantiate(digit....) //获得object
   myScript=myDigit.transform.Getcomponent(digtDisplay)  //获得与myDigit绑定的digitDisplay脚本组件实例或引用
   if(playerPrefs.Haskey("HighScore"))  //本地永久化储存场景
myScript.mystringScore=palyerPrefs.GetInt("HighScore").toString();  //从本地永久化数据中得到HighScore 数据                                          
                                                                                                  //并且设置myscript脚本实例的mystringScore属性值
 
d.  if(playprefs.Getint("HighScore")<myscore)  playerPrefs.setInt("Highscore",myscore);  //Getint得到本地数据;setIn写入本地化数据
 
 
 
***************切西瓜***************
1.
a. input.mousePosition.x //相对于屏幕坐标
b.  camera(那个camera).main.screenToWorldPoint(将屏幕坐标转换为world坐标)(new vectors(input.mousePositon.x...  ))  ; //input.mousePositon.x是屏幕坐标
c.  instantiate(myRay,middleposition,Queterion.AngleAxis(angle*180/3.14,vector3.forward))  //AngleAxis-->绕着forward矢量旋转(angle*180/3.14)角度
 
2.
d.
if(isclicked)
{
   if(ismouseDown)
   {  var ray:Ray=camera.main.screenPointToRay(input.mousePoint);  //将屏幕上的鼠标坐标转换为一条射线
       var  hit:RayCastHit;
       if(collider.Raycast(Ray...))  //判断刀是否切到水果,刀在每帧判断一次
   }
}
e. 当判断左键点击下去的位置(firstposition),经过一段时间后,左键再放开的位置(secondPosition)-->左键按下到放开这个过程可能会贯穿多帧,
不一定是在同一帧内完成的;除非按下到放开这个过程的time<=time.delta.Time(每帧的时长)
f.  void  start()
{    invokeReapting("Move",0,3.2);  //该方法只需要在start()中调用,然后每3.2秒,自动调用一次Move方法,
}
 
3.
g.  在script中获取script所绑定对象的本身:  使用(this): transform.local.....即可
h. 在script中获取其他节点,可以使用如下方法:myApple=instantiate(....)  ;
                                                                 myApple.transform.rigidbody...   ;  //transform类似于根节点
 
 
5.脚本的拆分原则:
如果一个脚本中(按对对象object的操作功能可以进行进一步的划分),那么就没必要将所有操作功能都写在一个大class中!
可以按照功能的进一步划分,写在多个子Class中,然后再统一绑定到该object上!  //这样维护性更好
 

 

技术详情点击: http://url.cn/RTBgdY
2019-09-17 23:22:51 luoyu510183 阅读数 332
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

开发了半年基于Unity的安卓应用,项目也基本快完结了。在开发过程中遇到了很多坑,然而Unity的技术支持几乎都在英文论坛里。直接百度,真的很难解决问题。本篇对这半年的开发经历做一个总结,希望能帮助其他开发者。

https://forum.unity.com/ 这个是Unity的官方论坛,个人认为解决技术问题很好的途径,就是回复不是很稳定,看情况,然后网络访问也不是很稳定。下面就从我在上面的提问开始写:

1. Gradle build failed 

原帖地址:https://forum.unity.com/threads/gradle-build-failed.647581/#post-4910888

错误信息为:

stderr[
FAILURE: Build failed with an exception.
* What went wrong:
A problem occurred configuring root project 'gradleOut'.
> Failed to find Build Tools revision 29.0.0

这个错误信息其实就是Unity选择了一个不存在的安卓BuildToolsVersion版本。这个问题的出现是由于两个原因:

a. 出现问题之前我PC上一直都用的Android 28,在Player settings里面目标API Level也是Android 9(28)。但是突然某天Android Studio给我下载了"29.0.0-rc1"这个测试版本的SDK(Unity使用的是Android Studio下载的SDK)。从此,Unity在Gradle Build的时候,并没有按照我设置里面的API Level 去直接选择Android 28这个版本,而是选择了29.0.0-rc1这个最新版本的BuildToolsVersion。这个可能是由于BuildToolsVersion总是选择最新的。

b. Unity即便选择了这个29.0.0-rc1 BuildToolsVersion版本,但是它写入Gradle文件的时候,却只写入了29.0.0. 所以,最终Gradle Build的时候就找不到29.0.0这个BuildToolsVersion版本。

解决方法:

1. 用Unity自己下载SDK, 在Android Studio中导入项目时选择使用项目中的SDK路径,即Unity的SDK路径。

2. 在安卓的SDK目录下删除29.0.0-rc1这个版本。

3. 用Unity导出Android Studio项目,然后在Build.gradle里把 buildToolsVersion '29.0.0'改为你想用的那个。

 

2. 在StreamingAssets中加载.XML文件可能会解析错误

原帖地址:https://forum.unity.com/threads/how-to-load-a-xml-file-in-android.662320/

首先,StreamingAssets里面的资源属于压缩只能读取的文件。所以要使用UnityWebRequest的方式来读取,不能直接去用C#文件流打开。否则在编辑器里模拟没问题,但是无法在设备上使用。

解析.XML文件的错误是这样的:在编辑器里面模拟加载没问题,在设备上打开也没问题,读取的字符串看起来也正常,就是反序列化的时候出现错误。 

原因是由于.XML文件的BOM造成的。BOM是文件刚开始的一个标记数据,标记字节顺序的,占三个字节:“EF BB BF”。这三个字节后面的才是反序列化的内容。在安卓上读取到BOM要跳过,把BOM后面的字符串进行反序列化操作。

解决方法:

1. 生成.XML文件的时候不要带BOM;

2. 读取的时候先判断前三个字节是否为BOM,如果是就从BOM后面开始反序列化。

 

3. Unity2018.x升级到Unity2019.1.0.f2时的图片撕裂问题

原帖地址:https://forum.unity.com/threads/the-images-become-weird-wrong-scale-since-i-upgraded-my-project-to-2019-1-0f2.670078/

upload_2019-4-30_10-59-25.png

如上图中所示,左右两个Image使用的是同一个Sprite,但是显示效果完全不同。我也确认了右边的Image不存在拉伸或压缩等行为,大小和Sprite的分辨率是一样。感觉这个问题应该只是存在于升级过程中,因为升级之前完全没出现过。

解决方法:

把Image用RawImage来替换掉。

 

4. 触摸屏导致的点击事件异常

原帖地址:https://forum.unity.com/threads/i-have-a-tough-problem-with-screen-touch.710951/

现象:由于我们使用的是红外触摸框,不知道某种原因用户在点击的时候,Unity会同时收到两个点击事件。这个在一些资源释放操作时会产生严重的bug。这是一个很隐蔽的问题,而且由不同的代码带来的错误现象也是不同。

解决方法:

点击的时候,把函数改为不响应状态。延时一段时间(如1ms)后再恢复函数的响应。

 

5. 加载位于AssetBundle或者Addressables中的Video Clip的注意事项

原帖地址:https://forum.unity.com/threads/how-to-disable-caching-compressionenabled-when-caching-a-videoclip-included-addressable-bundle.740708/

首先,不论是AssetBundle还是Addressables,Video Clip所在的那个资源包都要选择未压缩。否则你大概会收到这个错误消息:

AndroidVideoMedia::OpenExtractor could not translate archive:/CAB-5b4cd0a92d354858f98b1392dd686137/CAB-5b4cd0a92d354858f98b1392dd686137.resource to local file. Make sure file exists, is on disk (not in memory) and not compressed.
AndroidVideoMedia: Error opening extractor: -10004

不用怀疑什么格式问题,就是压缩造成的问题。

另外,Addressables的机制是从Remote服务器下载AddressableAssets然后保存在本地的Cache里面,再从本地已下载的Cache文件中把VideoClip加载到程序里。所以在请求更新包含VideoClip的AddressableAssets的时候,要把Caching.compressionEnabled设置为false,即不压缩保存。

但是这会带来另一个问题:Caching.compressionEnables如果全局都设置为false,那么其他的素材占用的空间一下子也会增加几倍。目前Unity还不支持在下载Addressables的时候选择是否要压缩再Caching。

解决方法:

把VideoClip相关的Assets单独出来处理,不要和其它的放在一起。比如,统一先把Caching压缩关闭再去更新VideoClips。更新结束后再把Caching压缩打开更新别的Assets。Addressables这个系统确实省了很多事,但是目前感觉还不够成熟。

2015-02-15 13:34:44 book_longssl 阅读数 824
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。



  1、在使用数组或ArrayList对象时应当注意
  [csharp]view plaincopy
  length=myArray.Length;
  for(inti=0;ilength;i++)
  {
  }
  避免
  [csharp]view plaincopy
  for(inti=0;imyArray.Length;i++)
  {
  }
  2、如果没有必要每帧都处理,则可以每隔几帧处理一次
  [csharp]view plaincopy
  voidUpdate(){if(Time.frameCount%6==0){DoSomething();}}
  3、定时重复调用可以使用InvokeRepeating函数实现,比如,启动0.5秒后每隔1秒执行一次 DoSomeThing 函数:
  [csharp]view plaincopy
  voidStart()
  {
  InvokeRepeating("DoSomeThing",0.5f,1.0f);
  }
  4、少使用临时变量,特别是在Update OnGUI等实时调用的函数中。
  [csharp]view plaincopy
  voidUpdate()
  {
  Vector3pos;
  pos=transform.position;
  }
  可以改为:
  [csharp]view plaincopy
  privateVector3pos;
  voidUpdate()
  {
  pos=transform.position;
  }
  5、主动进行垃圾回收
  [csharp]view plaincopy
  voidUpdate()
  {
  if(Time.frameCount%50==0)
  {
  System.GC.Collection();
  }
  }
  6、优化数学运算,尽量避免使用float,而使用int,特别是在手机游戏中,尽量少用复杂的数学函数,比如sin,cos等函数。改除法/为乘法,例如:使用x*0.5f而不是 x/2.0f 。
  7、压缩Mesh
  导入3D模型之后,在不影响显示效果的前提下,最好打开MeshCompression。Off,Low,Medium,High这几个选项,可酌情选取。对于单个Mesh最好使用一个材质。
  8、运行时尽量减少Tris和DrawCalls
  预览的时候,可点开Stats,查看图形渲染的开销情况。特别注意Tris和DrawCalls这两个参数。一般来说,要做到:Tris保持在7.5k以下,DrawCalls保持在35以下。
  9、避免大量使用Unity自带的Sphere等内建Mesh
  Unity内建的Mesh,多边形的数量比较大,如果物体不要求特别圆滑,可导入其他的简单3D模型代替。
  10、如果可能,将GameObject上不必要的脚本disable掉。如果你有一个大的场景在你的游戏中,并且敌方的位置在数千米意外,这是你可以disable你的敌方AI脚本直到它们接近摄像机为止。一个好的途径来开启或关闭GameObject是使用SetActiveRecursively(false),并且球形或盒型碰撞器设为trigger。
  11、删除空的Update方法。当通过Assets目录创建新的脚本时,脚本里会包括一个Update方法,当你不使用时删除它。
  12、引用一个游戏对象的最合乎逻辑的组件。有人可能会这样写someGameObject.transform,gameObject.rigidbody.transform.gameObject.rigidbody.transform,但是这样做了一些不必要的工作,你可以在最开始的地方引用它,像这样:
  privateTransform myTrans;
  void Start()
  {
  myTrans=transform;
  }
  13、协同是一个好方法。可以使用协同程序来代替不必每帧都执行的方法。(还有InvokeRepeating方法也是一个好的取代Update的方法)。
  14、尽可能不要再Update或FixedUpdate中使用搜索方法(例如GameObject.Find()),你可以像前面那样在Start方法里获得它。
  15、不要使用SendMessage之类的方法,他比直接调用方法慢了100倍,你可以直接调用或通过C#的委托来实现。
  16、使用javascript或Boo语言时,你最好确定变量的类型,不要使用动态类型,这样会降低效率,你可以在脚本开头使用#pragmastrict 来检查,这样当你编译你的游戏时就不会出现莫名其妙的错误了。





  
没有更多推荐了,返回首页