[翻转后台缓冲区和屏幕缓冲区]
}
对于2D游戏或UI系统来说,渲染流程主要就分为以上2个阶段。(这里假设游戏是全屏独占的,对于非全屏独占情况类似,但底层实现有较大差别)
事实上,第一阶段和第二阶段均可以被脏矩形算法优化,并且脏矩形算法必须要求double buffer支持。
1. 使用脏矩形算法优化第二阶段:
翻转前后台缓冲区,有两种方法。一种采用把内存数据直接Blt到显存中,另一种则是采用页面翻转。
之所以不采用页面翻转的原因在于:不是所有适配器支持页面翻转,且页面翻转需要渲染代码直接对VGA硬件显存读写,而VGA硬件总线一般会比较慢。
如果我们把物体绘制到后台缓冲(内存),然后再通过脏矩形Blt到显存,则可以大幅降低对VGA内存的访问。
算法流程(假设背景不变):
设置前台缓冲区背景(因为背景不变)
DirtyList初始化为空
while(1)
{
//逻辑代码:
....
//渲染代码:
后台缓冲区清屏,并绘制后台缓冲区背景
绘制后台缓冲区物体
Add每个物体的矩形到DirtyList,同时对DirtyList进行切割合并
把后台缓冲区中对应的脏矩形部分Blt到前台缓冲
清空DirtyList,并按按当前物体重建DirtyList
}
以上算法的缺点是,每次必须清空整个后台缓冲区然后重新绘制背景和所有物体。
但我们已假定背景不变,其实只要重画所有物体即可(不需要重新画背景。注:这里并不关心物体本身是否是脏的),于是得到进一步的优化:
设置前台缓冲区背景
设置后台缓冲区背景
DirtyList初始化为空
while(1)
{
//逻辑代码:
....
//渲染代码:
Add每个物体的矩形到DirtyList,同时对DirtyList进行切割合并
把这个DirtyList中的所有矩形作为后台缓存的剪裁区域
后台缓冲区清屏,并绘制后台缓冲区背景(实际上只操作了剪裁区域)
绘制后台缓冲区物体(实际上只操作了剪裁区域)
把后台缓冲区中对应的脏矩形部分Blt到前台缓冲
清空DirtyList,并按按当前物体重建DirtyList
}
以上算法,在Android里的实现方式如下:
public void run() {
int frameCount = 0;
Rect dirtyRect = new Rect(0,0,0,0);
while(1){
//逻辑代码:
//渲染代码:
if(frameCount++==0){
canvas = Holder.lockCanvas(); //设置全屏刷新,相当于设置脏矩形区域为整个屏幕
}else{
把所有物体的矩形加入dirtyRect,得到并集 //注:Android API并不支持脏矩形链,但其实是可以用脏矩形链实现不规则的dirtyRects的
canvas = Holder.lockCanvas(dirtyRect); //设置部分刷新
}
...//清空并绘制背景
...//绘制所有物体
dirtyRect.set(0,0,0,0); //清空矩形
把所有物体的矩形加入dirtyRect,重建dirtyRect
Holder.unlockCanvasAndPost(canvas);
}
}
注1:一般来说,游戏的背景不会一成不变,但很少每一帧都在变,在这种我们只要在背景变化前重设脏矩形区域为整个屏幕区域即可。
注2:把所有物体的矩形加入dirtyRect,重建dirtyRect 这一步也可以放到下一帧的逻辑代码前面。(因为只有在逻辑代码里修改物体状态才会导致脏矩形改变)
注3:Android里Holder.lockCanvas(dirtyRect)的意思是说,后面的绘图操作使用dirtyRect作为clip区域,并且在最后swap前后台缓冲时只刷新dirtyRect区域,并且刷新完了之后下一帧lock到的Canvas上的buffer数据除了dirtyRect以外都是上一帧画完时的数据。(dirtyRect因为本来就要重画,所以这块区域Android不会保存)
2. 使用脏矩形算法优化第一阶段:
在(1.)中的算法必须要重画所有物体,并且每帧的脏区的总和涉及了至少所有的物体(节省了第二阶段的时间以及第一阶段中绘制背景的时间)。而事实上游戏中的物体大多都是不变的,比如:TileGame中的树木、房子等,
在这种情况下,我们希望这些物体只绘制到后台缓存一次,以后除非物体状态被改变(或者与该物体重叠的区域上的物体状态被改变),否则就不再把这些物体再次绘制到后台缓存上。
由于一个物体是否要绘制到后台缓存取决于逻辑代码(物体状态),比如:游戏的上层逻辑 (游戏开发者)调用引擎的API接口去控制物体的位置、大小、动画帧索引,那么游戏的下层逻辑(引擎)就需要在这些API的实现里标记这些物体是脏的(MarkDirty)。
MarkDirty()函数:
i. 标记该物体的是脏的(mDirty=true),
ii. 把该物体的矩形加入DirtyList
iii. 检查是否有重叠的物体并标记这些重叠物体是脏的(但不需要care这些重叠区域,也不要扩大脏矩形区域)
iv. 对DirtyList进行切割合并。
注意:物体状态的改变主要分两种:一种只是Sprite本身图片改变,如:按钮被按下(image),另一种是位置、大小或可见性等属性改变(resize),这两种最好分开处理。
public void run() {
初始化1个背景单位到ObjectList,并call MarkDirty() //背景可以作为一个物体处理
初始化9个单位到ObjectList,并call MarkDirty() //ObjectList是Z-Order排序的
Rect dirtyRect = new Rect(0,0,0,0);
while(1)
{
//逻辑代码(内部可能处理InputEvent):
for(all Object in ObjectList){
thisObject.Update();
//Update函数内部:
// if(某个物体只是image需要改变){
// thisObject.MarkDirty();
// }else if(某个物体resize了){
// thisObject.MarkDirty();
// thisObject.scale = xxx;
// thisObject.position = xxx;
// thisObject.image = xxx;
// thisObject.MarkDirty();
// }else{