終極優化你的遊戲 —— 使用髒矩形技術

終極優化你的遊戲 —— 使用髒矩形技術

作者:Kylinx

 

 

 

       說明:本文由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高速實現了D3D2D貼圖的各種特效(帶通道或者不帶通道的紋理,帶BlendColor, 帶縮放旋轉,做加減法的Alpha混合之類的)之後,雖然發現可以不使用D3D的東西,但是如果畫面的東西很多的話,在一些內存帶寬不高的機器上的速度還是不夠理想,所以還是需要更多的優化。這時候我想起了DirtyRect

      什麼是髒矩形?簡單的說,就是遊戲每次畫面的刷新只更新需要更新的那一塊區域。Windows本身就是最好的例子。或者說Flash控件,也正是利用了髒矩形技術,所以他的效率才如此的高。傳統的遊戲循環如下:

     while( 遊戲沒有結束 )

      {

           if( Windows消息 )

           {

                 處理Windows消息

           }

           else if( 需要渲染 )

           {

                 清除遊戲屏幕的緩衝區

                 把遊戲中的物體畫到緩衝區裏面

                 把緩衝區更新到遊戲窗口上

                 鎖定遊戲速度,Sleep一段時間,限制FPS

           }

      }

      從上面的僞代碼可以看出,每次遊戲都要做清除緩衝區-〉渲染遊戲的物體-〉更新到窗口,而基本上我們寫遊戲至少要保證最低每秒鐘要刷新24幀以上(一般都在30)。所以上面的代碼每秒鐘要至少24次以上,畫面東西越多,耗費的CPU越多。

      不過我們也可以自然的想到,每次那麼多東西不一定都需要更新的,比如一個動畫,一般都有一個延遲,比如間隔200毫秒更新一次,那麼在這段時間是不需要重新畫的,只有更新了幀以後,表示這個動畫所在的範圍已經“髒”了,需要重新畫,這個時候才需要畫這個動畫。而這段時間之內我們可以節約大量的CPU時間,很自然,積少成多,總體下來這個數值是非常可觀的。再舉一個例子,一個靜止的遊戲物體,(比如一棵樹)是永遠都不需要更新的,除非這個樹的位置或者他的屬性發生了變化。這樣下來我們首先想到的是,每次我們都省略清除後臺緩衝這個步驟,這個非常重要,因爲上一次畫下來的東西都在這個緩衝區裏面,如果清除之後就什麼都沒有啦~~

      搞明白了這個原理以後,下面來看看具體實現過程中遇到的問題:

      遊戲中的物體不會是相互沒有遮擋的,所以如果遇到遮擋的問題怎麼辦?

      如果遊戲中有100個物體,裏面的物體相互遮擋關係總有一個順序,爲了簡化問題,只考慮兩個物體遮擋的情況,多個物體的遮擋可以根據這個來衍生。

      

 物體A

 

 

 

 

物體B

 

 

考慮上圖,物體B遮擋了物體A, 也就是說渲染順序是先畫A再畫B,這個順序由各自定義,(我自己就喜歡用一棵渲染樹來排序,當然如果你用連表或者其他數據結構來實現也沒有問題。)如果物體A的整個區域都需要更新,那麼對於B物體,需要更新的部分也就只有AB的交集部分(圖中的藍色區域),在畫B的時候,我們設置目標裁減區域(也就是屏幕緩衝的裁減區域)爲這個交集部分,則B在渲染的時候,相當於整個緩衝區大小就只有藍色區域那麼大,那麼裁減函數將會把B的數據區裁減到相應的位置(你實現的圖形函數中不會沒有做裁減的工作吧???如果沒有實現,你就不用看了,直接return算了,不然下面的東西你肯定不明白我說什麼)。怎麼樣,B物體相當於只畫了藍色區域這一部分的東西,比整個區域來說節約了不少時間吧?

      不知道上面說的你明白了沒有,如果沒有明白請多看幾遍,直到弄明白之後再往下看,不然千萬不要往下看。

      上面的例子大家肯定會問一個問題,我如何控制B只畫藍色區域的部分呢?這個問題我暫時不說,等到把所有的遮擋情況說完了再說。繼續看另外的遮擋情況

X

 

A

 

          C

 

D

 

E

 

 B

 

上面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畫的次序是在最後的,所以前面的就不需要畫了,對麼?答案是錯的,需要重新畫,因爲背景緩衝區我們一般情況下不去清除它,所以談不上畫的順序了。也就是說,AB相交的部分,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是渲染物體,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左右!(沒錯,是三萬一千五百)這個數字嚇人吧?如果髒技術未打開,只能在150左右,相差200倍阿!!!

 

如果LockFPS開啓,在我機器上(512M DDR)30DEMO,CPU佔用還是爲0,哈哈!

 

關於商業合作:

MSN:[email protected]

Mail:[email protected]

 

關於該引擎:演示用的這個引擎(代號ShinyFairy:閃靈,基於前期開發的GFX3.0系列,該系列已經成功運行在某商業遊戲公司的休閒遊戲系列),採用Kylinx歷時2年多開發的具有自主知識產權的基於2D遊戲的超級引擎,強大的數據加密,打包,圖形接口同時提供d3d8版本和mmx版本,該演示使用的是mmx版本,適用於各種休閒遊戲平臺,或者大型2D RPG/MMORPG均可適用,有意者可聯繫我詳談。

發佈了18 篇原創文章 · 獲贊 5 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章