还不懂飞机大战?最细学习笔记--全方位学习Pygame(附源码)
前言:
Hello大家好,我是Dream。近期不经意间翻到了去年指导一位朋友写的飞机大战,感觉写得各个方面都是很全的,正好也有同学问我Pygame 该怎么学习,那今天我把这篇文章分享给大家,希望大家看后可以有所启发。如果你觉得有用的话就赶紧收藏起来吧~
一、飞机大战源码自取
pygame
强化面向对象程序设计
使用 pygame
模块进行游戏开发
安装 pygame
pip install pygame
源码分享
所有源码均在此公众号内:关注此公众号,回复 飞机大战
、超级玛丽
、植物大战僵尸
获取源码,快点击我吧
你也可以直接进行下载:https://download.csdn.net/download/weixin_51390582/87315776
二、pygame 快速入门
- 新建飞机大战项目
- 导入游戏素材图片
游戏的第一印象
- 把一些静止的图像绘制到游戏窗口中
- 根据用户的交互或其他情况,移动这些图像,产生动画效果
- 根据图像之间是否发生重叠,判断敌机是否被摧毁等其他情况
可以将图片素材绘制到游戏的窗口上,开发游戏之前需要先知道如何建立游戏窗口!
1.游戏的初始化
- 要使用
pygame
提供的所有功能之前,需要调用init
方法 - 在游戏结束前需要调用一下
quit
方法
方法 | 说明 |
---|---|
pygame.init() |
导入并初始化所有 pygame 模块,使用其他模块之前,必须先调用 init 方法 |
pygame.quit() |
卸载所有 pygame 模块,在游戏结束之前调用! |
import pygame
pygame.init()
# 游戏代码...
pygame.quit()
创建游戏主窗口
pygame
专门提供了一个 模块pygame.display
用于创建、管理游戏窗口
方法 | 说明 |
---|---|
pygame.display.set_mode() |
初始化游戏显示窗口 |
pygame.display.update() |
刷新屏幕内容显示 |
set_mode
方法
set_mode(resolution=(0,0), flags=0, depth=0) -> Surface
- 作用 —— 创建游戏显示窗口
- 参数
resolution
指定屏幕的宽
和高
,默认创建的窗口大小和屏幕大小一致flags
参数指定屏幕的附加选项,例如是否全屏等等,默认不需要传递depth
参数表示颜色的位数,默认自动匹配
- 返回值
- 暂时可以理解为游戏的屏幕,游戏的元素都需要被绘制到游戏的屏幕 上
- 注意:必须使用变量记录
set_mode
方法的返回结果!因为后续所有的图像绘制都基于这个返回结果
# 创建游戏主窗口
screen = pygame.display.set_mode((480, 700))
2.图像绘制
在游戏中,能够看到的游戏元素大多都是图像。图像文件初始是保存在磁盘上的,如果需要使用,第一步就需要被加载到内存
要在屏幕上看到某一个图像的内容,需要按照三个步骤:
- 使用
pygame.image.load()
加载图像的数据 - 使用游戏屏幕对象,调用
blit
方法 将图像绘制到指定位置 - 调用
pygame.display.update()
方法更新整个屏幕的显示
提示:要想在屏幕上看到绘制的结果,就一定要调用
pygame.display.update()
方法
绘制背景图像
- 加载
background.png
创建背景 - 将 背景 绘制在屏幕的
(0, 0)
位置 - 调用屏幕更新显示背景图像
# 绘制背景图像
# 1> 加载图像
bg = pygame.image.load("./images/background.png")
# 2> 绘制在屏幕
screen.blit(bg, (0, 0))
# 3> 更新显示
pygame.display.update()
理解游戏中的坐标系
- 坐标系
- 原点 在 左上角
(0, 0)
- x 轴 水平方向向 右,逐渐增加
- y 轴 垂直方向向 下,逐渐增加
- 原点 在 左上角
- 在游戏中,所有可见的元素 都是以 矩形区域 来描述位置的
- 要描述一个矩形区域有四个要素:
(x, y) (width, height)
- 要描述一个矩形区域有四个要素:
pygame
专门提供了一个类pygame.Rect
用于描述 矩形区域
Rect(x, y, width, height) -> Rect
pygame.Rect
是一个比较特殊的类,内部只是封装了一些数字计算- 不执行
pygame.init()
方法同样能够直接使用
案例演练
- 定义
hero_rect
矩形描述 英雄的位置和大小 - 输出英雄的坐标原点(
x
和y
) - 输出英雄的尺寸(宽度 和 高度)
hero_rect = pygame.Rect(100, 500, 120, 126)
print("坐标原点 %d %d" % (hero_rect.x, hero_rect.y))
print("英雄大小 %d %d" % (hero_rect.width, hero_rect.height))
# size 属性会返回矩形区域的 (宽, 高) 元组
print("英雄大小 %d %d" % hero_rect.size)
绘制英雄图像
- 加载
me1.png
创建英雄飞机 - 将 英雄飞机 绘制在屏幕的
(200, 500)
位置 - 调用屏幕更新显示飞机图像
# 1> 加载图像
hero = pygame.image.load("./images/me1.png")
# 2> 绘制在屏幕
screen.blit(hero, (200, 500))
# 3> 更新显示
pygame.display.update()
理解 update()
方法的作用
可以在
screen
对象完成 所有blit
方法之后,统一调用一次display.update
方法,同样可以在屏幕上 看到最终的绘制结果
- 使用
display.set_mode()
创建的screen
对象 是一个内存中的屏幕数据对象- 可以理解成是 油画的画布
screen.blit
方法可以在 画布上绘制很多图像- 例如:英雄、敌机、子弹…
- 这些图像有可能会彼此重叠或者覆盖
display.update()
会将画布的最终结果绘制在屏幕上,这样可以提高屏幕绘制效率,增加游戏的流畅度
案例调整
# 绘制背景图像
# 1> 加载图像
bg = pygame.image.load("./images/background.png")
# 2> 绘制在屏幕
screen.blit(bg, (0, 0))
# 绘制英雄图像
# 1> 加载图像
hero = pygame.image.load("./images/me1.png")
# 2> 绘制在屏幕
screen.blit(hero, (200, 500))
# 3> 更新显示 - update 方法会把之前所有绘制的结果,一次性更新到屏幕窗口上
pygame.display.update()
3.游戏循环和游戏时钟
游戏循环
为了做到游戏程序启动后,不会立即退出,通常会在游戏程序中增加一个游戏循环
所谓 游戏循环就是一个无限循环
在创建游戏窗口代码下方,增加一个无限循环
- 注意:游戏窗口不需要重复创建
# 创建游戏主窗口
screen = pygame.display.set_mode((480, 700))
# 游戏循环
while True:
pass
现在英雄飞机已经被绘制到屏幕上了,怎么能够让飞机移动呢 ?
游戏中的动画实现原理
跟电影的原理类似,游戏中的动画效果,本质上是快速的在屏幕上绘制图像
- 电影是将多张静止的电影胶片连续、快速的播放,产生连贯的视觉效果!
一般在电脑上每秒绘制 60 次,就能够达到非常连续高品质的动画效果
- 每次绘制的结果被称为帧Frame
游戏循环
游戏的两个组成部分
游戏循环的开始就意味着游戏的正式开始
游戏循环的作用
- 保证游戏不会直接退出
- 变化图像位置 —— 动画效果
- 每隔
1 / 60 秒
移动一下所有图像的位置 - 调用
pygame.display.update()
更新屏幕显示
- 每隔
- 检测用户交互 —— 按键、鼠标等…
游戏时钟
pygame
专门提供了一个类 pygame.time.Clock
可以非常方便的设置屏幕绘制速度 —— 刷新帧率
要使用时钟对象需要两步:
1)在游戏初始化创建一个 时钟对象
2)在游戏循环中让时钟对象调用 tick(帧率)
方法
tick
方法会根据 上次被调用的时间,自动设置游戏循环中的延时
# 3. 创建游戏时钟对象
clock = pygame.time.Clock()
i = 0
# 游戏循环
while True:
# 设置屏幕刷新帧率
clock.tick(60)
print(i)
i += 1
英雄的简单动画实现
- 在游戏初始化定义一个
pygame.Rect
的变量记录英雄的初始位置 - 在游戏循环中每次让 英雄 的
y - 1
—— 向上移动 y <= 0
将英雄移动到屏幕的底部
提示:
- 每一次调用
update()
方法之前,需要把 所有的游戏图像都重新绘制一遍- 而且应该 最先 重新绘制 背景图像
# 4. 定义英雄的初始位置
hero_rect = pygame.Rect(150, 500, 102, 126)
while True:
# 可以指定循环体内部的代码执行的频率
clock.tick(60)
# 更新英雄位置
hero_rect.y -= 1
# 如果移出屏幕,则将英雄的顶部移动到屏幕底部
if hero_rect.y <= 0:
hero_rect.y = 700
# 绘制背景图片
screen.blit(bg, (0, 0))
# 绘制英雄图像
screen.blit(hero, hero_rect)
# 更新显示
pygame.display.update()
案例
- 英雄向上飞行,当英雄完全从上方飞出屏幕后
- 将飞机移动到屏幕的底部
if hero_rect.y + hero_rect.height <= 0:
hero_rect.y = 700
提示
Rect
的属性bottom = y + height
if hero_rect.bottom <= 0:
hero_rect.y = 700
在游戏循环中监听事件
事件 event
- 就是游戏启动后,用户针对游戏所做的操作
- 例如:点击关闭按钮,点击鼠标,按下键盘…
监听
- 在 游戏循环 中,判断用户 具体的操作
只有 捕获 到用户具体的操作,才能有针对性的做出响应
代码实现
pygame
中通过pygame.event.get()
可以获得用户当前所做动作的事件列表 .用户可以同一时间做很多事情- 提示:这段代码非常的固定,几乎所有的
pygame
游戏都 大同小异!
# 游戏循环
while True:
# 设置屏幕刷新帧率
clock.tick(60)
# 事件监听
for event in pygame.event.get():
# 判断用户是否点击了关闭按钮
if event.type == pygame.QUIT:
print("退出游戏...")
pygame.quit()
# 直接退出系统
exit()
4.理解精灵和精灵组
精灵和精灵组
在刚刚完成的案例中,图像加载、位置变化、绘制图像 都需要程序员编写代码分别处理
为了简化开发步骤,pygame
提供了两个类
pygame.sprite.Sprite
—— 存储 图像数据 image 和 位置 rect 的 对象pygame.sprite.Group
精灵
在游戏开发中,通常把显示图像的对象叫做精灵 Sprite
精灵需要有两个重要的属性
image
要显示的图像rect
图像要显示在屏幕的位置
默认的 update()
方法什么事情也没做
- 子类可以重写此方法,在每次刷新屏幕时,更新精灵位置
注意:pygame.sprite.Sprite
并没有提供 image
和 rect
两个属性
- 需要程序员从
pygame.sprite.Sprite
派生子类 - 并在子类的初始化方法 中,设置
image
和rect
属性
精灵组
- 一个 精灵组可以包含多个精灵对象
- 调用精灵组对象的
update()
方法- 可以自动调用组内每一个精灵 的
update()
方法
- 可以自动调用组内每一个精灵 的
- 调用精灵组对象的
draw(屏幕对象)
方法- 可以将 组内每一个精灵 的
image
绘制在rect
位置
- 可以将 组内每一个精灵 的
Group(*sprites) -> Group
注意:仍然需要调用
pygame.display.update()
才能在屏幕看到最终结果
派生精灵子类
- 新建
plane_sprites.py
文件 - 定义
GameSprite
继承自pygame.sprite.Sprite
注意
- 如果一个类的父类不是
object
- 在重写初始化方法 时,一定要 先
super()
一下父类的__init__
方法 - 保证父类中实现的
__init__
代码能够被正常执行
属性
image
精灵图像,使用image_name
加载rect
精灵大小,默认使用图像大小speed
精灵移动速度,默认为1
方法
update
每次更新屏幕时在游戏循环内调用- 让精灵的
self.rect.y += self.speed
- 让精灵的
提示
image
的get_rect()
方法,可以返回 pygame.Rect(0, 0, 图像宽, 图像高) 的对象
import pygame
class GameSprite(pygame.sprite.Sprite):
"""游戏精灵基类"""
def __init__(self, image_name, speed=1):
# 调用父类的初始化方法
super().__init__()
# 加载图像
self.image = pygame.image.load(image_name)
# 设置尺寸
self.rect = self.image.get_rect()
# 记录速度
self.speed = speed
def update(self, *args):
# 默认在垂直方向移动
self.rect.y += self.speed
使用游戏精灵和精灵组创建敌机
需求
- 使用刚刚派生的游戏精灵和精灵组创建敌机并且实现敌机动画
步骤
- 使用
from
导入plane_sprites
模块from
导入的模块可以 直接使用import
导入的模块需要通过 模块名. 来使用
- 在游戏初始化 创建 精灵对象 和 精灵组对象
- 在游戏循环中 让 精灵组 分别调用
update()
和draw(screen)
方法
职责
- 精灵
- 封装 图像 image、位置 rect 和 速度 speed
- 提供
update()
方法,根据游戏需求,更新位置 rect
- 精灵组
- 包含 多个 精灵对象
update
方法,让精灵组中的所有精灵调用update
方法更新位置draw(screen)
方法,在screen
上绘制精灵组中的所有精灵
实现步骤
- 导入
plane_sprites
模块
from plane_sprites import *
- 修改初始化部分代码
# 创建敌机精灵和精灵组
enemy1 = GameSprite("./images/enemy1.png")
enemy2 = GameSprite("./images/enemy1.png", 2)
enemy2.rect.x = 200
enemy_group = pygame.sprite.Group(enemy1, enemy2)
- 修改游戏循环部分代码
# 让敌机组调用 update 和 draw 方法
enemy_group.update()
enemy_group.draw(screen)
# 更新屏幕显示
pygame.display.update()
播放背景音乐
pygame提供了两个加载音乐文件的方法
pygame.mixer.music
,主要加载mp3
音频文件。pygame.mixer.Sound
,主要加载ogg
和wav
音频文件。
在使用音乐组件之前,需要初始化音乐播放器。
用了
pygame.init()
之后可以不使用pygame.mixer.init()
pygame.init()
进行全部模块的初始化
pygame.mixer.init()
或者只初始化音频部分
播放背景音乐
pygame.mixer.music.load('xx.mp3')
使用文件名作为参数载入音乐 ,音乐可以是 ogg、mp3
等格式。载入的音乐不会全部放到内容中,而是以流的形式播放的,即在播放的时候才会一点点从文件中读取。
pygame.mixer.music.stop()
停止播放,
pygame.mixer.music.pause()
暂停播放。
pygame.mixer.music.unpause()
取消暂停。
实现
import pygame
pygame.init()
pygame.mixer.music.load("sound/game_music.ogg")
pygame.mixer.music.set_volume(1)
# 播放背景音乐
pygame.mixer.music.play(-1)
# 暂停背景音乐
pygame.mixer.music.pause()
time.sleep(2)
# 恢复背景播放
pygame.mixer.music.unpause()
# 停止播放
pygame.mixer.music.stop()
播放片段音乐
pygame.mixer.Sound
会返回一个 Sound
对象,该对象有 stop
,play
等方法。
bullet_sound = pygame.mixer.Sound("sound/bullet.wav")
bullet_sound.set_volume(0.2)
bullet_sound.play()
import time
time.sleep(0.2)
bullet_sound.play()
time.sleep(0.2)
bullet_sound.play()
time.sleep(0.2)
bullet_sound.play()
time.sleep(0.2)
在屏幕上画出敌机的移动()从上到下画两个
游戏框架搭建
目标 —— 使用 面相对象 设计 飞机大战游戏类
5.明确主程序职责
回顾 快速入门案例,一个游戏主程序的职责可以分为两个部分:
- 游戏初始化
- 游戏循环
根据明确的职责,设计 PlaneGame
类如下:
提示 根据 职责 封装私有方法,可以避免某一个方法的代码写得太过冗长
如果某一个方法编写的太长,既不好阅读,也不好维护!
游戏初始化 —— __init__()
会调用以下方法:
方法 | 职责 |
---|---|
create_sprites(self) |
创建所有精灵和精灵组 |
游戏循环 —— start_game()
会调用以下方法:
方法 | 职责 |
---|---|
event_handler(self) |
事件监听 |
check_collide(self) |
碰撞检测 —— 子弹销毁敌机、敌机撞毁英雄 |
update_sprites(self) |
精灵组更新和绘制 |
game_over() |
游戏结束 |
6.实现飞机大战主游戏类
plane_main
- 封装 **主游戏类 **
- 创建 游戏对象
- 启动游戏
plane_sprites
- 封装游戏中 所有 需要使用的 精灵子类
- 提供游戏的 相关工具
代码实现
- 新建
plane_main.py
文件,并且设置为可执行 - 编写 基础代码
import pygame
from plane_sprites import *
class PlaneGame(object):
"""飞机大战主游戏"""
def __init__(self):
print("游戏初始化")
def start_game(self):
print("开始游戏...")
if __name__ == '__main__':
# 创建游戏对象
game = PlaneGame()
# 开始游戏
game.start_game()
游戏初始化部分
- 完成
__init__()
代码如下:
def __init__(self):
print("游戏初始化")
# 1. 创建游戏的窗口
self.screen = pygame.display.set_mode((480, 700))
# 2. 创建游戏的时钟
self.clock = pygame.time.Clock()
# 3. 调用私有方法,精灵和精灵组的创建
self.create_sprites()
def create_sprites(self):
pass
使用常量代替固定的数值
- 常量 —— 不变化的量
- 变量 —— 可以变化的量
应用场景
- 在开发时,可能会需要使用 固定的数值,例如 屏幕的高度 是
700
- 这个时候,建议 不要 直接使用固定数值,而应该使用 常量
常量的定义
- 定义 常量 和 定义 变量 的语法完全一样,都是使用 赋值语句
- 常量 的 命名 应该 所有字母都使用大写,单词与单词之间使用下划线连接
常量的好处
- 阅读代码时,通过 常量名 见名之意,不需要猜测数字的含义
- 如果需要 调整值,只需要 修改常量定义 就可以实现 统一修改
提示:Python 中并没有真正意义的常量,只是通过命名的约定 —— 所有字母都是大写的就是常量,开发时不要轻易的修改!
代码调整
- 在
plane_sprites.py
中增加常量定义
import pygame
# 游戏屏幕大小
SCREEN_RECT = pygame.Rect(0, 0, 480, 700)
- 修改
plane_main.py
中的窗口大小
self.screen = pygame.display.set_mode(SCREEN_RECT.size)
游戏循环部分
- 完成
start_game()
基础代码如下:
def start_game(self):
"""开始游戏"""
print("开始游戏...")
while True:
# 1. 设置刷新帧率
self.clock.tick(60)
# 2. 事件监听
self.event_handler()
# 3. 碰撞检测
self.check_collide()
# 4. 更新精灵组
self.update_sprites()
# 5. 更新屏幕显示
pygame.display.update()
def event_handler(self):
"""事件监听"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
PlaneGame.game_over()
def check_collide(self):
"""碰撞检测"""
pass
def update_sprites(self):
"""更新精灵组"""
pass
@staticmethod
def game_over():
"""游戏结束"""
print("游戏结束")
pygame.quit()
exit()
7.准备游戏精灵组
代码实现
- 创建精灵组方法
def create_sprites(self):
"""创建精灵组"""
# 背景组
self.back_group = pygame.sprite.Group()
# 敌机组
self.enemy_group = pygame.sprite.Group()
# 英雄组
self.hero_group = pygame.sprite.Group()
- 更新精灵组方法
def update_sprites(self):
"""更新精灵组"""
for group in [self.back_group, self.enemy_group, self.hero_group]:
group.update()
group.draw(self.screen)
游戏背景
背景交替滚动的思路确定
解决办法
- 创建两张背景图像精灵
- 第
1
张 完全和屏幕重合 - 第
2
张在 屏幕的正上方
- 第
- 两张图像 一起向下方运动
self.rect.y += self.speed
- 当 任意背景精灵的
rect.y >= 屏幕的高度
说明已经移动到屏幕下方 - 将移动到屏幕下方的这张图像设置到屏幕的正上方
rect.y = -rect.height
设计背景类
初始化方法
- 指定 背景图片
is_alt
判断是否是另一张图像False
表示 第一张图像,需要与屏幕重合True
表示 另一张图像,在屏幕的正上方
update() 方法
- 判断 是否移动出屏幕,如果是,将图像设置到 屏幕的正上方,从而实现 交替滚动
继承 如果父类提供的方法,不能满足子类的需求:
- 派生一个子类
- 在子类中针对特有的需求,重写父类方法,并且进行扩展
8.显示游戏背景
背景精灵的基本实现
- 在
plane_sprites
新建Background
继承自GameSprite
class Background(GameSprite):
"""游戏背景精灵"""
def update(self):
# 1. 调用父类的方法实现
super().update()
# 2. 判断是否移出屏幕,如果移出屏幕,将图像设置到屏幕的上方
if self.rect.y >= SCREEN_RECT.height:
self.rect.y = -self.rect.height
在 plane_main.py
中显示背景精灵
- 在
create_sprites
方法中创建 精灵 和 精灵组 - 在
update_sprites
方法中,让 精灵组 调用update()
和draw()
方法
create_sprites
方法
def create_sprites(self):
# 创建背景精灵和精灵组
bg1 = Background("./images/background.png")
bg2 = Background("./images/background.png")
bg2.rect.y = -bg2.rect.height
self.back_group = pygame.sprite.Group(bg1, bg2)
update_sprites
方法
def update_sprites(self):
self.back_group.update()
self.back_group.draw(self.screen)
利用初始化方法,简化背景精灵创建
思考 —— 上一小结完成的代码存在什么样的问题?能否简化?
- 在主程序中,创建的两个背景精灵,传入了相同的图像文件路径
- 创建 第二个 背景精灵时,在主程序中,设置背景精灵的图像位置
思考 —— 精灵初始位置的设置,应该由主程序负责?还是由精灵自己负责?
答案 —— 由精灵自己负责
- 根据面向对象设计原则,应该将对象的职责,封装到类的代码内部
- 尽量简化程序调用一方的代码调用
初始化方法
- 直接指定背景图片
is_alt
判断是否是另一张图像False
表示 第一张图像,需要与屏幕重合True
表示 另一张图像,在屏幕的正上方
在 plane_sprites.py
中实现 Background
的 初始化方法
def __init__(self, is_alt=False):
image_name = "./images/background.png"
super().__init__(image_name)
# 判断是否交替图片,如果是,将图片设置到屏幕顶部
if is_alt:
self.rect.y = -self.rect.height
修改 plane_main
的 __create_sprites
方法
# 创建背景精灵和精灵组
bg1 = Background()
bg2 = Background(True)
self.back_group = pygame.sprite.Group(bg1, bg2)
敌机出场
11.使用定时器添加敌机
运行备课代码,观察敌机的出现规律:
- 游戏启动后,每隔 1 秒 会 出现一架敌机
- 每架敌机 向屏幕下方飞行,飞行 速度各不相同
- 每架敌机出现的 水平位置 也不尽相同
- 当敌机 从屏幕下方飞出,不会再飞回到屏幕中
定时器
在 pygame
中可以使用 pygame.time.set_timer()
来添加 定时器, 所谓 定时器,就是 每隔一段时间,去 执行一些动作
set_timer(eventid, milliseconds) -> None
set_timer
可以创建一个事件
可以在游戏循环的事件监听方法中捕获到该事件
- 第 1 个参数事件代号需要基于常量
pygame.USEREVENT
来指定USEREVENT
是一个整数,再增加的事件可以使用USEREVENT + 1
指定,依次类推…
- 第 2 个参数是事件触发间隔的毫秒值
定时器事件的监听
- 通过
pygame.event.get()
可以获取当前时刻所有的事件列表 - 遍历列表并且判断
event.type
是否等于eventid
,如果相等,表示定时器事件发生
定义定时器事件
pygame
的 定时器 使用套路非常固定:
- 定义定时器常量——
eventid
- 在初始化方法中,调用
set_timer
方法设置定时器事件 - 在游戏循环中,监听定时器事件
1) 定义事件
- 在
plane_sprites.py
的顶部定义事件常量
# 敌机的定时器事件常量
CREATE_ENEMY_EVENT = pygame.USEREVENT
- 在
PlaneGame
的初始化方法中创建用户事件
# 4. 设置定时器事件 - 每秒创建一架敌机
pygame.time.set_timer(CREATE_ENEMY_EVENT, 1000)
2) 监听定时器事件
- 在
__event_handler
方法中增加以下代码:
def __event_handler(self):
for event in pygame.event.get():
# 判断是否退出游戏
if event.type == pygame.QUIT:
PlaneGame.game_over()
elif event.type == CREATE_ENEMY_EVENT:
print("敌机出场...")
12.设计 Enemy
类
- 游戏启动后,每隔 1 秒会出现一架敌机
- 每架敌机向屏幕下方飞行,飞行速度各不相同
- 每架敌机出现的水平位置也不尽相同
- 当敌机从屏幕下方飞出,不会再飞回到屏幕中
- 初始化方法
- 指定敌机图片
- 随机敌机的初始位置和初始速度
- 重写 update() 方法
- 判断是否飞出屏幕,如果是,从精灵组删除
敌机类的准备
- 在
plane_sprites
新建Enemy
继承自GameSprite
- 重写初始化方法,直接指定图片名称
- 重写
update
方法,判断是否飞出屏幕
class Enemy(GameSprite):
"""敌机精灵"""
def __init__(self):
# 1. 调用父类方法,创建敌机精灵,并且指定敌机的图像
super().__init__("./images/enemy1.png")
# 2. 设置敌机的随机初始速度
# 3. 设置敌机的随机初始位置
def update(self):
# 1. 调用父类方法,让敌机在垂直方向运动
super().update()
# 2. 判断是否飞出屏幕,如果是,需要将敌机从精灵组删除
if self.rect.y >= SCREEN_RECT.height:
print("敌机飞出屏幕...")
创建敌机
演练步骤
- 在
create_sprites
,添加敌机精灵组- 敌机是 定时被创建的,因此在初始化方法中,不需要创建敌机
- 在
event_handler
,创建敌机,并且添加到精灵组- 调用 精灵组 的
add
方法可以向精灵组添加精灵
- 调用 精灵组 的
- 在
update_sprites
,让 敌机精灵组 调用update
和draw
方法
演练代码
- 修改
plane_main
的create_sprites
方法
# 敌机组
self.enemy_group = pygame.sprite.Group()
- 修改
plane_main
的update_sprites
方法
self.enemy_group.update()
self.enemy_group.draw(self.screen)
- 定时出现敌机
elif event.type == CREATE_ENEMY_EVENT:
self.enemy_group.add(Enemy())
随机敌机位置和速度
1) 导入模块
在导入模块时,建议 按照以下顺序导入
1. 官方标准模块导入
2. 第三方模块导入
3. 应用程序模块导入
修改 plane_sprites.py
增加 random
的导入
import random
2) 随机位置
使用 pygame.Rect
提供的 bottom
属性,在指定敌机初始位置时,会比较方便
bottom = y + height
y = bottom - height
3) 代码实现
修改 初始化方法,随机敌机出现 速度 和 位置
def __init__(self):
# 1. 调用父类方法,创建敌机精灵,并且指定敌机的图像
super().__init__("./images/enemy1.png")
# 2. 设置敌机的随机初始速度 1 ~ 3
self.speed = random.randint(1, 3)
# 3. 设置敌机的随机初始位置
self.rect.bottom = 0
max_x = SCREEN_RECT.width - self.rect.width
self.rect.x = random.randint(0, max_x)
移出屏幕销毁敌机
敌机移出屏幕之后,如果 没有撞到英雄,敌机的历史使命已经终结
需要从 敌机组 删除,否则会造成 内存浪费
检测敌机被销毁
__del__
内置方法会在对象被销毁前调用,在开发中,可以用于 判断对象是否被销毁
def __del__(self):
print("敌机挂了 %s" % self.rect)
代码实现
判断敌机是否飞出屏幕,如果是,调用 kill()
方法从所有组中删除
def update(self):
super().update()
# 判断敌机是否移出屏幕
if self.rect.y >= SCREEN_RECT.height:
# 将精灵从所有组中删除
self.kill()
英雄登场
英雄需求
- 游戏启动后,英雄出现在屏幕的水平中间位置,距离屏幕底部
120
像素 - 英雄 每隔
0.5
秒发射一次子弹,每次连发三枚子弹 - 英雄默认不会移动,需要通过
左/右 方向键
,控制英雄在水平方向移动
子弹需求
- 子弹 从 英雄 的正上方发射 沿直线 向 上方 飞行
- 飞出屏幕后,需要从 精灵组 中删除
Hero —— 英雄
初始化方法
- 指定 英雄图片
- 初始速度 = 0 —— 英雄默认静止不动
- 定义
bullets
子弹精灵组 保存子弹精灵
重写 update() 方法
- 英雄需要 水平移动
- 并且需要保证不能 移出屏幕
增加 bullets
属性,记录所有 子弹精灵
增加 fire
方法,用于发射子弹
Bullet —— 子弹
初始化方法
- 指定 子弹图片
- 初始速度 = -2 —— 子弹需要向上方飞行
重写 update() 方法
- 判断 是否飞出屏幕,如果是,从 精灵组 删除
13.创建英雄
准备英雄类
- 在
plane_sprites
新建Hero
类 - 重写 初始化方法,直接指定 图片名称,并且将初始速度设置为
0
- 设置 英雄的初始位置
centerx = x + 0.5 * width
centery = y + 0.5 * height
bottom = y + height
class Hero(GameSprite):
"""英雄精灵"""
def __init__(self):
super().__init__("./images/me1.png", 0)
# 设置初始位置
self.rect.centerx = SCREEN_RECT.centerx
self.rect.bottom = SCREEN_RECT.bottom - 120
绘制英雄
- 在
__create_sprites
,添加 英雄精灵 和 英雄精灵组- 后续要针对 英雄 做 碰撞检测 以及 发射子弹
- 所以 英雄 需要 单独定义成属性
- 在
__update_sprites
,让 英雄精灵组 调用update
和draw
方法
代码实现
- 修改
__create_sprites
方法如下:
# 英雄组
self.hero = Hero()
self.hero_group = pygame.sprite.Group(self.hero)
- 修改
__update_sprites
方法如下:
self.hero_group.update()
self.hero_group.draw(self.screen)
14.移动英雄位置
在
pygame
中针对 键盘按键的捕获,有 两种 方式
- 第一种方式 判断
event.type == pygame.KEYDOWN
- 第二种方式
- 首先使用
pygame.key.get_pressed()
返回 所有按键元组 - 通过 键盘常量,判断元组中 某一个键是否被按下 —— 如果被按下,对应数值为
1
- 首先使用
提问 这两种方式之间有什么区别呢?
- 第一种方式
elif event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT:
print("向右移动...")
- 第二种方式
# 返回所有按键的元组,如果某个键被按下,对应的值会是1
keys_pressed = pygame.key.get_pressed()
# 判断是否按下了方向键
if keys_pressed[pygame.K_RIGHT]:
print("向右移动...")
结论
第一种方式 event.type
用户 必须要抬起按键 才算一次 按键事件,操作灵活性会大打折扣
第二种方式 用户可以按住方向键不放,就能够实现持续向某一个方向移动了,操作灵活性更好
移动英雄位置
演练步骤
- 在
Hero
类中重写update
方法- 用 速度
speed
和 英雄rect.x
进行叠加 - 不需要调用父类方法 —— 父类方法只是实现了单纯的垂直运动
- 用 速度
- 在
__event_handler
方法中根据 左右方向键 设置英雄的 速度- 向右 =>
speed = 2
- 向左 =>
speed = -2
- 其他 =>
speed = 0
- 向右 =>
代码演练
- 在
Hero
类,重写update()
方法,根据速度水平移动 英雄的飞机
def update(self):
# 飞机水平移动
self.rect.x += self.speed
- 调整键盘按键代码
# 获取用户按键
keys_pressed = pygame.key.get_pressed()
if keys_pressed[pygame.K_RIGHT]:
self.hero.speed = 2
elif keys_pressed[pygame.K_LEFT]:
self.hero.speed = -2
else:
self.hero.speed = 0
控制英雄运动边界
-
在
Hero
类的update()
方法判断 英雄 是否超出 屏幕边界 -
right = x + width
利用right
属性可以非常容易的针对右侧设置精灵位置
def update(self):
# 飞机水平移动
self.rect.x += self.speed
# 判断屏幕边界
if self.rect.left < 0:
self.rect.left = 0
if self.rect.right > SCREEN_RECT.right:
self.rect.right = SCREEN_RECT.right
15.发射子弹
需求回顾 —— 英雄需求
- 游戏启动后,英雄 出现在屏幕的 水平中间 位置,距离 屏幕底部
120
像素 - 英雄 每隔
0.5
秒发射一次子弹,每次 连发三枚子弹 - 英雄 默认不会移动,需要通过 左/右 方向键,控制 英雄 在水平方向移动
添加发射子弹事件
pygame
的 定时器 使用套路非常固定:
- 定义 定时器常量 ——
eventid
- 在 初始化方法 中,调用
set_timer
方法 设置定时器事件 - 在 游戏循环 中,监听定时器事件
代码实现
- 在
Hero
中定义fire
方法
def fire(self):
print("发射子弹...")
- 在
plane_main.py
的顶部定义 发射子弹 事件常量
# 英雄发射子弹事件
HERO_FIRE_EVENT = pygame.USEREVENT + 1
- 在
__init__
方法末尾中添加 发射子弹 事件
# 每隔 0.5 秒发射一次子弹
pygame.time.set_timer(HERO_FIRE_EVENT, 500)
- 在
__event_handler
方法中让英雄发射子弹
elif event.type == HERO_FIRE_EVENT:
self.hero.fire()
定义子弹类
需求回顾 —— 子弹需求
- 子弹 从 英雄 的正上方发射 沿直线 向 上方 飞行
- 飞出屏幕后,需要从 精灵组 中删除
Bullet —— 子弹
- 初始化方法
- 指定 子弹图片
- 初始速度 = -2 —— 子弹需要向上方飞行
- 重写 update() 方法
- 判断 是否飞出屏幕,如果是,从 精灵组 删除
定义子弹类
- 在
plane_sprites
新建Bullet
继承自GameSprite
- 重写 初始化方法,直接指定 图片名称,并且设置 初始速度
- 重写
update()
方法,判断子弹 飞出屏幕从精灵组删除
class Bullet(GameSprite):
"""子弹精灵"""
def __init__(self):
super().__init__("./images/bullet1.png", -2)
def update(self):
super().update()
# 判断是否超出屏幕,如果是,从精灵组删除
if self.rect.bottom < 0:
self.kill()
发射子弹
演练步骤
- 在
Hero
的 初始化方法 中创建 子弹精灵组 属性 - 修改
plane_main.py
的__update_sprites
方法,让 子弹精灵组 调用update
和draw
方法 - 实现
fire()
方法- 创建子弹精灵
- 设置初始位置 —— 在 英雄的正上方
- 将 子弹 添加到精灵组
代码实现
- 初始化方法
# 创建子弹的精灵组
self.bullets = pygame.sprite.Group()
- 修改
fire()
方法
def fire(self):
# 1. 创建子弹精灵
bullet = Bullet()
# 2. 设置精灵的位置
bullet.rect.bottom = self.rect.y - 20
bullet.rect.centerx = self.rect.centerx
# 3. 将精灵添加到精灵组
self.bullets.add(bullet)
一次发射三枚子弹
- 修改
fire()
方法,一次发射三枚子弹
def fire(self):
for i in (1, 2, 3):
# 1. 创建子弹精灵
bullet = Bullet()
# 2. 设置精灵的位置
bullet.rect.bottom = self.rect.y - i * 20
bullet.rect.centerx = self.rect.centerx
# 3. 将精灵添加到精灵组
self.bullets.add(bullet)
碰撞检测
pygame
提供了 两个非常方便 的方法可以实现碰撞检测:
pygame.sprite.groupcollide()
两个精灵组 中 所有的精灵 的碰撞检测
groupcollide(group1, group2, dokill1, dokill2, collided = None) -> Sprite_dict
如果将 dokill
设置为 True
,则 发生碰撞的精灵将被自动移除
collided
参数是用于 计算碰撞的回调函数
- 如果没有指定,则每个精灵必须有一个
rect
属性
pygame.sprite.spritecollide()
- 判断 某个精灵 和 指定精灵组 中的精灵的碰撞
spritecollide(sprite, group, dokill, collided = None) -> Sprite_list
- 如果将
dokill
设置为True
,则 指定精灵组 中 发生碰撞的精灵将被自动移除 collided
参数是用于 计算碰撞的回调函数- 如果没有指定,则每个精灵必须有一个
rect
属性
- 如果没有指定,则每个精灵必须有一个
- 返回 精灵组 中跟 精灵 发生碰撞的 精灵列表
16.碰撞实现
def check_collide(self):
# 1. 子弹摧毁敌机
pygame.sprite.groupcollide(self.hero.bullets, self.enemy_group, True, True)
# 2. 敌机撞毁英雄
enemies = pygame.sprite.spritecollide(self.hero, self.enemy_group, True)
# 判断列表时候有内容
if len(enemies) > 0:
# 让英雄牺牲
self.hero.kill()
# 结束游戏
PlaneGame.__game_over()
三、源码福利
所有源码均在此公众号内:关注此公众号,回复 飞机大战
、超级玛丽
、植物大战僵尸
获取源码,快点击我吧
最后,有任何问题,欢迎关注下面的公众号,获取第一时间消息、作者联系方式及每周抽奖等多重好礼! ↓↓↓
- 点赞
- 收藏
- 关注作者
评论(0)