终极优化你的游戏 —— 使用脏矩形技术



介绍说明:本文由kylinx本人亲自撰写欢迎各位游戏制作同仁转载和指点但是任何人不得在本人许可的外以任何理由篡改模糊本文谢谢联系方式:[email protected]

 

    很久以来由于工作上繁忙没有写新东西了~hoho~

    本文基于2D表现游戏在当今3D大行其道时代说2D是否显得格格不入?这个问题我不作讨论本人从事直都是2D游戏开发所以如果你认为讨论2D技术是个过时东西就此打住

    废话不多说下面进入正题

    优化直是我在中追求东西的想想让自己游戏在个古董机器能流畅运行或者说在当今机器上CPU占用率和内存占用率都很低情况(毕竟我非常讨厌个游戏独占了我所有CPU资源)

    如果从图形接口上作优化常用就是使用3D加速和CPU特殊指令(虽然说DirectDraw能够使用2D硬件加速但大部分机器支持仅仅是简单加速比如带ColorKey支持些稍微高级东西比如Alpha混合带Alpha通道纹理(表面)都不支持需要自己写优化起来还是使用CPU特殊指令)虽然说使用3D加速非常简单但是它缺点也非常明显:对硬件有苛刻要求如果仅仅是做2D游戏不推荐使用(新手作为练习写DEMO而使用倒还可以,我也这样使用过呵呵)使用特殊CPU指令最常见就是使用MMX指令了现在想找到块装了Windows95以上但不支持MMXCPU都有难度 ~自己花了大半年时间用MMX高速实现了D3D对2D贴图各种特效(带通道或者不带通道纹理带BlendColor, 带缩放旋转做加减法Alpha混合的类)的后虽然发现可以不使用D3D东西但是如果画面东西很多些内存带宽不高机器上速度还是不够理想所以还是需要更多优化这时候我想起了DirtyRect

    什么是脏矩形?简单就是游戏每次画面刷新只更新需要更新块区域Windows本身就是最好例子或者说FlashControl控件也正是利用了脏矩形技术所以他效率才如此传统游戏循环如下:

      while( 游戏没有结束 )

       {

              ( 有Windows消息 )

              { 

                     处理Windows消息

              }

               ( 需要渲染 )

              {

                     清除游戏屏幕缓冲区

                     把游戏中物体画到缓冲区里面

                     把缓冲区更新到游戏窗口上

                     锁定游戏速度Sleep段时间限制FPS

              }

       }

    从上面伪代码可以看出每次游戏都要做清除缓冲区-〉渲染游戏物体-〉更新到窗口而基本上我们写游戏至少要保证最低每秒钟要刷新24帧以上(般都在30)所以上面代码每秒钟要至少24次以上画面东西越多耗费CPU越多

    不过我们也可以自然想到每次那么多东西不定都需要更新比如个动画般都有个延迟比如间隔200毫秒更新那么在这段时间是不需要重新画只有更新了帧以后表示这个动画所在范围已经“脏”了需要重新画这个时候才需要画这个动画而这段时间的内我们可以节约大量CPU时间很自然积少成多总体下来这个数值是非常可观再举个例子个静止游戏物体(比如棵树)是永远都不需要更新除非这个树位置或者他属性发生了变化这样下来我们首先想到每次我们都省略清除后台缓冲这个步骤这个非常重要次画下来东西都在这个缓冲区里面如果清除的后就什么都没有啦~~

    搞明白了这个原理以后下面来看看具体实现过程中遇到问题:

    游戏中物体不会是相互没有遮挡所以如果遇到遮挡问题如何办?

    如果游戏中有100个物体里面物体相互遮挡关系总有个顺序为了简化问题只考虑两个物体遮挡情况多个物体遮挡可以根据这个来衍生

按此在新窗口浏览图片500)this.width=500\" src=/upload/article/a2005121015071318.jpg>

    考虑上图物体B遮挡了物体A, 也就是说渲染顺序是先画A再画B这个顺序由各自定义(我自己就喜欢用棵渲染树来排序当然如果你用连表或者其他数据结构来实现也没有问题)如果物体A整个区域都需要更新那么对于B物体需要更新部分也就只有A和B交集部分(图中蓝色区域)在画B时候我们设置目标裁减区域(也就是屏幕缓冲裁减区域)为这个交集部分则B在渲染时候相当于整个缓冲区大小就只有蓝色区域那么大那么裁减将会把B数据区裁减到相应位置(你实现图形中不会没有做裁减工作吧???如果没有实现你就不用看了直接算了不然下面东西你肯定不明白我说什么)如何样B物体相当于只画了蓝色区域这部分东西比整个区域来说节约了不少时间吧?



    不知道上面说你明白了没有如果没有明白请多看几遍直到弄明白的后再往下看不然千万不要往下看

    上面例子大家肯定会问个问题我如何控制B只画蓝色区域部分呢?这个问题我暂时不说等到把所有遮挡情况说完了再说继续看另外遮挡情况

按此在新窗口浏览图片500)this.width=500\" src=/upload/article/a2005121015263859.jpg>

    上面6个物体A,B,C,D,E,XX是我们游戏背景颜色假设画顺序是EADCB,如果E需要重新画那很显然A,B,C,D不需要做什么

    如果A,D都需要重新画那显然A,D只需要各画而B需要更新不是需要更新BD相交区域而是AB相交大区域也就是说小区域该忽略掉如果B需要重新画A,D,C需要重新画吗?也许有人会说B画次序是在最后所以前面就不需要画了对么?答案是错需要重新画背景缓冲区我们般情况下不去清除它所以谈不上画顺序了也就是说A和B相交部分A在下次画时候也需要更新D也同样(想通了吗?再举个例子如果B含有大量透明色如果B需要更新那么B区域首先要涂上X作为背景不然B非透明色如果变成了透明色那B在重新画时候由于透明色不需要画那么B上次留下来颜色就残留在X上面看起来当然不对啦同理对于A,D也样处理)

    上面理论部分不知道听明白了没有如果不明白话自己花点点时间去想象看假如明白了下面继续更加深入问题

    从上面理论解说部分可以看出脏矩形选取和优化是关键怎样得到最优化脏矩形表就成为了这个技术优化核心部分

    为了简单起见这里使用个链表来管理所有渲染物体

    为了实现我们所设计东西我设计了个非常简单类:

        CRenderObject

       {

       public:

              virtual ~CRenderObject{}

              virtual void OnRender( GraphicsDevice*pDevice ) = 0; //所有物体都在这里渲染

              virtual void OnUpdate( float TimeStamp ) = 0;//物体更新比如动画帧更新拉的类在这里面可以设置DirtyRect标志的类

              virtual bool IsDirty( ) = 0;//是否有脏矩形

              virtual bool GetBoundsRect(RECT*pRect) =0;//得到该物体范围

              virtual  GetDirtyRects ( RECT*pRectBuffer ) = 0;//该物体脏矩形个数填充到pRectBuffer里面返回填充了多少个

              ...其他

       };

我们还需要个简单能管理脏矩形和渲染物体

 CRenderObjectManager

{

pulibc:

       void RemoveRenderObject( CRenderObject*pObject );//删除个渲染物体

       void AddRenderObject( CRenderObject*pObject );//添加个渲染物体

       void Render( GraphicsDevice*pDevice );//渲染所有物体

       void Update( );//更新所有物体

       .....其他

protected:

       std::list< CRenderObject* >                 m_RenderObjects;

                                                           m_nCurrentDirtyRectCount;//当前脏矩形数量

       struct DirtyRect

       {

              RECT            Range;                  //脏矩形范围

                                AreaSize;               //脏矩形大小用来排序

       };

       BOOL                                               m_bHoleDirty;//是否全部脏了



       DirtyRect                                                 m_DirtyRects[128];//屏幕上最多脏矩形数量如果大于这个数量则认为屏幕所有范围都脏了
};

void CRenderObjectManager::Update

{

       m_bHoleDirty = false;

       m_nCurrentDirtyRectCount = 0;

        RECT DirtyRectBuffer[128];

       float TimeStamp = GetElapsedTime;

       for(std::list< CRenderObject* >::iterator it = m_RenderObjects.begin;

              it != m_RenderObjects.end; it)

       {

              CRenderObject*pObject = *it;
              pObject->OnUpdate( TimeStamp );

              (m_bHoleDirty  false && pObject->IsDirty )

              {

                      Count = pObject->GetDirtyRects(DirtyRectBuffer);

                     for( i =0; i<Count;i)

                     {

                            对于该物体个脏矩形DirtyRectBuffer

                            如果DirtyRectBuffer 没有在任何个已有脏矩形范围内

                            那么把这个脏矩形根据从大到小顺序添加到脏矩形范围内否则忽略这个脏矩形

                               如果脏矩形数量已经大于设定最大脏矩形范围设置所有所有屏幕都脏了标志

                     }

              }

       }

       如果屏幕所有都脏了填充背景颜色

       否则为每个脏矩形填充背景颜色

}

void CRenderObjectManager::Render( GraphicsDevice* pGraphics)

{

       for(std::list< CRenderObject* >::iterator it = m_RenderObjects.begin;

              it != m_RenderObjects.end; it)

       {

              CRenderObject*pObject = *it;
              (如果屏幕都脏了标志已经设定)

              {

                     RECT rcBoundsRect = { 0, 0, 0, 0 };

                     ( pObject->GetBoundsRect( rcBoundsRect ) )

                     {

                            //设置屏幕裁减区域

                            pGraphics->SetClipper( &rcBoundsRect );

                     }

                     pObject->OnRender( pGraphics );

              }

               



              {

                     

                     RECT rcBoundsRect = { 0, 0, 0, 0 };

                     ( pObject->GetBoundsRect( rcBoundsRect ) )

                     {

                            //如果该物体范围和脏矩形缓冲区任何个脏矩形有交集

                            for(  i=0; i<m_nCurrentDirtyRectCount; i )

                            {

                                   RECT rcIntersect;

                                   ( ::IntersectRect( &rcIntersect, &m_DirtyRects.Range, &rcBoundsRect ) )

                                   {     

                                          //只画交集部分

                                          pGraphics-> SetClipper ( &m_DirtyRects.Range );

                                          pObject->OnRender( pGraphics );

                                   }

                            }

                     }

              }

       }
}

 

    好了核心代码伪代码就在这里不知道大家看明白没有当然我在这里上面实现这种思路方法有个缺陷最坏情况下个也许会导致重新画很多次如图情况:

按此在新窗口浏览图片500)this.width=500\" src=/upload/article/2005121015113299.jpg>

假设A是渲染物体B,C,D,E是由大到小脏矩形范围那么很显然重叠部分就被反复画这是在分割脏矩形导致问题这样画下来如果A物体是采用了叠加混合到背景算法问题就出来了重画部分会变得非常亮所以脏矩形分割就显得非常重要也就是说把这些脏矩形还要分割为互相独立互不相交矩形至于分割算法嘛嘿嘿各位还是动下脑筋研究研究吧:)这个也是最值得优化地方了哈哈实在想不出最简单思路方法就是把彼此相交脏矩形都做个合并得到更大脏矩形虽然没有相交区域了但是也许这个脏矩形会变得比较大了哦:)

       最后大家定关心是我会不会提供源代码很抱歉不能我在我引擎中实现不是以简单链表去做棵比较复杂渲染树牵扯到东西就比较多了所以不方便提供代码不过可以给个演示吧:)再说大家如果真明白了我所说那就可以自己动手写下嘛不要怕失败/P^_^,

 

       好啦有关脏矩形技术就介绍到这里啦用好这个技术你会发现你游戏会在配置极低机器上也能运行如飞:)这种技术如果能用在现在市面上那么多游戏中就不必为个小游戏就强占了您100%CPU资源而烦恼拉:)

       如果您有更好思路方法或者指出其中不完善地方还请您不吝赐教大家多多交流:)

 

有关测试Demo

该Demo渲染部分由Kylinx花了近半年时间全部采用MMX写成已经成功实现d3d中对2d纹理操作速度非常快

 

有关Settings.ini

EnableDirtyRect = 1      //是否允许脏矩形技术0=关闭1=开启



LockFPS = 1         //是否锁定FPS0=关闭1=开启

    哈这个Demo在不锁定FPS,脏技术开启情况下Duron1.8G CPU,FPS达到 31500左右!(没错是 3万千 5百)这个数字吓人吧?如果脏技术未打开只能在150左右相差200倍阿!!!
 
    如果LockFPS开启在我机器上(512M DDR)跑30个DEMO,CPU占用还是为0哈哈!

有关商业合作:

MSN:[email protected]

Mail:[email protected]


    有关该引擎:演示用这个引擎(代号ShinyFairy:闪灵基于前期开发GFX3.0系列该系列已经成功运行在某商业游戏公司休闲游戏系列)采用Kylinx历时2年多开发具有自主知识产权基于2D游戏超级引擎强大数据加密打包图形接口同时提供d3d8版本和mmx版本该演示使用是mmx版本适用于各种休闲游戏平台或者大型2D RPG/MMORPG均可适用有意者可联系我详谈
Tags: 

延伸阅读

最新评论

发表评论