也许你已阅读过许多版本的pygame飞机大战游戏源代码,但我相信通过比较这个版本的程序与类似游戏代码的一些细节方面的差别,或许可以加深对游戏架构及pygame框架的理解。在写这个程序之前,我也阅读过多个版本的飞机大战游戏代码,最后总结了一个我认为较为合理的架构。这个版本的代码的特点是:使用一个全局函数显示游戏开始页面,接收到开始游戏的信号(按下s键)后切换到游戏页面,不同精灵的代码结构非常类似,事件处理代码集中在主程序类和玩家类中。在主程序类处理事件循环的while结构中,增加了一个判断循环是否结束的if语句,在我读到的pygame飞机大战代码里都没有这种写法,但很久以前我学习c++和java游戏编程都用过这种做法,在pygame利用起来减少了很多我读过的pygame飞机大战游戏代码中会出现的Surface已关闭的bug(当然,在调用pygame.quit()后立即调用sys.exit()能够更加立竿见影地消除这个常见bug)。此外,为了便于维护代码,这个程序采用了将游戏用到的各个类单独编写成文件的方式组织代码(顺便记录一下:以这种方式组织代码,使用pycharm作为编程工具时,如果编辑器在导入本地文件中的类时报错,那么需要右键点击被导入文件所在文件夹,将该文件夹标记为源文件夹)。以下是主程序的代码,代码中尽量用详尽的注释来说明代码的功能:
import os
import sys
import pygame
from settings import Settings
from player import Player
from rock import Rock
from explosion import Explosion
screen_width = 500
screen_height = 600
# pygame初始化。初始化代码强烈建议不要包含在任何代码块中,而放在程序开头
pygame.init()
# 声音系统初始化,不播放声音则不需执行
pygame.mixer.pre_init(44100, -16, 2, 2048)
pygame.mixer.init()
# 设置显示模式,只有设置了显示模式,才能使用pygame.image.load()创建表面
screen = pygame.display.set_mode((screen_width, screen_height))
bg_ground = pygame.image.load(os.path.join('imgs', 'space.png')).convert()
icon = pygame.image.load(os.path.join('imgs', 'plane_icon.png')).convert_alpha()
# 设置窗体图标及标题
pygame.display.set_icon(icon)
pygame.display.set_caption("太空大战")
# 为了保证在其他电脑上也可以准确显示字体,可以将程序用到的字体拷贝至程序文件夹
font_zh = os.path.join('fonts', '硬笔行书.ttf')
# 使用系统里已有的字体,这会导致代码在没有安装相关字体的电脑上运行失败
font_en = pygame.font.match_font('Source Code Pro')
'''游戏主程序类'''
class Game:
def __init__(self):
# 载入预设数据,为了使代码脉络清晰,预设常数和要用到的图片与声音资源均保存到一个单独的文件中
self.settings = Settings(screen.get_width(), screen.get_height())
self.screen = screen
# 将代表玩家的飞机图片缩小至1/3,程序开始后画在右上角表示剩余的游戏机会次数
self.player_img_small_width = int(self.settings.plane_image.get_width() / 3)
player_img_samll_height = int(self.settings.plane_image.get_height() / 3)
self.player_img_small = pygame.transform.smoothscale(self.settings.plane_image,
(self.player_img_small_width, player_img_samll_height))
# 主事件循环的标志
self.running = True
# 上一次执行主事件循环的时间,记录下来便于以基本固定的频率执行主事件循环
self.last_loop_time = 0
# 玩家
self.player = Player(self.settings, self.screen)
self.player_score = 0
# 岩石列表,这些岩石可以旋转,并以随机的水平和竖直速度运动,撞击玩家后玩家血量减少
self.rocks = pygame.sprite.Group()
# 按照预设的岩石数量生成岩石
for i in range(self.settings.rocks_number):
rock = Rock(self.settings, self.screen)
self.rocks.add(rock)
# 爆炸列表,每次飞机被撞击或玩家击毁敌对物体均生成爆炸
self.explosions = pygame.sprite.Group()
self.last_explosion_time = 0
# 播放玩游戏时的背景音乐
self.settings.bg_music.play(-1)
'''主事件循环'''
def run(self):
while self.running:
# 以基本固定的时间间隔重复运行,较clock.tick(fps)更精确
now = pygame.time.get_ticks()
if now - self.last_loop_time >= 1000 / self.settings.fps:
self.last_loop_time = now
# 事件监听
self.__handle_event()
''' 如果发生了QUIT事件,就会调用pygame.quit(),此时surface已经销毁,
不能再更新屏幕,也没有必要再进行碰撞检测等动作。如果不检测running标志,
那么程序关闭时会发生surface已关闭的错误'''
if self.running:
# 碰撞检测
self.__check_collide()
# 更新显示缓冲区并显示到屏幕上
self.__update()
'''处理事件'''
def __handle_event(self):
for event in pygame.event.get():
key_pressed = pygame.key.get_pressed()
# 按下窗体上的关闭按钮或者ESC键则停止主事件循环
if event.type == pygame.QUIT or key_pressed[pygame.K_ESCAPE]:
self.running = False
# 由于射击和移动等事件是由Player类处理的,需要将事件传递给player
self.player.handle_event(event)
'''碰撞检测,每次事件循环均需根据更新后的窗体上所有对象的位置判断是否发生碰撞'''
def __check_collide(self):
# 子弹摧毁岩石,简使用单的矩形检测,末尾两个True表示相撞的两个对象都删除
hits = pygame.sprite.groupcollide(self.rocks, self.player.bullets, True, True)
# 撞掉了的岩石添加回来,hits中保存了岩石与子弹的键值对,键为岩石(第一个参数)
for hit in hits:
rock = Rock(self.settings, self.screen)
self.rocks.add(rock)
# 击中岩石,以岩石宽度的1/5作为分数计算成绩
self.player_score += int(hit.rect.width / 5)
# 生成一个爆炸并添加至爆炸列表
explosion = Explosion(self.settings.explosion_animation, hit.rect.center,
self.settings.explosion_sound, hit.rect.width, hit.rect.height)
self.explosions.add(explosion)
# 岩石撞飞机,采用精确到像素级别的碰撞检测,其余类似于子弹击中岩石
hits = pygame.sprite.spritecollide(self.player, self.rocks, True, pygame.sprite.collide_mask)
for hit in hits:
rock = Rock(self.settings, self.screen)
self.rocks.add(rock)
# 以岩石的宽度作为减去血量的数值
self.player.blood -= int(hit.rect.width)
explosion = Explosion(self.settings.explosion_animation, hit.rect.center,
self.settings.explosion_sound, hit.rect.width, hit.rect.height)
self.explosions.add(explosion)
# 处理玩家血量减少后的逻辑:血量小于0时减掉1条命,并重新生成一个玩家,隐藏1秒钟后显示
if self.player.blood <= 0:
self.settings.player_lives -= 1
self.player = Player(self.settings, self.screen)
self.player.hide()
#如果剩余游戏机会为0
if self.settings.player_lives == 0:
#停止游戏循环
self.running = False
#停止游戏的背景音乐
self.settings.bg_music.stop()
#打开开始画面
show_start_page()
'''更新显示缓冲区并显示到屏幕'''
def __update(self):
# 在显示缓冲区绘制游戏背景
self.screen.blit(self.settings.bg_image, (0, 0))
# 在显示缓冲区绘制玩家
self.player.update()
self.player.draw()
# 在显示缓冲区绘制岩石
self.rocks.update()
for rock in self.rocks:
rock.draw()
# 绘制爆炸
self.explosions.update( )
for explosion in self.explosions:
explosion.draw(screen)
# 在显示缓冲区显示成绩
draw_text(screen, str(self.player_score), font_en, 16, 'red', pos_y=self.settings.vspace)
# 在顶部显示血量,剩余血量用一个绿色的实心矩形表示,满血用一个空心的白框矩形表示
pygame.draw.rect(screen, (0, 255, 0), (self.settings.hspace, self.settings.vspace, self.player.blood, 10))
pygame.draw.rect(screen, (255, 255, 255), (self.settings.hspace, self.settings.vspace, 100, 10), 2)
# 在顶部显示剩余的游戏机会
for i in range(self.settings.player_lives):
screen.blit(self.player_img_small,
(screen_width - (i + 1)*(self.player_img_small_width + 5) , self.settings.vspace))
# 将显示缓冲区的图形数据输出到屏幕,实现屏幕显示更新
pygame.display.update()
'''程序启动即显示开始页面,每次游戏结束后又显示开始页面'''
def show_start_page():
running = True
screen_rect = screen.get_rect()
# 载入开始页面的背景音乐并播放,演示pygame.mixer.music.load()的方式载入音乐
pygame.mixer.music.load(os.path.join('sound', 'background_02.wav'))
pygame.mixer.music.set_volume(0.1)
pygame.mixer.music.play(-1)
# 开始页面的事件循环
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.KEYUP:
if event.key == pygame.K_s:
running = False
pygame.mixer.music.stop()
game = Game()
game.run()
elif event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
#在事件循环中判断循环标志这个细节值得注意,不然最后一次事件循环执行pygame.quit()后还会继续执行屏幕更新等操作导致出错
if running:
screen.blit(bg_ground, screen_rect)
#以下几行演示pygame字体处理
draw_text(screen, '太空大战', font_zh, 60, 'red', pos_y=screen_height / 4)
# 以下各行文字从屏幕垂直中间位置开始输出,每行向下移动一个行高加20的距离
pos_y = screen_height / 2
# 设定文本的行高
line_height = pygame.font.Font(font_zh, 36).get_height() + 10
draw_text(screen, 'S键开始游戏', font_zh, 36, 'green', pos_y=pos_y)
draw_text(screen, 'ESC键退出游戏', font_zh, 36, 'yellow', pos_y=(pos_y + 30 + line_height))
draw_text(screen, '空格键发射子弹', font_zh, 36, 'yellow', pos_y=(pos_y + 30 + line_height * 2))
draw_text(screen, '←→↑↓键移动飞机', font_zh, 36, 'yellow', pos_y=(pos_y + 30 + line_height * 3))
pygame.display.update()
'''在指定位置,以指定的字体、大小、颜色输出指定文本,默认水平竖直均居中对齐,文字无底纹。
主要演示pygame的Font渲染文字,在需要给文字绘制底色时,较screen.draw.text效率更高。
这个函数实际上可以在任何需要在表面绘制文字的pygame程序中。'''
def draw_text(screen, text, fontname, fontsize, fontcolor, pos_x = None, pos_y = None, bgcolor = None):
fontcolor = pygame.color.Color(fontcolor)
font = pygame.font.Font(fontname, fontsize)
# 使用粗体模式
font.set_bold(True)
# 使用字体渲染文本
img = font.render(text, True, fontcolor, bgcolor)
rect = img.get_rect()
# 不指定位置就居中
if pos_x == None:
pos_x = (screen_width - rect.width) / 2
if pos_y == None:
pos_y = (screen_height - rect.height) / 2
rect.topleft = (pos_x, pos_y)
screen.blit(img, rect)
if __name__ == '__main__':
show_start_page()
程序中用到的预设值如下:
import pygame
import os
class Settings():
def __init__(self, screen_width, screen_height):
self.fps = 60 # 使用pygame.time.Clock.tick(fps)控制帧率用,本程序用1000/fps控制每次循环的时间
self.screen_width = screen_width
self.screen_height = screen_height
# 玩家飞机的宽度和高度
self.plane_width = 52
self.plane_height = 40
# 屏幕竖直方向和水平方向边缘留的空白宽度
self.vspace = 10
self.hspace = 5
# 玩家飞机的水平和竖直速度
self.player_speedx = 5
self.player_speedy = 8
# 玩家发射子弹的最小时间间隔,不设置的话子弹发射速度飞快
self.bullet_interval = 150
# 初始有三次游戏机会
self.player_lives = 3
# 将图片中部分区域变透明的颜色键,用于消除pygame读入png图片后透明区域生成的黑色像素
self.colorkey = (0,0,0)
# 生成岩石的数量
self.rocks_number = 10
# 随机生成岩石大小是的最大值和最小值
self.rocks_big_radius = 60
self.rocks_small_radius = 20
# 载入背景图片,并转换为pygame可快速处理的图像数据
bg_image = pygame.image.load(os.path.join('imgs', 'background.png')).convert()
# 将背景图片缩放至窗体大小
self.bg_image = pygame.transform.scale(bg_image, (screen_width, screen_height))
# 载入飞机图片并处理
plane_png = pygame.image.load(os.path.join('imgs', 'plane.png')).convert()
self.plane_image = pygame.transform.scale(plane_png, (self.plane_width, self.plane_height))
#将背景中的黑色设置为完全透明
self.plane_image.set_colorkey(self.colorkey)
#载入岩石图片并处理
self.rock_image = pygame.image.load(os.path.join('imgs', 'rock.png')).convert()
self.rock_image.set_colorkey((0, 0, 0))
img_dir = os.path.join('imgs')
# 开始页面背景图片
self.background_dir = os.path.join(img_dir, 'space.png')
self.background_img1 = pygame.image.load(self.background_dir).convert()
self.background_rect = self.background_img1.get_rect()
bullet_dir = os.path.join(img_dir, 'bullet.png')
self.bullet_img = pygame.image.load(bullet_dir).convert_alpha()
self.explosion_animation = []
for i in range(9):
explosion_dir = os.path.join(img_dir, f'regularExplosion0{i}.png')
explosion_img = pygame.image.load(explosion_dir).convert()
explosion_img.set_colorkey(self.colorkey)
self.explosion_animation.append(explosion_img)
sound_dir = os.path.join('sound')
# 射击音效
self.shoot_sound = pygame.mixer.Sound(os.path.join(sound_dir, 'Laser_Shoot14.wav'))
self.shoot_sound.set_volume(0.1)
# 爆炸音效
self.explosion_sound = pygame.mixer.Sound(os.path.join(sound_dir, 'Explosion7.wav'))
self.explosion_sound.set_volume(0.1)
# 游戏界面背景音效
self.bg_music = pygame.mixer.Sound(os.path.join(sound_dir, 'background_01.wav'))
self.bg_music.set_volume(0.3)
下次贴玩家飞机代码。目前只做了玩家飞机、岩石、子弹、爆炸四种角色。有人看就继续丰富功能,增加敌机向玩家发射子弹,掉宝等。
程序中用到的碰撞检测相关知识总结:
pygame
中的角色是Surface
类实例,该类是对矩形图片的封装,属性image
是图片,属性rect
是图片边界和位置。矩形图片中除了要显示的图形(图像)外,其余部分的颜色称为底色,底色一般是单一颜色。在屏幕显示角色时,应只显示图形(图像),不显示底色,或者说是使底色透明。图片可看作是1个2维列表记录每点颜色值。Pygame
常用两种方法使底色透明,第1种方法是用colorkeys
语句使底色颜色为透明,图形本身完全不透明。另一种方法是图片上所有点的颜色用RGBA
表示,A
为透明度,A=0
是完全透明,A=255
是完全不透明,A
从254到1是越来越透明。用此法可将底色设为完全透明,图形(图像)本身设为完全不透明。无论哪种方法,矩形图片透明底色的所有点可用0标记,图形(图像)本身完全不透明,所有不透明点可用1标记。将这种思想用在碰撞中,Surface
实例矩形图片中只有标记为1的完全不透明的所有点参加碰撞检测,Surface
实例矩形图片中标记为0的完全透明的所有点不参加碰撞检测。实现的方法就是使用pygame.mask
记录一个Surface
实例矩形图片中每点的0或1标记,检测碰撞方法根据mask
标记数据,仅检测两个Surface
实例矩形图片中各自标记为1的所有点之间是否覆盖来判断是否发生了碰撞。mask
也是一个2维数组,每一项仅占2进制的1位,和描述Surface
实例的图片的2维数组有对应关系。根据mask
标记检测碰撞需要2个步骤:- 1、
Surface
实例都要设置底色透明,使用pygame.mask
记录所有透明点和不透明点标记
#使用set_colorkey()将底色设为透明
surf = pygame.surface.Surface((20,20), 0, 32)
surf.fill(pygame.Color('white')) #底色
surf.set_colorkey(pygame.Color('white')) #底色透明
pygame.draw.circle(surf,red,(10,10),10) #画要显示的图形
rect = surf.get_rect(center=(90,35)) #rect定义图片边界和位置
mask=pygame.mask.from_surface(surf) #mask中透明点标记为0,不透明点标记为1
#使用convert_alpha()将图片保存为RGBA格式记录透明度
surf1=pygame.image.load('迷宫去底色.png').convert_alpha()
rect1 = background.get_rect()
mask1=pygame.mask.from_surface(surf1)
2、检测上面两个Surface实例surf、surf1碰撞方法
#用mas2k检查mask1,则计算offset时要用rect1-rect2
offset=rect1.x-rect.x,rect1.y-rect.y #被减数和减数次序不能交换
#碰撞返回第1个碰撞点在mask(不是mask1)坐标(x,y),注意同样也是surf的图片上碰撞点坐标
if mask.overlap(mask1,offset)!=None: #未碰撞返回None
if mask.overlap_area(mask1,offset)!=0: #返回发生碰撞的点数
mask.overlap_mask(mask1,offset) #返回mask记录surf上所有发生碰撞的点,宽高同surf.rect
-
判断某个精灵和指定精灵组 中的精灵的碰撞
spritecollide(sprite, group, dokill, collided = None) -> Sprite_list
- 如果将
dokill
设置为True
, 指定精灵组中发生碰撞的精灵将被自动移除 collided
参数用法同前。
- 如果将
-
判断两个精灵(
sprite_1
和sprite_2
)相互是否碰撞可以使用
sprite_1.mask.overlap(sprite_2.mask, offset)
方法,其中两个精灵都必须有mask
属性。mask
的创建参见“两个精灵组中所有的精灵的碰撞检测”中的说明。offset
是一个元组,计算时用参数对象的矩形与调用方法的对象的矩形的x坐标与y坐标对应相减。即:offset = sprite_2.rect.x - sprite_1.rect.x , sprite_2.rect.y - sprite_1.rect.y
-
(序列文章,未完待续)