银色月光下

漫漫长夜

  C++博客 :: 首页 :: 联系 :: 聚合  :: 管理
  4 Posts :: 7 Stories :: 0 Comments :: 0 Trackbacks

常用链接

留言簿(12)

我参与的团队

搜索

  •  

最新评论

阅读排行榜

评论排行榜


一些概述:

我们考虑选择一款游戏引擎,会考虑它的全面性——全平台,有丰富资源文档,社区,有良好灵活的开发方式,易上手但也可充分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是这样产生的,perusecode
fillVertex(vertext datas);
setShader(guiShader);
setShaderParams
setTexture;
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之类的控件,它提供了一些基础API

BeginGroup /EndGroup //这个可以做transform和 clip
DrawTexture/DrawTextureWithTexCoords//可取贴图坐标
Label //文字绘制

可以看到,能较简单地用来做GUI系统绘制底层, 

2. GL
GL类是unity封装的一组openGL的底层立即模式,功能很弱,基本上不实用,只可能适用于一些特殊的图形效果;

3. Graphics
Graphic.DrawTexture //在屏幕坐标上绘制纹理
Graphic. Blit //象素拷贝
Graphic. DrawMesh //网络绘制,程序化地绘制mesh(而不是一般情况那样创建 Mesh         GameObject

4.  GUITexture/GUILable (Component)
以组件方式提供在屏幕坐标绘制,有colorTint, alpha set

5.  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过程中,它将顶点系统写UIGeometry
public override void UILabel.OnFill
public override void UISprite.OnFill

3. 通过UIWidget.WriteToBuffers过程中,将UIGeometry的顶点信息写入UIPanel
4. 通过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=true

3)对touch的响应
        和windows上的事件不同,触屏设备上支持多点触屏,那么比如说事件响应时应带一触点id,对于控件来说,也需要有一个selected[] 和touchID[]属性,以支持比如说双指操作等

4. 实现
因为GUI是一个很孤立的系统,我希望它能脱离开unity的层级系统,而不是像unity那样与 GameObject纠结不清.
渲染上,它借鉴NGUI,创建正交相机,创建Mesh物体达到批渲染的目的,这应是效率最高,灵活度最好的方式吧.
设计器支持点选,拖拽,在editor playing状态下,inspector里进行编辑,并存成xml layout描述,并支持事件绑定代码的生成.
posted on 2014-02-25 19:51 lichking 阅读(1536) 评论(0)  编辑 收藏 引用 所属分类: GUI

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理