优化你的手游:使用脏矩形技术
本文基于2D表現(xiàn)的游戲,在當今3D大行其道的時代,說2D是否顯得格格不入?這個問題我不作討論,因為本人從事的一直都是2D游戲的開發(fā),所以如果你認為討論2D技術(shù)是一個過時的東西就此打住。?
??? 優(yōu)化一直是我在程序中追求的東西之一,想想讓自己的游戲在一個古董機器能流暢的運行或者說在當今的機器上,CPU占用率和內(nèi)存占用率都很低的情況。(畢竟我非常討厭一個游戲獨占了我所有的CPU資源)。?
??? 如果從圖形接口上作優(yōu)化,常用的就是使用3D加速和CPU的特殊指令(雖然說DirectDraw能夠使用2D硬件加速,但大部分機器支持的僅僅是簡單的加速,比如帶ColorKey的支持,連一些稍微高級一點的東西,比如Alpha混合,帶Alpha通道的紋理(表面)都不支持,需要自己寫,優(yōu)化起來還是使用CPU的特殊指令)。雖然說使用3D加速非常簡單,但是它的缺點也非常明顯:對硬件有苛刻的要求,如果僅僅是做2D游戲不推薦使用(新手作為練習寫DEMO而使用倒還可以,我也這樣使用過,呵呵)。使用特殊的CPU指令最常見的就是使用MMX指令了,現(xiàn)在想找到一塊裝了Windows95以上但不支持MMX的CPU都有難度 ~自己花了大半年的時間用MMX高速實現(xiàn)了D3D對2D貼圖的各種特效(帶通道或者不帶通道的紋理,帶BlendColor, 帶縮放旋轉(zhuǎn),做加減法的Alpha混合之類的)之后,雖然發(fā)現(xiàn)可以不使用D3D的東西,但是如果畫面的東西很多的話,在一些內(nèi)存帶寬不高的機器上的速度還是不夠理想,所以還是需要更多的優(yōu)化。這時候我想起了DirtyRect。
什么是臟矩形?簡單的說,就是游戲每次畫面的刷新只更新需要更新的那一塊區(qū)域。Windows本身就是最好的例子。或者說Flash控件,也正是利用了臟矩形技術(shù),所以他的效率才如此的高。傳統(tǒng)的游戲循環(huán)如下:
while( 游戲沒有結(jié)束 )
{
if( 有Windows消息 )
{
處理Windows消息
}
else if( 需要渲染 )
{
清除游戲屏幕的緩沖區(qū)
把游戲中的物體畫到緩沖區(qū)里面
把緩沖區(qū)更新到游戲窗口上
鎖定游戲速度,Sleep一段時間,限制FPS
}
}?
??? 從上面的偽代碼可以看出,每次游戲都要做清除緩沖區(qū)-〉渲染游戲的物體-〉更新到窗口,而基本上我們寫游戲至少要保證最低每秒鐘要刷新24幀以上(一般都在30)。所以上面的代碼每秒鐘要至少24次以上,畫面東西越多,耗費的CPU越多。
不過我們也可以自然的想到,每次那么多東西不一定都需要更新的,比如一個動畫,一般都有一個延遲,比如間隔200毫秒更新一次,那么在這段時間是不需要重新畫的,只有更新了幀以后,表示這個動畫所在的范圍已經(jīng)“臟”了,需要重新畫,這個時候才需要畫這個動畫。而這段時間之內(nèi)我們可以節(jié)約大量的CPU時間,很自然,積少成多,總體下來這個數(shù)值是非常可觀的。再舉一個例子,一個靜止的游戲物體,(比如一棵樹)是永遠都不需要更新的,除非這個樹的位置或者他的屬性發(fā)生了變化。這樣下來我們首先想到的是,每次我們都省略清除后臺緩沖這個步驟,這個非常重要,因為上一次畫下來的東西都在這個緩沖區(qū)里面,如果清除之后就什么都沒有啦~~?
??? 搞明白了這個原理以后,下面來看看具體實現(xiàn)過程中遇到的問題:?
??? 游戲中的物體不會是相互沒有遮擋的,所以如果遇到遮擋的問題怎么辦??
??? 如果游戲中有100個物體,里面的物體相互遮擋關(guān)系總有一個順序,為了簡化問題,只考慮兩個物體遮擋的情況,多個物體的遮擋可以根據(jù)這個來衍生。
??? 考慮上圖,物體B遮擋了物體A, 也就是說渲染順序是先畫A再畫B,這個順序由各自定義,(我自己就喜歡用一棵渲染樹來排序,當然如果你用連表或者其他數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)也沒有問題。)如果物體A的整個區(qū)域都需要更新,那么對于B物體,需要更新的部分也就只有A與B的交集部分(圖中的藍色區(qū)域),在畫B的時候,我們設(shè)置目標裁減區(qū)域(也就是屏幕緩沖的裁減區(qū)域)為這個交集部分,則B在渲染的時候,相當于整個緩沖區(qū)大小就只有藍色區(qū)域那么大,那么裁減函數(shù)將會把B的數(shù)據(jù)區(qū)裁減到相應(yīng)的位置(你實現(xiàn)的圖形函數(shù)中不會沒有做裁減的工作吧???如果沒有實現(xiàn),你就不用看了,直接return算了,不然下面的東西你肯定不明白我說什么)。怎么樣,B物體相當于只畫了藍色區(qū)域這一部分的東西,比整個區(qū)域來說節(jié)約了不少時間吧??
??? 不知道上面說的你明白了沒有,如果沒有明白請多看幾遍,直到弄明白之后再往下看,不然千萬不要往下看。
上面的例子大家肯定會問一個問題,我如何控制B只畫藍色區(qū)域的部分呢?這個問題我暫時不說,等到把所有的遮擋情況說完了再說。繼續(xù)看另外的遮擋情況
??? 上面6個物體A,B,C,D,E,X。X是我們的游戲背景顏色,假設(shè)畫的順序是EADCB,如果E需要重新畫,那很顯然,A,B,C,D不需要做什么?
??? 如果A,D都需要重新畫,那顯然A,D只需要各畫一次。而B需要更新的,不是需要更新BD相交的區(qū)域,而是AB相交的大區(qū)域,也就是說小區(qū)域該忽略掉,如果B需要重新畫,A,D,C需要重新畫嗎?也許有人會說,B畫的次序是在最后的,所以前面的就不需要畫了,對么?答案是錯的,需要重新畫,因為背景緩沖區(qū)我們一般情況下不去清除它,所以談不上畫的順序了。也就是說,A與B相交的部分,A在下次畫的時候也需要更新,D也同樣(想通了嗎?再舉一個例子,如果B含有大量的透明色,如果B需要更新的話,那么B的區(qū)域首先要涂上X作為背景,不然B非透明色如果變成了透明色的話,那B在重新畫的時候,由于透明色不需要畫,那么B上一次留下來的顏色就殘留在X上面,看起來當然不對啦,同理對于A,D也一樣處理)。
上面的理論部分不知道聽明白了沒有,如果不明白的話自己花一點點時間去想象看。假如明白了的話,下面繼續(xù)更加深入的問題。?
??? 從上面的理論解說部分可以看出,臟矩形的選取和優(yōu)化是關(guān)鍵。怎樣得到最優(yōu)化的臟矩形表,就成為了這個技術(shù)優(yōu)化的核心部分。?
??? 為了簡單起見,這里使用的是一個鏈表來管理所有的渲染物體。?
??? 為了實現(xiàn)我們所設(shè)計的手游賬號購買平臺,我設(shè)計了一個非常簡單的類:
class CRenderObject
{
public:
virtual ~CRenderObject(){}
virtual void OnRender( GraphicsDevice*pDevice ) = 0; //所有物體都在這里渲染
virtual void OnUpdate( float TimeStamp ) = 0;//物體更新,比如動畫幀更新拉之類的,在這里面可以設(shè)置DirtyRect標志之類的
virtual bool IsDirty( ) = 0;//是否有臟矩形
virtual bool GetBoundsRect(RECT*pRect) =0;//得到該物體的范圍
virtual int GetDirtyRects ( RECT*pRectBuffer ) = 0;//該物體的臟矩形個數(shù),填充到pRectBuffer里面,返回填充了多少個
...其他函數(shù)
};?
??? 我們還需要一個簡單的能管理臟矩形和渲染物體的類
class CRenderObjectManager
{
pulibc:
void RemoveRenderObject( CRenderObject*pObject );//刪除一個渲染物體
void AddRenderObject( CRenderObject*pObject );//添加一個渲染物體
void Render( GraphicsDevice*pDevice );//渲染所有的物體
void Update( );//更新所有物體
.....其他函數(shù)
protected:
std::list< CRenderObject* > m_RenderObjects;
int m_nCurrentDirtyRectCount;//當前臟矩形數(shù)量
struct DirtyRect
{
RECT Range; //臟矩形范圍
int AreaSize; //臟矩形大小,用來排序
};
BOOL m_bHoleDirty;//是否全部臟了
DirtyRect m_DirtyRects[128];//屏幕上最多的臟矩形數(shù)量,如果大于這個數(shù)量則認為屏幕所有范圍都臟了
};
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++) <br="">{
對于該物體的每一個臟矩形DirtyRectBuffer[i]
如果DirtyRectBuffer[i] 沒有在任何一個已有的臟矩形范圍內(nèi)
那么把這個臟矩形根據(jù)從大到小的順序添加到臟矩形范圍內(nèi),否則忽略這個臟矩形
如果臟矩形數(shù)量已經(jīng)大于設(shè)定的最大臟矩形范圍,設(shè)置所有所有屏幕都臟了的標志,
}
}
}
如果屏幕所有都臟了,填充背景顏色
否則為每一個臟矩形填充背景顏色
}
void CRenderObjectManager::Render( GraphicsDevice* pGraphics)
{
for(std::list< CRenderObject* >::iterator it = m_RenderObjects.begin();
it != m_RenderObjects.end(); it++)
{
CRenderObject*pObject = *it;
if(如果屏幕都臟了的標志已經(jīng)設(shè)定)
{
RECT rcBoundsRect = { 0, 0, 0, 0 };
if( pObject->GetBoundsRect( rcBoundsRect ) )
{
//設(shè)置屏幕裁減區(qū)域
pGraphics->SetClipper( &rcBoundsRect );
}
pObject->OnRender( pGraphics );
}
else
{
RECT rcBoundsRect = { 0, 0, 0, 0 };
if( pObject->GetBoundsRect( rcBoundsRect ) )
{
//如果該物體的范圍與臟矩形緩沖區(qū)的任何一個臟矩形有交集的話
for( int i=0; i<m_ncurrentdirtyrectcount; i++="" )="" <br="">{
RECT rcIntersect;
if( ::IntersectRect( &rcIntersect, &m_DirtyRects[i].Range, &rcBoundsRect ) )
{
//只畫交集的部分
pGraphics-> SetClipper ( &m_DirtyRects[i].Range );
pObject->OnRender( pGraphics );
}
}
}
}
}
}?
??? 好了,核心代碼的偽代碼就在這里,不知道大家看明白沒有,當然我在這里上面實現(xiàn)的這種方法有一個缺陷,最壞情況下一個也許會導(dǎo)致重新畫很多次,如圖的情況:
??? 假設(shè)A是渲染物體,B,C,D,E是由大到小的臟矩形范圍,那么很顯然,重疊的部分就被反復(fù)畫。。。這是在分割臟矩形導(dǎo)致的問題,這樣畫下來,如果A物體是采用了疊加混合到背景的算法的話,問題就出來了,YY號重畫的部分會變得非常亮。所以臟矩形的分割就顯得非常重要,也就是說把這些臟矩形還要分割為互相獨立的互不相交的矩形,至于分割算法嘛,嘿嘿,各位還是動一下腦筋思考思考吧:)這個也是最值得優(yōu)化的地方了,哈哈。實在想不出的話,最簡單的方法就是把彼此相交的臟矩形都做一個合并,得到更大的臟矩形,雖然沒有相交的區(qū)域了,但是也許這個臟矩形會變得比較大了哦:)
最后,大家一定關(guān)心的是我會不會提供源代碼,很抱歉的說,不能。我在我的引擎中實現(xiàn)的不是以簡單的鏈表去做的,用的是一棵比較復(fù)雜的渲染樹,牽扯到的東西就比較多了,所以不方便提供代碼,不過可以給一個演示吧:)再說大家如果真的明白了我所說的,那就可以自己動手寫一下嘛,不要怕失敗/。
??? 好啦,關(guān)于臟矩形的技術(shù)就介紹到這里啦,用好這個技術(shù)你會發(fā)現(xiàn)你的游戲會在配置極低的機器上也能運行如飛的:)這種技術(shù)如果能用在現(xiàn)在市面上的那么多的游戲中的話,就不必為一個小游戲就強占了您100%的CPU資源而煩惱拉:)
如果您有更好的方法或者指出其中的不完善的地方還請您不吝賜教,大家多多交流:)
關(guān)于測試的Demo?
??? 該Demo渲染部分由Kylinx花了近半年的時間,全部采用MMX寫成,已經(jīng)成功實現(xiàn)d3d中對2d紋理的操作,速度非常快
關(guān)于Settings.ini
EnableDirtyRect = 1 //是否允許臟矩形技術(shù),0=關(guān)閉,1=開啟
LockFPS = 1 //是否鎖定FPS,0=關(guān)閉,1=開啟
哈,這個Demo在不鎖定FPS,臟技術(shù)開啟的的情況下,我的Duron1.8G CPU,FPS達到 31500左右!(沒錯,是三萬一千五百)這個數(shù)字嚇人吧?如果臟技術(shù)未打開,只能在150左右,相差200倍阿!!!
如果LockFPS開啟,在我機器上(512M DDR)跑30個DEMO,CPU占用還是為0,哈哈!?
??? 關(guān)于該引擎:演示用的這個引擎(代號ShinyFairy:閃靈,基于前期開發(fā)的GFX3.0系列,該系列已經(jīng)成功運行在某商業(yè)游戲公司的休閑游戲系列),采用Kylinx歷時2年多開發(fā)的具有自主知識產(chǎn)權(quán)的基于2D游戲的超級引擎,強大的數(shù)據(jù)加密,打包,圖形接口同時提供d3d8版本和mmx版本,該演示使用的是mmx版本,適用于各種休閑游戲平臺,或者大型2D RPG/MMORPG均可適用。
總結(jié)
以上是生活随笔為你收集整理的优化你的手游:使用脏矩形技术的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 游戏市场阴影下的手游厂商,和他们无法触碰
- 下一篇: J2ME手机游戏引擎程序结构简述