本文基于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 b