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