还不懂飞机大战?最细学习笔记--全方位学习Pygame(附源码)
Posted 是Dream呀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了还不懂飞机大战?最细学习笔记--全方位学习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[知了堂学习笔记]_纯JS制作《飞机大战》游戏_第1讲(实现思路与游戏界面的实现)