网上有很多超人游戏,例如:超人训练、超人锤练、超人磨练、超人考验、超人飞跃、超人冲刺等。一般用火柴人扮演超人,能跑、能跳、能攀峭壁,能力超强,游戏场面宏大,关卡一个比一个难。但由于只是学习编程,精力应放在用pygame实现超人的跑、跳、攀爬等各种动作上,因此仅设置3个关卡,来验证超人动作代码。查看游戏源代码是不可能的,只能是自己玩或看别人玩游戏,想办法用pygame实现看到的动作。这些游戏都是敏捷类游戏,老人可能玩不了,小孩玩坏眼晴。幸好在哔哩哔哩网站上有一个玩“超人训练”游戏的视频,显示了通关的各个动作,根据所看到动作完成pygame“超人训练”的游戏。读者也可对照视频,查看所做游戏是否实现了“超人训练”游戏的大部分动作。该链接如下:https://www.bilibili.com/video/av370211982/ 视频中的超人游戏使用了大障碍图形,在x轴方向障碍图形很长。对于大障碍图形,如每次移动都从大障碍图形取出能充满整个窗口的部分图形作为分障碍图形,那么为了侦测分障碍图形中的障碍,必须为分障碍图形创建mask,这样每移动一次,就要重新创建1次mask,显然不太合理。当然也可将整个大障碍图形作为一个角色,创建大障碍图形的mask,角色移动,实际是大障碍图形的反向移动,该方法优点是在x和y方向可同时移动,实现比较简单,如果关卡不太多,也是一个可行方法,但是如关卡太多,大障碍图形必然很大,这样做可能会影响程序运行速度。还可用传统方法,将大障碍图形分割为多个和窗体等宽的分障碍图形,分障碍图形在y方向可以和窗体等高,这样超人在y方向自己运动。也可象视频中的游戏,分障碍图形比窗体要宽,这样超人在y方向自己也不运动,由分障碍图形在y方向反向移动完成。本例采用分障碍图形在y方向和窗体等高,大障碍图形分割后,所有分障碍图形如下。 共有5个图形,除去开始和结束图形,共有3个关卡。每个分障碍图形由3种颜色组成,白色为底色,设为透明,超人在黑色障碍上行走,超人碰到红色障碍游戏结束。为了使用分障碍图形,必须在程序中用surface类实例保存分障碍图形,得到并保存该surface类实例的侦测黑色的mask和侦测红色的mask,将所有分障碍图形及其两个mask保存到字典bgs中,这里n是键,是分障碍图形号。值是列表,格式为:[障碍图形的surface类实例,该实例的黑色碰撞mask,该实例的红色碰撞mask]。见第135-139行。其中用pygame.mask.from_threshold创建侦测某种颜色mask,原理请参见本人的博文:函数pygame.mask.from_threshold()用阈值确定mask碰撞点原理及使用方法。 超人有多个造型,每个造型都应有自己的mask。如造型比较多,一般将多个造型集中放到一张图片中,换造型时候从大图中取出相应的造型,这时必须为取出造型创建mask,超人每改变一次造型,就要创建1次mask,比较费时。可一次性分割所有造型,或使用独立的造型图片,并为其创建mask,保存到列表中,方便使用。 本程序超人有7个独立的造型图片,都是面向右侧,超人向右侧移动要使用这7个图片作为造型。超人如向左侧移动,要面向左侧,还需要7个造型,从已有的7个造型图片沿y轴翻转得到。因此共计14个造型。每个造型都要有自己的mask。直接使用这些独立的造型图片,为每个造型创建mask,保存到字典mans中。具体代码见第140-149行。7个造型图片如下: 超人要不断进行造型变换。如造型变换前未碰到黑色,造型变换后,必须保证造型仍未碰到黑色。这对于超人造型图形设计有一些要求。超人站立(或卧式)图片要等高宽,超人图形中心点在图片中心,超人图形距图片上(下、左或右)边界距离距离一致,在变换后,变换前后造型中心对齐,使到黑色障碍的距离,造型变换前后基本相同。 在视频中显示的游戏,超人动作包括:向左或向右奔跑、爬行、坐着向下滑行、跳跃和攀爬峭壁。用上下左右和空格键变换不同动作。根据这些,本程序设定这5个键用途,左键:超人造型面向左侧,沿x轴向左侧前进;右键:超人造型面向右侧,沿x轴向右侧前进;向下键:超人趴下准备爬行;向上键:超人坐下准备向下滑行;空格键:跳跃,如碰到峭壁攀爬峭壁。因此可以看出,不能同时按下左键和右键。因为爬行、坐着向下滑行和跳跃都需要前进,因此左(或右)键和向上、向下或空格键中某1个键可同时按下。用变量man_state记录超人运动动状态,man_state=1跑步、=2爬行、=3坐下滑、=4跳跃、=5攀爬。为了同时查看某时刻键盘这5个键是否按下,用语句pygame.key.get_pressed()得到所有键按下状态,并保存到列表key_list中(第162行)。change_state(key_list)用来根据玩家所按下键确定超人当前状态(第45行)。这个方法必须在所有有关超人代码的最前边调用(第165行)。在其后先要调用超人奔跑man_Move(key_list)方法(第166行),因该方法和造型无关,换句话说,无论那种造型都要沿x轴方向前进或后退,并且在这个方法中,还要决定是否碰到峭壁,如遇到峭壁,必须改变造型为攀爬峭壁造型。因此在其后才能调用方法change_model(center,key_list)(第167行)改变造型。最后调用manJumpClimbDown(key_list)方法(第170行),完成跳跃、攀爬或下落功能。 下边看方法man_Move(key_list)方法(第64行),该方法使超人向左或向右奔跑,具体向那个方向奔跑,决定于dx是正数还是负数,在方法change_state中第47-52行根据按下左键还是右键来确定为正还是负,同时确定超人面向左侧还是右侧。超人实际并不真正奔跑,其x坐标保持不变,是由障碍背景反向移动来完成(第67行)。超人在黑色障碍上奔跑,其脚总是保持在黑色障碍的的上方,和黑色障碍并不接触,如果是上坡,前进就可能碰到黑色障碍,因此每前进一步,就要检测是否碰到黑色障碍(第68行),如碰到黑色障碍,超人沿y方向上升dy值,脱离和黑色障碍接触。但也可能碰到峭壁,无论上移多少,总是会碰到黑色障碍。这里涉及到判断超人是否遇到峭壁的判据。本程序设定上坡的坡度>45度就是峭壁。因此第2次判断是否碰到黑色障碍为真后(第70行),就认为是峭壁了,超人恢复到移动前的位置,将峭壁标志is_cliff设为真,令man_state=5,表示造型要改为攀爬。因此,障碍背景中不能有>45度的上坡。如果需要较大坡度,可在第2次判断是否碰到黑色障碍为真后,进行第3次判断为真后,判断为峭壁,这时坡度大约为70多度。这里没有超人走下坡的代码,而是使用公用下行的代码,在方法manJumpClimbDown()中(第78行)。 change_model(center,key_list)方法(第14行)根据man_state值修改超人造型,参数1是变换造型前的造型的中心位置,变换后的造型要保持这个位置不变,前边提到,这对造型设计有所要求。修改造型很简单,需要注意的是修改造型后的这种情况,在变换造型前超人未和黑色障碍发生碰撞,造型转换后却发生碰撞。造型设计要设法避免这种情况发生,此种情况发生了,程序要进行纠正。原始造型图片尺寸较大,被缩小3倍(第143行),实际使用的站立造型尺寸变为宽40高60,卧式尺寸变为宽60高40。如从爬行转到奔跑,奔跑中心点高,爬行中心点低,两者造型中心点距下边界距离分别为30和20,差值为10,转换后跑步双腿一定会碰到黑色,超人最多上行10,就能避免碰到黑色障碍。第35-39行语句能够解决造型从爬行转跑步产生碰撞问题,即使超人上行过多,第94-99行自动下行程序也能使超人距离黑色障碍不大于dy。如果程序循环3次,上行15,超人仍然和黑色发生碰撞,就会执行第40行语句,说明这次转换不是从爬行转奔跑,而是从奔跑转爬行,爬行造型宽度大于奔跑造型宽度,转换后爬行造型比变换前奔跑造型中心点距左(或右)边界距离多出10,宽度增加使超人在左侧,也许是在右侧碰到黑色障碍,超人先右移10,仍然发生碰撞,再左移20,使碰撞消失。 最后介绍manJumpClimbDown(key_list)方法,将完成跳跃、攀爬和下落功能。先介绍跳跃功能。如当前不是正在跳跃,jump_mark=True,表示允许跳跃。玩家按空格键,在change_state方法中,将令man_state=4(第58行)。执行change_model方法后,将令jump_hight=20(第28行),将使超人在20帧内每帧都上行dy,令jump_mark=False(第29行),在完成本次跳跃前,不允许再修改jump_hight。在方法manJumpClimbDown中,如jump_hight>0,超人上行,直到jump_hight=0或碰到黑色障碍。如跳跃时左右键都未按下,跳跃直上直下,如左键或右键按下,超人跳跃同时还向左或右移动。如碰到峭壁(第88行),将变为攀爬造型,按住空格键,超人沿峭壁连续上升,松开空格键,超人沿峭壁连续下降,直到碰到黑色障碍。不是以上两种情况,超人自动下降,直到碰到黑色障碍(第94-99行)。 bump(what_color)方法是检测是否和参数1指定颜色发生碰撞,发生碰撞返回真,否则返回假。因为在窗体中同时存在两个分障碍背景,所以必须检测超人是否和两个分障碍背景中的障碍是否发生碰撞。侦测和颜色发生碰撞,使用pygame.mask.from_threshold创建侦测某种颜色mask方法。 另外当程序运行后,玩家控制超人运动,在超人左上角有个绿色小圆,这是用来调试的。从前面的描述可知,程序设计目的之一是在屏幕显示的超人,不能碰到黑色障碍且距离黑色障碍非常近处,向左或向右奔跑、向上攀爬或沿峭壁落下。但是有各种原因可能达不到这个目的,使超人显示时碰到黑色障碍。由于超人奔跑、攀爬时距离黑色障碍太近,程序员也许不能准确判断在屏幕显示的超人是否碰到黑色障碍。为此在显示超人前(第177行),增加了第171-174行的代码,判断超人是否和黑色发生碰撞,未发生碰撞,超人左上为绿色圆,否则超人左上为蓝色圆,看到蓝色圆,说明和黑色障碍发生碰撞,要检查原因。最后程序应去掉这些代码。 在第130行取出蓝天白云图片保存为background。第157行是将窗体背景设置为白色,但被注释掉。第158行将窗体背景设置为蓝天白云。而障碍背景bg0-bg4的白色都设置为透明色,这样在这些障碍背景的白色部分都能看到蓝天白云。蓝天白云图形并不参加碰撞检测,对程序运行没有影响,只是增加游戏的可观赏性或使游戏看起来更加真实。运行效果如下,为减少GIF文件尺寸,没加蓝天白云背景。 完整程序如下。可能有不合理的地方,欢迎批评指正。
import pygame
def bump(what_color):
global bg_No,man_No,man_rect,bg_x
offset = man_rect.x - bg_x, man_rect.y
offset1= man_rect.x - (bg_x+640), man_rect.y
if what_color=='red':
return bgs[bg_No][2].overlap(mans[man_No][man_face+1],offset) or\
bgs[bg_No+1][2].overlap(mans[man_No][man_face+1],offset1)
else:
return bgs[bg_No][1].overlap(mans[man_No][man_face+1],offset) or\
bgs[bg_No+1][1].overlap(mans[man_No][man_face+1],offset1)
def change_model(center,key_list):
global man_mask,man_image,man_rect,man_No,man_face,man_state,jump_hight,jump_mark,is_cliff
if man_state==1:
if key_list[pygame.K_LEFT] or key_list[pygame.K_RIGHT]:
man_No+=1
if man_No>3:
man_No=0
elif man_No>3:
man_No=0
elif man_state==2:
man_No=4
elif man_state==3:
man_No=5
elif man_state==4 and jump_mark and not(is_cliff):
jump_hight=20
jump_mark=False
elif man_state==5:
man_No=6
man_image=mans[man_No][man_face]
man_mask=mans[man_No][man_face+1]
man_rect=man_image.get_rect(center=center)
for n in range(3):
if bump('black'):
man_rect.centery-=5
else:
return
if bump('black'):
bg_Move(-10)
if bump('black'):
bg_Move(20)
def change_state(key_list):
global man_face,dx,man_state,bg_No,is_cliff
if key_list[pygame.K_LEFT]:
man_face=2
dx=abs(dx)
elif key_list[pygame.K_RIGHT]:
man_face=0
dx=-abs(dx)
if key_list[pygame.K_DOWN]:
man_state=2
elif key_list[pygame.K_UP]:
man_state=3
elif key_list[pygame.K_SPACE]:
man_state=4
elif is_cliff:
man_state=5
else:
man_state=1
def man_Move(key_list):
global dx,dy,is_cliff,man_rect,man_No,man_state
if key_list[pygame.K_LEFT] or key_list[pygame.K_RIGHT]:
bg_Move(dx)
if bump('black'):
man_rect.centery-=dy
if bump('black'):
bg_Move(-dx)
man_rect.centery+=dy
is_cliff=True
man_state=5
else:
is_cliff=False
def manJumpClimbDown(key_list):
global dy,man_state,is_cliff,man_y,jump_mark,jump_hight,man_rect
if jump_hight>0:
man_rect.centery-=dy
if man_rect.y<=0 or bump('black'):
man_rect.centery+=5
jump_hight=0
else:
jump_hight-=1
else:
if key_list[pygame.K_SPACE] and man_rect.y>0 and is_cliff:
man_state=5
change_model(man_rect.center,key_list)
man_rect.centery-=5
if man_rect.y<=0 or bump('black'):
man_rect.centery+=5
else:
man_rect.centery+=5
if bump('black'):
man_rect.centery-=5
jump_mark=True
is_cliff=False
def initialization():
global is_cliff,jump_mark,jump_hight,man_state,bg_x,bg_No,man_No,man_face,bgs,mans,key_list
is_cliff=False
jump_mark=True
jump_hight=0
man_state=1
bg_x=-330
bg_No=0
man_No=0
man_face=0
key_list = pygame.key.get_pressed()
change_model((330,200),key_list)
def bg_Move(dx):
global bg_x,bg_No
bg_x+=dx
if dx<0:
if bg_x<-640:
bg_x+=640
bg_No+=1
else:
if bg_x>0:
bg_x-=640
bg_No-=1
pygame.init()
size = width, height = 640,480
pygame.display.set_caption("检测矩形和颜色的碰撞")
screen = pygame.display.set_mode(size)
background=pygame.image.load("蓝天白云图.png").convert_alpha()
dx=5
dy=5
bgs={}
mans={}
for n in range(5):
bg=pygame.image.load("bg"+str(n)+".png").convert_alpha()
maskBlack=pygame.mask.from_threshold(bg,pygame.Color('black'),(1,1,1,255))
maskRed=pygame.mask.from_threshold(bg,pygame.Color('red'),(1,1,1,255))
bgs[n]=[bg,maskBlack,maskRed]
for m in range(7):
man=pygame.image.load("人"+str(m+1)+".png").convert_alpha()
r=man.get_rect()
man=pygame.transform.scale(man,(int(r.width//3),int(r.height//3)))
man1=pygame.transform.flip(man,True,False)
mask0=pygame.mask.from_surface(man)
mask1=pygame.mask.from_surface(man1)
mans[m]=[man,mask0,man1,mask1]
initialization()
fclock = pygame.time.Clock()
fps = 15
running = True
while running:
if bump('red'):
initialization()
screen.blit(background,(0,0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
key_list = pygame.key.get_pressed()
if key_list[pygame.K_r]:
initialization()
change_state(key_list)
man_Move(key_list)
change_model(man_rect.center,key_list)
screen.blit(bgs[bg_No][0], (bg_x,0))
screen.blit(bgs[bg_No+1][0],(bg_x+640,0))
manJumpClimbDown(key_list)
color1=pygame.Color('green')
if bump('black'):
color1=pygame.Color('blue')
pygame.draw.circle(man_image,color1,(5,5),5)
if man_rect.y<=0:
man_rect.y=1
screen.blit(man_image, man_rect)
pygame.display.update()
fclock.tick(fps)
pygame.quit()
|