用pygame实现网上游戏‘球球情侣‘(用pygame.mask完成碰撞检测)

Posted geng_zhaoying

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用pygame实现网上游戏‘球球情侣‘(用pygame.mask完成碰撞检测)相关的知识,希望对你有一定的参考价值。

网上有个’球球情侣’游戏,游戏中有两个不同颜色的球,玩者首先用鼠标画曲线画出球移动的路径,分别右击两个球,使两球沿曲线移动,如果两球碰到一起,进入下一关。编写很多关是游戏公司的事,这里只编写最简单的一关,说明实现游戏的方法。用pygame编写了’球球情侣’游戏。游戏运行效果如下:

可以看到本程序要实现两个功能,第一,用鼠标画多条曲线,右击球后,球移动,不能再画线。第二,球沿所画曲线向下移动,同时检测两球是否发生碰撞。拖动鼠标画线功能被封装在DrawLines类中。球沿所画曲线向下移动功能被封装在Ball类中。本程序使用pygame.mask完成碰撞检测,使两球沿所画曲线向下移动。
在DrawLines类中,创建一个Surface类实例self.image,和主窗体等宽高,把黑线画在self.image上,然后再把self.image拷贝到窗体上显示。可画多条曲线。当鼠标按下,记录曲线第1点,也是鼠标移到新位置后画线段的起点。鼠标移动,从记录的线段起点到鼠标当前位置画线段,并记录鼠标当前位置为鼠标移到新位置后画线段的起点。鼠标抬起,结束画曲线。实现的基本代码如下,非完整程序,只是说明问题。

white=pygame.Color('white')             	#定义Color类实例为白色,第101行
self.image=pygame.surface.Surface(size,0,32)	#Surface实例,和主窗体尺寸相同,第6行
self.image.fill(white)				#背景色为白色,第7行
self.image.set_colorkey(white)			#背景透明,使在bg上画图似乎是在主窗体上
def drawAline(Event):             	#画线函数,第13行。下句如鼠标左键按下且允许画线
    if Event.type==MOUSEBUTTONDOWN and Event.button==1 and DrawLines.canDraw:
        self.mark=1                          #mark=1表示鼠标被按下,如鼠标移动则画线
        self.start_pos=event.pos           #画线起始位置。下句如鼠标左键抬起且允许画线
    if Event.type==MOUSEBUTTONUP and event.button==1 and DrawLines.canDraw:
        self.mark=0   #表示曲线已完成,mark=0鼠标未抬起标志。下句如鼠标按下移动且允许画线           
    if Event.type==MOUSEMOTION and self.mark==1 and DrawLines.canDraw:              
        pygame.draw.line(self.image,black,self.start_pos,event.pos,10)   #画线段
        self.start_pos=event.pos 		#鼠标移动新位置后,画线段的起始位置
    def draw(self,aSurface):			#把self.image拷贝到窗体显示,第22行。
        aSurface.blit(self.image,(0,0))

球沿所画曲线向下移动功能被封装在Ball类中。右击任意两球中的一个,就要停止画线,被右击的球开始沿曲线移动。为确保球正确沿曲线移动,向下移动的初始位置沿y轴方向不能碰到黑线或红色块,而且距离黑线的距离不大于一次移动距离,即类实例变量self.dy。但是所画的黑线可能不能满足这个条件,因此在球沿曲线移动前,要做一些准备工作。移动前所画黑线可能碰到或没碰到球两种情况,如碰到球,球向上移动直到和黑线距离不大于self.dy,称为状态1,如没碰到球,球向下移动直到和黑线距离不大于self.dy,称为状态2,这两个状态是准备状态,而球自动沿所画曲线移动称为状态3,用self.state记录状态。第43行方法state1or2(self,pos)根据鼠标右击时鼠标的位置pos,先判断是否右击了球,如右击了球,画线结束,再分辨是状态1还是状态2。在方法update()中,根据self.state状态,做不同工作,注意,状态1或状态2结束后,都转为状态3。状态1和2比较好理解,这里重点介绍状态3,如何使球正确沿曲线移动。每次循环(每1帧)执行1次update(),在执行完状态1或2后,进入状态3的第1帧,如上所述,第1帧球一定没碰到黑或红色,第79条语句一定不成立,直接执行第81-82条语句,第83-85条语句判断是否越界。第86-87条语句判断是否碰到黑或红,如没碰到,第2帧执行第79条语句一定不成立,继续执行第81-82条语句,保持x原方向沿曲线下行。如第1帧执行第86-87条语句判断碰到黑或红,可能是+dx,也可能是+dy使球碰到黑或红,无论那种情况,必须执行第87条语句。第2帧执行第79条语句,若没碰到黑或红,说明第1帧是由于+dy使球碰到黑或红,球保持x原方向沿曲线下行;若碰到黑或红,说明第1帧是由于+dx使球碰到黑或红,此时要求球回到上1帧位置,必须反向移动,注意上1帧y方向已-dy,y方向已回到原位。就这样,一帧接着一帧,使球不断运动。
方法collide_color(self)用来判断球是否碰到障碍(第40行),包括红色或黑色。要检测碰撞到那种颜色,要执行第70-72行语句。该方法中碰撞检测是使用pygame.mask,该mask用来记录图形中哪些点颜色是透明的,标记为0,那些点颜色是不透明的,标记为1,在用mask碰撞检测时,只检测不透明点是否发生碰撞,不检测透明点是否发生碰撞。例如Ball类中创建mask语句如下(第30-35行)。

self.image=pygame.surface.Surface((2*radius,2*radius),0,32)
self.image.fill(white) 					#底色为白色
self.image.set_colorkey(white) 				#设置透明色
pygame.draw.circle(self.image,color,(radius,radius),radius)  #画圆
self.mask=pygame.mask.from_surface(self.image) 	#建立mask
self.rect = self.image.get_rect(center=pos)

保存黑线的Surface类实例使用同样方法建立mask(第6-10行),但创建mask必须在画完黑线后(第127行)。有了球和保存黑线的Surface类实例的两个mask,就可以用第41-42行检测碰撞,如返回None,说明没有发生碰撞,如发生碰撞,将返回一个坐标元组,是Ball.mask中发生碰撞的第一个点的坐标,也是保存黑线的Surface类实例的图形的坐标点,因此通过该坐标可以得到该坐标点的颜色值(第70-72行)。也可把判断是否碰到红色和碰到黑色或红色(所有颜色)放到一个方法中,方法定义如下:

def collide_color(self,whatColor='allColor'):
    offset = self.rect.x, self.rect.y
    p=Ball.mask.overlap(self.mask,offset)
    if whatColor=='red' and p!=None:
        return Ball.bg.get_at(p)==pygame.Color('red')
    else:
        return p!=None
#如判断是否碰到所有障碍,这样调用:
self.collide_color()
#如判断是否碰到红色障碍,这样调用:
self.collide_color('red')

mask碰撞详细原理可参考本人博文:pygame.mask原理及使用pygame.mask实现精准碰撞检测。该程序也可使用pygame.mask.from_threshold()方法检测颜色的碰撞,详细原理可参考本人博文:函数pygame.mask.from_threshold()用阈值确定mask碰撞点原理及使用方法。
完整程序如下:

import pygame
from pygame.locals import *
class DrawLines():
    canDraw=True  #是否允许画,无论有多少个类实例,类变量是唯一的,所有类实例共用。使用方法:类名.类变量名
    def __init__(self,size):    
        self.image=pygame.surface.Surface(size, 0, 32)
        self.image.fill(white)
        self.image.set_colorkey(white)        
        pygame.draw.rect(self.image,red,(100,200,300,50),0)
        pygame.draw.rect(self.image,red, (230,10,40,200), 0)               
        self.mark=0              #=0,表示鼠标未按下
        self.start_pos=(0,0)
    def drawAline(self,Event):
        if Event.type==MOUSEBUTTONDOWN and Event.button==1 and DrawLines.canDraw:#如鼠标左键按下且允许画线
            self.mark=1                        #mark=1表示鼠标被按下,如鼠标移动则画线
            self.start_pos=event.pos
        if Event.type==MOUSEBUTTONUP and event.button==1 and DrawLines.canDraw:   #鼠标左键抬起且允许画线
            self.mark=0                         #表示当前曲线已完成,mark=0,鼠标抬起标志            
        if Event.type==MOUSEMOTION and self.mark==1 and DrawLines.canDraw:  #鼠标按下移动且允许画线            
            pygame.draw.line(self.image,black,self.start_pos,event.pos,10)
            self.start_pos=event.pos
    def draw(self,aSurface):                    #把self.image拷贝到窗体显示
        aSurface.blit(self.image,(0,0))
class Ball():      
    stop=False   #类变量,无论有多少个类实例,是唯一的,所有类实例共用。使用方法是:类名.类变量名
    winFailStr=' Press key r replay!'
    mask=None       #保存黑线的Surface类实例的mask
    bg=None         #保存主窗体引用
    def __init__(self,Screen,color, pos, radius):  #参数3为球颜色,参数4为球圆心,参数5为球半径
        self.image=pygame.surface.Surface((2*radius,2*radius), 0, 32)
        self.image.fill(white)
        self.image.set_colorkey(white)
        pygame.draw.circle(self.image,color,(radius,radius),radius)
        self.mask=pygame.mask.from_surface(self.image)
        self.rect = self.image.get_rect(center=pos)        
        self.screen=Screen        
        self.dx=5                               #每帧沿x轴移动距离dx
        self.dy=5                               #每帧沿y轴移动距离dy
        self.state=0                            #=1或2或3。看下面注释。        
    def collide_color(self):                    #判断是否碰到黑线或红色方块
        offset = self.rect.x, self.rect.y#应是球坐标减黑线所在的Surface类实例坐标,但其坐标为(0,0)
        return Ball.mask.overlap(self.mask,offset)!=None    #=None,未发生碰撞
    def state1or2(self,pos):                    #首先判断是否结束画曲线,如结束,判断是state=1或2
        if self.state>0:                        #已完成状态判断,不再判断,返回False原因,见第130行注释
            return False                        #返回False,表示已确定了等级,不必再一次计算
        if self.rect.collidepoint(pos):         #如鼠标点击球结束画曲线
            DrawLines.canDraw=False             #不允许再画曲线            
            if self.collide_color():            #调用实例方法检测是否碰到黑线,画线碰到黑线,为状态1
                self.state=1        
            else:                               #画线未碰到黑线,为状态2
                self.state=2        
            return True                         #如点击球,返回True
        else:
            return False                        #如未点击球,返回False
    def update(self):
        if DrawLines.canDraw:       #如正在画线,不执行该方法
            return
        if self.state==1:           #为状态1,球将沿y轴向上移动,如能使球距黑线<dy,转state=3,出界游戏结束
            if self.collide_color():
                self.rect.centery-=self.dy
                if self.rect.centery<25:                    
                    Ball.stop=True                  #越界,结束程序,输了
                    Ball.winFailStr='You fail!1'+Ball.winFailStr
            else:
                self.state=3
        elif self.state==2:         #为状态2,沿y轴向下移动,如能使球距黑线<dy,转state=3
            if not(self.collide_color()):   #如没有碰到黑色或红色
                self.rect.centery+=self.dy  #球下行
            else:               #到此可能碰到红色,也可能碰到黑色,要检查是否碰到红色,碰到红色游戏结束
                offset = self.rect.x, self.rect.y       #开始检查是否碰到红色,见第41行注解
                p=Ball.mask.overlap(self.mask,offset)   #p是黑线所在的Surface类实例和球产生碰撞第1点坐标            
                if Ball.bg.get_at(p)==red:              #如该坐标点是红色
                    Ball.stop=True    #第1次必须碰到黑色,如第1次碰到红色,可能没画黑线,结束程序,输了
                    Ball.winFailStr='You fail!'+Ball.winFailStr
                else:                       #到此,第1次碰到的是黑色,
                    self.rect.centery-=10   #脱离碰撞
                    self.state=3            #进入状态3
        elif self.state==3:       #为状态3,沿所画曲线移动,x和y坐标都要变,遇到黑或红反向移动,移到边界结束
            if self.collide_color():        #初始设置保证第1次肯定不成立
                self.dx=-self.dx
            self.rect.centerx+=self.dx      #y和x方向增加dy或dx,可能有两种情况,碰到或没碰到黑线
            self.rect.centery+=self.dy
            if self.rect.centerx<24 or self.rect.centerx>475 or self.rect.centery>475:   #越界
                Ball.stop=True              #越界,结束程序,输了
                Ball.winFailStr='You fail!3'+Ball.winFailStr
            if self.collide_color():        #如碰到,y方向退回原位置
                self.rect.centery-=self.dy  #此时有两种可能,碰到,下次x方向反向,否则不反向
    def draw(self):
        self.screen.blit(self.image,self.rect)
def reSet():                                #重玩游戏,调用此方法
    global ballB,ballG,mousePos,rightClick,drawLines,black #全局变量要初始化
    ballB=Ball(screen,blue,(160,125),25)    #放弃旧圆,创建新圆,使圆在初始状态
    ballG=Ball(screen,green,(340,125),25)
    Ball.stop=False
    DrawLines.canDraw=True
    Ball.winFailStr=' Press key r replay!'
    drawLines=DrawLines(size)
    Ball.bg=drawLines.image
    mousePos=(0,0)
    rightClick=False
white=pygame.Color('white')
bgcolor = pygame.Color('cyan')
blue=pygame.Color('blue')
red=pygame.Color('red')
green=pygame.Color('green')
black=pygame.Color('black')
pygame.init()
size = width, height = 500,500
screen = pygame.display.set_mode(size)
pygame.display.set_caption("球球情侣")
reSet()
fclock = pygame.time.Clock()
fps = 20
running = True
font1 = pygame.font.SysFont("arial", 25)
while running:
    screen.fill(bgcolor)    
    for event in pygame.event.get():        
        if event.type == pygame.QUIT:     #是否退出游戏
            running = False               #退出游戏
        if event.type == pygame.KEYUP and event.key == pygame.K_r:     #按r键后,重玩游戏
            reSet()
        drawLines.drawAline(event)                  #用鼠标画线,将曲线的各端点保存到列表
        if event.type==MOUSEBUTTONDOWN and event.button==3:     #鼠标右键按下事件
            mousePos=event.pos                                  #鼠标右键按下事件时坐标
            rightClick=True                                     #鼠标右键按下标志
            Ball.mask=pygame.mask.from_surface(drawLines.image)
    drawLines.draw(screen)    
    if rightClick:
        rightClick=False    #下句,or前方法返回True,就不再执行or后方法,如or前方法已做过判断,要返回False
        if ballB.state1or2(mousePos) or ballG.state1or2(mousePos):  #若右键点击了蓝球或绿球,要结束画线            
            mousePos=(0,0)            
    if not(Ball.stop):
        ballB.update()    
        ballG.update()
    ballB.draw()
    ballG.draw()    
    surface1=font1.render(Ball.winFailStr,True,[255,0,0])  #不能显示中文
    screen.blit(surface1, (10, 470))         #显示输赢字符串
    pygame.display.update()
    if ballB.rect.collidepoint(ballG.rect.center) and not(Ball.stop):#检测1个Rect中心是否在另1Rect中,是返回真    
        Ball.stop=True                                  #显示你赢了,圆停止运动
        Ball.winFailStr='You win!'+Ball.winFailStr
    fclock.tick(fps)
pygame.quit()

以上是关于用pygame实现网上游戏‘球球情侣‘(用pygame.mask完成碰撞检测)的主要内容,如果未能解决你的问题,请参考以下文章

用pygame实现网上游戏‘球球情侣‘(用pygame.mask完成碰撞检测)

用pygame实现网上游戏‘球球情侣‘(检测矩形和某颜色碰撞的例子)

用pygame实现网上游戏‘球球情侣‘(检测矩形和某颜色碰撞的例子)

用pygame实现网上游戏‘球球情侣‘(检测矩形和某颜色碰撞的例子)

Pygame小游戏:死磕《球球版—贪吃蛇蛇》,你中招了嘛?

手把手教你使用Python写贪吃蛇游戏(pygame)