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

38 篇文章 0 订阅
36 篇文章 0 订阅

转载: http://dev.gameres.com/articles.asp?style=a&page=1 作者: Kylinx

 

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

 

 

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

 

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

 

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

 

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

 

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

 

 

     

 while( 游戏没有结束 )
{
     if( 有Windows消息 )
     {
          处理Windows消息
     }
     else if( 需要渲染 )
     {
         清除游戏屏幕的缓冲区

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

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

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

 

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

 

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

 

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

 

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

 

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

 


 

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

 

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

 

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

 


 

 

上面 6 个物体 A,B,C,D,E,X X 是我们的游戏背景颜色,假设画的顺序是 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 也一样处理)。

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

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

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

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

 

class 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 int GetDirtyRects ( RECT*pRectBuffer ) = 0;//该物体的脏矩形个数,填充到pRectBuffer里面,返回填充了多少个

              ...其他函数
};
 

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

 

class CRenderObjectManager
{

pulibc:

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

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

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

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

       .....其他函数

protected:

       std::list< CRenderObject* >                 m_RenderObjects;

       int                                                    m_nCurrentDirtyRectCount;//当前脏矩形数量

       struct DirtyRect
       {

              RECT            Range;                  //脏矩形范围

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

       };

       BOOL                                               m_bHoleDirty;//是否全部脏了

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

void CRenderObjectManager::Update()
{

       m_bHoleDirty = false;

       m_nCurrentDirtyRectCount = 0;

       static 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 );

              if(m_bHoleDirty == false && pObject->IsDirty() )

              {
                     int Count = pObject->GetDirtyRects(DirtyRectBuffer);

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

                            对于该物体的每一个脏矩形DirtyRectBuffer[i]

                            如果DirtyRectBuffer[i] 没有在任何一个已有的脏矩形范围内

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

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

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

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

}

void CRenderObjectManager::Render( GraphicsDevice* pGraphics)
{

       for(std::list< CRenderObject* >::iterator it = m_RenderObjects.begin();
              it != m_RenderObjects.end(); it++)
       {

              CRenderObject*pObject = *it;
              if(如果屏幕都脏了的标志已经设定)
              {
                     RECT rcBoundsRect = { 0, 0, 0, 0 };

                     if( pObject->GetBoundsRect( rcBoundsRect ) )
                     {
                            //设置屏幕裁减区域

                            pGraphics->SetClipper( &rcBoundsRect );
                     }

                     pObject->OnRender( pGraphics );
              }
              else 
              {

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

                     if( pObject->GetBoundsRect( rcBoundsRect ) )
                     {

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

                            for( int i=0; i<m_nCurrentDirtyRectCount; i++ )
                            {

                                   RECT rcIntersect;

                                   if( ::IntersectRect( &rcIntersect, &m_DirtyRects[i].Range, &rcBoundsRect ) )
                                   {     

                                          //只画交集的部分

                                          pGraphics-> SetClipper ( &m_DirtyRects[i].Range );

                                          pObject->OnRender( pGraphics );
                                   }
                            }
                     }
              }
       }
}

 

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

 


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

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

 

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

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

 

关于测试的 Demo

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

 

关于 Settings.ini

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

LockFPS = 1         // 是否锁定 FPS 0= 关闭, 1= 开启

哈,这个 Demo 在不锁定 FPS, 脏技术开启的的情况下,我的 Duron1.8G CPU,FPS 达到 31500 左右!(没错,是三万一千五百)这个数字吓人吧?如果脏技术未打开,只能在 150 左右,相差 200 倍阿!!!

 

如果 LockFPS 开启,在我机器上( 512M DDR) 30 DEMO,CPU 占用还是为 0 ,哈哈!

 

 

例子下载: http://dev.gameres.com/Program/Visual/2D/DirtyRectDemo.rar

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值