PyGame中的脏矩形优化技术

目录

问题

脏矩形优化 (Dirty Rectangle)

原理

一、获得屏幕需要重绘的区域

二、在上一帧绘制的区域用对应区域的背景图片覆盖

三、在新的位置上绘制兔子

四、代码实现

半成品游戏中的实践

进一步优化

一些提示


问题

这几天在尝试用pygame写一个小游戏。当我在给游戏加上一个背景图片,游戏最大帧率一下子降低了近一半!原因是我一直采用全屏重绘的方式来更新屏幕图像。这是因为不同于只占屏幕的一小部分其他一些精灵图像,背景图片是铺满整个屏幕的,每次绘制这么大一个图像,自然要增加很大一部分绘制时间。

脏矩形优化 (Dirty Rectangle)

解决方案之一是使用脏矩形优化技术。(不要吐槽这个名字,外国人的思维方式和咱们不太一样)

屏幕图像每一帧和上一帧通常不是完全不同的。一般我们会在游戏中让一个游戏人物移动、让一个炮弹横飞,但是背景图片基本是每一帧都不会改变的,这样的话我们在每一帧里只需要重绘同上一帧相比发生改变的地方就行了,而不是把没有改变的地方也重新画上与上一帧一模一样的东西。只重绘需要重绘的屏幕区域,会让屏幕绘制时间大大缩短。

原理

一、获得屏幕需要重绘的区域

需要重绘哪些区域取决于我们向屏幕缓冲区中的哪些区域更新了图像。

幸好,pygame为我们提供了这样的接口:

pygame.sprite.RenderUpdates

这是一个pygame.sprite.Group的扩展对象,它的draw()方法能返回组内精灵图像的更新区域,那是一个由rect对象组成的list列表,剩下的功能和pygame.sprite.Group无二。

sprite_group = pygame.sprite.RenderUpdates()
sprite_group.add(sprite_1)
sprite_group.add(sprite_2)

while True:
    # other code
    
    # screen 为pygame.display.set_mode()返回的屏幕缓冲区对象
    # 绘制精灵组内精灵 并 返回本次更新区域的列表,列表由rect对象组成
    dirty_rects = sprite_group.draw(screen) 
    
    # 向update方法传入需要重绘区域的列表,即可重绘指定区域
    # 如果不传入参数,则默认重绘整个屏幕
    pygame.display.update(dirty_rects)
    
    # other code

 

当然你也可以不使用pygame提供给你的这个精灵管理类来获取需要重绘的区域,我们也可以使用自己写的类,秩序在更新屏幕缓冲区的时候记录下所有更新区域就好,这么做有时候反而更方便灵活。

 

像上面代码那样获取了dirty_rects直接就把这个列表传入pygame.display.update()重绘这些区域是错误的做法,因为那样会导致下面第二张图这样的情景:

背景图片来自好友伊语的作品
图中是错误做法导致的结果

 

图中我在每次需要重绘的区域画上了红色框框。可以看到,上图中游戏一共更新了7帧,精灵组(sprite_group)中有一个兔子外观的精灵(sprite)。我让兔子精灵往右移动,每一帧会在新的地方绘制兔子的图像,并且屏幕上相应的地方会重绘,但是旧的地方(兔子上一帧的位置)我们并没有更新!在新的一帧里,那些上一帧存在兔子图像而与新一帧兔子图像没有交集的地方应该被绘制上背景图片的内容,而不是像我们现在这样还依旧留存着上一帧剩下的内容。

 

二、在上一帧绘制的区域用对应区域的背景图片覆盖

在下一帧开始绘制的时候,我们不应该直接急着绘制新兔子,而是要先用背景图片的对应区域覆盖掉上一帧的兔子。

三、在新的位置上绘制兔子

然后我们就可以在新的区域上画上兔子了,注意要保存这个新区域,因为下一帧的时候我们要利用本帧这个区域来绘制背景图像覆盖掉本帧的兔子

四、代码实现

伪代码如下:

screen_buffer = {屏幕缓冲区}

rect_old = {空}

whlie True:

        background_slice_image = background_image.slice(area=rect_old)    # 获取上一帧兔子位置区域的背景图片

        screen_buffer.blit(area=rect_old, img=background_slice_image )    # 用那个区域的背景图片覆盖了缓冲区上一帧的兔子

        rect_new = {兔子新位置区域}

        screen_buffer.blit(area=rect_new , img=rabbit_image )    # 在缓冲区新位置上绘制这个兔子

        display.update(rect_old & rect_new)      # 重绘 rect_old 和 rect_new 这两个区域

        rect_old = rect_new    # 现在的 rect_new 就要成为下一帧的 rect_old 了

 

半成品游戏中的实践

这是一个兔子接萝卜的游戏。游戏中暂时有兔子、萝卜这些精灵,由一个精灵组对象管理;还有动画特效管理器对象,负责一些动画特效的绘制;还有房间对象,负责房间背景图和一些背景粒子效果(图中为简陋的雪花)。

 下面这是加上重绘提示框框的效果,可以看到每一帧实际重绘的区域。

 

 下面是按照上面伪代码思路写的游戏代码,这是主循环中的一部分代码,即向缓冲区绘制并汇总所有由于可绘图对象(精灵组、动画管理器、房间)绘制造成的“脏区域”,然后重绘这些“脏区域”。

 

进一步优化

由下图可以看到,一些重绘区域重叠甚至小区域被大区域完全包裹了起来,有些已经重绘的地方再次重绘是不必要的,比如图中所有被黄圈圈划起来的地方(萝卜、雪花、萝卜筐上那个龙动画特效),这些地方只需要按照外层最大的矩形区域重绘一次就行了,而不是像现在这样那些黄圈圈的地方再次重绘一次。

我们需要把那些黄圈圈的区域从dirty_rects(脏矩形汇总列表)中识别出来并删去, 我们需要依次检测dirty_rects中每个rect元素看是不是这个rect代表的区域被其他rect区域包含。

rect对象有一个colliderect()方法,可以检测一个rect矩形是不是被另一个rect矩形完全包含。

如果两个rect矩形大部分区域相交,我们也可以考虑将这两个矩形合并成一个大矩形。rect对象也有对应的方法来检测矩形相交。

脏矩形合并的核心规则如下

  • 如果两个矩形合并后总面积小于两个矩形各自面积之和,则允许合并,并且优先合并相交面积最大的两个矩形。

  • 合并到最后,若上一条规则已经不满足,但是剩余矩形总数量大于3,则强制继续合并。并优先合并面积增加量最小的两个矩形。

具体细节可以参考这篇文章

一些提示

pygame.display.update() 和 pygame.display.flip()

前者可以传入脏矩形列表,后者不可以;后者可以使用OpenGL等技术,可以使用硬件加速。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值