网上有很多篮球类游戏,这里使用pygame编写了一个。游戏中有一个投篮手,一个防守者。投篮手运球避开防守,跳起投篮,投中得一分。投篮手离篮筐越近,投篮准确率越高,但离篮筐越近,越可能碰到防守者,如碰到,游戏结束。下边是游戏的效果图。
游戏背景是篮球场,如上图。有3个角色,投篮手、防守者和篮球各1个,分别用类来定义。角色动画造型采用火柴人,这是因为火柴人造型容易找到,即使自己画一个也不难。可惜本人画图能力太差,只能从一段视频中一帧一帧抠出造型。所有图形背景都要设置为透明。3个角色所有图形如下:
投篮手运球有4个黑色造型,帧号0-3,当按空格键将启动跳投,将帧号修改为4,跳投有12个黑色造型,帧号4-15,所有造型用来实现投篮手运球和跳投动画。在前1篇博文“篮球游戏中的运球_用列表保存每帧图片”中,可以看到在每秒4帧图形情况下,4个造型就可以完成运球动画,注意球也是造型的一部分。但在每秒4帧图形情况下,跳投动画仅用4帧图形是不可能的,试了一下,每秒4帧,感觉要用12帧图形,即跳投从起跳到再落下需用12*\0.25秒=3秒时间,才有满意的动画效果。跳投的12个图形有有点凑合事,但还是能看到动画效果的,如把12个图形仔细画一下,可能效果更好。注意,跳投的第4、5和6帧中图形是有篮球的,后边帧无篮球,在适当时候(本例在第8帧),篮球角色将出现在投篮者上方,以此为起点向篮板运动,在第13帧碰到篮板,第14和15帧球向下运动,如投中,直接掉到蓝中,如未投中,从侧方向下掉。篮球只有一个图形。
首先要解决的问题是那种情况投篮能中,那种情况投篮不中。网上投篮游戏投中规则五花八门。本游戏规则是:距离篮筐越远投篮越不准,在某一点投篮,那次投中,那次投不中无规律,或者说是随机的,但投中概率是定值。设从投篮点到篮筐距离为L,令n=(L//100)取整数,使用随机数发生器产生1到n+1之间随机整数。规定随机数为1,投中,其它随机整数投不中。如点到篮筐架距离Y<100,n=0,投中率为100%;如200>L>99,n=1,投中率为50%;如300>L>199,n=2,投中率为33%,等等。
防守者只有2个黄色造型,似乎少了点,在本例勉强够用。当防守者和投篮手距离大于200,防守者退到防守初始位置。小于200,而且投篮手正在运球,防守者逼近防守,即防守者面向投篮手移动若干距离(应为整数),如碰到投篮手,游戏结束。所谓面向,即防守者沿防守者和投篮手连线移动。这样,这个连线L,以及防守者和投篮手两点沿x、y轴的差值dx和dy组成一个直角三角形。如防守者每帧沿x轴移动dx1,沿y轴移动dy1,只有dx1/dy1=dx/dy,就保证防守者面向投篮手移动。可令dy1=7,则dx1=7dx/dy。但dy为0,要产生被0除错误。为避免此种情况,可先判断dx和dy大小,如dx<dy,采用上式,否则,dx1=7,dy1=7dy/dx。注意dx和dy是不可能同时为0的,因同时为0前,投篮手和防守者就会接近,发生碰撞,导致游戏结束。这样做,不同方向移动的距离可能不同,上例中,最大值为两直角边都为7,距离为9.899,只能取整数为:9,如较小直角边为0,则距离为7。另外,沿x、y轴增量都只能是整数,因此方向也有误差。
以下是游戏全部程序,做了详细注解,应该较容易读懂程序。水平有限,不免有考虑不周之处,欢迎批评指正。仅拷贝源程序不能正确运行,需要制作投篮手和防守者全部造型,球造型,背景篮球场,放到源程序所在文件夹中。将把源程序和所有图像打包上传,有需要的读者可下载。玩网络游戏时,由于投篮者随鼠标移动,在打开程序或重玩游戏时,必须把鼠标移到程序窗口外边界,避免程序一开始就和防守者发生碰撞,导致游戏结束。
import pygame import math import random import os class Ball(): #篮球类 def __init__(self,screen): #screen是游戏主窗体,Surface类实例 self.screen=screen b=pygame.image.load('b.png').convert_alpha() #得到篮球图形 r=b.get_rect() self.p=pygame.transform.scale(b,(r.width//2,r.height//2)) #缩小图形 self.x,self.y,self.xi,self.yi=0,0,0,0#(x,y)篮球坐标,(xi,yi)是篮球两个位置间增量 self.frameNum=9 #篮球帧编号(1-8),=9,篮球不可见 self.mark=0 #此次投篮中否,=0不中,=1中 self.score=0 #投篮投中次数(得分) def draw(self): #主程序调用,实现篮球动画 if self.frameNum==9: #篮球帧编号=9,篮球不可见 return if self.frameNum==1: #第1帧计算必要数据,下句坐标(self.x,self.y)是球运行起点 dx,dy=(400-self.x),(40-self.y) #坐标(400,530)点是球碰到篮板上的点 self.xi=dx//6 #篮球从起始点到篮板每帧沿x轴前进的增量 self.yi=dy//6 #篮球从起始点到篮板每帧沿y轴前进的增量 dist=math.sqrt((dx**2)+(dy**2)) #投篮点距离篮板距离 n=int(dist//100) #除数越小,总投中率越低 if random.randint(1,n+1)==1: #随机数为1投中,n+1避免dist<100为0 self.mark=1 #投中标记为1 else: self.mark=0 #投不中为0 if self.frameNum>=1 and self.frameNum<6: #从第1帧到第5帧,球按此法前进 self.x+=self.xi #篮球每帧沿x轴增加1个增量值 self.y+=self.yi #篮球每帧沿y轴增加1个增量值 self.frameNum+=1 elif self.frameNum==6: #此帧球将碰到篮板,要准确控制碰到篮板的落点 self.x=400 #球碰到篮板的x坐标 self.frameNum+=1 if self.mark==1: #投中,篮球落点y轴方向靠近篮筐 self.y=90 else: #投不中,篮球落点y轴方向离篮筐较远 self.y=70 else: #篮球下落的两个点,即第7,8帧 if self.mark==0: #球未投中,球除下落,还沿x轴方向移动,球从篮筐两侧落下 if self.xi>=0: #如球从左到右,最后两帧,球沿x轴方向继续从左向右移动 self.x+=30 else: self.x-=30 #否则最后两帧,球沿x轴方向继续从右向左移动 self.y+=25 #如投中x坐标不变,即球直接下落穿过篮筐 self.frameNum+=1 self.screen.blit(self.p, (self.x, self.y)) #在屏幕指定位置绘制篮球 if self.frameNum==9 and self.mark==1: #球所有动作完成,判断得分是否加1 self.score+=1 class Guard(): #防守者类 def __init__(self,screen): ##screen是游戏主窗体,Surface类实例 self.screen=screen self.images=[] for n in range(2): #将2帧图像保存到列表中 p = pygame.image.load(str(n+16)+'.png').convert_alpha()#文件名为16.png,17.png r=p.get_rect() p = pygame.transform.scale(p, (r.width//6, r.height//6)) #调整图像的大小 self.images.append(p) self.frameNum=0 #帧编号,0-1 self.x,self.y=400,300 #防守运动员在窗体的初始坐标 self.PlayerX,self.PlayerY=0,0 #此时投篮手坐标 self.PlayerFrameNum=0 #此时投篮手帧号 self.rect=None#调用blit绘制图形,返回rect记录图形在screen坐标和图形宽和高,用来检测碰撞 def draw(self): #主程序调用,实现防守者动画 p=self.images[self.frameNum] #取出当前帧图形 if self.PlayerX-self.x<0: #面向投篮手 p=pygame.transform.flip(p,True,False) dx,dy=self.PlayerX-self.x,self.PlayerY-self.y #防守者和投篮手两点沿x、y轴的差值dx和dy dist=math.sqrt((dx**2)+(dy**2)) #计算投篮手和防守者距离 dx1,dy1=0,0 #防守者每帧沿x轴移动dx1,沿y轴移动dy1 if dist>200: #如距投篮者>200,返回初始点 self.x,self.y=400,300 elif self.PlayerFrameNum<4: #如投篮者未投篮,逼近投篮者,如投篮者投篮,防守者位置不变 if abs(dx)<abs(dy): #保证abs(dy)不为0,使下句dx/dy一定不会被0除 d=abs(dx/dy) dy1=7 #如矩形长边为7, dx1=int((dy1*d)//1) #dx1可能是:0,1,2,3,4,5,6,7 else: #保证abs(dx)不为0,使下句dy/dx一定不会被0除 d=abs(dy/dx) dx1=7 #如矩形长边为7, dy1=int((dx1*d)//1) #dy1可能是:0,1,2,3,4,5,6,7 if dx<0: #得到dx的正负号 dx1=-dx1 if dy<0: #得到dy的正负号 dy1=-dy1 self.x+=dx1 #防守者移动 self.y+=dy1#下句返回rect用来检测碰撞,其属性x,y是图形在游戏窗口坐标,width,hight是图形宽和高 self.rect=self.screen.blit(p,(self.x,self.y)) #在屏幕指定位置绘制防守者 self.frameNum+=1 if self.frameNum==2: self.frameNum=0 class Player(): #投篮手类 def __init__(self,screen): #screen是游戏主窗体,Surface类实例 self.screen=screen self.images=[] for n in range(16): #将16帧图像(包括运球和跳投图像)保存到列表中 p = pygame.image.load(str(n)+'.png').convert_alpha()#文件名为1.png,2.png... r=p.get_rect() p = pygame.transform.scale(p, (r.width//6, r.height//6)) #调整图像的大小 self.images.append(p) self.frameNum=0 #帧编号,运球为0到3,跳投为4到15 self.x,self.y=0,0 #图像在窗体的坐标 self.mouseX,self.mouseY=0,0 #此时鼠标坐标值 self.jumpUpOrDown=-10 #按空格键后投篮手向上跳,初始值为负数。到最高点后下落,为正数 self.rect=None#调用blit绘制图形,返回rect记录图形在screen坐标和图形宽和高,用来检测碰撞 def dribble(self): #运球动画 p=self.images[self.frameNum] if self.mouseX-self.x<0: #面向鼠标 p=pygame.transform.flip(p,True,False) self.x,self.y=self.mouseX,self.mouseY #投篮手坐标=鼠标坐标 if self.x<1: #控制投篮手必须在篮球场中 self.x=1 if self.x+90>width: self.x=width-90 if self.y<230: self.y=230 if self.y+120>height: self.y=height-120 self.rect=self.screen.blit(p,(self.x,self.y)) #在指定位置绘制图形,返回rect self.frameNum+=1 if self.frameNum==4: self.frameNum=0 def jumpShot(self): #跳投动画 p=self.images[self.frameNum] if self.x>width/2: #面向篮板 p=pygame.transform.flip(p,True,False) self.screen.blit(p, (self.x, self.y)) #跳投初始位置是运球转跳投时位置 self.y+=self.jumpUpOrDown #以后先向上(y值减少),到最高点后下降 self.frameNum+=1 if self.frameNum==9: #开始下落,下落值为正 self.jumpUpOrDown=10 if self.frameNum==16: #=16,跳起投篮结束,转运球 self.frameNum=0 self.jumpUpOrDown=-10 pygame.init() os.environ['SDL_VIDEO_WINDOW_POS']="%d,%d"%(200,40) #游戏窗口距左侧和顶部点数为200,40 size = width, height = 800,600 #创建游戏窗口大小 screen = pygame.display.set_mode(size) pygame.display.set_caption("投手运球和跳投") #设置窗口标题 bg_img = pygame.image.load("篮球场1.png").convert() #背景篮球场图像 fclock = pygame.time.Clock() #创建控制频率的clock fps = 4 #定义刷新频率 player=Player(screen) #投篮手类实例 ball=Ball(screen) #篮球类实例 guard=Guard(screen) #防守者类实例 font1 = pygame.font.SysFont('宋体', 50, True) #创建字体 gameOver=False #该次游戏是否结束,初始不结束 running = True #程序是否结束,初始运行 while running: for event in pygame.event.get(): if event.type == pygame.QUIT: #处理退出事件 running = False #程序结束 if event.type == pygame.MOUSEMOTION: #鼠标移动事件 player.mouseX,player.mouseY=event.pos #将鼠标位置传递给投篮手用于运球 if event.type == pygame.KEYUP: #按键后抬起事件,避免长按键不抬起 if event.key == pygame.K_SPACE: #按空格键后抬起 if player.frameNum<4: #如在运球状态,转投篮状态 player.frameNum=4 #已在投篮状态不处理 if event.key == pygame.K_r and gameOver==True: #按r键后抬起,重玩游戏 gameOver=False ball.score=0 screen.blit(bg_img, (0, 0)) #绘制篮球场背景 surface1=font1.render('score:'+str(ball.score),True,[255,0,0]) #不能显示中文 screen.blit(surface1, (20, 20)) #显示进球数(得分) if gameOver==True: #如果该次游戏结束,后边程序不再执行 fclock.tick(fps) #fps是每秒多少帧,减去程序运行时间,为实现fps,还需延迟时间 continue if player.frameNum>=4: #如果投篮手帧号>=4,投篮手正在跳投 player.jumpShot() if player.frameNum==8: #第8帧跳起手中无球,篮球要出现并开始向篮板运动 ball.frameNum=1 #球向篮板运动第1帧 ball.x=player.x #球向篮板运动的起始位置 ball.y=player.y else: #如果投篮手帧号<4,投篮手正在运球 player.dribble() ball.draw() #篮球动画 guard.PlayerX,guard.PlayerY=player.x,player.y #将投篮手位置传递给防守者 guard.PlayerFrameNum=player.frameNum #将投篮手帧号传递给防守者 guard.draw() #防守运动员动画 if player.frameNum<4: #仅在投篮手运球时,判断和防守者是否发生碰撞 if player.rect.colliderect(guard.rect): #检测投篮者和防守者是否发生碰撞 gameOver=True #发生碰撞,游戏结束 surface2=font1.render('if play again,press key r',True,[255,0,0]) screen.blit(surface2, (20, 100)) #显示如继续玩,按r键 pygame.display.flip() #刷新游戏场景 fclock.tick(fps) #fps是每秒多少帧,减去程序运行时间,为实现fps,还需延迟时间 pygame.quit()