Pygame——飞机大战

准备工作

1.准备python环境

2.准备pycharm,新建工程

3.安装pygame

Python环境的配置

配过多次python环境,久病也没成医,现配还是得查。还好开发项目之前python的环境已经配置完成了。

如果有一天,我又需要配一遍python环境,我会回来把过程记在这里。

新建工程

先下载pycharm

https://www.jetbrains.com/pycharm/

白嫖一个community版够用了,安装一路点next

新建工程,命名为Plane_war

Pygame的安装

新建一个main.py,输入import pygame报错,点小灯泡,然后让pycharm自动下载就好

总体设计

1.游戏的框架

2.什么是游戏循环——书写主游戏类和主函数

3.展示主界面

游戏框架

一个游戏由三部分组成,即初始化,游戏循环,退出

初始化完成的是游戏正式开始之前的准备工作,如导入资源,加载存档等

游戏循环意味着游戏正式开始,一般会有一个死循环,该循环以固定的帧率刷新屏幕,直到满足某个条件时,游戏退出

退出时的代码做一些释放资源,存储等操作

书写主游戏类

根据上面的说明,我们知道主游戏类至少具有:

初始化方法:初始化并创建一些资源

主方法:运行游戏循环,因此我们先写一个死循环占位

退出方法:退出游戏,不妨使用exit方法

因此,我们新建文件main_game.py

在其中定义Main_Game类:

注意:在使用pygame提供的模块前,应先执行pygame.init()方法来进行一些初始化操作,在退出游戏之前,也应该执行oygame.quit()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pygame


class Main_Game:
def __init__(self):
"""游戏初始化"""
pygame.init()

def start(self):
"""游戏循环"""
while True:
pass

@staticmethod
def game_over():
"""游戏结束"""
pygame.quit()
print("游戏结束")
exit()

接着,主函数main中,新建一个Main_Game对象并调用其start方法

1
2
3
4
5
import main_game

game = main_game.Main_Game()

game.start()

展示主界面

在pygame中,创建主界面的方法为:

pygame.display.set_mode(resolution(0, 0),flags=0,depth=0)->Surface

resolution属性制定主界面的宽和高,默认值为全屏,flags指定附加选项,depth指定颜色位数

我们需要接收创建出的Surface对象,之后的绘制工作全都在它上面进行

因此,不妨在Main_Game的init方法中,加入:

self.screen = pygame.display.set_mode()

这时再执行代码,就能看到黑色的框创建出来了

细化框架

1.分析游戏循环中应该执行那些操作

2.细化游戏循环的框架

3.设置帧率

4.实现退出游戏功能

游戏循环分析

我们知道,在游戏运行中,我们可以通过操作键盘和鼠标对游戏中的角色进行控制,因此我们需要一个事件处理函数

每当一次游戏循环运行时,游戏中的人物会移动,我们需要更新显示,因此我们需要一个显示更新函数

在Main_Game类中加入上述两个函数,并先使用pass占位

1
2
3
4
5
6
7
def __event_handler(self):
"""事件处理"""
pass

def __update_screen(self):
"""更新屏幕"""
pass

在游戏循环中,分别调用这两个函数:

1
2
3
4
5
6
7
def start(self):
"""游戏循环"""
while True:

self.__event_handler()

self.__update_screen()

设置帧率

帧率的设置需要定义设置时钟对象

要设置时钟对象,需要使用pygame中的time模块中的Clock

在Main_Game的init方法中,创建时钟对象:

1
2
3
4
5
6
7
def __init__(self):
"""游戏初始化"""
pygame.init()
# 创建主界面
self.screen = pygame.display.set_mode()
# 设置帧率
self.clock = pygame.time.Clock()

在start的游戏循环中,调用clock的tick方法,指定帧率为60

这里,为了避免直接使用整数,我们建立一个constant.py文件,在其中定义:

FLAME_PER_SECOND = 60

然后start方法改为:

1
2
3
4
5
6
7
8
def start(self):
"""游戏循环"""
while True:
self.clock.tick(constants.FLAME_PRE_SECOND)

self.__event_handler()

self.__update_screen()

实现退出事件监听

游戏的退出是事件的一种,我们在事件处理函数中,如果检测到退出事件,应调用game_over函数

想要得到发生的事件,需要使用pygame.event.get()函数来得到当前事件列表

event_handler函数改成:

1
2
3
4
5
def __event_handler(self):
"""事件处理"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
Main_Game.game_over()

现在,点击黑色框的×可以将游戏正常退出了

工具类

1.Rect类

2.Sprite

3.Sprite组

Rect类

pygame.Rect是pygame中描述矩形的类,其中主要属性有:

x,y分布表示左上点的位置横纵坐标,其中屏幕的最左上点坐标为(0,0)

left表示最左边x坐标,right类推

top表示顶部y坐标,bottom类推

center是一个二元组(centerx,centery),表示中心位置

size是一个二元组(width,height)表示矩形大小

创建Rect对象需要传入四元组(x, y, w, h)

Sprite

pygame.sprite.Sprite类,翻译过来是精灵。实际上表示游戏中可以独立活动的一个整体,比如一架飞机,或者一颗子弹。

使用Sprite的好处是,我们可以把一个独立活动的整体设置成一个对象,定义了其update方法之后,就可以让它自动在屏幕上,以我们设置好的方法移动,避免我们在每次循环中手动绘制游戏画面

主要方法和属性有:

image是一个图像

导入一个图像可以使用pygame.image.load(路径)方法

rect是该精灵的位置大小等信息

update方法:设置更新精灵位置的方式

kill方法:销毁该精灵,并在所有组中删除

精灵组

我们可以把精灵加入精灵组,这样我们就可以同时调用一组精灵的update方法,并方便我们针对不同种类的精灵进行不同的操作

精灵组位于pygame.sprite.Group

主要方法和属性:

add(精灵列表):向该组中添加精灵

update方法:调用该组中所有精灵的update方法

draw(Surface):把组中所有精灵的位置绘制在屏幕上

Sprites():返回组中精灵的列表

需要更新精灵的位置时,一般要三步:

使用精灵组的update方法,更新精灵的位置属性

使用精灵组的draw方法,绘制在屏幕上

调用pygame.display.update()方法来更新显示

精灵和精灵组

1.创建游戏精灵

2.在主游戏类中创建游戏精灵组

3.创建背景精灵

4.实现背景的更新

5.进一步完善

创建游戏精灵

经过上篇,我们知道,游戏精灵就是游戏中所有运动的对象,它继承自pygame.sprite.Sprite,而我们创建自己的游戏精灵,至少需要重写其init和update方法

在本游戏中,我们先创建一个基本游戏精灵,并令其它的精灵都继承自这个基本精灵。

新建一个类,命名为:Game_Sprite,继承自pygame.sprite.Sprite

基本精灵应该有一个图片类型的数据以绘制其图像,指定其初始位置,还应该有横向和纵向的速度,因此,我们的init函数重写为:

这里默认了游戏中所有类创建初始位置是(0,0),初始横向速度为0,纵向速度为1,即默认水平

1
2
3
4
5
6
7
8
9
def __init__(self, image_file: str, init_pos=(0, 0), speed_x=0, speed_y=1):
super().__init__()
# 初始化图像
self.image = pygame.image.load(image_file)
# 初始化位置
self.rect = pygame.Rect(init_pos[0], init_pos[1], self.image.get_width(), self.image.get_height())
# 初始化速度
self.speed_x = speed_x
self.speed_y = speed_y

这样,我们的update函数就可以使用速度来更新精灵的位置了:

1
2
3
def update(self, *args, **kwargs) -> None:
self.rect.x += self.speed_x
self.rect.y += self.speed_y

创建游戏精灵组

在游戏初始化时,我们需要创建精灵,因此,我们先添加一个创建精灵组函数,统一创建我们想要的精灵组,然后在主类初始化的时候调用它

1
2
3
def __create_sprites(self):
"""创建精灵和组"""
pass

主类的初始化函数修改为:

1
2
3
4
5
6
7
8
9
def __init__(self):
"""游戏初始化"""
pygame.init()
# 创建主界面
self.screen = pygame.display.set_mode(constants.GAME_SCREEN_RECT.size)
# 设置帧率
self.clock = pygame.time.Clock()
# 创建精灵组
self.__create_sprites()

创建背景精灵

我们首先创建背景精灵,绘制背景图案

首先,在constants.py这个存放常数的文件里面添加背景图片的路径:

1
BACKGROUND_FILE = "./images/background.png"

新建一个类,命名为Background_Sprite,继承自我们的游戏精灵Game_Sprite,重写其init方法为调用父类的init方法并传入背景图片路径

1
2
3
4
5
6
7
8
9
10
import pygame
import game_sprite
import constants


class Background_Sprite(game_sprite.Game_Sprite):
"""背景精灵"""

def __init__(self):
super().__init__(constants.BACKGROUND_FILE)

在主类的__create_sprites函数里,先为背景精灵创建一个背景精灵组:

1
2
3
4
5
def __create_sprites(self):
"""创建精灵和组"""
self.background_sprite_group = pygame.sprite.Group()
background_sprites = background_sprite.Background_Sprite()
self.background_sprite_group.add(background_sprites)

更新屏幕

尽管我们已经创建了背景精灵,但为了能够让它正常地运动起来,我们还需要调用精灵组的update方法,这里,我们在主类中实现一个更新屏幕的函数,在其中调用每个精灵组的update和draw方法以更新它们在屏幕中的位置:

1
2
3
4
5
def __update_screen(self):
"""更新屏幕"""
self.background_sprite_group.update()
self.background_sprite_group.draw(self.screen)
pygame.display.update()

在每个游戏循环的最后都加入这个更新屏幕方法

1
2
3
4
5
6
7
8
9
def start(self):
"""游戏循环"""
while True:
# 帧率
self.clock.tick(constants.FLAME_PRE_SECOND)
# 事件处理
self.__event_handler()
# 更新屏幕
self.__update_screen()

这样,我们就可以运行的时候看到下降的背景画面了

进一步完善

背景虽然下降了,但下降到屏幕外面就没了,我们显然不希望这样,我们希望背景能够一直存在,并且一直动态地下降,这样,我们就使用两张图片交替显示的方式,当一张图片完全移动到屏幕外面的时候,我们再重新把它放在屏幕的最上方

可以通过修改背景精灵的update方法来实现这样的功能,每次update时,先调用父类的update使其正常下移,然后判断图片的y坐标是否已经下降到屏幕外(y > 屏幕的高度),若是,则把y重新调到上面去

1
2
3
4
def update(self, *args, **kwargs) -> None:
super(Background_Sprite, self).update()
if self.rect.y >= constants.GAME_SCREEN_RECT.height:
self.rect.y = -constants.GAME_SCREEN_RECT.height

写完这里,可以再执行一下测试能否实现上述的功能

之后就是两张图片如何交替移动的问题了,其实很简单,我们只需要把背景图片组中添加两个初始位置不同的图片就可以了,一张的初始位置在(0,0),另一张不妨就设置到第一张的上方

为此,我们修改一下主类的创建背景精灵函数:

1
2
3
4
5
6
7
8
def __create_sprites(self):
"""创建精灵和组"""
self.background_sprite_group = pygame.sprite.Group()
background_sprite1 = background_sprite.Background_Sprite()
background_sprite2 = background_sprite.Background_Sprite()
background_sprite2.rect.y = -constants.GAME_SCREEN_RECT.height
self.background_sprite_group.add(background_sprite1)
self.background_sprite_group.add(background_sprite2)

这样再执行,背景图片就可以非常连贯地交替下移了

敌机

1.创建敌机

2.定时器——敌机随机出现

创建敌机

首先,把敌机图片的路径写入到constants里面

1
2
# 敌机图片路径
ENEMY_FILE = "./images/enemy1.png"

然后,创建敌机精灵类,命名为Enemy_Sprite

1
2
3
4
5
import pygame
import game_sprite


class Enemy_Sprite(game_sprite.Game_Sprite):

敌机出现时总是在屏幕最上方,位置x随机,垂直速度speed_y为1~3随机,没有水平速度

这样,我们定义敌机的init方法为:

1
2
3
4
5
def __init__(self):
super(Enemy_Sprite, self).__init__(constants.ENEMY_FILE)
self.rect.x = random.randint(0, constants.GAME_SCREEN_RECT.width - self.rect.width)
self.rect.bottom = 0
self.speed_y = random.randint(1, 3)

敌机的更新方式不需要大的改动,但有一点需要注意的是,当敌机已经飞出屏幕的时候,我们就不需要它了,所以,我们需要把这个敌机对象杀死

这就需要使用到我们的析构函数del和精灵的kill方法

析构函数和update:

1
2
3
4
def update(self, *args, **kwargs) -> None:
super(Enemy_Sprite, self).update()
if self.rect.y == constants.GAME_SCREEN_RECT.y:
self.kill()

这时候可以在主类里面创建一个敌机,看看是不是可以正确行动

在create_sprites里面添加:

1
2
3
4
# 创建敌机组
self.enemy_sprite_group = pygame.sprite.Group()
enemy_sprite1 = enemy_sprite.Enemy_Sprite()
self.enemy_sprite_group.add(enemy_sprite1)

然后在update_screen里面添加:

1
2
3
# 更新敌机
self.enemy_sprite_group.update()
self.enemy_sprite_group.draw(self.screen)

随机出现的敌机

我们需要使用pygame中的计时器pygame.time.set_timer()

使用方法是:传入两个参数,第一个为事件id,第二个为事件间隔时间,单位毫秒

事件id是自己定义的,设定计时器之后,每次计时器到达时间点事件id会被放入事件列表

可以使用pygame自带的事件id,pygame.USERENEVT,之后的事件可以在此基础上+1

在constants里面设定敌机出现事件的事件id和时间间隔:

1
2
3
4
# 创建敌机事件id
ENEMY_CREATE_EVENT_ID = pygame.USEREVENT
# 创建敌机事件间隔
ENEMY_CREATE_EVENT_TIME = 1000

在主类的init方法里添加:

1
2
# 创建敌机计时器
pygame.time.set_timer(constants.ENEMY_CREATE_EVENT_ID, constants.ENEMY_CREATE_EVENT_TIME)

这样,事件处理列表中需要变为:

1
2
3
4
5
6
7
8
def __event_handler(self):
"""事件处理"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
Main_Game.game_over()
elif event.type == constants.ENEMY_CREATE_EVENT_ID:
enemy = enemy_sprite.Enemy_Sprite()
self.enemy_sprite_group.add(enemy)

再把之前为了测试敌机的行为写的代码去掉,即create_sprites中创建敌机组时,直接创建一个空组,不再上来就为其添加一个敌机

运行,敌机可以随机创建了

主角

1.创建英雄

2.英雄发射子弹

创建英雄

首先添加英雄图片路径,在constant里加入:

1
2
# 英雄图片路径
HERO_FILE = "./images/me1.png"

创建英雄精灵类Hero_Sprite,我们假设英雄初始位置在界面底部中间位置,初始无速度,定义其init方法:

1
2
3
4
5
6
class Hero_Sprite(game_sprite.Game_Sprite):
def __init__(self):
super(Hero_Sprite, self).__init__(constants.HERO_FILE)
self.rect.bottom = constants.GAME_SCREEN_RECT.height
self.rect.centerx = constants.GAME_SCREEN_RECT.centerx
self.speed_y = 0

然后在主类create_sprites中添加英雄组,在英雄组中加入英雄:

1
2
3
4
# 创建英雄组
self.hero_sprite_group = pygame.sprite.Group()
hero = hero_sprite.Hero_Sprite()
self.hero_sprite_group.add(hero)

同时在更新屏幕函数中添加英雄组的更新

1
2
3
# 更新英雄
self.hero_sprite_group.update()
self.hero_sprite_group.draw(self.screen)

发射子弹

添加子弹图片路径

1
2
# 子弹图片路径
BULLET_FILE = "./images/bullet1.png"

子弹从英雄图片的正上方发射,默认竖直向上移动

所以,创建子弹时,我们需要传出英雄的位置参数

定义子弹类Bullet_Sprite,其初始化方法:

1
2
3
4
5
6
class Bullet_Sprite(game_sprite.Game_Sprite):
def __init__(self, x, y):
super(Bullet_Sprite, self).__init__(constants.BULLET_FILE)
self.speed_y = -4
self.rect.x = x
self.rect.y = y

由于子弹飞出屏幕之后我们也需要销毁它,同样改写update:

1
2
3
4
def update(self, *args, **kwargs) -> None:
super(Bullet_Sprite, self).update()
if self.rect.y <= 0:
self.kill()

在create_sprite函数里面添加子弹组,并向里面添加一发子弹来测试效果,因为子弹的发射初始位置和英雄的位置有关,我们遍历英雄组,取英雄位置作为子弹的初始参数

1
2
3
4
5
# 创建子弹组
self.bullet_sprite_group = pygame.sprite.Group()
for sprite in self.hero_sprite_group:
bullet = bullet_sprite.Bullet_Sprite(sprite.rect.centerx, sprite.rect.top)
self.bullet_sprite_group.add(bullet)

在更新屏幕函数里面添加更新子弹:

1
2
3
4
# 更新子弹
self.bullet_sprite_group.update()
self.bullet_sprite_group.draw(self.screen)
pygame.display.update()

可以看到子弹的行为正常

接下来我们添加定时发射子弹,首先添加发射子弹事件id,并设置定时器

constants中:

1
2
3
4
# 添加子弹事件id
BULLET_CREATE_EVENT_ID = pygame.USEREVENT + 1
# 创建子弹事件间隔
BULLET_CREATE_EVENT_TIME = 500

主类的init中添加:

1
2
# 创建子弹计时器
pygame.time.set_timer(constants.BULLET_CREATE_EVENT_ID, constants.BULLET_CREATE_EVENT_TIME)

把刚刚为了测试而写在create_sprite里面遍历英雄发射子弹的代码,移动到事件处理函数里:

1
2
3
4
5
6
7
8
9
10
11
12
def __event_handler(self):
"""事件处理"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
Main_Game.game_over()
elif event.type == constants.ENEMY_CREATE_EVENT_ID:
enemy = enemy_sprite.Enemy_Sprite()
self.enemy_sprite_group.add(enemy)
elif event.type == constants.BULLET_CREATE_EVENT_ID:
for sprite in self.hero_sprite_group:
bullet = bullet_sprite.Bullet_Sprite(sprite.rect.centerx, sprite.rect.top)
self.bullet_sprite_group.add(bullet)

这样,我们的英雄就会持续发出子弹了

主角操作

1.捕获键盘实现英雄的移动

2.碰撞检测来摧毁敌机

3.碰撞检测牺牲英雄

英雄移动

为了方便控制英雄,我们修改创建英雄方法,把创建出的英雄也作为主类的一个属性保存起来

1
2
3
4
# 创建英雄组
self.hero_sprite_group = pygame.sprite.Group()
self.hero = hero_sprite.Hero_Sprite()
self.hero_sprite_group.add(self.hero)

我们使用pygame.key.get_pressed()方法,该方法可以检测当前被按下的键,并返回一个元组,元组中,凡是被按下的键对应的值都是true

我们在检测事件中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
key_pressed = pygame.key.get_pressed()
speed_x = 0
speed_y = 0
if key_pressed[pygame.K_RIGHT]:
speed_x += 3
if key_pressed[pygame.K_LEFT]:
speed_x -= 3
if key_pressed[pygame.K_DOWN]:
speed_y += 3
if key_pressed[pygame.K_UP]:
speed_y -= 3
self.hero.speed_x = speed_x
self.hero.speed_y = speed_y

这样,英雄就可以在我们的控制下移动了,但有问题的是,此时英雄可以移出屏幕外,我们不希望这样,因此我们在英雄的update方法里面增加一个边界检测:

1
2
3
4
5
6
7
8
9
10
def update(self, *args, **kwargs) -> None:
super(Hero_Sprite, self).update()
if self.rect.top < 0:
self.rect.top = 0
if self.rect.bottom > constants.GAME_SCREEN_RECT.height:
self.rect.bottom = constants.GAME_SCREEN_RECT.height
if self.rect.left < 0:
self.rect.left = 0
if self.rect.right > constants.GAME_SCREEN_RECT.width:
self.rect.right = constants.GAME_SCREEN_RECT.width

这样,英雄的移动就做好了

摧毁敌机

使用碰撞检测pygame.sprite.groupcollide()来判断子弹和敌机是否碰撞。

传入四个参数,前两个是检测碰撞的组,后面接上两个True,表示检测到碰撞后清除碰撞在一起的对象

可以在事件监听函数中加入碰撞检测

1
pygame.sprite.groupcollide(self.enemy_sprite_group, self.bullet_sprite_group, True, True)

牺牲英雄

同样使用碰撞检测,只不过这次不使用英雄组,而是使用英雄对象,检测某个组和某个对象有碰撞的函数是pygame.sprite.spritecollide,参数是对象,组名,是否杀死,我们检测英雄对象和敌机组有没有碰撞,这个函数会返回敌机列表

1
2
3
4
enemies = pygame.sprite.spritecollide(self.hero, self.enemy_sprite_group, True)
if len(enemies) > 0:
self.hero.kill()
self.game_over()