2015-03-16 17:18:32 huang9012 阅读数 17312
对项目优化有很多,如:mesh合并 ,减少DrawCall和模型骨骼以及物理计算,合并材质球,优化代码等等,
前面发过一篇【整理】unity3d优化总结篇 
现在继续补上,该内容为本人经验以及网上收集整理,希望大家有更好的优化方法,能够继续跟帖,一起探讨,共同进步。

优化: 

1. 更新不透明贴图的压缩格式为ETC 4bit,因为android市场的手机中的GPU有多种,
每家的GPU支持不同的压缩格式,但他们都兼容ETC格式,

2. 对于透明贴图,我们只能选择RGBA 16bit 或者RGBA 32bit。

3. 减少FPS,在ProjectSetting-> Quality中的
VSync Count 参数会影响你的FPS,EveryVBlank相当于FPS=60,EverySecondVBlank = 30;
这两种情况都不符合游戏的FPS的话,我们需要手动调整FPS,首先关闭垂直同步这个功能,然后在代码的Awake方法里手动设置FPS(Application.targetFrameRate = 45;)
降低FPS的好处:
1)省电,减少手机发热的情况;
2)能都稳定游戏FPS,减少出现卡顿的情况。

4. 当我们设置了FPS后,再调整下Fixed timestep这个参数,
这个参数在ProjectSetting->Time中,目的是减少物理计算的次数,来提高游戏性能。

5. 尽量少使用Update LateUpdate FixedUpdate,这样也可以提升性能和节省电量。
多使用事件(不是SendMessage,使用自己写的,或者C#中的事件委托)。

6. 待机时,调整游戏的FPS为1,节省电量。

7. 图集大小最好不要高于1024,否则游戏安装之后、低端机直接崩溃、原因是手机系统版本低于2.2、超过1000的图集无法读取、导致。
2.2 以上没有遇见这个情况。
注意手机的RAM 与 ROM、小于 512M的手机、直接放弃机型适配。

VSCount 垂直同步
   unity3d中新建一个场景空的时候,帧速率(FPS总是很低),大概在60~70之间。
一直不太明白是怎么回事,现在基本上明白了。我在这里解释一下原因,如有错误,欢迎指正。
在Unity3D中当运行场景打开Profiler的时候,我们会看到VSync 这一项占了很大的比重。
这个是什么呢,这个就是垂直同步,稍后再做解释。
我们可以关闭VSync来提高帧速率,选择edit->project settings->Quality。


在右侧面板中可以找到VSync Count,把它选成Don't Sync。
 
这就关闭了VSync(垂直同步),现在在运行场景看看,帧速率是不是提高很多。

现在来说说什么是垂直同步,要知道什么是垂直同步,必须要先明白显示器的工作原理,
显示器上的所有图像都是一线一线的扫描上去的,无论是隔行扫描还是逐行扫描,
显示器都有两种同步参数——水平同步和垂直同步。

什么叫水平同步?什么叫垂直同步?
垂直和水平是CRT中两个基本的同步信号,水平同步信号决定了CRT画出一条横越屏幕线的时间,
垂直同步信号决定了CRT从屏幕顶部画到底部,再返回原始位置的时间,
而恰恰是垂直同步代表着CRT显示器的刷新率水平。

为什么关闭垂直同步信号会影响游戏中的FPS数值?
如果我们选择等待垂直同步信号(也就是我们平时所说的垂直同步打开),
那么在游戏中或许强劲的显卡迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,
显卡无法绘制下一屏,只有等85单位的信号到达,才可以绘制。
这样FPS自然要受到操作系统刷新率运行值的制约。

而如果我们选择不等待垂直同步信号(也就是我们平时所说的关闭垂直同步),那么游戏中作完一屏画面,
显卡和显示器无需等待垂直同步信号就可以开始下一屏图像的绘制,自然可以完全发挥显卡的实力。
但是不要忘记,正是因为垂直同步的存在,才能使得游戏进程和显示器刷新率同步,使得画面更加平滑和稳定。
取消了垂直同步信号,固然可以换来更快的速度,但是在图像的连续性上势必打折扣。
这也正是很多朋友抱怨关闭垂直后发现画面不连续的理论原因。

合并材质球unity 3d中每倒入一次模型就多一个材质球,可我的这些模型都是共用一张贴图的就想共用一个材质球,所以每次都要删除再附上,很麻烦。怎么才能合并这些材质球?
采用TexturePacking吧
1、遍历gameobject,取出material,并根据shader来将material分类
2、调用Unity自带的PackTextures函数来合并每个shader分类中的material所对应的textures(PackTextures函数有缺陷,不过可以将就用)
3、根据合并的大的texture来更新原有模型的texture、material已经uv坐标值。

需要注意的是:需要合并的纹理应该是物体在场景中距离相近的,如果物体在场景中的距离较远,
则不建议合并纹理,因为这样做很有可能非但起不到优化的作用,反而降低了运行效率。 

mesh合并 
分为2种方式合并
1.自带的合并必须勾选静态。
 
所有被勾选了“Static”的GameObject,其中的Mesh Filter中的mesh都会被合并到 "Combined Mesha (root: scene)" 中


2.也可以用脚本来合并mesh 。
[C#] 纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
using System.Collections;
 
public class MyClass : MonoBehaviour
{
    void Start ()
    {
        MeshFilter [] meshFilters = GetComponentsInChildren<MeshFilter> ();
        CombineInstance[] combine = new CombineInstance[meshFilters.Length];
 
        for (int i = 0; i < meshFilters.Length; i++) {
            combine [i].mesh = meshFilters [i].sharedMesh;
            combine [i].transform = meshFilters [i].transform.localToWorldMatrix;
            meshFilters [i].gameObject.active = false;
        }
 
            transform.GetComponent<MeshFilter> ().mesh = new Mesh ();
            transform.GetComponent<MeshFilter> ().mesh.CombineMeshes (combine);
            transform.gameObject.active = true;
    }
}

1. 先在 Unity 中建立 空物件 ( Empty ) 
2. 再创建2个 Cube 方块,并放入 空物件底下 (可以改成你自己的模型)
3. 把 MyClass 代码丟进 空物件上 。
4. (可选) 建立一个 Material 材质,并且丢进 空物件上
5. 执行

 


 
========================================分割线====================================
  • 角色Material数量
2-3
  • 骨骼数量
小于30个
  • 面片数量
300-1500
  • 一般角色应该没有IK结点
这是因为角色的动作大多数都是事先设定好的,并不需要经过IK操作来进行实时计算(Rogdoll除外),所以在模型导入时,不要将IK结点一起导入。

2、静态实体

  • 不要附加Animation Component
在静态实体上附加Animation部件虽然对结果没有影响,但却会增加一定的CPU开销来调用这一组件,所以尽量去掉该组件。
  • 网格顶点数
小于500
  • UV值范围尽量不要超过(0, 1)区间
尽量保证UV值不越界,这对于将来的纹理拼合优化很有帮助。

3、地形

  • 地形的分辨率大小
长宽均尽量小于257。这是因为地形太大,会造成大量顶点数据,给你的内存带宽造成一定的影响,在目前的ios设备中,内存带宽是非常有限的,需要尽量节省。同时,如果用Unity自带的地形,一定也要使用Occlusion Culling,因为Unity的刷地形工具虽然方便,但却是framekiller,刷过之后,你会发现drawcall增加的非常多。
  • 混合纹理数量
不要超过4。地形的混合操作是很耗时的,应该尽量避免。能合并的纹理尽量合并。

4、纹理

  • 纹理格式
建议png或tga。不用转成ios硬件支持的PVRTC格式,因为Unity在发布时会帮你自动转的。
  • 纹理尺寸
长宽小于1024。同时应该尽可能地小,够用就好,以保证纹理对内存带宽的影响达到最小。
  • 支持Mipmap
建议生成Mipmap。虽然这种做法会增加一些应用程序的大小,但在游戏运行时,系统会根据需求应用Mipmap来渲染,从而减少内存带宽。
  • 检查Alpha值
如果纹理的alpha通道均为1,则用RGB的24位纹理来代替RGBA的32位纹理。(据说Unity内部会进行自动检测)

5、光源

  • 光源“Important”个数
建议1个,一般为方向光。“Important”个数应该越小越少。个数越多,drawcall越多。
  • Pixel Light数目
1-2个。

6、粒子特效

  • 屏幕上的最大粒子数
建议小于200个粒子。
  • 每个粒子发射器发射的最大粒子数
建议不超过50个。
  • 粒子大小
如果可以的话,粒子的size应该尽可能地小。因为Unity的粒子系统的shader无论是alpha test还是alpha blending都是一笔不小的开销。同时,对于非常小的粒子,建议粒子纹理去掉alpha通道。
  • 尽量不要开启粒子的碰撞功能。
非常耗时。

7、音频

  • 游戏中播放时间较长的音乐(如背景音乐)
使用.ogg或.mp3的压缩格式。
  • 较短音乐(如枪声)
使用.wav和.aif的未压缩音频格式。

8、相机

  • 裁剪平面
将远平面设置成合适的距离。远平面过大会将一些不必要的物体加入渲染,降低效率。
  • 根据不同的物体设置不同的远裁剪平面
Unity提供了可以根据不同的layer来设置不同的view distance,所以我们可以实现将物体进行分层,大物体层设置的可视距离大些,而小物体层可以设置地小些,另外,一些开销比较大的实体(如粒子系统)可以设置得更小些等等。

9、碰撞

  • 尽量不用MeshCollider
如果可以的话,尽量不用MeshCollider,以节省不必要的开销。如果不能避免的话,尽量用减少Mesh的面片数,或用较少面片的代理体来代替。

10、其他

  • Drawcall
尽可能地减少Drawcall的数量。IOS设备上建议不超过100。减少的方法主要有如下几种:Frustum Culling,Occlusion Culling,Texture Packing。Frustum Culling是Unity内建的,我们需要做的就是寻求一个合适的远裁剪平面;Occlusion Culling,遮挡剔除,Unity内嵌了Umbra,一个非常好OC库。但Occlusion Culling也并不是放之四海而皆准的,有时候进行OC反而比不进行还要慢,建议在OC之前先确定自己的场景是否适合利用OC来优化;Texture Packing,或者叫Texture Atlasing,是将同种shader的纹理进行拼合,根据Unity的static batching的特性来减少draw call。建议使用,但也有弊端,那就是一定要将场景中距离相近的实体纹理进行拼合,否则,拼合后很可能会增加每帧渲染所需的纹理大小,加大内存带宽的负担。这也就是为什么会出现“DrawCall降了,渲染速度也变慢了”的原因。

  • 非运动物体尽量打上Static标签
Unity在运行时会对static物体进行自动优化处理,所以应该尽可能将非运行实体勾上static标签。

  • 场景中尽可能地使用prefab
尽可能地使用prefab的实例化物体,以降低内存带宽的负担。检查实体的PrefabType,尽量将其变成PrefabInstance,而不是ModelPrefabInstance。


========================================分割线==================================== 

移动平台相对于PC机,具有体积小,计算弱,带宽少的特点。

因此做手机游戏的开发,优化的方向,与力度对比PC游戏都有所区别。


必须要做到优化流程,合理利用资源。

目前在手机上面,还不能够像PC游戏那样追求高质量渲染效果,为了让手机不那么容易发烫,还要控制cpu,gpu,不能让他们全速运算。


材质方面:

纹理方面,建议使用压缩纹理,

Android上面使用ETC1,苹果上面使用PVRTC。


UV坐标控制在0到1之间,人物模型面数控制在1500内,骨骼控制在30个以内。

场景中使用一个主光(不能再多了)。


尽量减少alphaTest和alphaBlend材质的使用。在手机上,这是很杀效率的。


骨骼动画方面:

在动画方面可以考虑不使用插值,固定的帧率的动画。

如果要做插值,考虑使用四元数(表示旋转)和向量(表示位移)来做插值。

四元数做插值速度比矩阵来的快,Slerp提供了平滑插值。



========================================分割线==================================== 

优化的常规技巧
剖析你的游戏。
不要花费时间来优化那些晦涩的代码或者缩减图形文件的大小,除非这是你游戏的瓶颈。
第一次剖析你的游戏将会使你发现你游戏的瓶颈。Apple's Shark是一个很好的用来剖析基于OpenGL的程序的工具。
再次剖析你的游戏。
优化之后不要忘记再剖析一次你的游戏,这样可以检查你所做的优化是否达到了预期的效果。
当然,这样做也可能会使你发现更多的瓶颈。
流程第一、性能第二。花费时间来使你游戏的创建尽可能地流畅。
尽可能快地修正游戏中的错误将会使你后期更容易优化你的游戏。
在Scene View中测试场景。
这样做将会使你清楚了解这个场景中的物体或者附加在物体上的脚本是否降低了游戏性能。
如果Scene View反应迟钝,那么有可能是图形方面的原因,如果Scene View反应不迟钝,那么瓶颈可能出在脚本或者物理系统上。
禁用指定游戏物体。
在play模式下,尝试禁用并启用游戏物体来排查出游戏慢的原因。

网格
如果可能的话,把相邻的物体(网格)合并为一个只有一个材质的物体(网格)。比如,你的游戏中包含一个桌子,上面有一堆东西,你完全可以在3D程序中将它们合并在一起(这可能也需要你将这些物体的纹理合并为一个大的纹理集)。减少需要渲染的物体的数量可以极大地提高游戏性能。

不要有不必要的网格。
如果你的游戏场景中有一个人物,那么他应该是一个网格。如果你有一个船,那么它也应该只是一个网格。
每一个网格只用一种材质。
使用极少的面数的网格(比如500个多边形以下)。
最好把你人物的三角面数量控制在1500-2000个之间。
这个数量可以说是游戏质量和性能之间一个均衡值。如果你的模型有四边形,那么在导入模型的时候,引擎将会把每个四边形变为两个三角形。

光照
像素光。
像素光可以让你的游戏看起来效果很牛逼,但是不要使用过多的像素光。
在你的游戏中可以使用质量管理器来调节像素光的数量来取得一个性能和质量的均衡点.

性能占用顺序:聚光灯>点光源>平行光。
一个好的点亮场景的方法就是先得到你想要的效果,然后看看哪些光更重要;
在保持光效的前提下看看哪些光可以去掉。

点光源和聚光灯只影响它们范围内的网格。
如果一个网格处于点光源或者聚光灯的照射范围之外,并且光源的attenuate开关是打开的,那么这个网格将不会被光源所影响,这样就可以节省性能开销。
这样做理论上来讲可以使用很多小的点光源而且依然能有一个好的性能,因为这些光源只影响一小部分物体。
一个网格在有8个以上光源影响的时候,只响应前8个最亮的光源。

贴图
在外观不变的前提下,贴图大小越小越好。
如果你的显卡的显存不够大的话,你游戏中的贴图将会被转存到系统内存中,在显卡调用它们的时候再传到显卡中。
对于比较新的电脑来说,内存和显卡之间有足够的带宽来达到一个很好的性能;
如果你很无耻地用了巨多的大图片的话,在低显存的电脑上运行你的游戏的时候,你的游戏必然会挂掉。
倒是没有必要在图形编辑软件中调整贴图的大小。你可以在unity导入贴图的时候进行调整。

不要使用低质量的图片。
在小播放界面的游戏中使用低质量的jpeg图片或者低色彩的png图片亦或是gif图片没什么问题
在发布游戏的时候,引擎会自动压缩这些图片,多重压缩和解压将会降低图片的质量,所以最好保持贴图文件的分辨率为原始分辨率。
这样就会减少多重压缩和解压所导致的图片失真现象。

Shaders
多重效果的shader就比看起来样式很单一的shader要更耗费资源。
同样在一个拥有贴图和光反射的物体上,使用VertexLit Diffuse shader无疑是最省资源的。


========================================分割线==================================== 

在美术制作场景的过程中,会使用到大量的粒子系统。
比如场景中的火把。在我们的一个地下城场景中,美术们放置了大量的火把。整个场景中的各个地方,有100来个火把。

unity中,在摄像机范围外的粒子系统虽然不会被绘制。
但是update是一直持续的。这也就意味着,这100多个火把,不论是否可见都在更新。

这个设计应该是很不合理的,在我看过的其他引擎中,都会有一个开关,来控制不可见的粒子系统是否需要update。
有的粒子系统在不可见的时候需要更新,比如爆炸。有的不需要更新,比如火堆火把。

为了避免不必要的update开销,尤其是最后游戏是要发布到页游平台(web player只能使用一个cpu的核)。
于是写了一个脚本,控制不可见的粒子系统就不更新。

该脚本主要是用到了2个MonoBehaviour的函数。
OnBecameInvisible() 当变为不可见   和   OnBecameVisible() 当变成可见。 

要这2个函数起作用的前提是,该GameObject绑定了MeshRender组件。
所以,我们要在粒子系统的GameObject放置在一个GameObject  下,且给该GameObject绑定一个MeshRender 与 MeshFilter。
MeshFilter中的mesh可以随便找个cube。

在Start() 的时候,把最GameObject的scale设置为很小,以保证该cube不被看见。
其实遍历所有的child,把active设置为false。

在OnBecameVisible 中 遍历所有child,把active设置为true。
在OnBecameInvisible中 遍历所有child,把active设置为false。


========================================分割线==================================== 

Unity 性能优化 Draw Call 

本帖隐藏的内容

Unity(或者说基本所有图形引擎)生成一帧画面的处理过程大致可以这样简化描述:引擎首先经过简单的可见性测试,确定摄像机可以看到的物体,然后把这些物体的顶点(包括本地位置、法线、UV等),索引(顶点如何组成三角形),变换(就是物体的位置、旋转、缩放、以及摄像机位置等),相关光源,纹理,渲染方式(由材质/Shader决定)等数据准备好,然后通知图形API——或者就简单地看作是通知GPU——开始绘制,GPU基于这些数据,经过一系列运算,在屏幕上画出成千上万的三角形,最终构成一幅图像。

在Unity中,每次引擎准备数据并通知GPU的过程称为一次Draw Call。这一过程是逐个物体进行的,对于每个物体,不只GPU的渲染,引擎重新设置材质/Shader也是一项非常耗时的操作。因此每帧的Draw Call次数是一项非常重要的性能指标,对于iOS来说应尽量控制在20次以内,这个值可以在编辑器的Statistic窗口看到。

Unity内置了Draw Call Batching技术,从名字就可以看出,它的主要目标就是在一次Draw Call中批量处理多个物体。只要物体的变换和材质相同,GPU就可以按完全相同的方式进行处理,即可以把它们放在一个Draw Call中。Draw Call Batching技术的核心就是在可见性测试之后,检查所有要绘制的物体的材质,把相同材质的分为一组(一个Batch),然后把它们组合成一个物体(统一变换),这样就可以在一个Draw Call中处理多个物体了(实际上是组合后的一个物体)。

但Draw Call Batching存在一个缺陷,就是它需要把一个Batch中的所有物体组合到一起,相当于创建了一个与这些物体加起来一样大的物体,与此同时就需要分配相应大小的内存。这不仅会消耗更多内存,还需要消耗CPU时间。特别是对于移动的物体,每一帧都得重新进行组合,这就需要进行一些权衡,否则得不偿失。但对于静止不动的物体来说,只需要进行一次组合,之后就可以一直使用,效率要高得多。

Unity提供了Dynamic Batching和Static Batching两种方式。Dynamic Batching是完全自动进行的,不需要也无法进行任何干预,对于顶点数在300以内的可移动物体,只要使用相同的材质,就会组成Batch。Static Batching则需要把静止的物体标记为Static,然后无论大小,都会组成Batch。如前文所说,Static Batching显然比Dynamic Batching要高效得多,于是,Static Batching功能是收费的……

要有效利用Draw Call Batching,首先是尽量减少场景中使用的材质数量,即尽量共享材质,对于仅纹理不同的材质可以把纹理组合到一张更大的纹理中(称为Texture Atlasing)。然后是把不会移动的物体标记为Static。此外还可以通过CombineChildren脚本(Standard Assets/Scripts/Unity Scripts/CombineChildren)手动把物体组合在一起,但这个脚本会影响可见性测试,因为组合在一起的物体始终会被看作一个物体,从而会增加GPU要处理的几何体数量,因此要小心使用。

对于复杂的静态场景,还可以考虑自行设计遮挡剔除算法,减少可见的物体数量同时也可以减少Draw Call。

总之,理解Draw Call和Draw Call Batching原理,根据场景特点设计相应的方案来尽量减少Draw Call次数才是王道,其它方面亦然。



Draw Call Batching (绘制调用批处理)

To draw an object on the screen, the engine has to issue a draw call to the graphics API (OpenGL ES in the case of iOS). Every single draw call requires a significant amount of work on the part of the graphics API, causing significant performance overhead on the CPU side.
在屏幕上渲染物体,引擎需要发出一个绘制调用来访问图形API(iOS系统中为OpenGL ES)。
每个绘制调用需要进行大量的工作来访问图形API,从而导致了CPU方面显著的性能开销。

Unity combines a number of objects at runtime and draws them together with a single draw call. This operation is called "batching". The more objects Unity can batch together, the better rendering performance you will get.
Unity在运行时可以将一些物体进行合并,从而用一个绘制调用来渲染他们。这一操作,我们称之为“批处理”。
一般来说,Unity批处理的物体越多,你就会得到越好的渲染性能。

Built-in batching support in Unity has significant benefit over simply combining geometry in the modeling tool (or using theCombineChildren script from the Standard Assets package). Batching in Unity happensafter visibility determination step. The engine does culling on each object individually, and the amount of rendered geometry is going to be the same as without batching. Combining geometry in the modeling tool, on the other hand, prevents effecient culling and results in much higher amount of geometry being rendered.
Unity中内建的批处理机制所达到的效果要明显强于使用几何建模工具(或使用Standard Assets包中的CombineChildren脚本)的批处理效果。
这是因为,Unity引擎的批处理操作是在物体的可视裁剪操作之后进行的。
Unity先对每个物体进行裁剪,然后再进行批处理,这样可以使渲染的几何总量在批处理前后保持不变。
但是,使用几何建模工具来拼合物体,会妨碍引擎对其进行有效的裁剪操作,从而导致引擎需要渲染更多的几何面片。

Materials
材质
Only objects sharing the same material can be batched together. Therefore, if you want to achieve good batching, you need to share as many materials among different objects as possible.
只有拥有相同材质的物体才可以进行批处理。
因此,如果你想要得到良好的批处理效果,你需要在程序中尽可能地复用材质和物体。

If you have two identical materials which differ only in textures, you can combine those textures into a single big texture - a process often calledtexture atlasing. Once textures are in the same atlas, you can use single material instead.
如果你的两个材质仅仅是纹理不同,那么你可以通过 纹理拼合 操作来将这两张纹理拼合成一张大的纹理。
一旦纹理拼合在一起,你就可以使用这个单一材质来替代之前的两个材质了。

If you need to access shared material properties from the scripts, then it is important to note that modifyingRenderer.material will create a copy of the material. Instead, you should useRenderer.sharedMaterial to keep material shared.
如果你需要通过脚本来访问复用材质属性,那么值得注意的是改变Renderer.material将会造成一份材质的拷贝。
因此,你应该使用Renderer.sharedMaterial来保证材质的共享状态。

Dynamic Batching
动态批处理
Unity can automatically batch moving objects into the same draw call if they share the same material.
如果动态物体共用着相同的材质,那么Unity会自动对这些物体进行批处理。

Dynamic batching is done automatically and does not require any additional effort on your side.
动态批处理操作是自动完成的,并不需要你进行额外的操作。

Tips:
提醒:
1、      Batching dynamic objects has certain overheadper vertex, so batching is applied only to meshes containing less than900 vertex attributes in total.
             批处理动态物体需要在每个顶点上进行一定的开销,所以动态批处理仅支持小于900顶点的网格物体。

2、      If your shader is using Vertex Position, Normal and single UV, then you can batch up to 300 verts and if your shader is using Vertex Position, Normal, UV0, UV1 and
            Tangent, then only 180 verts.
            Please note: attribute count limit might be changed in future
            如果你的着色器使用顶点位置,法线和UV值三种属性,那么你只能批处理300顶点以下的物体;
如果你的着色器需要使用顶点位置,法线,UV0,UV1和切向量,那你只
            能批处理180顶点以下的物体。
            请注意:属性数量的限制可能会在将来进行改变。

4、      Don't use scale. Objects with scale (1,1,1) and (2,2,2) won't batch.
            不要使用缩放尺度(scale)。分别拥有缩放尺度(1,1,1)和(2,2,2)的两个物体将不会进行批处理。

5、      Uniformly scaled objects won't be batched with non-uniformly scaled ones.
            统一缩放尺度的物体不会与非统一缩放尺度的物体进行批处理。
           Objects with scale (1,1,1) and (1,2,1) won't be batched. On the other hand (1,2,1) and (1,3,1) will be.
           使用缩放尺度(1,1,1)和 (1,2,1)的两个物体将不会进行批处理,但是使用缩放尺度(1,2,1)和(1,3,1)的两个物体将可以进行批处理。

6、     Using different material instances will cause batching to fail.
           使用不同材质的实例化物体(instance)将会导致批处理失败。

7、     Objects with lightmaps have additional (hidden) material parameter: offset/scale in lightmap, so lightmapped objects won't be batched (unless they point to same
           portions of lightmap)
           拥有lightmap的物体含有额外(隐藏)的材质属性,比如:lightmap的偏移和缩放系数等。所以,拥有lightmap的物体将不会进行批处理(除非他们指向lightmap的同一
           部分)。

8、     Multi-pass shaders will break batching. E.g. Almost all unity shaders supports several lights in forward rendering, effectively doing additional pass for them
           多通道的shader会妨碍批处理操作。比如,几乎unity中所有的着色器在前向渲染中都支持多个光源,并为它们有效地开辟多个通道。

9、     Using instances of a prefab automatically are using the same mesh and material.
           预设体的实例会自动地使用相同的网格模型和材质。

Static Batching
静态批处理

Static batching, on the other hand, allows the engine to reduce draw calls for geometry of any size (provided it does not move and shares the same material). Static batching is significantly more efficient than dynamic batching. You should choose static batching as it will require less CPU power.
相对而言,静态批处理操作允许引擎对任意大小的几何物体进行批处理操作来降低绘制调用(只要这些物体不移动,并且拥有相同的材质)。因此,静态批处理比动态批处理更加有效,你应该尽量低使用它,因为它需要更少的CPU开销。

In order to take advantage of static batching, you need explicitly specify that certain objects are static and willnot move, rotate or scale in the game. To do so, you can mark objects as static using the Static checkbox in the Inspector:
为了更好地使用静态批处理,你需要明确指出哪些物体是静止的,并且在游戏中永远不会移动、旋转和缩放。想完成这一步,你只需要在检测器(Inspector)中将Static复选框打勾即可,如下图所示:


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.
使用静态批处理操作需要额外的内存开销来储存合并后的几何数据。在静态批处理之前,如果一些物体共用了同样的几何数据,那么引擎会在编辑以及运行状态对每个物体创建一个几何数据的备份。这并不总是一个好的想法,因为有时候,你将不得不牺牲一点渲染性能来防止一些物体的静态批处理,从而保持较少的内存开销。比如,将浓密森里中树设为Static,会导致严重的内存开销。

Static batching is only available in Unity iOS Advanced.
静态批处理目前只支持Unity iOS Advanced。



备注:最近一直在研究Unity3D的性能优化问题,这段时间可能会多翻译这方面的文章。

前两天,MadFinger,就是当今iOS与Android上画质最牛逼闪闪的游戏之一——ShadowGun的开发商,令人惊异地放出了一个ShadowGun的样例关卡以及若干可免费使用的Shader,国外同行们的分享精神真的是令人赞叹不已。原文在这里,以下是我的一些摘录和笔记。

首先是一些优化常识。针对图形方面的优化主要包括三角形数量,纹理所占内存,以及Shader,前两项基本没什么好讲的,针对设备机能的限制制定相应的指标即可,所以Shader就成为了图形性能优化的关键。

Alpha blending

在Unity官方文档中讲,由于硬件原因,在iOS设备上使用alpha-test会造成很大的性能开销,应尽量使用alpha-blend代替。这里提到,在同屏使用alpha-blend的面数,尤其是这些面所占屏幕面积的大小,对性能也会造成很大影响。原因是使用alpha-blend的面会造成overdraw的增加,这尤其对低性能设备的影响很大。不过没有购买Pro版,没有Occlusion Culling功能的话,就不必顾虑这一问题了,反正overdraw是必然的。

复杂的Per-pixel shader

Per-pixel shader即Fragment shader,顾名思义是要对每个渲染到屏幕上的像素做处理的shader,如果per-pixel shader比较复杂且需要处理的像素很多时,也就是使用该shader的面占屏幕面积很大时,对性能的影响甚至要超过alpha blending。因此复杂的per-pixel shader只适用于小物体。

下面是对几个Shader的逐一讲解:

Environment specular maps(Shader Virtual Gloss Per Vertex Additive)
Specular map通常都是利用贴图的alpha通道来定义物体表面的光滑程度(反光度),这个shader的特点是per-vertex计算反光度的,有着相当不错的效果的同时比per-pixel的shader性能要高得多。这个shader很适用于关卡环境等占很大区域的模型。

经过优化的动态角色光照和阴影(Light probes和BRDF Shader)
传统的Lightmaps无法支持动态物体,对此Unity提供了Light probes技术,预先把动态物体的光照信息保存在代理对象(即Light probes)中,运行时动态物体从距离最近的Probe中获取光照信息。

Unity本身还提供了一个效果非常棒的专为移动设备优化过的角色Shader,支持Diffuse、Specular和Normal maps,并通过一个特殊的脚本生成贴图用于模仿BRDF光照效果。最终产生的效果堪比次时代大作中的角色光影效果。

雾和体积光(Shader Blinking Godrays)
目前在移动设备上要开启真正的雾效基本不可行,ShadowGun的方案是通过简单的网格+透明贴图(称为雾面)来模拟雾效。在玩家靠近时,雾面逐渐变淡,同时fog plane的顶点也会移开(即使完全透明的alpha面也会消耗很多渲染时间)。

使用这个Shader的网格需要经过处理:

顶点的alpha值用于决定顶点是否可以移动(在例子中0为不可动,1为可动)。
顶点法线决定移动的方向
然后Shader通过计算与观察者的距离来控制雾面的淡入/淡出。
这个Shader还可以用来做体积光和其它一些alpha效果。

飞机坠毁的浓烟效果(Shader Scroll 2 Layers Sine Alpha-blended)
通过粒子产生浓烟的代价太高,所以ShadowGun中使用了网格+贴图动画来制作这个效果。通过混合两层贴图并让它们交错移动来产生动画效果。其中顶点alpha值用于让网格的边缘看起来比较柔和,同时使用顶点颜色来模拟从火焰到烟雾的过渡效果。

带动态效果的天空盒(Shader Scroll 2 Layers Multiplicative)
通过两张贴图的混合和移动产生云的动态效果。

旗帜和衣服的飘动效果(Shader Lightmap + Wind)
同样利用顶点alpha值决定哪些顶点可以移动,然后shader的参数用于调整摆动的方向和速度。
=======================分割线========================

一、程序方面
  01、务必删除脚本中为空或不需要的默认方法;
  02、只在一个脚本中使用OnGUI方法;
  03、避免在OnGUI中对变量、方法进行更新、赋值,输出变量建议在Update内;
  04、同一脚本中频繁使用的变量建议声明其为全局变量,脚本之间频繁调用的变量或方法建议声明为全局静态变量或方法;
  05、不要去频繁获取组件,将其声明为全局变量;
  06、数组、集合类元素优先使用Array,其次是List;
  07、脚本在不使用时脚本禁用之,需要时再启用;
  08、可以使用Ray来代替OnMouseXXX类方法;
  09、需要隐藏/显示或实例化来回切换的对象,尽量不要使用SetActiveRecursively或active,而使用将对象远远移出相机范围和移回原位的做法;
  10、尽量少用模运算和除法运算,比如a/5f,一定要写成a*0.2f。
  11、对于不经常调用或更改的变量或方法建议使用Coroutines & Yield;
  12、尽量直接声明脚本变量,而不使用GetComponent来获取脚本;
iPhone
  13、尽量使用整数数字,因为iPhone的浮点数计算能力很差;
  14、不要使用原生的GUI方法;
  15、不要实例化(Instantiate)对象,事先建好对象池,并使用Translate“生成”对象;
 

二、模型方面
  01、合并使用同贴图的材质球,合并使用相同材质球的Mesh;
  02、角色的贴图和材质球只要一个,若必须多个则将模型离分离为多个部分;
  02、骨骼系统不要使用太多;
  03、当使用多角色时,将动画单独分离出来;
  04、使用层距离来控制模型的显示距离;
  05、阴影其实包含两方面阴暗和影子,建议使用实时影子时把阴暗效果烘焙出来,不要使用灯光来调节光线阴暗。
  06、少用像素灯和使用像素灯的Shader;
  08、如果硬阴影可以解决问题就不要用软阴影,并且使用不影响效果的低分辨率阴影;
  08、实时阴影很耗性能,尽量减小产生阴影的距离;
  09、允许的话在大场景中使用线性雾,这样可以使远距离对象或阴影不易察觉,因此可以通过减小相机和阴影距离来提高性能;
  10、使用圆滑组来尽量减少模型的面数;
  11、项目中如果没有灯光或对象在移动那么就不要使用实时灯光;
  12、水面、镜子等实时反射/折射的效果单独放在Water图层中,并且根据其实时反射/折射的范围来调整;
  13、碰撞对效率的影响很小,但碰撞还是建议使用Box、Sphere碰撞体;
  14、建材质球时尽量考虑使用Substance;
  15、尽量将所有的实时反射/折射(如水面、镜子、地板等等)都集合成一个面;
  16、假反射/折射没有必要使用过大分辨率,一般64*64就可以,不建议超过256*256;
  17、需要更改的材质球,建议实例化一个,而不是使用公共的材质球;
  18、将不须射线或碰撞事件的对象置于IgnoreRaycast图层;
  19、将水面或类似效果置于Water图层
  20、将透明通道的对象置于TransparentFX图层;
  21、养成良好的标签(Tags)、层次(Hieratchy)和图层(Layer)的条理化习惯,将不同的对象置于不同的标签或图层,三者有效的结合将很方便的按名称、类别和属性来查找;
  22、通过Stats和Profile查看对效率影响最大的方面或对象,或者使用禁用部分模型的方式查看问题到底在哪儿;
  23、使用遮挡剔除(Occlusion Culling)处理大场景,一种较原生的类LOD技术,并且能够“分割”作为整体的一个模型。

三、其它
  场景中如果没有使用灯光和像素灯,就不要使用法线贴图,因为法线效果只有在有光源(Direct Light/Point Light/Angle Light/Pixel Light)的情况下才有效果。




2017-03-28 22:18:16 jxw167 阅读数 2393

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

最近给读者分享一下关于Unity3D的优化,这个问题对于开发者来说都是比较头疼的问题,这里先介绍一下关于项目开发通常的做法。开发项目前期由于赶进度,不停的堆积功能和资源,这样项目完成后,包体非常庞大,代码写的也很乱,后期项目进入优化阶段,包括代码的重构,各种BUG的修复,从而导致版本开始变的不可控制,项目研发周期也是不停的延期延期,这种情况在大部分公司都是常见的问题。其实作为老板来说,他肯定要着急看到项目成果,所以就不停的督促开发人员加班加点的搞,站在老板的角度考虑问题,情有可原。

但是,作为开发者来说,我们不能按照老板的节奏走,这样后面不仅坑的是自己,也把公司坑掉了,后期项目由于各种问题很容易夭折,这种局面是不可控制的,因为项目开发完成了,后期还会加入很多功能,如果前期没有规划好,就会出现前一发而动全身的情况,这样也预示着项目即将死掉。如何避免这种情况发生,在这些系列文章中给读者分享一下笔者关于这些问题的解决方案。

用Unity引擎开发,我们就要了解它们的工作机制,这样在项目进行优化时也会有针对性,关于优化方面,笔者也做过一些视频讲座,当然这些系列文章是视频中没有的,Unity首先遇到的问题就是效率问题,很多项目由于效率问题夭折了,所以这个问题的解决非常重要,如何优化效率也就是运行帧率,针对于Unity就是减少Draw Call的数量。

 网上关于Draw Call的介绍非常多,这里再给读者向细的方向讲一下,其实Draw Call的执行就是CPU与GPU之间的通信,这就涉及到渲染流水线的概念,渲染流水线的起点是CPU,主要分三个阶段:

1、CPU把数据加载到显存中

2、设置渲染状态

3、调用Draw Call

根据上面提到的三个步骤,再详细介绍一下,所有游戏中渲染所需要的数据都需要从硬盘中加载到内存中,然后网格和纹理等数据被加载到显卡上的存储空间也就是我们所说的显存。这是因为显卡对于显存的访问速度更快,效果如下:

实际项目开发中,真实渲染中需要加载到显存中的数据比图中显示的更复杂,举例说明一下:顶点的位置信息、法线方向、顶点颜色、纹理坐标等等。

当把数据加载到显存后,在内存中的数据部分可以移除,因为对于一些数据来说,CPU仍然要访问它们,从硬盘加载到内存的过程是十分耗时的,这个也要考虑的。在这之后,开发者还需要通过CPU来设置渲染状态,从而“指导”GPU如何进行渲染工作。

接下来介绍设置渲染状态了,渲染状态定义了场景中的网格是如何被渲染的,如果我们编程没有更改渲染状态,所有的网格都将使用同一种渲染状态。如下图所示效果演示:


以上图片只是表达这个意思,同样的渲染状态,材质是一样的。准备好上述工作后,CPU就需要调用一个渲染命令来告诉GPU:数据准备好了,可以按照我的设置开始渲染,这个渲染命令就是Draw Call,讲了这么多终于回来了。

 实际上,Draw Call就是一个命令,它的发起方是CPU,接收方是GPU。当给定了一个Draw Call时,GPU就会根据渲染状态(比如材质,纹理,着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些像素,这个过程也会涉及到GPU流水线。如果有读者对此不清楚可以看看笔者以前的博客有详细的介绍。接下来我们继续解释Draw Call ,给开发者造成的误区认为 造成Draw Call问题的主要原因是GPU, 认为GPU上的状态切换是耗时的,其实不是的,真正的罪魁祸手是CPU。

下面我们就介绍CPU和GPU工作原理,大家先想一下,如果没有流水线,那么CPU需要等到GPU完成上一个渲染任务才能再次发送渲染命令。但这种方法显然会造成效率低下。我们需要让CPU和GPU可以并行工作,解决方法是使用一个命令缓冲区(Command Buffer)。命令缓冲区包含了一个命令队列,它是由CPU向其中添加命令,而由GPU从中读取命令,添加和读取的过程是相互独立的。命令缓冲区使得CPU和GPU可以相互独立工作。当CPU需要渲染一些对象时,他可以向命令缓冲区中添加命令,而当GPU完成了上一次的渲染任务后,它就可以从命令队列中再取出一个命令并执行它。

命令缓冲区中的命令有很多种,而Draw Call是其中一种,其它命令还有改变渲染状态等。效果如下图所示:

图中显示的是CPU与GPU通过缓冲区进行交互,Draw Call执行的是图中灰色的显示的,而白色的是改变渲染状态的,这个相对来说比较耗时间。

为什么Draw Call 多了会影响帧率?读者可以做一个实验,比如你复制1000个文本文件到另一个文件夹中,每个文件大小是100K,总计大小是10MB,这个要花费很长时间。我们再来创建一个单独的文件,它的大小是10M,然后也把它从一个文件夹复制到另一个文件夹。这次复制的时间少很多。主要原因在于,每一个复制动作需要很多额外的操作,比如分配内存等,如果复制1000个文件,他要开辟内存1000次,这个开销将会很大。

渲染过程跟这个类似,在每次调用Draw Call之前,CPU需要向GPU发送很多内容,包括数据、状态和命令等。在这一阶段,CPU要完成很多工作。一旦CPU完成了这些准备工作,GPU就可以开始本次的渲染。GPU渲染能力是很强的,渲染200个还是2000个三角网格没啥区别,因此渲染速度往往快于CPU提交命令的速度。如果需要执行的数据很多,也就是说Draw Call 数量很多,CPU就会把大量的时间花费在提交Draw Call上,造成CPU的过载,这个是需要我们避免的。

如何减少Draw Call的数量,这个在Unity开发中常见,减少Draw Call的方法很多,介绍一下批处理的方法。在前面提到过,复制文件的问题,CPU的时间都花费在准备Draw Call的工作上了。优化的想法就是把很多小的DrawCall合并成一个大的Draw Call,这就是批处理思想。

需要注意的是,合并网格是需要CPU在内存中执行的,合并的过程是需要消耗时间的。因此,批处理技术更加适合于哪些静态的物体,当然动态的如果合并的话,也需要在CPU中执行,我在以前的博客中也有介绍。要注意的是不要每一帧都去重新进行合并再发送给GPU,这对空间和时间都会造成一定的影响。

要做到减少Draw Call需要注意以下两点:

1、避免使用大量很小的网格,如果不可避免需要使用很小的网格结构,可以考虑合并,当然,模型的材质也可以考虑通用。

2、避免使用过多的材质,尽量在不同的网格之间共用同一个材质。

3、遇到有相同动作的物体,可以使用合并动态网格的方式进行优化,效果还不错。

在本文最后把动态网格合并的代码给读者展示如下:

public class CombineOpMesh : MonoBehaviour {


    void Start()
    {
        CombineToMesh(this.gameObject);
    }


    public void CombineToMesh(GameObject _go)
    {
        SkinnedMeshRenderer[] smr = _go.GetComponentsInChildren<SkinnedMeshRenderer>();
        List<CombineInstance> lcom = new List<CombineInstance>();


        List<Material> lmat = new List<Material>();
        List<Transform> ltra = new List<Transform>();


        for (int i = 0; i < smr.Length; i++ )
        {
            lmat.AddRange(smr[i].materials);
            ltra.AddRange(smr[i].bones);


            for (int sub = 0; sub < smr[i].sharedMesh.subMeshCount; sub++ )
            {
                CombineInstance ci = new CombineInstance();
                ci.mesh = smr[i].sharedMesh;
                ci.subMeshIndex = sub;
                lcom.Add(ci);
            }
            Destroy(smr[i].gameObject);
        }


        SkinnedMeshRenderer _r = _go.GetComponent<SkinnedMeshRenderer>();
        if (_r == null)
            _r = _go.AddComponent<SkinnedMeshRenderer>();


        _r.sharedMesh = new Mesh();
        _r.bones = ltra.ToArray();
        _r.materials = new Material[] { lmat[0] };
        _r.rootBone = _go.transform;
        _r.sharedMesh.CombineMeshes(lcom.ToArray(), true, false);
    }
}
实现原理是:首先去遍历每个对象的SkinnderMeshRenderer,然后将其所有的动态对象组合成一个大的对象并且将骨骼动画赋值给他,这样,我们就实现了动态对象的优化。

实现效果图对比如下,首先展示的是没有合并的动态动画的Draw Call数量:


然后再挂上文提到的合并脚本后的效果如下所示:


具体操作方式如下所示:





2017-03-29 22:32:30 jxw167 阅读数 996

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

继续给读者分享Unity3D关于优化技术,首先我们要做的是找到优化目标,对于项目我们主要关心的是程序运行效率,资源加载处理换句话说就是包体的大小,最后是代码的设计,下面就给读者逐一分析讲解:

一、程序优化

很多开发者认为程序优化主要是美术的事情,其实不然,代码写的不好一样的卡顿,举个例子,对于类似一些子弹类的特效,如果每次使用的时候去生成,使用完再销毁,这样会导致产生很多内存碎片,从而会导致卡帧的情况发生,这个就是程序自己的事情了,解决方式使用对象池进行预加载处理。再举一个例子,比如我们经常讨论的资源加载的效率,记得我以前做一款横版游戏时,有一个功能是从2D场景切换到3D场景,为了不影响体验中间不能加进度条,需要直接加载。这个问题比较棘手,有的同学可能会说,预先加载,但是由于前面也有场景资源而且占用一部分内存,如果预加载会产生内存瞬间增大,导致内存崩掉,这种方式行不通。读者可以考虑一下相机的裁剪范围,3D相机是通过视景体裁剪的,在视景体外的物体不参与渲染,这样可以优化效率,我们的解决方案也是基于这种方式处理的,采用的方式是通过设置摄像机的视距逐步增大处理的,可以用一个for循环,依次增大相机的距离,刚开始距离比较小,加载的资源比较小,速度非常快,进入场景后再逐步放大,这种方式解决了我们的问题。

再通过案例说一下,也是代码导致的问题,比如大家在开发接口时喜欢用静态类去做,如果只是几个类还是可以接受的,多了就会出问题。为什么呢?这就要介绍一下静态类和单例类的区别,关于这个问题在面试时也经常被问到。静态类是常驻内存的,它会一直占用内存,一般在游戏开发中会用静态类去处理。其它的用单例模式去做,这样的好处是在程序执行时只保留一个备份。单例模式主要是针对管理类处理的,这也体现了模块化思想。

二、资源的处理

再说一下资源的加载效率优化,很多项目开发并没有对建模进行优化,比如模型的面数控制在多少?模型的材质都有哪些?这个关系到模型占用内存的大小,网上也有很多关于这方面的介绍,其实这个没有一个固定的标准,因为现在硬件提高很快,但是我们自己在制作时要本着一个原则,就是模型面数尽量少,可以通过材质去表现的,可以使用比较少的面去处理,也就是Shader的渲染,材质渲染要注意的问题是尽量少用透明材质或者说有深度测试的Shader,如果在游戏场景中使用的比较多,会比较耗的。因为他要改变渲染状态模式,材质的使用原则有高光,法线,环境映射这些贴图,这些渲染主要是针对玩家自身的,不要对游戏中所有的角色进行这样的渲染。在策划制作美术需求时,尽量考虑到美术材质的重用问题,也就是建立美术的素材库。

作为游戏中的主角,不仅需要材质渲染而且还需要在其身上挂武器还有要生成特效,对于网络游戏来说,同屏的人数越多这些处理也要耗费CPU和GPU,在制作时需要考虑到粒子对于游戏的影响,游戏中的粒子特效生成和消失都要消耗CPU计算的,而粒子的材质渲染需要GPU提供支持。针对这种情况,我们只能去自己测试,比如在场景中加入多少个粒子影响效率,为了测试方便我们需要整一个小的Demo去测试,把游戏场景也放进去,模拟游戏的环境,不用整太多代码,可以让特效随机播放,必须打包到手机上进行测试,同样的原理也可以测试角色动作这些。分开测试完成后再将它们整合在一起,再测试一下效率,这样也就是我们所说的压力测试,测试结果可以供美术参考后期制作。当然实际运行还要在游戏中查看效率,这个会在后面给读者介绍。

三、架构设计

游戏架构设计与优化也是息息相关的,没有游戏架构的产品在上线后进行版本迭代时会出现问题,这也是考验产品的架构设计,运营会根据用户对上线产品的反馈做出改变,运营会快速的提出需求,要求产品增加功能或者调整数值等等。能否快速的响应这些需求变化?这就要看架构设计了。常用的架构设计有MVC,工厂模式,抽象工厂模式,状态模式以及FSM有限状态机等等。

其实做架构设计的目的是帮助开发者能够在已有的框架下快速的开发以及迭代逻辑开发,大家可以思考一下,游戏团队是多个人协助完成的,如果没有架构,在开发逻辑的时候,每个人都是按照自己的想法去整,那样就乱套了,如果是一个人开发还好,多个人那就容易出问题了。如果后期这个人离职,新来的人看不懂,再按照自己的想法重新再搞一套就形成了一个恶性循环。这个项目后期就死掉了。

架构设计首先应用在游戏UI上,使用的架构设计是MVC,每个UI对应的就是View也就是窗体的显示,C就是控制View的切换也就是UI的切换,M表示的游戏界面的数值操作。这样程序只需要在对应的View模块和Cotrol模块下写逻辑就可以了,非常方便。而且这么做的好处另一个是可以真正的将程序和美术分离开,美术制作不影响程序的实现,UI上不要挂接任何脚本,可以在程序运行时动态挂载,方便美术更新效果。

游戏中的角色都有自己的动作,动作之间是可以切换的,很多程序的写法就是封装动作接口,直接传一个参数,这样的缺点就是很容易搞混,特别是对于多个动作之间互相切换时容易发生错误,而如果采用FSM有限状态机就可以避免这种问题发生,技能特效也可以应用FSM有限状态机去设计。

在MVC模式的运用上,还可以用于武器模块的开发,在枪战游戏中,玩家可以更换多种枪支,枪支显示可以用View,更换可以用Controller去控制,Model模块就是枪支的数据,最近我们就用这种设计模式开发了一款枪战游戏。

架构设计不拘泥于形式,游戏的种类无非就是那么几种,对于开发者来说能够熟练掌握几种架构设计模式就足够了。

以上写的三点,目的是告诉读者在解决效率优化时要多思考,不能只局限于某个方面,从下篇博客开始讲解具体执行。


2017-04-02 18:06:16 jxw167 阅读数 4196

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

架构设计也是优化的一种,一款游戏如果没有一个好的架构,程序出现问题很难做到及时的响应,读者可以试想一下,如果编程的时候只是为了实现功能而实现功能,到头来就是代码越写越乱,各种功能交织在一起。出现问题后到处找BUG修复,优化就更无从谈起了,谁也不希望把即将上线的项目代码,重新重构一遍,那还不如重新再做一遍,所以架构设计在游戏开发中是非常重要的,而且是必须要掌握的技术。下面重点给读者介绍MVC和FSM架构模式,效果如下所示:


MVC架构模式在前面的优化系列中给读者提过,下面就详细的告诉读者如何编写代码?给读者详细的分析一下,拿窗口的切换进行距离,游戏运行时,首先是创立界面,我们把每个界面作为一个窗口,比如LoginWindow,HeroWindow等。它们也是作为View界面,这样游戏中就会出现很多界面的设计,效果如下所示:


下面分析界面的编写,上文提到的每个UI就是一个Window,也是一个View,这么多View肯定有自己的共性,我们可以将其定义为基类,基类称为BaseWindow,代码编写如下所示:

namespace Game.View
{
    public abstract class BaseWindow
    {
        protected Transform mRoot;

        protected EScenesType mScenesType; //场景类型
        protected string mResName;         //资源名
        protected bool mResident;          //是否常驻 
        protected bool mVisible = false;   //是否可见
      

        //类对象初始化
        public abstract void Init();

        //类对象释放
        public abstract void Realse();

        //窗口控制初始化
        protected abstract void InitWidget();

        //窗口控件释放
        protected abstract void RealseWidget();

        //游戏事件注册
        protected abstract void OnAddListener();

        //游戏事件注消
        protected abstract void OnRemoveListener();

        //显示初始化
        public abstract void OnEnable();

        //隐藏处理
        public abstract void OnDisable();

        //每帧更新
        public virtual void Update(float deltaTime) { }

        //取得所以场景类型
        public EScenesType GetScenseType()
        {
            return mScenesType;
        }

        //是否已打开
        public bool IsVisible() { return mVisible;  }

        //是否常驻
        public bool IsResident() { return mResident; }

        //显示
        public void Show()
        {
            if (mRoot == null)
            {
                if (Create())
                {
                    InitWidget();
                }
            }

            if (mRoot && mRoot.gameObject.activeSelf == false)
            {
                mRoot.gameObject.SetActive(true);

                mVisible = true;

                 OnEnable();

                OnAddListener();
            }
        }

        //隐藏
        public void Hide()
        {
            if (mRoot && mRoot.gameObject.activeSelf == true)
            {
                OnRemoveListener();
                OnDisable();

                if (mResident)
                {
                    mRoot.gameObject.SetActive(false);
                }
                else
                {
                    RealseWidget();
                    Destroy();
                }
            }

            mVisible = false;
        }

        //预加载
        public void PreLoad()
        {
            if (mRoot == null)
            {
                if (Create())
                {
                    InitWidget();
                }
            }
        }

        //延时删除
        public void DelayDestory()
        {
            if (mRoot)
            {
                RealseWidget();
                Destroy();
            }
        }

        //创建窗体
        private bool Create()
        {
            if (mRoot)
            {
                Debug.LogError("Window Create Error Exist!");
                return false;
            }

            if (mResName == null || mResName == "")
            {
                Debug.LogError("Window Create Error ResName is empty!");
                return false;
            }

            if (GameMethod.GetUiCamera.transform== null)
            {
                Debug.LogError("Window Create Error GetUiCamera is empty! WindowName = " + mResName);
                return false;
            }

            GameObject obj = LoadUiResource.LoadRes(GameMethod.GetUiCamera.transform, mResName);

            if (obj == null)
            {
                Debug.LogError("Window Create Error LoadRes WindowName = " + mResName);
                return false;
            }

            mRoot = obj.transform;

            mRoot.gameObject.SetActive(false);

            return true;
        }

        //销毁窗体
        protected void Destroy()
        {
            if (mRoot)
            {
                LoadUiResource.DestroyLoad(mRoot.gameObject);
                mRoot = null;
            }
        }

        //取得根节点
        public Transform GetRoot()
        {
            return mRoot;
        }
    }
}

以上是关于窗体的基类,实现了所有窗体需要创建的方法,创建窗体,销毁窗体,预加载,掩藏 等操作。实行按的具体操作类需要继承该窗体类,比如LoginWindow的写法如下所示:

namespace Game.View
{
    public class LobbyWindow : BaseWindow
    {
        public LobbyWindow()
        {
            mScenesType = EScenesType.EST_Login; //场景类型
            mResName = GameConstDefine.LoadGameLobbyUI; //资源名字
            mResident = false;  //是否常驻内存
        }

        ////////////////////////////继承接口/////////////////////////
        //类对象初始化
        public override void Init()
        {
            EventCenter.AddListener(EGameEvent.eGameEvent_LobbyEnter, Show);
            EventCenter.AddListener(EGameEvent.eGameEvent_LobbyExit, Hide); 
        }

        //类对象释放
        public override void Realse()
        {
            EventCenter.RemoveListener(EGameEvent.eGameEvent_LobbyEnter, Show);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_LobbyExit, Hide); 
        }

        //窗口控件初始化
        protected override void InitWidget()
        {           
            mHomepage = mRoot.FindChild("StartMenuManager/StartMenuBtn/Homepage").GetComponent<UIToggle>();
            UIGuideCtrl.Instance.AddUiGuideEventBtn(mHomepage.gameObject);
            mBattle = mRoot.FindChild("StartMenuManager/StartMenuBtn/Battle").GetComponent<UIToggle>();
            mMarket = mRoot.FindChild("StartMenuManager/StartMenuBtn/Market").GetComponent<UIToggle>();
            mInteraction = mRoot.FindChild("StartMenuManager/StartMenuBtn/Interaction").GetComponent<UIToggle>();

            UIGuideCtrl.Instance.AddUiGuideEventBtn(mMarket.gameObject);
            mDiamondText = mRoot.FindChild("Status/Diamond/Label").GetComponent<UILabel>();
            mGoldText = mRoot.FindChild("Status/Gold/Label").GetComponent<UILabel>();
            mSettingBtn = mRoot.FindChild("Status/Setting").GetComponent<UIButton>();

            UIGuideCtrl.Instance.AddUiGuideEventBtn(mBattle.gameObject);
            EventDelegate.Add(mHomepage.onChange, OnHomePageChange);
            EventDelegate.Add(mBattle.onChange, OnBattleChange);
            EventDelegate.Add(mMarket.onChange, OnMarketChange);
            EventDelegate.Add(mInteraction.onChange, OnInteractionChange);
            UIEventListener.Get(mHeadIcon.transform.parent.gameObject).onClick += InfoPersonPress;
        }
        private void InfoPersonPress(GameObject go)
        {
            GameLog.Instance.AddUIEvent(GameLog.UIEventType.UIEventType_PersonalInfo);

            LobbyCtrl.Instance.AskPersonInfo();
            //PresonInfoCtrl.Instance.Enter();
        }
      
        //窗口控件释放
        protected override void RealseWidget()
        {
        }


        //游戏事件注册
        protected override void OnAddListener()
        {
            EventCenter.AddListener(EGameEvent.eGameEent_ChangeMoney, RefreshMoney);
            EventCenter.AddListener<bool>(EGameEvent.eGameEvent_ReceiveLobbyMsg, NewChat);
            EventCenter.AddListener(EGameEvent.eGameEvent_AddNewMailReq, NoticeNewMail);
            EventCenter.AddListener(EGameEvent.eGameEvent_ChangeNickName,ChangeNickName);
            EventCenter.AddListener(EGameEvent.eGameEvent_ChangeHeadID, ChangeHeadID);

            EventCenter.AddListener(EGameEvent.eGameEvent_ChangeUserLevel, ChangeLevel);
        }
        private void ChangeHeadID()
        {
            mHeadIcon.spriteName = GameUserModel.Instance.GameUserHead.ToString();
        }

        //游戏事件注消
        protected override void OnRemoveListener()
        {
            EventCenter.RemoveListener(EGameEvent.eGameEent_ChangeMoney, RefreshMoney);
            EventCenter.RemoveListener<bool>(EGameEvent.eGameEvent_ReceiveLobbyMsg, NewChat);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_AddNewMailReq, NoticeNewMail);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_ChangeNickName, ChangeNickName);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_ChangeHeadID, ChangeHeadID);
            EventCenter.RemoveListener(EGameEvent.eGameEvent_ChangeUserLevel, ChangeLevel);
        }

        //显示
        public override void OnEnable()
        {

        }

        //隐藏
        public override void OnDisable()
        {

        }

        public void ChangeUserLevel()
        {
            mLevel.text = GameUserModel.Instance.UserLevel.ToString();
        }
	//回调事件
        public void OnHomePageChange()
        {
            //todo
            mLevel.text = GameUserModel.Instance.UserLevel.ToString();
            mNickName.text = GameUserModel.Instance.GameUserNick;
            mHeadIcon.spriteName = GameUserModel.Instance.GameUserHead.ToString();
            mGold.text = GameUserModel.Instance.mGameUserGold.ToString();
            mDiamond.text = GameUserModel.Instance.mGameUserDmd.ToString();
            VipSignLevel.text = "VIP "+GameUserModel.Instance.GameUserVipLevel.ToString();
            int level = GameUserModel.Instance.UserLevel;
            mLevel.text = level.ToString();
            
            LevelConfigInfo leveinfo = ConfigReader.GetLevelInfo(level);
            if (leveinfo != null)
            {
                mExp.text = GameUserModel.Instance.GameUserExp + "/" + leveinfo.LevelUpExp;
                mExp.transform.parent.GetComponent<UISprite>().fillAmount = GameUserModel.Instance.GameUserExp / leveinfo.LevelUpExp;
                if (level >= 29 && GameUserModel.Instance.GameUserExp >= leveinfo.LevelUpExp)
                {
                    level = 30;
                    mLevel.text = level.ToString();
                    mExp.gameObject.SetActive(false);
                    mExp.transform.parent.GetComponent<UISprite>().fillAmount = 1f;
                }
            }
            
            if (mHomepage.value)
            {
                GameLog.Instance.AddUIEvent(GameLog.UIEventType.UIEventType_HomePage);
                HomePageCtrl.Instance.Enter();
            }
            else
            {
                HomePageCtrl.Instance.Exit();
            }
        }

        public void OnBattleChange()
        {
            if (mBattle.value)
            {
                GameLog.Instance.AddUIEvent(GameLog.UIEventType.UIEventType_Battle);
                BattleCtrl.Instance.Enter();
            }
            else
            {
                BattleCtrl.Instance.Exit();
            }
        }

        public void OnMarketChange()
        {
            if (mMarket.value)
            {
                GameLog.Instance.AddUIEvent(GameLog.UIEventType.UIEventType_Market);
                MarketCtrl.Instance.Enter();
            }
            else
            {
                MarketCtrl.Instance.Exit();
            }
        }

        public void OnInteractionChange()
        {
            if (mInteraction.value)
            {
                GameLog.Instance.AddUIEvent(GameLog.UIEventType.UIEventType_Friend);
                SocialCtrl.Instance.Enter();
            }
            else
            {
                SocialCtrl.Instance.Exit();
            }
        }

        public void MailListBtn(GameObject obj)
        {
            mMailBtnBg.SetActive(false);

            MailCtrl.Instance.Enter();
        }
    }


}
该类主要是实现整个UI的逻辑编写,其他的窗口编写类似。下面编写Control控制类代码的编写,示例代码如下:

    public class LoginCtrl : Singleton<LoginCtrl>
    {
        public void Enter()
        {
            EventCenter.Broadcast(EGameEvent.eGameEvent_LoginEnter);
        }

        public void Exit()
        {
            EventCenter.Broadcast(EGameEvent.eGameEvent_LoginExit);
        }

        //登陆
        public void Login(string account, string pass)
        {

        }

        //登陆错误反馈
        public void LoginError(int code)
        {

        }

        //接收GateServer信息
        public void RecvGateServerInfo(Stream stream)
        {

        }

        //登陆失败
        public void LoginFail()
        {

        }

        //选择LoginServer
        public void SelectLoginServer(int i)
        {

        }
        //开始游戏
        public void GamePlay()
        {

        }
   }
控制类的主要作用是进行一些消息的传递,这也是为了解除模块之间的耦合性,该模块的主要核心是Enter函数和Exit函数。后面继续代码的编写。。。。


2017-04-04 12:49:16 jxw167 阅读数 3033

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

下面给读者讲解在游戏开发中经常使用的FSM有限状态机的实现,有限状态机在状态切换中使用的非常多,比如足球游戏,动作游戏等。角色在游戏场景中经常需要动作的切换,比如Idle,Run,Attack,Skill等,这些技能状态之间的切换,我们通常会使用FSM去处理。我们在游戏中将动作切换和技能切换放在一起使用FSM处理,下面通过代码的方式给读者进行封装处理:

	public interface EntityFSM
	{
		bool CanNotStateChange{set;get;}
		FsmState State { get; }
		void Enter(Ientity entity , float stateLast);
		bool StateChange(Ientity entity , EntityFSM state);
		void Execute(Ientity entity);
		void Exit(Ientity Ientity);
	}

先定义一个抽象类用于具体状态的实现,游戏中的具体状态,我们通过枚举的方式定义如下:

	public enum FsmState 
	{
		FSM_STATE_FREE,
		FSM_STATE_RUN,
       		 FSM_STATE_SING,
		FSM_STATE_RELEASE,
        	FSM_STATE_LEADING,
        	FSM_STATE_LASTING,
		FSM_STATE_DEAD,
       	 	FSM_STATE_ADMOVE,
        	FSM_STATE_FORCEMOVE,
        	FSM_STATE_RELIVE,
        	FSM_STATE_IDLE,
	}
下面告诉大家如何使用我们具体的动作状态切换,具体实现就是继承上面定义的接口:

 public class EntityIdleFSM : EntityFSM
	{
        public static readonly EntityFSM Instance = new EntityIdleFSM();
		
		public FsmState State
		{
			get
            		{
               		 	return FsmState.FSM_STATE_IDLE;
			}
		}
		
		public bool CanNotStateChange{
			set;get;
		}
		
		public bool StateChange(Ientity entity , EntityFSM fsm)
        	{
			return CanNotStateChange;
		}
		
		public void Enter(Ientity entity , float last)
        	{
            		entity.OnEnterIdle();
		}
		
		public void Execute(Ientity entity)
        	{
            		if (EntityStrategyHelper.IsTick(entity, 3.0f))
           	 	{
                		entity.OnFSMStateChange(EntityFreeFSM.Instance);
            		}
		}
		
		public void Exit(Ientity entity){
			
		}
	}
在这个函数中使用了一个接口Ientity,其实它主要实现的是状态之间的切换,下面就把该类的主要功能实现如下所示:

 /// <summary>
        /// 状态改变
        /// </summary>
        /// <param name="fsm"></param>
        /// <param name="last"></param>
        public void OnFSMStateChange(EntityFSM fsm, float last)
        {
            if (this.FSM != null && this.FSM.StateChange(this, fsm))
            {
                return;
            }

            if (this.FSM == fsm && this.FSM != null && this.FSM.State == FsmState.FSM_STATE_DEAD)
            {
                return;
            }
            if (this.FSM != null)
            {
                this.FSM.Exit(this);
            }

            if (this.FSM != null)
                this.RealEntity.FSMStateName = fsm.ToString();

            this.FSM = fsm;
            StrategyTick = Time.time;
            this.FSM.Enter(this, last);
        }

        public void OnFSMStateChange(EntityFSM fsm)
        {
            if (this.FSM != null && this.FSM.StateChange(this, fsm))
            {
                return;
            }

            if (this.FSM == fsm && this.FSM != null && (this.FSM.State == FsmState.FSM_STATE_DEAD))
            {
                return;
            }

            if (this.FSM != null)
            {
                this.FSM.Exit(this);
            }

            this.FSM = fsm;
            if (this.FSM != null)
                this.RealEntity.FSMStateName = fsm.ToString();
            StrategyTick = Time.time;
            this.FSM.Enter(this, 0.0f);
        }

该类还提供了各个动作的接口,实现如下所示:

	public virtual void OnEnterIdle()
        {
            this.RealEntity.PlayerAnimation("idle");
        }

        /// <summary>
        /// Run状态进入时调用
        /// </summary>
        public virtual void OnEnterMove()
        {
        }

这个RealEntity类实现的是具体的动作或者是新动画触发函数封装事例代码如下所示:

	public void PlayerIdleAnimation()
    	{
        	if (this.animation == null)
        	{
           	 	return;
       		 }

        PlayerAnimation("idle");
        this.animation.PlayQueued("free");
    }

	public void PlayerFreeAnimation()
    {
        if (this.animation == null)
        {
            return;
        }

        PlayerAnimation("free");
	}

	public void PlayerRunAnimation(){
		if (this.animation == null) {
			return;
		}
        PlayerAnimation("walk");
	}

技能的接口实现跟这个类似,代码如下所示:

	public class EntityReleaseSkillFSM : EntityFSM
	{
		public static readonly EntityFSM Instance = new EntityReleaseSkillFSM();
		public FsmState State{
			get
			{
				return FsmState.FSM_STATE_RELEASE;
			}
		}
		public bool CanNotStateChange{
			set;get;
		}
		
		public bool StateChange(Ientity entity , EntityFSM fsm){
			return CanNotStateChange;
		}
		
		public void Enter(Ientity entity , float last){
			//Debug.LogError("prepareplayskill enter!");
			entity.OnEntityReleaseSkill();
		}
		
		public void Execute(Ientity entity){
            //entity.OnEntityPrepareAttack ();
		}

		public void Exit(Ientity entity){
		    
		}
	}
这样我们的FSM状态机就实现完成了,使用该框架开发了多款游戏,效果如下:






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