pygame.mask原理及使用pygame.mask实现精准碰撞检测
Posted geng_zhaoying
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了pygame.mask原理及使用pygame.mask实现精准碰撞检测相关的知识,希望对你有一定的参考价值。
通常一个游戏中会有很多角色出现,而这些角色之间的“碰撞”在所难免,例如炮弹是否击中了飞机等。碰撞检测在绝大多数游戏中都是一个必须处理的至关重要的问题。pygame提供了多种碰撞的检测方法,包括矩形碰撞检测、圆形碰撞检测和使用mask的精准碰撞检测。
一:矩形碰撞的缺点。
在游戏中一般用矩形图片来呈现角色,用矩形定义角色(图片)边界和位置,最简单的角色之间碰撞检测是检测两个角色的边界矩形是否存在重叠来完成。pygame.Rect类中有许多检测碰撞的方法,可检测点、矩形等是否在另一矩形中。但矩形碰撞检测方法有很大缺陷,例如两个圆形角色,它们都是边界正方形的内切圆。当两圆圆心y坐标之差略小于两圆半径之和,两圆沿x轴相向而行,两圆还未碰到,一个圆边界正方形右下角就会碰到另一个圆的左上角。编写一个pygame程序使用矩形碰撞检测演示两个圆还未发生碰撞就检测的碰撞的现象。程序没有使用pygame.Rect类中检测矩形碰撞方法。在定义Circle类时以pygame.sprite.Sprite为基类,使用pygame.sprite.collide_rect方法检测矩形碰撞。这样做仅是希望下面说明圆形碰撞时基本使用同一程序,因为圆形碰撞必须从Sprite类派生。完整程序如下。
import pygame
class Circle(pygame.sprite.Sprite):
def __init__(self,pos,color):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((40,40)) #创建1个32X32的Surface实例image
#self.image.set_colorkey((1,2,3)) #设置image中颜色(1,2,3)的颜色为透明色
self.image.fill((1, 2, 3)) #底色为黑色,由于上条,底色变为透明
pygame.draw.circle(self.image,pygame.Color(color),(20,20),15)#在image上画圆,要显示的图形
self.rect = self.image.get_rect(center=pos) #rect是圆形图片外边界,将image移到指定位置
#self.radius=15 #圆的半径
def draw(self,aSurface):
aSurface.blit(self.image,self.rect) #从Sprite派生类不放入Group,类实例显示需调用自定义draw
pygame.init()
screen = pygame.display.set_mode((200,100))
pygame.display.set_caption("图形之间碰撞")
clock = pygame.time.Clock()
circleRed=Circle((30,30),'red') #创建红色Circle实例
circleblue=Circle((100,55),'blue') #创建红色Circle实例
run=True
while run:
screen.fill((255, 255, 255))
for event in pygame.event.get():
if event.type == pygame.QUIT:
run=False
if event.type==pygame.KEYDOWN: #如是键按下事件
if event.key==pygame.K_RIGHT: #键盘右键使圆向右移动
circleblue.rect.x+=5
elif event.key==pygame.K_LEFT: #键盘左键使圆向左移动
circleblue.rect.x-=5
if pygame.sprite.collide_rect(circleRed,circleblue): #矩形碰撞检测
#if pygame.sprite.collide_circle(circleRed,circleblue): #圆形碰撞检测
#if pygame.sprite.collide_circle_ratio(0.75)(circleRed,circleblue): #圆形碰撞检测可改变半径值
screen.fill((220, 220, 220)) #窗体背景为灰色
else:
screen.fill((255, 255, 255)) #窗体背景为白色
circleRed.draw(screen) #显示两个圆
circleblue.draw(screen)
clock.tick(10)
pygame.display.update()
pygame.quit()
程序运行后,可看到两个矩形中都有个圆,窗体背景是白色,见图1。用左移键使两图接近,最后两矩形发生碰撞,窗体背景变灰色,说明发生碰撞,但圆还未碰到一起,见图2。将第7行语句注释去掉,重新运行。将看不到矩形,用左移键使两圆接近,两圆还有一段距离才会碰到,但窗体背景由白色变灰,说明发生碰撞,这显然不合理,见图3。
二:圆形碰撞及其局限性
为解决两个圆形角色碰撞问题,必须采用圆形碰撞检测方法。首先定义以pygame.sprite.Sprite为基类的Circle类,见上边程序第3行。然后使用pygame给出的两个圆形碰撞函数,用来检测两个圆形角色之间的碰撞。两个圆形碰撞函数如下。
pygame.sprite.collide_circle(sprite1,sprite2) #两个参数都是圆形角色类实例
pygame.sprite.collide_circle_ratio(k)(sprite1,sprite2)
第1个方法,其默认检测两个角色边界矩形的外接圆之间碰撞,相碰返回真。但如果Sprite派生类的圆形角色类中有属性名称为radius(第11句),则以半径为radius的圆作为检测碰撞对象。第2个方法和第1个方法相同,但可以用修改k值来缩放被检测碰撞的两个圆半径,k是一个浮点数,1.0是相同的大小,2.0是它的两倍,0.5是它的一半。这对于一些圆形角色类,角色是边界正方形的内切圆,却没有名称是radius的属性时,可令k=内切圆半径/边界矩形的外接圆半径,从而使两个圆形角色边界正方形的内切圆作为检测碰撞对象。注意上边计算k的公式对于所有圆无论大小都是定值,是根号2的倒数,因此可用于一个大圆和一个小圆的碰撞。
仍然使用前边程序验证。首先验证没有属性radius情况,将第7、32行加注释符号#,去掉第33行注释符,运行后,窗体背景是白色,见图1。用左移键使两图接近,矩形未碰到,窗体背景变灰色,见图4,说明发生了碰撞,验证了没有属性radius,默认检测两个角色边界矩形的外接圆之间碰撞。然后验证有属性radius情况,去掉第7、11行注释符,运行,实现了两圆的碰撞,见图5。最后,验证没有属性radius,是用公式2实现了两圆的碰撞,第11、33行加注释符号#,去掉第34行注释符,运行,也实现了两圆的碰撞,k=0.75,计算后的半径略大于实际半径,和图5看不出区别,见图6。
游戏中,很多角色并不是圆形,而是不规则形状,例如动物、人物和各种物体等。使用矩形碰撞和圆形碰撞都无法实现精准碰撞检测。而用pygame.mask可以实现角色之间的精准碰撞检测。
三:pygame.mask原理和使用
为了实现不规则形状角色之间精准碰撞检测,可以使用pygame.mask。2维游戏有很多角色,例如人、动物或物体,一般用包含这些图形(图像)的矩形图片来呈现。pygame中的角色是Surface类实例,该类是对矩形图片的封装,属性image是图片,属性rect是图片边界和位置。矩形图片中除了要显示的图形(图像)外,其余部分的颜色称为底色,底色一般是单一颜色。在屏幕显示角色时,应只显示图形(图像),不显示底色,或者说是使底色透明。图片可看作是1个2维列表记录每点颜色值。Pygame常用两种方法使底色透明,第1种方法是用colorkeys语句使底色颜色为透明,图形本身完全不透明,例如上边程序第7行。另1种方法是图片上所有点的颜色用RGBA表示,A为透明度,A=0是完全透明,=255是完全不透明,A从254到1是越来越透明。用此法可将底色设为完全透明,图形(图像)本身设为完全不透明。无论哪种方法,矩形图片透明底色的所有点可用0标记,图形(图像)本身完全不透明,所有不透明点可用1标记。将这种思想用在碰撞中,Surface实例矩形图片中只有标记为1的完全不透明的所有点参加碰撞检测,Surface实例矩形图片中标记为0的完全透明的所有点不参加碰撞检测。实现的方法就是使用pygame.mask记录一个Surface实例矩形图片中每点的0或1标记,检测碰撞方法根据mask标记数据,仅检测两个Surface实例矩形图片中各自标记为1的所有点之间是否覆盖来判断是否发生了碰撞。mask也是一个2维数组,每一项仅占2进制的1位,和描述Surface实例的图片的2维数组有对应关系。根据mask标记检测碰撞需要2个步骤:
1.Surface实例都要设置底色透明,使用pygame.mask记录所有透明点和不透明点标记
#方法1
surf = pygame.surface.Surface((20,20), 0, 32)
surf.fill(pygame.Color('white')) #底色
surf.set_colorkey(pygame.Color('white')) #底色透明
pygame.draw.circle(surf,red,(10,10),10) #画要显示的图形
rect = surf.get_rect(center=(90,35)) #rect定义图片边界和位置
mask=pygame.mask.from_surface(surf) #mask中透明点标记为0,不透明点标记为1
#方法2
surf1=pygame.image.load('迷宫去底色.png').convert_alpha()
rect1 = background.get_rect()
mask1=pygame.mask.from_surface(surf1)
2.检测上面两个Surface实例surf、surf1碰撞方法
offset=rect1.x-rect.x,rect1.y-rect.y #被减数和减数次序不能交换
#碰撞返回第1个碰撞点在mask(不是mask1)坐标(x,y),注意同样也是surf的图片上碰撞点坐标
if mask.overlap(mask1,offset)!=None: #未碰撞返回None
if mask.overlap_area(mask1,offset)!=0: #返回发生碰撞的点数
mask.overlap_mask(mask1,offset) #返回mask记录surf上所有发生碰撞的点,宽高同surf.rect
编写一个pygame程序使用mask碰撞检测方法检测1个圆和一个圆环发生碰撞的情况。程序运行后背景为白色,如图7。用左移键使圆接近圆环,当圆和圆环发生碰撞,背景色变灰色,如图8;到了圆环内部,因不发生碰撞,背景色恢复为白色,如图9。
程序如下:
import pygame
#class Circle(pygame.sprite.Sprite):
class Circle():
def __init__(self,pos,color,radius,width):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((100,100)) #创建1个32X32的Surface实例image
self.image.set_colorkey((1,2,3)) #设置image中颜色(1,2,3)的颜色为透明色
self.image.fill((1, 2, 3)) #底色为黑色,由于上条,底色变为透明
self.radius=radius
self.width=width
pygame.draw.circle(self.image,pygame.Color(color),(50,50),self.radius,self.width) #在image上画圆
self.rect = self.image.get_rect(center=pos) #rect定义image边界和位置,将image移到指定位置
self.mask= pygame.mask.from_surface(self.image) #创建记录透明点和不透明点的mask
def draw(self,aSurface):
aSurface.blit(self.image,self.rect) #Sprite派生类实例不放入Group,类实例显示需调用自定义draw
pygame.init()
screen = pygame.display.set_mode((200,100))
pygame.display.set_caption("圆和环碰撞")
clock = pygame.time.Clock()
circleRed=Circle((50,50),'red',45,10) #创建红色Circle实例为圆环
circleblue=Circle((150,50),'blue',15,0) #创建红色Circle实例为实心圆
run=True
while run:
screen.fill((255, 255, 255))
for event in pygame.event.get():
if event.type == pygame.QUIT:
run=False
if event.type==pygame.KEYDOWN: #按键后按下事件
if event.key==pygame.K_RIGHT: #使圆向右
circleblue.rect.x+=5
elif event.key==pygame.K_LEFT: #使圆向左
circleblue.rect.x-=5
#if pygame.sprite.collide_mask(circleblue,circleRed):
offset=circleRed.rect.x-circleblue.rect.x,circleRed.rect.y-circleblue.rect.y#被减数和减数次序不能交换
if circleblue.mask.overlap(circleRed.mask, offset)!=None:
screen.fill((200, 200, 200))
else:
screen.fill((255, 255, 255))
circleRed.draw(screen)
circleblue.draw(screen)
clock.tick(10)
pygame.display.update()
pygame.quit()
在实际使用中,如果某类有多个实例,为简化程序设计,会将pygame.sprite.Sprite类作为这个类的基类,相应的判断碰撞的方法也要改变。例如将第2个程序可以这样修改,将第2、34行前注释符去掉,为第3、35和36行增加注释符或删除,运行效果相同。注意第34行语句,返回两个圆的第1个碰撞点在circleblue.mask的坐标(x,y),同样也是circleblue图片上碰撞点坐标,这点很重要,使用这个坐标就可得到circleblue图片上碰撞点颜色。这些论述同样适用于第36行语句。使用Sprite类作为基类的目的,主要是要使用pygame.sprite.Group。检测单个sprite和sprite.group的碰撞2个方法和一个group和group之间碰撞方法如下:
pygame.sprite.spritecollide(aSprite,group,True,collided)-> Sprite_list
pygame.sprite.spritecollideany(aSprite,group,collided)-> group中1个Sprite实例或None
pygame.sprite.groupcollide(group1,group2,dokill1,dokill2,collided=None)->Sprite_dict
第1个方法返回一个列表,包含group中所有和aSprite发生碰撞的Sprite派生类实例,通过返回列表可以得到所有被碰撞的Sprite派生类实例的所有属性。参数3是一个布尔值,如为True,所有发生碰撞的Sprite派生类实例将从group中删除。ollided参数决定采用那种方法判断两个sprite是否发生碰撞,具体选项在下边列出。如忽略collided参数,采用矩形碰撞,在此情况下要求所有精灵必须有一个用来定义角色边界的属性“rect”。参数ollided有以下5种选择,这些选项都对应相应的碰撞方法,除了第2个其余的前边都使用过。
collide_rect, collide_rect_ratio, collide_circle,collide_circle_ratio ,collide_mask
第2个方法是第一个方法的简化版,仅返回group中和aSprite发生碰撞的一个Sprite派生类实例,如无碰撞发生返回None,该方法不能删除group中发生碰撞的Sprite派生类实例。该方法运行时间明显小于第1方法,如仅判断是否发生碰撞,建议使用该法。参数collided意义相同。
第3个方法返回一个字典,字典中包含两个group之间发生碰撞所有Sprite派生类实例,这个字典的键是group1中的所有Sprite派生类实例,值是和键(group1的Sprite派生类实例)发生碰撞的group2中的Sprite派生类实例,没有发生碰撞的键,值则为None。换句话讲,在这个返回字典中,如果键值不为None,说明键(group1的Sprite派生类实例)和group2中的某个Sprite派生类实例发生了碰撞,这个键对应的值是group2中的那个和键发生碰撞的Sprite派生类实例引用,如键对应的值是None,说明该键没和任一角色发生碰撞。dokill1=True将删除group1中发生碰撞的所有Sprite派生类实例。dokill2意义相同。
用一个例子说明Group使用。程序运行后,有10个圆固定不动,每个圆可能是4种颜色中的一种。1个圆随鼠标移动。背景色为白色。当移动的圆碰到固定不动的圆,背景色变为被碰撞圆的颜色,由于碰撞圆的颜色和背景同色,将会看不见。程序运行效果如图10。
完整程序如下。
import pygame
import random
class Circle(pygame.sprite.Sprite):
def __init__(self,pos,color,*grps): #*args表示有多个(个数不定)无名参数
super().__init__(*grps)
self.color=color
self.image = pygame.Surface((32, 32)) #创建1个32X32的Surface实例image
self.image.set_colorkey((1,2,3)) #设置image中颜色(1,2,3)的颜色为透明色
self.image.fill((1, 2, 3)) #底色为黑色,由于上条,底色变为透明
pygame.draw.circle(self.image, pygame.Color(color), (15, 15), 15) #在image上画圆
self.rect = self.image.get_rect(center=pos) #将image移到指定位置
self.mask= pygame.mask.from_surface(self.image)
pygame.init()
screen = pygame.display.set_mode((400, 300))
pygame.display.set_caption("两圆相碰改变背景色")
clock = pygame.time.Clock()
colors = ['green', 'yellow', 'red', 'blue']
objects = pygame.sprite.Group()
for n in range(10): #创建10个圆,位置随机但固定不变,颜色随机
pos = random.randint(20, 380), random.randint(20, 280)
Circle(pos, random.choice(colors), objects) #创建Circle实例并增加到列表objects
player = Circle(pygame.mouse.get_pos(), 'dodgerblue') #创建1个圆,将随鼠标移动和10个圆发生碰撞
run=True
while run:
for e in pygame.event.get():
if e.type == pygame.QUIT:
run=False
player.rect.center=pygame.mouse.get_pos()#player随鼠标移动。下句得到objects中和player碰撞sprite
aSprite=pygame.sprite.spritecollideany(player,objects,pygame.sprite.collide_mask)#无碰撞为None
if aSprite: #如发生碰撞,背景变为被碰撞圆的颜色,碰撞圆的颜色和背景同色,将看不见
screen.fill(pygame.Color(aSprite.color))
else:
screen.fill((245,245,245)) #无碰撞背景色是白色
objects.update()
objects.draw(screen)
screen.blit(player.image,player.rect)
clock.tick(10)
pygame.display.update()
pygame.quit()
以上是关于pygame.mask原理及使用pygame.mask实现精准碰撞检测的主要内容,如果未能解决你的问题,请参考以下文章
pygame.mask原理及使用pygame.mask实现精准碰撞检测
函数pygame.mask.from_threshold()用阈值确定mask碰撞点原理及使用方法
函数pygame.mask.from_threshold()用阈值确定mask碰撞点原理及使用方法
函数pygame.mask.from_threshold()用阈值确定mask碰撞点原理及使用方法