一些概述:我们考虑选择一款游戏引擎,会考虑它的全面性——全平台,有丰富资源文档,社区,有良好灵活的开发方式,易上手但也可充分hack,适合开发各种游戏等,从综合角度上说,unity3D是很好的选择.unity相比其它引擎的好处是——3D引擎可以用来做2D,而2D游戏引擎却不可能够制作3D,有人会提出,3D引擎做2D会不会不够专业,优化得不够好,资源开销大?至少从理论上不应是这样,在图形绘制普遍采用GPU加速的今天, 2D游戏引擎只是3D的一个特殊场景而已。话虽这样说,但大家都知道,unity在2D方面是支持不足的,一个证据是,在unity3D没有高效率的原生GUI,谁也不会在产品里使用unity 自已GUI.这里探讨的就是, 如何在unity里做高效的2D,先分类下2D的实际应用。分类:游戏里2D的应用包括, GUI和2D场景,GUI和2D场景虽都是2D,但侧重还是有所不同,我们看看对GUI和场景所需要的支持,2D场景: 动画,2D碰撞,卷轴与伪3D坐标转换,遮罩,特效混合GUI: 1. layout: 排版方式, html的盒子模型,图文混排,区域裁剪, 层级,绝对坐标,相对坐标,停靠(Anchor), 今天的GUI已经普遍采用DSL对其进行描述,如HTML, WPF使用的XAML, 还有各种GUI系统的XML,这是因为GUI的树状层级性,GUI控件所抽象出的属件和事件概念. 2. 消息模型,控件封装,从鼠标,键盘,touch事件中转化成GUI消息,并处理消息传递.3. GUI handler,如何query到一个控件,并绑定消息处理.可以看到,2D场景与GUI的关注点是不一样的,基本可以看做是两个系统选择:目前unity里2D的选择有(google之,就不占篇幅了),GUI: EZGUI, NGUI,2D: 2DKIT, unity2D(3.0后的官方的2D方案)原理: 3D渲染管道的过程是: 1. 准备顶点图元信息,包括position(空间位置),color(颜色),UV(贴图坐标),normal(影响光照),其它光照信息,骨骼数据等. 2. 视锥几何裁剪,几何细分/变换(DX11的geo shader); 3. 顶点变换, local->world->view(camera),项点光照处理, (vertext shader) 4. 象素插值,就是由顶点信息到顶点间象素的插值. 5. 象素处理,纹理查找,象素值输出(ps shader). 6. depth测试,alpha测试,模板测试,颜色混合. 7. 视口变换.由于这几个步骤在管道中流水进行,且有大量的硬件变换和象素填充单元,这就是GPU加速的奥秘了.流水线走完一次,被称为一次drawcall,一般来说drawcall越少,渲染效率越高. 对于2D.GUI, 1. 顶点数据一般是矩形,变换一般是单位矩阵(对于游戏的GUI,还有一些很怪的GUI带有变换信息的GUI(比如我实现的那一个), 还有3D里的GUI不在此讨论内) 2. 着色,GUI一般需要的着色,纹理*颜色(如unity材质里的color tint); 3. Z测试值可以不需要(也有用做深度排序),, 按前后排序绘制就可以了 4. alpha混合 src*alpha+dst*(1-alpha) (特效的混合经常是不同 的方式); 5. 裁剪,一般通过写入模板缓冲(stencil buffer)进行裁剪.思考:在这样的渲染策略下,如何减少drawcall?而且在unity的框架内,我们有怎样的灵活度,制作GUI system呢?NGUI是怎样做的?
(二)2D优化在API调用上,一次draw call是这样产生的,perusecodefillVertex(vertext datas);setShader(guiShader);setShaderParamssetTexture;otherRenderStateSetting;draw call所以在场景里渲染时,中间状态改了,比如说,1. 多提交了一次顶点,2. 换了个texture, 3. 修改了渲染参数,都会导致draw call 增加,对于1,一次提交尽多的顶点,对于GUI来说,就是一次提交多个矩形对于 2,把多个texture进行合并,比如说NGUI里的图集对于 3, 渲染参数在unity里就是material,因此尽量减少material数量和设置改变(2也看做材质参数)以上说的不尽然,从根本上,drawcall 减少,可能带来其它开销,比如说带宽压力增加,具体优化还是以实际测试为主 (三)unity3D里制作2D的手段1. GUI类UnityEngine.GUI是一个静态类,里面提供了诸如Button之类的控件,它提供了一些基础APIBeginGroup /EndGroup //这个可以做transform和 clipDrawTexture/DrawTextureWithTexCoords//可取贴图坐标Label //文字绘制可以看到,能较简单地用来做GUI系统绘制底层, 2. GLGL类是unity封装的一组openGL的底层立即模式,功能很弱,基本上不实用,只可能适用于一些特殊的图形效果;3. GraphicsGraphic.DrawTexture //在屏幕坐标上绘制纹理Graphic. Blit //象素拷贝Graphic. DrawMesh //网络绘制,程序化地绘制mesh(而不是一般情况那样创建 Mesh GameObject4. GUITexture/GUILable (Component)以组件方式提供在屏幕坐标绘制,有colorTint, alpha set5. MeshRender/MeshFilter (Component)网格物体,可以通过设置网格(顶点数据)和关联 material 实现场景绘制,因此通过设置正交相机(就是无远小近大效果的非真实相机),渲染2D效果;6. unity2D / Sprite,基本可以理解成一个预置好的plane mesh,unity在工具上提供了较方便的编辑特性,并提供了动画,碰撞检测系统;性能:我这里直接说结论了, 6==5==3>>1>2,所以看到,GL不予考虑,在unity里可直接用做GUI绘制的原生GUI效率很差,GUITexture绘制虽快,但缺少裁剪功能和优化控制, 这点对于sprite也成立;MeshRender/MeshFilter (Component) 和shader的结合可以做到2D绘制一切效果; 因此这正是NGUI的作法(接下来分析NGUI)
(四)关于NGUI的4点说明1. NGUI版本NGUI版本推出很快,而且版本之间变异较大,并不向下兼容,推荐选择3.0以上的版本2. 效率NGUI避免问题的同时,也少了一些莫名其妙的优化,drawcall增加,新版本效率反而稍差3. 关于夹层问题在3.0 以前,物体的渲染次序,是根据atalas,depth, Z, panel几者相关,往往有出人意料的结果,摸一处而乱全身,3.0后,只和 depth有关,简单很多.4. UIpanel UIPanel至少发生一次draw call, 使用过多的panel 会造成效率损失,过去版本使用panel控制渲染次序,现在则没这种需要,目前我使用panel的唯一理由是它提供的clip功能。为什么选择NGUI社区广,文档多,效率不错,功能足够,容易入门NGUI如何地不好据说NGUI很多人吐槽,欢迎举证NGUI适合一开始从unity进入游戏开发的朋友,很容易上手,但对于从其它游戏引擎或GUI系统,进入的话,通过比较就知道问题所在,学习其它GUI,首先要熟悉一推配置(或API),这是让人稍觉头痛之处,但过了这个阶段,GUI开发就变成了一件高效率和灵活的事,因为配置可以工具化,所以GUI系统会提供设计器,如visual stdio designer, 各种UI编辑器等,但NGUI使用的是untiy自己的层级系统的设计器,这个就将实现的很多细节暴露出来,NGUI并未实现一个专业的设计器功能.关于事件系统,1. 有些GUI系统将脚本与布局写在了一块(如plain js in html),这也就是逻辑事件与GUI写在一块了,如果事件处理较轻,是很方便的,2. 还有通过query的方式,通过查找GUI物体,绑定GUI事件,(如 JQuery),这种做法,将布局与事件处理解耦了,是一种更好的做法,我们想想,将来界面改版,但逻辑类似,那么,逻辑代码不怎么需要改动,而界面设计也变成了一件独立的事. (我的意思是说,NGUI的事件系统做得略搓) (五)呜呼哀哉,文档图传不上,折腾了半天(代码以NGUI3.0为例)渲染:真正完成渲染的,其实是在_UIDrawCallXXX的隐藏 GameObject里,根据渲染顺序被组装成 Mesh数据,最终渲染的就是这个Mesh了,这就是显示的结果和编辑器里的GameObject并不一致的原因,因为最终显示出来的并不是这些UI对应的GameObject,而是一个隐藏节点 _UIDrawCall可以在代码里打开显示(UIPanel.cs) GameObject go = UnityEditor.EditorUtility.CreateGameObjectWithHideFlags("_UIDrawCall [" + mat.name + "]", HideFlags.DontSave | HideFlags.NotEditable); //HideFlags.HideAndDontSave);可以看到,层级是是这样的,_UIDrawCall [Font Material]_UIDrawCall [num]UI Root (2D) Camera Anchor Panel Label Sprite打开_UIDrawCall看看里面,可以看到,每个DrawCall包含一个Mesh每个DrawCall有个 render queue=2,表示它的渲染次序,两个三角形表示其中只有一个窗口,它所在的图集是 num1以上图示的是NGUI里最重要的几个类,渲染流程其实很简单.1. UIWidget是所有控件的基类, 将所有的widget按depth排序, 此处没有其它规则(对于3.0有效); static public int CompareFunc (UIWidget left, UIWidget right)2. 在UIWidget.OnFill过程中,它将顶点系统写UIGeometrypublic override void UILabel.OnFillpublic override void UISprite.OnFill3. 通过UIWidget.WriteToBuffers过程中,将UIGeometry的顶点信息写入UIPanel4. 通过UIPanel.SubmitDrawCall, 将顶点信息写入UIDrawCall的Mesh (六)第6篇,结束篇我希望在unity里有这样一种的GUI系统,(类HTML)1. XML格式描述的layout 的层级关系(类html), 1)渲染顺序 根据层极的自动depth 同一层级深度按顺序 层级的子层级之间不可穿插 比如说 <sprite name="A"> <sprite name="a1"> <sprite name="a2"> <sprite name="a3"> <sprite name="B"> <sprite name="b1"> <sprite name="b2"> <sprite name="b3"> 这里的深度关系可能是 A.depth = 1 //depth不需要指定,这里是示意 深度关系 a1.depth = 1.1 a2.depth = 1.2 a3.depth = 1.3 B.depth=2 b1.depth=2.1 b2.depth=2.2 b3.depth=2.3 2)裁剪(clip) 父结点可指定对子结点进行裁剪, <panel name="A" clip="true"> <sprite name="a1"> //被父结点裁剪 3)停靠(anchor) 子结点相对父结点可设置停靠, lefttop, leftmiddle, leftbottom, middleup, center, middlebottom,righttop,rightmiddle,rightbutton 9个锚点+offset 例: <spirte name="icon" anchor="lefttop" offsetx="8,8"> //左上对齐,偏移为 8,8 4) 自动排版 vertical//坚版 horical//横版 hgrid//2维对齐,从左到右,换行 vgrid//"2维对齐,从上到下,换列" box//html那样的盒子模型,可用于图文混编 <panel name="A" layout=" hgrid " gridItem="4" gridOffset="80,80"> //从左向右,4格换行,然后从上到下排列,(80,80)为一个格子. <stub layout="box"> <lable text="呵呵"> </lable><icon type="laugh"></cion> //这样聊天表情超链接神马的也直接支持了,本来了,类似html. 5) 样式与layout分离,类似于 CSS <style name="title style"> clip="true", anchor="middleTop",........... <rect name="title" style=" title style ">2. query 1) 路径查找 <sprite name="A"> <sprite name="a1"> 具有tag查找能力,只限于第一个tag var widget = gui.find("sprite. sprite ") 2)名字找 var widgets = gui.findByName("a1") var widget = widgets[0]; 3)id查找(类似html.ID,唯一) <sprite name="A"> <sprite name="a1" id="tobeHandle"> var widget = gui.findByID("tobeHandle ") 4)class查找(类似html. class) var widgets = gui.findByClass("title") var widget = widgets[0]; 3. 事件系统1) layout与逻辑分离,总是通过query查找,并绑定事件 var widget = gui.findByID("tobeHandle ") widget.onClick += ()=> { debug.logInfo("tobeHandle is clicked") }2)独占消息或是向上传递 作为 widget的一个属性, <sprite name="A"> <sprite name="a1" id="to be handle" propagate="true"> //父结点A也响应此消息 或代码 widget.propagate=true3)对touch的响应 和windows上的事件不同,触屏设备上支持多点触屏,那么比如说事件响应时应带一触点id,对于控件来说,也需要有一个selected[] 和touchID[]属性,以支持比如说双指操作等4. 实现因为GUI是一个很孤立的系统,我希望它能脱离开unity的层级系统,而不是像unity那样与 GameObject纠结不清.渲染上,它借鉴NGUI,创建正交相机,创建Mesh物体达到批渲染的目的,这应是效率最高,灵活度最好的方式吧.设计器支持点选,拖拽,在editor playing状态下,inspector里进行编辑,并存成xml layout描述,并支持事件绑定代码的生成.
|