Python游戏编程(十五)飞机大战
我们将用面向对象的思维写一个飞机大战的小游戏 。
分为几个类:
- 全局设置类:包括一些基本的参数以及音乐、图片文件的路劲。
- 子弹基础类
- 玩家子弹
- 敌人子弹
- 飞机基类
- 玩家飞机基类
- 敌人飞机类
目录
(一)class Settings():
(二)class Bullet:
(三)玩家、敌人子弹类
(四)class Plane:
(五)玩家飞机
(六)敌人飞机
(七)设置方法
(八)主函数
(一)class Settings():
import sys, time, random
import pygame
from pygame.locals import *
import threading
SCREENWIDTH = 512
SCREENHEIGHT = 768
Gray = (128, 128, 128)
# 全局的设置类
class Settings():
def __init__(self):
# 定义游戏窗口大小,为背景图片的一半
self.screen_size = (self.screen_width, self.screen_height) = (SCREENWIDTH, SCREENHEIGHT)
self.bg_color = Gray
self.bg_image = './images/bg2.jpg'
# 不能播放 mp3,所以转成 wav
self.bg_music = './music/01.wav'
self.gameover_image = './images/gameover.jpg'
self.title = '飞机大战'
# 英雄机参数
self.move_step = 5 # 键盘控制的速度
self.hero_style = './images/me.png'
# 敌机参数
self.enemy_speed = 4 # 敌机飞行速度
self.enemy_style_list = ['./images/e0.png', './images/e1.png', './images/e2.png']
# 子弹参数
self.bullet_style = './images/pd.png'
self.bullet_hero_v = 10 # 英雄机子弹速度
self.bullet_enemy_v = 8 # 敌机子弹速度
# 实例化设置对象
settings = Settings()
设置一个全局的类,其中包括一些基本的参数。
最后对于一个类,都需要经过初始化和实例化后才可以发挥作用,类中使用__init__自动初始化,然后创建了一个settings对象,进行类的实例化。
(二)class Bullet:
# 子弹基类
class Bullet:
def __init__(self, screen, x, y):
self.x = x
self.y = y
self.screen = screen
self.image = pygame.image.load(settings.bullet_style)
def __del__(self):
pass
def bulletRect(self):
bulletrect = self.image.get_rect()
bulletrect.topleft = (self.x, self.y)
return bulletrect
def display(self):
self.screen.blit(self.image, (self.x, self.y))
def move(self):
return True
设置子弹的基础,玩家的子弹和敌机的子弹都要从这个子弹基类继承。
当一个Python程序结束之后,Python解释器会自动调用__del__()方法来释放内存;当然我们也可以根据需要自己调用__del__()方法来删除对象,释放占用的内存。因为我们想要提高计算机的效率,暂时用不到的事情可以先不让计算机做。
pygame.image.load()方法用来加载图片。
用blit()方法在一个Surface对象上面绘制一个Surface对象。
最后建立move()方法,调用这个方法返回布尔值,通过布尔值来确定书否移除这个子弹。
(三)玩家、敌人子弹类
玩家子弹类和英雄子弹类都继承子弹类,实现的功能差不多。
# 英雄子弹类
class HeroBullet(Bullet):
def __init__(self, screen, x, y):
super().__init__(screen, x, y)
def move(self):
self.y -= settings.bullet_hero_v
# 判断子弹是否出界
if self.y <= -20:
return True
# 敌机子弹类
class EnemyBullet(Bullet):
def __init__(self, screen, x, y):
super().__init__(screen, x, y)
def move(self):
self.y += settings.bullet_enemy_v
# 判断子弹是否出界
if self.y >= settings.screen_height:
return True
Python在类中可任意通过__init__方法实现对实例中代码实现自动初始化,但是Python本身不会自动执行初始化操作,但是我们可能要用带继承类的其他方法,所以要调用super().__init__()方法来来实现继承类的初始化。
(四)class Plane:
# 飞机基类
class Plane:
def __init__(self, screen, style, geo):
self.screen = screen
self.image = pygame.image.load(style)
self.bullet_list = []
self.x = geo[0]
self.y = geo[1]
self.is_dead = False
self.finished = False
self.bomb_seq = ['4','4','3','3','2','2','1','1']
def __del__(self):
pass
def planeRect(self):
planerect = self.image.get_rect()
planerect.topleft = (self.x, self.y)
return planerect
def display(self):
for b in self.bullet_list:
b.display()
# 回收子弹
if b.move():
self.bullet_list.remove(b)
# 爆炸效果
if self.is_dead:
death_x = self.x
death_y = self.y
death_w = self.image.get_width()
death_h = self.image.get_height()
try:
bomb_image = './images/bomb'+self.bomb_seq.pop()+'.png'
self.image = pygame.image.load(bomb_image)
except:
self.image = pygame.image.load('./images/bomb4.png')
self.finished = True
finally:
x = death_x + (death_w - self.image.get_width())/2
y = death_y + (death_h - self.image.get_height())/2
self.screen.blit(self.image, (x, y))
else:
# 重新绘制飞机
self.screen.blit(self.image, (self.x, self.y))
def fire(self):
self.bullet_list.append(Bullet(self.screen, self.x, self.y))
print(len(self.bullet_list))
def over(self):
#print("Oops: plane over ...")
#del self
return self.finished
screen为屏幕的Surface对象,表示一个矩形图像。
pop()方法:用来删除列表中最后一个元素。
>>> bomb_seq = ['4','4','3','3','2','2','1','1']
>>> bomb_seq.pop()
'1'
>>> bomb_seq
['4', '4', '3', '3', '2', '2', '1']
(五)玩家飞机
# 英雄飞机
class HeroPlane(Plane):
def __init__(self, screen):
# 英雄机初始位置
geo = (200, 600)
super().__init__(screen, settings.hero_style, geo)
self.step = settings.move_step
# 英雄机移动范围
self.limit_left = -(self.image.get_width()/2)+10
self.limit_right = settings.screen_width-self.image.get_width()/2-10
self.limit_top = 5
self.limit_bottom = settings.screen_height-self.image.get_height()
def fire(self):
self.bullet_list.append(HeroBullet(self.screen, self.x+53, self.y))
def move_left(self):
if self.x <= self.limit_left:
pass
else:
self.x -= self.step
def move_right(self):
if self.x >= self.limit_right:
pass
else:
self.x += self.step
def move_up(self):
if self.y <= self.limit_top:
pass
else:
self.y -= self.step
def move_down(self):
if self.y >= self.limit_bottom:
pass
else:
self.y += self.step
其中玩家移动的范围下左右都是由screen_size决定的,但是上方我们直接设置一个值就好了,这里我们设置为5:self.limit_top = 5,意思是玩家飞机最高能飞到5个像素的位置。
然后是英雄飞机左右上下的移动,先判断是否超出边界,如果在边界范围内的话,就进行“移动”,也就是更改相应方向的坐标。
(六)敌人飞机
# 敌机
class EnemyPlane(Plane):
def __init__(self, screen):
geo = (random.choice(range(408)), -75)
enemy_style = settings.enemy_style_list[random.choice(range(3))]
super().__init__(screen, enemy_style, geo)
self.pipe_x = self.image.get_width()/2-1 # 1 for the width of bullet
self.pipe_y = self.image.get_height()
#self.planeRect.topleft = geo
#是否要删除敌机,返回布尔值
def move(self, hero):
self.y += settings.enemy_speed
if self.y > settings.screen_height:
return True
# 飞机的碰撞检测
#使用位置检测碰撞:
#if self.x > hero.x-50 and self.x < hero.x+50 and self.y > hero.y-40 and self.y < hero.y+40:
#使用Rect对象的colliderect()方法:
if self.planeRect().colliderect(hero.planeRect()):
self.is_dead = True
hero.is_dead = True
# 看看我中弹了没
for bo in hero.bullet_list:
if self.planeRect().colliderect(bo.bulletRect()):
#if bo.x > self.x+12 and bo.x < self.x+92 and bo.y < self.y+60 and bo.y > self.y:
hero.bullet_list.remove(bo)
# 爆炸
self.is_dead = True
# 看看英雄机中弹了没
for bo in self.bullet_list:
if hero.planeRect().colliderect(bo.bulletRect()):
#if bo.x > hero.x+25 and bo.x < hero.x+75 and bo.y > hero.y+5 and bo.y < hero.y+50:
self.bullet_list.remove(bo)
hero.is_dead = True
def fire(self):
self.bullet_list.append(EnemyBullet(self.screen, self.x+self.pipe_x, self.y+self.pipe_y))
(七)设置方法
这里写一个事件检测和绘制文字的函数,把功能放到这个函数里面,选哟这两个功能的时候就调用相应的函数就可以了。
def check_event(hero, usedbullet):
#Key event capture and key_control
which = pygame.key.get_pressed()
if which[K_SPACE]:
hero.fire()
usedbullet += 1
print("Space...")
if which[K_LEFT] or which[K_a]:
hero.move_left()
print("Left...")
elif which[K_RIGHT] or which[K_d]:
hero.move_right()
print("Right...")
elif which[K_UP] or which[K_w]:
hero.move_up()
print("Up...")
elif which[K_DOWN] or which[K_s]:
hero.move_down()
print("Down...")
for event in pygame.event.get():
if event.type == MOUSEMOTION:
hero.x = event.pos[0]
hero.y = event.pos[1]
if event.type == QUIT:
print("exit...")
sys.exit()
return usedbullet
#draw score
def drawText(text, font, screen, x, y):
TEXTCOLOR = (0, 0, 0)
textobj = font.render(text, 1, TEXTCOLOR)
textrect = textobj.get_rect()
textrect.topleft = (x, y)
screen.blit(textobj, textrect)
事件检测这里使用的是pygame.key.get_pressed()这个方法,在前面的游戏中有使用pygame.event.get()这个方法,二者之间有一些不同,在这个游戏里面,用这两种事件调用的方法游戏的操作会有显著的区别。
大家可以在上面代码中修改一下,在游戏操作里面体验一下有神么不同。
看看官网中的介绍:
get()方法中:
This will get all the messages and remove them from the queue. If a type or sequence of types is given only those messages will be removed from the queue.
get_pressed()方法中:
Returns a sequence of boolean values representing the state of every key on the keyboard. Use the key constant values to index the array. A True
value means the that button is pressed.
"""
event = pygame.event.get()
if event.type == KEYDOWN:
if event.key == K_SPACE:
hero.fire()
print("Space...")
if event.key == K_LEFT or K_a:
hero.move_left()
if event.key == K_RIGHT or K_d:
hero.move_right()
if event.key == K_UP or K_w:
hero.move_up()
if event.key == K_DOWN or K_s:
hero.move_down()
"""
(八)主函数
def main():
# 初始化 Pygame
pygame.init()
# 创建一个游戏窗口
screen = pygame.display.set_mode(settings.screen_size, 0, 0)
# 设置窗口标题
pygame.display.set_caption(settings.title)
# 在窗口中加载游戏背景
background = pygame.image.load(settings.bg_image)
#设置积分器
font = pygame.font.SysFont(None, 48)
# 背景音乐
pygame.mixer.init()
pygame.mixer.music.load(settings.bg_music)
#pygame.mixer.music.play()
# 创建英雄机
hero = HeroPlane(screen)
play = True
enemylist = []
bg_y = -(settings.screen_height)
usedbullet = 0
score = 0
while True:
#检查退出
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if not pygame.mixer.music.get_busy():
print("play music")
pygame.mixer.music.play()
# 从坐标(0, -768)开始绘图,所以看到的是背景图片的下半部
screen.blit(background, (0, bg_y))
bg_y += 2
if bg_y >= 0:
bg_y = -(settings.screen_height)
if play:
drawText('Score:{}'.format(score),font,screen,256,256)
drawText('Used Bullet:{}'.format(usedbullet),font,screen,256,300)
# 绘制英雄机
hero.display()
# 随机绘制敌机
if random.choice(range(50)) == 10:
enemylist.append(EnemyPlane(screen))
# 遍历敌机并绘制移动
for em in enemylist:
em.display()
# 敌机随机发炮弹
if random.choice(range(50)) == 10:
em.fire()
# 判断敌机是否出界
if em.move(hero):
enemylist.remove(em)
# 判断敌机是否炸毁
if em.over():
score += 1
enemylist.remove(em)
# 英雄机炸毁,游戏结束
if hero.over():
play = False
#pygame.display.flip()
pygame.display.update()
else:
gameover = pygame.image.load(settings.gameover_image)
screen.blit(gameover, ((settings.screen_width-gameover.get_width())/2,
(settings.screen_height-gameover.get_height())/2))
pygame.display.update()
#print("Game Over")
#continue
# 检查按键事件
usedbullet = check_event(hero,usedbullet)
time.sleep(0.04)
# 判断是否为主运行程序,是则调用 main()
if __name__ == '__main__':
main()
对于全局变量报错:UnboundLocalError: local variable ‘usedbullet’ referenced before assignment的解释:
在程序中设置的 sum 属于全局变量,而在函数中没有 sum 的定义,根据python访问局部变量和全局变量的规则:当搜索一个变量的时候,python先从局部作用域开始搜索,如果在局部作用域没有找到那个变量,那样python就在全局变量中找这个变量,如果找不到抛出异常(NAMEERROR或者Unbound-LocalError,这取决于python版本。)
如果内部函数有引用外部函数的同名变量或者全局变量,并且对这个变量有修改.那么python会认为它是一个局部变量,又因为函数中没有sum的定义和赋值,所以报错。
其中if __name__ == ‘__main__‘:是只作为脚本执行时,执行main()函数,如果作为包导入其他脚本中时,则不执行。
这个游戏修改了很多天,对于初学者,需要理解面向对象思维的编程特点。当然如果按照之前的面向过程的思路来写的话,代码可能会短一些,但是修改起来就会有很大的问题,这也是面向对象和面型过程二者之间的区别,我会在下一个专栏中介绍这些,期待自己学习的进步吧!
这个飞机大战的游戏还是未完成的状态,我们可以在以上代码中进行一些修改,为这个游戏增加一些功能,让这个游戏变得更好玩,当然,在我们增加功能的过程中,也是对Python编程和pygame模块的更深入的理解。
参考:
- http://c.biancheng.net/view/2371.html
- https://www.runoob.com/w3cnote/python-unboundlocalerror.html
- https://gitee.com/luhuadong/Python_Learning/repository/archive/master.zip
还没有评论,来说两句吧...